fix(core): 删脚本分镜联动删故事板帧 + 演员库自取 person + 分镜编辑行抽组件

- views.py: delete-script-segment 同步删对应 StoryboardFrame(原 SET_NULL 留孤儿帧致故事板仍显示已删的场),并重排剩余帧 sort_order
- actor-library.tsx: 演员库改自取 person 资产(全局 assets 已懒加载不再全量预载,否则演员库全空),保存后 reload 即时入列
- pipeline.tsx: 抽出 EditableShotField 组件、PromptBox 参数化
- 附 perf-report 产物

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-17 19:18:15 +08:00
co-authored by Claude Opus 4.8
parent f1bc5933ef
commit c62abed536
5 changed files with 174 additions and 182 deletions
+11
View File
@@ -43,6 +43,8 @@ from .models import (
ProjectStage,
ScriptSegment,
ScriptVersion,
StoryboardFrame,
StoryboardVersion,
SubtitleTrack,
Timeline,
TimelineClip,
@@ -429,6 +431,15 @@ class ProjectViewSet(TeamScopedViewSetMixin, ModelViewSet):
script = segment.script_version
if script.segments.count() <= 1:
return Response({"detail": "至少保留一个分镜"}, status=status.HTTP_400_BAD_REQUEST)
# 删本场脚本时,它对应的故事板分镜帧也一起删 —— frame.script_segment 是 SET_NULL,
# 不主动删会留下孤儿帧,故事板里仍显示这一(已删的)场。各版本受影响的帧统一收口后重排 sort_order。
affected_storyboards = list(StoryboardVersion.objects.filter(frames__script_segment=segment).distinct())
StoryboardFrame.objects.filter(script_segment=segment).delete()
for sb in affected_storyboards:
for index, frame in enumerate(sb.frames.order_by("sort_order")):
if frame.sort_order != index:
frame.sort_order = index
frame.save(update_fields=["sort_order", "updated_at"])
segment.delete()
for index, seg in enumerate(script.segments.order_by("sort_order")):
if seg.sort_order != index:
+19 -3
View File
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { CSSProperties } from "react";
import { api } from "../api";
import type { Asset } from "../types";
import { useBodyScrollLock, MediaLightbox } from "./overlays";
@@ -52,8 +53,22 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
setCandidates([]); setPicked(null); setActorName(""); setTriReady(false);
}, [open, initialStudio]);
// person 资产 → 演员;按 preset / mine 分两 tab
const people = useMemo(() => assets.filter((a) => a.category === "person" && previewOf(a)), [assets]);
// 演员库自取 person 资产:全局 assets 已不再全量预载(按需懒加载),不能再依赖传入的 assets prop,
// 否则平台预设/我的演员全空。打开时拉一页(团队 person 资产),保存后 reload 让新人物即时入列。
const [fetched, setFetched] = useState<Asset[]>([]);
const reload = useCallback(async () => {
const res = await api.assetsPage({ category: "person", pageSize: 200, ordering: "-created_at" }).catch(() => null);
setFetched(res?.results ?? []);
}, []);
useEffect(() => { if (open) void reload(); }, [open, reload]);
// person 类资产 → 演员;按 preset / mine 分两 tab。合并 prop(兼容旧调用)+ 自取,按 id 去重。
const source = useMemo(() => {
const map = new Map<string, Asset>();
for (const a of [...assets, ...fetched]) map.set(a.id, a);
return [...map.values()];
}, [assets, fetched]);
const people = useMemo(() => source.filter((a) => a.category === "person" && previewOf(a)), [source]);
const list = people.filter((a) => (tab === "preset" ? isPreset(a) : !isPreset(a)));
// 选某张候选立绘 → 进右侧栏(预填名字,清三视图标志)
@@ -99,6 +114,7 @@ export function ActorLibrary({ open, mode, initialStudio, assets, onClose, onPic
const name = actorName.trim();
if (name && name !== picked.name && onRename) await onRename(picked.id, name);
onRefresh();
void reload(); // 新保存的人物即时进「我的演员」列表(自取,不依赖父级全局 assets)
setStudio(false);
setCandidates([]); setPicked(null); setActorName(""); setTriReady(false);
setTab("mine");
+6 -5
View File
@@ -564,11 +564,7 @@
.draft-shot-input:focus { border-color: var(--heat); box-shadow: 0 0 0 2px var(--heat-12); }
.draft-shot-input::placeholder { color: var(--black-alpha-32); }
/* ── 行38 · 资产卡可编辑提示词 + 重跑/替换 ── */
.asset-prompt-edit { width: 100%; margin-top: 8px; font-family: var(--font-mono); font-size: 12px; line-height: 1.55; letter-spacing: .01em; color: var(--black-alpha-72); background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 8px 10px; outline: none; resize: vertical; min-height: 56px; transition: border-color var(--t-base), background var(--t-base), box-shadow var(--t-base); }
.asset-prompt-edit:hover { border-color: var(--heat-20); }
.asset-prompt-edit:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
.asset-prompt-edit::placeholder { color: var(--black-alpha-32); }
/* ── 行38 · 资产卡可编辑提示词 + 重跑/替换 ──(.asset-prompt-edit 移到顶层:演员库弹窗 portal 到 body,嵌在 .pipeline-page 内会失效) */
.asset-card-actions { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
.asset-card-actions .btn { display: inline-flex; align-items: center; }
@@ -680,6 +676,11 @@
@keyframes edXfadeFlash { from { opacity: 0.72; } to { opacity: 0; } }
/* ── 流程步骤4 · 演员库覆盖层(portal 到 body,顶层选择器才生效)── */
/* 资产卡/演员库工作台的提示词输入框:顶层定义,页面内(资产卡)与 portal 弹窗(添加人物工作台)都生效 */
.asset-prompt-edit { width: 100%; margin-top: 8px; font-family: var(--font-mono); font-size: 12px; line-height: 1.55; letter-spacing: .01em; color: var(--black-alpha-72); background: var(--background-base); border: 1px solid var(--border-faint); border-radius: var(--r-sm); padding: 8px 10px; outline: none; resize: vertical; min-height: 56px; transition: border-color var(--t-base), background var(--t-base), box-shadow var(--t-base); }
.asset-prompt-edit:hover { border-color: var(--heat-20); }
.asset-prompt-edit:focus { border-color: var(--heat); background: var(--surface); color: var(--accent-black); box-shadow: 0 0 0 3px var(--heat-12); }
.asset-prompt-edit::placeholder { color: var(--black-alpha-32); }
.actorlib-bg { position: fixed; inset: 0; background: rgba(0,0,0,.42); z-index: 1020; display: flex; align-items: center; justify-content: center; padding: 40px; }
.actorlib { background: var(--surface); border: 1px solid var(--border-faint); border-radius: var(--r-md); width: min(920px, 100%); max-height: calc(100vh - 80px); overflow: hidden; display: flex; flex-direction: column; box-shadow: 0 16px 48px rgba(0,0,0,.18); }
.actorlib-h { display: flex; align-items: center; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border-faint); }
+84 -73
View File
@@ -270,10 +270,10 @@ function AddTagInline({ onAdd, placeholder, ariaLabel }: { onAdd?: (value: strin
// DOM,React 不再 reconcile 其子节点。受控写法(把 state 当 children 回灌)在输入触发换行时,浏览器会
// 往 contentEditable 里插 <div>/<br>,React 拿单个文本子节点去对账就会 removeChild 崩溃 → 整页白屏。
// 读取走 onInput 写回上层 state(供「生成/重跑」带上),与渲染解耦,既能编辑又不崩。
function PromptBox({ value, onChange }: { value: string; onChange: (v: string) => void }) {
function PromptBox({ value, onChange, className = "prompt-box", id, ariaLabel, stop = true }: { value: string; onChange: (v: string) => void; className?: string; id?: string; ariaLabel?: string; stop?: boolean }) {
const ref = useRef<HTMLDivElement | null>(null);
// 只挂载时灌初值;后续用户编辑由 DOM 自己维护,不受 value 变化驱动(避免重渲染打断输入/白屏)。
// 切换实体/草稿时上层用 key 强制重挂载,会重新走这次初值灌入,所以不需要把 value 进依赖。
// 切换实体/草稿/版本时上层用 key 强制重挂载,会重新走这次初值灌入,所以不需要把 value 进依赖。
useEffect(() => {
if (ref.current && ref.current.textContent !== value) ref.current.textContent = value;
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -281,16 +281,50 @@ function PromptBox({ value, onChange }: { value: string; onChange: (v: string) =
return (
<div
ref={ref}
className="prompt-box"
className={className}
id={id}
role={ariaLabel ? "textbox" : undefined}
aria-label={ariaLabel}
contentEditable
suppressContentEditableWarning
spellCheck={false}
data-stop
{...(stop ? { "data-stop": true } : {})}
onInput={(e) => onChange(e.currentTarget.textContent || "")}
/>
);
}
// 分镜「旁白 / 画面」可编辑行:同样**非受控** contentEditable,落库走 onBlur(回车=失焦提交)。
// 受控写法(把 narration 当 children)在提交后父组件用旧值重渲染时,React 会把 DOM 文本重置回旧值 →
// 「按回车后变成空白」。这里不放受控子节点:初值/外部改动(切版本、AI 改稿)由 effect 同步进 DOM,
// 但**正在编辑(聚焦中)时绝不覆盖**,避免打断输入。
function EditableShotField({ value, placeholder, onCommit }: { value: string; placeholder: string; onCommit: (text: string) => void }) {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const el = ref.current;
if (!el || document.activeElement === el) return; // 聚焦中=用户在打字,别覆盖
if (el.textContent !== value) el.textContent = value;
}, [value]);
return (
<div
ref={ref}
className="shot-v"
contentEditable
suppressContentEditableWarning
spellCheck={false}
data-placeholder={placeholder}
data-empty={value ? undefined : "true"}
onFocus={(e) => { e.currentTarget.removeAttribute("data-empty"); }}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); e.currentTarget.blur(); } }}
onBlur={(e) => {
const text = (e.currentTarget.textContent || "").trim();
if (!text) e.currentTarget.setAttribute("data-empty", "true");
if (text !== value) onCommit(text);
}}
/>
);
}
// 行34 · 「添加分镜」插入的本地可编辑空白卡片:输入旁白/画面,提交后落库
function DraftShotCard({ draft, onCommit, onCancel }: {
draft: { id: string; afterId: string | null; narration: string; visual: string };
@@ -2071,39 +2105,19 @@ export function PipelinePage(props: {
</div>
<div className="shot-row">
<span className="shot-k"></span>
<div
className="shot-v"
contentEditable
suppressContentEditableWarning
spellCheck={false}
data-placeholder="(旁白)点击编辑"
data-empty={narration ? undefined : "true"}
onFocus={(event) => { event.currentTarget.removeAttribute("data-empty"); }}
onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); event.currentTarget.blur(); } }}
onBlur={(event) => {
const text = (event.currentTarget.textContent || "").trim();
if (!text) event.currentTarget.setAttribute("data-empty", "true");
if (text !== narration) void onUpdateShot({ segment_id: shot.id, narration: text });
}}
>{narration}</div>
<EditableShotField
value={narration}
placeholder="(旁白)点击编辑"
onCommit={(text) => { void onUpdateShot({ segment_id: shot.id, narration: text }); }}
/>
</div>
<div className="shot-row">
<span className="shot-k"></span>
<div
className="shot-v"
contentEditable
suppressContentEditableWarning
spellCheck={false}
data-placeholder="(画面描述)点击编辑"
data-empty={visualShown ? undefined : "true"}
onFocus={(event) => { event.currentTarget.removeAttribute("data-empty"); }}
onKeyDown={(event) => { if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) { event.preventDefault(); event.currentTarget.blur(); } }}
onBlur={(event) => {
const text = (event.currentTarget.textContent || "").trim();
if (!text) event.currentTarget.setAttribute("data-empty", "true");
if (text !== visualShown) void onUpdateShot({ segment_id: shot.id, visual_prompt: text });
}}
>{visualShown}</div>
<EditableShotField
value={visualShown}
placeholder="(画面描述)点击编辑"
onCommit={(text) => { void onUpdateShot({ segment_id: shot.id, visual_prompt: text }); }}
/>
</div>
{/* ScriptDraft 结构化字段:镜型(钩子/痛点/卖点/CTA)+ 商品露出方式(只读展示) */}
{(shot.role || shot.product_exposure) ? (
@@ -2298,7 +2312,13 @@ export function PipelinePage(props: {
const previewTriAsset = (triPreviewId && productVersions.includes(triPreviewId)) ? triPreviewId : adoptedTriAsset;
const hasTriView = productVersions.length > 0;
const triPanelShow = triPanelOpen || hasTriView || triGenerating;
const productAssetUrl = assetUrl(productCover); // 商品卡显示商品主图,三视图在右侧面板
// 商品主图:优先用序列化器内嵌的 preview_url(全局 assets 已不再全量取,assetUrl 反查会落空),
// 依次 封面 → 主图 → 首图,最后才回退旧的全局反查。
const productAssetUrl =
productRecord?.cover_preview_url
|| productRecord?.images?.find((img) => img.is_primary)?.preview_url
|| productRecord?.images?.[0]?.preview_url
|| assetUrl(productCover); // 商品卡显示商品主图,三视图在右侧面板
const triViewGen = `${productName} 商品三视图,从左到右:正面 / 侧面 / 背面,统一光照,白色背景,16:9`;
const runProductTri = () => { setTriPanelOpen(true); setTriPreviewId(null); void genBaseAsset("product", triViewGen, undefined, "tri:product"); };
return (
@@ -2522,23 +2542,8 @@ export function PipelinePage(props: {
</div>
);
})}
{/* 人物区末尾「添加人物」卡:点开「添加人物工作台」(studio) */}
{kind === "person" && (
<div className="asset-card-2" data-asset-kind="person" data-stop role="button" tabIndex={0}
title="打开添加人物工作台" style={{ cursor: "pointer" }}
onClick={() => setActorLib({ mode: "browse", studio: true })}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setActorLib({ mode: "browse", studio: true }); } }}>
<div className="placeholder thumb-2" style={{ flexDirection: "column", gap: 8 }}>
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M12 5v14M5 12h14" /></svg>
<span className="ph-frame"></span>
</div>
<div className="body-2">
<div className="hstack"><strong style={{ fontSize: "13.5px" }}></strong><span className="spacer"></span><span className="pill neutral"><span className="dot"></span></span></div>
<div className="muted mono" style={{ fontSize: 12, marginTop: 6 }}>// AI 生成 / 本地上传 → 三视图 → 入库</div>
</div>
</div>
)}
{entities.length === 0 && pendingTags.length === 0 && (kind !== "scene" || sceneDrafts.length === 0) && kind !== "person" && (
{/* 添加人物入口统一走右上「+ 新增人物」按钮(原末尾常驻卡与之重复,已移除) */}
{entities.length === 0 && pendingTags.length === 0 && (kind !== "scene" || sceneDrafts.length === 0) && (
<div className="placeholder" style={{ gridColumn: "1 / -1", minHeight: "120px", flexDirection: "column", gap: "10px" }}>
<span className="ph-frame">// 暂无{KIND_LABEL[kind]}资产 · 点右上「新增{KIND_LABEL[kind]}」</span>
</div>
@@ -2616,16 +2621,15 @@ export function PipelinePage(props: {
<div className="note-copy"><strong></strong> · , <a href="#stage-1" onClick={(event) => { event.preventDefault(); goStage(1); }}>Stage 1 </a> ,</div>
</div>
<div className="muted mono" style={{ fontSize: "12px", fontWeight: 500, marginBottom: "6px", letterSpacing: ".04em" }}>// 整张提示词(重跑时生效,可编辑)</div>
{/* key 跟版本走:切版本重新种子;onInput 实时回写 state → 整张重跑用的就是你编辑后的文本 */}
<div
className="prompt-edit"
contentEditable
suppressContentEditableWarning
spellCheck={false}
id="sb-prompt-edit"
{/* key 跟版本走:切版本重新种子;onChange 实时回写 state → 整张重跑用的就是你编辑后的文本 */}
<PromptBox
key={displayedStoryboard?.id || "no-version"}
onInput={(event) => setStoryboardPrompt((event.currentTarget.textContent || "").trim())}
>{displayedStoryboard?.prompt || SB_PROMPT_DEFAULT}</div>
className="prompt-edit"
id="sb-prompt-edit"
stop={false}
value={displayedStoryboard?.prompt || SB_PROMPT_DEFAULT}
onChange={(v) => setStoryboardPrompt(v.trim())}
/>
<div className="sb-stage-actions">
<button className="pill-cta heat" type="button" id="sb-rerun-btn" disabled={loading} onClick={() => onGenerateStoryboard(storyboardPrompt || SB_PROMPT_DEFAULT)}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12a8 8 0 0 1 14-5.5L21 9" /><path d="M21 4v5h-5" /><path d="M20 12a8 8 0 0 1-14 5.5L3 15" /><path d="M3 20v-5h5" /></svg>
@@ -2651,9 +2655,18 @@ export function PipelinePage(props: {
<div className="divider" style={{ marginTop: "16px" }}></div>
<div className="muted mono" style={{ fontSize: "12px", fontWeight: 500, marginBottom: "8px", letterSpacing: ".04em" }}>// 绑定的资产</div>
<div style={{ display: "flex", gap: "6px", flexWrap: "wrap" }} id="sb-bound-assets">
{groups.filter((g) => g.adopted_asset).length ? groups.filter((g) => g.adopted_asset).map((g) => (
<span className="asset-tag" key={g.id}><span className="dotc"></span>{assetName(g.adopted_asset) || KIND_LABEL[g.kind] || g.kind}({KIND_LABEL[g.kind] || g.kind})</span>
)) : <span className="muted-2 mono" style={{ fontSize: "12px" }}>// 暂无绑定资产</span>}
{(() => {
// 与 Stage 2 同源:用 metadata.label 取名(不依赖全局 assets 列表),排除三视图组并按名字去重,
// 否则同一角色的立绘组+三视图组+多版本会各算一个,且名字会退化成「人物(人物)」。
const bound = [
...groupsByKind("product").filter((g) => g.adopted_asset).map((g) => ({ id: g.id, name: (g.metadata?.label || "").trim() || KIND_LABEL.product, kind: "product" as const })),
...buildEntities("person").filter((e) => e.group.adopted_asset).map((e) => ({ id: e.key, name: e.name, kind: "person" as const })),
...buildEntities("scene").filter((e) => e.group.adopted_asset).map((e) => ({ id: e.key, name: e.name, kind: "scene" as const })),
];
return bound.length ? bound.map((b) => (
<span className="asset-tag" key={b.id}><span className="dotc"></span>{b.name}({KIND_LABEL[b.kind]})</span>
)) : <span className="muted-2 mono" style={{ fontSize: "12px" }}>// 暂无绑定资产</span>;
})()}
</div>
</div>
</div>
@@ -3177,16 +3190,14 @@ export function PipelinePage(props: {
<span className="label">// 本场提示词(重跑生效,可编辑)</span>
<span className="mono" style={{ fontSize: "12px", color: "var(--black-alpha-48)" }}></span>
</div>
<div
className="vd-prompt-edit"
contentEditable
suppressContentEditableWarning
spellCheck={false}
role="textbox"
aria-label="视频提示词"
<PromptBox
key={vdSeg.id}
onInput={(event) => setVdPrompt((event.currentTarget.textContent || "").trim())}
>{`${videoPrompt}${vdSeg.sort_order + 1} 段,时长 ${vdSeg.target_duration_seconds}`}</div>
className="vd-prompt-edit"
ariaLabel="视频提示词"
stop={false}
value={`${videoPrompt}${vdSeg.sort_order + 1} 段,时长 ${vdSeg.target_duration_seconds}`}
onChange={(v) => setVdPrompt(v.trim())}
/>
</div>
</div>
</div>
+54 -101
View File
@@ -1,11 +1,11 @@
{
"ts": "2026-06-17T10:24:13.586Z",
"ts": "2026-06-17T10:57:57.302Z",
"base": "http://127.0.0.1:5174",
"apiChecks": [
{
"name": "page_size 生效 /api/assets/",
"pass": true,
"detail": "count=227 返回=200 next=有"
"detail": "count=239 返回=200 next=有"
},
{
"name": "page_size 生效 /api/products/",
@@ -15,31 +15,31 @@
{
"name": "page_size 生效 /api/projects/",
"pass": true,
"detail": "count=19 返回=19 next=无"
"detail": "count=21 返回=21 next=无"
},
{
"name": "延迟 /api/assets/?page_size=200",
"pass": false,
"pass": true,
"soft": true,
"detail": "2235ms / 参考 1200ms (HTTP 200)"
"detail": "720ms / 参考 1200ms (HTTP 200)"
},
{
"name": "延迟 /api/products/?page_size=200",
"pass": true,
"soft": true,
"detail": "962ms / 参考 1200ms (HTTP 200)"
"detail": "597ms / 参考 1200ms (HTTP 200)"
},
{
"name": "延迟 /api/projects/?page_size=200",
"pass": false,
"soft": true,
"detail": "2703ms / 参考 1200ms (HTTP 200)"
"detail": "2004ms / 参考 1200ms (HTTP 200)"
},
{
"name": "延迟 /api/ops/notifications/?page_size=100",
"pass": false,
"soft": true,
"detail": "1959ms / 参考 1200ms (HTTP 200)"
"detail": "1228ms / 参考 1200ms (HTTP 200)"
}
],
"pageFailed": 0,
@@ -47,9 +47,9 @@
{
"name": "dashboard",
"route": "http://127.0.0.1:5174/dashboard",
"unique": 9,
"unique": 6,
"budget": 14,
"rawRequests": 11,
"rawRequests": 7,
"waterfall": 0,
"duplicates": [],
"failed": [],
@@ -58,15 +58,11 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100",
"GET /api/ops/notifications/?page_size=1",
"GET /api/assets/summary/"
],
"elapsedMs": 6445,
"elapsedMs": 5557,
"problems": [],
"warnings": [],
"pass": true
@@ -74,9 +70,9 @@
{
"name": "products",
"route": "http://127.0.0.1:5174/products",
"unique": 8,
"unique": 5,
"budget": 6,
"rawRequests": 10,
"rawRequests": 6,
"waterfall": 0,
"duplicates": [],
"failed": [],
@@ -85,26 +81,20 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100"
"GET /api/ops/notifications/?page_size=1"
],
"elapsedMs": 3892,
"elapsedMs": 3488,
"problems": [],
"warnings": [
"首屏接口数 8 > 预算 6(架构性过量拉取,待决策)"
],
"warnings": [],
"pass": true
},
{
"name": "projects",
"route": "http://127.0.0.1:5174/projects",
"unique": 8,
"unique": 5,
"budget": 6,
"rawRequests": 10,
"rawRequests": 6,
"waterfall": 0,
"duplicates": [],
"failed": [],
@@ -113,26 +103,20 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100"
"GET /api/ops/notifications/?page_size=1"
],
"elapsedMs": 3765,
"elapsedMs": 3502,
"problems": [],
"warnings": [
"首屏接口数 8 > 预算 6(架构性过量拉取,待决策)"
],
"warnings": [],
"pass": true
},
{
"name": "library",
"route": "http://127.0.0.1:5174/library",
"unique": 11,
"unique": 8,
"budget": 8,
"rawRequests": 14,
"rawRequests": 10,
"waterfall": 0,
"duplicates": [
{
@@ -147,20 +131,14 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100",
"GET /api/ops/notifications/?page_size=1",
"GET /api/assets/summary/",
"GET /api/assets/facets/?meta_keys=gender,age,role&tab=people"
],
"elapsedMs": 6012,
"elapsedMs": 5728,
"problems": [],
"warnings": [
"首屏接口数 11 > 预算 8(架构性过量拉取,待决策)"
],
"warnings": [],
"pass": true
},
{
@@ -168,28 +146,22 @@
"route": "http://127.0.0.1:5174/account",
"unique": 8,
"budget": 8,
"rawRequests": 11,
"rawRequests": 9,
"waterfall": 0,
"duplicates": [
{
"key": "GET /api/billing/ledgers/?page=1&page_size=10",
"n": 2
}
],
"duplicates": [],
"failed": [],
"breakdown": [
"GET /api/billing/ledgers/?page=1&page_size=10 ×2",
"GET /api/auth/me/",
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/ai/models/",
"GET /api/ops/notifications/?page_size=1",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100"
"GET /api/billing/ledgers/?page=1&page_size=10"
],
"elapsedMs": 5753,
"elapsedMs": 5743,
"problems": [],
"warnings": [],
"pass": true
@@ -197,9 +169,9 @@
{
"name": "team",
"route": "http://127.0.0.1:5174/team",
"unique": 8,
"unique": 6,
"budget": 4,
"rawRequests": 10,
"rawRequests": 8,
"waterfall": 0,
"duplicates": [],
"failed": [],
@@ -208,26 +180,24 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=1",
"GET /api/auth/team/members/",
"GET /api/ops/notifications/?page_size=100"
],
"elapsedMs": 3555,
"elapsedMs": 5500,
"problems": [],
"warnings": [
"首屏接口数 8 > 预算 4(架构性过量拉取,待决策)"
"首屏接口数 6 > 预算 4(架构性过量拉取,待决策)"
],
"pass": true
},
{
"name": "messages",
"route": "http://127.0.0.1:5174/messages",
"unique": 8,
"unique": 5,
"budget": 4,
"rawRequests": 11,
"rawRequests": 7,
"waterfall": 0,
"duplicates": [],
"failed": [],
@@ -236,27 +206,23 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100",
"GET /api/ops/notifications/?page_size=1",
"GET /api/ops/notifications/?page=1&page_size=10"
],
"elapsedMs": 3468,
"elapsedMs": 3485,
"problems": [],
"warnings": [
"首屏接口数 8 > 预算 4(架构性过量拉取,待决策)"
"首屏接口数 5 > 预算 4(架构性过量拉取,待决策)"
],
"pass": true
},
{
"name": "productDetail",
"route": "http://127.0.0.1:5174/products/045ad8d6-8b15-486b-a4a9-f6968eb1555f",
"unique": 9,
"unique": 6,
"budget": 8,
"rawRequests": 11,
"rawRequests": 7,
"waterfall": 0,
"duplicates": [],
"failed": [],
@@ -265,27 +231,21 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100",
"GET /api/ops/notifications/?page_size=1",
"GET /api/assets/?page_size=200&product=045ad8d6-8b15-486b-a4a9-f6968eb1555f"
],
"elapsedMs": 5956,
"elapsedMs": 5755,
"problems": [],
"warnings": [
"首屏接口数 9 > 预算 8(架构性过量拉取,待决策)"
],
"warnings": [],
"pass": true
},
{
"name": "pipeline",
"route": "http://127.0.0.1:5174/pipeline/1048451e-3b4a-4b66-a8f4-cb865096de2f",
"unique": 11,
"unique": 7,
"budget": 10,
"rawRequests": 13,
"rawRequests": 8,
"waterfall": 0,
"duplicates": [],
"failed": [],
@@ -295,20 +255,13 @@
"GET /api/products/",
"GET /api/projects/",
"GET /api/billing/summary/",
"GET /api/billing/ledgers/?page=1&page_size=10",
"GET /api/billing/trend/",
"GET /api/auth/team/members/",
"GET /api/ai/models/",
"GET /api/ai/tasks/",
"GET /api/ops/notifications/?page_size=100",
"POST /api/projects/1048451e-3b4a-4b66-a8f4-cb865096de2f/generate-base-asset/",
"GET /api/ops/notifications/?page_size=1",
"GET /api/projects/1048451e-3b4a-4b66-a8f4-cb865096de2f/pending-assets/"
],
"elapsedMs": 5702,
"elapsedMs": 6015,
"problems": [],
"warnings": [
"首屏接口数 11 > 预算 10(架构性过量拉取,待决策)"
],
"warnings": [],
"pass": true
}
]