后端生成闸+多项修复;前端全站更新;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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user