perf(core): 项目列表改轻量序列化 + 计数走真实 count,根治刷新 2-3s

/api/projects/ 列表此前与详情共用重序列化器(嵌套 阶段/片段/故事板/时间线 + 每资产算 url),
20 个项目把全套关联拉出 → 实测 ~2-3s,正是每次刷新慢的主因。

- 新增 ProjectListSerializer(轻:名/状态/阶段/product/封面/脚本数/镜数);
  ProjectViewSet.get_serializer_class 列表用轻的、详情用全量;
  get_queryset 列表走轻查询(select_related product + annotate 计数,不做那串 12 个重 prefetch)。
  实测 /api/projects/ 2-3s → 0.48s。
- 新增 /api/projects/summary/(total + by_status,只跑 COUNT,不拉项目对象)。
- 计数显示(侧栏徽标 / 仪表盘 总项目·SKU·N 个)改用分页响应的真实 count(productTotal/projectTotal),
  不再用「已加载 20 条」的长度;projects 列表页脚本状态/镜数改读 *_count 字段。

闸:9 页全绿、无瀑布、API 全过;projects 端点提速 ~5x。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 19:27:10 +08:00
co-authored by Claude Opus 4.8
parent c62abed536
commit a0ed8666df
7 changed files with 76 additions and 11 deletions
+7 -3
View File
@@ -6,13 +6,17 @@ import { money, stageMeta, statusPill } from "./stage-config";
import { Progress } from "../components/pipeline-stage";
import { IconKitSvg } from "../components/IconKitSvg";
export function Dashboard({ products, projects, billing, userName, navigate }: {
export function Dashboard({ products, projects, productTotal, projectTotal, billing, userName, navigate }: {
products: Product[];
projects: Project[];
productTotal?: number; // 后端真实总数(分页 count),用于「N 个/SKU」展示
projectTotal?: number;
billing: BillingSummary | null;
userName?: string;
navigate: (page: Page) => void;
}) {
const projCount = projectTotal ?? projects.length;
const prodCount = productTotal ?? products.length;
// 资产总数:走轻量 summary 接口(各 tab 计数之和),不再吃全局 assets 全量数组
const [assetCount, setAssetCount] = useState<number | null>(null);
useEffect(() => {
@@ -43,8 +47,8 @@ export function Dashboard({ products, projects, billing, userName, navigate }: {
</button>
</div>
</div>
<div className="stats with-corners"><span className="corner-tr">+</span><span className="corner-bl">+</span><KpiStat label="总项目" badge="ALL" value={projects.length} delta={`↑ 本月 +${Math.max(projects.length, 0)}`} /><KpiStat label="进行中" badge="WIP" value={running} delta="待处理" /><KpiStat label="成片" badge="DONE" value={completed} delta="导出完成" /><button className="stat" type="button" onClick={() => navigate("account")}><div className="lbl"> <span className="badge">¥</span></div><div className="v">{money(billing?.account.balance)}</div><div className="bar"><span style={{ width: "38%" }} /></div><div className="sub"> {money(billing?.account.reserved_balance)}</div></button></div>
<div className="dash-grid"><div><div className="section-h"><h2></h2><button className="more" type="button" onClick={() => navigate("projects")}>[ ALL · {projects.length} ] </button></div><div className="card-hard">{projects.slice(0, 6).map((project) => <button className="recent-row" key={project.id} type="button" onClick={() => navigate("pipeline")}><div className="placeholder thumb"><span className="ph-frame">9:16</span></div><div className="recent-meta"><div className="name">{project.name}</div><div className="sub">{productTitle(project.product)} / AI / 4 </div></div><Progress status={project.current_stage} /><span className={`pill ${statusPill(project.status)}`}><span className="dot" />{stageMeta[project.current_stage]?.label || project.current_stage}</span><span className="btn btn-sm"></span></button>)}</div></div><div style={{ display: "flex", flexDirection: "column", gap: 24 }}><div><div className="section-h"><h2></h2><span className="more">[ /shortcuts ]</span></div><div className="shortcuts"><Shortcut name="package" title="" desc={`${products.length} SKU`} onClick={() => navigate("products")} /><Shortcut name="images" title="" desc={`${assetCount ?? ""} `} onClick={() => navigate("library")} /><Shortcut name="creditCard" title="" desc={money(billing?.account.balance)} onClick={() => navigate("account")} /><Shortcut name="clapperboard" title="" desc={`${projects.length} `} onClick={() => navigate("projects")} /></div></div><div><div className="section-h"><h2></h2><span className="more">[ FAQ ]</span></div><div className="tip"><strong></strong> <span className="mono">[ ]</span> token </div></div></div></div>
<div className="stats with-corners"><span className="corner-tr">+</span><span className="corner-bl">+</span><KpiStat label="总项目" badge="ALL" value={projCount} delta={`↑ 本月 +${Math.max(projCount, 0)}`} /><KpiStat label="进行中" badge="WIP" value={running} delta="待处理" /><KpiStat label="成片" badge="DONE" value={completed} delta="导出完成" /><button className="stat" type="button" onClick={() => navigate("account")}><div className="lbl"> <span className="badge">¥</span></div><div className="v">{money(billing?.account.balance)}</div><div className="bar"><span style={{ width: "38%" }} /></div><div className="sub"> {money(billing?.account.reserved_balance)}</div></button></div>
<div className="dash-grid"><div><div className="section-h"><h2></h2><button className="more" type="button" onClick={() => navigate("projects")}>[ ALL · {projCount} ] </button></div><div className="card-hard">{projects.slice(0, 6).map((project) => <button className="recent-row" key={project.id} type="button" onClick={() => navigate("pipeline")}><div className="placeholder thumb"><span className="ph-frame">9:16</span></div><div className="recent-meta"><div className="name">{project.name}</div><div className="sub">{productTitle(project.product)} / AI / 4 </div></div><Progress status={project.current_stage} /><span className={`pill ${statusPill(project.status)}`}><span className="dot" />{stageMeta[project.current_stage]?.label || project.current_stage}</span><span className="btn btn-sm"></span></button>)}</div></div><div style={{ display: "flex", flexDirection: "column", gap: 24 }}><div><div className="section-h"><h2></h2><span className="more">[ /shortcuts ]</span></div><div className="shortcuts"><Shortcut name="package" title="" desc={`${prodCount} SKU`} onClick={() => navigate("products")} /><Shortcut name="images" title="" desc={`${assetCount ?? ""} `} onClick={() => navigate("library")} /><Shortcut name="creditCard" title="" desc={money(billing?.account.balance)} onClick={() => navigate("account")} /><Shortcut name="clapperboard" title="" desc={`${projCount} `} onClick={() => navigate("projects")} /></div></div><div><div className="section-h"><h2></h2><span className="more">[ FAQ ]</span></div><div className="tip"><strong></strong> <span className="mono">[ ]</span> token </div></div></div></div>
</>
);
}
+3 -3
View File
@@ -528,7 +528,7 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
if (!`${project.name} ${productTitle(project.product)}`.toLowerCase().includes(query.toLowerCase())) return false;
if (catFilter && productCat(project.product) !== catFilter) return false;
if (sourceFilter !== "all") {
const hasScript = (project.script_versions?.length || 0) > 0;
const hasScript = (project.script_version_count ?? project.script_versions?.length ?? 0) > 0;
if (sourceFilter === "has" && !hasScript) return false;
if (sourceFilter === "none" && hasScript) return false;
}
@@ -663,7 +663,7 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
<tbody id="list-tbody">
{pageItems.map((project) => {
const no = projStageNo(project);
const shots = project.video_segments.length || 4;
const shots = project.video_segment_count ?? project.video_segments?.length ?? 4;
const cover = projCover(project.name);
return (
<tr key={project.id} data-status={projBucket(project)} data-name={project.name} onClick={() => openPipeline(project.id)}>
@@ -674,7 +674,7 @@ export function ProjectsPage({ products, projects, navigate, openPipeline, onDel
</div>
</td>
<td>{productTitle(project.product)}</td>
<td><span className="muted">{(project.script_versions?.length || 0) > 0 ? "AI 已生成" : "暂无脚本"}</span></td>
<td><span className="muted">{(project.script_version_count ?? project.script_versions?.length ?? 0) > 0 ? "AI 已生成" : "暂无脚本"}</span></td>
<td>
<div className="hstack">
<div className="prog">{[1, 2, 3, 4, 5].map((i) => <span key={i} className={i < no ? "done" : i === no ? "cur" : ""} />)}</div>