419 lines
17 KiB
HTML
419 lines
17 KiB
HTML
|
|
<!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>
|