webui-new/development/simpleadmin/www/sms.html
2025-03-24 22:39:52 +08:00

419 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>