feat: 添加双卡支持
This commit is contained in:
+42
-5
@@ -24,6 +24,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<nav class="nav-links" aria-label="管理导航">
|
<nav class="nav-links" aria-label="管理导航">
|
||||||
<a href="#dashboard">状态</a>
|
<a href="#dashboard">状态</a>
|
||||||
|
<a href="#sim">SIM</a>
|
||||||
<a href="#inbox">收件箱</a>
|
<a href="#inbox">收件箱</a>
|
||||||
<a href="#send">短信</a>
|
<a href="#send">短信</a>
|
||||||
<a href="#console">AT 控制台</a>
|
<a href="#console">AT 控制台</a>
|
||||||
@@ -127,6 +128,36 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</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">
|
<div class="stat-strip">
|
||||||
<article class="stat-tile">
|
<article class="stat-tile">
|
||||||
<span class="eyebrow">signal</span>
|
<span class="eyebrow">signal</span>
|
||||||
@@ -139,9 +170,9 @@
|
|||||||
<span class="stat-tile__hint" id="moduleVersion">--</span>
|
<span class="stat-tile__hint" id="moduleVersion">--</span>
|
||||||
</article>
|
</article>
|
||||||
<article class="stat-tile">
|
<article class="stat-tile">
|
||||||
<span class="eyebrow">sim · iccid</span>
|
<span class="eyebrow">current sim</span>
|
||||||
<span class="stat-tile__value stat-tile__value--mono" id="iccid">--</span>
|
<span class="stat-tile__value" id="currentSim">--</span>
|
||||||
<span class="stat-tile__hint">ICCID 查询来自当前串口模组。</span>
|
<span class="stat-tile__hint">发送短信会使用当前绑定的 SIM。</span>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,7 +192,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<h2>收到的<em>短信</em></h2>
|
<h2>收到的<em>短信</em></h2>
|
||||||
</div>
|
</div>
|
||||||
<button class="button button--secondary" id="messagesButton" type="button">刷新短信</button>
|
<div class="inbox-actions">
|
||||||
|
<button class="button button--secondary" id="messagesButton" type="button">刷新短信</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inbox-panel">
|
<div class="inbox-panel">
|
||||||
<div class="inbox-meta">
|
<div class="inbox-meta">
|
||||||
@@ -190,6 +223,10 @@
|
|||||||
<h2>发送<em>短信</em></h2>
|
<h2>发送<em>短信</em></h2>
|
||||||
<p>PDU 模式发送,提交后等待模组返回结果。</p>
|
<p>PDU 模式发送,提交后等待模组返回结果。</p>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="send-sim-banner" aria-live="polite">
|
||||||
|
<span>当前发送SIM</span>
|
||||||
|
<strong id="sendSimLabel">--</strong>
|
||||||
|
</p>
|
||||||
<form id="smsForm" class="stack-form">
|
<form id="smsForm" class="stack-form">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span class="field__label">目标号码</span>
|
<span class="field__label">目标号码</span>
|
||||||
@@ -382,4 +419,4 @@
|
|||||||
<script src="/assets/admin.js" type="module"></script>
|
<script src="/assets/admin.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
+323
-84
@@ -66,7 +66,9 @@
|
|||||||
--r-pill: 9999px;
|
--r-pill: 9999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
@@ -85,12 +87,18 @@ body {
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input, textarea, select {
|
button,
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
font: inherit;
|
font: inherit;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
a { color: inherit; text-decoration: none; }
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
::selection {
|
::selection {
|
||||||
background: rgba(204, 120, 92, 0.22);
|
background: rgba(204, 120, 92, 0.22);
|
||||||
@@ -104,9 +112,16 @@ a { color: inherit; text-decoration: none; }
|
|||||||
|
|
||||||
/* ---------- Type primitives ---------------------------------------------- */
|
/* ---------- Type primitives ---------------------------------------------- */
|
||||||
|
|
||||||
h1, h2, h3, h4, p { margin: 0; }
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
h1, h2 {
|
h1,
|
||||||
|
h2 {
|
||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
font-family: var(--font-serif);
|
font-family: var(--font-serif);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -119,7 +134,8 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* italic serif as editorial flourish — DESIGN allows italic for emphasis */
|
/* italic serif as editorial flourish — DESIGN allows italic for emphasis */
|
||||||
h1 em, h2 em {
|
h1 em,
|
||||||
|
h2 em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
@@ -158,7 +174,9 @@ h3 {
|
|||||||
color: var(--ink);
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spike--coral { color: var(--primary); }
|
.spike--coral {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Eyebrows (caption-uppercase) --------------------------------- */
|
/* ---------- Eyebrows (caption-uppercase) --------------------------------- */
|
||||||
|
|
||||||
@@ -191,7 +209,9 @@ h3 {
|
|||||||
box-shadow: 0 0 0 3px rgba(204, 120, 92, 0.18);
|
box-shadow: 0 0 0 3px rgba(204, 120, 92, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow--spike { gap: 6px; }
|
.eyebrow--spike {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Coral inline text-link */
|
/* Coral inline text-link */
|
||||||
.text-link {
|
.text-link {
|
||||||
@@ -201,7 +221,9 @@ h3 {
|
|||||||
transition: color 120ms ease;
|
transition: color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-link:hover { color: var(--primary-active); }
|
.text-link:hover {
|
||||||
|
color: var(--primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Top nav ------------------------------------------------------- */
|
/* ---------- Top nav ------------------------------------------------------- */
|
||||||
|
|
||||||
@@ -266,7 +288,9 @@ h3 {
|
|||||||
transition: color 120ms ease;
|
transition: color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-links a:hover { color: var(--ink); }
|
.nav-links a:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
.top-nav__actions {
|
.top-nav__actions {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
@@ -304,7 +328,9 @@ h3 {
|
|||||||
color: var(--on-primary);
|
color: var(--on-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--primary:active { background: var(--primary-active); }
|
.button--primary:active {
|
||||||
|
background: var(--primary-active);
|
||||||
|
}
|
||||||
|
|
||||||
.button--secondary {
|
.button--secondary {
|
||||||
background: var(--canvas);
|
background: var(--canvas);
|
||||||
@@ -335,7 +361,9 @@ h3 {
|
|||||||
color: var(--error);
|
color: var(--error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--danger:active { background: rgba(198, 69, 69, 0.16); }
|
.button--danger:active {
|
||||||
|
background: rgba(198, 69, 69, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
.button--compact {
|
.button--compact {
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
@@ -357,8 +385,14 @@ h3 {
|
|||||||
transition: background-color 120ms ease;
|
transition: background-color 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button:hover { background: var(--surface-card); }
|
.icon-button:hover {
|
||||||
.icon-button svg { width: 14px; height: 14px; }
|
background: var(--surface-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.button:focus-visible,
|
.button:focus-visible,
|
||||||
.icon-button:focus-visible,
|
.icon-button:focus-visible,
|
||||||
@@ -406,7 +440,9 @@ h3 {
|
|||||||
padding-block: 12px;
|
padding-block: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-copy h1 { margin-top: 18px; }
|
.hero-copy h1 {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-actions {
|
.hero-actions {
|
||||||
margin-top: 32px;
|
margin-top: 32px;
|
||||||
@@ -416,7 +452,9 @@ h3 {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-divider { color: var(--muted-soft); }
|
.hero-divider {
|
||||||
|
color: var(--muted-soft);
|
||||||
|
}
|
||||||
|
|
||||||
/* Runtime card (hero right side) */
|
/* Runtime card (hero right side) */
|
||||||
.runtime-card {
|
.runtime-card {
|
||||||
@@ -541,7 +579,9 @@ h3 {
|
|||||||
background: var(--canvas);
|
background: var(--canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-band--soft { background: var(--surface-soft); }
|
.section-band--soft {
|
||||||
|
background: var(--surface-soft);
|
||||||
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -563,7 +603,9 @@ h3 {
|
|||||||
margin-bottom: var(--space-lg);
|
margin-bottom: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading--compact p { margin-top: var(--space-sm); }
|
.section-heading--compact p {
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Status bento -------------------------------------------------- */
|
/* ---------- Status bento -------------------------------------------------- */
|
||||||
|
|
||||||
@@ -646,7 +688,103 @@ h3 {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-data-status { min-height: 18px; }
|
.mobile-data-status {
|
||||||
|
min-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SIM manager */
|
||||||
|
.sim-manager {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
background: var(--surface-card);
|
||||||
|
border: 1px solid var(--hairline);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-manager__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-manager__header h3 {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-manager__status {
|
||||||
|
min-width: 180px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px 12px;
|
||||||
|
border: 1px solid var(--hairline);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
background: var(--canvas);
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot.is-active {
|
||||||
|
border-color: rgba(93, 184, 114, 0.55);
|
||||||
|
box-shadow: inset 3px 0 0 var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot__topline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot__name {
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot__state {
|
||||||
|
border-radius: var(--r-pill);
|
||||||
|
background: var(--surface-soft);
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot.is-active .sim-slot__state {
|
||||||
|
background: rgba(93, 184, 114, 0.16);
|
||||||
|
color: #2f7c44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot__phone {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
color: var(--body-strong);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot__phone[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Stat strip (3-up compact) */
|
/* Stat strip (3-up compact) */
|
||||||
.stat-strip {
|
.stat-strip {
|
||||||
@@ -670,7 +808,9 @@ h3 {
|
|||||||
border-color: var(--surface-cream-strong);
|
border-color: var(--surface-cream-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-tile .eyebrow { margin-bottom: var(--space-md); }
|
.stat-tile .eyebrow {
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
.stat-tile__value {
|
.stat-tile__value {
|
||||||
font-family: var(--font-serif);
|
font-family: var(--font-serif);
|
||||||
@@ -682,13 +822,6 @@ h3 {
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-tile__value--mono {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-tile__hint {
|
.stat-tile__hint {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding-top: var(--space-sm);
|
padding-top: var(--space-sm);
|
||||||
@@ -699,6 +832,14 @@ h3 {
|
|||||||
|
|
||||||
/* ---------- Inbox --------------------------------------------------------- */
|
/* ---------- Inbox --------------------------------------------------------- */
|
||||||
|
|
||||||
|
.inbox-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
.inbox-panel {
|
.inbox-panel {
|
||||||
border-radius: var(--r-lg);
|
border-radius: var(--r-lg);
|
||||||
background: var(--surface-card);
|
background: var(--surface-card);
|
||||||
@@ -759,26 +900,6 @@ h3 {
|
|||||||
letter-spacing: -0.2px;
|
letter-spacing: -0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-sim {
|
|
||||||
border: 1px solid var(--hairline);
|
|
||||||
border-radius: var(--r-pill);
|
|
||||||
background: var(--surface-soft);
|
|
||||||
color: var(--muted);
|
|
||||||
padding: 3px 9px;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.5;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-item__header time {
|
.message-item__header time {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
@@ -841,14 +962,36 @@ h3 {
|
|||||||
gap: var(--space-lg);
|
gap: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.send-sim-banner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: -4px 0 var(--space-md);
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-sim-banner span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-sim-banner strong {
|
||||||
|
color: var(--body-strong);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field__label,
|
.field__label,
|
||||||
.stack-form label > span:not(.field__label),
|
.stack-form label>span:not(.field__label),
|
||||||
.channel-form label > span:not(.field__label) {
|
.channel-form label>span:not(.field__label) {
|
||||||
color: var(--body-strong);
|
color: var(--body-strong);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -868,7 +1011,9 @@ h3 {
|
|||||||
transition: border-color 120ms ease, box-shadow 120ms ease;
|
transition: border-color 120ms ease, box-shadow 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-input::placeholder { color: var(--muted-soft); }
|
.text-input::placeholder {
|
||||||
|
color: var(--muted-soft);
|
||||||
|
}
|
||||||
|
|
||||||
.text-input:focus {
|
.text-input:focus {
|
||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
@@ -876,7 +1021,9 @@ h3 {
|
|||||||
box-shadow: 0 0 0 3px rgba(204, 120, 92, 0.18);
|
box-shadow: 0 0 0 3px rgba(204, 120, 92, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-input { appearance: auto; }
|
.select-input {
|
||||||
|
appearance: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.text-area {
|
.text-area {
|
||||||
min-height: 144px;
|
min-height: 144px;
|
||||||
@@ -908,8 +1055,13 @@ h3 {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-status.is-error { color: var(--error); }
|
.form-status.is-error {
|
||||||
.form-status.is-success { color: var(--success); }
|
color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-status.is-success {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Console card -------------------------------------------------- */
|
/* ---------- Console card -------------------------------------------------- */
|
||||||
|
|
||||||
@@ -994,7 +1146,9 @@ h3 {
|
|||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-line:last-child { border-bottom: 0; }
|
.log-line:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.log-level {
|
.log-level {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
@@ -1006,9 +1160,17 @@ h3 {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-line.is-error .log-level { color: var(--error); }
|
.log-line.is-error .log-level {
|
||||||
.log-line.is-warn .log-level { color: var(--warning); }
|
color: var(--error);
|
||||||
.log-line.is-info .log-level { color: var(--accent-teal); }
|
}
|
||||||
|
|
||||||
|
.log-line.is-warn .log-level {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-line.is-info .log-level {
|
||||||
|
color: var(--accent-teal);
|
||||||
|
}
|
||||||
|
|
||||||
.log-message {
|
.log-message {
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
@@ -1018,8 +1180,15 @@ h3 {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state { color: var(--muted); font-size: 14px; }
|
.empty-state {
|
||||||
.empty-state--light { color: var(--muted); font-size: 14px; }
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state--light {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Footer (slim, single row) ------------------------------------ */
|
/* ---------- Footer (slim, single row) ------------------------------------ */
|
||||||
|
|
||||||
@@ -1044,7 +1213,9 @@ h3 {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand--footer .brand__mark { color: var(--on-dark); }
|
.brand--footer .brand__mark {
|
||||||
|
color: var(--on-dark);
|
||||||
|
}
|
||||||
|
|
||||||
.footer__meta {
|
.footer__meta {
|
||||||
color: var(--on-dark-soft);
|
color: var(--on-dark-soft);
|
||||||
@@ -1056,7 +1227,9 @@ h3 {
|
|||||||
|
|
||||||
/* ---------- Modal --------------------------------------------------------- */
|
/* ---------- Modal --------------------------------------------------------- */
|
||||||
|
|
||||||
body.modal-open { overflow: hidden; }
|
body.modal-open {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -1068,7 +1241,9 @@ body.modal-open { overflow: hidden; }
|
|||||||
padding: var(--space-lg);
|
padding: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.is-open { display: flex; }
|
.modal.is-open {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.modal__backdrop {
|
.modal__backdrop {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1098,7 +1273,9 @@ body.modal-open { overflow: hidden; }
|
|||||||
padding-bottom: var(--space-lg);
|
padding-bottom: var(--space-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__header h2 { margin-top: 10px; }
|
.modal__header h2 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.push-settings {
|
.push-settings {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1114,7 +1291,9 @@ body.modal-open { overflow: hidden; }
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header--light { color: var(--muted); }
|
.panel-header--light {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.channel-list-panel .panel-header {
|
.channel-list-panel .panel-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1203,7 +1382,9 @@ body.modal-open { overflow: hidden; }
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.channel-list__item.is-active .channel-list__meta { color: var(--primary); }
|
.channel-list__item.is-active .channel-list__meta {
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
.channel-form {
|
.channel-form {
|
||||||
border-radius: var(--r-lg);
|
border-radius: var(--r-lg);
|
||||||
@@ -1218,7 +1399,9 @@ body.modal-open { overflow: hidden; }
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-fieldset:disabled { opacity: 0.55; }
|
.form-fieldset:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-form__grid {
|
.channel-form__grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1248,19 +1431,27 @@ body.modal-open { overflow: hidden; }
|
|||||||
accent-color: var(--primary);
|
accent-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-row--wide { grid-column: 1 / -1; }
|
.field-row--wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-form__actions { margin-top: var(--space-lg); }
|
.channel-form__actions {
|
||||||
|
margin-top: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
.is-hidden { display: none !important; }
|
.is-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Responsive ---------------------------------------------------- */
|
/* ---------- Responsive ---------------------------------------------------- */
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
|
|
||||||
.hero-grid,
|
.hero-grid,
|
||||||
.workbench-grid,
|
.workbench-grid,
|
||||||
.push-settings,
|
.push-settings,
|
||||||
.bento-grid {
|
.bento-grid,
|
||||||
|
.sim-slot-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1270,12 +1461,15 @@ body.modal-open { overflow: hidden; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
|
||||||
.container,
|
.container,
|
||||||
.top-nav__inner {
|
.top-nav__inner {
|
||||||
width: min(100% - 32px, 1200px);
|
width: min(100% - 32px, 1200px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav { height: auto; }
|
.top-nav {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.top-nav__inner {
|
.top-nav__inner {
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
@@ -1292,13 +1486,21 @@ body.modal-open { overflow: hidden; }
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-nav__actions { margin-left: auto; }
|
.top-nav__actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-band { padding: 56px 0 64px; }
|
.hero-band {
|
||||||
|
padding: 56px 0 64px;
|
||||||
|
}
|
||||||
|
|
||||||
.hero-copy { min-height: 0; }
|
.hero-copy {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.section-band { padding: 64px 0; }
|
.section-band {
|
||||||
|
padding: 64px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1306,15 +1508,42 @@ body.modal-open { overflow: hidden; }
|
|||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-strip { grid-template-columns: 1fr; }
|
.inbox-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-manager {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-slot {
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-manager__header {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sim-manager__status {
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-strip {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.form-card,
|
.form-card,
|
||||||
.console-card,
|
.console-card,
|
||||||
.bento-hero,
|
.bento-hero,
|
||||||
.bento-control,
|
.bento-control,
|
||||||
.runtime-card { padding: var(--space-lg); }
|
.runtime-card {
|
||||||
|
padding: var(--space-lg);
|
||||||
|
}
|
||||||
|
|
||||||
.at-form { grid-template-columns: 1fr; }
|
.at-form {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.message-item__header {
|
.message-item__header {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1322,9 +1551,13 @@ body.modal-open { overflow: hidden; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-item__header time,
|
.message-item__header time,
|
||||||
.message-status { width: max-content; }
|
.message-status {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
.log-window { height: 360px; }
|
.log-window {
|
||||||
|
height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
.log-line {
|
.log-line {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1347,10 +1580,16 @@ body.modal-open { overflow: hidden; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.channel-form,
|
.channel-form,
|
||||||
.channel-list-panel { padding: 20px; }
|
.channel-list-panel {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.channel-form__grid { grid-template-columns: 1fr; }
|
.channel-form__grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.switch-row,
|
.switch-row,
|
||||||
.field-row--wide { grid-column: auto; }
|
.field-row--wide {
|
||||||
}
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
+158
-20
@@ -2,7 +2,9 @@ const state = {
|
|||||||
busy: false,
|
busy: false,
|
||||||
mobileDataBusy: false,
|
mobileDataBusy: false,
|
||||||
trafficBusy: false,
|
trafficBusy: false,
|
||||||
|
simBusy: false,
|
||||||
currentMobileData: null,
|
currentMobileData: null,
|
||||||
|
currentSim: null,
|
||||||
pushChannels: [],
|
pushChannels: [],
|
||||||
selectedPushChannel: -1,
|
selectedPushChannel: -1,
|
||||||
pushLoaded: false
|
pushLoaded: false
|
||||||
@@ -44,10 +46,20 @@ const elements = {
|
|||||||
mobileDataConsume: document.querySelector('#mobileDataConsume'),
|
mobileDataConsume: document.querySelector('#mobileDataConsume'),
|
||||||
mobileDataTrafficHint: document.querySelector('#mobileDataTrafficHint'),
|
mobileDataTrafficHint: document.querySelector('#mobileDataTrafficHint'),
|
||||||
mobileDataStatus: document.querySelector('#mobileDataStatus'),
|
mobileDataStatus: document.querySelector('#mobileDataStatus'),
|
||||||
|
simMode: document.querySelector('#simMode'),
|
||||||
|
simStatus: document.querySelector('#simStatus'),
|
||||||
|
simSlot0: document.querySelector('#simSlot0'),
|
||||||
|
simSlot1: document.querySelector('#simSlot1'),
|
||||||
|
simSlot0State: document.querySelector('#simSlot0State'),
|
||||||
|
simSlot1State: document.querySelector('#simSlot1State'),
|
||||||
|
simSlot0Phone: document.querySelector('#simSlot0Phone'),
|
||||||
|
simSlot1Phone: document.querySelector('#simSlot1Phone'),
|
||||||
|
simSwitchButtons: [...document.querySelectorAll('[data-sim-switch]')],
|
||||||
moduleModel: document.querySelector('#moduleModel'),
|
moduleModel: document.querySelector('#moduleModel'),
|
||||||
moduleVersion: document.querySelector('#moduleVersion'),
|
moduleVersion: document.querySelector('#moduleVersion'),
|
||||||
iccid: document.querySelector('#iccid'),
|
currentSim: document.querySelector('#currentSim'),
|
||||||
smsForm: document.querySelector('#smsForm'),
|
smsForm: document.querySelector('#smsForm'),
|
||||||
|
sendSimLabel: document.querySelector('#sendSimLabel'),
|
||||||
smsStatus: document.querySelector('#smsStatus'),
|
smsStatus: document.querySelector('#smsStatus'),
|
||||||
messagesButton: document.querySelector('#messagesButton'),
|
messagesButton: document.querySelector('#messagesButton'),
|
||||||
messageCount: document.querySelector('#messageCount'),
|
messageCount: document.querySelector('#messageCount'),
|
||||||
@@ -225,12 +237,97 @@ function renderMobileData(mobileData = {}) {
|
|||||||
elements.mobileDataConsume.disabled = state.mobileDataBusy || state.trafficBusy || !enabled;
|
elements.mobileDataConsume.disabled = state.mobileDataBusy || state.trafficBusy || !enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStatus(status, info) {
|
function getActiveSimSlot(sim = {}) {
|
||||||
|
return (sim.slots || []).find(slot => slot.active) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSimDisplayLabel(slot) {
|
||||||
|
return slot?.phoneLabel || slot?.phoneNumber || slot?.name || 'SIM';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActiveSimLabel() {
|
||||||
|
return getSimDisplayLabel(getActiveSimSlot(state.currentSim || {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSendSimLabel() {
|
||||||
|
setText(elements.sendSimLabel, getActiveSimLabel());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSimSlot(sim = {}, slotNumber) {
|
||||||
|
return (sim.slots || []).find(slot => slot.slot === slotNumber) || {
|
||||||
|
slot: slotNumber,
|
||||||
|
name: `SIM${slotNumber + 1}`,
|
||||||
|
active: false,
|
||||||
|
phoneNumber: '',
|
||||||
|
phoneLabel: `SIM${slotNumber + 1}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSimSlotPhone(slot) {
|
||||||
|
if (slot?.phoneNumber) {
|
||||||
|
return slot.phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot?.phoneLabel && slot.phoneLabel !== slot.name) {
|
||||||
|
return slot.phoneLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSimSlot(slot, card, stateNode, phoneNode) {
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.classList.toggle('is-active', Boolean(slot.active));
|
||||||
|
setText(stateNode, slot.active ? '当前使用' : '未选中');
|
||||||
|
const phoneText = formatSimSlotPhone(slot);
|
||||||
|
if (phoneNode) {
|
||||||
|
phoneNode.textContent = phoneText;
|
||||||
|
phoneNode.hidden = !phoneText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSimStatus(sim = {}) {
|
||||||
|
state.currentSim = sim;
|
||||||
|
|
||||||
|
const supported = Boolean(sim.supported && sim.canSwitch);
|
||||||
|
const activeSlot = getActiveSimSlot(sim);
|
||||||
|
const modeLabel = supported ? (sim.modeLabel || '双卡') : '单卡/未检测到双卡';
|
||||||
|
|
||||||
|
setText(elements.simMode, modeLabel);
|
||||||
|
|
||||||
|
renderSimSlot(getSimSlot(sim, 0), elements.simSlot0, elements.simSlot0State, elements.simSlot0Phone);
|
||||||
|
renderSimSlot(getSimSlot(sim, 1), elements.simSlot1, elements.simSlot1State, elements.simSlot1Phone);
|
||||||
|
|
||||||
|
elements.simSwitchButtons.forEach((button) => {
|
||||||
|
const slot = Number.parseInt(button.dataset.simSwitch, 10);
|
||||||
|
const isActive = activeSlot?.slot === slot;
|
||||||
|
button.disabled = state.simBusy || Boolean(sim.switching) || !supported || isActive;
|
||||||
|
button.textContent = isActive ? '正在使用' : `切到SIM${slot + 1}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (state.simBusy || sim.switching) {
|
||||||
|
setStatusMessage(elements.simStatus, '正在切换SIM...');
|
||||||
|
} else if (supported && activeSlot) {
|
||||||
|
setStatusMessage(elements.simStatus, `当前 ${getSimDisplayLabel(activeSlot)}`, 'success');
|
||||||
|
} else if (!supported) {
|
||||||
|
setStatusMessage(elements.simStatus, '双卡不可用');
|
||||||
|
} else {
|
||||||
|
setStatusMessage(elements.simStatus, '');
|
||||||
|
}
|
||||||
|
renderSendSimLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatus(status, info, sim) {
|
||||||
const data = status.data || {};
|
const data = status.data || {};
|
||||||
const infoData = info.data || {};
|
const infoData = info.data || {};
|
||||||
const model = data.model || infoData.model || {};
|
const model = data.model || infoData.model || {};
|
||||||
const signal = data.signal || {};
|
const signal = data.signal || {};
|
||||||
const ready = Boolean(data.ready);
|
const ready = Boolean(data.ready);
|
||||||
|
const simData = sim?.data || data.sim || {};
|
||||||
|
const activeSim = getActiveSimSlot(simData);
|
||||||
|
|
||||||
setText(elements.readyBadge, ready ? '模组已就绪' : '模组未就绪');
|
setText(elements.readyBadge, ready ? '模组已就绪' : '模组未就绪');
|
||||||
setText(elements.panelStatus, ready ? 'ready' : 'not ready');
|
setText(elements.panelStatus, ready ? 'ready' : 'not ready');
|
||||||
@@ -246,7 +343,8 @@ function renderStatus(status, info) {
|
|||||||
setText(elements.signalHint, signal.ber === undefined ? 'RSSI 与误码率将在刷新后显示。' : `误码率 ${signal.ber},质量 ${signal.quality || '未知'}。`);
|
setText(elements.signalHint, signal.ber === undefined ? 'RSSI 与误码率将在刷新后显示。' : `误码率 ${signal.ber},质量 ${signal.quality || '未知'}。`);
|
||||||
setText(elements.moduleModel, model.model || '--');
|
setText(elements.moduleModel, model.model || '--');
|
||||||
setText(elements.moduleVersion, model.version || '--');
|
setText(elements.moduleVersion, model.version || '--');
|
||||||
setText(elements.iccid, infoData.iccid || '--');
|
setText(elements.currentSim, getSimDisplayLabel(activeSim) || infoData.simLabel || '--');
|
||||||
|
renderSimStatus(simData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLogLine(line) {
|
function parseLogLine(line) {
|
||||||
@@ -314,11 +412,8 @@ function formatDateTime(value) {
|
|||||||
|
|
||||||
function renderMessages(messages, meta = {}) {
|
function renderMessages(messages, meta = {}) {
|
||||||
elements.messageList.replaceChildren();
|
elements.messageList.replaceChildren();
|
||||||
const currentSimLabel = meta.currentSim?.simLabel || '当前SIM未知';
|
const countLabel = `最近 ${messages.length} 条`;
|
||||||
const countLabel = meta.partitionBySim ? `${currentSimLabel} 最近 ${messages.length} 条` : `全部SIM 最近 ${messages.length} 条`;
|
const scopeLabel = '新短信自动显示';
|
||||||
const scopeLabel = meta.partitionBySim
|
|
||||||
? (meta.currentSimKnown ? '仅显示当前SIM收到的短信' : '未识别当前SIM,收件箱已隐藏')
|
|
||||||
: 'SIM隔离未启用';
|
|
||||||
|
|
||||||
setText(elements.messageCount, countLabel);
|
setText(elements.messageCount, countLabel);
|
||||||
setText(elements.messageScope, scopeLabel);
|
setText(elements.messageScope, scopeLabel);
|
||||||
@@ -353,16 +448,8 @@ function renderMessages(messages, meta = {}) {
|
|||||||
text.className = 'message-text';
|
text.className = 'message-text';
|
||||||
text.textContent = message.text || '';
|
text.textContent = message.text || '';
|
||||||
|
|
||||||
const metaRow = document.createElement('div');
|
|
||||||
metaRow.className = 'message-meta';
|
|
||||||
|
|
||||||
const sim = document.createElement('span');
|
|
||||||
sim.className = 'message-sim';
|
|
||||||
sim.textContent = message.simLabel || '未知SIM';
|
|
||||||
|
|
||||||
metaRow.append(sim);
|
|
||||||
header.append(sender, status, time);
|
header.append(sender, status, time);
|
||||||
item.append(header, metaRow, text);
|
item.append(header, text);
|
||||||
fragment.append(item);
|
fragment.append(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -679,10 +766,54 @@ async function refreshLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshMessages() {
|
async function refreshMessages() {
|
||||||
const messages = await requestJSON('/api/sms/received?limit=50');
|
const params = new URLSearchParams({ limit: '50', scope: 'all' });
|
||||||
|
const messages = await requestJSON(`/api/sms/received?${params.toString()}`);
|
||||||
renderMessages(messages.data || [], messages.meta || {});
|
renderMessages(messages.data || [], messages.meta || {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshSim() {
|
||||||
|
const sim = await requestJSON('/api/modem/sim?refresh=true');
|
||||||
|
renderSimStatus(sim.data || {});
|
||||||
|
return sim;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchSim(slot) {
|
||||||
|
if (state.simBusy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotNumber = Number.parseInt(slot, 10);
|
||||||
|
if (!Number.isInteger(slotNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.confirm(`切换到SIM${slotNumber + 1}会重新初始化SIM、驻网和短信功能,期间可能短暂收不到短信。确定继续吗?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.simBusy = true;
|
||||||
|
renderSimStatus({
|
||||||
|
...(state.currentSim || {}),
|
||||||
|
switching: true
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await requestJSON('/api/modem/sim/switch', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ slot: slotNumber })
|
||||||
|
});
|
||||||
|
renderSimStatus(result.data || {});
|
||||||
|
setStatusMessage(elements.simStatus, result.message || `已切换到SIM${slotNumber + 1}`, 'success');
|
||||||
|
await refreshAll();
|
||||||
|
} catch (err) {
|
||||||
|
setStatusMessage(elements.simStatus, err.message, 'error');
|
||||||
|
await refreshSim().catch(() => {});
|
||||||
|
} finally {
|
||||||
|
state.simBusy = false;
|
||||||
|
renderSimStatus(state.currentSim || {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleMobileData() {
|
async function toggleMobileData() {
|
||||||
if (state.mobileDataBusy) {
|
if (state.mobileDataBusy) {
|
||||||
return;
|
return;
|
||||||
@@ -754,7 +885,8 @@ async function refreshAll() {
|
|||||||
try {
|
try {
|
||||||
const status = await requestJSON('/api/status');
|
const status = await requestJSON('/api/status');
|
||||||
const info = await requestJSON('/api/modem/info');
|
const info = await requestJSON('/api/modem/info');
|
||||||
renderStatus(status, info);
|
const sim = await requestJSON('/api/modem/sim?refresh=true');
|
||||||
|
renderStatus(status, info, sim);
|
||||||
setStatusMessage(elements.mobileDataStatus, '');
|
setStatusMessage(elements.mobileDataStatus, '');
|
||||||
await refreshMessages();
|
await refreshMessages();
|
||||||
await refreshLogs();
|
await refreshLogs();
|
||||||
@@ -784,7 +916,8 @@ async function sendSMS(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit.disabled = true;
|
submit.disabled = true;
|
||||||
setStatusMessage(elements.smsStatus, '发送中...');
|
renderSendSimLabel();
|
||||||
|
setStatusMessage(elements.smsStatus, `正在使用 ${getActiveSimLabel()} 发送...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await requestJSON('/api/sms/send', {
|
const result = await requestJSON('/api/sms/send', {
|
||||||
@@ -845,6 +978,11 @@ elements.pushChannelForm.elements.type.addEventListener('change', handlePushType
|
|||||||
elements.refreshButton.addEventListener('click', refreshAll);
|
elements.refreshButton.addEventListener('click', refreshAll);
|
||||||
elements.mobileDataToggle.addEventListener('click', toggleMobileData);
|
elements.mobileDataToggle.addEventListener('click', toggleMobileData);
|
||||||
elements.mobileDataConsume.addEventListener('click', consumeMobileDataTraffic);
|
elements.mobileDataConsume.addEventListener('click', consumeMobileDataTraffic);
|
||||||
|
elements.simSwitchButtons.forEach((button) => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
switchSim(button.dataset.simSwitch);
|
||||||
|
});
|
||||||
|
});
|
||||||
elements.logsButton.addEventListener('click', refreshLogs);
|
elements.logsButton.addEventListener('click', refreshLogs);
|
||||||
elements.messagesButton.addEventListener('click', refreshMessages);
|
elements.messagesButton.addEventListener('click', refreshMessages);
|
||||||
elements.smsForm.addEventListener('submit', sendSMS);
|
elements.smsForm.addEventListener('submit', sendSMS);
|
||||||
|
|||||||
+60
-7
@@ -374,6 +374,7 @@ class APIServer {
|
|||||||
signal,
|
signal,
|
||||||
operator,
|
operator,
|
||||||
mobileData,
|
mobileData,
|
||||||
|
sim: this.modem.getCachedSimStatus?.() || null,
|
||||||
uptime: process.uptime()
|
uptime: process.uptime()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -398,11 +399,14 @@ class APIServer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await this.modem.sendSMS(phone, message);
|
const result = await this.modem.sendSMS(phone, message);
|
||||||
|
const success = Boolean(result?.success);
|
||||||
|
const simLabel = result?.simLabel || '未知SIM';
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success,
|
success,
|
||||||
message: success ? '短信发送成功' : '短信发送失败'
|
message: success ? `短信发送成功,发送SIM: ${simLabel}` : `短信发送失败,发送SIM: ${simLabel}`,
|
||||||
|
data: result
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('发送短信失败:', err);
|
logger.error('发送短信失败:', err);
|
||||||
@@ -416,10 +420,12 @@ class APIServer {
|
|||||||
// 查询收到的短信
|
// 查询收到的短信
|
||||||
this.app.get('/api/sms/received', async (req, res) => {
|
this.app.get('/api/sms/received', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const currentSimIdentity = await this.smsProcessor.getCurrentSimIdentity();
|
await this.smsProcessor.getCurrentSimIdentity();
|
||||||
|
const currentSimIdentity = this.smsProcessor.getCurrentModemSimIdentity();
|
||||||
const result = this.smsProcessor.listReceivedMessages(req.query.limit, {
|
const result = this.smsProcessor.listReceivedMessages(req.query.limit, {
|
||||||
currentSimIdentity,
|
currentSimIdentity,
|
||||||
scope: req.query.scope
|
scope: req.query.scope,
|
||||||
|
simSlot: req.query.simSlot ?? req.query.slot
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -456,14 +462,17 @@ class APIServer {
|
|||||||
// 查询模组信息
|
// 查询模组信息
|
||||||
this.app.get('/api/modem/info', async (req, res) => {
|
this.app.get('/api/modem/info', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const iccid = await this.modem.getICCID({ refresh: true });
|
const simStatus = this.modem.getCachedSimStatus?.();
|
||||||
const simIdentity = this.smsProcessor.setCurrentSimFromICCID(iccid);
|
const currentSlot = simStatus?.slots?.find(slot => slot.active);
|
||||||
|
const simIdentity = this.smsProcessor.setCurrentSimIdentity(
|
||||||
|
this.smsProcessor.getCurrentModemSimIdentity()
|
||||||
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
model: this.modem.modelInfo,
|
model: this.modem.modelInfo,
|
||||||
iccid,
|
phoneNumber: currentSlot?.phoneNumber || '',
|
||||||
simId: simIdentity?.simId || null,
|
simId: simIdentity?.simId || null,
|
||||||
simLabel: simIdentity?.simLabel || '未知SIM',
|
simLabel: simIdentity?.simLabel || '未知SIM',
|
||||||
ready: this.modem.ready
|
ready: this.modem.ready
|
||||||
@@ -478,6 +487,50 @@ class APIServer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 查询SIM状态和双卡能力
|
||||||
|
this.app.get('/api/modem/sim', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const sim = await this.modem.getSimStatus({
|
||||||
|
refreshIdentity: req.query.refresh !== 'false'
|
||||||
|
});
|
||||||
|
this.smsProcessor.setCurrentSimIdentity(this.smsProcessor.getCurrentModemSimIdentity());
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: sim
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('查询SIM状态失败:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 切换当前SIM卡槽
|
||||||
|
this.app.post('/api/modem/sim/switch', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const slot = req.body?.slot ?? req.body?.sim;
|
||||||
|
const result = await this.modem.switchSim(slot);
|
||||||
|
const currentSlot = result.status.slots.find(item => item.active);
|
||||||
|
this.smsProcessor.setCurrentSimIdentity(this.smsProcessor.getCurrentModemSimIdentity());
|
||||||
|
const currentLabel = currentSlot?.phoneLabel || currentSlot?.phoneNumber || currentSlot?.name || '目标SIM';
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: result.changed ? `已绑定并切到${currentLabel}` : `${currentLabel}已绑定并切换`,
|
||||||
|
data: result.status
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('切换SIM失败:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 查询移动数据连接状态
|
// 查询移动数据连接状态
|
||||||
this.app.get('/api/modem/mobile-data', async (req, res) => {
|
this.app.get('/api/modem/mobile-data', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ async function main() {
|
|||||||
logger.info('配置加载完成');
|
logger.info('配置加载完成');
|
||||||
|
|
||||||
// 创建模组管理器
|
// 创建模组管理器
|
||||||
const modem = new ModemManager(config.serial, config.mobileData);
|
const modem = new ModemManager(config.serial, config.mobileData, config);
|
||||||
|
|
||||||
// 创建长短信管理器
|
// 创建长短信管理器
|
||||||
const concatManager = new ConcatManager();
|
const concatManager = new ConcatManager();
|
||||||
|
|||||||
+844
-93
File diff suppressed because it is too large
Load Diff
+196
-72
@@ -27,13 +27,126 @@ class SMSProcessor {
|
|||||||
this.loadReceivedMessages();
|
this.loadReceivedMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeIncomingPduEvent(input) {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
return {
|
||||||
|
pdu: input,
|
||||||
|
metadata: {
|
||||||
|
type: 'CMT',
|
||||||
|
raw: '',
|
||||||
|
simSlot: null,
|
||||||
|
receivedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input && typeof input === 'object') {
|
||||||
|
return {
|
||||||
|
pdu: String(input.pdu || input.pduHex || ''),
|
||||||
|
metadata: {
|
||||||
|
...(input.metadata || {}),
|
||||||
|
simSlot: this.normalizeSimSlot(input.metadata?.simSlot ?? input.simSlot),
|
||||||
|
receivedAt: input.metadata?.receivedAt || input.receivedAt || new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pdu: '',
|
||||||
|
metadata: {
|
||||||
|
type: 'CMT',
|
||||||
|
raw: '',
|
||||||
|
simSlot: null,
|
||||||
|
receivedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveIncomingSimIdentity({ metadata = {} } = {}) {
|
||||||
|
const urcSlot = this.normalizeSimSlot(metadata.simSlot);
|
||||||
|
if (urcSlot !== null) {
|
||||||
|
return this.createSimIdentityFromSlot(urcSlot, {
|
||||||
|
simSource: metadata.simSource || 'urc'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const identified = this.modem?.identifyIncomingSms?.(metadata);
|
||||||
|
if (identified?.slot !== undefined && identified?.slot !== null) {
|
||||||
|
return this.createSimIdentityFromSlot(identified.slot, {
|
||||||
|
simSource: identified.source || 'slot'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const directSource = metadata.simSource || (metadata.type === 'CMT' || metadata.type === 'CMTI' ? 'urc' : '');
|
||||||
|
if (directSource) {
|
||||||
|
return {
|
||||||
|
simId: crypto.createHash('sha256').update(`unknown-${directSource}`).digest('hex').slice(0, 24),
|
||||||
|
simLabel: '未知SIM',
|
||||||
|
simSlot: null,
|
||||||
|
simSource: directSource,
|
||||||
|
smsCenter: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
createSimIdentityFromSlot(slotNumber, extra = {}) {
|
||||||
|
const slot = this.normalizeSimSlot(slotNumber);
|
||||||
|
if (slot === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const simStatus = this.modem?.getCachedSimStatus?.();
|
||||||
|
const slotInfo = simStatus?.slots?.find(item => item.slot === slot);
|
||||||
|
const name = slotInfo?.phoneLabel || slotInfo?.phoneNumber || slotInfo?.name || `SIM${slot + 1}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
simId: crypto.createHash('sha256').update(`slot:${slot}`).digest('hex').slice(0, 24),
|
||||||
|
simLabel: name,
|
||||||
|
simSlot: slot,
|
||||||
|
simSource: extra.simSource || 'slot',
|
||||||
|
smsCenter: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
normalizeSimSlot(value) {
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const normalized = value.trim().toUpperCase();
|
||||||
|
if (normalized === 'SIM1') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (normalized === 'SIM2') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const slot = Number.parseInt(value, 10);
|
||||||
|
return slot === 0 || slot === 1 ? slot : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatSimSource(source) {
|
||||||
|
const labels = {
|
||||||
|
urc: 'URC上报',
|
||||||
|
bind_context: '绑定上下文',
|
||||||
|
active_context: '当前卡槽',
|
||||||
|
slot: '卡槽',
|
||||||
|
current_slot: '当前卡槽'
|
||||||
|
};
|
||||||
|
|
||||||
|
return labels[source] || '未知';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理接收到的PDU短信
|
* 处理接收到的PDU短信
|
||||||
*/
|
*/
|
||||||
async processPDU(pduHex) {
|
async processPDU(input) {
|
||||||
try {
|
try {
|
||||||
logger.info('开始解析PDU数据...');
|
logger.info('开始解析PDU数据...');
|
||||||
|
|
||||||
|
const smsEvent = this.normalizeIncomingPduEvent(input);
|
||||||
|
const pduHex = smsEvent.pdu;
|
||||||
|
|
||||||
// 解析PDU
|
// 解析PDU
|
||||||
const parsed = parse(pduHex);
|
const parsed = parse(pduHex);
|
||||||
|
|
||||||
@@ -47,12 +160,15 @@ class SMSProcessor {
|
|||||||
const text = parsed.data?.getText?.() || '';
|
const text = parsed.data?.getText?.() || '';
|
||||||
const part = parsed.data?.parts?.[0];
|
const part = parsed.data?.parts?.[0];
|
||||||
const header = part?.header;
|
const header = part?.header;
|
||||||
const simIdentity = await this.getCurrentSimIdentity();
|
const simIdentity = this.resolveIncomingSimIdentity({
|
||||||
|
metadata: smsEvent.metadata
|
||||||
|
});
|
||||||
|
|
||||||
logger.info('✓ PDU解析成功');
|
logger.info('✓ PDU解析成功');
|
||||||
logger.info(`发送者: ${sender}`);
|
logger.info(`发送者: ${sender}`);
|
||||||
logger.info(`时间戳: ${timestamp}`);
|
logger.info(`时间戳: ${timestamp}`);
|
||||||
logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`);
|
logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`);
|
||||||
|
logger.info(`识别方式: ${this.formatSimSource(simIdentity?.simSource)}`);
|
||||||
logger.info(`内容: ${text}`);
|
logger.info(`内容: ${text}`);
|
||||||
|
|
||||||
// 检查是否为长短信
|
// 检查是否为长短信
|
||||||
@@ -95,6 +211,7 @@ class SMSProcessor {
|
|||||||
logger.info(`发送者: ${sender}`);
|
logger.info(`发送者: ${sender}`);
|
||||||
logger.info(`时间戳: ${timestamp}`);
|
logger.info(`时间戳: ${timestamp}`);
|
||||||
logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`);
|
logger.info(`收件SIM: ${simIdentity?.simLabel || '未知SIM'}`);
|
||||||
|
logger.info(`识别方式: ${this.formatSimSource(simIdentity?.simSource)}`);
|
||||||
logger.info(`内容: ${text}`);
|
logger.info(`内容: ${text}`);
|
||||||
logger.info('====================');
|
logger.info('====================');
|
||||||
|
|
||||||
@@ -127,6 +244,9 @@ class SMSProcessor {
|
|||||||
receivedAt: new Date().toISOString(),
|
receivedAt: new Date().toISOString(),
|
||||||
simId: normalizedSimIdentity?.simId || '',
|
simId: normalizedSimIdentity?.simId || '',
|
||||||
simLabel: normalizedSimIdentity?.simLabel || '未知SIM',
|
simLabel: normalizedSimIdentity?.simLabel || '未知SIM',
|
||||||
|
simSlot: normalizedSimIdentity?.simSlot ?? null,
|
||||||
|
simSource: normalizedSimIdentity?.simSource || 'unknown',
|
||||||
|
smsCenter: normalizedSimIdentity?.smsCenter || '',
|
||||||
status: 'received',
|
status: 'received',
|
||||||
statusText: '已接收'
|
statusText: '已接收'
|
||||||
};
|
};
|
||||||
@@ -164,16 +284,26 @@ class SMSProcessor {
|
|||||||
*/
|
*/
|
||||||
listReceivedMessages(limit = 50, options = {}) {
|
listReceivedMessages(limit = 50, options = {}) {
|
||||||
const safeLimit = this.resolveReceivedMessagesLimit(limit);
|
const safeLimit = this.resolveReceivedMessagesLimit(limit);
|
||||||
const requestedScope = String(options.scope || 'current').trim().toLowerCase();
|
const requestedSimSlot = this.normalizeSimSlot(options.simSlot);
|
||||||
|
const hasRequestedScope = options.scope !== undefined && options.scope !== null && String(options.scope).trim() !== '';
|
||||||
|
const requestedScope = requestedSimSlot !== null
|
||||||
|
? `sim${requestedSimSlot + 1}`
|
||||||
|
: hasRequestedScope
|
||||||
|
? String(options.scope).trim().toLowerCase()
|
||||||
|
: (this.shouldDefaultToAllSimMessages() ? 'all' : 'current');
|
||||||
const currentSim = this.normalizeSimIdentity(options.currentSimIdentity);
|
const currentSim = this.normalizeSimIdentity(options.currentSimIdentity);
|
||||||
const includeAllSims = requestedScope === 'all' && this.allowAllSimMessages;
|
const autoAllSims = !hasRequestedScope && requestedScope === 'all';
|
||||||
|
const includeAllSims = requestedSimSlot === null && requestedScope === 'all';
|
||||||
|
|
||||||
let effectiveScope = includeAllSims ? 'all' : 'current';
|
let effectiveScope = includeAllSims ? 'all' : 'current';
|
||||||
let messages = this.receivedMessages;
|
let messages = this.receivedMessages;
|
||||||
|
|
||||||
if (this.partitionMessagesBySim && !includeAllSims) {
|
if (requestedSimSlot !== null) {
|
||||||
|
messages = this.receivedMessages.filter(message => this.normalizeSimSlot(message.simSlot) === requestedSimSlot);
|
||||||
|
effectiveScope = `sim${requestedSimSlot + 1}`;
|
||||||
|
} else if (this.partitionMessagesBySim && !includeAllSims) {
|
||||||
if (currentSim?.simId) {
|
if (currentSim?.simId) {
|
||||||
messages = this.receivedMessages.filter(message => message.simId === currentSim.simId);
|
messages = this.receivedMessages.filter(message => this.messageMatchesSim(message, currentSim));
|
||||||
} else {
|
} else {
|
||||||
messages = [];
|
messages = [];
|
||||||
effectiveScope = 'unavailable';
|
effectiveScope = 'unavailable';
|
||||||
@@ -190,14 +320,38 @@ class SMSProcessor {
|
|||||||
scope: effectiveScope,
|
scope: effectiveScope,
|
||||||
requestedScope,
|
requestedScope,
|
||||||
partitionBySim: this.partitionMessagesBySim,
|
partitionBySim: this.partitionMessagesBySim,
|
||||||
allSimMessagesAllowed: this.allowAllSimMessages,
|
allSimMessagesAllowed: this.allowAllSimMessages || includeAllSims || autoAllSims,
|
||||||
allSimMessagesDenied: this.partitionMessagesBySim && requestedScope === 'all' && !this.allowAllSimMessages,
|
allSimMessagesDenied: false,
|
||||||
|
simSlot: requestedSimSlot,
|
||||||
currentSim,
|
currentSim,
|
||||||
currentSimKnown: Boolean(currentSim?.simId)
|
currentSimKnown: Boolean(currentSim?.simId)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldDefaultToAllSimMessages() {
|
||||||
|
if (this.config.inbox?.dualSimDefaultScope === 'current') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const simStatus = this.modem?.getCachedSimStatus?.();
|
||||||
|
return simStatus?.supported === true && (simStatus.mode === 0 || simStatus.mode === 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
messageMatchesSim(message, currentSim) {
|
||||||
|
if (!message || !currentSim) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.simId && currentSim.simId && message.simId === currentSim.simId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageSlot = this.normalizeSimSlot(message.simSlot);
|
||||||
|
const currentSlot = this.normalizeSimSlot(currentSim.simSlot);
|
||||||
|
return messageSlot !== null && currentSlot !== null && messageSlot === currentSlot;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 限制收件箱查询条数。
|
* 限制收件箱查询条数。
|
||||||
*/
|
*/
|
||||||
@@ -319,6 +473,9 @@ class SMSProcessor {
|
|||||||
receivedAt: String(item.receivedAt || new Date().toISOString()),
|
receivedAt: String(item.receivedAt || new Date().toISOString()),
|
||||||
simId: simIdentity?.simId || '',
|
simId: simIdentity?.simId || '',
|
||||||
simLabel: simIdentity?.simLabel || '未知SIM',
|
simLabel: simIdentity?.simLabel || '未知SIM',
|
||||||
|
simSlot: simIdentity?.simSlot ?? null,
|
||||||
|
simSource: simIdentity?.simSource || item.simSource || 'unknown',
|
||||||
|
smsCenter: '',
|
||||||
status: String(item.status || 'received'),
|
status: String(item.status || 'received'),
|
||||||
statusText: String(item.statusText || '已接收')
|
statusText: String(item.statusText || '已接收')
|
||||||
};
|
};
|
||||||
@@ -331,7 +488,9 @@ class SMSProcessor {
|
|||||||
if (item.simId || item.simLabel) {
|
if (item.simId || item.simLabel) {
|
||||||
return this.normalizeSimIdentity({
|
return this.normalizeSimIdentity({
|
||||||
simId: item.simId,
|
simId: item.simId,
|
||||||
simLabel: item.simLabel
|
simLabel: item.simLabel,
|
||||||
|
simSlot: item.simSlot,
|
||||||
|
simSource: item.simSource
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,10 +498,6 @@ class SMSProcessor {
|
|||||||
return this.normalizeSimIdentity(item.sim);
|
return this.normalizeSimIdentity(item.sim);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.simIccid || item.iccid) {
|
|
||||||
return this.createSimIdentityFromICCID(item.simIccid || item.iccid);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,34 +516,31 @@ class SMSProcessor {
|
|||||||
return this.currentSimLookup;
|
return this.currentSimLookup;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.modem?.getICCID) {
|
if (this.modem?.isSimSwitching?.()) {
|
||||||
this.currentSimIdentity = null;
|
return this.currentSimIdentity;
|
||||||
this.currentSimCheckedAt = now;
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentSimLookup = (async () => {
|
const modemSim = this.getCurrentModemSimIdentity();
|
||||||
try {
|
if (modemSim?.simId) {
|
||||||
const iccid = await this.modem.getICCID({ refresh: true });
|
return this.setCurrentSimIdentity(modemSim);
|
||||||
return this.setCurrentSimFromICCID(iccid);
|
}
|
||||||
} catch (err) {
|
|
||||||
logger.warn(`查询当前SIM标识失败: ${err.message}`);
|
|
||||||
this.currentSimIdentity = null;
|
|
||||||
this.currentSimCheckedAt = Date.now();
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
this.currentSimLookup = null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
return this.currentSimLookup;
|
this.currentSimIdentity = null;
|
||||||
|
this.currentSimCheckedAt = now;
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
getCurrentModemSimIdentity() {
|
||||||
* 使用ICCID更新当前SIM身份缓存。
|
const simStatus = this.modem?.getCachedSimStatus?.();
|
||||||
*/
|
const activeSlot = simStatus?.activeSlot;
|
||||||
setCurrentSimFromICCID(iccid) {
|
if (activeSlot === null || activeSlot === undefined) {
|
||||||
return this.setCurrentSimIdentity(this.createSimIdentityFromICCID(iccid));
|
return this.currentSimIdentity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identity = this.createSimIdentityFromSlot(activeSlot, {
|
||||||
|
simSource: 'current_slot'
|
||||||
|
});
|
||||||
|
return identity || this.currentSimIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -400,59 +552,31 @@ class SMSProcessor {
|
|||||||
return this.currentSimIdentity;
|
return this.currentSimIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据ICCID生成稳定但不暴露完整卡号的SIM标识。
|
|
||||||
*/
|
|
||||||
createSimIdentityFromICCID(iccid) {
|
|
||||||
const normalized = this.normalizeICCID(iccid);
|
|
||||||
if (!normalized) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
simId: crypto.createHash('sha256').update(`iccid:${normalized}`).digest('hex').slice(0, 24),
|
|
||||||
simLabel: this.formatSimLabel(normalized)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeICCID(iccid) {
|
|
||||||
return String(iccid || '').replace(/\D/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
formatSimLabel(iccid) {
|
|
||||||
const value = String(iccid || '');
|
|
||||||
if (!value) {
|
|
||||||
return '未知SIM';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `ICCID ...${value.slice(-6)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeSimIdentity(identity) {
|
normalizeSimIdentity(identity) {
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof identity === 'string') {
|
if (typeof identity === 'string') {
|
||||||
return this.createSimIdentityFromICCID(identity);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof identity !== 'object' || Array.isArray(identity)) {
|
if (typeof identity !== 'object' || Array.isArray(identity)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identity.iccid || identity.simIccid) {
|
const simSlot = this.normalizeSimSlot(identity.simSlot ?? identity.slot);
|
||||||
return this.createSimIdentityFromICCID(identity.iccid || identity.simIccid);
|
|
||||||
}
|
|
||||||
|
|
||||||
const simId = String(identity.simId || identity.id || '').trim();
|
const simId = String(identity.simId || identity.id || '').trim();
|
||||||
if (!simId) {
|
if (!simId && simSlot === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
simId,
|
simId: simId || crypto.createHash('sha256').update(`slot:${simSlot}`).digest('hex').slice(0, 24),
|
||||||
simLabel: String(identity.simLabel || identity.label || '未知SIM')
|
simLabel: String(identity.simLabel || identity.label || (simSlot === null ? '未知SIM' : `SIM${simSlot + 1}`)),
|
||||||
|
simSlot,
|
||||||
|
simSource: String(identity.simSource || identity.source || 'unknown'),
|
||||||
|
smsCenter: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user