fix: 测试清单(8) 新一轮 — ZWQ/PMC/设置/团队 多项修复

视频/工作台/商品库(ZWQ):
- row19 视频阶段移除已取消的【上传视频】按钮 (pipeline.tsx)
- row20 工作台统计栏(总项目/进行中/成片)点击跳转视频项目对应 tab (dashboard/projects/route-config/App)
- row21 最新项目跳转改用真实 projectId, 不再跳到最近打开项目 (dashboard.tsx)
- row22+PMC28 网格/列表视图模式持久化(localStorage), 跨菜单记住用户选择 (projects/ai-tools + 新 hook use-view-mode.ts)
- row24 已有三视图的演员/模特直接复用, 不再强制点AI生成三视图 (pipeline.tsx)

图片生成/设置(PMC):
- row27 任务中心: 6个进行中状态补全归类, 不再把生成中误判为失败栏 (stage-config.ts)
- row29 头像上传500: avatar_url 存公读直链替代超长预签名URL(URLField max_length=200溢出) (accounts/views.py, 需部署)
- row30 显示名保存后回显: 改读 first_name 回退 username, 修刷新仍旧名 (settings.tsx)
- row31/32 隐藏【创作默认】【显示】两个设置模块 (settings.tsx)

平台套图(YYX):
- row3 图片预览铺满占位 (ai-tools-page.css)

团队:
- PMC25 充值/邀请成员/设置月限额三弹窗禁止点遮罩误关 (team.tsx)

含平台套图线上提示词优化(services.py/views.py/api.ts, 前序会话遗留一并提交)
构建: tsc --noEmit=0, npm run build=0 (1755模块); 后端 py_compile OK
注: row29 头像/后端项需部署生效

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-06-29 16:45:29 +08:00
co-authored by Claude Opus 4.8
parent f3b89f3d49
commit ffada7caab
15 changed files with 292 additions and 82 deletions
+12 -19
View File
@@ -1615,8 +1615,6 @@ export function PipelinePage(props: {
const [propsTab, setPropsTab] = useState<"subtitle" | "transition" | "bgm">("subtitle");
const edHydratedRef = useRef(false);
const bgmFileRef = useRef<HTMLInputElement | null>(null);
const videoUploadRef = useRef<HTMLInputElement | null>(null);
const [uploadTargetSeg, setUploadTargetSeg] = useState<string | null>(null);
// 提交一个新编辑态(进 undo 栈)
const commitEdit = useCallback((next: EditorState) => {
setEdHistory((h) => [...h.slice(-49), edState]);
@@ -2350,16 +2348,6 @@ export function PipelinePage(props: {
// (旧的转场黑闪提示层已删:双缓冲槽位的 opacity 过渡就是真转场预览)
// Stage 4 / 5 文件上传
function onPickVideoFile(event: ChangeEvent<HTMLInputElement>) {
const file = event.target.files?.[0];
event.target.value = "";
if (file && uploadTargetSeg) onUploadVideoSegment(uploadTargetSeg, file);
setUploadTargetSeg(null);
}
function triggerVideoUpload(segmentId: string) {
setUploadTargetSeg(segmentId);
videoUploadRef.current?.click();
}
// 导出全部:把本项目所有已采用视频片段打成一个 zip 下载
const [exporting, setExporting] = useState(false);
const [exportErr, setExportErr] = useState("");
@@ -3355,18 +3343,12 @@ export function PipelinePage(props: {
<div className="bar-wrap"><span style={{ width: `${pct}%` }}></span></div>
<span className="muted mono" style={{ fontSize: "12px" }}>{pct}%</span>
<button className="btn btn-sm btn-primary" type="button" disabled={loading || !segments.length || activeVideoCount > 0} onClick={() => guardVideoGen(shots, null, submitAllVideosOptimistic)}>{loading ? <><span className="spinner btn-spin" aria-hidden="true" />提交中…</> : anyStarted ? "↻ 全部重跑" : "▶ 开始生成视频"}</button>
<button className="btn btn-sm" type="button" disabled={loading || !segments.length} onClick={() => triggerVideoUpload((segments.find((s) => s.status !== "succeeded") || segments[0]).id)}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: "4px" }}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="M17 8l-5-5-5 5" /><path d="M12 3v12" /></svg>
上传视频
</button>
{/* 导出全部:把所有已完成片段打包成 zip 一次性下载 */}
<button className="btn btn-sm" type="button" disabled={exporting || segDone === 0} title={exportErr || "把所有已完成视频片段打包下载"} onClick={() => void exportAllVideos()}>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ marginRight: "4px" }}><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="M7 10l5 5 5-5" /><path d="M12 15V3" /></svg>
{exporting ? "导出中…" : exportErr ? "导出失败" : "导出全部"}
</button>
</div>
<input ref={videoUploadRef} type="file" accept="video/*" style={{ display: "none" }} onChange={onPickVideoFile} />
{segments.length ? (
<div className="video-grid" id="video-grid">
{segments.map((seg) => {
@@ -3963,6 +3945,17 @@ export function PipelinePage(props: {
const triVersions = triGroup?.candidate_assets ?? [];
const viewTriAsset = (adTriId && triVersions.includes(adTriId)) ? adTriId : (triGroup?.adopted_asset || triVersions.at(-1) || "");
const triUrl = candUrl(triGroup, viewTriAsset);
// 已有三视图判定(ZWQ-row24):优先看 BaseGroup(流水线内生成的三视图组);其次看资产 metadata
// 沿用 library.tsx 同款判定:tri_view/triview/has_triview/three_view/tri_views 任一为真即已有;
// 模特库正面图(metadata.view==="frontal")本身与三视图同批生成,直接算已就绪。
const assetAlreadyHasTri: boolean = (() => {
if (triUrl) return true;
const m: Record<string, unknown> = (viewPortraitAsset ? byId.get(viewPortraitAsset)?.metadata : null) || {};
const has = m.tri_view ?? m.triview ?? m.has_triview ?? m.three_view ?? m.tri_views;
if (has === true || (Array.isArray(has) && has.length > 0) || (typeof has === "string" && (has as string).trim())) return true;
if (m.view === "frontal") return true; // 模特库正面图:generate_model 同批出了三视图
return false;
})();
const pBK = `addet-portrait:${entity.key}`;
// 没立绘时(尤其 seed 首次生成),立绘在 worker 跑 ~30s,本地 isBusy 早已落回 → 用 pendingHas 兜底转圈
const busyPortrait = isBusy(pBK) || (!portraitUrl && pendingHas(adDetail.kind, entity.name));
@@ -4037,7 +4030,7 @@ export function PipelinePage(props: {
{triUrl && <button className="ad-zoom-btn" type="button" title="查看大图" onClick={() => setPreview({ src: triUrl, kind: "image", name: `${entity.name} · 三视图` })}>{zoomSvg}</button>}
</div>
</div>
{!triUrl && !busyTri && (
{!assetAlreadyHasTri && !busyTri && (
<div className="asset-detail-tip">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="9" /><path d="M12 8v4M12 16h.01" /></svg>
<span>{viewPortraitAsset ? "三视图据这版立绘生成,保证正/侧/背多角度一致。点下方按钮生成。" : "请先生成左侧「立绘」,有了立绘才能据它生成三视图。"}</span>