From df94c3bdf1a4b9c969bf4bd6071693d83ed3db9b Mon Sep 17 00:00:00 2001
From: ddCat <2695092045@qq.com>
Date: Sun, 6 Sep 2026 07:42:02 +0800
Subject: [PATCH] =?UTF-8?q?chore(web):=20=E5=89=8D=E7=AB=AF=E8=B4=A8?=
=?UTF-8?q?=E9=87=8F=E6=A3=80=E6=9F=A5=20-=20=E8=A7=84=E8=8C=83=E6=A8=A1?=
=?UTF-8?q?=E5=9E=8B=E7=BC=96=E8=BE=91=E6=A0=BC=E5=BC=8F=E5=B9=B6=E6=B6=88?=
=?UTF-8?q?=E9=99=A4=E5=9B=9E=E5=BD=92=E6=B5=8B=E8=AF=95=E7=BC=A9=E8=BF=9B?=
=?UTF-8?q?=E4=BE=9D=E8=B5=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
docs/content/docs/progress/pending-test.mdx | 1 +
web/src/components/choice-browser.css | 133 ++++++++++++++----
web/src/components/choice-browser.tsx | 25 +---
web/src/components/model-editor-modal.css | 124 +++++++++++++---
web/src/components/model-editor-modal.tsx | 9 +-
web/src/components/model-protocol-browser.tsx | 6 +-
.../pages/admin/components/admin-shell.tsx | 8 +-
.../admin/components/channel-model-editor.tsx | 7 +-
.../logical-models/logical-models-page.tsx | 124 +++++++++-------
web/test/admin-ui-regressions.test.ts | 2 +-
web/test/canvas-grid-viewport.test.tsx | 2 +-
11 files changed, 305 insertions(+), 136 deletions(-)
diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx
index 23bf0fe7c..0825d8782 100644
--- a/docs/content/docs/progress/pending-test.mdx
+++ b/docs/content/docs/progress/pending-test.mdx
@@ -11,6 +11,7 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
- 默认前端测试命令已接入模型编辑表单、价格档、前台模型与画布网格回归;最终 `bun run test` 的 39 + 4 + 1120 + 1 项测试全部通过,前端生产构建通过,仍有既有包体积与混合静动态导入警告。
- 隔离组件页使用明确的测试协议数据完成浅色桌面、390px 暗色窄屏、协议搜索选择与详情联动、长内容滚动和固定底栏验收;后台编辑器跨页签必填校验能定位基本信息,取消修改会确认且可继续编辑。校验期间无 XHR/fetch 请求及控制台错误,不代表真实路由或后端保存已验收。
- 本轮相对 rc2 无后端源码变更,未重跑后端全量;rc2 记录的既有后端测试失败仍不视为解决。
+- 标签推送后的远程质量检查中,后端全量与支付插件检查通过,前端被 9 个文件的格式检查阻断。主分支追加格式修复,并将协议浏览器静态回归的定位条件改为忽略换行缩进;本地 1164 项前端测试复跑通过。已推送的 rc3 标签不改写,该补充提交不改变应用业务逻辑。
- [ ] 发布产物验收:前后端 Linux amd64/arm64 镜像、双架构更新器校验和、部署 Compose 附件以及镜像内版本和提交信息。
- [ ] 部署后验收:实际模型新增/编辑保存、真实协议与计费组合、后台路由切换、画布缩放以及登录和任务生成。本次不部署、不写入真实模型配置、不执行收费生成。
diff --git a/web/src/components/choice-browser.css b/web/src/components/choice-browser.css
index a914f897f..7e211fdeb 100644
--- a/web/src/components/choice-browser.css
+++ b/web/src/components/choice-browser.css
@@ -7,12 +7,18 @@
}
.choice-browser-panels {
display: grid;
- grid-template-columns: minmax(0, 1.35fr) minmax(0, .65fr);
+ grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.65fr);
overflow: hidden;
border: 1px solid var(--workspace-border);
border-radius: var(--r-md);
}
-.choice-browser-list { height: 248px; overflow-y: auto; overscroll-behavior: contain; padding: 4px; scrollbar-gutter: stable; }
+.choice-browser-list {
+ height: 248px;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding: 4px;
+ scrollbar-gutter: stable;
+}
.choice-browser-option {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 120px) 16px;
@@ -23,26 +29,105 @@
border-radius: var(--r-sm);
cursor: pointer;
}
-.choice-browser-option:hover { background: var(--surface-hover); }
-.choice-browser-option[data-selected="true"] { background: var(--workspace-accent-soft); box-shadow: inset 0 0 0 1px var(--workspace-border-strong); }
-.choice-browser-option:has(input:disabled) { opacity: .5; cursor: not-allowed; }
-.choice-browser-option:has(input:focus-visible) { outline: 2px solid var(--ring); outline-offset: -2px; }
-.choice-browser-option input { accent-color: var(--workspace-accent); width: 14px; height: 14px; }
-.choice-browser-main { display: grid; min-width: 0; gap: 2px; }
-.choice-browser-main strong { font-size: var(--fs-caption); font-weight: 600; overflow-wrap: anywhere; }
-.choice-browser-main > span, .choice-browser-vendor { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-label); color: var(--muted-foreground); }
-.choice-browser-main > span { font-family: monospace; }
-.choice-browser-detail { min-width: 0; max-height: 248px; overflow-y: auto; overscroll-behavior: contain; padding: 16px; border-left: 1px solid var(--workspace-border); }
-.choice-browser-detail > span { display: block; margin-bottom: 4px; color: var(--muted-foreground); font-size: var(--fs-label); }
-.choice-browser-detail > strong { font-size: var(--fs-caption); overflow-wrap: anywhere; }
-.choice-browser-detail dl { display: grid; gap: 8px; margin-top: 12px; font-size: var(--fs-caption); }
-.choice-browser-detail dl > div { display: grid; grid-template-columns: 4em minmax(0, 1fr); gap: 8px; }
-.choice-browser-detail dt, .choice-browser-detail p { color: var(--muted-foreground); }
-.choice-browser-detail dd, .choice-browser-detail p { margin: 0; overflow-wrap: anywhere; }
-.choice-browser-state { display: grid; min-height: 100px; place-items: center; color: var(--muted-foreground); font-size: var(--fs-caption); padding: 16px; }
-@container (max-width: 620px) {
- .choice-browser-panels { grid-template-columns: minmax(0, 1fr); }
- .choice-browser-detail { border-left: 0; border-top: 1px solid var(--workspace-border); }
- .choice-browser-option { grid-template-columns: minmax(0, 1fr) 16px; }
- .choice-browser-vendor { display: none; }
+.choice-browser-option:hover {
+ background: var(--surface-hover);
+}
+.choice-browser-option[data-selected="true"] {
+ background: var(--workspace-accent-soft);
+ box-shadow: inset 0 0 0 1px var(--workspace-border-strong);
+}
+.choice-browser-option:has(input:disabled) {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+.choice-browser-option:has(input:focus-visible) {
+ outline: 2px solid var(--ring);
+ outline-offset: -2px;
+}
+.choice-browser-option input {
+ accent-color: var(--workspace-accent);
+ width: 14px;
+ height: 14px;
+}
+.choice-browser-main {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+.choice-browser-main strong {
+ font-size: var(--fs-caption);
+ font-weight: 600;
+ overflow-wrap: anywhere;
+}
+.choice-browser-main > span,
+.choice-browser-vendor {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: var(--fs-label);
+ color: var(--muted-foreground);
+}
+.choice-browser-main > span {
+ font-family: monospace;
+}
+.choice-browser-detail {
+ min-width: 0;
+ max-height: 248px;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ padding: 16px;
+ border-left: 1px solid var(--workspace-border);
+}
+.choice-browser-detail > span {
+ display: block;
+ margin-bottom: 4px;
+ color: var(--muted-foreground);
+ font-size: var(--fs-label);
+}
+.choice-browser-detail > strong {
+ font-size: var(--fs-caption);
+ overflow-wrap: anywhere;
+}
+.choice-browser-detail dl {
+ display: grid;
+ gap: 8px;
+ margin-top: 12px;
+ font-size: var(--fs-caption);
+}
+.choice-browser-detail dl > div {
+ display: grid;
+ grid-template-columns: 4em minmax(0, 1fr);
+ gap: 8px;
+}
+.choice-browser-detail dt,
+.choice-browser-detail p {
+ color: var(--muted-foreground);
+}
+.choice-browser-detail dd,
+.choice-browser-detail p {
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+.choice-browser-state {
+ display: grid;
+ min-height: 100px;
+ place-items: center;
+ color: var(--muted-foreground);
+ font-size: var(--fs-caption);
+ padding: 16px;
+}
+@container (max-width: 620px) {
+ .choice-browser-panels {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .choice-browser-detail {
+ border-left: 0;
+ border-top: 1px solid var(--workspace-border);
+ }
+ .choice-browser-option {
+ grid-template-columns: minmax(0, 1fr) 16px;
+ }
+ .choice-browser-vendor {
+ display: none;
+ }
}
diff --git a/web/src/components/choice-browser.tsx b/web/src/components/choice-browser.tsx
index aaf50f6b3..e7a3ad588 100644
--- a/web/src/components/choice-browser.tsx
+++ b/web/src/components/choice-browser.tsx
@@ -42,24 +42,12 @@ export function ChoiceBrowser({
const [query, setQuery] = useState("");
const selected = options.find((option) => option.value === value);
const normalizedQuery = query.trim().toLowerCase();
- const visible = options.filter((option) =>
- [option.label, option.subtitle, option.vendor, option.searchText].join(" ").toLowerCase().includes(normalizedQuery),
- );
+ const visible = options.filter((option) => [option.label, option.subtitle, option.vendor, option.searchText].join(" ").toLowerCase().includes(normalizedQuery));
const unavailable = disabled || loading || Boolean(error);
return (
-
}
- placeholder={placeholder}
- value={query}
- onChange={(event) => setQuery(event.target.value)}
- />
+
} placeholder={placeholder} value={query} onChange={(event) => setQuery(event.target.value)} />
{loading || error || !options.length ? (
{loading ? "正在读取可选项…" : error || "暂无可用选项"}
@@ -76,14 +64,7 @@ export function ChoiceBrowser({
{option.vendor}
-
onChange?.(option.value)}
- />
+
onChange?.(option.value)} />
))}
{!visible.length && (
diff --git a/web/src/components/model-editor-modal.css b/web/src/components/model-editor-modal.css
index 40b81226c..4d92fe8f8 100644
--- a/web/src/components/model-editor-modal.css
+++ b/web/src/components/model-editor-modal.css
@@ -1,23 +1,103 @@
-.model-editor-modal .ant-modal-container { display: flex; flex-direction: column; height: min(880px, calc(100dvh - 48px)); }
-.model-editor-modal .ant-modal-header, .model-editor-modal .ant-modal-footer { padding: 18px 24px; flex-shrink: 0; }
-.model-editor-modal .ant-modal-footer { border-top: 1px solid var(--workspace-border); }
-.model-editor-modal .ant-modal-body { display: flex; flex-direction: column; overflow: hidden; }
-.model-editor-modal .ant-modal-body > .ant-form { display: flex; flex-direction: column; height: 100%; min-height: 0; }
-.model-editor-modal .ant-tabs { display: flex; flex: 1; min-height: 0; }
-.model-editor-modal .ant-tabs-nav { flex-shrink: 0; margin: 0; padding-inline: 24px; }
-.model-editor-modal .ant-tabs-body-holder { min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
-.model-editor-modal .ant-modal-body > .model-editor-panel { flex: 1; overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
-.model-editor-panel { display: grid; gap: 16px; padding: 20px 24px; min-width: 0; }
-.model-editor-panel .ant-form-item { margin-bottom: 0; }
-.model-editor-connection-fields > .ant-form-item { grid-column: auto; min-width: 0; }
-.model-editor-subtitle { margin: 4px 0 0; color: var(--muted-foreground); font-size: var(--fs-caption); font-weight: 400; }
-.model-editor-section { display: grid; gap: 12px; min-width: 0; }
-.model-editor-section + .model-editor-section { border-top: 1px solid var(--workspace-border); padding-top: 16px; }
-.model-editor-section h2 { margin: 0; font-size: var(--fs-body); font-weight: 600; }
-.model-editor-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
-.model-editor-footer-actions { display: flex; gap: 8px; margin-left: auto; }
-@media (max-width: 600px) {
- .model-editor-modal .ant-modal-container { height: calc(100dvh - 24px); }
- .model-editor-panel { padding: 16px; }
- .model-editor-modal .ant-modal-header, .model-editor-modal .ant-modal-footer { padding: 16px; }
+.model-editor-modal .ant-modal-container {
+ display: flex;
+ flex-direction: column;
+ height: min(880px, calc(100dvh - 48px));
+}
+.model-editor-modal .ant-modal-header,
+.model-editor-modal .ant-modal-footer {
+ padding: 18px 24px;
+ flex-shrink: 0;
+}
+.model-editor-modal .ant-modal-footer {
+ border-top: 1px solid var(--workspace-border);
+}
+.model-editor-modal .ant-modal-body {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+.model-editor-modal .ant-modal-body > .ant-form {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ min-height: 0;
+}
+.model-editor-modal .ant-tabs {
+ display: flex;
+ flex: 1;
+ min-height: 0;
+}
+.model-editor-modal .ant-tabs-nav {
+ flex-shrink: 0;
+ margin: 0;
+ padding-inline: 24px;
+}
+.model-editor-modal .ant-tabs-body-holder {
+ min-height: 0;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ scrollbar-gutter: stable;
+}
+.model-editor-modal .ant-modal-body > .model-editor-panel {
+ flex: 1;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ scrollbar-gutter: stable;
+}
+.model-editor-panel {
+ display: grid;
+ gap: 16px;
+ padding: 20px 24px;
+ min-width: 0;
+}
+.model-editor-panel .ant-form-item {
+ margin-bottom: 0;
+}
+.model-editor-connection-fields > .ant-form-item {
+ grid-column: auto;
+ min-width: 0;
+}
+.model-editor-subtitle {
+ margin: 4px 0 0;
+ color: var(--muted-foreground);
+ font-size: var(--fs-caption);
+ font-weight: 400;
+}
+.model-editor-section {
+ display: grid;
+ gap: 12px;
+ min-width: 0;
+}
+.model-editor-section + .model-editor-section {
+ border-top: 1px solid var(--workspace-border);
+ padding-top: 16px;
+}
+.model-editor-section h2 {
+ margin: 0;
+ font-size: var(--fs-body);
+ font-weight: 600;
+}
+.model-editor-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+.model-editor-footer-actions {
+ display: flex;
+ gap: 8px;
+ margin-left: auto;
+}
+@media (max-width: 600px) {
+ .model-editor-modal .ant-modal-container {
+ height: calc(100dvh - 24px);
+ }
+ .model-editor-panel {
+ padding: 16px;
+ }
+ .model-editor-modal .ant-modal-header,
+ .model-editor-modal .ant-modal-footer {
+ padding: 16px;
+ }
}
diff --git a/web/src/components/model-editor-modal.tsx b/web/src/components/model-editor-modal.tsx
index 40131ecd0..a6308bf4c 100644
--- a/web/src/components/model-editor-modal.tsx
+++ b/web/src/components/model-editor-modal.tsx
@@ -27,14 +27,7 @@ export function ModelEditorModal({
admin?: boolean;
children?: ReactNode | ((tabs: ReactNode) => ReactNode);
}) {
- const tabs = items?.length ? (
-
({ ...item, forceRender: true, children: {item.children}
}))}
- />
- ) : null;
+ const tabs = items?.length ? ({ ...item, forceRender: true, children: {item.children}
}))} /> : null;
const content = typeof children === "function" ? children(tabs) : children || tabs;
return (
diff --git a/web/src/components/model-protocol-browser.tsx b/web/src/components/model-protocol-browser.tsx
index f70df9312..4a632c3b8 100644
--- a/web/src/components/model-protocol-browser.tsx
+++ b/web/src/components/model-protocol-browser.tsx
@@ -1,7 +1,11 @@
import { ChoiceBrowser } from "./choice-browser";
import type { ModelProtocolDefinition, ProtocolCapability } from "@/lib/model-protocols";
-export function ModelProtocolBrowser({ capability, protocols, ...props }: {
+export function ModelProtocolBrowser({
+ capability,
+ protocols,
+ ...props
+}: {
capability?: ProtocolCapability;
protocols: ModelProtocolDefinition[];
value?: string;
diff --git a/web/src/pages/admin/components/admin-shell.tsx b/web/src/pages/admin/components/admin-shell.tsx
index 45c6bf617..212aeb9df 100644
--- a/web/src/pages/admin/components/admin-shell.tsx
+++ b/web/src/pages/admin/components/admin-shell.tsx
@@ -164,7 +164,13 @@ export function AdminShell() {
}>
+
+ 正在加载管理页面…
+
+ }
+ >
diff --git a/web/src/pages/admin/components/channel-model-editor.tsx b/web/src/pages/admin/components/channel-model-editor.tsx
index 650e5059b..fbc3904e0 100644
--- a/web/src/pages/admin/components/channel-model-editor.tsx
+++ b/web/src/pages/admin/components/channel-model-editor.tsx
@@ -276,12 +276,7 @@ export function ChannelModelEditor({
]}
/>
- validateChannelModelProtocol(form.getFieldValue("capability"), value, protocols) }]}
- >
+ validateChannelModelProtocol(form.getFieldValue("capability"), value, protocols) }]}>
{protocolError && (
diff --git a/web/src/pages/admin/logical-models/logical-models-page.tsx b/web/src/pages/admin/logical-models/logical-models-page.tsx
index 6697395ec..09397f944 100644
--- a/web/src/pages/admin/logical-models/logical-models-page.tsx
+++ b/web/src/pages/admin/logical-models/logical-models-page.tsx
@@ -378,8 +378,12 @@ export default function LogicalModelsPage() {
模型启用 保存后生效
-
-
+
+
}
@@ -387,63 +391,81 @@ export default function LogicalModelsPage() {
{
key: "identity",
label: "基本信息",
- children: <>
- {editingModel?.configurationError || editingModel?.availabilityError ? (
-
- ) : null}
- } title="前台展示">
-
-
-
-
-
-
-
-
-
-
-
- >,
+
+
+
+
+ >
+ ),
},
{
key: "routes",
label: "供应线路",
- children: <>
- } title="供应线路">
-
-
-
- >,
+ children: (
+ <>
+ } title="供应线路">
+
+
+
+
+
+ >
+ ),
},
{
key: "capabilities",
label: "能力与参数",
- children: <>
-
-
-
-
-
-
-
-
-
-
- >,
+ children: (
+ <>
+
+
+
+
+
+
+
+
+
+
+ >
+ ),
},
]}
>
@@ -459,7 +481,9 @@ export default function LogicalModelsPage() {
modelForm.setFieldsValue({ routes: [], capabilitySpec: emptyCapabilitySpec(changedValues.capability), defaultOptions: {}, pricePolicy: "channel", billingMode: "fixed_request" });
}}
>
-
+
+
+
{tabs}
)}
diff --git a/web/test/admin-ui-regressions.test.ts b/web/test/admin-ui-regressions.test.ts
index 9fcf31b1e..b3a9f6e24 100644
--- a/web/test/admin-ui-regressions.test.ts
+++ b/web/test/admin-ui-regressions.test.ts
@@ -66,7 +66,7 @@ test("model editor presents protocols in a searchable inline radio browser inste
Bun.file(new URL("../src/pages/admin/components/channel-model-editor.tsx", import.meta.url)).text(),
Bun.file(new URL("../src/styles/admin-ui.css", import.meta.url)).text(),
]);
- const protocolSection = sourceSection(source, ' {
setProperty: (name: string, value: string) => properties.set(name, value),
},
dataset: { canvasViewportInteracting: "true" },
- querySelector: (selector: string) => selector === "[data-canvas-world-layer]" ? world : grid,
+ querySelector: (selector: string) => (selector === "[data-canvas-world-layer]" ? world : grid),
});
const graphics: ViewportTransform[] = [];
const previews: ViewportTransform[] = [];