后端生成闸+多项修复;前端全站更新;QA 审计与报告

后端:
- 新增 celery_health 生成前置闸——无 worker 在线时图片/视频生成入口
  一律 503,防"提交到 ARK 后无人轮询、结果悬空+额度冻结"的数据丢失
- 拼接导出:帧率跟随源众数、字幕逐句重映射、原声/BGM 混音修复
- 资金核算审计脚本 + genesis 账目回填 + 滞留预留清理命令
- 接入 yunqi provider 与豆包 TTS 模型(catalog/migrations/bootstrap 命令)

前端:全站页面更新(pipeline/library/products/projects/team/account 等),
新增共享 pager 分页组件

QA:刷新 function-audit 全量输出,新增 full-qa 报告
文档:BP 产品介绍资料、design/CLAUDE.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-15 10:06:15 +08:00
co-authored by Claude Fable 5
parent 890cb9ab67
commit 216a711291
92 changed files with 5114 additions and 2027 deletions
+51 -21
View File
@@ -3,8 +3,11 @@ import type { CSSProperties, FormEvent } from "react";
import type { Product, Project } from "../types";
import type { Page } from "./route-config";
import { ConfirmModal, EmptyPanel } from "../components/overlays";
import { Pager } from "../components/pager";
import "../project-wizard-page.css";
const PROJ_PAGE_SIZE = 10; // 项目列表/网格每页条数
// 时长 / 脚本风格 / 人设 — 与 projects-new.html 基线对齐(创建页仅作视觉选择,
// 脚本走向细节进入 Stage 1;onCreate 契约只携带 name + product)。
const WIZ_DURATIONS = [
@@ -29,14 +32,18 @@ const WIZ_PERSONAS = [
const WIZ_PAGE_SIZE = 7; // 4 列 × 2 行 = 8 格,首格为「创建新商品」→ 每页 7 商品
export function ProjectWizardPage({ products, onBack, onCreate }: {
export function ProjectWizardPage({ products, projects = [], onBack, onCreate }: {
products: Product[];
projects?: Project[];
onBack: () => void;
onCreate: (payload: { name: string; product: string; metadata?: Record<string, unknown> }) => Promise<unknown> | void;
}) {
const [productId, setProductId] = useState(products[0]?.id || "");
const product = products.find((item) => item.id === productId) || products[0];
const [name, setName] = useState(() => `${(products[0]?.title || "商品").split(" ")[0]} · 痛点种草 · v1`);
// 默认项目名跟随所选商品+脚本风格,版本号按已有同名项目自动递增(避免连建多个同名 v1);
// 用户手动改过名字后不再覆盖。
const [name, setName] = useState("");
const [nameTouched, setNameTouched] = useState(false);
// Step 1 · 商品选择器本地交互态
const [pickSearch, setPickSearch] = useState("");
@@ -56,6 +63,15 @@ export function ProjectWizardPage({ products, onBack, onCreate }: {
if (!productId && products[0]) setProductId(products[0].id);
}, [productId, products]);
useEffect(() => {
if (nameTouched) return;
const styleName = WIZ_STYLES.find((s) => s.id === scriptStyle)?.name || "痛点种草";
const base = `${(product?.title || "商品").split(" ")[0]} · ${styleName}`;
let version = 1;
while (projects.some((p) => p.name === `${base} · v${version}`)) version += 1;
setName(`${base} · v${version}`);
}, [nameTouched, product, scriptStyle, projects]);
// 分类清单
const cats = useMemo(
() => ["全部", ...Array.from(new Set(products.map((p) => p.category || "未分类")))],
@@ -119,23 +135,30 @@ export function ProjectWizardPage({ products, onBack, onCreate }: {
const config2Done = !!duration && !!styleObj && name.trim().length >= 2;
const canStart = product1Done && config2Done;
function submit(event: FormEvent) {
// 提交中状态:创建项目后 App 端全量刷新需数秒,按钮要给「创建中…」反馈,避免用户以为没点上
const [starting, setStarting] = useState(false);
async function submit(event: FormEvent) {
event.preventDefault();
if (!canStart || !product) return;
if (!canStart || !product || starting) return;
// 向导选项(时长档/脚本风格/人设/选中卖点)随项目一起持久化进 metadata,Stage 1 生成脚本时可用
const selectedPoints = Object.entries(points).filter(([, on]) => on).map(([id]) => id);
void onCreate({
name: name.trim() || `${product.title} · 短视频`,
product: product.id,
metadata: {
wizard: {
duration,
script_style: scriptStyle,
persona,
selling_point_ids: selectedPoints
setStarting(true);
try {
await onCreate({
name: name.trim() || `${product.title} · 短视频`,
product: product.id,
metadata: {
wizard: {
duration,
script_style: scriptStyle,
persona,
selling_point_ids: selectedPoints
}
}
}
});
});
} finally {
setStarting(false);
}
}
const productCover = (p: Product): CSSProperties | undefined => {
@@ -273,7 +296,7 @@ export function ProjectWizardPage({ products, onBack, onCreate }: {
<div className="config-row">
<div className="field">
<label className="field-label">项目名称<span className="req">*</span></label>
<input className="input" value={name} onChange={(event) => setName(event.target.value)} />
<input className="input" value={name} onChange={(event) => { setName(event.target.value); setNameTouched(true); }} />
</div>
<div className="field">
<label className="field-label">视频时长<span className="req">*</span></label>
@@ -346,9 +369,9 @@ export function ProjectWizardPage({ products, onBack, onCreate }: {
{/* ── 底部「开始」CTA ── */}
<div className="wiz-start-bar">
<button className={`btn-start${canStart ? "" : " disabled"}`} type="submit" disabled={!canStart}>
<button className={`btn-start${canStart && !starting ? "" : " disabled"}`} type="submit" disabled={!canStart || starting}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M5 3l14 9-14 9V3z" /></svg>
<span>开始</span>
<span>{starting ? "创建中…" : "开始"}</span>
</button>
</div>
</div>
@@ -455,6 +478,12 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
}
return true;
});
// 分页:每页 10 个,切 tab / 搜索 / 筛选回第 1 页(列表与网格共用)
const [page, setPage] = useState(1);
useEffect(() => { setPage(1); }, [tab, query, catFilter, sourceFilter, timeFilter]);
const totalPages = Math.max(1, Math.ceil(filtered.length / PROJ_PAGE_SIZE));
const curPage = Math.min(page, totalPages);
const pageItems = filtered.slice((curPage - 1) * PROJ_PAGE_SIZE, curPage * PROJ_PAGE_SIZE);
async function confirmDelete() {
if (!deleteTarget) return;
@@ -545,7 +574,7 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
</div>
</div>
<div className="result-meta" id="result-meta">// 显示 <span className="count">{filtered.length}</span> / {projects.length} 个项目</div>
<div className="result-meta" id="result-meta">// 显示 <span className="count">{pageItems.length}</span> / {filtered.length} 个项目</div>
{view === "list" ? (
<div id="list-view">
@@ -562,7 +591,7 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
</tr>
</thead>
<tbody id="list-tbody">
{filtered.map((project) => {
{pageItems.map((project) => {
const no = projStageNo(project);
const shots = project.video_segments.length || 4;
const cover = projCover(project.name);
@@ -603,7 +632,7 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
</table>
</div>
) : (
<div className="proj-grid">{filtered.map((project) => {
<div className="proj-grid">{pageItems.map((project) => {
const cover = projCover(project.name);
return (
<article className="proj-card" key={project.id} onClick={() => openPipeline(project.id)}>
@@ -617,6 +646,7 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
);
})}</div>
)}
<Pager page={curPage} total={filtered.length} pageSize={PROJ_PAGE_SIZE} onChange={setPage} />
{filtered.length === 0 && <EmptyPanel title="当前筛选下没有项目" action="新建视频项目" onAction={() => navigate("projectWizard")} />}
<ConfirmModal open={Boolean(deleteTarget)} title="确认删除项目" detail={`即将删除 ${deleteTarget?.name || ""}。`} confirmText="删除" onCancel={() => setDeleteTarget(null)} onConfirm={confirmDelete} />
</section>