Skip to content

Instantly share code, notes, and snippets.

@liuran001
Last active August 8, 2025 08:05
Show Gist options
  • Save liuran001/59a3ee3c6270926073e61c9a588dcc3a to your computer and use it in GitHub Desktop.
Save liuran001/59a3ee3c6270926073e61c9a588dcc3a to your computer and use it in GitHub Desktop.
SmsForwarder Web
<!DOCTYPE html>
<html lang="zh-CN">
<!-- 转载请注明来源,谢谢 -->
<!-- SmsForwarder 下载地址:https://github.com/pppscn/SmsForwarder -->
<!-- 关注 Telegram @BDovo_Channel 谢谢喵 -->
<!-- 食用方法:在 Download 下新建一个目录,然后将这个 html 下载并放进去(文件名应当为 index.html) ––>
<!-- 打开 短信转发器,将 主动控制·服务端 中的 Web客户端 设置为刚才创建的目录 -->
<!-- 目前仅支持 校验签名 加密,强烈建议打开以保证安全 -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>短信转发器Web客户端</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js" async></script>
<style>
:root {
--primary: #4361ee;
--primary-dark: #3a56d4;
--secondary: #7209b7;
--success: #2ec4b6;
--warning: #ff9f1c;
--danger: #e71d36;
--light: #f8f9fa;
--dark: #212529;
--gray: #6c757d;
--light-gray: #f1f3f5;
--border-radius: 12px;
--shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
--transition: all 0.3s ease;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #f0f5ff 0%, #e8f4ff 100%);
color: #333;
line-height: 1.5;
padding: 10px;
min-height: 100vh;
}
.app-container {
width: 100%;
max-width: 1220px;
margin: 0 auto;
padding: 20px 0;
}
.main-content {
display: grid;
grid-template-columns: 500px 1fr;
grid-template-rows: auto;
gap: 20px;
align-items: start;
}
/* 大屏设备:左侧设置+历史,右侧响应结果 */
.left-column {
grid-area: 1 / 1 / 2 / 2;
display: flex;
flex-direction: column;
gap: 20px;
min-width: 0; /* 防止内容撑开 */
width: 500px; /* 固定宽度 */
}
.response-card {
grid-area: 1 / 2 / 2 / 3;
min-width: 0; /* 防止内容撑开 */
width: 100%; /* 自适应宽度 */
}
@media (max-width: 1200px) {
.app-container {
max-width: 700px;
padding: 10px 0;
}
.main-content {
grid-template-columns: 1fr;
grid-template-rows: auto auto auto;
gap: 15px;
}
.left-column {
grid-area: 1 / 1 / 2 / 2;
display: contents; /* 让子元素直接参与网格布局 */
width: 100%;
}
.settings-card {
grid-area: 1 / 1 / 2 / 2;
order: 1;
margin-bottom: 0; /* 移除底部边距,因为使用gap控制间距 */
}
.response-card {
grid-area: 2 / 1 / 3 / 2;
order: 2;
margin-bottom: 0;
}
.history-card {
grid-area: 3 / 1 / 4 / 2;
order: 3;
margin-bottom: 0;
}
}
/* 主卡片样式 */
.card {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 18px;
margin-bottom: 15px;
transition: var(--transition);
}
.card:hover {
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12);
}
.card-header {
padding-bottom: 12px;
margin-bottom: 15px;
border-bottom: 2px solid var(--primary);
display: flex;
align-items: center;
gap: 10px;
}
.card-header i {
font-size: 20px;
color: var(--primary);
flex-shrink: 0;
}
.card-header h2 {
font-size: 20px;
color: var(--primary);
margin: 0;
}
.card-header p {
color: var(--gray);
font-size: 14px;
margin: 0;
}
/* 表单样式 */
.form-group {
margin-bottom: 15px;
position: relative;
}
.form-label {
display: block;
font-weight: 600;
color: #444;
margin-bottom: 6px;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
}
.form-label i {
color: var(--primary);
font-size: 16px;
flex-shrink: 0;
}
.form-control {
width: 100%;
padding: 12px;
border-radius: 8px;
border: 1px solid #dee2e6;
font-size: 15px;
transition: var(--transition);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
}
.form-control:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15);
}
textarea.form-control {
resize: vertical;
min-height: 100px;
}
.password-toggle {
position: absolute;
right: 12px;
top: 40px;
background: none;
border: none;
color: #adb5bd;
cursor: pointer;
font-size: 16px;
transition: var(--transition);
}
.password-toggle:hover {
color: var(--primary);
}
.radio-group {
display: flex;
gap: 8px;
margin-top: 6px;
flex-wrap: nowrap;
}
.radio-item {
flex: 1;
min-width: 0;
}
.radio-item input {
display: none;
}
.radio-item label {
display: block;
padding: 8px 6px;
border: 1px solid #dee2e6;
border-radius: 6px;
text-align: center;
cursor: pointer;
transition: var(--transition);
font-weight: 500;
color: #555;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.radio-item input:checked + label {
background: var(--primary);
color: white;
border-color: var(--primary);
}
/* 按钮样式 */
.btn {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
width: 100%;
padding: 12px;
border-radius: 8px;
border: none;
font-size: 15px;
font-weight: 600;
cursor: pointer;
background: var(--primary);
color: white;
transition: var(--transition);
}
.btn:hover:not(:disabled) {
background: var(--primary-dark);
}
.btn:disabled {
background: #a0b0f8;
cursor: not-allowed;
opacity: 0.7;
}
.btn-action {
background: var(--secondary);
}
.btn-action:hover:not(:disabled) {
background: #5e07a1;
}
/* 折叠区域样式 */
.collapse-section {
margin-top: 15px;
border-top: 1px solid #e9ecef;
padding-top: 15px;
position: relative;
}
.collapse-toggle {
background: none;
border: none;
color: var(--gray);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
transition: var(--transition);
width: 100%;
text-align: left;
}
.collapse-toggle:hover {
color: var(--primary);
}
.collapse-toggle i:first-child {
color: var(--primary);
}
.collapse-content {
max-height: 0;
overflow: hidden;
transition: all 0.3s ease;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
padding: 0;
opacity: 0;
transform: translateY(-10px);
}
.collapse-content.expanded {
max-height: 300px;
padding: 15px;
opacity: 1;
transform: translateY(0);
}
.form-group-inline {
display: flex;
flex-direction: column;
width: 100%;
}
.inline-fields-row {
display: flex;
gap: 15px;
width: 100%;
}
.inline-field-column {
flex: 1;
display: flex;
flex-direction: column;
}
.inline-field-column .form-label {
margin-bottom: 6px;
}
.inline-field-column .form-control {
width: 100%;
}
@media (max-width: 600px) {
.inline-fields-row {
flex-direction: column;
gap: 15px;
}
.inline-field-column {
flex: none;
}
}
/* 响应结果区域 */
.response-container {
margin-top: 15px;
background: #f8f9ff;
border-radius: 10px;
overflow: hidden;
border: 1px solid #e9ecef;
}
.response-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
background: linear-gradient(90deg, var(--primary), #5770f3);
color: white;
}
.response-title {
font-weight: 600;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
line-height: 1;
}
.response-title i {
font-size: 16px;
flex-shrink: 0;
}
.copy-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 5px;
line-height: 1;
}
.copy-btn i {
font-size: 14px;
flex-shrink: 0;
}
.copy-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.response-content {
padding: 12px;
max-height: 980px;
overflow-y: auto;
background: white;
}
#response {
min-height: auto;
}
/* 响应结果样式 */
.response-container {
display: flex;
flex-direction: column;
}
.response-content {
flex: 1;
}
/* 短信/通话记录样式 - 减小间距 */
.record-list {
display: flex;
flex-direction: column;
gap: 8px; /* 减小条目间距 */
}
.record-item {
background: white;
border-radius: 8px;
padding: 10px; /* 减小内边距 */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border-left: 4px solid var(--primary);
transition: var(--transition);
}
.record-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.record-header {
display: flex;
justify-content: space-between;
margin-bottom: 6px; /* 减小间距 */
}
.record-header-info {
display: flex;
flex-direction: column;
gap: 1px; /* 减小间距 */
}
.record-name {
font-weight: 700;
font-size: 15px;
color: var(--dark);
margin: 0;
}
.record-number {
color: #555;
font-size: 13px;
margin: 0;
}
.record-time {
color: #777;
font-size: 13px;
white-space: nowrap;
margin: 0;
display: flex;
align-items: center;
gap: 4px;
}
/* 短信内容容器 */
.record-content {
margin: 6px 0; /* 减小间距 */
padding: 10px; /* 减小内边距 */
background: #f0f5ff;
border-radius: 8px;
font-size: 14px;
line-height: 1.4;
word-wrap: break-word;
overflow-wrap: break-word;
white-space: pre-wrap;
word-break: break-word;
hyphens: auto;
}
.record-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
color: #666;
margin-top: 3px; /* 减小间距 */
}
.record-sim {
display: flex;
align-items: center;
gap: 5px;
background: #e9ecef;
padding: 3px 8px;
border-radius: 16px;
font-size: 12px;
}
.record-actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 5px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: var(--transition);
border: none;
display: flex;
align-items: center;
gap: 4px;
}
.reply-btn {
background: #2ec4b6;
color: white;
}
.reply-btn:hover {
background: #25a99d;
}
.call-btn {
background: var(--primary);
color: white;
}
.call-btn:hover {
background: var(--primary-dark);
}
/* JSON高亮样式 - 添加横向滚动 */
.json-container {
max-width: 100%;
overflow-x: auto;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
margin-top: 10px;
}
.json-key {
color: #881391;
}
.json-string {
color: #c41a16;
}
.json-number {
color: #1c00cf;
}
.json-boolean {
color: #0d22aa;
}
.json-null {
color: #707070;
}
.json-punctuation {
color: #333;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 20px;
color: #777;
font-size: 13px;
background: white;
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.empty-state i {
font-size: 36px;
margin-bottom: 10px;
color: #dee2e6;
display: block;
}
.empty-state p {
margin: 0;
text-align: center;
}
/* 状态提示 */
.status-info {
padding: 10px 12px;
border-radius: 8px;
background: rgba(67, 97, 238, 0.08);
color: var(--primary);
font-size: 13px;
margin: 8px 0 15px;
display: flex;
align-items: center;
gap: 8px;
border-left: 3px solid var(--primary);
}
.status-info i {
font-size: 16px;
flex-shrink: 0;
}
/* 历史面板 */
.history-panel {
max-height: 400px;
overflow-y: auto;
}
.history-item {
padding: 10px 12px;
border-left: 3px solid #dee2e6;
margin-bottom: 8px;
background: #f9fafb;
border-radius: 8px;
transition: var(--transition);
cursor: pointer;
}
.history-item:hover {
border-left-color: var(--primary);
background: white;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
.history-item-title {
font-weight: 600;
margin-bottom: 4px;
color: var(--dark);
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
}
.history-item-title i {
font-size: 14px;
flex-shrink: 0;
}
.history-item-time {
font-size: 11px;
color: var(--gray);
}
/* 历史面板头部 */
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.history-title {
display: flex;
align-items: center;
gap: 8px;
}
.clear-history-btn {
background: #e9ecef;
border: none;
border-radius: 6px;
padding: 5px 10px;
font-size: 12px;
cursor: pointer;
transition: var(--transition);
display: flex;
align-items: center;
gap: 5px;
}
.clear-history-btn i {
font-size: 12px;
flex-shrink: 0;
}
.clear-history-btn:hover {
background: #dee2e6;
}
/* 加载动画 */
.loader {
display: flex;
justify-content: center;
padding: 30px;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(67, 97, 238, 0.1);
border-radius: 50%;
border-top: 3px solid var(--primary);
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
body {
padding: 8px;
}
.card {
padding: 15px;
}
.radio-group {
gap: 4px;
flex-wrap: nowrap;
}
.radio-item {
flex: 1;
min-width: 0;
}
.radio-item label {
padding: 6px 3px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.inline-fields-row {
gap: 8px;
}
.inline-field-column .form-label {
font-size: 12px;
}
.inline-field-column .form-control {
padding: 8px;
font-size: 13px;
}
.record-header {
flex-direction: column;
gap: 8px;
}
.record-time {
align-self: flex-start;
}
}
@media (max-width: 480px) {
.card {
padding: 12px;
}
.form-control {
padding: 10px;
}
.btn {
padding: 10px;
font-size: 14px;
}
.radio-group {
gap: 3px;
flex-wrap: nowrap;
}
.radio-item label {
padding: 5px 2px;
font-size: 10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.inline-fields-row {
gap: 6px;
}
.inline-field-column .form-label {
font-size: 11px;
}
.inline-field-column .form-control {
padding: 6px;
font-size: 12px;
}
}
@media (max-width: 360px) {
.radio-group {
flex-direction: column;
gap: 4px;
}
.radio-item label {
padding: 8px 6px;
font-size: 13px;
}
.inline-fields-row {
flex-direction: column;
gap: 15px;
}
.inline-field-column {
flex: none;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="main-content">
<!-- 左侧列:设置 + 历史记录 -->
<div class="left-column">
<!-- 设置区域 -->
<div class="card settings-card">
<div class="card-header">
<i class="fas fa-sync-alt"></i>
<div>
<h2>短信转发器Web客户端</h2>
<p>安全、高效地管理您的短信和通话记录</p>
</div>
</div>
<div class="status-info">
<i class="fas fa-shield-alt"></i>
<div>通信安全: <strong id="signingMethod">原生Web Crypto API</strong> <span id="encryptionStatus">(已启用加密)</span></div>
</div>
<form id="apiForm">
<div class="form-group">
<label for="endpoint" class="form-label">
<i class="fas fa-list"></i>功能列表
</label>
<select id="endpoint" class="form-control" name="endpoint" required>
<option value="/config/query">远程查配置</option>
<option value="/clone/pull">客户端从服务端拉取配置</option>
<option value="/clone/push">客户端向服务端推送配置</option>
<option value="/sms/send">远程发短信</option>
<option value="/sms/query">远程查短信</option>
<option value="/call/query">远程查通话</option>
<option value="/battery/query">远程查电量</option>
<option value="/wol/send">远程WOL</option>
<option value="/location/query">远程查定位</option>
<option value="/contact/add">远程加话簿</option>
<option value="/contact/query">远程查话簿</option>
</select>
</div>
<div id="dynamicFields"></div>
<button type="button" id="sendButton" class="btn btn-action">
<i class="fas fa-paper-plane"></i>发送请求
</button>
<!-- 折叠的连接设置 -->
<div class="collapse-section">
<button type="button" class="collapse-toggle" id="collapseToggle">
<i class="fas fa-cog"></i>连接设置
<i class="fas fa-chevron-down" id="collapseIcon"></i>
</button>
<div class="collapse-content" id="collapseContent">
<div class="form-group">
<label for="url" class="form-label">
<i class="fas fa-server"></i>服务地址
</label>
<input type="text" id="url" class="form-control" name="url" placeholder="http://your-server-address:port">
</div>
<div class="form-group">
<label for="key" class="form-label">
<i class="fas fa-key"></i>签名密钥
</label>
<input type="password" id="key" class="form-control" name="key" placeholder="留空则跳过加密">
<button type="button" class="password-toggle" id="toggleKey">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
</div>
</form>
</div>
<!-- 历史记录 -->
<div class="card history-card">
<div class="history-header">
<div class="history-title">
<i class="fas fa-history"></i>
<h2>历史记录</h2>
</div>
<button class="clear-history-btn" id="clearHistoryBtn">
<i class="fas fa-trash-alt"></i>清空
</button>
</div>
<div class="history-panel" id="historyPanel">
<div class="empty-state">
<i class="fas fa-clock"></i>
<p>暂无历史记录</p>
</div>
</div>
</div>
</div>
<!-- 右侧:响应结果 -->
<div class="card response-card">
<div class="card-header">
<i class="fas fa-code"></i>
<h2>响应结果</h2>
</div>
<div class="response-container">
<div class="response-header">
<div class="response-title">
<i class="fas fa-terminal"></i>服务器响应
</div>
<button class="copy-btn" id="copyButton">
<i class="fas fa-copy"></i>复制结果
</button>
</div>
<div class="response-content">
<div id="response">
<div class="empty-state">
<i class="fas fa-inbox"></i>
<p>等待您发送请求...</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// DOM元素引用
const dynamicFieldsEl = document.getElementById('dynamicFields');
const urlInput = document.getElementById('url');
const keyInput = document.getElementById('key');
const toggleKeyBtn = document.getElementById('toggleKey');
const endpointSelect = document.getElementById('endpoint');
const sendButton = document.getElementById('sendButton');
const responseEl = document.getElementById('response');
const methodEl = document.getElementById('signingMethod');
const copyBtn = document.getElementById('copyButton');
const historyPanel = document.getElementById('historyPanel');
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
const collapseToggle = document.getElementById('collapseToggle');
const collapseContent = document.getElementById('collapseContent');
const collapseIcon = document.getElementById('collapseIcon');
// 历史记录最大数量
const MAX_HISTORY_ITEMS = 15;
// Cookie管理函数
function setCookie(name, value, days) {
const d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${encodeURIComponent(value)};expires=${d.toUTCString()};path=/`;
}
function getCookie(name) {
const cookies = `; ${document.cookie}`;
const parts = cookies.split(`; ${name}=`);
return parts.length === 2 ? decodeURIComponent(parts.pop().split(';').shift()) : null;
}
// 初始化表单
function initForm() {
// 设置默认服务地址为当前页面的origin
urlInput.value = window.location.origin;
// 尝试从cookie获取保存的密钥
const savedKey = getCookie('sms_forwarder_key');
if (savedKey) {
keyInput.value = savedKey;
}
// 检查是否有成功的历史记录
const history = JSON.parse(localStorage.getItem('apiHistory') || '[]');
const hasSuccessfulHistory = history.length > 0;
// 如果没有成功的历史记录,自动展开连接设置
if (!hasSuccessfulHistory) {
toggleCollapse(true);
}
// 设置事件监听器
setupEventListeners();
// 初始化动态表单字段
setupDynamicFields();
// 更新加密状态显示
updateEncryptionStatus();
// 加载历史记录
loadHistory();
}
// 设置事件监听器
function setupEventListeners() {
// 端点选择变化
endpointSelect.addEventListener('change', setupDynamicFields);
// 发送按钮点击
sendButton.addEventListener('click', sendRequest);
// 复制按钮点击
copyBtn.addEventListener('click', copyToClipboard);
// 密钥显示/隐藏切换
toggleKeyBtn.addEventListener('click', toggleKeyVisibility);
// 清空历史记录按钮
clearHistoryBtn.addEventListener('click', clearHistory);
// 折叠切换
collapseToggle.addEventListener('click', (e) => {
e.stopPropagation(); // 阻止事件冒泡
toggleCollapse();
});
// 点击外侧区域关闭折叠
document.addEventListener('click', (e) => {
const collapseSection = document.querySelector('.collapse-section');
const collapseContent = document.getElementById('collapseContent');
// 检查是否点击了折叠区域外部
if (collapseSection && collapseContent && !collapseSection.contains(e.target)) {
// 如果折叠内容已展开,则关闭它
if (collapseContent.classList.contains('expanded')) {
toggleCollapse();
}
}
});
// 阻止折叠内容区域内的点击事件冒泡
if (collapseContent) {
collapseContent.addEventListener('click', (e) => {
e.stopPropagation();
});
}
// 密钥输入监听
keyInput.addEventListener('input', updateEncryptionStatus);
}
// 密钥显示/隐藏切换
function toggleKeyVisibility() {
if (keyInput.type === 'password') {
keyInput.type = 'text';
toggleKeyBtn.innerHTML = '<i class="fas fa-eye-slash"></i>';
} else {
keyInput.type = 'password';
toggleKeyBtn.innerHTML = '<i class="fas fa-eye"></i>';
}
}
// 折叠切换
function toggleCollapse(forceExpand = false) {
const isExpanded = collapseContent.classList.contains('expanded');
const shouldExpand = forceExpand || !isExpanded;
if (shouldExpand) {
collapseContent.classList.add('expanded');
collapseIcon.style.transform = 'rotate(180deg)';
} else {
collapseContent.classList.remove('expanded');
collapseIcon.style.transform = 'rotate(0deg)';
}
}
// 更新加密状态显示
function updateEncryptionStatus() {
const keyValue = keyInput.value.trim();
const encryptionStatus = document.getElementById('encryptionStatus');
if (keyValue) {
encryptionStatus.textContent = '(已启用加密)';
encryptionStatus.style.color = '#2ec4b6';
} else {
encryptionStatus.textContent = '(未加密)';
encryptionStatus.style.color = '#ff9f1c';
}
}
// 更新动态表单字段
function setupDynamicFields() {
const endpoint = endpointSelect.value;
dynamicFieldsEl.innerHTML = '';
if (endpoint === '/sms/query' || endpoint === '/call/query') {
const fields = getFieldsForEndpoint(endpoint);
createField(fields[0]);
const group = document.createElement('div');
group.className = 'form-group form-group-inline';
const columnsContainer = document.createElement('div');
columnsContainer.className = 'inline-fields-row';
const createColumn = (id, name, label, icon, value) => {
const column = document.createElement('div');
column.className = 'inline-field-column';
const labelEl = document.createElement('label');
labelEl.className = 'form-label';
labelEl.htmlFor = id;
labelEl.innerHTML = `<i class="${icon}"></i>${label}`;
column.appendChild(labelEl);
const input = document.createElement('input');
input.className = 'form-control';
input.id = id;
input.name = name;
input.type = 'number';
input.required = true;
input.value = value;
input.min = '1';
column.appendChild(input);
return column;
};
columnsContainer.appendChild(createColumn('page_num', 'page_num', '页码', 'fas fa-file-alt', '1'));
columnsContainer.appendChild(createColumn('page_size', 'page_size', '分页大小', 'fas fa-list-ol', '10'));
group.appendChild(columnsContainer);
dynamicFieldsEl.appendChild(group);
for (let i = 3; i < fields.length; i++) {
createField(fields[i]);
}
return;
}
const fields = getFieldsForEndpoint(endpoint);
fields.forEach(field => createField(field));
}
// 根据不同端点获取字段配置
function getFieldsForEndpoint(endpoint) {
const endpointFields = {
'/clone/pull': [
{name: 'version_code', type: 'number', label: '客户端App版本号', required: true, defaultValue: '', icon: 'fas fa-code-branch'}
],
'/clone/push': [
{name: 'config_data', type: 'textarea', label: '配置数据(JSON格式)', required: true, defaultValue: '{}', icon: 'fas fa-file-code'}
],
'/sms/send': [
{name: 'sim_slot', type: 'radio', options: [{value: '1', label: 'SIM卡1'}, {value: '2', label: 'SIM卡2'}], required: true, defaultValue: '1', label: '选择SIM卡', icon: 'fas fa-sim-card'},
{name: 'phone_numbers', type: 'text', label: '接收手机号码', required: true, defaultValue: '', placeholder: '多个手机号用分号分隔', icon: 'fas fa-phone'},
{name: 'msg_content', type: 'textarea', label: '短信内容', required: true, defaultValue: '', icon: 'fas fa-comment-alt'}
],
'/sms/query': [
{name: 'type', type: 'radio', options: [{value: '1', label: '接收'}, {value: '2', label: '发送'}], required: true, defaultValue: '1', label: '短信类型', icon: 'fas fa-inbox'},
{name: 'page_num', type: 'number', label: '页码', required: true, defaultValue: '1', icon: 'fas fa-file-alt'},
{name: 'page_size', type: 'number', label: '分页大小', required: true, defaultValue: '10', icon: 'fas fa-list-ol'},
{name: 'keyword', type: 'text', label: '搜索关键字', required: false, defaultValue: '', placeholder: '模糊匹配短信内容', icon: 'fas fa-search'}
],
'/call/query': [
{name: 'type', type: 'radio', options: [{value: '1', label: '呼入'}, {value: '2', label: '呼出'}, {value: '3', label: '未接'}], required: false, defaultValue: '3', label: '通话类型', icon: 'fas fa-phone-alt'},
{name: 'page_num', type: 'number', label: '页码', required: true, defaultValue: '1', icon: 'fas fa-file-alt'},
{name: 'page_size', type: 'number', label: '分页大小', required: true, defaultValue: '10', icon: 'fas fa-list-ol'},
{name: 'phone_number', type: 'text', label: '手机号码', required: false, defaultValue: '', placeholder: '模糊匹配号码', icon: 'fas fa-phone'}
],
'/wol/send': [
{name: 'mac', type: 'text', label: '网卡MAC地址', required: true, defaultValue: '', placeholder: '例如:24:5E:BE:0C:45:9A', icon: 'fas fa-network-wired'}
],
'/contact/add': [
{name: 'phone_number', type: 'text', label: '手机号码', required: true, defaultValue: '', placeholder: '多个手机号用分号分隔', icon: 'fas fa-address-book'},
{name: 'name', type: 'text', label: '联系人姓名', required: false, defaultValue: '', icon: 'fas fa-user'}
]
};
return endpointFields[endpoint] || [];
}
// 创建表单字段
function createField(field) {
const fieldGroup = document.createElement('div');
fieldGroup.className = 'form-group';
const label = document.createElement('label');
label.className = 'form-label';
label.htmlFor = field.name;
if (field.icon) {
label.innerHTML = `<i class="${field.icon}"></i>${field.label}`;
} else {
label.textContent = field.label;
}
fieldGroup.appendChild(label);
if (field.type === 'radio') {
const radioGroup = document.createElement('div');
radioGroup.className = 'radio-group';
field.options.forEach(option => {
const radioItem = document.createElement('div');
radioItem.className = 'radio-item';
const radioInput = document.createElement('input');
radioInput.type = 'radio';
radioInput.id = `${field.name}_${option.value}`;
radioInput.name = field.name;
radioInput.value = option.value;
radioInput.checked = option.value === field.defaultValue;
const radioLabel = document.createElement('label');
radioLabel.htmlFor = radioInput.id;
radioLabel.textContent = option.label;
radioItem.appendChild(radioInput);
radioItem.appendChild(radioLabel);
radioGroup.appendChild(radioItem);
});
fieldGroup.appendChild(radioGroup);
} else {
const fieldInput = field.type === 'textarea' ?
document.createElement('textarea') :
document.createElement('input');
fieldInput.className = 'form-control';
fieldInput.id = field.name;
fieldInput.name = field.name;
fieldInput.required = field.required;
fieldInput.value = field.defaultValue;
if (field.placeholder) fieldInput.placeholder = field.placeholder;
if (field.type === 'number') {
fieldInput.type = 'number';
fieldInput.min = '1';
}
fieldGroup.appendChild(fieldInput);
}
dynamicFieldsEl.appendChild(fieldGroup);
}
// 发送请求
async function sendRequest() {
// 验证必填字段
const urlInputValue = urlInput.value.trim();
const keyValue = keyInput.value.trim();
const endpoint = endpointSelect.value;
// 检查服务地址
if (!urlInputValue) {
alert('请输入服务地址');
return;
}
// 检查动态字段
if (!document.getElementById('apiForm').checkValidity()) return;
const timestamp = Date.now();
let data = {};
// 特殊处理 /clone/push 端点
if (endpoint === '/clone/push') {
try {
const configData = document.getElementById('config_data').value;
data = JSON.parse(configData);
} catch {
showResponse('错误: 配置数据不是有效的JSON格式');
return;
}
} else {
// 收集动态字段的值
document.querySelectorAll('#dynamicFields input, #dynamicFields textarea').forEach(field => {
if (field.type === 'radio' && !field.checked) return;
data[field.name] = field.value;
});
}
// 禁用按钮并显示加载状态
sendButton.disabled = true;
sendButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 请求中...';
// 显示加载状态
responseEl.innerHTML = '<div class="loader"><div class="spinner"></div></div>';
try {
let requestBody;
if (keyValue) {
// 生成签名
const sign = await generateSign(timestamp, keyValue);
requestBody = { data, timestamp, sign };
// 显示签名方法
methodEl.textContent = window.crypto?.subtle ?
"原生Web Crypto API" : "CryptoJS库";
} else {
// 跳过加密,直接发送数据
requestBody = { data };
methodEl.textContent = "无加密";
}
// 发送请求
const response = await fetch(urlInputValue + endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody)
});
const responseData = await response.json();
// 处理响应
if (response.status !== 200 || responseData.code !== 200) {
throw new Error(responseData.msg || `请求失败,状态码: ${response.status}`);
}
// 保存密钥到cookie(仅在有密钥时)
if (keyValue) {
setCookie('sms_forwarder_key', keyValue, 7);
}
// 记录历史请求
addToHistory(urlInputValue, endpoint, responseData);
// 格式化响应
if (endpoint === '/sms/query' || endpoint === '/call/query') {
responseEl.innerHTML = formatResponseData(responseData.data, endpoint);
} else {
responseEl.innerHTML = highlightJson(responseData.data);
}
} catch (error) {
responseEl.innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>${error.message}</p></div>`;
} finally {
// 恢复按钮状态
sendButton.disabled = false;
sendButton.innerHTML = '<i class="fas fa-paper-plane"></i> 发送请求';
}
}
// 生成签名
async function generateSign(timestamp, key) {
const message = `${timestamp}\n${key}`;
if (window.crypto?.subtle) {
try {
const enc = new TextEncoder();
const cryptoKey = await crypto.subtle.importKey(
"raw", enc.encode(key), {name: "HMAC", hash: "SHA-256"}, false, ["sign"]
);
const signature = await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(message));
// 转换为base64
const uint8Array = new Uint8Array(signature);
const base64 = btoa(String.fromCharCode(...uint8Array));
return encodeURIComponent(base64);
} catch (e) {
console.warn('Web Crypto API签名失败,将使用CryptoJS', e);
}
}
// 使用CryptoJS作为备选方案
const signature = CryptoJS.HmacSHA256(message, key);
const base64 = CryptoJS.enc.Base64.stringify(signature);
return encodeURIComponent(base64);
}
// 高亮显示JSON - 添加横向滚动支持
function highlightJson(obj) {
if (!obj) return "无数据";
const jsonString = JSON.stringify(obj, null, 2);
const highlighted = jsonString
.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)/g, match =>
/:$/.test(match) ? `<span class="json-key">${match}</span>` : `<span class="json-string">${match}</span>`)
.replace(/\b(true|false)\b/g, `<span class="json-boolean">$&</span>`)
.replace(/\b(null)\b/g, `<span class="json-null">$&</span>`)
.replace(/-?\d+(\.\d+)?([eE][+-]?\d+)?/g, `<span class="json-number">$&</span>`)
.replace(/[{}[\],]/g, `<span class="json-punctuation">$&</span>`);
// 使用容器包裹并添加横向滚动
return `<div class="json-container"><pre style="margin: 0; white-space: pre-wrap; word-wrap: break-word;">${highlighted}</pre></div>`;
}
// 格式化响应数据
function formatResponseData(data, endpoint) {
if (!data || !Array.isArray(data) || !data.length)
return '<div class="empty-state"><i class="fas fa-inbox"></i><p>没有找到匹配的数据</p></div>';
return `<div class="record-list">${data.map(item => {
const date = item.date ? new Date(item.date).toLocaleString() :
item.dateLong ? new Date(item.dateLong).toLocaleString() : '未知时间';
const name = item.name ? item.name : '未知联系人';
const number = item.number || '未知号码';
const simInfo = item.sim_id ? `SIM卡 ${item.sim_id}` : '未知SIM卡';
// 短信记录
if (endpoint === '/sms/query') {
return `<div class="record-item">
<div class="record-header">
<div class="record-header-info">
<div class="record-name">${name}</div>
<div class="record-number">${number}</div>
</div>
<div class="record-time"><i class="far fa-clock"></i> ${date}</div>
</div>
<div class="record-content">${item.content || '无内容'}</div>
<div class="record-footer">
<div class="record-sim"><i class="fas fa-sim-card"></i> ${simInfo}</div>
<div class="record-actions">
<button class="action-btn reply-btn" onclick="setSMSForm('${number.replace(/'/g, "\\'")}')">
<i class="fas fa-reply"></i>回复
</button>
</div>
</div>
</div>`;
}
// 通话记录
if (endpoint === '/call/query') {
const callType = item.type === 1 ? '呼入' : item.type === 2 ? '呼出' : '未接';
const duration = item.duration ?
`${Math.floor(item.duration/60)}分${item.duration%60}秒` : '未知时长';
return `<div class="record-item">
<div class="record-header">
<div class="record-header-info">
<div class="record-name">${name}</div>
<div class="record-number">${number}</div>
</div>
<div class="record-time"><i class="far fa-clock"></i> ${date}</div>
</div>
<div class="record-footer">
<div class="record-sim"><i class="fas fa-phone-alt"></i> ${callType} · ${duration}</div>
<div class="record-sim"><i class="fas fa-sim-card"></i> ${simInfo}</div>
</div>
</div>`;
}
return highlightJson(item);
}).join('')}</div>`;
}
// 添加历史记录
function addToHistory(url, endpoint, responseData) {
const history = JSON.parse(localStorage.getItem('apiHistory') || '[]');
// 创建历史记录项
const historyItem = {
id: Date.now(),
timestamp: new Date().toISOString(),
url,
endpoint,
name: endpointSelect.options[endpointSelect.selectedIndex].text,
response: responseData
};
// 添加到历史队列前部
history.unshift(historyItem);
// 最多保留MAX_HISTORY_ITEMS条历史记录
if (history.length > MAX_HISTORY_ITEMS) history.pop();
// 保存到本地存储
localStorage.setItem('apiHistory', JSON.stringify(history));
// 更新历史面板
renderHistory(history);
}
// 加载历史记录
function loadHistory() {
const history = JSON.parse(localStorage.getItem('apiHistory') || '[]');
renderHistory(history);
}
// 渲染历史记录
function renderHistory(history) {
if (!history || !history.length) {
historyPanel.innerHTML = '<div class="empty-state"><i class="fas fa-clock"></i><p>暂无历史记录</p></div>';
return;
}
historyPanel.innerHTML = history.map(item => {
const time = new Date(item.timestamp).toLocaleTimeString();
const date = new Date(item.timestamp).toLocaleDateString();
return `<div class="history-item" data-id="${item.id}">
<div class="history-item-title">
<i class="fas fa-history"></i>${item.name}
</div>
<div>${item.url}${item.endpoint}</div>
<div class="history-item-time">${date} ${time}</div>
</div>`;
}).join('');
// 添加点击事件
document.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const id = item.dataset.id;
const history = JSON.parse(localStorage.getItem('apiHistory') || '[]');
const historyItem = history.find(i => i.id == id);
if (historyItem) {
// 回填历史数据
urlInput.value = historyItem.url;
// 设置端点选择
for (let i = 0; i < endpointSelect.options.length; i++) {
if (endpointSelect.options[i].value === historyItem.endpoint) {
endpointSelect.selectedIndex = i;
break;
}
}
// 重新创建表单字段
setupDynamicFields();
// 显示响应
if (historyItem.response.data) {
if (historyItem.endpoint === '/sms/query' || historyItem.endpoint === '/call/query') {
responseEl.innerHTML = formatResponseData(historyItem.response.data, historyItem.endpoint);
} else {
responseEl.innerHTML = highlightJson(historyItem.response.data);
}
}
}
});
});
}
// 清空历史记录
function clearHistory() {
localStorage.removeItem('apiHistory');
historyPanel.innerHTML = '<div class="empty-state"><i class="fas fa-clock"></i><p>暂无历史记录</p></div>';
}
// 回填短信表单
function setSMSForm(number) {
// 设置端点为发送短信
for (let i = 0; i < endpointSelect.options.length; i++) {
if (endpointSelect.options[i].value === '/sms/send') {
endpointSelect.selectedIndex = i;
break;
}
}
// 更新表单字段
setupDynamicFields();
// 设置电话号码字段(需要等待DOM更新)
setTimeout(() => {
const phoneNumbersInput = document.getElementById('phone_numbers');
if (phoneNumbersInput) {
phoneNumbersInput.value = number;
document.getElementById('msg_content')?.focus();
}
}, 100);
}
// 复制到剪贴板
function copyToClipboard() {
const text = responseEl.innerText;
navigator.clipboard.writeText(text).then(() => {
// 显示成功反馈
const originalText = copyBtn.innerHTML;
copyBtn.innerHTML = '<i class="fas fa-check"></i> 已复制';
setTimeout(() => {
copyBtn.innerHTML = originalText;
}, 2000);
}).catch(err => {
console.error('复制失败:', err);
});
}
// 显示响应内容
function showResponse(content) {
responseEl.innerHTML = content;
}
// 初始化页面
document.addEventListener('DOMContentLoaded', initForm);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment