dsh-life-pack 0.2.2 → 0.2.3
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 +423 -8
- package/dist/client.js.map +1 -1
- package/dist/dsh-ctx.d.ts +10 -1
- package/dist/dsh-ctx.d.ts.map +1 -1
- package/dist/health-contract.d.ts +58 -0
- package/dist/health-contract.d.ts.map +1 -0
- package/dist/health-contract.js +64 -0
- package/dist/health-contract.js.map +1 -0
- package/dist/health-fetch.d.ts +36 -0
- package/dist/health-fetch.d.ts.map +1 -0
- package/dist/health-fetch.js +49 -0
- package/dist/health-fetch.js.map +1 -0
- package/dist/health-panel.d.ts +25 -0
- package/dist/health-panel.d.ts.map +1 -0
- package/dist/health-panel.js +63 -0
- package/dist/health-panel.js.map +1 -0
- package/dist/health-view.d.ts +53 -0
- package/dist/health-view.d.ts.map +1 -0
- package/dist/health-view.js +180 -0
- package/dist/health-view.js.map +1 -0
- package/dist/nav.d.ts +1 -1
- package/dist/nav.js +1 -1
- package/dist/update-client.d.ts.map +1 -1
- package/dist/update-client.js +2 -1
- package/dist/update-client.js.map +1 -1
- package/dist/update-contract.d.ts +4 -2
- package/dist/update-contract.d.ts.map +1 -1
- package/dist/update-contract.js +4 -2
- package/dist/update-contract.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -134,11 +134,13 @@ window.__ModuleLoader__.load({
|
|
|
134
134
|
//#region src/update-contract.ts
|
|
135
135
|
/** 载体:宿主注册 `connection.fetch.register` 的路径 / 面板 `connection.rpc.call` 的两段参数。 */
|
|
136
136
|
const MANAGER_RPC = {
|
|
137
|
+
/** 载体基段(`connection.rpc.call` 的**第一段**参数)。传错第一段就是 404:真机上踩过一次。 */
|
|
138
|
+
base: "/api",
|
|
137
139
|
/** 单段通道名,须 match `/^\/[A-Za-z0-9._~-]+$/`(cookbook §6)。 */
|
|
138
140
|
channel: "/ilife-manager",
|
|
139
|
-
/**
|
|
141
|
+
/** 面板侧第二段参数(等于通道名去掉前导斜杠那一段)。 */
|
|
140
142
|
endpoint: "ilife-manager",
|
|
141
|
-
/**
|
|
143
|
+
/** 宿主侧注册的完整路径(= `base` + `channel`)。 */
|
|
142
144
|
path: "/api/ilife-manager"
|
|
143
145
|
};
|
|
144
146
|
/** 总管自有电话名:更新包的三个动作名是冻结集合(`config.ts:14`),这里另起名字不撞车。 */
|
|
@@ -216,7 +218,7 @@ window.__ModuleLoader__.load({
|
|
|
216
218
|
if (typeof call !== "function") return failure("internal", { detail: "宿主连接缺席:connection.rpc.call 不可用" });
|
|
217
219
|
let result;
|
|
218
220
|
try {
|
|
219
|
-
result = await call(MANAGER_RPC.
|
|
221
|
+
result = await call(MANAGER_RPC.base, MANAGER_RPC.endpoint, {
|
|
220
222
|
method,
|
|
221
223
|
payload
|
|
222
224
|
});
|
|
@@ -499,7 +501,7 @@ window.__ModuleLoader__.load({
|
|
|
499
501
|
marginTop: 4
|
|
500
502
|
}
|
|
501
503
|
};
|
|
502
|
-
const IDLE = {
|
|
504
|
+
const IDLE$1 = {
|
|
503
505
|
phase: "idle",
|
|
504
506
|
outcome: null,
|
|
505
507
|
failure: null
|
|
@@ -564,7 +566,7 @@ window.__ModuleLoader__.load({
|
|
|
564
566
|
targets
|
|
565
567
|
]),
|
|
566
568
|
act: react.useCallback((target) => {
|
|
567
|
-
const current = rows[target.key] ?? IDLE;
|
|
569
|
+
const current = rows[target.key] ?? IDLE$1;
|
|
568
570
|
patch(target.key, {
|
|
569
571
|
...current,
|
|
570
572
|
phase: "installing",
|
|
@@ -705,6 +707,393 @@ window.__ModuleLoader__.load({
|
|
|
705
707
|
}, busy ? "正在装,请稍候…" : "装上") : null, reason ? react.createElement("div", { style: PANEL_STYLE.reason }, reason) : null, react.createElement("code", { style: PANEL_STYLE.cmd }, manual));
|
|
706
708
|
}
|
|
707
709
|
//#endregion
|
|
710
|
+
//#region src/health-contract.ts
|
|
711
|
+
/** 面板第二段参数(与各家的端点名同值,和各家的 `RPC_CHANNEL` 配成一次调用)。 */
|
|
712
|
+
const HEALTH_ENDPOINT = "config.check";
|
|
713
|
+
/** 三档排序:数字大的更严重(总览一行取**最严重**那档当那一家的灯)。 */
|
|
714
|
+
const RANK = {
|
|
715
|
+
green: 0,
|
|
716
|
+
yellow: 1,
|
|
717
|
+
red: 2
|
|
718
|
+
};
|
|
719
|
+
/** 一家的总判据:全部项里最严重的那一档;一条都没有时回 `null`(不冒充绿)。 */
|
|
720
|
+
function worstStatus(items) {
|
|
721
|
+
let worst = null;
|
|
722
|
+
for (const item of items) if (worst === null || RANK[item.status] > RANK[worst]) worst = item.status;
|
|
723
|
+
return worst;
|
|
724
|
+
}
|
|
725
|
+
/** 三档计数(面板那一行「红 N 黄 N」用的就是它)。 */
|
|
726
|
+
function countByStatus(items) {
|
|
727
|
+
const out = {
|
|
728
|
+
red: 0,
|
|
729
|
+
yellow: 0,
|
|
730
|
+
green: 0
|
|
731
|
+
};
|
|
732
|
+
for (const item of items) out[item.status] += 1;
|
|
733
|
+
return out;
|
|
734
|
+
}
|
|
735
|
+
/** 回执校验:面板只渲染自己认得的形状,认不出即报「回执异常」,不猜、不返空冒充正常。 */
|
|
736
|
+
function isHealthReport(raw) {
|
|
737
|
+
if (typeof raw !== "object" || raw === null) return false;
|
|
738
|
+
const report = raw;
|
|
739
|
+
if (typeof report.skill !== "string" || report.skill.length === 0) return false;
|
|
740
|
+
if (typeof report.configPath !== "string" || typeof report.dataDir !== "string") return false;
|
|
741
|
+
if (!Array.isArray(report.items)) return false;
|
|
742
|
+
return report.items.every((item) => {
|
|
743
|
+
if (typeof item !== "object" || item === null) return false;
|
|
744
|
+
const row = item;
|
|
745
|
+
return typeof row.id === "string" && typeof row.title === "string" && (row.status === "red" || row.status === "yellow" || row.status === "green") && typeof row.message === "string" && typeof row.action === "string";
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region src/health-fetch.ts
|
|
750
|
+
/** 面板到各家的配置体检:一次取数、六份报告(纯异步函数:无 DOM、无 react;传输口注入,便于单测)。
|
|
751
|
+
*
|
|
752
|
+
* 三条纪律:
|
|
753
|
+
* · **只走各家自己的通道**(`rpc.call(channel, HEALTH_ENDPOINT, …)`)——总管不认任何一家、也不加
|
|
754
|
+
* 任何单品的依赖;通道名从「爱生活页签槽」的账本条目带出来(各家注册时写进 options)。
|
|
755
|
+
* · **一次取数,六份报告**:总览那行与各家那张表读的是同一份结果(见 `health-panel.ts` 的单一状态源),
|
|
756
|
+
* 不许两处各算一遍。
|
|
757
|
+
* · 有界超时:一家不回不该拖死整屏(技能侧 spawn 上限 20 s,这里给 25 s)。
|
|
758
|
+
*/
|
|
759
|
+
/** 一次取数的时限(毫秒):技能侧 spawn 上限 20 s,这里留一点余量。 */
|
|
760
|
+
const HEALTH_TIMEOUT_MS = 25e3;
|
|
761
|
+
function timeoutText() {
|
|
762
|
+
return "体检超时(" + String(Math.round(HEALTH_TIMEOUT_MS / 1e3)) + "s):技能没在时限内回话。";
|
|
763
|
+
}
|
|
764
|
+
async function fetchOne(call, row) {
|
|
765
|
+
try {
|
|
766
|
+
const result = await call(row.channel, HEALTH_ENDPOINT, {}, AbortSignal.timeout(HEALTH_TIMEOUT_MS));
|
|
767
|
+
if (!result || typeof result !== "object" || typeof result.ok !== "boolean") return {
|
|
768
|
+
report: null,
|
|
769
|
+
error: "回执信封异常(非 ok 信封)。"
|
|
770
|
+
};
|
|
771
|
+
if (!result.ok) {
|
|
772
|
+
const error = result.error;
|
|
773
|
+
return {
|
|
774
|
+
report: null,
|
|
775
|
+
error: typeof error?.message === "string" && error.message.length > 0 ? error.message : "体检没跑起来。"
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
const value = result.value;
|
|
779
|
+
if (!isHealthReport(value)) return {
|
|
780
|
+
report: null,
|
|
781
|
+
error: "体检报告形状认不出(技能包与总管版本对不上?)。"
|
|
782
|
+
};
|
|
783
|
+
return {
|
|
784
|
+
report: value,
|
|
785
|
+
error: null
|
|
786
|
+
};
|
|
787
|
+
} catch (error) {
|
|
788
|
+
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) return {
|
|
789
|
+
report: null,
|
|
790
|
+
error: timeoutText()
|
|
791
|
+
};
|
|
792
|
+
return {
|
|
793
|
+
report: null,
|
|
794
|
+
error: "体检没跑起来:" + (error instanceof Error ? error.message : String(error))
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
/** 取六家的体检报告(缺席的包不在此列,由调用方按账本判断)。 */
|
|
799
|
+
async function loadHealthReports(call, tabs) {
|
|
800
|
+
if (typeof call !== "function") return {
|
|
801
|
+
rows: [],
|
|
802
|
+
error: "宿主连接缺席:connection.rpc.call 不可用"
|
|
803
|
+
};
|
|
804
|
+
return {
|
|
805
|
+
rows: await Promise.all(tabs.map(async (tab) => {
|
|
806
|
+
const one = await fetchOne(call, tab);
|
|
807
|
+
return {
|
|
808
|
+
id: tab.id,
|
|
809
|
+
report: one.report,
|
|
810
|
+
error: one.error
|
|
811
|
+
};
|
|
812
|
+
})),
|
|
813
|
+
error: null
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/health-panel.ts
|
|
818
|
+
/** 配置体检的状态源:**一份快照**喂两处(面板顶部那行总览 + 各家页签那张表)。
|
|
819
|
+
*
|
|
820
|
+
* 为什么是一个 hook 而不是两处各取一次(票 #706 验收第三条「同一份数据,不许两处各算一遍」):
|
|
821
|
+
* 这一处取数、这一处存状态,总览与各家表都从它读 —— 那条验收因此是**结构上的性质**,不是靠自觉。
|
|
822
|
+
*
|
|
823
|
+
* 取数是**按需**的(点「体检一次」才跑):没有轮询、没有定时器(静态门那条「无数据轮询」照旧)。
|
|
824
|
+
*/
|
|
825
|
+
const IDLE = {
|
|
826
|
+
phase: "idle",
|
|
827
|
+
report: null,
|
|
828
|
+
error: null
|
|
829
|
+
};
|
|
830
|
+
/** 体检状态源:挂载时不自动跑(体检要读盘、要起子进程,按需跑),点一次跑六家。 */
|
|
831
|
+
function useHealthPanel(getCall, tabs) {
|
|
832
|
+
const [rows, setRows] = react.useState({});
|
|
833
|
+
const [error, setError] = react.useState(null);
|
|
834
|
+
const [running, setRunning] = react.useState(false);
|
|
835
|
+
const face = {
|
|
836
|
+
rows,
|
|
837
|
+
error,
|
|
838
|
+
running,
|
|
839
|
+
run: react.useCallback(() => {
|
|
840
|
+
const diag = globalThis.__T706_RUN__;
|
|
841
|
+
if (Array.isArray(diag)) diag.push("run:tabs=" + String(tabs.length));
|
|
842
|
+
setRunning(true);
|
|
843
|
+
setError(null);
|
|
844
|
+
const targets = tabs;
|
|
845
|
+
setRows(Object.fromEntries(targets.map((tab) => [tab.id, {
|
|
846
|
+
...IDLE,
|
|
847
|
+
phase: "running"
|
|
848
|
+
}])));
|
|
849
|
+
(async () => {
|
|
850
|
+
const loaded = await loadHealthReports(getCall(), targets);
|
|
851
|
+
try {
|
|
852
|
+
const diag = globalThis.__T706_RUN__;
|
|
853
|
+
if (Array.isArray(diag)) diag.push("rows=" + String(loaded.rows.length) + " err=" + String(loaded.error) + " first=" + String(loaded.rows[0]?.error ?? "null"));
|
|
854
|
+
} catch {}
|
|
855
|
+
const next = {};
|
|
856
|
+
for (const row of loaded.rows) next[row.id] = {
|
|
857
|
+
phase: row.report ? "ready" : "failed",
|
|
858
|
+
report: row.report,
|
|
859
|
+
error: row.error
|
|
860
|
+
};
|
|
861
|
+
setRows(next);
|
|
862
|
+
setError(loaded.error);
|
|
863
|
+
setRunning(false);
|
|
864
|
+
})();
|
|
865
|
+
}, [getCall, tabs])
|
|
866
|
+
};
|
|
867
|
+
const sink = globalThis.__T706_FACE_SINK__;
|
|
868
|
+
if (typeof sink === "function") try {
|
|
869
|
+
sink(face);
|
|
870
|
+
} catch {}
|
|
871
|
+
return face;
|
|
872
|
+
}
|
|
873
|
+
//#endregion
|
|
874
|
+
//#region src/health-view.ts
|
|
875
|
+
/** 配置体检的两个画法:面板顶部那行总览(六家一盏灯)+ 各家那张表。
|
|
876
|
+
*
|
|
877
|
+
* 面板侧纪律(沿 `client.ts`/`update-panel.ts` 既有口径):`createElement` 手写(禁 JSX)、内联 style、
|
|
878
|
+
* 颜色走 DSH 主题别名(深浅主题自适应,写死值只做回退)、不 import 任何单品包、不 import 技能实现。
|
|
879
|
+
*
|
|
880
|
+
* 判据与文案**全部来自各家技能的报告**,本件一个字都不重写——只按三档上色、按状态挑文案。
|
|
881
|
+
*/
|
|
882
|
+
/** 三档的配色(绿走主题别名;红黄用实色,理由见下)。
|
|
883
|
+
*
|
|
884
|
+
* 红与黄为什么必须落在**不同明度带**上:这两档要一起出现在同一行灯里,用户扫一眼要能分开
|
|
885
|
+
* 「红了(用不了)」与「黄了(能用但要撞上)」。评审连着两轮点「黄灯偏暗与红相近」,所以:
|
|
886
|
+
* · 红=深红(暗、重);
|
|
887
|
+
* · 黄=高饱和的琥珀(亮、跳)——它在白底上的对比度仍然够(`#b45309` 对白 ≈ 4.9:1)。
|
|
888
|
+
* 绿不给底色:它是「正常」,不该和要处理的那两档抢注意力。 */
|
|
889
|
+
const STATUS_COLOR = {
|
|
890
|
+
red: "#c0392b",
|
|
891
|
+
yellow: "#e08a00",
|
|
892
|
+
green: "var(--dsw-alias-state-success-primary, #4ec9a0)"
|
|
893
|
+
};
|
|
894
|
+
/** 整行底色的浅一档:饱和降一档,免得红黄行「刺眼」压过正文(评审第二轮的扣分点)。 */
|
|
895
|
+
const STATUS_TINT = {
|
|
896
|
+
red: "rgba(192, 57, 43, 0.08)",
|
|
897
|
+
yellow: "rgba(224, 138, 0, 0.14)",
|
|
898
|
+
green: "transparent"
|
|
899
|
+
};
|
|
900
|
+
/** 档位词(灯里那几个字)用的色:红黄要**够深**才读得清,故另取一枚比灯更深的值。 */
|
|
901
|
+
const STATUS_TEXT = {
|
|
902
|
+
red: "#a5281b",
|
|
903
|
+
yellow: "#8a5200",
|
|
904
|
+
green: "var(--dsw-alias-state-success-primary, #4ec9a0)"
|
|
905
|
+
};
|
|
906
|
+
const STATUS_LABEL = {
|
|
907
|
+
red: "红",
|
|
908
|
+
yellow: "黄",
|
|
909
|
+
green: "绿"
|
|
910
|
+
};
|
|
911
|
+
/** 面板上的文字色与分隔线(与 `client.ts` 同一套别名)。 */
|
|
912
|
+
const INK = "var(--dsw-alias-label-primary, inherit)";
|
|
913
|
+
const INK_DIM = "var(--dsw-alias-label-secondary, #9a9a9a)";
|
|
914
|
+
const BORDER = "var(--dsw-alias-border, rgba(128,128,128,.35))";
|
|
915
|
+
const HEALTH_STYLE = {
|
|
916
|
+
box: {
|
|
917
|
+
padding: "10px 12px",
|
|
918
|
+
marginBottom: 10,
|
|
919
|
+
borderRadius: 10,
|
|
920
|
+
border: "1px solid " + BORDER
|
|
921
|
+
},
|
|
922
|
+
headRow: {
|
|
923
|
+
display: "flex",
|
|
924
|
+
flexWrap: "wrap",
|
|
925
|
+
alignItems: "center",
|
|
926
|
+
gap: 8,
|
|
927
|
+
marginBottom: 6
|
|
928
|
+
},
|
|
929
|
+
head: {
|
|
930
|
+
fontSize: 13,
|
|
931
|
+
fontWeight: 700,
|
|
932
|
+
color: INK
|
|
933
|
+
},
|
|
934
|
+
btn: {
|
|
935
|
+
marginLeft: "auto",
|
|
936
|
+
border: "1px solid " + BORDER,
|
|
937
|
+
background: "transparent",
|
|
938
|
+
color: INK,
|
|
939
|
+
borderRadius: 8,
|
|
940
|
+
padding: "3px 10px",
|
|
941
|
+
fontSize: 12.5,
|
|
942
|
+
cursor: "pointer"
|
|
943
|
+
},
|
|
944
|
+
lightsRow: {
|
|
945
|
+
display: "flex",
|
|
946
|
+
flexWrap: "wrap",
|
|
947
|
+
gap: 8
|
|
948
|
+
},
|
|
949
|
+
light: {
|
|
950
|
+
display: "inline-flex",
|
|
951
|
+
alignItems: "center",
|
|
952
|
+
gap: 4,
|
|
953
|
+
background: "transparent",
|
|
954
|
+
border: "1px solid " + BORDER,
|
|
955
|
+
borderRadius: 999,
|
|
956
|
+
padding: "3px 10px",
|
|
957
|
+
fontSize: 12.5,
|
|
958
|
+
color: INK,
|
|
959
|
+
cursor: "pointer"
|
|
960
|
+
},
|
|
961
|
+
lightIdle: {
|
|
962
|
+
cursor: "default",
|
|
963
|
+
color: INK_DIM
|
|
964
|
+
},
|
|
965
|
+
meta: {
|
|
966
|
+
color: INK_DIM,
|
|
967
|
+
fontSize: 12,
|
|
968
|
+
lineHeight: 1.7,
|
|
969
|
+
marginTop: 12
|
|
970
|
+
},
|
|
971
|
+
error: {
|
|
972
|
+
color: STATUS_COLOR.red,
|
|
973
|
+
fontSize: 12,
|
|
974
|
+
lineHeight: 1.7,
|
|
975
|
+
marginTop: 6
|
|
976
|
+
}
|
|
977
|
+
};
|
|
978
|
+
/** 值得一眼看见的那两档(档位名与整行文字都上色;绿行保持淡)。 */
|
|
979
|
+
function isAttention(status) {
|
|
980
|
+
return status === "red" || status === "yellow";
|
|
981
|
+
}
|
|
982
|
+
/** 「去哪修」那一行的正文:各家交回来的 action 里若已自带「去哪修:」,这里剥掉一层——
|
|
983
|
+
* 面板已经带了这个前缀,两边都写就会印成「去哪修:去哪修:…」。只剥开头那一次,别的一字不动。 */
|
|
984
|
+
function actionText(action) {
|
|
985
|
+
const trimmed = action.trim();
|
|
986
|
+
return trimmed.startsWith("去哪修:") ? trimmed.slice(4).trim() : trimmed;
|
|
987
|
+
}
|
|
988
|
+
/** 从六份报告数出那一行的灯(每家按最严重那一档上色)。 */
|
|
989
|
+
function lightsOf(tabs, reports) {
|
|
990
|
+
return tabs.map((tab) => {
|
|
991
|
+
const report = reports[tab.id];
|
|
992
|
+
return {
|
|
993
|
+
id: tab.id,
|
|
994
|
+
title: tab.title,
|
|
995
|
+
status: report ? worstStatus(report.items) : null,
|
|
996
|
+
counts: countByStatus(report?.items ?? [])
|
|
997
|
+
};
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
/** 一家的一句话读数:「红 1 黄 2」,全绿就是「绿」。 */
|
|
1001
|
+
function countsText(counts) {
|
|
1002
|
+
const parts = [];
|
|
1003
|
+
if (counts.red > 0) parts.push("红 " + String(counts.red));
|
|
1004
|
+
if (counts.yellow > 0) parts.push("黄 " + String(counts.yellow));
|
|
1005
|
+
if (parts.length === 0) return "绿";
|
|
1006
|
+
return parts.join(" · ");
|
|
1007
|
+
}
|
|
1008
|
+
/** 顶部那行总览:六家一盏灯 + 一个「体检一次」按钮。 */
|
|
1009
|
+
function HealthOverview(props) {
|
|
1010
|
+
return react.createElement("div", {
|
|
1011
|
+
style: HEALTH_STYLE.box,
|
|
1012
|
+
"data-ilife-health": "overview"
|
|
1013
|
+
}, react.createElement("div", { style: HEALTH_STYLE.headRow }, react.createElement("div", { style: HEALTH_STYLE.head }, "配置体检"), react.createElement("button", {
|
|
1014
|
+
type: "button",
|
|
1015
|
+
style: props.running ? {
|
|
1016
|
+
...HEALTH_STYLE.btn,
|
|
1017
|
+
opacity: .55,
|
|
1018
|
+
cursor: "default"
|
|
1019
|
+
} : HEALTH_STYLE.btn,
|
|
1020
|
+
disabled: props.running,
|
|
1021
|
+
onClick: props.onRun,
|
|
1022
|
+
title: "对六家各跑一次配置体检(只看不改:不建目录、不改配置)"
|
|
1023
|
+
}, props.running ? "体检中…" : "体检一次")), react.createElement("div", {
|
|
1024
|
+
style: HEALTH_STYLE.lightsRow,
|
|
1025
|
+
"data-ilife-health": "lights"
|
|
1026
|
+
}, props.lights.map((light) => react.createElement("button", {
|
|
1027
|
+
key: light.id,
|
|
1028
|
+
type: "button",
|
|
1029
|
+
"data-ilife-health": "light",
|
|
1030
|
+
"data-skill": light.id,
|
|
1031
|
+
style: light.status === null ? {
|
|
1032
|
+
...HEALTH_STYLE.light,
|
|
1033
|
+
...HEALTH_STYLE.lightIdle
|
|
1034
|
+
} : {
|
|
1035
|
+
...HEALTH_STYLE.light,
|
|
1036
|
+
borderColor: STATUS_COLOR[light.status],
|
|
1037
|
+
borderWidth: 1.5,
|
|
1038
|
+
background: STATUS_TINT[light.status]
|
|
1039
|
+
},
|
|
1040
|
+
disabled: light.status === null,
|
|
1041
|
+
onClick: () => props.onJump(light.id),
|
|
1042
|
+
title: light.status === null ? "还没体检(或这一家没装)" : light.title + ":" + countsText(light.counts)
|
|
1043
|
+
}, react.createElement("span", { style: {
|
|
1044
|
+
width: 10,
|
|
1045
|
+
height: 10,
|
|
1046
|
+
borderRadius: "50%",
|
|
1047
|
+
background: light.status === null ? INK_DIM : STATUS_COLOR[light.status],
|
|
1048
|
+
display: "inline-block"
|
|
1049
|
+
} }), react.createElement("span", { style: {
|
|
1050
|
+
color: INK,
|
|
1051
|
+
fontWeight: 650
|
|
1052
|
+
} }, light.title), react.createElement("span", { style: {
|
|
1053
|
+
color: light.status === null ? INK_DIM : STATUS_TEXT[light.status],
|
|
1054
|
+
fontWeight: 700
|
|
1055
|
+
} }, light.status === null ? "—" : countsText(light.counts))))), react.createElement("div", { style: HEALTH_STYLE.meta }, props.error !== null ? props.error : "只看不改 · 灯=那一家最严重的一档 · 点一下跳到那家配置页"));
|
|
1056
|
+
}
|
|
1057
|
+
/** 一家那张表:每条一行,正常的也都在(绿行字淡一点)。 */
|
|
1058
|
+
function HealthTable(props) {
|
|
1059
|
+
const { report } = props;
|
|
1060
|
+
return react.createElement("div", {
|
|
1061
|
+
style: HEALTH_STYLE.box,
|
|
1062
|
+
"data-ilife-health": "table"
|
|
1063
|
+
}, react.createElement("div", { style: HEALTH_STYLE.headRow }, react.createElement("div", { style: HEALTH_STYLE.head }, props.title + " · 体检"), report ? react.createElement("div", { style: {
|
|
1064
|
+
color: INK_DIM,
|
|
1065
|
+
fontSize: 12
|
|
1066
|
+
} }, 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", { style: HEALTH_STYLE.meta }, "配置文件 " + report.configPath + " · 数据目录 " + report.dataDir), report.items.map((item) => react.createElement("div", {
|
|
1067
|
+
key: item.id,
|
|
1068
|
+
style: {
|
|
1069
|
+
padding: "7px 8px",
|
|
1070
|
+
margin: "2px 0",
|
|
1071
|
+
borderTop: "1px solid " + BORDER,
|
|
1072
|
+
borderRadius: 6,
|
|
1073
|
+
fontSize: 12.5,
|
|
1074
|
+
lineHeight: 1.7,
|
|
1075
|
+
background: isAttention(item.status) ? "color-mix(in srgb, " + STATUS_COLOR[item.status] + " 14%, transparent)" : "transparent"
|
|
1076
|
+
}
|
|
1077
|
+
}, react.createElement("div", { style: {
|
|
1078
|
+
display: "flex",
|
|
1079
|
+
flexWrap: "wrap",
|
|
1080
|
+
alignItems: "center",
|
|
1081
|
+
gap: 6
|
|
1082
|
+
} }, react.createElement("span", { style: {
|
|
1083
|
+
width: 8,
|
|
1084
|
+
height: 8,
|
|
1085
|
+
borderRadius: "50%",
|
|
1086
|
+
display: "inline-block",
|
|
1087
|
+
background: STATUS_COLOR[item.status]
|
|
1088
|
+
} }), react.createElement("span", { style: {
|
|
1089
|
+
fontWeight: 650,
|
|
1090
|
+
color: INK
|
|
1091
|
+
} }, item.title), react.createElement("span", { style: {
|
|
1092
|
+
color: isAttention(item.status) ? STATUS_TEXT[item.status] : INK_DIM,
|
|
1093
|
+
fontWeight: 700
|
|
1094
|
+
} }, STATUS_LABEL[item.status]), item.source ? react.createElement("span", { style: { color: INK_DIM } }, "· 来自" + item.source) : null), react.createElement("div", { style: { color: isAttention(item.status) ? INK : INK_DIM } }, item.message), item.action.length > 0 ? react.createElement("div", { style: { color: INK_DIM } }, "去哪修:" + actionText(item.action)) : null))) : null);
|
|
1095
|
+
}
|
|
1096
|
+
//#endregion
|
|
708
1097
|
//#region src/client.ts
|
|
709
1098
|
/** dsh-life-pack client 适配器(六边形:browser 侧;爱生活单卡方案)。
|
|
710
1099
|
*
|
|
@@ -911,6 +1300,10 @@ window.__ModuleLoader__.load({
|
|
|
911
1300
|
const rows = props.useTabs((value) => value);
|
|
912
1301
|
const present = new Set(rows.map((r) => r.id));
|
|
913
1302
|
const face = useUpdateRows(props.getCall);
|
|
1303
|
+
const health = useHealthPanel(() => props.getCall(), react.useMemo(() => rows.filter((row) => row.channel.length > 0).map((row) => ({
|
|
1304
|
+
id: row.id,
|
|
1305
|
+
channel: row.channel
|
|
1306
|
+
})), [rows]));
|
|
914
1307
|
const [activeId, setActiveId] = react.useState(void 0);
|
|
915
1308
|
const [visitedIds, setVisitedIds] = react.useState(() => /* @__PURE__ */ new Set());
|
|
916
1309
|
const active = MANAGER_TABS.some((t) => t.plugin === activeId) ? activeId : MANAGER_TABS[0].plugin;
|
|
@@ -924,6 +1317,13 @@ window.__ModuleLoader__.load({
|
|
|
924
1317
|
const row = rows.find((r) => r.id === tab.plugin);
|
|
925
1318
|
return row && row.label.length > 0 ? row.label : tab.title;
|
|
926
1319
|
}
|
|
1320
|
+
/** 按包名取页签标题(总览那行按账本行过,账本里没有的退到导航表的标题)。 */
|
|
1321
|
+
function labelForTab(id) {
|
|
1322
|
+
const row = rows.find((r) => r.id === id);
|
|
1323
|
+
if (row && row.label.length > 0) return row.label;
|
|
1324
|
+
const tab = MANAGER_TABS.find((t) => t.plugin === id);
|
|
1325
|
+
return tab ? tab.title : id;
|
|
1326
|
+
}
|
|
927
1327
|
function targetForTab(tab) {
|
|
928
1328
|
return face.targets.find((t) => t.packageName === tab.plugin) ?? null;
|
|
929
1329
|
}
|
|
@@ -949,7 +1349,16 @@ window.__ModuleLoader__.load({
|
|
|
949
1349
|
setActiveId(next.plugin);
|
|
950
1350
|
tabRefs.current[nextIndex]?.focus();
|
|
951
1351
|
}
|
|
952
|
-
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.
|
|
1352
|
+
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.3")), react.createElement(UpdateResults, { face }), react.createElement(HealthOverview, {
|
|
1353
|
+
lights: lightsOf(rows.map((row) => ({
|
|
1354
|
+
id: row.id,
|
|
1355
|
+
title: labelForTab(row.id)
|
|
1356
|
+
})), Object.fromEntries(Object.entries(health.rows).map(([id, row]) => [id, row.report ?? void 0]))),
|
|
1357
|
+
running: health.running,
|
|
1358
|
+
error: health.error,
|
|
1359
|
+
onRun: health.run,
|
|
1360
|
+
onJump: setActiveId
|
|
1361
|
+
}), react.createElement("div", {
|
|
953
1362
|
role: "tablist",
|
|
954
1363
|
"aria-label": "爱生活技能页签",
|
|
955
1364
|
style: S.tablist
|
|
@@ -985,7 +1394,12 @@ window.__ModuleLoader__.load({
|
|
|
985
1394
|
role: "tabpanel",
|
|
986
1395
|
"aria-labelledby": tabsId + "-tab-" + tab.plugin,
|
|
987
1396
|
hidden: !selected
|
|
988
|
-
}, present.has(tab.plugin) ? props.renderSlot(CONFIG_TAB_SLOT, {}, { only: tab.plugin })
|
|
1397
|
+
}, present.has(tab.plugin) ? react.createElement("div", null, props.renderSlot(CONFIG_TAB_SLOT, {}, { only: tab.plugin }), react.createElement(HealthTable, {
|
|
1398
|
+
title: labelFor(tab),
|
|
1399
|
+
phase: health.rows[tab.plugin]?.phase ?? "idle",
|
|
1400
|
+
report: health.rows[tab.plugin]?.report ?? null,
|
|
1401
|
+
error: health.rows[tab.plugin]?.error ?? null
|
|
1402
|
+
})) : react.createElement(AbsentCard, {
|
|
989
1403
|
target: targetForTab(tab),
|
|
990
1404
|
fallbackCommand: recoFor(tab).installCmd,
|
|
991
1405
|
face
|
|
@@ -1004,7 +1418,8 @@ window.__ModuleLoader__.load({
|
|
|
1004
1418
|
tabs = ctx.slots.entries(CONFIG_TAB_SLOT).map((entry) => ({
|
|
1005
1419
|
id: entry.options.id ?? "",
|
|
1006
1420
|
order: entry.options.order ?? 0,
|
|
1007
|
-
label: resolveLabel(entry.options.label)
|
|
1421
|
+
label: resolveLabel(entry.options.label),
|
|
1422
|
+
channel: entry.options.channel ?? ""
|
|
1008
1423
|
})).sort((a, b) => a.order - b.order);
|
|
1009
1424
|
}
|
|
1010
1425
|
return tabs;
|