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="前台展示"> -
- - + 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[] = [];