dsh-life-pack 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.js +380 -163
- package/dist/client.js.map +1 -1
- package/dist/dsh-ctx.d.ts +9 -5
- package/dist/dsh-ctx.d.ts.map +1 -1
- package/dist/health-fetch.d.ts +5 -12
- package/dist/health-fetch.d.ts.map +1 -1
- package/dist/health-fetch.js +12 -3
- package/dist/health-fetch.js.map +1 -1
- package/dist/health-panel.d.ts +6 -0
- package/dist/health-panel.d.ts.map +1 -1
- package/dist/health-panel.js +13 -0
- package/dist/health-panel.js.map +1 -1
- package/dist/health-view.d.ts +46 -10
- package/dist/health-view.d.ts.map +1 -1
- package/dist/health-view.js +111 -81
- package/dist/health-view.js.map +1 -1
- package/dist/manager-version.d.ts +7 -0
- package/dist/manager-version.d.ts.map +1 -0
- package/dist/manager-version.js +62 -0
- package/dist/manager-version.js.map +1 -0
- package/dist/nav.d.ts +18 -2
- package/dist/nav.d.ts.map +1 -1
- package/dist/nav.js +10 -8
- package/dist/nav.js.map +1 -1
- package/dist/update-client.d.ts +5 -0
- package/dist/update-client.d.ts.map +1 -1
- package/dist/update-client.js +16 -4
- package/dist/update-client.js.map +1 -1
- package/dist/update-contract.d.ts +18 -0
- package/dist/update-contract.d.ts.map +1 -1
- package/dist/update-contract.js +19 -1
- package/dist/update-contract.js.map +1 -1
- package/dist/update-host.d.ts.map +1 -1
- package/dist/update-host.js +4 -0
- package/dist/update-host.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -38,42 +38,48 @@ window.__ModuleLoader__.load({
|
|
|
38
38
|
slotId: "ilife:memo",
|
|
39
39
|
order: 70,
|
|
40
40
|
title: "备忘录",
|
|
41
|
-
plugin: "dsh-memo-ilife"
|
|
41
|
+
plugin: "dsh-memo-ilife",
|
|
42
|
+
channel: "/ilife-memo"
|
|
42
43
|
},
|
|
43
44
|
{
|
|
44
45
|
skill: "calorie",
|
|
45
46
|
slotId: "ilife:calorie",
|
|
46
47
|
order: 75,
|
|
47
48
|
title: "卡路里",
|
|
48
|
-
plugin: "dsh-calorie"
|
|
49
|
+
plugin: "dsh-calorie",
|
|
50
|
+
channel: "/ilife-calorie"
|
|
49
51
|
},
|
|
50
52
|
{
|
|
51
53
|
skill: "schedule",
|
|
52
54
|
slotId: "ilife:schedule",
|
|
53
55
|
order: 80,
|
|
54
|
-
title: "
|
|
55
|
-
plugin: "dsh-schedule-ilife"
|
|
56
|
+
title: "作息管家",
|
|
57
|
+
plugin: "dsh-schedule-ilife",
|
|
58
|
+
channel: "/ilife-schedule-ilife"
|
|
56
59
|
},
|
|
57
60
|
{
|
|
58
61
|
skill: "home",
|
|
59
62
|
slotId: "ilife:home",
|
|
60
63
|
order: 85,
|
|
61
|
-
title: "
|
|
62
|
-
plugin: "dsh-home-ilife"
|
|
64
|
+
title: "居家管家",
|
|
65
|
+
plugin: "dsh-home-ilife",
|
|
66
|
+
channel: "/ilife-home-ilife"
|
|
63
67
|
},
|
|
64
68
|
{
|
|
65
69
|
skill: "chef",
|
|
66
70
|
slotId: "ilife:chef",
|
|
67
71
|
order: 90,
|
|
68
|
-
title: "
|
|
69
|
-
plugin: "dsh-chef"
|
|
72
|
+
title: "私家大厨",
|
|
73
|
+
plugin: "dsh-chef",
|
|
74
|
+
channel: "/ilife-chef"
|
|
70
75
|
},
|
|
71
76
|
{
|
|
72
77
|
skill: "bill",
|
|
73
78
|
slotId: "ilife:cookie",
|
|
74
79
|
order: 95,
|
|
75
|
-
title: "
|
|
76
|
-
plugin: "dsh-bill-ilife"
|
|
80
|
+
title: "饼干记账",
|
|
81
|
+
plugin: "dsh-bill-ilife",
|
|
82
|
+
channel: "/ilife-bill-ilife"
|
|
77
83
|
}
|
|
78
84
|
];
|
|
79
85
|
function recoFor(tab) {
|
|
@@ -139,10 +145,17 @@ window.__ModuleLoader__.load({
|
|
|
139
145
|
* 一处定义、两边引用(结构纪律「概念唯一」):名字改了只有这一行要动。
|
|
140
146
|
* 宿主判据为什么要读这个字面量、以及它的边界,见 `readPanelRegistered` 的头注。 */
|
|
141
147
|
const CONFIG_TAB_SLOT = "ilife.config-tab";
|
|
148
|
+
/** 载体基段:`connection.rpc.call` 的**第一段**参数,宿主注册路径与它同源(`'/api' + 通道名`)。
|
|
149
|
+
*
|
|
150
|
+
* 一处定义(#735):总管自己的电话、以及面板到各家的每一通电话,第一段都必须是它。
|
|
151
|
+
* 真机实现(`dsh-client-connection/lib/client.js`):URL = `${第一段}/${第二段}`、信封 `method` = 第二段。
|
|
152
|
+
* 传错第一段就是 404:总管自己的电话在 #678 栽过一次,各家的配置体检在 #735 又栽过一次
|
|
153
|
+
* (那次传的是通道名 `/ilife-<技能>`,拼出来的地址上没有注册过那条路由,六家全 404)。 */
|
|
154
|
+
const CARRIER_BASE = "/api";
|
|
142
155
|
/** 载体:宿主注册 `connection.fetch.register` 的路径 / 面板 `connection.rpc.call` 的两段参数。 */
|
|
143
156
|
const MANAGER_RPC = {
|
|
144
157
|
/** 载体基段(`connection.rpc.call` 的**第一段**参数)。传错第一段就是 404:真机上踩过一次。 */
|
|
145
|
-
base:
|
|
158
|
+
base: CARRIER_BASE,
|
|
146
159
|
/** 单段通道名,须 match `/^\/[A-Za-z0-9._~-]+$/`(cookbook §6)。 */
|
|
147
160
|
channel: "/ilife-manager",
|
|
148
161
|
/** 面板侧第二段参数(等于通道名去掉前导斜杠那一段)。 */
|
|
@@ -160,8 +173,19 @@ window.__ModuleLoader__.load({
|
|
|
160
173
|
* (`update.phoneNames`,`host.ts:335`);面板侧既不能 import 更新包(浏览器产物纯度门)
|
|
161
174
|
* 也不该把名字再写一份(写两份必然走散)。宿主把名字表连同版本行一起交出去,
|
|
162
175
|
* 面板一行字面量都不留。 */
|
|
163
|
-
targets: "ilife-manager.targets"
|
|
176
|
+
targets: "ilife-manager.targets",
|
|
177
|
+
/** 总管自述版本(票 #737):入参 `{}`,回包 `{version}`。
|
|
178
|
+
*
|
|
179
|
+
* 宿主读**自己这份已安装包**的 `package.json`(`manager-version.ts`),面板只渲染读到的值。
|
|
180
|
+
* 为什么走电话:面板是浏览器产物,禁 node 内建(`test/client-bundle-48.test.mjs` 看门),
|
|
181
|
+
* 读盘只许在宿主半;与卡路里 #130 同一条路(host 读 → RPC → client 纯渲染)。 */
|
|
182
|
+
version: "ilife-manager.version"
|
|
164
183
|
};
|
|
184
|
+
/** 版本读不到时两侧共用的降级字面量(面板照原样显示,不假装知道版本)。
|
|
185
|
+
*
|
|
186
|
+
* 一处定义、两侧引用:宿主半 `manager-version.ts` 读失败时回它,面板半 `update-client.ts`
|
|
187
|
+
* 归一化时也认它——面板侧不许 import 宿主半(会把 node 内建带进浏览器束)。 */
|
|
188
|
+
const VERSION_UNKNOWN = "unknown";
|
|
165
189
|
/** 装不了 / 装不成的人话原因表:八种 `blockedReason` + 过程错误码(更新包 README 第 8、12 节)。 */
|
|
166
190
|
const REASON_TEXT = {
|
|
167
191
|
"unknown-profile": "使用范围认不出(名字非法或目录不存在):先检查使用范围名与目录,这种情形不给手工命令。",
|
|
@@ -187,14 +211,15 @@ window.__ModuleLoader__.load({
|
|
|
187
211
|
}
|
|
188
212
|
//#endregion
|
|
189
213
|
//#region src/update-client.ts
|
|
190
|
-
/**
|
|
214
|
+
/** 面板到宿主的四条流程(纯异步函数:无 DOM、无 react;传输口由调用方注入,便于单测)。
|
|
191
215
|
*
|
|
192
|
-
*
|
|
216
|
+
* 四条流程与更新包 README 的对应关系:
|
|
193
217
|
* - 检查更新:`updateCheck`(第 3 节第 3 步的 `host.call(UPD_CHECK, {})`);
|
|
194
218
|
* - 装上更新:`updateCheck` 拿凭证 → `updateInstall` 提交 → **按间隔轮询 `updateStatus`**
|
|
195
219
|
* 直到任务离开 `installing`/`verifying`(第 3 节第 3 步与第 9 节:安装是后台跑,
|
|
196
220
|
* 调用方不干等;凭证过期 `check-expired` 是正常错误码,重查一次再提交,见第 11 节);
|
|
197
|
-
* - 装上缺席的包:走总管自有的 `ilife-manager.install
|
|
221
|
+
* - 装上缺席的包:走总管自有的 `ilife-manager.install`(更新包的三个电话只管已装的包);
|
|
222
|
+
* - 总管自述版本:走总管自有的 `ilife-manager.version`(宿主读自己那份包描述文件,见 `manager-version.ts`)。
|
|
198
223
|
*/
|
|
199
224
|
/** 轮询上限:1 秒一次、最多 900 次(覆盖更新包 15 分钟的安装时限)。 */
|
|
200
225
|
const MAX_POLL_TRIES = 900;
|
|
@@ -253,6 +278,16 @@ window.__ModuleLoader__.load({
|
|
|
253
278
|
}
|
|
254
279
|
};
|
|
255
280
|
}
|
|
281
|
+
/** 取总管自述版本(票 #737):宿主读**自己这份已安装包**的 `package.json`,面板只渲染读到的值。
|
|
282
|
+
*
|
|
283
|
+
* 单次、无轮询;宿主读不到会回 `unknown`,传输层失败也回同一个字面量——面板照原样显示,
|
|
284
|
+
* 不假装知道版本(与卡路里 #130 的降级口径一致)。 */
|
|
285
|
+
async function loadManagerVersion(call) {
|
|
286
|
+
const out = await callManager(call, MANAGER_ACTIONS.version, {});
|
|
287
|
+
if (!out.ok) return VERSION_UNKNOWN;
|
|
288
|
+
const raw = out.value?.version;
|
|
289
|
+
return typeof raw === "string" && raw.trim().length > 0 ? raw.trim() : VERSION_UNKNOWN;
|
|
290
|
+
}
|
|
256
291
|
/** 查一家:默认查新版(`updateCheck`,顺带回最新版本与凭证);给 `phoneName` 时查那个电话。 */
|
|
257
292
|
async function checkTarget(call, target, phoneName) {
|
|
258
293
|
const name = phoneName ?? target.phones?.check;
|
|
@@ -494,17 +529,40 @@ window.__ModuleLoader__.load({
|
|
|
494
529
|
marginBottom: 8,
|
|
495
530
|
borderRadius: 10,
|
|
496
531
|
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))",
|
|
532
|
+
borderLeftWidth: 3,
|
|
497
533
|
fontSize: 13,
|
|
498
534
|
lineHeight: 1.7
|
|
499
535
|
},
|
|
500
536
|
rowHead: {
|
|
501
|
-
|
|
537
|
+
display: "flex",
|
|
538
|
+
alignItems: "baseline",
|
|
539
|
+
gap: 6,
|
|
502
540
|
marginBottom: 2
|
|
503
541
|
},
|
|
542
|
+
rowTitle: {
|
|
543
|
+
fontWeight: 700,
|
|
544
|
+
color: "var(--dsw-alias-label-primary, inherit)"
|
|
545
|
+
},
|
|
546
|
+
rowPkg: {
|
|
547
|
+
color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
|
|
548
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
549
|
+
fontSize: 12
|
|
550
|
+
},
|
|
504
551
|
rowMeta: {
|
|
505
552
|
color: "var(--dsw-alias-label-secondary, #9a9a9a)",
|
|
506
553
|
fontSize: 12
|
|
507
554
|
},
|
|
555
|
+
/** 版本号用等宽字:数字要能对齐着读(「当前」「最新」两列一眼比大小)。 */
|
|
556
|
+
ver: {
|
|
557
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
558
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
559
|
+
fontWeight: 600
|
|
560
|
+
},
|
|
561
|
+
/** 结论那一行:**只有要动的那几档上色**(绿/灰不抢注意力,照 health-view 的三档口径)。 */
|
|
562
|
+
verdictLine: {
|
|
563
|
+
fontSize: 12.5,
|
|
564
|
+
marginTop: 2
|
|
565
|
+
},
|
|
508
566
|
reason: {
|
|
509
567
|
color: "var(--dsw-alias-state-error-primary, #ff6b6b)",
|
|
510
568
|
fontSize: 12,
|
|
@@ -513,7 +571,26 @@ window.__ModuleLoader__.load({
|
|
|
513
571
|
skill: {
|
|
514
572
|
color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
|
|
515
573
|
fontSize: 12,
|
|
516
|
-
marginTop: 4
|
|
574
|
+
marginTop: 4,
|
|
575
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace"
|
|
576
|
+
},
|
|
577
|
+
/** 行内主按钮(装上/装上更新):品牌底色 —— 全屏只有「能点的」这一种是这个颜色。 */
|
|
578
|
+
btnPrimary: {
|
|
579
|
+
border: "1px solid transparent",
|
|
580
|
+
background: "var(--dsw-alias-brand-primary, #0a84ff)",
|
|
581
|
+
color: "#fff",
|
|
582
|
+
borderRadius: 8,
|
|
583
|
+
padding: "5px 14px",
|
|
584
|
+
fontSize: 13,
|
|
585
|
+
fontWeight: 700,
|
|
586
|
+
cursor: "pointer",
|
|
587
|
+
marginTop: 6
|
|
588
|
+
},
|
|
589
|
+
/** 分档标记:**形状是第二条读数**(灰度截图、色弱视角下也分得开哪一行要动)。 */
|
|
590
|
+
mark: {
|
|
591
|
+
fontWeight: 700,
|
|
592
|
+
fontSize: 13,
|
|
593
|
+
lineHeight: 1
|
|
517
594
|
},
|
|
518
595
|
overlay: {
|
|
519
596
|
position: "fixed",
|
|
@@ -525,12 +602,14 @@ window.__ModuleLoader__.load({
|
|
|
525
602
|
padding: 16,
|
|
526
603
|
background: "rgba(0,0,0,.45)"
|
|
527
604
|
},
|
|
605
|
+
/** 浮层是**竖排两段**:题头固定、结果列表自己滚(题头跟着滚会把关闭键滚出视野 —— 用户实测反馈)。 */
|
|
528
606
|
dialog: {
|
|
529
607
|
width: "100%",
|
|
530
608
|
maxWidth: 560,
|
|
531
609
|
maxHeight: "80vh",
|
|
532
|
-
|
|
533
|
-
|
|
610
|
+
display: "flex",
|
|
611
|
+
flexDirection: "column",
|
|
612
|
+
overflow: "hidden",
|
|
534
613
|
borderRadius: 12,
|
|
535
614
|
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.4))",
|
|
536
615
|
background: "var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #222))",
|
|
@@ -542,12 +621,21 @@ window.__ModuleLoader__.load({
|
|
|
542
621
|
alignItems: "center",
|
|
543
622
|
justifyContent: "space-between",
|
|
544
623
|
gap: 8,
|
|
545
|
-
|
|
624
|
+
flex: "0 0 auto",
|
|
625
|
+
padding: "12px 14px",
|
|
626
|
+
borderBottom: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))"
|
|
546
627
|
},
|
|
547
628
|
dialogTitle: {
|
|
548
629
|
fontSize: 14,
|
|
549
630
|
fontWeight: 700
|
|
550
631
|
},
|
|
632
|
+
/** 唯一会滚的那一段。 */
|
|
633
|
+
dialogBody: {
|
|
634
|
+
flex: "1 1 auto",
|
|
635
|
+
minHeight: 0,
|
|
636
|
+
overflowY: "auto",
|
|
637
|
+
padding: "12px 14px"
|
|
638
|
+
},
|
|
551
639
|
close: {
|
|
552
640
|
border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
|
|
553
641
|
background: "transparent",
|
|
@@ -565,6 +653,45 @@ window.__ModuleLoader__.load({
|
|
|
565
653
|
marginTop: 4
|
|
566
654
|
}
|
|
567
655
|
};
|
|
656
|
+
const ROW_TONE = {
|
|
657
|
+
update: {
|
|
658
|
+
mark: "↑",
|
|
659
|
+
color: "var(--dsw-alias-brand-primary, #0a84ff)",
|
|
660
|
+
tint: "rgba(10,132,255,.08)"
|
|
661
|
+
},
|
|
662
|
+
blocked: {
|
|
663
|
+
mark: "!",
|
|
664
|
+
color: "var(--dsw-alias-state-warning-primary, #d8a300)",
|
|
665
|
+
tint: "rgba(216,163,0,.10)"
|
|
666
|
+
},
|
|
667
|
+
failed: {
|
|
668
|
+
mark: "✕",
|
|
669
|
+
color: "var(--dsw-alias-state-error-primary, #ff6b6b)",
|
|
670
|
+
tint: "rgba(255,107,107,.10)"
|
|
671
|
+
},
|
|
672
|
+
ok: {
|
|
673
|
+
mark: "✓",
|
|
674
|
+
color: "var(--dsw-alias-state-success-primary, #4ec9a0)",
|
|
675
|
+
tint: "transparent"
|
|
676
|
+
},
|
|
677
|
+
idle: {
|
|
678
|
+
mark: "·",
|
|
679
|
+
color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
|
|
680
|
+
tint: "transparent"
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
/** 这一行算哪一档:取数失败 > 装不上 > 有新版 > 待重启/源码装 > 已是最新 > 未查。 */
|
|
684
|
+
function rowToneOf(row, verdict) {
|
|
685
|
+
if (row.failure !== null || row.phase === "failed") return "failed";
|
|
686
|
+
if (row.phase === "installing") return "update";
|
|
687
|
+
if (verdict === null) return "idle";
|
|
688
|
+
switch (verdict.kind) {
|
|
689
|
+
case "update-available": return "update";
|
|
690
|
+
case "blocked": return "blocked";
|
|
691
|
+
case "up-to-date": return "ok";
|
|
692
|
+
default: return "idle";
|
|
693
|
+
}
|
|
694
|
+
}
|
|
568
695
|
const IDLE$1 = {
|
|
569
696
|
phase: "idle",
|
|
570
697
|
outcome: null,
|
|
@@ -749,18 +876,43 @@ window.__ModuleLoader__.load({
|
|
|
749
876
|
const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
|
|
750
877
|
const busy = row.phase === "installing";
|
|
751
878
|
const showManual = manual !== null && (row.phase === "failed" || verdict?.kind === "blocked");
|
|
879
|
+
const tone = rowToneOf(row, verdict);
|
|
880
|
+
const paint = ROW_TONE[tone];
|
|
881
|
+
/** 这一行「当前/最新」两个版本号:不等才算有新版,把「最新」那格上色(用户扫一眼先看有没有箭头)。 */
|
|
882
|
+
const running = snapshot?.runningVersion ?? target.runningVersion ?? null;
|
|
883
|
+
const latest = snapshot?.latestVersion ?? null;
|
|
884
|
+
const newer = running !== null && latest !== null && latest !== running;
|
|
885
|
+
const lit = tone === "update" || tone === "blocked" || tone === "failed";
|
|
752
886
|
return react.createElement("div", {
|
|
753
887
|
key: target.key,
|
|
754
|
-
style:
|
|
755
|
-
|
|
888
|
+
style: {
|
|
889
|
+
...PANEL_STYLE.row,
|
|
890
|
+
borderLeftColor: paint.color,
|
|
891
|
+
background: paint.tint
|
|
892
|
+
}
|
|
893
|
+
}, react.createElement("div", { style: PANEL_STYLE.rowHead }, react.createElement("span", {
|
|
894
|
+
style: {
|
|
895
|
+
...PANEL_STYLE.mark,
|
|
896
|
+
color: paint.color
|
|
897
|
+
},
|
|
898
|
+
"aria-hidden": "true"
|
|
899
|
+
}, paint.mark), react.createElement("span", { style: PANEL_STYLE.rowTitle }, target.title), react.createElement("span", { style: PANEL_STYLE.rowPkg }, "· " + target.packageName)), react.createElement("div", { style: PANEL_STYLE.rowMeta }, "当前 ", react.createElement("b", { style: PANEL_STYLE.ver }, String(running ?? "未安装")), " · 最新 ", react.createElement("b", { style: newer ? {
|
|
900
|
+
...PANEL_STYLE.ver,
|
|
901
|
+
color: paint.color
|
|
902
|
+
} : PANEL_STYLE.ver }, String(latest ?? "未查"))), react.createElement("div", { style: lit ? {
|
|
903
|
+
...PANEL_STYLE.verdictLine,
|
|
904
|
+
color: paint.color,
|
|
905
|
+
fontWeight: 600
|
|
906
|
+
} : {
|
|
907
|
+
...PANEL_STYLE.verdictLine,
|
|
908
|
+
color: "var(--dsw-alias-label-secondary, #9a9a9a)"
|
|
909
|
+
} }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), verdict?.action ? react.createElement("button", {
|
|
756
910
|
type: "button",
|
|
757
911
|
style: busy ? {
|
|
758
|
-
...PANEL_STYLE.
|
|
759
|
-
opacity: .55
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
marginTop: 6
|
|
763
|
-
},
|
|
912
|
+
...PANEL_STYLE.btnPrimary,
|
|
913
|
+
opacity: .55,
|
|
914
|
+
cursor: "default"
|
|
915
|
+
} : PANEL_STYLE.btnPrimary,
|
|
764
916
|
disabled: busy,
|
|
765
917
|
onClick: () => props.face.act(target)
|
|
766
918
|
}, verdict.action === "install" ? "装上" : "装上更新") : null, row.failure ? react.createElement("div", { style: PANEL_STYLE.reason }, "装不上:" + row.failure.message) : null, versionLines(target, snapshot).map((line, index) => react.createElement("div", {
|
|
@@ -782,13 +934,13 @@ window.__ModuleLoader__.load({
|
|
|
782
934
|
onKeyDown: (event) => {
|
|
783
935
|
if (event.key === "Escape") close();
|
|
784
936
|
}
|
|
785
|
-
}, react.createElement("div", { style: PANEL_STYLE.dialogHead }, react.createElement("div", { style: PANEL_STYLE.dialogTitle }, "
|
|
937
|
+
}, react.createElement("div", { style: PANEL_STYLE.dialogHead }, react.createElement("div", { style: PANEL_STYLE.dialogTitle }, "检查更新"), react.createElement("button", {
|
|
786
938
|
type: "button",
|
|
787
939
|
style: PANEL_STYLE.close,
|
|
788
940
|
onClick: () => close(),
|
|
789
941
|
title: "关闭",
|
|
790
942
|
"aria-label": "关闭"
|
|
791
|
-
}, "×")), body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。"))) : null);
|
|
943
|
+
}, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。")))) : null);
|
|
792
944
|
}
|
|
793
945
|
/** 缺席卡:**按两处独立事实分三态**,每态只给能改变状态的动作。
|
|
794
946
|
*
|
|
@@ -881,12 +1033,20 @@ window.__ModuleLoader__.load({
|
|
|
881
1033
|
/** 面板到各家的配置体检:一次取数、六份报告(纯异步函数:无 DOM、无 react;传输口注入,便于单测)。
|
|
882
1034
|
*
|
|
883
1035
|
* 三条纪律:
|
|
884
|
-
* ·
|
|
885
|
-
*
|
|
1036
|
+
* · **只走各家自己的电话**(`rpc.call(CARRIER_BASE, <电话名>, {method, payload}, …)`)——总管不认任何
|
|
1037
|
+
* 一家的端点、也不加任何单品的依赖;电话名与端点名都是各家契约件公开的事实。
|
|
1038
|
+
* ⚠ **载体形状**(#735 真机踩到):第一段必须是**载体基段** `/api`,第二段是那家的电话名
|
|
1039
|
+
* (=它注册在 `/api<通道>` 上的那一段),端点名写进载荷的 `method`。写错第一段就是 404,
|
|
1040
|
+
* 而 404 只落成「那一家取不到数」,屏上看起来像按钮坏了(先例:总管自己的电话在 #678 栽过同一处,
|
|
1041
|
+
* 见 `update-contract.ts` 的 `CARRIER_BASE`)。
|
|
886
1042
|
* · **一次取数,六份报告**:总览那行与各家那张表读的是同一份结果(见 `health-panel.ts` 的单一状态源),
|
|
887
1043
|
* 不许两处各算一遍。
|
|
888
1044
|
* · 有界超时:一家不回不该拖死整屏(技能侧 spawn 上限 20 s,这里给 25 s)。
|
|
889
1045
|
*/
|
|
1046
|
+
/** 一家的电话名:通道名去掉前导斜杠那一段(各家宿主半注册在 `/api` + 通道名,信封 `method` 就是它)。 */
|
|
1047
|
+
function phoneOf(channel) {
|
|
1048
|
+
return channel.startsWith("/") ? channel.slice(1) : channel;
|
|
1049
|
+
}
|
|
890
1050
|
/** 一次取数的时限(毫秒):技能侧 spawn 上限 20 s,这里留一点余量。 */
|
|
891
1051
|
const HEALTH_TIMEOUT_MS = 25e3;
|
|
892
1052
|
function timeoutText() {
|
|
@@ -894,7 +1054,10 @@ window.__ModuleLoader__.load({
|
|
|
894
1054
|
}
|
|
895
1055
|
async function fetchOne(call, row) {
|
|
896
1056
|
try {
|
|
897
|
-
const result = await call(row.channel,
|
|
1057
|
+
const result = await call(CARRIER_BASE, phoneOf(row.channel), {
|
|
1058
|
+
method: HEALTH_ENDPOINT,
|
|
1059
|
+
payload: {}
|
|
1060
|
+
}, AbortSignal.timeout(HEALTH_TIMEOUT_MS));
|
|
898
1061
|
if (!result || typeof result !== "object" || typeof result.ok !== "boolean") return {
|
|
899
1062
|
report: null,
|
|
900
1063
|
error: "回执信封异常(非 ok 信封)。"
|
|
@@ -1001,6 +1164,17 @@ window.__ModuleLoader__.load({
|
|
|
1001
1164
|
} catch {}
|
|
1002
1165
|
return face;
|
|
1003
1166
|
}
|
|
1167
|
+
/** 汇总行该显示哪一句错(票 #735):取数过程本身的错优先;**一家报告都没回来**时,把第一条家错误顶上来。
|
|
1168
|
+
*
|
|
1169
|
+
* 为什么要有这一格:各家的错只画在那家页签里的体检表上,一行摘要看不见 ⇒ 用户点完只见「体检中…」闪一下、
|
|
1170
|
+
* 接着仍旧「还没体检」,会以为按钮坏了(真机上就是这么被误判的;那次六家全 404,汇总行一声不吭)。
|
|
1171
|
+
* 有报告回来时不顶:那时各家那张表里已经把错说清楚了,摘要行只管读数。 */
|
|
1172
|
+
function summaryErrorOf(face) {
|
|
1173
|
+
if (face.error !== null) return face.error;
|
|
1174
|
+
const rows = Object.values(face.rows);
|
|
1175
|
+
if (rows.some((row) => row.report !== null)) return null;
|
|
1176
|
+
return rows.find((row) => row.error !== null)?.error ?? null;
|
|
1177
|
+
}
|
|
1004
1178
|
//#endregion
|
|
1005
1179
|
//#region src/health-view.ts
|
|
1006
1180
|
/** 配置体检的两个画法:面板顶部那行总览(六家一盏灯)+ 各家那张表。
|
|
@@ -1070,45 +1244,40 @@ window.__ModuleLoader__.load({
|
|
|
1070
1244
|
fontWeight: 700,
|
|
1071
1245
|
color: INK
|
|
1072
1246
|
},
|
|
1073
|
-
/**
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
border: "1px solid " + BORDER,
|
|
1077
|
-
background: "transparent",
|
|
1078
|
-
color: INK,
|
|
1079
|
-
borderRadius: 8,
|
|
1080
|
-
padding: "6px 16px",
|
|
1081
|
-
fontSize: 13,
|
|
1082
|
-
fontWeight: 600,
|
|
1083
|
-
cursor: "pointer"
|
|
1084
|
-
},
|
|
1085
|
-
lightsRow: {
|
|
1247
|
+
/** 顶部那一行汇总(票 #732):一行装下「配置体检 + 一句总账 + 体检按钮」,三样都不独占行。
|
|
1248
|
+
* 它替掉了原先「标题行 + 单独一行的按钮 + 一排灯按钮 + 两行图例」(同屏实测 153px → 一行)。 */
|
|
1249
|
+
summaryRow: {
|
|
1086
1250
|
display: "flex",
|
|
1087
|
-
flexWrap: "wrap",
|
|
1088
|
-
gap: 8
|
|
1089
|
-
},
|
|
1090
|
-
light: {
|
|
1091
|
-
display: "inline-flex",
|
|
1092
1251
|
alignItems: "center",
|
|
1093
|
-
gap:
|
|
1094
|
-
|
|
1095
|
-
border: "1px solid " + BORDER,
|
|
1096
|
-
borderRadius: 999,
|
|
1097
|
-
padding: "3px 10px",
|
|
1098
|
-
fontSize: 12.5,
|
|
1099
|
-
color: INK,
|
|
1100
|
-
cursor: "pointer"
|
|
1252
|
+
gap: 10,
|
|
1253
|
+
marginBottom: 10
|
|
1101
1254
|
},
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1255
|
+
summaryText: {
|
|
1256
|
+
flex: "1 1 auto",
|
|
1257
|
+
minWidth: 0,
|
|
1258
|
+
fontSize: 12.5,
|
|
1259
|
+
whiteSpace: "nowrap",
|
|
1260
|
+
overflow: "hidden",
|
|
1261
|
+
textOverflow: "ellipsis"
|
|
1105
1262
|
},
|
|
1263
|
+
/** 各家那张表里的说明行(「还没体检:点上面那个…」那几句)。 */
|
|
1106
1264
|
meta: {
|
|
1107
1265
|
color: INK_DIM,
|
|
1108
1266
|
fontSize: 12,
|
|
1109
1267
|
lineHeight: 1.7,
|
|
1110
1268
|
marginTop: 12
|
|
1111
1269
|
},
|
|
1270
|
+
btn: {
|
|
1271
|
+
border: "1px solid " + BORDER,
|
|
1272
|
+
background: "transparent",
|
|
1273
|
+
color: INK,
|
|
1274
|
+
borderRadius: 8,
|
|
1275
|
+
flex: "0 0 auto",
|
|
1276
|
+
padding: "3px 10px",
|
|
1277
|
+
fontSize: 12.5,
|
|
1278
|
+
fontWeight: 600,
|
|
1279
|
+
cursor: "pointer"
|
|
1280
|
+
},
|
|
1112
1281
|
error: {
|
|
1113
1282
|
color: STATUS_COLOR.red,
|
|
1114
1283
|
fontSize: 12,
|
|
@@ -1192,11 +1361,12 @@ window.__ModuleLoader__.load({
|
|
|
1192
1361
|
id: tab.id,
|
|
1193
1362
|
title: tab.title,
|
|
1194
1363
|
status: report ? worstStatus(items) : null,
|
|
1195
|
-
counts: countByStatus(items)
|
|
1364
|
+
counts: countByStatus(items),
|
|
1365
|
+
hasChannel: tab.hasChannel ?? true
|
|
1196
1366
|
};
|
|
1197
1367
|
});
|
|
1198
1368
|
}
|
|
1199
|
-
/** 一家的一句话读数:「红 1 黄 2
|
|
1369
|
+
/** 一家的一句话读数:「红 1 黄 2」,全绿就是「绿」。面板侧要**上色**的那几处不用它(见 `countSegments`)。 */
|
|
1200
1370
|
function countsText(counts) {
|
|
1201
1371
|
const parts = [];
|
|
1202
1372
|
if (counts.red > 0) parts.push("红 " + String(counts.red));
|
|
@@ -1204,25 +1374,57 @@ window.__ModuleLoader__.load({
|
|
|
1204
1374
|
if (parts.length === 0) return "绿";
|
|
1205
1375
|
return parts.join(" · ");
|
|
1206
1376
|
}
|
|
1207
|
-
/**
|
|
1208
|
-
*
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1377
|
+
/** 把一份计数拆成**按档分色**的几截。红黄各自一截、各自上色,互不串色;
|
|
1378
|
+
* 「黄 3」这一截为什么不能被染成红——见 `countSegments` 的注释(出图复评逮到的真缺陷)。 */
|
|
1379
|
+
function countSegsOf(light) {
|
|
1380
|
+
if (light.status === null) return [{
|
|
1381
|
+
status: null,
|
|
1382
|
+
text: "—"
|
|
1383
|
+
}];
|
|
1384
|
+
const segs = [];
|
|
1385
|
+
for (const status of ["red", "yellow"]) {
|
|
1386
|
+
const count = light.counts[status];
|
|
1387
|
+
if (count > 0) segs.push({
|
|
1388
|
+
status,
|
|
1389
|
+
text: STATUS_LABEL[status] + " " + String(count)
|
|
1390
|
+
});
|
|
1391
|
+
}
|
|
1392
|
+
return segs.length > 0 ? segs : [{
|
|
1393
|
+
status: "green",
|
|
1394
|
+
text: "绿"
|
|
1395
|
+
}];
|
|
1396
|
+
}
|
|
1397
|
+
/** 几截读数之间那个分隔符:不许染档位色(它不属于任何一档)。 */
|
|
1398
|
+
const SEG_SEP = " · ";
|
|
1399
|
+
/** 汇总行那一句:绿要显眼(它是「正常」这件事本身),没通道的那几家不冒充绿。
|
|
1400
|
+
*
|
|
1401
|
+
* 「没有体检出口」与「还没跑过体检」是**两件事**(票 #735):前者那颗「体检一次」按下去不会有任何反应,
|
|
1402
|
+
* 必须自己说出来——两态共用一句「还没体检」时,用户与诊断都会被引到「点了没跑」上去。 */
|
|
1403
|
+
function overallText(lights) {
|
|
1404
|
+
const pending = lights.filter((light) => !light.hasChannel).length;
|
|
1405
|
+
const withChannel = lights.filter((light) => light.hasChannel);
|
|
1406
|
+
const tail = pending > 0 ? " · " + String(pending) + " 家没有体检出口" : "";
|
|
1407
|
+
if (withChannel.length === 0) {
|
|
1408
|
+
const what = lights.length === 0 ? "还没有任何一家登记体检出口" : String(lights.length) + " 家都没有体检出口(通道名没登记上)";
|
|
1409
|
+
return react.createElement("span", { style: {
|
|
1410
|
+
color: STATUS_TEXT.red,
|
|
1215
1411
|
fontWeight: 700
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1412
|
+
} }, "没有可体检的家:" + what);
|
|
1413
|
+
}
|
|
1414
|
+
const allGreen = withChannel.every((light) => light.status === "green");
|
|
1415
|
+
if (allGreen && pending === 0) return react.createElement("span", { style: {
|
|
1416
|
+
color: STATUS_COLOR.green,
|
|
1417
|
+
fontWeight: 700
|
|
1418
|
+
} }, "六家正常");
|
|
1218
1419
|
const parts = [];
|
|
1219
1420
|
for (const status of ["red", "yellow"]) {
|
|
1220
|
-
|
|
1421
|
+
let count = 0;
|
|
1422
|
+
for (const light of lights) count += light.counts[status];
|
|
1221
1423
|
if (count === 0) continue;
|
|
1222
1424
|
if (parts.length > 0) parts.push(react.createElement("span", {
|
|
1223
1425
|
key: status + "-sep",
|
|
1224
1426
|
style: { color: INK_DIM }
|
|
1225
|
-
},
|
|
1427
|
+
}, SEG_SEP));
|
|
1226
1428
|
parts.push(react.createElement("span", {
|
|
1227
1429
|
key: status,
|
|
1228
1430
|
style: {
|
|
@@ -1231,21 +1433,54 @@ window.__ModuleLoader__.load({
|
|
|
1231
1433
|
}
|
|
1232
1434
|
}, STATUS_LABEL[status] + " " + String(count)));
|
|
1233
1435
|
}
|
|
1234
|
-
if (parts.length === 0) return
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
}, "绿")];
|
|
1241
|
-
return parts;
|
|
1436
|
+
if (parts.length === 0) return react.createElement("span", { style: { color: INK_DIM } }, (allGreen ? "有出口的都正常" : "还没体检") + tail);
|
|
1437
|
+
parts.push(react.createElement("span", {
|
|
1438
|
+
key: "todo",
|
|
1439
|
+
style: { color: INK }
|
|
1440
|
+
}, " 要处理" + tail));
|
|
1441
|
+
return react.createElement("span", null, parts);
|
|
1242
1442
|
}
|
|
1243
|
-
/**
|
|
1244
|
-
|
|
1443
|
+
/** 那枚小数字用的底色(比灯色浅一档,压在页签上不抢眼)。 */
|
|
1444
|
+
const STATUS_DOT = {
|
|
1445
|
+
red: STATUS_COLOR.red,
|
|
1446
|
+
yellow: STATUS_COLOR.yellow,
|
|
1447
|
+
green: STATUS_COLOR.green
|
|
1448
|
+
};
|
|
1449
|
+
/** 页签圆点的颜色与语义(票 #732):从「实心/空心=装没装」改成「颜色=体检档位」。 */
|
|
1450
|
+
const TAB_DOT = {
|
|
1451
|
+
/** 没装(页签槽账本里没有这一家)。 */
|
|
1452
|
+
absent: STATUS_DOT.green,
|
|
1453
|
+
/** 装了、但还没跑过体检。 */
|
|
1454
|
+
unchecked: INK_DIM
|
|
1455
|
+
};
|
|
1456
|
+
/** 页签圆点该用什么色:装了按档位(红黄绿),没装/没跑过走缺席态。 */
|
|
1457
|
+
function tabDotColor(light) {
|
|
1458
|
+
if (!light.hasChannel) return TAB_DOT.absent;
|
|
1459
|
+
return light.status === null ? TAB_DOT.unchecked : STATUS_DOT[light.status];
|
|
1460
|
+
}
|
|
1461
|
+
/** 页签上那枚小数字(票 #732):红黄那几家,家名后带一条计数;全绿与没跑过不带。 */
|
|
1462
|
+
const TAB_NOTE_STYLE = {
|
|
1463
|
+
marginLeft: 5,
|
|
1464
|
+
border: "1px solid " + BORDER,
|
|
1465
|
+
borderRadius: 999,
|
|
1466
|
+
padding: "0 5px",
|
|
1467
|
+
fontSize: 11,
|
|
1468
|
+
fontWeight: 700
|
|
1469
|
+
};
|
|
1470
|
+
/** 那枚小数字写什么:只报「要处理」的那两档;一家里红黄都有时先报红那一档(圆点已经按最严重的档上色了)。 */
|
|
1471
|
+
function tabNote(light) {
|
|
1472
|
+
if (!light.hasChannel || light.status === null || light.status === "green") return null;
|
|
1473
|
+
return countSegsOf(light)[0] ?? null;
|
|
1474
|
+
}
|
|
1475
|
+
/** 顶部那一行汇总(票 #732:不再一家一盏灯按钮,一行总账 + 一个体检按钮)。
|
|
1476
|
+
*
|
|
1477
|
+
* 与页签的分工:这行只答「一共几条要处理」;「哪几家、各几条」由页签的圆点与小数字答;
|
|
1478
|
+
* 「具体哪一条坏了、去哪修」由那家页签里的 `HealthTable` 答。 */
|
|
1479
|
+
function HealthSummaryLine(props) {
|
|
1245
1480
|
return react.createElement("div", {
|
|
1246
|
-
style: HEALTH_STYLE.
|
|
1247
|
-
"data-ilife-health": "
|
|
1248
|
-
}, react.createElement("div", { style: HEALTH_STYLE.
|
|
1481
|
+
style: HEALTH_STYLE.summaryRow,
|
|
1482
|
+
"data-ilife-health": "summary"
|
|
1483
|
+
}, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检"), react.createElement("div", { style: HEALTH_STYLE.summaryText }, props.error !== null ? react.createElement("span", { style: { color: STATUS_TEXT.red } }, props.error) : overallText(props.lights)), react.createElement("button", {
|
|
1249
1484
|
type: "button",
|
|
1250
1485
|
style: props.running ? {
|
|
1251
1486
|
...HEALTH_STYLE.btn,
|
|
@@ -1254,63 +1489,8 @@ window.__ModuleLoader__.load({
|
|
|
1254
1489
|
} : HEALTH_STYLE.btn,
|
|
1255
1490
|
disabled: props.running,
|
|
1256
1491
|
onClick: props.onRun,
|
|
1257
|
-
title: "
|
|
1258
|
-
}, props.running ? "体检中…" : "体检一次"))
|
|
1259
|
-
style: HEALTH_STYLE.lightsRow,
|
|
1260
|
-
"data-ilife-health": "lights"
|
|
1261
|
-
}, props.lights.map((light) => react.createElement("button", {
|
|
1262
|
-
key: light.id,
|
|
1263
|
-
type: "button",
|
|
1264
|
-
"data-ilife-health": "light",
|
|
1265
|
-
"data-skill": light.id,
|
|
1266
|
-
style: light.status === null ? {
|
|
1267
|
-
...HEALTH_STYLE.light,
|
|
1268
|
-
...HEALTH_STYLE.lightIdle
|
|
1269
|
-
} : {
|
|
1270
|
-
...HEALTH_STYLE.light,
|
|
1271
|
-
borderColor: STATUS_COLOR[light.status],
|
|
1272
|
-
borderWidth: 1.5,
|
|
1273
|
-
background: STATUS_TINT[light.status]
|
|
1274
|
-
},
|
|
1275
|
-
disabled: light.status === null,
|
|
1276
|
-
onClick: () => props.onJump(light.id),
|
|
1277
|
-
title: light.status === null ? "还没体检(或这一家没装)" : light.title + ":" + countsText(light.counts)
|
|
1278
|
-
}, react.createElement("span", { style: {
|
|
1279
|
-
width: 10,
|
|
1280
|
-
height: 10,
|
|
1281
|
-
borderRadius: "50%",
|
|
1282
|
-
background: light.status === null ? INK_DIM : STATUS_COLOR[light.status],
|
|
1283
|
-
display: "inline-block"
|
|
1284
|
-
} }), react.createElement("span", { style: {
|
|
1285
|
-
color: INK,
|
|
1286
|
-
fontWeight: 650
|
|
1287
|
-
} }, light.title), ...countSegments(light)))), react.createElement("div", { style: HEALTH_STYLE.meta }, props.error !== null ? props.error : [
|
|
1288
|
-
react.createElement("span", {
|
|
1289
|
-
key: "r",
|
|
1290
|
-
style: {
|
|
1291
|
-
color: STATUS_TEXT.red,
|
|
1292
|
-
fontWeight: 700
|
|
1293
|
-
}
|
|
1294
|
-
}, "红"),
|
|
1295
|
-
react.createElement("span", { key: "r2" }, "=坏了 · "),
|
|
1296
|
-
react.createElement("span", {
|
|
1297
|
-
key: "y",
|
|
1298
|
-
style: {
|
|
1299
|
-
color: STATUS_TEXT.yellow,
|
|
1300
|
-
fontWeight: 700
|
|
1301
|
-
}
|
|
1302
|
-
}, "黄"),
|
|
1303
|
-
react.createElement("span", { key: "y2" }, "=还没配 · "),
|
|
1304
|
-
react.createElement("span", {
|
|
1305
|
-
key: "g",
|
|
1306
|
-
style: {
|
|
1307
|
-
color: STATUS_TEXT.green,
|
|
1308
|
-
fontWeight: 700
|
|
1309
|
-
}
|
|
1310
|
-
}, "绿"),
|
|
1311
|
-
react.createElement("span", { key: "g2" }, "=正常 | 只看不改:不建目录、不改配置、不自动重置"),
|
|
1312
|
-
react.createElement("span", { key: "tab" }, " · 上面那排圆点是体检灯,点一下跳到那家配置页;下面那排圆点是页签(实心=装上了)")
|
|
1313
|
-
]));
|
|
1492
|
+
title: "对六家各跑一次配置体检(只看不改:不建目录、不改配置、不自动重置)"
|
|
1493
|
+
}, props.running ? "体检中…" : "体检一次"));
|
|
1314
1494
|
}
|
|
1315
1495
|
/** 正正常常的那些:绿条只留「名字」,一句话与「去哪修」都不印(票面验收第一条:正常的收成一行)。 */
|
|
1316
1496
|
function isAttentionItem(item) {
|
|
@@ -1420,6 +1600,9 @@ window.__ModuleLoader__.load({
|
|
|
1420
1600
|
* 文案与网址都取自 nav.ts 的两张静态表,本文件只画不算。
|
|
1421
1601
|
*/
|
|
1422
1602
|
const inject = ["slots", "connection"];
|
|
1603
|
+
/** 包名 → 本家那条客户端通道(票 #735)。取值面是导航表那份镜像,不再从页签槽账本的自定义选项里读:
|
|
1604
|
+
* 装机槽位面不透传自定义键,读了恒是空串(详见 `nav.ts` 上 `ManagerTab.channel` 的注释)。 */
|
|
1605
|
+
const CHANNEL_BY_PLUGIN = new Map(MANAGER_TABS.map((tab) => [tab.plugin, tab.channel]));
|
|
1423
1606
|
/** 总管视觉(内联 style;颜色走 DSH 主题别名,深浅主题自适应,写死值只做回退;与技能面板同语言)。 */
|
|
1424
1607
|
const S = {
|
|
1425
1608
|
headRow: {
|
|
@@ -1597,6 +1780,23 @@ window.__ModuleLoader__.load({
|
|
|
1597
1780
|
style: S.moreRow
|
|
1598
1781
|
}, react.createElement("span", { style: S.morePkg }, row.pkg), react.createElement("span", { style: S.moreDesc }, row.desc), react.createElement("span", { style: S.moreIcon }, react.createElement(ExternalIcon, null)))));
|
|
1599
1782
|
}
|
|
1783
|
+
/** 总管自述版本(票 #737):**不手写**,问宿主——宿主读自己这份已安装包的 `package.json`。
|
|
1784
|
+
*
|
|
1785
|
+
* 单次取数(`loadManagerVersion`),无轮询;初值 `unknown`,读不到也停在 `unknown`。
|
|
1786
|
+
* 与卡路里 #130 同一条路:面板是浏览器产物、禁 node 内建,读盘只许在宿主半。 */
|
|
1787
|
+
function useManagerVersion(getCall) {
|
|
1788
|
+
const [version, setVersion] = react.useState(VERSION_UNKNOWN);
|
|
1789
|
+
react.useEffect(() => {
|
|
1790
|
+
let alive = true;
|
|
1791
|
+
loadManagerVersion(getCall()).then((next) => {
|
|
1792
|
+
if (alive) setVersion(next);
|
|
1793
|
+
});
|
|
1794
|
+
return () => {
|
|
1795
|
+
alive = false;
|
|
1796
|
+
};
|
|
1797
|
+
}, [getCall]);
|
|
1798
|
+
return version;
|
|
1799
|
+
}
|
|
1600
1800
|
/** 爱生活面板:总设置区 + 检查更新(七家)+ 爱生活页签条(slot 驱动)+ 技能设置页投影/缺席卡。 */
|
|
1601
1801
|
function LifePackSection(props) {
|
|
1602
1802
|
const tabsId = react.useId();
|
|
@@ -1604,10 +1804,18 @@ window.__ModuleLoader__.load({
|
|
|
1604
1804
|
const rows = props.useTabs((value) => value);
|
|
1605
1805
|
const present = new Set(rows.map((r) => r.id));
|
|
1606
1806
|
const face = useUpdateRows(props.getCall);
|
|
1807
|
+
const managerVersion = useManagerVersion(props.getCall);
|
|
1607
1808
|
const health = useHealthPanel(() => props.getCall(), react.useMemo(() => rows.filter((row) => row.channel.length > 0).map((row) => ({
|
|
1608
1809
|
id: row.id,
|
|
1609
1810
|
channel: row.channel
|
|
1610
1811
|
})), [rows]));
|
|
1812
|
+
/** 六家的灯(票 #732):按**页签槽账本**那一排过(装了但没体检通道的也在内,它显缺席态)。 */
|
|
1813
|
+
const lights = react.useMemo(() => lightsOf(rows.map((row) => ({
|
|
1814
|
+
id: row.id,
|
|
1815
|
+
title: row.label,
|
|
1816
|
+
hasChannel: row.channel.length > 0
|
|
1817
|
+
})), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))), [rows, health.rows]);
|
|
1818
|
+
const lightOf = (id) => lights.find((light) => light.id === id);
|
|
1611
1819
|
const [activeId, setActiveId] = react.useState(void 0);
|
|
1612
1820
|
const [visitedIds, setVisitedIds] = react.useState(() => /* @__PURE__ */ new Set());
|
|
1613
1821
|
const active = MANAGER_TABS.some((t) => t.plugin === activeId) ? activeId : MANAGER_TABS[0].plugin;
|
|
@@ -1621,13 +1829,6 @@ window.__ModuleLoader__.load({
|
|
|
1621
1829
|
const row = rows.find((r) => r.id === tab.plugin);
|
|
1622
1830
|
return row && row.label.length > 0 ? row.label : tab.title;
|
|
1623
1831
|
}
|
|
1624
|
-
/** 按包名取页签标题(总览那行按账本行过,账本里没有的退到导航表的标题)。 */
|
|
1625
|
-
function labelForTab(id) {
|
|
1626
|
-
const row = rows.find((r) => r.id === id);
|
|
1627
|
-
if (row && row.label.length > 0) return row.label;
|
|
1628
|
-
const tab = MANAGER_TABS.find((t) => t.plugin === id);
|
|
1629
|
-
return tab ? tab.title : id;
|
|
1630
|
-
}
|
|
1631
1832
|
function targetForTab(tab) {
|
|
1632
1833
|
return face.targets.find((t) => t.packageName === tab.plugin) ?? null;
|
|
1633
1834
|
}
|
|
@@ -1653,21 +1854,20 @@ window.__ModuleLoader__.load({
|
|
|
1653
1854
|
setActiveId(next.plugin);
|
|
1654
1855
|
tabRefs.current[nextIndex]?.focus();
|
|
1655
1856
|
}
|
|
1656
|
-
return react.createElement("div", null, react.createElement("div", { style: S.headRow }, react.createElement("div", { style: S.head }, "爱生活"), react.createElement("div", { style: S.headActions }, react.createElement(CheckUpdateButton, { face }), react.createElement(PanelActions, null))), react.createElement("div", { style: S.meta }, react.createElement("div", null, "
|
|
1657
|
-
lights
|
|
1658
|
-
id: row.id,
|
|
1659
|
-
title: labelForTab(row.id)
|
|
1660
|
-
})), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))),
|
|
1857
|
+
return react.createElement("div", null, react.createElement("div", { style: S.headRow }, react.createElement("div", { style: S.head }, "爱生活"), react.createElement("div", { style: S.headActions }, react.createElement(CheckUpdateButton, { face }), react.createElement(PanelActions, null))), react.createElement("div", { style: S.meta }, react.createElement("div", null, "总管 dsh-life-pack · " + managerVersion)), react.createElement(UpdateResults, { face }), react.createElement(HealthSummaryLine, {
|
|
1858
|
+
lights,
|
|
1661
1859
|
running: health.running,
|
|
1662
|
-
error: health
|
|
1663
|
-
onRun: health.run
|
|
1664
|
-
onJump: setActiveId
|
|
1860
|
+
error: summaryErrorOf(health),
|
|
1861
|
+
onRun: health.run
|
|
1665
1862
|
}), react.createElement("div", {
|
|
1666
1863
|
role: "tablist",
|
|
1667
1864
|
"aria-label": "爱生活技能页签",
|
|
1668
1865
|
style: S.tablist
|
|
1669
1866
|
}, MANAGER_TABS.map((tab, index) => {
|
|
1670
1867
|
const selected = tab.plugin === active;
|
|
1868
|
+
const light = lightOf(tab.plugin);
|
|
1869
|
+
const dotColor = light === void 0 ? present.has(tab.plugin) ? TAB_DOT.unchecked : TAB_DOT.absent : tabDotColor(light);
|
|
1870
|
+
const note = light === void 0 ? null : tabNote(light);
|
|
1671
1871
|
return react.createElement("button", {
|
|
1672
1872
|
key: tab.plugin,
|
|
1673
1873
|
ref: (element) => {
|
|
@@ -1688,8 +1888,25 @@ window.__ModuleLoader__.load({
|
|
|
1688
1888
|
},
|
|
1689
1889
|
onKeyDown: (event) => {
|
|
1690
1890
|
onTabKeyDown(event, index);
|
|
1891
|
+
},
|
|
1892
|
+
title: light === void 0 ? "这一家没装(或产物没注册页签槽)" : light.title + ":" + (light.hasChannel ? light.status === null ? "还没体检" : "体检读数见本页签下方的表" : "没有配置体检出口")
|
|
1893
|
+
}, react.createElement("span", {
|
|
1894
|
+
"data-ilife-health": "tab-dot",
|
|
1895
|
+
style: {
|
|
1896
|
+
width: 6,
|
|
1897
|
+
height: 6,
|
|
1898
|
+
borderRadius: "50%",
|
|
1899
|
+
background: dotColor,
|
|
1900
|
+
display: "inline-block",
|
|
1901
|
+
flex: "0 0 auto"
|
|
1902
|
+
}
|
|
1903
|
+
}), labelFor(tab), note === null ? null : react.createElement("span", {
|
|
1904
|
+
"data-ilife-health": "tab-note",
|
|
1905
|
+
style: {
|
|
1906
|
+
...TAB_NOTE_STYLE,
|
|
1907
|
+
color: note.status === null ? void 0 : STATUS_TEXT[note.status]
|
|
1691
1908
|
}
|
|
1692
|
-
},
|
|
1909
|
+
}, note.text));
|
|
1693
1910
|
})), MANAGER_TABS.filter((tab) => tab.plugin === active || visitedIds.has(tab.plugin)).map((tab) => {
|
|
1694
1911
|
const selected = tab.plugin === active;
|
|
1695
1912
|
return react.createElement("div", {
|
|
@@ -1724,7 +1941,7 @@ window.__ModuleLoader__.load({
|
|
|
1724
1941
|
id: entry.options.id ?? "",
|
|
1725
1942
|
order: entry.options.order ?? 0,
|
|
1726
1943
|
label: resolveLabel(entry.options.label),
|
|
1727
|
-
channel: entry.options.
|
|
1944
|
+
channel: CHANNEL_BY_PLUGIN.get(entry.options.id ?? "") ?? ""
|
|
1728
1945
|
})).sort((a, b) => a.order - b.order);
|
|
1729
1946
|
}
|
|
1730
1947
|
return tabs;
|