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