Last active
August 8, 2025 08:05
-
-
Save liuran001/59a3ee3c6270926073e61c9a588dcc3a to your computer and use it in GitHub Desktop.
SmsForwarder Web
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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