Files
sms_forwarding/public/admin.html
T
2026-06-27 16:57:48 +08:00

422 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SMS Gateway · 管理台</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400;1,500&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="/assets/admin.css">
</head>
<body>
<header class="top-nav">
<div class="top-nav__inner">
<a class="brand" href="/admin" aria-label="SMS Gateway 管理台">
<svg class="brand__mark" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
<span class="brand__name">SMS Gateway</span>
</a>
<nav class="nav-links" aria-label="管理导航">
<a href="#dashboard">状态</a>
<a href="#sim">SIM</a>
<a href="#inbox">收件箱</a>
<a href="#send">短信</a>
<a href="#console">AT 控制台</a>
<a href="#logs">日志</a>
</nav>
<div class="top-nav__actions">
<button class="button button--secondary" id="pushSettingsButton" type="button">推送渠道</button>
</div>
</div>
</header>
<main>
<!-- ============ HERO ============ -->
<section class="hero-band" id="status">
<div class="container hero-grid">
<div class="hero-copy">
<span class="eyebrow eyebrow--coral">
<span class="eyebrow__dot" aria-hidden="true"></span>
<span id="readyBadge">连接检测中</span>
</span>
<h1>短信<em>网关</em><br>管理台</h1>
<p class="lede">集中查看 ML307A 模组状态、收发短信、执行 AT 命令、检查运行日志。</p>
<div class="hero-actions">
<a class="text-link" href="#send">发送短信</a>
<span class="hero-divider" aria-hidden="true">·</span>
<a class="text-link" href="#console">查看 AT 控制台</a>
</div>
</div>
<aside class="runtime-card" aria-label="运行摘要">
<header class="runtime-card__header">
<span class="runtime-card__label">
<svg class="spike spike--coral" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
runtime
</span>
<span class="runtime-card__status">
<span class="status-dot" id="panelStatusDot" aria-hidden="true"></span>
<strong id="panelStatus">loading</strong>
</span>
</header>
<div class="runtime-card__hero">
<span class="runtime-card__caption">uptime</span>
<span class="runtime-card__value" id="uptime">--</span>
</div>
<dl class="runtime-card__meta">
<div>
<dt>operator</dt>
<dd id="operator">--</dd>
</div>
<div>
<dt>signal</dt>
<dd id="signalQuality">--</dd>
</div>
</dl>
</aside>
</div>
</section>
<!-- ============ STATUS BENTO ============ -->
<section class="section-band section-band--soft" id="dashboard">
<div class="container">
<div class="section-heading">
<div>
<span class="eyebrow eyebrow--spike">
<svg class="spike spike--coral" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
status board
</span>
<h2>当前<em>状态</em></h2>
</div>
<button class="button button--primary" id="refreshButton" type="button">刷新状态</button>
</div>
<div class="bento-grid">
<article class="bento-hero">
<span class="eyebrow eyebrow--coral">
<span class="eyebrow__dot" aria-hidden="true"></span>
connection
</span>
<h3 id="connectionState">--</h3>
<p id="connectionHint">等待状态刷新。</p>
</article>
<article class="bento-control">
<span class="eyebrow">mobile data</span>
<h3 id="mobileDataState">--</h3>
<p id="mobileDataHint">启动时会自动断开应用层拨号。</p>
<div class="bento-control__actions">
<button class="button button--secondary button--compact" id="mobileDataToggle" type="button"
data-action="disable">强制关闭流量</button>
<button class="button button--primary button--compact is-hidden" id="mobileDataConsume"
type="button">消耗一点流量</button>
<span class="form-status mobile-data-traffic-hint" id="mobileDataTrafficHint">开启流量后才会显示消耗流量按钮。</span>
<p class="form-status mobile-data-status" id="mobileDataStatus" role="status"></p>
</div>
</article>
</div>
<div class="sim-manager" id="sim">
<div class="sim-manager__header">
<div>
<span class="eyebrow">sim manager</span>
<h3 id="simMode">--</h3>
</div>
<p class="form-status sim-manager__status" id="simStatus" role="status"></p>
</div>
<div class="sim-slot-grid" aria-label="SIM卡槽">
<article class="sim-slot" id="simSlot0">
<div class="sim-slot__topline">
<span class="sim-slot__name">SIM1</span>
<span class="sim-slot__state" id="simSlot0State">--</span>
</div>
<span class="sim-slot__phone" id="simSlot0Phone">--</span>
<button class="button button--secondary button--compact" type="button" data-sim-switch="0">切到SIM1</button>
</article>
<article class="sim-slot" id="simSlot1">
<div class="sim-slot__topline">
<span class="sim-slot__name">SIM2</span>
<span class="sim-slot__state" id="simSlot1State">--</span>
</div>
<span class="sim-slot__phone" id="simSlot1Phone">--</span>
<button class="button button--secondary button--compact" type="button" data-sim-switch="1">切到SIM2</button>
</article>
</div>
</div>
<div class="stat-strip">
<article class="stat-tile">
<span class="eyebrow">signal</span>
<span class="stat-tile__value" id="signalValue">--</span>
<span class="stat-tile__hint" id="signalHint">RSSI 与误码率将在刷新后显示。</span>
</article>
<article class="stat-tile">
<span class="eyebrow">module</span>
<span class="stat-tile__value" id="moduleModel">--</span>
<span class="stat-tile__hint" id="moduleVersion">--</span>
</article>
<article class="stat-tile">
<span class="eyebrow">current sim</span>
<span class="stat-tile__value" id="currentSim">--</span>
<span class="stat-tile__hint">发送短信会使用当前绑定的 SIM。</span>
</article>
</div>
</div>
</section>
<!-- ============ INBOX ============ -->
<section class="section-band" id="inbox">
<div class="container">
<div class="section-heading">
<div>
<span class="eyebrow eyebrow--spike">
<svg class="spike spike--coral" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
inbox
</span>
<h2>收到的<em>短信</em></h2>
</div>
<div class="inbox-actions">
<button class="button button--secondary" id="messagesButton" type="button">刷新短信</button>
</div>
</div>
<div class="inbox-panel">
<div class="inbox-meta">
<span id="messageCount">最近 0 条</span>
<span class="inbox-meta__hint" id="messageScope">新短信会自动出现在这里</span>
</div>
<div class="message-list" id="messageList" aria-live="polite">
<p class="empty-state empty-state--light">暂无收到的短信。</p>
</div>
</div>
</div>
</section>
<!-- ============ WORKBENCH ============ -->
<section class="section-band section-band--soft">
<div class="container workbench-grid">
<section class="form-card" id="send">
<div class="section-heading section-heading--compact">
<span class="eyebrow eyebrow--spike">
<svg class="spike spike--coral" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
compose
</span>
<h2>发送<em>短信</em></h2>
<p>PDU 模式发送,提交后等待模组返回结果。</p>
</div>
<p class="send-sim-banner" aria-live="polite">
<span>当前发送SIM</span>
<strong id="sendSimLabel">--</strong>
</p>
<form id="smsForm" class="stack-form">
<label class="field">
<span class="field__label">目标号码</span>
<input class="text-input" type="tel" name="phone" autocomplete="tel" placeholder="13800138000" required>
</label>
<label class="field">
<span class="field__label">短信内容</span>
<textarea class="text-input text-area" name="message" rows="6" placeholder="输入要发送的短信内容"
required></textarea>
</label>
<div class="form-actions">
<button class="button button--primary" type="submit">发送短信</button>
<p class="form-status" id="smsStatus" role="status"></p>
</div>
</form>
</section>
<section class="console-card" id="console">
<header class="panel-header">
<span class="panel-header__label">
<svg class="spike spike--coral" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
at console
</span>
<span class="panel-header__status">
<span class="status-dot status-dot--amber" aria-hidden="true"></span>
<strong id="consoleState">idle</strong>
</span>
</header>
<form id="atForm" class="at-form">
<input class="text-input text-input--mono" name="command" value="AT+CSQ" autocomplete="off"
spellcheck="false" required>
<button class="button button--primary" type="submit">执行</button>
</form>
<pre class="console-output" id="atOutput"> 等待命令...</pre>
</section>
</div>
</section>
<!-- ============ LOGS ============ -->
<section class="section-band" id="logs">
<div class="container">
<div class="section-heading">
<div>
<span class="eyebrow eyebrow--spike">
<svg class="spike spike--coral" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
logs
</span>
<h2>运行<em>日志</em></h2>
</div>
<button class="button button--secondary" id="logsButton" type="button">刷新日志</button>
</div>
<div class="log-window" id="logWindow" aria-live="polite">
<p class="empty-state">等待日志加载。</p>
</div>
</div>
</section>
</main>
<!-- ============ PUSH MODAL ============ -->
<div class="modal" id="pushModal" aria-hidden="true">
<div class="modal__backdrop" data-close-push-modal></div>
<section class="modal__dialog" role="dialog" aria-modal="true" aria-labelledby="pushModalTitle">
<header class="modal__header">
<div>
<span class="eyebrow eyebrow--spike">
<svg class="spike spike--coral" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
push channels
</span>
<h2 id="pushModalTitle">推送渠道<em>设置</em></h2>
</div>
<button class="icon-button" id="pushModalClose" type="button" aria-label="关闭推送渠道设置">
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M3 3l10 10M13 3L3 13" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
</svg>
</button>
</header>
<div class="push-settings">
<aside class="channel-list-panel" aria-label="推送渠道列表">
<div class="panel-header panel-header--light">
<span class="panel-header__label">channels</span>
<div class="channel-add">
<select class="text-input select-input channel-add__select" id="pushAddTypeSelect" aria-label="新增推送类型">
<option value="dingtalk">钉钉机器人</option>
<option value="feishu">飞书机器人</option>
<option value="telegram">Telegram Bot</option>
<option value="pushplus">PushPlus</option>
<option value="serverchan">Server酱</option>
<option value="post_json">POST JSON</option>
<option value="bark">Bark</option>
<option value="get">GET</option>
<option value="custom">自定义 POST</option>
</select>
<button class="button button--secondary button--compact" id="pushAddButton" type="button">新增</button>
</div>
</div>
<div class="channel-list" id="pushChannelList"></div>
</aside>
<form class="channel-form" id="pushChannelForm">
<fieldset class="form-fieldset" id="pushFormFields" disabled>
<div class="channel-form__grid">
<label class="switch-row">
<input type="checkbox" name="enabled">
<span>启用通道</span>
</label>
<label class="field">
<span class="field__label">通道名称</span>
<input class="text-input" type="text" name="name" maxlength="80" placeholder="钉钉通知">
</label>
<label class="field">
<span class="field__label">推送类型</span>
<select class="text-input select-input" name="type">
<option value="dingtalk">钉钉机器人</option>
<option value="feishu">飞书机器人</option>
<option value="telegram">Telegram Bot</option>
<option value="pushplus">PushPlus</option>
<option value="serverchan">Server酱</option>
<option value="post_json">POST JSON</option>
<option value="bark">Bark</option>
<option value="get">GET</option>
<option value="custom">自定义 POST</option>
</select>
</label>
<label class="field field-row field-row--wide" data-push-field="url">
<span class="field__label" id="pushUrlLabel">Webhook URL</span>
<input class="text-input" type="text" name="url" autocomplete="off">
</label>
<label class="field field-row field-row--wide" data-push-field="secret">
<span class="field__label" id="pushSecretLabel">加签密钥</span>
<input class="text-input" type="password" name="secret" autocomplete="new-password">
</label>
<label class="field field-row" data-push-field="key1">
<span class="field__label" id="pushKey1Label">Key1</span>
<input class="text-input" type="text" name="key1" autocomplete="off">
</label>
<label class="field field-row" data-push-field="key2">
<span class="field__label" id="pushKey2Label">Key2</span>
<input class="text-input" type="text" name="key2" autocomplete="off">
</label>
<label class="field field-row field-row--wide" data-push-field="customBody">
<span class="field__label">自定义请求体 JSON</span>
<textarea class="text-input text-area text-area--code" name="customBody" rows="7"
spellcheck="false"></textarea>
</label>
</div>
</fieldset>
<div class="form-actions channel-form__actions">
<button class="button button--primary" id="pushSaveButton" type="submit">保存设置</button>
<button class="button button--secondary" id="pushTestButton" type="button">发送测试</button>
<button class="button button--danger" id="pushDeleteButton" type="button">删除通道</button>
<p class="form-status" id="pushStatus" role="status"></p>
</div>
</form>
</div>
</section>
</div>
<footer class="footer">
<div class="container footer__inner">
<span class="brand brand--footer">
<svg class="brand__mark" viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 1v22M1 12h22M3.5 3.5l17 17M20.5 3.5l-17 17" stroke="currentColor" stroke-width="2.4"
stroke-linecap="round" fill="none" />
</svg>
<span>SMS Gateway</span>
</span>
<span class="footer__meta">© <span id="footerYear">2026</span> · Basic Auth protected · Built on Node.js +
ML307A</span>
</div>
</footer>
<script src="/assets/admin.js" type="module"></script>
</body>
</html>