From cbf5b9f054a196bf6e5d553e80fcf325a6883b88 Mon Sep 17 00:00:00 2001 From: LUHANG0 <71977704+LUHANG0@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:41:38 +0800 Subject: [PATCH] =?UTF-8?q?style(admin):=20=E7=AE=A1=E7=90=86=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=20-=20=E7=BB=9F=E4=B8=80=E5=B7=A5=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=E4=B8=8E=E6=A8=A1=E5=9E=8B=E7=BC=96=E8=BE=91=E5=99=A8=E5=B8=83?= =?UTF-8?q?=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/layout/workspace-page.tsx | 84 +- .../components/model-capability-editor.tsx | 698 +++++++++-- web/src/components/model-protocol-picker.tsx | 28 +- .../pages/admin/components/admin-shell.tsx | 29 +- web/src/pages/admin/components/admin-ui.tsx | 99 +- .../admin/components/analytics-panel.tsx | 193 ++- .../components/channel-model-manager.tsx | 650 ++++++---- .../components/credit-operations-panel.tsx | 210 +++- .../components/feature-availability-panel.tsx | 10 +- web/src/styles/globals.css | 1048 ++++++++++++++++- 10 files changed, 2505 insertions(+), 544 deletions(-) diff --git a/web/src/components/layout/workspace-page.tsx b/web/src/components/layout/workspace-page.tsx index 945efd50a..d2da16913 100644 --- a/web/src/components/layout/workspace-page.tsx +++ b/web/src/components/layout/workspace-page.tsx @@ -30,7 +30,25 @@ export function PageHeader({ title, description, meta, actions }: { title: strin ); } -export function ListToolbar({ children, filters, activeFilters, trailing, active, onReset, className }: { children: ReactNode; filters?: ReactNode; activeFilters?: ReactNode; trailing?: ReactNode; active?: boolean; onReset?: () => void; className?: string }) { +export function ListToolbar({ + children, + filters, + filtersAlwaysVisible = false, + activeFilters, + trailing, + active, + onReset, + className, +}: { + children: ReactNode; + filters?: ReactNode; + filtersAlwaysVisible?: boolean; + activeFilters?: ReactNode; + trailing?: ReactNode; + active?: boolean; + onReset?: () => void; + className?: string; +}) { const [filtersOpen, setFiltersOpen] = useState(false); useEffect(() => { @@ -43,16 +61,22 @@ export function ListToolbar({ children, filters, activeFilters, trailing, active {children} {filters ? ( <> - -
{filters}
+ {!filtersAlwaysVisible ? ( + + ) : null} +
{filters}
) : null} {activeFilters ?
{activeFilters}
: null}
- {active && onReset ? : null} + {active && onReset ? ( + + ) : null} {trailing}
@@ -75,7 +99,21 @@ function pageItems(current: number, pages: number): (number | "…")[] { return [1, "…", current - 1, current, current + 1, "…", pages]; } -export function PaginationBar({ current, pageSize, total, onChange, pageSizeOptions = [20, 50, 100], alwaysShow = false }: { current: number; pageSize: number; total: number; onChange: (page: number, pageSize: number) => void; pageSizeOptions?: number[]; alwaysShow?: boolean }) { +export function PaginationBar({ + current, + pageSize, + total, + onChange, + pageSizeOptions = [20, 50, 100], + alwaysShow = false, +}: { + current: number; + pageSize: number; + total: number; + onChange: (page: number, pageSize: number) => void; + pageSizeOptions?: number[]; + alwaysShow?: boolean; +}) { if (!alwaysShow && total <= pageSize && current === 1) return null; const pages = Math.max(1, Math.ceil(total / pageSize)); const start = total === 0 ? 0 : (current - 1) * pageSize + 1; @@ -84,21 +122,25 @@ export function PaginationBar({ current, pageSize, total, onChange, pageSizeOpti return (
{total === 0 ? "共 0 条" : `${start}-${end} / 共 ${total} 条`} - ({ value: size, label: `${size} 条/页` }))} onChange={(value) => onChange(1, Number(value))} />
- - {items.map((item) => item === "…" ? ( - … - ) : ( - - ))} - + + {items.map((item) => + item === "…" ? ( + + … + + ) : ( + + ), + )} +
); diff --git a/web/src/components/model-capability-editor.tsx b/web/src/components/model-capability-editor.tsx index aab079d30..0108f1dd0 100644 --- a/web/src/components/model-capability-editor.tsx +++ b/web/src/components/model-capability-editor.tsx @@ -25,14 +25,15 @@ type Props = { capability?: "text" | "image" | "video"; model?: string; disabled?: boolean; + section?: "all" | "protocol" | "references"; }; -export function ModelCapabilityEditor({ value, onChange, protocol, capability = "video", model = "", disabled = false }: Props) { +export function ModelCapabilityEditor({ value, onChange, protocol, capability = "video", model = "", disabled = false, section = "all" }: Props) { if (capability === "text") { - return ; + return ; } if (capability === "image") { - return ; + return ; } const profile = normalizeModelCapabilityConfig(value || defaultModelCapabilityConfig(protocol)).video!; const update = (patch: Partial) => onChange?.({ version: 1, video: { ...profile, ...patch } }); @@ -41,168 +42,676 @@ export function ModelCapabilityEditor({ value, onChange, protocol, capability = const durationValues = (profile.duration.values || []).join(","); const resolutionOptions = Array.from(new Set([...VIDEO_RESOLUTION_CAPABILITY_OPTIONS, ...profile.resolutions])); + if (section === "references") { + return ( +
+
+ + updateReferences({ minImages: value || 0 })} /> + { + const maxImages = value || 0; + updateReferences({ maxImages, minImages: Math.min(profile.references.minImages, maxImages) }); + }} + /> + updateReferences({ maxImageBytes: mbToBytes(value) })} /> + + + updateReferences({ maxAudios: value || 0 })} /> + updateReferences({ maxAudioBytes: mbToBytes(value) })} /> + updateReferences({ maxAudioDurationSeconds: value || 0 })} /> + update({ generateAudio })} /> + + + updateReferences({ maxVideos: value || 0 })} /> + updateReferences({ maxVideoBytes: mbToBytes(value) })} /> + updateReferences({ maxVideoDurationSeconds: value || 0 })} /> + + + updateReferences({ promptMaxChars: value || 1 })} /> + update({ watermark })} /> + +
+
+ ); + } + + if (section === "protocol") { + return ( +
+
+ + + profile.operations.includes(item.value))} + onChange={(defaultOperation) => update({ defaultOperation })} + /> + + + + + updateDuration( + value === "enum" + ? { selection: "enum", values: profile.duration.values?.length ? profile.duration.values : [profile.duration.default] } + : { selection: "range", min: profile.duration.min || 1, max: profile.duration.max || 15, step: profile.duration.step || 1 }, + ) + } + /> + {profile.duration.selection === "enum" ? ( + + updateDuration({ values: parseIntegerList(event.target.value), default: parseIntegerList(event.target.value)[0] || profile.duration.default })} + /> + + ) : ( +
+ updateDuration({ min: value || 1 })} /> + updateDuration({ max: value || 1 })} /> + updateDuration({ step: value || 1 })} /> + updateDuration({ default: value || 1 })} /> +
+ )} +
+ +
+ + ({ label: item, value: item }))} onChange={(defaultRatio) => update({ defaultRatio })} /> + + + ({ label: item.toUpperCase(), value: item }))} + onChange={(defaultResolution) => update({ defaultResolution })} + /> + +
+
+
+
+ ); + } + return (
-
-
视频能力参数
这些参数会同步到创造页、画布和生成校验
+
+
+
视频能力参数
+
这些参数会同步到创造页、画布和生成校验
+
协议模板可继续调整
- -
- updateReferences({ promptMaxChars: value || 1 })} /> - updateReferences({ minImages: value || 0 })} /> - { const maxImages = value || 0; updateReferences({ maxImages, minImages: Math.min(profile.references.minImages, maxImages) }); }} /> - updateReferences({ maxImageBytes: mbToBytes(value) })} /> - updateReferences({ maxVideos: value || 0 })} /> - updateReferences({ maxVideoBytes: mbToBytes(value) })} /> - updateReferences({ maxVideoDurationSeconds: value || 0 })} /> - updateReferences({ maxAudios: value || 0 })} /> - updateReferences({ maxAudioBytes: mbToBytes(value) })} /> - updateReferences({ maxAudioDurationSeconds: value || 0 })} /> -
-
- - - updateDuration(value === "enum" ? { selection: "enum", values: profile.duration.values?.length ? profile.duration.values : [profile.duration.default] } : { selection: "range", min: profile.duration.min || 1, max: profile.duration.max || 15, step: profile.duration.step || 1 })} /> - {profile.duration.selection === "enum" ? ( - updateDuration({ values: parseIntegerList(event.target.value), default: parseIntegerList(event.target.value)[0] || profile.duration.default })} /> - ) : ( -
- updateDuration({ min: value || 1 })} /> - updateDuration({ max: value || 1 })} /> - updateDuration({ step: value || 1 })} /> - updateDuration({ default: value || 1 })} /> + + +
+ + profile.operations.includes(item.value))} + onChange={(defaultOperation) => update({ defaultOperation })} + /> +
- )} +
+ + + updateDuration( + value === "enum" + ? { selection: "enum", values: profile.duration.values?.length ? profile.duration.values : [profile.duration.default] } + : { selection: "range", min: profile.duration.min || 1, max: profile.duration.max || 15, step: profile.duration.step || 1 }, + ) + } + /> + {profile.duration.selection === "enum" ? ( + + updateDuration({ values: parseIntegerList(event.target.value), default: parseIntegerList(event.target.value)[0] || profile.duration.default })} + /> + + ) : ( +
+ updateDuration({ min: value || 1 })} /> + updateDuration({ max: value || 1 })} /> + updateDuration({ step: value || 1 })} /> + updateDuration({ default: value || 1 })} /> +
+ )} +
+ +
+ + ({ label: item, value: item }))} onChange={(defaultRatio) => update({ defaultRatio })} /> + + + ({ label: item.toUpperCase(), value: item }))} + onChange={(defaultResolution) => update({ defaultResolution })} + /> + +
+
+ {section === "all" ? ( + +
+ updateReferences({ maxVideos: value || 0 })} /> + updateReferences({ maxVideoBytes: mbToBytes(value) })} /> + updateReferences({ maxVideoDurationSeconds: value || 0 })} /> +
+
+ ) : null}
- -
- ({ label: item, value: item }))} onChange={(defaultRatio) => update({ defaultRatio })} /> - ({ label: item.toUpperCase(), value: item }))} onChange={(defaultResolution) => update({ defaultResolution })} /> -
-
- update({ generateAudio })} /> - update({ watermark })} /> -
-
+ {section === "all" ? ( + <> + +
+ updateReferences({ minImages: value || 0 })} /> + { + const maxImages = value || 0; + updateReferences({ maxImages, minImages: Math.min(profile.references.minImages, maxImages) }); + }} + /> + updateReferences({ maxImageBytes: mbToBytes(value) })} /> +
+
- -
- profile.operations.includes(item.value))} onChange={(defaultOperation) => update({ defaultOperation })} /> -
-
+ +
+ updateReferences({ maxAudios: value || 0 })} /> + updateReferences({ maxAudioBytes: mbToBytes(value) })} /> + updateReferences({ maxAudioDurationSeconds: value || 0 })} /> +
+ update({ generateAudio })} /> +
+ + +
+ updateReferences({ promptMaxChars: value || 1 })} /> + update({ watermark })} /> +
+
+ + ) : null}
); } -function TextCapabilityEditor({ value, onChange, protocol, disabled }: Pick) { +function TextCapabilityEditor({ value, onChange, protocol, disabled, section }: Pick) { const profile = value?.text || defaultModelCapabilityConfig(protocol).text!; const updateReferences = (patch: Partial) => { onChange?.({ version: 1, text: { references: { ...profile.references, ...patch } } }); }; + if (section === "references") { + return ( +
+
+ + updateReferences({ maxImages: next || 0 })} /> + updateReferences({ maxImageBytes: mbToBytes(next) })} /> + + + updateReferences({ maxVideos: next || 0 })} /> + updateReferences({ maxVideoBytes: mbToBytes(next) })} /> + + + updateReferences({ promptMaxChars: next || 1 })} /> + +
+
+ ); + } + return (
文本理解能力
默认不假设支持图片或视频,只有明确配置后相关请求才会进入该模型。
- -
- updateReferences({ promptMaxChars: next || 1 })} /> + +
updateReferences({ maxImages: next || 0 })} /> updateReferences({ maxImageBytes: mbToBytes(next) })} /> +
+
+ +
updateReferences({ maxVideos: next || 0 })} /> updateReferences({ maxVideoBytes: mbToBytes(next) })} />
+ + updateReferences({ promptMaxChars: next || 1 })} /> +
); } -function ImageCapabilityEditor({ value, onChange, protocol, model, disabled }: Required> & Pick) { +function ImageCapabilityEditor({ value, onChange, protocol, model, disabled, section }: Required> & Pick) { const profile = normalizeModelCapabilityConfig(value || { version: 1, image: defaultImageCapabilityConfig(protocol, model) }).image!; const update = (patch: Partial) => onChange?.({ version: 1, image: { ...profile, ...patch } }); const updateReferences = (patch: Partial) => update({ references: { ...profile.references, ...patch } }); const updateSize = (patch: Partial) => update({ size: { ...profile.size, ...patch } }); const updateQuality = (patch: Partial) => update({ quality: { ...profile.quality, ...patch } }); + if (section === "references") { + return ( +
+
+ + updateReferences({ maxImages: maxImages || 0 })} /> + updateReferences({ maxImageBytes: mbToBytes(maxImageBytes) })} /> + updateReferences({ maskSupported })} /> + + + updateReferences({ promptMaxChars: promptMaxChars || 1 })} /> + +
+
+ ); + } + + if (section === "protocol") { + return ( +
+
+ + update({ maxOutputs: maxOutputs || 1 })} /> + + + { + const parameter = value as ImageCapabilityConfig["size"]["parameter"]; + updateSize( + parameter === "none" + ? { parameter, values: [], default: "auto", allowCustom: false } + : { parameter, values: profile.size.values.length ? profile.size.values : ["1:1"], default: profile.size.default === "auto" ? "1:1" : profile.size.default }, + ); + }} + /> + {profile.size.parameter !== "none" ? ( + <> + + ({ label: item, value: item }))} + onChange={(defaultValue) => updateSize({ default: defaultValue })} + /> + + updateSize({ allowCustom })} /> + + ) : null} + + + updateQuality({ supported })} /> + {profile.quality.supported ? ( +
+ + ({ label: item, value: item }))} + onChange={(defaultValue) => updateQuality({ default: defaultValue })} + /> + +
+ ) : null} + update({ transparentBackground })} /> + update({ responseFormat: { supported } })} /> + update({ outputFormat: { supported } })} /> +
+
+
+ ); + } + return (
-
-
图片能力参数
生成界面和后端请求都会按此处裁剪参数
+
+
+
图片能力参数
+
生成界面和后端请求都会按此处裁剪参数
+
当前模型独立生效
- -
- updateReferences({ promptMaxChars: promptMaxChars || 1 })} /> + +
updateReferences({ maxImages: maxImages || 0 })} /> updateReferences({ maxImageBytes: mbToBytes(maxImageBytes) })} /> - update({ maxOutputs: maxOutputs || 1 })} />
updateReferences({ maskSupported })} />
- - { - const parameter = value as ImageCapabilityConfig["size"]["parameter"]; - updateSize(parameter === "none" ? { parameter, values: [], default: "auto", allowCustom: false } : { parameter, values: profile.size.values.length ? profile.size.values : ["1:1"], default: profile.size.default === "auto" ? "1:1" : profile.size.default }); - }} - /> - {profile.size.parameter !== "none" ? <> - ({ label: item, value: item }))} onChange={(defaultValue) => updateSize({ default: defaultValue })} /> - updateSize({ allowCustom })} /> -
- : null} + + + update({ maxOutputs: maxOutputs || 1 })} /> + + + { + const parameter = value as ImageCapabilityConfig["size"]["parameter"]; + updateSize( + parameter === "none" + ? { parameter, values: [], default: "auto", allowCustom: false } + : { parameter, values: profile.size.values.length ? profile.size.values : ["1:1"], default: profile.size.default === "auto" ? "1:1" : profile.size.default }, + ); + }} + /> + {profile.size.parameter !== "none" ? ( + <> + + ({ label: item, value: item }))} + onChange={(defaultValue) => updateSize({ default: defaultValue })} + /> + + updateSize({ allowCustom })} /> +
+ + ) : null} + - + updateQuality({ supported })} /> - {profile.quality.supported ?
- ({ label: item, value: item }))} onChange={(defaultValue) => updateQuality({ default: defaultValue })} /> -
: null} + {profile.quality.supported ? ( +
+ + ({ label: item, value: item }))} + onChange={(defaultValue) => updateQuality({ default: defaultValue })} + /> + +
+ ) : null} update({ transparentBackground })} /> -
+
update({ responseFormat: { supported } })} /> update({ outputFormat: { supported } })} />
+ + + updateReferences({ promptMaxChars: promptMaxChars || 1 })} /> +
); } -function CapabilityGroup({ title, children }: { title: string; children: ReactNode }) { - return
{title}
{children}
; +function CapabilityGroup({ title, description, children }: { title: string; description?: string; children: ReactNode }) { + return ( +
+ + {title} + {description ? {description} : null} + +
{children}
+
+ ); +} + +function CapabilityBlock({ title, children }: { title: string; children: ReactNode }) { + return ( +
+
{title}
+
{children}
+
+ ); +} + +function ReferenceCard({ title, description, children }: { title: string; description: string; children: ReactNode }) { + return ( +
+
+

{title}

+

{description}

+
+
{children}
+
+ ); +} + +function ProtocolParameterCard({ step, title, description, children }: { step: string; title: string; description: string; children: ReactNode }) { + return ( +
+
+ {step} +
+

{title}

+

{description}

+
+
+
{children}
+
+ ); } function Field({ label, children }: { label: string; children: ReactNode }) { - return ; + return ( + + ); } function NumberField({ label, value, min, max, disabled, onChange }: { label: string; value?: number; min: number; max?: number; disabled: boolean; onChange: (value: number | null) => void }) { - return
{label}
; + return ( + + ); } function BooleanField({ label, value, disabled, onChange }: { label: string; value: { supported: boolean; default: boolean }; disabled: boolean; onChange: (value: { supported: boolean; default: boolean }) => void }) { - return
{label}
支持该参数
onChange({ ...value, supported })} /> onChange({ ...value, default: defaultValue })} />
; + return ( +
+
+
{label}
+
可发送参数与默认值
+
+
+ + +
+
+ ); } function ParameterField({ label, description, supported, disabled, onChange }: { label: string; description: string; supported: boolean; disabled: boolean; onChange: (supported: boolean) => void }) { - return
{label}
{description}
; + return ( +
+
+
{label}
+
{description}
+
+ +
+ ); } function bytesToMB(value: number) { @@ -214,5 +723,12 @@ function mbToBytes(value: number | null) { } function parseIntegerList(value: string) { - return Array.from(new Set(value.split(",").map((item) => Number(item.trim())).filter((item) => Number.isInteger(item) && item > 0))).sort((left, right) => left - right); + return Array.from( + new Set( + value + .split(",") + .map((item) => Number(item.trim())) + .filter((item) => Number.isInteger(item) && item > 0), + ), + ).sort((left, right) => left - right); } diff --git a/web/src/components/model-protocol-picker.tsx b/web/src/components/model-protocol-picker.tsx index 170341dc9..444a2409e 100644 --- a/web/src/components/model-protocol-picker.tsx +++ b/web/src/components/model-protocol-picker.tsx @@ -57,7 +57,19 @@ export function CapabilityCardPicker({ value, onChange, density = "comfortable" ); } -export function ProtocolCardPicker({ capability, value, onChange, density = "comfortable", protocols = [] }: { capability?: ModelCapabilityChoice; value?: ModelProtocol; onChange?: (value: ModelProtocol) => void; density?: PickerDensity; protocols?: ModelProtocolDefinition[] }) { +export function ProtocolCardPicker({ + capability, + value, + onChange, + density = "comfortable", + protocols = [], +}: { + capability?: ModelCapabilityChoice; + value?: ModelProtocol; + onChange?: (value: ModelProtocol) => void; + density?: PickerDensity; + protocols?: ModelProtocolDefinition[]; +}) { const availableProtocols = protocols.filter((item) => item.capability === capability && item.enabled !== false); return (
@@ -79,8 +91,8 @@ export function ProtocolCardPicker({ capability, value, onChange, density = "com
-
{protocol.label}
-
{protocol.create}
+
{protocol.label}
+
{protocol.create}
{selected ? ( @@ -104,7 +116,15 @@ export function ProtocolCardPicker({ capability, value, onChange, density = "com ); } -export function ModelCapabilityProtocolModal({ value, onChange, protocols = [] }: { value: { capability: ModelCapabilityChoice; protocol: ModelProtocol }; onChange: (value: { capability: ModelCapabilityChoice; protocol: ModelProtocol }) => void; protocols?: ModelProtocolDefinition[] }) { +export function ModelCapabilityProtocolModal({ + value, + onChange, + protocols = [], +}: { + value: { capability: ModelCapabilityChoice; protocol: ModelProtocol }; + onChange: (value: { capability: ModelCapabilityChoice; protocol: ModelProtocol }) => void; + protocols?: ModelProtocolDefinition[]; +}) { const [open, setOpen] = useState(false); const [draft, setDraft] = useState(value); diff --git a/web/src/pages/admin/components/admin-shell.tsx b/web/src/pages/admin/components/admin-shell.tsx index 7d54a5dbd..c19b2703e 100644 --- a/web/src/pages/admin/components/admin-shell.tsx +++ b/web/src/pages/admin/components/admin-shell.tsx @@ -97,26 +97,26 @@ export function AdminShell() {