后端:apps/ai QualityWord(stage/slot/text/sort/enabled)+ migration;services.py quality_words/quality_suffix, 4 个 builder 接入(person/model_tryon/storyboard/video)无配置回落原写死值零回归;adminpanel CRUD + 审计。 前端:adminApi quality 系列;Admin 质量词页(按 stage 分组卡 + 编辑模式开关 + 行内增改删 chip)。 测试:adminpanel 23 + accounts 22 = 45 单测过(CRUD/权限/生成读配置+回落/停用词不取); 无头 e2e _admin-p4.mjs 4 断言过 + 0 console error + live 库零残留;tsc+build 绿。 注:apps.ai 3 个 image_edit 路由测试失败为进场前既有(已 stash 比对 HEAD 同样失败),非本 Phase 引入。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
135 lines
4.7 KiB
TypeScript
135 lines
4.7 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { X } from "lucide-react";
|
|
import { adminApi } from "../../api";
|
|
import { IconKitSvg } from "../../components/IconKitSvg";
|
|
import type { AdminQualityWord } from "../../types";
|
|
|
|
type Notify = (type: "success" | "error" | "info", text: string) => void;
|
|
|
|
// 与后端 QualityWord.Stage 对齐(生成侧已接入这 4 个阶段)
|
|
const STAGES = [
|
|
{ key: "person", label: "人物立绘" },
|
|
{ key: "model_tryon", label: "模特上身图" },
|
|
{ key: "storyboard", label: "故事板" },
|
|
{ key: "video", label: "视频" }
|
|
];
|
|
|
|
export function AdminQualityPage({ notify }: { notify: Notify }) {
|
|
const [words, setWords] = useState<AdminQualityWord[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [adding, setAdding] = useState<Record<string, string>>({});
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
setWords(await adminApi.qualityWords());
|
|
} catch {
|
|
notify("error", "加载质量词失败");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
useEffect(() => { void load(); }, [load]);
|
|
|
|
async function add(stage: string) {
|
|
const text = (adding[stage] || "").trim();
|
|
if (!text) return;
|
|
try {
|
|
await adminApi.createQualityWord({ stage, text });
|
|
setAdding((a) => ({ ...a, [stage]: "" }));
|
|
notify("success", "已添加质量词");
|
|
await load();
|
|
} catch {
|
|
notify("error", "添加失败");
|
|
}
|
|
}
|
|
|
|
async function saveText(w: AdminQualityWord, text: string) {
|
|
const t = text.trim();
|
|
if (!t || t === w.text) return;
|
|
try {
|
|
await adminApi.updateQualityWord(w.id, { text: t });
|
|
notify("success", "已更新");
|
|
await load();
|
|
} catch {
|
|
notify("error", "更新失败");
|
|
}
|
|
}
|
|
|
|
async function del(w: AdminQualityWord) {
|
|
try {
|
|
await adminApi.deleteQualityWord(w.id);
|
|
notify("success", "已删除");
|
|
await load();
|
|
} catch {
|
|
notify("error", "删除失败");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="page-head">
|
|
<div>
|
|
<h1>质量词</h1>
|
|
<div className="sub"><span className="mono">// 平台单层</span> · 各生成阶段的画质 / 风格词,留空则用默认</div>
|
|
</div>
|
|
<div className="actions">
|
|
<button className={`btn${editMode ? " btn-primary" : ""}`} type="button" onClick={() => setEditMode((v) => !v)}>
|
|
{editMode ? "完成编辑" : "编辑模式"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="empty-state show"><div className="ic-empty"><IconKitSvg name="type" size={24} /></div><h3>加载中…</h3><p>// fetching quality words</p></div>
|
|
) : (
|
|
<div className="qw-stages">
|
|
{STAGES.map((s) => {
|
|
const ws = words.filter((w) => w.stage === s.key);
|
|
return (
|
|
<div key={s.key} className="card-hard qw-stage">
|
|
<div className="qw-stage-h">
|
|
<span className="qw-stage-title">{s.label}</span>
|
|
<span className="mono qw-stage-hint">// {s.key}</span>
|
|
</div>
|
|
{ws.length === 0 && !editMode && <div className="qw-empty mono">未配置 · 使用默认质量词</div>}
|
|
<div className="qw-chips">
|
|
{ws.map((w) =>
|
|
editMode ? (
|
|
<span key={w.id} className="qw-chip editing">
|
|
<input
|
|
className="qw-chip-input"
|
|
defaultValue={w.text}
|
|
onBlur={(e) => saveText(w, e.target.value)}
|
|
onKeyDown={(e) => { if (e.key === "Enter") e.currentTarget.blur(); }}
|
|
/>
|
|
<button className="qw-del" type="button" aria-label="删除" onClick={() => del(w)}><X size={12} /></button>
|
|
</span>
|
|
) : (
|
|
<span key={w.id} className="qw-chip">{w.text}</span>
|
|
)
|
|
)}
|
|
{editMode && (
|
|
<span className="qw-add">
|
|
<input
|
|
className="qw-chip-input"
|
|
placeholder="+ 添加质量词"
|
|
value={adding[s.key] || ""}
|
|
onChange={(e) => setAdding((a) => ({ ...a, [s.key]: e.target.value }))}
|
|
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void add(s.key); } }}
|
|
/>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|