webui-new/development/simpleadmin/www/sms.html

419 lines
17 KiB
HTML
Raw Normal View History

2025-03-24 22:39:52 +08:00
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>模块管理</title>
<!-- 导入自定义和Bootstrap样式 -->
<link rel="stylesheet" href="css/styles.css" />
<link rel="stylesheet" href="css/bootstrap.min.css" />
<!-- 网站图标 -->
<link rel="icon" href="favicon.ico" />
<!-- 导入Bootstrap和Alpine.js脚本 -->
<script src="js/bootstrap.bundle.min.js"></script>
<script src="js/alpinejs.min.js" defer></script>
<!-- Contributed By: snjzb -->
</head>
<body>
<main>
<div class="container my-4" x-data="fetchSMS()">
<nav class="navbar navbar-expand-lg mt-2">
<div class="container-fluid">
<a class="navbar-brand" href="/">
<span class="mb-0 h4 fw-bold">模块管理</span></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarText"
aria-controls="navbarText" aria-expanded="false" aria-label="切换导航">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav me-auto mb-2 ml-4 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="/">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/network.html">网络</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/scanner.html">扫描</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/settings.html">设置</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/sms.html">短信</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/console">控制台</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/deviceinfo.html">设备信息</a>
</li>
</ul>
<span class="navbar-text">
<button class="btn btn-link text-reset" id="darkModeToggle">
暗黑模式
</button>
</span>
</div>
</div>
</nav>
<div class="row mt-5 mb-4">
<div class="col">
<div class="card">
<div class="card-header">短信收件箱</div>
<div class="card-body">
<div class="card-text">
<div class="col">
<div style="
max-height: 400px;
overflow-y: scroll;
overflow-x: hidden;
">
<div x-show="isLoading">
<h4>正在获取短信...</h4>
</div>
<table class="table table-hover border-success" x-show="!isLoading">
<tbody>
<!-- 没有消息时显示 -->
<!-- Display when there are no messages -->
<template x-if="messages.length === 0 && !isLoading">
<div>
<p>收件箱为空</p>
</div>
</template>
<!-- 循环显示短信消息 -->
<!-- "Loop display SMS messages" -->
<template x-for="(message, index) in messages" :key="index">
<tr>
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" :value="index"
x-model="selectedMessages" />
<div class="row column-gap-1 mb-2">
<div class="col-md-3">
<p x-text="'发件人: ' + senders[index]"></p>
</div>
<div class="col">
<p x-text="'日期时间: ' + dates[index]"></p>
</div>
</div>
<div class="col-md-9">
<p x-text="message.text"></p>
</div>
</div>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 添加判断只有当messages数组有内容时才显示全选复选框及其区域 -->
<!-- Add a judgment, only when the messages array has content will the select all checkbox and its area be displayed" -->
<div class="card-body border-top" x-show="messages.length > 0">
<div class="form-check">
<input id="selectAllCheckbox" class="form-check-input" type="checkbox" @change="toggleAll($event)" />
<label class="form-check-label">全选</label>
</div>
</div>
<div class="card-footer">
<div class="d-grid gap-2 d-md-flex justify-content-md-start">
<!-- 刷新按钮 -->
<!-- Refresh button -->
<button class="btn btn-success" type="button" @click="init()">
刷新
</button>
<!-- 删除选中短信按钮 -->
<!-- Delete selected SMS button -->
<button class="btn btn-danger" type="button" @click="deleteSelectedSMS()">
删除选中的短信
</button>
</div>
</div>
</div>
</div>
</div>
<div class="row mt-5 mb-4">
<div class="col">
<div class="card">
<div class="card-header">发送短信</div>
<div class="card-body">
<div class="mb-3">
<label for="phoneNumber" class="form-label">收件人</label>
<input type="text" class="form-control" id="phoneNumber" x-model="phoneNumber"
placeholder="输入收件人号码" />
</div>
<div class="mb-3">
<label for="messageToSend" class="form-label">短信内容</label>
<textarea class="form-control" id="messageToSend" rows="3" x-model="messageToSend"
placeholder="输入短信内容"></textarea>
</div>
<div id="notification" class="alert" style="display: none;"></div>
<button class="btn btn-primary" @click="sendSMS()">
发送短信
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<script src="js/dark-mode.js"></script>
<script>
function fetchSMS() {
return {
isLoading: false,
atCommandResponse: "",
messages: [],
senders: [],
dates: [],
selectedMessages: [],
phoneNumber: '',
messageToSend: '',
messageIndices: [], // 确保初始化messageIndices数组
// 清除现有数据
clearData() {
this.messages = [];
this.senders = [];
this.dates = [];
this.selectedMessages = [];
this.messageIndices = [];
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
},
// 请求获取短信
requestSMS() {
this.isLoading = true;
fetch(`/cgi-bin/get_atcommand?${new URLSearchParams({ atcmd: "AT+CSMS=1;+CSDH=0;+CNMI=2,1,0,0,0;+CMGF=1;+CSCA?;+CSMP=17,167,0,8;+CPMS=\"ME\",\"ME\",\"ME\";+CSCS=\"UCS2\";+CMGL=\"ALL\"" })}`)
.then(response => response.text())
.then(data => {
this.atCommandResponse = data.split('\n')
.filter(line => line.trim() !== "OK" && line.trim() !== "")
.join('\n');
})
.finally(() => {
this.isLoading = false;
this.clearData();
this.parseSMSData(this.atCommandResponse);
});
},
// 解析短信数据的方法
parseSMSData(data) {
const cmglRegex = /^\s*\+CMGL:\s*(\d+),"[^"]*","([^"]*)"[^"]*,"([^"]*)"/gm;
const cscaRegex = /^\s*\+CSCA:\s*"([^"]*)"/gm;
this.messageIndices = [];
this.serviceCenters = [];
this.dates = [];
this.senders = [];
this.messages = [];
let match;
let lastIndex = null;
while ((match = cmglRegex.exec(data)) !== null) {
const index = parseInt(match[1]);
const senderHex = match[2];
const sender = senderHex.startsWith("003") ? this.convertHexToText(senderHex) : senderHex;
const dateStr = match[3].replace(/\+\d{2}$/, "");
const date = this.parseCustomDate(dateStr);
if (isNaN(date)) {
console.error(`Invalid Date: ${dateStr}`);
continue;
}
const startIndex = cmglRegex.lastIndex;
const endIndex = data.indexOf("+CMGL:", startIndex) !== -1 ? data.indexOf("+CMGL:", startIndex) : data.length;
const messageHex = data.substring(startIndex, endIndex).trim();
const message = /^[0-9a-fA-F]+$/.test(messageHex) ? this.convertHexToText(messageHex) : messageHex;
if (lastIndex !== null && this.messages[lastIndex].sender === sender && (date - this.messages[lastIndex].date) / 1000 <= 1) {
this.messages[lastIndex].text += " " + message;
this.messages[lastIndex].indices.push(index);
this.dates[lastIndex] = this.formatDate(date);
} else {
this.messageIndices.push([index]);
this.senders.push(sender);
this.dates.push(this.formatDate(date));
this.messages.push({ text: message, sender: sender, date: date, indices: [index] });
lastIndex = this.messages.length - 1;
}
}
while ((match = cscaRegex.exec(data)) !== null) {
const serviceCenterHex = match[1];
const serviceCenter = this.convertHexToText(serviceCenterHex);
this.serviceCenters.push(serviceCenter);
}
},
// 将十六进制转换为文本(假设使用 UTF-16BE 编码)
convertHexToText(hex) {
const bytes = new Uint8Array(hex.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
return new TextDecoder('utf-16be').decode(bytes);
},
// 自定义解析日期函数
parseCustomDate(dateStr) {
const [datePart, timePart] = dateStr.split(',');
const [day, month, year] = datePart.split('/').map(part => parseInt(part, 10));
const [hour, minute, second] = timePart.split(':').map(part => parseInt(part, 10));
// 将日期转换为标准格式的日期对象
return new Date(Date.UTC(2000 + year, month - 1, day, hour, minute, second));
},
// 自定义格式化日期函数
formatDate(date) {
const year = date.getUTCFullYear() - 2000;
const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
const day = date.getUTCDate().toString().padStart(2, '0');
const hour = date.getUTCHours().toString().padStart(2, '0');
const minute = date.getUTCMinutes().toString().padStart(2, '0');
const second = date.getUTCSeconds().toString().padStart(2, '0');
return `${day}/${month}/${year},${hour}:${minute}:${second}`;
},
// 删除选中的短信
deleteSelectedSMS() {
if (this.selectedMessages.length === 0) {
console.warn("没有选中的短信");
return;
}
if (!this.messageIndices || this.messageIndices.length === 0) {
console.error("短信索引未正确初始化或为空");
return;
}
// 检查是否全选
const isAllSelected = this.selectedMessages.length === this.messages.length;
if (isAllSelected) {
// 如果全选,则调用删除所有短信的方法
this.deleteAllSMS();
} else {
// 否则,删除选中的短信
const indicesToDelete = [];
this.selectedMessages.forEach(index => {
indicesToDelete.push(...this.messages[index].indices);
});
if (indicesToDelete.length === 0) {
console.warn("没有有效的短信索引");
return;
}
// 拼接 AT 命令
const atCommands = indicesToDelete.map((index, i) => i === 0 ? `AT+CMGD=${index}` : `+CMGD=${index}`).join(';');
fetch(`/cgi-bin/get_atcommand?${new URLSearchParams({ atcmd: atCommands })}`)
.finally(() => {
this.selectedMessages = [];
this.requestSMS();
});
}
},
// 删除所有短信
deleteAllSMS() {
fetch(`/cgi-bin/get_atcommand?${new URLSearchParams({ atcmd: "AT+CMGD=,4" })}`)
.finally(() => {
this.init();
});
},
// 发送短信
encodeUCS2(input) {
let output = '';
for (let i = 0; i < input.length; i++) {
const hex = input.charCodeAt(i).toString(16).toUpperCase().padStart(4, '0');
output += hex;
}
return output;
},
async sendSMS() {
let phoneNumberWithCountryCode;
if (this.phoneNumber.length < 11) {
phoneNumberWithCountryCode = this.phoneNumber;
} else {
const serviceCenterPrefix = this.serviceCenters[0].substring(0, 3);
phoneNumberWithCountryCode = `${serviceCenterPrefix}${this.phoneNumber}`;
}
const encodedPhoneNumber = this.encodeUCS2(phoneNumberWithCountryCode);
const messageSegments = this.splitMessage(this.messageToSend, 70); // 将消息分段
const uid = Math.floor(Math.random() * 256); // 生成随机的UID
const totalSegments = messageSegments.length;
let allSegmentsSent = true;
let errorCode = null;
for (let i = 0; i < totalSegments; i++) {
const segment = messageSegments[i];
const encodedMessage = this.encodeUCS2(segment);
const currentSegment = i + 1;
const Command = `${uid},${currentSegment},${totalSegments}`;
const params = new URLSearchParams({
number: encodedPhoneNumber,
msg: encodedMessage,
Command: Command
});
try {
const response = await fetch(`/cgi-bin/send_sms?${params.toString()}`);
const data = await response.text();
console.log("Response from server:", data);
// 检查返回的数据中是否包含 '+CMS ERROR'
if (data.includes('+CMS ERROR')) {
errorCode = data.match(/\+CMS ERROR: (\d+)/)?.[1];
console.error("SMS send error:", data);
allSegmentsSent = false;
break; // 停止发送剩余的段
}
} catch (error) {
console.error("Fetch error:", error);
allSegmentsSent = false;
break; // 停止发送剩余的段
}
}
if (allSegmentsSent) {
this.showNotification("SMS sent successfully!");
} else {
this.showNotification(`SMS sending failed!: ${errorCode}`);
}
},
splitMessage(message, length) {
const segments = [];
for (let i = 0; i < message.length; i += length) {
segments.push(message.substring(i, i + length));
}
return segments;
},
showNotification(message, type) {
const notification = document.getElementById('notification');
notification.innerText = message;
notification.className = `alert alert-${type}`;
notification.style.display = 'block';
setTimeout(() => {
notification.style.display = 'none';
}, 3000); // 3秒后自动关闭
},
// 初始化
// Initialize
init() {
this.clearData();
this.requestSMS();
},
// 全选/取消全选
// Select all/deselect all
toggleAll(event) {
this.selectedMessages = event.target.checked ? this.messages.map((_, index) => index) : [];
}
};
}
</script>
</body>
</html>