chore(web): 前端质量检查 - 规范模型编辑格式并消除回归测试缩进依赖
This commit is contained in:
@@ -11,6 +11,7 @@ description: 当前项目已实现功能的手工验证清单与未确认边界
|
||||
- 默认前端测试命令已接入模型编辑表单、价格档、前台模型与画布网格回归;最终 `bun run test` 的 39 + 4 + 1120 + 1 项测试全部通过,前端生产构建通过,仍有既有包体积与混合静动态导入警告。
|
||||
- 隔离组件页使用明确的测试协议数据完成浅色桌面、390px 暗色窄屏、协议搜索选择与详情联动、长内容滚动和固定底栏验收;后台编辑器跨页签必填校验能定位基本信息,取消修改会确认且可继续编辑。校验期间无 XHR/fetch 请求及控制台错误,不代表真实路由或后端保存已验收。
|
||||
- 本轮相对 rc2 无后端源码变更,未重跑后端全量;rc2 记录的既有后端测试失败仍不视为解决。
|
||||
- 标签推送后的远程质量检查中,后端全量与支付插件检查通过,前端被 9 个文件的格式检查阻断。主分支追加格式修复,并将协议浏览器静态回归的定位条件改为忽略换行缩进;本地 1164 项前端测试复跑通过。已推送的 rc3 标签不改写,该补充提交不改变应用业务逻辑。
|
||||
- [ ] 发布产物验收:前后端 Linux amd64/arm64 镜像、双架构更新器校验和、部署 Compose 附件以及镜像内版本和提交信息。
|
||||
- [ ] 部署后验收:实际模型新增/编辑保存、真实协议与计费组合、后台路由切换、画布缩放以及登录和任务生成。本次不部署、不写入真实模型配置、不执行收费生成。
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="choice-browser">
|
||||
<Input
|
||||
id={id}
|
||||
{...aria}
|
||||
allowClear
|
||||
aria-label={`搜索${label}`}
|
||||
disabled={unavailable}
|
||||
prefix={<Search className="size-3.5" aria-hidden="true" />}
|
||||
placeholder={placeholder}
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
/>
|
||||
<Input id={id} {...aria} allowClear aria-label={`搜索${label}`} disabled={unavailable} prefix={<Search className="size-3.5" aria-hidden="true" />} placeholder={placeholder} value={query} onChange={(event) => setQuery(event.target.value)} />
|
||||
{loading || error || !options.length ? (
|
||||
<div className="choice-browser-state" role={error ? "alert" : "status"}>
|
||||
{loading ? "正在读取可选项…" : error || "暂无可用选项"}
|
||||
@@ -76,14 +64,7 @@ export function ChoiceBrowser({
|
||||
<span className="choice-browser-vendor" title={option.vendor}>
|
||||
{option.vendor}
|
||||
</span>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
value={option.value}
|
||||
checked={option.value === value}
|
||||
disabled={unavailable || option.disabled}
|
||||
onChange={() => onChange?.(option.value)}
|
||||
/>
|
||||
<input type="radio" name={name} value={option.value} checked={option.value === value} disabled={unavailable || option.disabled} onChange={() => onChange?.(option.value)} />
|
||||
</label>
|
||||
))}
|
||||
{!visible.length && (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,7 @@ export function ModelEditorModal({
|
||||
admin?: boolean;
|
||||
children?: ReactNode | ((tabs: ReactNode) => ReactNode);
|
||||
}) {
|
||||
const tabs = items?.length ? (
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={onTabChange}
|
||||
animated={false}
|
||||
items={items.map((item) => ({ ...item, forceRender: true, children: <div className="model-editor-panel">{item.children}</div> }))}
|
||||
/>
|
||||
) : null;
|
||||
const tabs = items?.length ? <Tabs activeKey={activeKey} onChange={onTabChange} animated={false} items={items.map((item) => ({ ...item, forceRender: true, children: <div className="model-editor-panel">{item.children}</div> }))} /> : null;
|
||||
const content = typeof children === "function" ? children(tabs) : children || tabs;
|
||||
|
||||
return (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -164,7 +164,13 @@ export function AdminShell() {
|
||||
</Tooltip>
|
||||
<section className="flex min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<MobileAdminNavigation />
|
||||
<Suspense fallback={<div className="p-8 text-sm text-foreground/60" role="status">正在加载管理页面…</div>}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="p-8 text-sm text-foreground/60" role="status">
|
||||
正在加载管理页面…
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</section>
|
||||
|
||||
@@ -276,12 +276,7 @@ export function ChannelModelEditor({
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
className="admin-model-protocol-field"
|
||||
name="protocol"
|
||||
label="调用协议"
|
||||
rules={[{ validator: async (_, value) => validateChannelModelProtocol(form.getFieldValue("capability"), value, protocols) }]}
|
||||
>
|
||||
<Form.Item className="admin-model-protocol-field" name="protocol" label="调用协议" rules={[{ validator: async (_, value) => validateChannelModelProtocol(form.getFieldValue("capability"), value, protocols) }]}>
|
||||
<ModelProtocolBrowser disabled={busy} capability={modelCapability} protocols={protocols} loading={protocolLoading} error={protocolError} />
|
||||
</Form.Item>
|
||||
{protocolError && (
|
||||
|
||||
@@ -378,8 +378,12 @@ export default function LogicalModelsPage() {
|
||||
模型启用 <span className="text-xs text-foreground/50">保存后生效</span>
|
||||
</label>
|
||||
<div className="model-editor-footer-actions">
|
||||
<Button disabled={saving} onClick={closeEditor}>取消</Button>
|
||||
<Button type="primary" loading={saving} onClick={() => void saveModel()}>{editingModel ? "保存修改" : "添加模型"}</Button>
|
||||
<Button disabled={saving} onClick={closeEditor}>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" loading={saving} onClick={() => void saveModel()}>
|
||||
{editingModel ? "保存修改" : "添加模型"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -387,63 +391,81 @@ export default function LogicalModelsPage() {
|
||||
{
|
||||
key: "identity",
|
||||
label: "基本信息",
|
||||
children: <>
|
||||
{editingModel?.configurationError || editingModel?.availabilityError ? (
|
||||
<Alert
|
||||
className="mb-4"
|
||||
type="warning"
|
||||
showIcon
|
||||
message={editingModel.configurationError ? "当前供应线路无法覆盖全部创作端能力" : "当前供应线路暂不可结算"}
|
||||
description={editingModel.configurationError || editingModel.availabilityError}
|
||||
/>
|
||||
) : null}
|
||||
<EditorSection icon={<Layers3 className="size-4" />} title="前台展示">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Form.Item name="name" label="显示名称" rules={[{ required: true, message: "请填写显示名称" }]}>
|
||||
<Input placeholder="例如:Seedance 视频" />
|
||||
children: (
|
||||
<>
|
||||
{editingModel?.configurationError || editingModel?.availabilityError ? (
|
||||
<Alert
|
||||
className="mb-4"
|
||||
type="warning"
|
||||
showIcon
|
||||
message={editingModel.configurationError ? "当前供应线路无法覆盖全部创作端能力" : "当前供应线路暂不可结算"}
|
||||
description={editingModel.configurationError || editingModel.availabilityError}
|
||||
/>
|
||||
) : null}
|
||||
<EditorSection icon={<Layers3 className="size-4" />} title="前台展示">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Form.Item name="name" label="显示名称" rules={[{ required: true, message: "请填写显示名称" }]}>
|
||||
<Input placeholder="例如:Seedance 视频" />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="模型代码" rules={[{ required: true, message: "请填写模型代码" }]}>
|
||||
<Input placeholder="例如:seedance-video" />
|
||||
</Form.Item>
|
||||
<Form.Item name="icon" label="模型 Logo">
|
||||
<ModelIconPicker />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item name="description" label="简短说明">
|
||||
<Input.TextArea autoSize={{ minRows: 2, maxRows: 4 }} placeholder="说明适合的创作场景,不描述供应渠道。" />
|
||||
</Form.Item>
|
||||
<Form.Item name="code" label="模型代码" rules={[{ required: true, message: "请填写模型代码" }]}>
|
||||
<Input placeholder="例如:seedance-video" />
|
||||
<Form.Item name="capability" label="类型">
|
||||
<Segmented
|
||||
block
|
||||
options={[
|
||||
{ label: "文本", value: "text" },
|
||||
{ label: "图片", value: "image" },
|
||||
{ label: "视频", value: "video" },
|
||||
{ label: "音频", value: "audio" },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="icon" label="模型 Logo"><ModelIconPicker /></Form.Item>
|
||||
</div>
|
||||
<Form.Item name="description" label="简短说明">
|
||||
<Input.TextArea autoSize={{ minRows: 2, maxRows: 4 }} placeholder="说明适合的创作场景,不描述供应渠道。" />
|
||||
</Form.Item>
|
||||
<Form.Item name="capability" label="类型">
|
||||
<Segmented block options={[{ label: "文本", value: "text" }, { label: "图片", value: "image" }, { label: "视频", value: "video" }, { label: "音频", value: "audio" }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="sortOrder" label="前台排序">
|
||||
<InputNumber className="w-full" precision={0} />
|
||||
</Form.Item>
|
||||
</EditorSection>
|
||||
</>,
|
||||
<Form.Item name="sortOrder" label="前台排序">
|
||||
<InputNumber className="w-full" precision={0} />
|
||||
</Form.Item>
|
||||
</EditorSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "routes",
|
||||
label: "供应线路",
|
||||
children: <>
|
||||
<EditorSection icon={<GitBranch className="size-4" />} title="供应线路">
|
||||
<RouteFields channelModels={modelChannelModels} channelNames={channelNames} channelEnabled={channelEnabled} form={modelForm} capability={modelCapability} />
|
||||
</EditorSection>
|
||||
<EditorSection title="系统规格价格"><PricingFields /></EditorSection>
|
||||
</>,
|
||||
children: (
|
||||
<>
|
||||
<EditorSection icon={<GitBranch className="size-4" />} title="供应线路">
|
||||
<RouteFields channelModels={modelChannelModels} channelNames={channelNames} channelEnabled={channelEnabled} form={modelForm} capability={modelCapability} />
|
||||
</EditorSection>
|
||||
<EditorSection title="系统规格价格">
|
||||
<PricingFields />
|
||||
</EditorSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "capabilities",
|
||||
label: "能力与参数",
|
||||
children: <>
|
||||
<EditorSection title="创作端可选能力">
|
||||
<Form.Item name="capabilitySpec" noStyle>
|
||||
<CapabilityScopeEditor capability={modelCapability} sourceSpecs={modelSourceSpecs} mode="front" />
|
||||
</Form.Item>
|
||||
</EditorSection>
|
||||
<EditorSection title="默认参数">
|
||||
<Form.Item name="defaultOptions" noStyle>
|
||||
<DefaultOptionsEditor spec={modelCapabilitySpec} />
|
||||
</Form.Item>
|
||||
</EditorSection>
|
||||
</>,
|
||||
children: (
|
||||
<>
|
||||
<EditorSection title="创作端可选能力">
|
||||
<Form.Item name="capabilitySpec" noStyle>
|
||||
<CapabilityScopeEditor capability={modelCapability} sourceSpecs={modelSourceSpecs} mode="front" />
|
||||
</Form.Item>
|
||||
</EditorSection>
|
||||
<EditorSection title="默认参数">
|
||||
<Form.Item name="defaultOptions" noStyle>
|
||||
<DefaultOptionsEditor spec={modelCapabilitySpec} />
|
||||
</Form.Item>
|
||||
</EditorSection>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
>
|
||||
@@ -459,7 +481,9 @@ export default function LogicalModelsPage() {
|
||||
modelForm.setFieldsValue({ routes: [], capabilitySpec: emptyCapabilitySpec(changedValues.capability), defaultOptions: {}, pricePolicy: "channel", billingMode: "fixed_request" });
|
||||
}}
|
||||
>
|
||||
<Form.Item name="enabled" hidden><Switch /></Form.Item>
|
||||
<Form.Item name="enabled" hidden>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{tabs}
|
||||
</Form>
|
||||
)}
|
||||
|
||||
@@ -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, '<Form.Item\n className="admin-model-protocol-field"', "{protocolError && (");
|
||||
const protocolSection = sourceSection(compactSource(source), '<Form.Item className="admin-model-protocol-field"', "{protocolError && (");
|
||||
|
||||
expect(protocolSection).toContain("<ModelProtocolBrowser");
|
||||
expect(protocolSection).not.toContain("<Select");
|
||||
|
||||
@@ -58,7 +58,7 @@ describe("canvas screen-space background", () => {
|
||||
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[] = [];
|
||||
|
||||
Reference in New Issue
Block a user