dsh-life-pack 0.2.5 → 0.2.7

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 CHANGED
@@ -132,6 +132,13 @@ window.__ModuleLoader__.load({
132
132
  ];
133
133
  //#endregion
134
134
  //#region src/update-contract.ts
135
+ /** 爱生活页签槽名(总管声明 `children`、各单品技能设置页注册进来;单段名,避开官方 `settings.*` 前缀)。
136
+ *
137
+ * 为什么住这里:这个名字**两侧都要用**——面板侧 `client.ts` 拿它注册/投影槽,
138
+ * 宿主侧(`update-env.ts` 的 `readPanelRegistered`)拿它判「已装产物里到底有没有注册代码」。
139
+ * 一处定义、两边引用(结构纪律「概念唯一」):名字改了只有这一行要动。
140
+ * 宿主判据为什么要读这个字面量、以及它的边界,见 `readPanelRegistered` 的头注。 */
141
+ const CONFIG_TAB_SLOT = "ilife.config-tab";
135
142
  /** 载体:宿主注册 `connection.fetch.register` 的路径 / 面板 `connection.rpc.call` 的两段参数。 */
136
143
  const MANAGER_RPC = {
137
144
  /** 载体基段(`connection.rpc.call` 的**第一段**参数)。传错第一段就是 404:真机上踩过一次。 */
@@ -147,7 +154,7 @@ window.__ModuleLoader__.load({
147
154
  const MANAGER_ACTIONS = {
148
155
  /** 装上缺席的目标包:入参 `{packageName, version}`,回包 `{packageName, version}`。 */
149
156
  install: "ilife-manager.install",
150
- /** 七个更新目标的表:入参 `{}`,回包 `{targets: [{key, title, packageName, phones, runningVersion, installedVersion, skill}]}`。
157
+ /** 七个更新目标的表:入参 `{}`,回包 `{targets: [{key, title, packageName, phones, runningVersion, installedVersion, panelRegistered, skill}]}`。
151
158
  *
152
159
  * 电话名为什么由宿主转交而不是面板侧写死:更新包的三个电话名只有它自己知道
153
160
  * (`update.phoneNames`,`host.ts:335`);面板侧既不能 import 更新包(浏览器产物纯度门)
@@ -308,6 +315,13 @@ window.__ModuleLoader__.load({
308
315
  function isAbsent(target) {
309
316
  return target.runningVersion === null && target.installedVersion === null;
310
317
  }
318
+ /** 一家页签此刻的状态:账本里已注册 → `connected`(面板显示它自己的设置页),否则按宿主装机读数分三态。 */
319
+ function slotStateOf(target, inLedger) {
320
+ if (inLedger) return "connected";
321
+ if (target === null || isAbsent(target)) return "absent";
322
+ if (target.panelRegistered === false) return "unregistered-product";
323
+ return "not-connected";
324
+ }
311
325
  /** 把快照翻成一行结论:还没查 / 已是最新 / 有新版 / 装不了(附人话原因)。 */
312
326
  function verdictOf(target, snapshot) {
313
327
  const absent = isAbsent(target);
@@ -480,17 +494,40 @@ window.__ModuleLoader__.load({
480
494
  marginBottom: 8,
481
495
  borderRadius: 10,
482
496
  border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))",
497
+ borderLeftWidth: 3,
483
498
  fontSize: 13,
484
499
  lineHeight: 1.7
485
500
  },
486
501
  rowHead: {
487
- fontWeight: 700,
502
+ display: "flex",
503
+ alignItems: "baseline",
504
+ gap: 6,
488
505
  marginBottom: 2
489
506
  },
507
+ rowTitle: {
508
+ fontWeight: 700,
509
+ color: "var(--dsw-alias-label-primary, inherit)"
510
+ },
511
+ rowPkg: {
512
+ color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
513
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
514
+ fontSize: 12
515
+ },
490
516
  rowMeta: {
491
517
  color: "var(--dsw-alias-label-secondary, #9a9a9a)",
492
518
  fontSize: 12
493
519
  },
520
+ /** 版本号用等宽字:数字要能对齐着读(「当前」「最新」两列一眼比大小)。 */
521
+ ver: {
522
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
523
+ color: "var(--dsw-alias-label-primary, inherit)",
524
+ fontWeight: 600
525
+ },
526
+ /** 结论那一行:**只有要动的那几档上色**(绿/灰不抢注意力,照 health-view 的三档口径)。 */
527
+ verdictLine: {
528
+ fontSize: 12.5,
529
+ marginTop: 2
530
+ },
494
531
  reason: {
495
532
  color: "var(--dsw-alias-state-error-primary, #ff6b6b)",
496
533
  fontSize: 12,
@@ -499,7 +536,26 @@ window.__ModuleLoader__.load({
499
536
  skill: {
500
537
  color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
501
538
  fontSize: 12,
502
- marginTop: 4
539
+ marginTop: 4,
540
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace"
541
+ },
542
+ /** 行内主按钮(装上/装上更新):品牌底色 —— 全屏只有「能点的」这一种是这个颜色。 */
543
+ btnPrimary: {
544
+ border: "1px solid transparent",
545
+ background: "var(--dsw-alias-brand-primary, #0a84ff)",
546
+ color: "#fff",
547
+ borderRadius: 8,
548
+ padding: "5px 14px",
549
+ fontSize: 13,
550
+ fontWeight: 700,
551
+ cursor: "pointer",
552
+ marginTop: 6
553
+ },
554
+ /** 分档标记:**形状是第二条读数**(灰度截图、色弱视角下也分得开哪一行要动)。 */
555
+ mark: {
556
+ fontWeight: 700,
557
+ fontSize: 13,
558
+ lineHeight: 1
503
559
  },
504
560
  overlay: {
505
561
  position: "fixed",
@@ -511,12 +567,14 @@ window.__ModuleLoader__.load({
511
567
  padding: 16,
512
568
  background: "rgba(0,0,0,.45)"
513
569
  },
570
+ /** 浮层是**竖排两段**:题头固定、结果列表自己滚(题头跟着滚会把关闭键滚出视野 —— 用户实测反馈)。 */
514
571
  dialog: {
515
572
  width: "100%",
516
573
  maxWidth: 560,
517
574
  maxHeight: "80vh",
518
- overflow: "auto",
519
- padding: "12px 14px",
575
+ display: "flex",
576
+ flexDirection: "column",
577
+ overflow: "hidden",
520
578
  borderRadius: 12,
521
579
  border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.4))",
522
580
  background: "var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #222))",
@@ -528,12 +586,21 @@ window.__ModuleLoader__.load({
528
586
  alignItems: "center",
529
587
  justifyContent: "space-between",
530
588
  gap: 8,
531
- marginBottom: 8
589
+ flex: "0 0 auto",
590
+ padding: "12px 14px",
591
+ borderBottom: "1px solid var(--dsw-alias-border, rgba(128,128,128,.3))"
532
592
  },
533
593
  dialogTitle: {
534
594
  fontSize: 14,
535
595
  fontWeight: 700
536
596
  },
597
+ /** 唯一会滚的那一段。 */
598
+ dialogBody: {
599
+ flex: "1 1 auto",
600
+ minHeight: 0,
601
+ overflowY: "auto",
602
+ padding: "12px 14px"
603
+ },
537
604
  close: {
538
605
  border: "1px solid var(--dsw-alias-border, rgba(128,128,128,.35))",
539
606
  background: "transparent",
@@ -551,6 +618,45 @@ window.__ModuleLoader__.load({
551
618
  marginTop: 4
552
619
  }
553
620
  };
621
+ const ROW_TONE = {
622
+ update: {
623
+ mark: "↑",
624
+ color: "var(--dsw-alias-brand-primary, #0a84ff)",
625
+ tint: "rgba(10,132,255,.08)"
626
+ },
627
+ blocked: {
628
+ mark: "!",
629
+ color: "var(--dsw-alias-state-warning-primary, #d8a300)",
630
+ tint: "rgba(216,163,0,.10)"
631
+ },
632
+ failed: {
633
+ mark: "✕",
634
+ color: "var(--dsw-alias-state-error-primary, #ff6b6b)",
635
+ tint: "rgba(255,107,107,.10)"
636
+ },
637
+ ok: {
638
+ mark: "✓",
639
+ color: "var(--dsw-alias-state-success-primary, #4ec9a0)",
640
+ tint: "transparent"
641
+ },
642
+ idle: {
643
+ mark: "·",
644
+ color: "var(--dsw-alias-label-tertiary, #8a8a8a)",
645
+ tint: "transparent"
646
+ }
647
+ };
648
+ /** 这一行算哪一档:取数失败 > 装不上 > 有新版 > 待重启/源码装 > 已是最新 > 未查。 */
649
+ function rowToneOf(row, verdict) {
650
+ if (row.failure !== null || row.phase === "failed") return "failed";
651
+ if (row.phase === "installing") return "update";
652
+ if (verdict === null) return "idle";
653
+ switch (verdict.kind) {
654
+ case "update-available": return "update";
655
+ case "blocked": return "blocked";
656
+ case "up-to-date": return "ok";
657
+ default: return "idle";
658
+ }
659
+ }
554
660
  const IDLE$1 = {
555
661
  phase: "idle",
556
662
  outcome: null,
@@ -564,20 +670,23 @@ window.__ModuleLoader__.load({
564
670
  const [loadError, setLoadError] = react.useState(null);
565
671
  const [checking, setChecking] = react.useState(false);
566
672
  const [open, setOpen] = react.useState(false);
567
- react.useEffect(() => {
568
- let alive = true;
569
- (async () => {
570
- const loaded = await loadTargets(getCall());
571
- if (!alive) return;
572
- if (loaded.ok) {
573
- setTargets(loaded.value.targets);
574
- setPollMs(loaded.value.pollMs);
575
- } else setLoadError(loaded.message);
576
- })();
577
- return () => {
578
- alive = false;
579
- };
673
+ const alive = react.useRef(true);
674
+ react.useEffect(() => () => {
675
+ alive.current = false;
676
+ }, []);
677
+ /** 重取七个目标的表(装机读数变了就重取:缺席卡的态是拿这张表算的,缓存住它会继续说旧话)。 */
678
+ const reload = react.useCallback(async () => {
679
+ const loaded = await loadTargets(getCall());
680
+ if (!alive.current) return;
681
+ if (loaded.ok) {
682
+ setTargets(loaded.value.targets);
683
+ setPollMs(loaded.value.pollMs);
684
+ setLoadError(null);
685
+ } else setLoadError(loaded.message);
580
686
  }, [getCall]);
687
+ react.useEffect(() => {
688
+ reload();
689
+ }, [reload]);
581
690
  const patch = react.useCallback((key, next) => {
582
691
  setRows((previous) => ({
583
692
  ...previous,
@@ -658,6 +767,7 @@ window.__ModuleLoader__.load({
658
767
  outcome: null,
659
768
  failure: refreshed
660
769
  });
770
+ await reload();
661
771
  return;
662
772
  }
663
773
  patch(target.key, {
@@ -673,6 +783,7 @@ window.__ModuleLoader__.load({
673
783
  getCall,
674
784
  patch,
675
785
  pollMs,
786
+ reload,
676
787
  rows
677
788
  ])
678
789
  };
@@ -730,18 +841,43 @@ window.__ModuleLoader__.load({
730
841
  const manual = manualForDisplay(target, row.failure?.manual ?? row.outcome?.manual ?? null);
731
842
  const busy = row.phase === "installing";
732
843
  const showManual = manual !== null && (row.phase === "failed" || verdict?.kind === "blocked");
844
+ const tone = rowToneOf(row, verdict);
845
+ const paint = ROW_TONE[tone];
846
+ /** 这一行「当前/最新」两个版本号:不等才算有新版,把「最新」那格上色(用户扫一眼先看有没有箭头)。 */
847
+ const running = snapshot?.runningVersion ?? target.runningVersion ?? null;
848
+ const latest = snapshot?.latestVersion ?? null;
849
+ const newer = running !== null && latest !== null && latest !== running;
850
+ const lit = tone === "update" || tone === "blocked" || tone === "failed";
733
851
  return react.createElement("div", {
734
852
  key: target.key,
735
- style: PANEL_STYLE.row
736
- }, react.createElement("div", { style: PANEL_STYLE.rowHead }, target.title + " · " + target.packageName), react.createElement("div", { style: PANEL_STYLE.rowMeta }, "当前 " + String(snapshot?.runningVersion ?? target.runningVersion ?? "未安装") + " · 最新 " + String(snapshot?.latestVersion ?? "未查")), react.createElement("div", null, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), verdict?.action ? react.createElement("button", {
853
+ style: {
854
+ ...PANEL_STYLE.row,
855
+ borderLeftColor: paint.color,
856
+ background: paint.tint
857
+ }
858
+ }, react.createElement("div", { style: PANEL_STYLE.rowHead }, react.createElement("span", {
859
+ style: {
860
+ ...PANEL_STYLE.mark,
861
+ color: paint.color
862
+ },
863
+ "aria-hidden": "true"
864
+ }, 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 ? {
865
+ ...PANEL_STYLE.ver,
866
+ color: paint.color
867
+ } : PANEL_STYLE.ver }, String(latest ?? "未查"))), react.createElement("div", { style: lit ? {
868
+ ...PANEL_STYLE.verdictLine,
869
+ color: paint.color,
870
+ fontWeight: 600
871
+ } : {
872
+ ...PANEL_STYLE.verdictLine,
873
+ color: "var(--dsw-alias-label-secondary, #9a9a9a)"
874
+ } }, busy ? "正在装,请稍候…" : verdict?.text ?? (checking ? "检查中…" : "")), verdict?.action ? react.createElement("button", {
737
875
  type: "button",
738
876
  style: busy ? {
739
- ...PANEL_STYLE.btn,
740
- opacity: .55
741
- } : {
742
- ...PANEL_STYLE.btn,
743
- marginTop: 6
744
- },
877
+ ...PANEL_STYLE.btnPrimary,
878
+ opacity: .55,
879
+ cursor: "default"
880
+ } : PANEL_STYLE.btnPrimary,
745
881
  disabled: busy,
746
882
  onClick: () => props.face.act(target)
747
883
  }, 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", {
@@ -763,22 +899,24 @@ window.__ModuleLoader__.load({
763
899
  onKeyDown: (event) => {
764
900
  if (event.key === "Escape") close();
765
901
  }
766
- }, react.createElement("div", { style: PANEL_STYLE.dialogHead }, react.createElement("div", { style: PANEL_STYLE.dialogTitle }, "检查更新(七家)"), react.createElement("button", {
902
+ }, react.createElement("div", { style: PANEL_STYLE.dialogHead }, react.createElement("div", { style: PANEL_STYLE.dialogTitle }, "检查更新"), react.createElement("button", {
767
903
  type: "button",
768
904
  style: PANEL_STYLE.close,
769
905
  onClick: () => close(),
770
906
  title: "关闭",
771
907
  "aria-label": "关闭"
772
- }, "×")), body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。"))) : null);
908
+ }, "×")), react.createElement("div", { style: PANEL_STYLE.dialogBody }, body, react.createElement("div", { style: PANEL_STYLE.dialogNote }, "装与更新都在宿主后台跑:关掉这块不影响正在进行的安装,再点「检查更新」可以看到最新状态。")))) : null);
773
909
  }
774
- /** 缺席卡:**按宿主的装机读数分两态**,不让层与层的结论互相顶替。
910
+ /** 缺席卡:**按两处独立事实分三态**,每态只给能改变状态的动作。
775
911
  *
776
- * - 宿主说磁盘上没有这个包 →「未安装」+「装上」按钮(这是本票原有的能力);
777
- * - 宿主说装着(`installedVersion` 非空)却没进页签槽 → 说实情、**不给「装上」**:
778
- * 重装改不了产物里有没有注册代码这件事(真机实测:作息/居家/大厨/记账 的已发布 0.2.0
779
- * 产物里根本没有页签槽注册,见票 #723)。给出可复制命令仅作参考。
912
+ * - `absent`(磁盘上没有这个包)→「未安装」+「装上」按钮(本票原有的能力);
913
+ * - `unregistered-product`(装着,但产物里没有页签槽注册代码)→ 说实情、**不给按钮**:
914
+ * 重装改不了产物里有没有注册代码(真机实测:作息/居家/大厨/记账 的已发布 0.2.0
915
+ * 产物里根本没有注册代码,见票 #723);给可复制命令只是参考;
916
+ * - `not-connected`(装着、产物也有注册代码,设置页却没送到浏览器)→ 重启宿主;仍不行看日志;
917
+ * - `connected` 不归本卡管:那时面板显示的是那家自己的设置页。
780
918
  *
781
- * 原实现只看页签槽 ledger 就断言「未安装」——那对上面第二种情形是说假话,且点「装上」
919
+ * 原实现只看页签槽 ledger 就断言「未安装」——那对后两种情形是说假话,且点「装上」
782
920
  * 会走进更新包的流程(它已经装着 ⇒ 没有新版 ⇒ 没有凭证)并报出误导的「查新版失败」。
783
921
  */
784
922
  function AbsentCard(props) {
@@ -787,10 +925,20 @@ window.__ModuleLoader__.load({
787
925
  const busy = row?.phase === "installing";
788
926
  const manual = (target ? manualForDisplay(target, row?.failure?.manual ?? row?.outcome?.manual ?? null) : null) ?? props.fallbackCommand;
789
927
  const installed = target ? target.installedVersion ?? target.runningVersion : null;
928
+ const state = slotStateOf(target, props.inLedger);
790
929
  let reason = row?.failure?.message ?? null;
791
930
  if (target && reason === null && row?.outcome && row.outcome.snapshot.blockedReason !== null) reason = verdictOf(target, row.outcome.snapshot).text;
792
- const headline = !target ? "这个页签对应的插件还没装。" : installed === null ? "未安装[" + target.packageName + "],装上后这个页签就能用了。" : "插件已装(" + installed + "),但它这一版没把设置页接上爱生活面板:要等它发新版才对得上(重启与重装都不会变)。";
793
- return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), target && installed === null ? react.createElement("button", {
931
+ let headline;
932
+ switch (state) {
933
+ case "absent":
934
+ headline = target ? "未安装[" + target.packageName + "],装上后这个页签就能用了。" : "这个页签对应的插件还没装。";
935
+ break;
936
+ case "unregistered-product":
937
+ headline = target.packageName + " " + String(installed) + " 装着,但它这一版的产物里没有爱生活页签的注册代码:得等它发一个带注册代码的新版才对得上。重启与重装都不会变(重装只是把同一版再装一次)。";
938
+ break;
939
+ default: headline = target.packageName + " " + String(installed) + " 装着、产物里也有注册代码,但它的设置页没送到浏览器:重启宿主后再看这个页签;仍然这样就去宿主日志里找它的报错。";
940
+ }
941
+ return react.createElement("div", { style: PANEL_STYLE.reco }, react.createElement("div", null, headline), state === "absent" ? react.createElement("button", {
794
942
  type: "button",
795
943
  style: busy ? {
796
944
  ...PANEL_STYLE.btn,
@@ -801,7 +949,9 @@ window.__ModuleLoader__.load({
801
949
  marginTop: 8
802
950
  },
803
951
  disabled: busy,
804
- onClick: () => props.face.act(target)
952
+ onClick: () => {
953
+ if (target) props.face.act(target);
954
+ }
805
955
  }, busy ? "正在装,请稍候…" : "装上") : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
806
956
  }
807
957
  //#endregion
@@ -990,8 +1140,10 @@ window.__ModuleLoader__.load({
990
1140
  green: "var(--dsw-alias-state-success-primary, #4ec9a0)"
991
1141
  };
992
1142
  /** 整行底色的浅一档:饱和降一档,免得红黄行「刺眼」压过正文(评审第二轮的扣分点)。 */
1143
+ /** 红黄两档整行上底的底色。红灯要**明显重于**黄灯:复评实测原先两个底色亮度几乎一样
1144
+ * (红 `#FBF3F3` 对黄 `#FCF3E5`,只差蓝通道),最严重那一档反而最淡——红色提到 0.14。 */
993
1145
  const STATUS_TINT = {
994
- red: "rgba(192, 57, 43, 0.06)",
1146
+ red: "rgba(192, 57, 43, 0.14)",
995
1147
  yellow: "rgba(224, 138, 0, 0.10)",
996
1148
  green: "transparent"
997
1149
  };
@@ -1035,45 +1187,40 @@ window.__ModuleLoader__.load({
1035
1187
  fontWeight: 700,
1036
1188
  color: INK
1037
1189
  },
1038
- /** 「体检一次」不挤在标题右端:它单独占一行、与标题同一起始边(视觉复评两轮点「孤立在右上缺层次」)。 */
1039
- runRow: { margin: "8px 0 10px" },
1040
- btn: {
1041
- border: "1px solid " + BORDER,
1042
- background: "transparent",
1043
- color: INK,
1044
- borderRadius: 8,
1045
- padding: "6px 16px",
1046
- fontSize: 13,
1047
- fontWeight: 600,
1048
- cursor: "pointer"
1049
- },
1050
- lightsRow: {
1190
+ /** 顶部那一行汇总(票 #732):一行装下「配置体检 + 一句总账 + 体检按钮」,三样都不独占行。
1191
+ * 它替掉了原先「标题行 + 单独一行的按钮 + 一排灯按钮 + 两行图例」(同屏实测 153px → 一行)。 */
1192
+ summaryRow: {
1051
1193
  display: "flex",
1052
- flexWrap: "wrap",
1053
- gap: 8
1054
- },
1055
- light: {
1056
- display: "inline-flex",
1057
1194
  alignItems: "center",
1058
- gap: 4,
1059
- background: "transparent",
1060
- border: "1px solid " + BORDER,
1061
- borderRadius: 999,
1062
- padding: "3px 10px",
1063
- fontSize: 12.5,
1064
- color: INK,
1065
- cursor: "pointer"
1195
+ gap: 10,
1196
+ marginBottom: 10
1066
1197
  },
1067
- lightIdle: {
1068
- cursor: "default",
1069
- color: INK_DIM
1198
+ summaryText: {
1199
+ flex: "1 1 auto",
1200
+ minWidth: 0,
1201
+ fontSize: 12.5,
1202
+ whiteSpace: "nowrap",
1203
+ overflow: "hidden",
1204
+ textOverflow: "ellipsis"
1070
1205
  },
1206
+ /** 各家那张表里的说明行(「还没体检:点上面那个…」那几句)。 */
1071
1207
  meta: {
1072
1208
  color: INK_DIM,
1073
1209
  fontSize: 12,
1074
1210
  lineHeight: 1.7,
1075
1211
  marginTop: 12
1076
1212
  },
1213
+ btn: {
1214
+ border: "1px solid " + BORDER,
1215
+ background: "transparent",
1216
+ color: INK,
1217
+ borderRadius: 8,
1218
+ flex: "0 0 auto",
1219
+ padding: "3px 10px",
1220
+ fontSize: 12.5,
1221
+ fontWeight: 600,
1222
+ cursor: "pointer"
1223
+ },
1077
1224
  error: {
1078
1225
  color: STATUS_COLOR.red,
1079
1226
  fontSize: 12,
@@ -1128,11 +1275,6 @@ window.__ModuleLoader__.load({
1128
1275
  }
1129
1276
  return prefixes.sort((a, b) => b.length - a.length);
1130
1277
  }
1131
- /** 那一行「配置文件 … · 数据目录 …」+ 各条报文里的长路径,都缩成「公共前缀 + 短尾巴」。 */
1132
- function shorten(path, prefixes) {
1133
- const hit = prefixes.find((prefix) => path.startsWith(prefix + "/"));
1134
- return hit === void 0 ? path : "…/" + path.slice(hit.length + 1);
1135
- }
1136
1278
  /** 一家里所有会印出来的路径。 */
1137
1279
  function pathsOf(report) {
1138
1280
  const out = [report.configPath, report.dataDir];
@@ -1157,15 +1299,17 @@ window.__ModuleLoader__.load({
1157
1299
  function lightsOf(tabs, reports) {
1158
1300
  return tabs.map((tab) => {
1159
1301
  const report = reports[tab.id];
1302
+ const items = report?.items ?? [];
1160
1303
  return {
1161
1304
  id: tab.id,
1162
1305
  title: tab.title,
1163
- status: report ? worstStatus(report.items) : null,
1164
- counts: countByStatus(report?.items ?? [])
1306
+ status: report ? worstStatus(items) : null,
1307
+ counts: countByStatus(items),
1308
+ hasChannel: tab.hasChannel ?? true
1165
1309
  };
1166
1310
  });
1167
1311
  }
1168
- /** 一家的一句话读数:「红 1 黄 2」,全绿就是「绿」。 */
1312
+ /** 一家的一句话读数:「红 1 黄 2」,全绿就是「绿」。面板侧要**上色**的那几处不用它(见 `countSegments`)。 */
1169
1313
  function countsText(counts) {
1170
1314
  const parts = [];
1171
1315
  if (counts.red > 0) parts.push("红 " + String(counts.red));
@@ -1173,12 +1317,103 @@ window.__ModuleLoader__.load({
1173
1317
  if (parts.length === 0) return "绿";
1174
1318
  return parts.join(" · ");
1175
1319
  }
1176
- /** 顶部那行总览:六家一盏灯 + 一个「体检一次」按钮。 */
1177
- function HealthOverview(props) {
1320
+ /** 把一份计数拆成**按档分色**的几截。红黄各自一截、各自上色,互不串色;
1321
+ * 「黄 3」这一截为什么不能被染成红——见 `countSegments` 的注释(出图复评逮到的真缺陷)。 */
1322
+ function countSegsOf(light) {
1323
+ if (light.status === null) return [{
1324
+ status: null,
1325
+ text: "—"
1326
+ }];
1327
+ const segs = [];
1328
+ for (const status of ["red", "yellow"]) {
1329
+ const count = light.counts[status];
1330
+ if (count > 0) segs.push({
1331
+ status,
1332
+ text: STATUS_LABEL[status] + " " + String(count)
1333
+ });
1334
+ }
1335
+ return segs.length > 0 ? segs : [{
1336
+ status: "green",
1337
+ text: "绿"
1338
+ }];
1339
+ }
1340
+ /** 几截读数之间那个分隔符:不许染档位色(它不属于任何一档)。 */
1341
+ const SEG_SEP = " · ";
1342
+ /** 汇总行那一句:绿要显眼(它是「正常」这件事本身),没通道的那几家不冒充绿。 */
1343
+ function overallText(lights) {
1344
+ const pending = lights.filter((light) => !light.hasChannel).length;
1345
+ const withReport = lights.filter((light) => light.hasChannel);
1346
+ const tail = pending > 0 ? " · " + String(pending) + " 家还没体检" : "";
1347
+ if (withReport.length > 0 && withReport.every((light) => light.status === "green")) return react.createElement("span", { style: {
1348
+ color: STATUS_COLOR.green,
1349
+ fontWeight: 700
1350
+ } }, "六家正常" + tail);
1351
+ const parts = [];
1352
+ for (const status of ["red", "yellow"]) {
1353
+ let count = 0;
1354
+ for (const light of lights) count += light.counts[status];
1355
+ if (count === 0) continue;
1356
+ if (parts.length > 0) parts.push(react.createElement("span", {
1357
+ key: status + "-sep",
1358
+ style: { color: INK_DIM }
1359
+ }, SEG_SEP));
1360
+ parts.push(react.createElement("span", {
1361
+ key: status,
1362
+ style: {
1363
+ color: STATUS_TEXT[status],
1364
+ fontWeight: 700
1365
+ }
1366
+ }, STATUS_LABEL[status] + " " + String(count)));
1367
+ }
1368
+ if (parts.length === 0) return react.createElement("span", { style: { color: INK_DIM } }, "还没体检" + tail);
1369
+ parts.push(react.createElement("span", {
1370
+ key: "todo",
1371
+ style: { color: INK }
1372
+ }, " 要处理" + tail));
1373
+ return react.createElement("span", null, parts);
1374
+ }
1375
+ /** 那枚小数字用的底色(比灯色浅一档,压在页签上不抢眼)。 */
1376
+ const STATUS_DOT = {
1377
+ red: STATUS_COLOR.red,
1378
+ yellow: STATUS_COLOR.yellow,
1379
+ green: STATUS_COLOR.green
1380
+ };
1381
+ /** 页签圆点的颜色与语义(票 #732):从「实心/空心=装没装」改成「颜色=体检档位」。 */
1382
+ const TAB_DOT = {
1383
+ /** 没装(页签槽账本里没有这一家)。 */
1384
+ absent: STATUS_DOT.green,
1385
+ /** 装了、但还没跑过体检。 */
1386
+ unchecked: INK_DIM
1387
+ };
1388
+ /** 页签圆点该用什么色:装了按档位(红黄绿),没装/没跑过走缺席态。 */
1389
+ function tabDotColor(light) {
1390
+ if (!light.hasChannel) return TAB_DOT.absent;
1391
+ return light.status === null ? TAB_DOT.unchecked : STATUS_DOT[light.status];
1392
+ }
1393
+ /** 页签上那枚小数字(票 #732):红黄那几家,家名后带一条计数;全绿与没跑过不带。 */
1394
+ const TAB_NOTE_STYLE = {
1395
+ marginLeft: 5,
1396
+ border: "1px solid " + BORDER,
1397
+ borderRadius: 999,
1398
+ padding: "0 5px",
1399
+ fontSize: 11,
1400
+ fontWeight: 700
1401
+ };
1402
+ /** 那枚小数字写什么:只报「要处理」的那两档;一家里红黄都有时先报红那一档(圆点已经按最严重的档上色了)。 */
1403
+ function tabNote(light) {
1404
+ if (!light.hasChannel || light.status === null || light.status === "green") return null;
1405
+ return countSegsOf(light)[0] ?? null;
1406
+ }
1407
+ /** 顶部那一行汇总(票 #732:不再一家一盏灯按钮,一行总账 + 一个体检按钮)。
1408
+ *
1409
+ * 与页签的分工:这行只答「一共几条要处理」;「哪几家、各几条」由页签的圆点与小数字答;
1410
+ * 「具体哪一条坏了、去哪修」由那家页签里的 `HealthTable` 答。 */
1411
+ function HealthSummaryLine(props) {
1412
+ const hasLights = props.lights.length > 0;
1178
1413
  return react.createElement("div", {
1179
- style: HEALTH_STYLE.box,
1180
- "data-ilife-health": "overview"
1181
- }, react.createElement("div", { style: HEALTH_STYLE.headRow }, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检")), react.createElement("div", { style: HEALTH_STYLE.runRow }, react.createElement("button", {
1414
+ style: HEALTH_STYLE.summaryRow,
1415
+ "data-ilife-health": "summary"
1416
+ }, 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) : hasLights ? overallText(props.lights) : react.createElement("span", { style: { color: INK_DIM } }, "还没体检")), react.createElement("button", {
1182
1417
  type: "button",
1183
1418
  style: props.running ? {
1184
1419
  ...HEALTH_STYLE.btn,
@@ -1187,40 +1422,8 @@ window.__ModuleLoader__.load({
1187
1422
  } : HEALTH_STYLE.btn,
1188
1423
  disabled: props.running,
1189
1424
  onClick: props.onRun,
1190
- title: "对六家各跑一次配置体检(只看不改:不建目录、不改配置)"
1191
- }, props.running ? "体检中…" : "体检一次")), react.createElement("div", {
1192
- style: HEALTH_STYLE.lightsRow,
1193
- "data-ilife-health": "lights"
1194
- }, props.lights.map((light) => react.createElement("button", {
1195
- key: light.id,
1196
- type: "button",
1197
- "data-ilife-health": "light",
1198
- "data-skill": light.id,
1199
- style: light.status === null ? {
1200
- ...HEALTH_STYLE.light,
1201
- ...HEALTH_STYLE.lightIdle
1202
- } : {
1203
- ...HEALTH_STYLE.light,
1204
- borderColor: STATUS_COLOR[light.status],
1205
- borderWidth: 1.5,
1206
- background: STATUS_TINT[light.status]
1207
- },
1208
- disabled: light.status === null,
1209
- onClick: () => props.onJump(light.id),
1210
- title: light.status === null ? "还没体检(或这一家没装)" : light.title + ":" + countsText(light.counts)
1211
- }, react.createElement("span", { style: {
1212
- width: 10,
1213
- height: 10,
1214
- borderRadius: "50%",
1215
- background: light.status === null ? INK_DIM : STATUS_COLOR[light.status],
1216
- display: "inline-block"
1217
- } }), react.createElement("span", { style: {
1218
- color: INK,
1219
- fontWeight: 650
1220
- } }, light.title), react.createElement("span", { style: {
1221
- color: light.status === null ? INK_DIM : STATUS_TEXT[light.status],
1222
- fontWeight: 700
1223
- } }, light.status === null ? "—" : countsText(light.counts))))), react.createElement("div", { style: HEALTH_STYLE.meta }, props.error !== null ? props.error : "只看不改 · 灯=那一家最严重的一档 · 点一下跳到那家配置页"));
1425
+ title: "对六家各跑一次配置体检(只看不改:不建目录、不改配置、不自动重置)"
1426
+ }, props.running ? "体检中…" : "体检一次"));
1224
1427
  }
1225
1428
  /** 正正常常的那些:绿条只留「名字」,一句话与「去哪修」都不印(票面验收第一条:正常的收成一行)。 */
1226
1429
  function isAttentionItem(item) {
@@ -1237,10 +1440,11 @@ window.__ModuleLoader__.load({
1237
1440
  }, react.createElement("div", { style: HEALTH_STYLE.headRow }, react.createElement("div", { style: HEALTH_STYLE.head }, props.title + " · 体检"), report ? react.createElement("div", { style: {
1238
1441
  color: INK_DIM,
1239
1442
  fontSize: 12
1240
- } }, countsText(countByStatus(report.items))) : null), props.phase === "idle" ? react.createElement("div", { style: HEALTH_STYLE.meta }, "还没体检:点上面那个「体检一次」。") : null, props.phase === "running" ? react.createElement("div", { style: HEALTH_STYLE.meta }, "正在体检…") : null, props.error !== null ? react.createElement("div", { style: HEALTH_STYLE.error }, props.error) : null, report ? react.createElement("div", null, react.createElement("div", {
1241
- style: HEALTH_STYLE.meta,
1242
- title: prefixes.join(" ")
1243
- }, "配置文件 " + shorten(report.configPath, prefixes) + " · 数据目录 " + shorten(report.dataDir, prefixes)), report.items.filter(isAttentionItem).map((item) => react.createElement("div", {
1443
+ } }, countsText(countByStatus(report.items))) : null), props.phase === "idle" ? react.createElement("div", { style: HEALTH_STYLE.meta }, "还没体检:点上面那个「体检一次」。") : null, props.phase === "running" ? react.createElement("div", { style: HEALTH_STYLE.meta }, "正在体检…") : null, props.error !== null ? react.createElement("div", { style: HEALTH_STYLE.error }, props.error) : null, report ? react.createElement("div", null, report.dataDir !== "" ? react.createElement("div", { style: {
1444
+ color: INK_DIM,
1445
+ fontSize: 11.5,
1446
+ marginTop: 2
1447
+ } }, "落点根目录:" + report.dataDir + (report.configPath !== "" ? "(配置文件 " + report.configPath + ")" : "")) : null, report.items.filter(isAttentionItem).map((item) => react.createElement("div", {
1244
1448
  key: item.id,
1245
1449
  "data-ilife-health": "item",
1246
1450
  "data-status": item.status,
@@ -1267,18 +1471,12 @@ window.__ModuleLoader__.load({
1267
1471
  },
1268
1472
  "aria-hidden": "true"
1269
1473
  }, STATUS_MARK[item.status]), react.createElement("span", { style: {
1270
- width: 8,
1271
- height: 8,
1272
- borderRadius: "50%",
1273
- display: "inline-block",
1274
- background: STATUS_COLOR[item.status]
1275
- } }), react.createElement("span", { style: {
1276
1474
  fontWeight: 650,
1277
1475
  color: INK
1278
1476
  } }, item.title), react.createElement("span", { style: {
1279
1477
  color: STATUS_TEXT[item.status],
1280
1478
  fontWeight: 700
1281
- } }, STATUS_LABEL[item.status]), item.source ? react.createElement("span", { style: { color: INK_DIM } }, "· 来自" + item.source) : null), react.createElement("div", {
1479
+ } }, STATUS_LABEL[item.status]), item.source && item.source !== "默认值" ? react.createElement("span", { style: { color: INK_DIM } }, "· 来自" + item.source) : null), react.createElement("div", {
1282
1480
  style: { color: INK },
1283
1481
  title: item.message
1284
1482
  }, shortenPathsIn(item.message, prefixes)), item.action.length > 0 ? react.createElement("div", { style: { color: INK_DIM } }, "去哪修:" + actionText(item.action)) : null)), report.items.some((item) => !isAttentionItem(item)) ? react.createElement("div", {
@@ -1289,16 +1487,27 @@ window.__ModuleLoader__.load({
1289
1487
  alignItems: "center",
1290
1488
  gap: "2px 10px",
1291
1489
  marginTop: 8,
1292
- paddingTop: 6,
1293
- borderTop: "1px solid " + BORDER,
1490
+ padding: "6px 8px",
1491
+ borderLeft: "3px solid " + STATUS_COLOR.green,
1492
+ borderRadius: 6,
1294
1493
  fontSize: 12,
1295
1494
  lineHeight: 1.6,
1296
1495
  color: INK_DIM
1297
1496
  }
1298
- }, react.createElement("span", { style: {
1497
+ }, react.createElement("span", {
1498
+ style: {
1499
+ width: 8,
1500
+ height: 8,
1501
+ borderRadius: "50%",
1502
+ display: "inline-block",
1503
+ background: STATUS_COLOR.green,
1504
+ flex: "0 0 auto"
1505
+ },
1506
+ "aria-hidden": "true"
1507
+ }), react.createElement("span", { style: {
1299
1508
  fontWeight: 650,
1300
1509
  color: INK
1301
- } }, "正常 " + String(countByStatus(report.items).green) + " 条"), report.items.filter((item) => !isAttentionItem(item)).map((item) => react.createElement("span", {
1510
+ } }, "正常 " + String(countByStatus(report.items).green) + " 条"), react.createElement("span", { style: { color: INK_DIM } }, "(下列各项都正常工作)"), report.items.filter((item) => !isAttentionItem(item)).map((item) => react.createElement("span", {
1302
1511
  key: item.id,
1303
1512
  "data-ilife-health": "ok-item"
1304
1513
  }, "· " + item.title))) : null) : null);
@@ -1324,8 +1533,6 @@ window.__ModuleLoader__.load({
1324
1533
  * 文案与网址都取自 nav.ts 的两张静态表,本文件只画不算。
1325
1534
  */
1326
1535
  const inject = ["slots", "connection"];
1327
- /** 爱生活页签槽(总管声明的 children,技能设置页注册进来;单段名,避开官方 settings.* 前缀)。 */
1328
- const CONFIG_TAB_SLOT = "ilife.config-tab";
1329
1536
  /** 总管视觉(内联 style;颜色走 DSH 主题别名,深浅主题自适应,写死值只做回退;与技能面板同语言)。 */
1330
1537
  const S = {
1331
1538
  headRow: {
@@ -1514,6 +1721,13 @@ window.__ModuleLoader__.load({
1514
1721
  id: row.id,
1515
1722
  channel: row.channel
1516
1723
  })), [rows]));
1724
+ /** 六家的灯(票 #732):按**页签槽账本**那一排过(装了但没体检通道的也在内,它显缺席态)。 */
1725
+ const lights = react.useMemo(() => lightsOf(rows.map((row) => ({
1726
+ id: row.id,
1727
+ title: row.label,
1728
+ hasChannel: row.channel.length > 0
1729
+ })), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))), [rows, health.rows]);
1730
+ const lightOf = (id) => lights.find((light) => light.id === id);
1517
1731
  const [activeId, setActiveId] = react.useState(void 0);
1518
1732
  const [visitedIds, setVisitedIds] = react.useState(() => /* @__PURE__ */ new Set());
1519
1733
  const active = MANAGER_TABS.some((t) => t.plugin === activeId) ? activeId : MANAGER_TABS[0].plugin;
@@ -1527,13 +1741,6 @@ window.__ModuleLoader__.load({
1527
1741
  const row = rows.find((r) => r.id === tab.plugin);
1528
1742
  return row && row.label.length > 0 ? row.label : tab.title;
1529
1743
  }
1530
- /** 按包名取页签标题(总览那行按账本行过,账本里没有的退到导航表的标题)。 */
1531
- function labelForTab(id) {
1532
- const row = rows.find((r) => r.id === id);
1533
- if (row && row.label.length > 0) return row.label;
1534
- const tab = MANAGER_TABS.find((t) => t.plugin === id);
1535
- return tab ? tab.title : id;
1536
- }
1537
1744
  function targetForTab(tab) {
1538
1745
  return face.targets.find((t) => t.packageName === tab.plugin) ?? null;
1539
1746
  }
@@ -1559,21 +1766,20 @@ window.__ModuleLoader__.load({
1559
1766
  setActiveId(next.plugin);
1560
1767
  tabRefs.current[nextIndex]?.focus();
1561
1768
  }
1562
- 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, "总开关 · 开关(缺省启用,只读)"), react.createElement("div", null, "总管 dsh-life-pack · 0.2.5")), react.createElement(UpdateResults, { face }), react.createElement(HealthOverview, {
1563
- lights: lightsOf(rows.map((row) => ({
1564
- id: row.id,
1565
- title: labelForTab(row.id)
1566
- })), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))),
1769
+ 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, "总开关 · 开关(缺省启用,只读)"), react.createElement("div", null, "总管 dsh-life-pack · 0.2.6")), react.createElement(UpdateResults, { face }), react.createElement(HealthSummaryLine, {
1770
+ lights,
1567
1771
  running: health.running,
1568
1772
  error: health.error,
1569
- onRun: health.run,
1570
- onJump: setActiveId
1773
+ onRun: health.run
1571
1774
  }), react.createElement("div", {
1572
1775
  role: "tablist",
1573
1776
  "aria-label": "爱生活技能页签",
1574
1777
  style: S.tablist
1575
1778
  }, MANAGER_TABS.map((tab, index) => {
1576
1779
  const selected = tab.plugin === active;
1780
+ const light = lightOf(tab.plugin);
1781
+ const dotColor = light === void 0 ? present.has(tab.plugin) ? TAB_DOT.unchecked : TAB_DOT.absent : tabDotColor(light);
1782
+ const note = light === void 0 ? null : tabNote(light);
1577
1783
  return react.createElement("button", {
1578
1784
  key: tab.plugin,
1579
1785
  ref: (element) => {
@@ -1594,8 +1800,25 @@ window.__ModuleLoader__.load({
1594
1800
  },
1595
1801
  onKeyDown: (event) => {
1596
1802
  onTabKeyDown(event, index);
1803
+ },
1804
+ title: light === void 0 ? "这一家没装(或产物没注册页签槽)" : light.title + ":" + (light.hasChannel ? light.status === null ? "还没体检" : "体检读数见本页签下方的表" : "没有配置体检出口")
1805
+ }, react.createElement("span", {
1806
+ "data-ilife-health": "tab-dot",
1807
+ style: {
1808
+ width: 6,
1809
+ height: 6,
1810
+ borderRadius: "50%",
1811
+ background: dotColor,
1812
+ display: "inline-block",
1813
+ flex: "0 0 auto"
1814
+ }
1815
+ }), labelFor(tab), note === null ? null : react.createElement("span", {
1816
+ "data-ilife-health": "tab-note",
1817
+ style: {
1818
+ ...TAB_NOTE_STYLE,
1819
+ color: note.status === null ? void 0 : STATUS_TEXT[note.status]
1597
1820
  }
1598
- }, (present.has(tab.plugin) ? "● " : "○ ") + labelFor(tab));
1821
+ }, note.text));
1599
1822
  })), MANAGER_TABS.filter((tab) => tab.plugin === active || visitedIds.has(tab.plugin)).map((tab) => {
1600
1823
  const selected = tab.plugin === active;
1601
1824
  return react.createElement("div", {
@@ -1612,6 +1835,7 @@ window.__ModuleLoader__.load({
1612
1835
  })) : react.createElement(AbsentCard, {
1613
1836
  target: targetForTab(tab),
1614
1837
  fallbackCommand: recoFor(tab).installCmd,
1838
+ inLedger: present.has(tab.plugin),
1615
1839
  face
1616
1840
  }));
1617
1841
  }), react.createElement(MorePluginsCard, null));