@dsh-plus/access-gate 0.3.19 → 0.3.21

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/lib/client.js CHANGED
@@ -12,9 +12,72 @@ window.__ModuleLoader__.load({
12
12
  kind: "idle",
13
13
  text: ""
14
14
  };
15
+ /** 状态行 + 动作按钮(两种形态共用,类名随形态切换度量)。 */
16
+ function CardFoot(props) {
17
+ const p = props.prefix;
18
+ const section = props.variant === "section";
19
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
20
+ className: section ? `${p}-sectionFooter` : `${p}-footer`,
21
+ children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
22
+ className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
23
+ role: "status",
24
+ children: props.status.text
25
+ }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
26
+ type: "button",
27
+ className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
28
+ disabled: action.disabled === true,
29
+ onClick: action.onClick,
30
+ children: action.label
31
+ }, action.key))]
32
+ });
33
+ }
34
+ /** 只读提示 + 字段区 + 页脚(两种形态共用)。 */
35
+ function CardBody(props) {
36
+ const p = props.prefix;
37
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
38
+ props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
39
+ className: `${p}-readOnly`,
40
+ role: "status",
41
+ children: props.readOnlyNotice
42
+ }) : null,
43
+ props.children,
44
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFoot, { ...props })
45
+ ] });
46
+ }
15
47
  function CardChrome(props) {
16
48
  const p = props.prefix;
17
49
  const badge = props.statusBadge;
50
+ if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
51
+ className: `${p}-section`,
52
+ "aria-label": props.title,
53
+ children: [
54
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
55
+ className: `${p}-sectionHead`,
56
+ children: [
57
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
58
+ className: `${p}-sectionTitle`,
59
+ children: props.title
60
+ }),
61
+ badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
62
+ className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
63
+ children: badge.text
64
+ }) : null,
65
+ props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
66
+ className: `${p}-pending`,
67
+ children: props.dirtyLabel
68
+ }) : null
69
+ ]
70
+ }),
71
+ props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
72
+ className: `${p}-sectionDesc`,
73
+ children: props.description
74
+ }),
75
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
76
+ className: `${p}-sectionBody`,
77
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
78
+ })
79
+ ]
80
+ });
18
81
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
19
82
  className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
20
83
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -44,38 +107,44 @@ window.__ModuleLoader__.load({
44
107
  }) : null,
45
108
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
46
109
  ]
47
- }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
110
+ }), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
48
111
  className: `${p}-body`,
49
- children: [
50
- props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
51
- className: `${p}-readOnly`,
52
- role: "status",
53
- children: props.readOnlyNotice
54
- }) : null,
55
- props.children,
56
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
57
- className: `${p}-footer`,
58
- children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
59
- className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
60
- role: "status",
61
- children: props.status.text
62
- }) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
63
- type: "button",
64
- className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
65
- disabled: action.disabled === true,
66
- onClick: action.onClick,
67
- children: action.label
68
- }, action.key))]
69
- })
70
- ]
112
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
71
113
  }) : null]
72
114
  });
73
115
  }
116
+ /** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
117
+ function CardLoading(props) {
118
+ if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
119
+ className: `${props.prefix}-section`,
120
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
121
+ className: `${props.prefix}-readOnly`,
122
+ role: "status",
123
+ children: props.text
124
+ })
125
+ });
126
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
127
+ className: `${props.prefix}-card`,
128
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
129
+ className: `${props.prefix}-readOnly`,
130
+ role: "status",
131
+ children: props.text
132
+ })
133
+ });
134
+ }
74
135
  //#endregion
75
136
  //#region ../shared/src/client/config-slots.ts
76
137
  /** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
77
138
  const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
78
139
  /**
140
+ * 视图 → 外壳形态:插件页 page 视图渲染在官方页面容器内(宿主已画标题、图标、
141
+ * 面包屑与页面级内边距),再套卡片会形成卡片套卡片并压缩移动端可用宽度,
142
+ * 故用无边框分节;summary 与 legacy 槽位保持卡片形态。
143
+ */
144
+ function cardVariantFor(view) {
145
+ return view === "page" ? "section" : "card";
146
+ }
147
+ /**
79
148
  * 把一张配置卡片同时注册进 legacy 与新版三个槽位。
80
149
  * 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
81
150
  */
@@ -101,6 +170,17 @@ window.__ModuleLoader__.load({
101
170
  }, reg.component));
102
171
  }
103
172
  //#endregion
173
+ //#region ../shared/src/client/draft.ts
174
+ /**
175
+ * 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
176
+ * 脏判定、保存(revision fencing + 重新装载)与状态行。
177
+ *
178
+ * 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
179
+ * 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
180
+ * 依赖,必须是模块级稳定函数(不要内联箭头函数)。
181
+ * @module @dsh-plus/shared/client/draft
182
+ */
183
+ //#endregion
104
184
  //#region ../shared/src/client/fields.tsx
105
185
  /**
106
186
  * 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
@@ -269,6 +349,9 @@ window.__ModuleLoader__.load({
269
349
  * - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
270
350
  * - footer 按钮等宽换行、状态文字独占一行;
271
351
  * - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
352
+ * - 分节形态(-section,插件页 page 视图内嵌宿主页面):无边框/背景、横向零
353
+ * 内边距,页脚不 sticky——官方插件页已提供页面级内边距,再叠一层卡片会
354
+ * 缩窄移动端可用宽度(卡片套卡片)。
272
355
  * 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
273
356
  * @module @dsh-plus/shared/client/styles
274
357
  */
@@ -315,6 +398,15 @@ window.__ModuleLoader__.load({
315
398
  .${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
316
399
  .${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
317
400
  .${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
401
+ /* 宿主内嵌分节形态(插件页 page 视图):官方设置页同构——无边框/背景,
402
+ 横向零内边距(宽度交给宿主页面容器),小节标题 13px/600,页脚非 sticky。 */
403
+ .${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
404
+ .${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
405
+ .${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
406
+ .${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
407
+ .${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
408
+ .${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
409
+ .${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
318
410
  .${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
319
411
  .${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
320
412
  .${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
@@ -336,6 +428,8 @@ window.__ModuleLoader__.load({
336
428
  .${prefix}-status{flex-basis:100%;flex:none}
337
429
  .${prefix}-btn{min-height:44px;flex:1;min-width:96px}
338
430
  .${prefix}-statusBadge{display:none}
431
+ .${prefix}-sectionBadge{display:inline-block}
432
+ .${prefix}-sectionFooter{position:static}
339
433
  }
340
434
  @media (pointer:coarse){
341
435
  .${prefix}-btn{min-height:44px}
@@ -422,36 +516,113 @@ ${extra}
422
516
  if (diag.verdict === "autologin") return t("diagAutoLogin");
423
517
  return t("diagBlock");
424
518
  }
519
+ /** 本页诊断读侧:围栏状态端点(配置保存后 snapshot 推进时重读)。 */
520
+ function useGateDiag(snapshot) {
521
+ const [diag, setDiag] = (0, react.useState)(null);
522
+ (0, react.useEffect)(() => {
523
+ let alive = true;
524
+ fetch("/dsh-plus/gate/status").then((res) => res.ok ? res.json() : null).then((body) => {
525
+ if (alive && body !== null) setDiag(body);
526
+ }).catch(() => {});
527
+ return () => {
528
+ alive = false;
529
+ };
530
+ }, [snapshot]);
531
+ return diag;
532
+ }
533
+ /** 诊断面板:本页判定 / 客户端 IP / 放行原因 / 官方登录态(附自动登录与非法条目告警)。 */
534
+ function DiagPanel({ t, diag }) {
535
+ if (diag === null) return null;
536
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
537
+ className: `dag-diag${diag.invalidEntries.length > 0 ? " dag-diagWarn" : ""}`,
538
+ children: [
539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
540
+ className: "dag-diagTitle",
541
+ children: t("diagTitle")
542
+ }),
543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
544
+ className: "dag-diagRow",
545
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
546
+ className: "dag-diagKey",
547
+ children: t("diagVerdict")
548
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
549
+ className: "dag-diagVal",
550
+ children: verdictLabel(t, diag)
551
+ })]
552
+ }),
553
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
554
+ className: "dag-diagRow",
555
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
556
+ className: "dag-diagKey",
557
+ children: t("diagClientIp")
558
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
559
+ className: "dag-diagVal",
560
+ children: diag.clientIp ?? "—"
561
+ })]
562
+ }),
563
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
564
+ className: "dag-diagRow",
565
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
566
+ className: "dag-diagKey",
567
+ children: t("diagReason")
568
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
569
+ className: "dag-diagVal",
570
+ children: diag.reason !== null ? reasonText(t, diag.reason) : "—"
571
+ })]
572
+ }),
573
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
574
+ className: "dag-diagRow",
575
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
576
+ className: "dag-diagKey",
577
+ children: t("diagOfficial")
578
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
579
+ className: "dag-diagVal",
580
+ children: diag.officialAuthed ? t("diagOfficialYes") : t("diagOfficialNo")
581
+ })]
582
+ }),
583
+ diag.autoLoginActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
584
+ className: "dag-diagRow",
585
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
586
+ className: "dag-diagKey",
587
+ children: t("diagAutoLogin")
588
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
589
+ className: "dag-diagVal",
590
+ children: diag.autoLoginReady ? t("diagAutoLoginReady") : t("diagAutoLoginNoSecret")
591
+ })]
592
+ }) : null,
593
+ diag.invalidEntries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
594
+ className: "dag-diagRow",
595
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
596
+ className: "dag-diagKey",
597
+ children: "!"
598
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
599
+ className: "dag-diagVal",
600
+ children: [t("diagInvalid"), diag.invalidEntries.join("、")]
601
+ })]
602
+ }) : null
603
+ ]
604
+ });
605
+ }
425
606
  function AccessGateCard(props) {
426
607
  const { t, scope, api } = props;
427
608
  const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
428
609
  const value = snapshot.value;
429
610
  const [open, setOpen] = (0, react.useState)(props.view === "page");
430
611
  const [draft, setDraft] = (0, react.useState)(null);
431
- const [diag, setDiag] = (0, react.useState)(null);
432
612
  const [saving, setSaving] = (0, react.useState)(false);
433
613
  const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
614
+ const diag = useGateDiag(snapshot);
434
615
  (0, react.useEffect)(() => {
435
616
  if (value === void 0 || draft !== null) return;
436
617
  setDraft(draftFromValue(value));
437
618
  }, [value, draft]);
438
- (0, react.useEffect)(() => {
439
- let alive = true;
440
- fetch("/dsh-plus/gate/status").then((res) => res.ok ? res.json() : null).then((body) => {
441
- if (alive && body !== null) setDiag(body);
442
- }).catch(() => {});
443
- return () => {
444
- alive = false;
445
- };
446
- }, [snapshot]);
447
619
  const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(toPatch(draft)) !== JSON.stringify(toPatch(draftFromValue(value))), [value, draft]);
448
620
  if (props.view === "summary") return t("summaryLine");
449
- if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
450
- className: "dag-card",
451
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
452
- className: "dag-readOnly",
453
- children: t("loading")
454
- })
621
+ const variant = cardVariantFor(props.view);
622
+ if (value === void 0 || draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
623
+ prefix: "dag",
624
+ variant,
625
+ text: t("loading")
455
626
  });
456
627
  const edit = (key, editValue) => {
457
628
  setDraft({
@@ -484,6 +655,7 @@ ${extra}
484
655
  description: t("description"),
485
656
  open,
486
657
  onToggle: setOpen,
658
+ variant,
487
659
  statusBadge: {
488
660
  text: t(draft.enabled ? "enabledOn" : "enabledOff"),
489
661
  on: draft.enabled
@@ -561,75 +733,10 @@ ${extra}
561
733
  disabled,
562
734
  onEdit: (v) => edit("trustForwardedFor", v)
563
735
  }),
564
- diag !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
565
- className: `dag-diag${diag.invalidEntries.length > 0 ? " dag-diagWarn" : ""}`,
566
- children: [
567
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
568
- className: "dag-diagTitle",
569
- children: t("diagTitle")
570
- }),
571
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
572
- className: "dag-diagRow",
573
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
574
- className: "dag-diagKey",
575
- children: t("diagVerdict")
576
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
577
- className: "dag-diagVal",
578
- children: verdictLabel(t, diag)
579
- })]
580
- }),
581
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
582
- className: "dag-diagRow",
583
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
584
- className: "dag-diagKey",
585
- children: t("diagClientIp")
586
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
587
- className: "dag-diagVal",
588
- children: diag.clientIp ?? "—"
589
- })]
590
- }),
591
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
592
- className: "dag-diagRow",
593
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
594
- className: "dag-diagKey",
595
- children: t("diagReason")
596
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
597
- className: "dag-diagVal",
598
- children: diag.reason !== null ? reasonText(t, diag.reason) : "—"
599
- })]
600
- }),
601
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
602
- className: "dag-diagRow",
603
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
604
- className: "dag-diagKey",
605
- children: t("diagOfficial")
606
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
607
- className: "dag-diagVal",
608
- children: diag.officialAuthed ? t("diagOfficialYes") : t("diagOfficialNo")
609
- })]
610
- }),
611
- diag.autoLoginActive ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
612
- className: "dag-diagRow",
613
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
614
- className: "dag-diagKey",
615
- children: t("diagAutoLogin")
616
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
617
- className: "dag-diagVal",
618
- children: diag.autoLoginReady ? t("diagAutoLoginReady") : t("diagAutoLoginNoSecret")
619
- })]
620
- }) : null,
621
- diag.invalidEntries.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
622
- className: "dag-diagRow",
623
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
624
- className: "dag-diagKey",
625
- children: "!"
626
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
627
- className: "dag-diagVal",
628
- children: [t("diagInvalid"), diag.invalidEntries.join("、")]
629
- })]
630
- }) : null
631
- ]
632
- }) : null
736
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagPanel, {
737
+ t,
738
+ diag
739
+ })
633
740
  ]
634
741
  });
635
742
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsh-plus/access-gate",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
4
4
  "description": "dsh-plus service+ui plugin: dsh web 全量访问围栏——与官方 browser-auth 合并:官方 cookie 为唯一凭据,可选 IP 附加围栏,未认证导航渲染 token 输入页(PWA 可恢复),loopback 管理通道",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -31,7 +31,7 @@
31
31
  }
32
32
  },
33
33
  "dependencies": {
34
- "@dsh-plus/shared": "0.1.29"
34
+ "@dsh-plus/shared": "0.1.31"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@deepseek-ai/cordis": "^4.0.4",
@@ -15,12 +15,15 @@
15
15
 
16
16
  import {
17
17
  CardChrome,
18
+ CardLoading,
18
19
  type CardStatusState,
19
20
  CheckRow,
21
+ cardVariantFor,
20
22
  IDLE_STATUS,
21
23
  type NamespaceSettingsApi,
22
24
  type PluginConfigViewProps,
23
25
  type Scope,
26
+ type ScopeSnapshot,
24
27
  } from '@dsh-plus/shared/client'
25
28
  import { type ReactElement, useEffect, useMemo, useState, useSyncExternalStore } from 'react'
26
29
  import type { DictKey, Translate } from './i18n.ts'
@@ -101,6 +104,72 @@ function verdictLabel(t: Translate, diag: Diag): string {
101
104
  return t('diagBlock')
102
105
  }
103
106
 
107
+ /** 本页诊断读侧:围栏状态端点(配置保存后 snapshot 推进时重读)。 */
108
+ function useGateDiag(snapshot: ScopeSnapshot): Diag | null {
109
+ const [diag, setDiag] = useState<Diag | null>(null)
110
+ // biome-ignore lint/correctness/useExhaustiveDependencies: 配置保存后刻意重取诊断
111
+ useEffect(() => {
112
+ let alive = true
113
+ fetch('/dsh-plus/gate/status')
114
+ .then((res) => (res.ok ? res.json() : null))
115
+ .then((body: Diag | null) => {
116
+ if (alive && body !== null) setDiag(body)
117
+ })
118
+ .catch(() => {})
119
+ return () => {
120
+ alive = false
121
+ }
122
+ }, [snapshot])
123
+ return diag
124
+ }
125
+
126
+ /** 诊断面板:本页判定 / 客户端 IP / 放行原因 / 官方登录态(附自动登录与非法条目告警)。 */
127
+ function DiagPanel({ t, diag }: { t: Translate; diag: Diag | null }): ReactElement | null {
128
+ if (diag === null) return null
129
+ return (
130
+ <div className={`dag-diag${diag.invalidEntries.length > 0 ? ' dag-diagWarn' : ''}`}>
131
+ <p className="dag-diagTitle">{t('diagTitle')}</p>
132
+ <div className="dag-diagRow">
133
+ <span className="dag-diagKey">{t('diagVerdict')}</span>
134
+ <span className="dag-diagVal">{verdictLabel(t, diag)}</span>
135
+ </div>
136
+ <div className="dag-diagRow">
137
+ <span className="dag-diagKey">{t('diagClientIp')}</span>
138
+ <span className="dag-diagVal">{diag.clientIp ?? '—'}</span>
139
+ </div>
140
+ <div className="dag-diagRow">
141
+ <span className="dag-diagKey">{t('diagReason')}</span>
142
+ <span className="dag-diagVal">
143
+ {diag.reason !== null ? reasonText(t, diag.reason) : '—'}
144
+ </span>
145
+ </div>
146
+ <div className="dag-diagRow">
147
+ <span className="dag-diagKey">{t('diagOfficial')}</span>
148
+ <span className="dag-diagVal">
149
+ {diag.officialAuthed ? t('diagOfficialYes') : t('diagOfficialNo')}
150
+ </span>
151
+ </div>
152
+ {diag.autoLoginActive ? (
153
+ <div className="dag-diagRow">
154
+ <span className="dag-diagKey">{t('diagAutoLogin')}</span>
155
+ <span className="dag-diagVal">
156
+ {diag.autoLoginReady ? t('diagAutoLoginReady') : t('diagAutoLoginNoSecret')}
157
+ </span>
158
+ </div>
159
+ ) : null}
160
+ {diag.invalidEntries.length > 0 ? (
161
+ <div className="dag-diagRow">
162
+ <span className="dag-diagKey">!</span>
163
+ <span className="dag-diagVal">
164
+ {t('diagInvalid')}
165
+ {diag.invalidEntries.join('、')}
166
+ </span>
167
+ </div>
168
+ ) : null}
169
+ </div>
170
+ )
171
+ }
172
+
104
173
  export function AccessGateCard(props: CardProps): ReactElement | string | null {
105
174
  const { t, scope, api } = props
106
175
  const snapshot = useSyncExternalStore(
@@ -111,9 +180,9 @@ export function AccessGateCard(props: CardProps): ReactElement | string | null {
111
180
  // 0.1.6-alpha.2 插件页 page 视图为表单落地页,默认展开;旧槽位无 view,保持折叠。
112
181
  const [open, setOpen] = useState(props.view === 'page')
113
182
  const [draft, setDraft] = useState<Draft | null>(null)
114
- const [diag, setDiag] = useState<Diag | null>(null)
115
183
  const [saving, setSaving] = useState(false)
116
184
  const [status, setStatus] = useState<CardStatusState>(IDLE_STATUS)
185
+ const diag = useGateDiag(snapshot)
117
186
 
118
187
  // 首次拿到解析值后播种草稿;后续 Host 更新不覆盖在途编辑(与官方 staged 表单一致)。
119
188
  useEffect(() => {
@@ -121,21 +190,6 @@ export function AccessGateCard(props: CardProps): ReactElement | string | null {
121
190
  setDraft(draftFromValue(value))
122
191
  }, [value, draft])
123
192
 
124
- // 本页诊断:读围栏状态端点(配置保存后 snapshot 推进时重读)。
125
- // biome-ignore lint/correctness/useExhaustiveDependencies: 配置保存后刻意重取诊断
126
- useEffect(() => {
127
- let alive = true
128
- fetch('/dsh-plus/gate/status')
129
- .then((res) => (res.ok ? res.json() : null))
130
- .then((body: Diag | null) => {
131
- if (alive && body !== null) setDiag(body)
132
- })
133
- .catch(() => {})
134
- return () => {
135
- alive = false
136
- }
137
- }, [snapshot])
138
-
139
193
  const dirty = useMemo(
140
194
  () =>
141
195
  value !== undefined &&
@@ -147,12 +201,11 @@ export function AccessGateCard(props: CardProps): ReactElement | string | null {
147
201
  // 插件页 summary 视图只出一行简介(hooks 已全部落定,可安全提前返回)。
148
202
  if (props.view === 'summary') return t('summaryLine')
149
203
 
204
+ // 插件页 page 视图内嵌宿主页面容器(已带页面级内边距与标题),用无边框分节。
205
+ const variant = cardVariantFor(props.view)
206
+
150
207
  if (value === undefined || draft === null) {
151
- return (
152
- <li className="dag-card">
153
- <p className="dag-readOnly">{t('loading')}</p>
154
- </li>
155
- )
208
+ return <CardLoading prefix="dag" variant={variant} text={t('loading')} />
156
209
  }
157
210
  const edit = <K extends keyof Draft>(key: K, editValue: Draft[K]): void => {
158
211
  setDraft({ ...draft, [key]: editValue })
@@ -185,6 +238,7 @@ export function AccessGateCard(props: CardProps): ReactElement | string | null {
185
238
  description={t('description')}
186
239
  open={open}
187
240
  onToggle={setOpen}
241
+ variant={variant}
188
242
  statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
189
243
  dirty={dirty}
190
244
  dirtyLabel={t('unsaved')}
@@ -250,48 +304,7 @@ export function AccessGateCard(props: CardProps): ReactElement | string | null {
250
304
  disabled={disabled}
251
305
  onEdit={(v) => edit('trustForwardedFor', v)}
252
306
  />
253
- {diag !== null ? (
254
- <div className={`dag-diag${diag.invalidEntries.length > 0 ? ' dag-diagWarn' : ''}`}>
255
- <p className="dag-diagTitle">{t('diagTitle')}</p>
256
- <div className="dag-diagRow">
257
- <span className="dag-diagKey">{t('diagVerdict')}</span>
258
- <span className="dag-diagVal">{verdictLabel(t, diag)}</span>
259
- </div>
260
- <div className="dag-diagRow">
261
- <span className="dag-diagKey">{t('diagClientIp')}</span>
262
- <span className="dag-diagVal">{diag.clientIp ?? '—'}</span>
263
- </div>
264
- <div className="dag-diagRow">
265
- <span className="dag-diagKey">{t('diagReason')}</span>
266
- <span className="dag-diagVal">
267
- {diag.reason !== null ? reasonText(t, diag.reason) : '—'}
268
- </span>
269
- </div>
270
- <div className="dag-diagRow">
271
- <span className="dag-diagKey">{t('diagOfficial')}</span>
272
- <span className="dag-diagVal">
273
- {diag.officialAuthed ? t('diagOfficialYes') : t('diagOfficialNo')}
274
- </span>
275
- </div>
276
- {diag.autoLoginActive ? (
277
- <div className="dag-diagRow">
278
- <span className="dag-diagKey">{t('diagAutoLogin')}</span>
279
- <span className="dag-diagVal">
280
- {diag.autoLoginReady ? t('diagAutoLoginReady') : t('diagAutoLoginNoSecret')}
281
- </span>
282
- </div>
283
- ) : null}
284
- {diag.invalidEntries.length > 0 ? (
285
- <div className="dag-diagRow">
286
- <span className="dag-diagKey">!</span>
287
- <span className="dag-diagVal">
288
- {t('diagInvalid')}
289
- {diag.invalidEntries.join('、')}
290
- </span>
291
- </div>
292
- ) : null}
293
- </div>
294
- ) : null}
307
+ <DiagPanel t={t} diag={diag} />
295
308
  </CardChrome>
296
309
  )
297
310
  }