完善二期清单
This commit is contained in:
+42
-17
@@ -15,6 +15,7 @@ import type {
|
||||
Project,
|
||||
Team,
|
||||
TeamMember,
|
||||
TimelineSavePayload,
|
||||
User,
|
||||
UserPreference
|
||||
} from "./types";
|
||||
@@ -127,6 +128,8 @@ export function App() {
|
||||
const [projectDetailError, setProjectDetailError] = useState(false); // 进管线拉详情失败:从「永久 loading」改为可重试,避免卡死进不来
|
||||
const [detailRetry, setDetailRetry] = useState(0); // 手动重试计数,变化即重新触发详情 useEffect
|
||||
const [exportResult, setExportResult] = useState<ExportPoll | null>(null);
|
||||
// 合成成片单飞:合成不占全局 loading,自己守一把锁,防止连点起两轮轮询
|
||||
const exportingRef = useRef(false);
|
||||
const [preferences, setPreferences] = useState<UserPreference | null>(null);
|
||||
const [sessions, setSessions] = useState<LoginSession[]>([]);
|
||||
|
||||
@@ -455,6 +458,44 @@ export function App() {
|
||||
if (res) setExportResult(res);
|
||||
}, [activeProjectId]);
|
||||
|
||||
// 合成成片(把各场视频用 ffmpeg 拼成一条):**不走 action()** —— 拼接要几十秒到几分钟,
|
||||
// 占住全局 loading + 单飞锁会把整条流水线按钮全禁掉(期间用户还想播放/重跑单场)。
|
||||
// 进度靠 exportResult 内联展示;后端对在跑的任务会复用,连点不会拼两遍。
|
||||
// payload 给定(V2 剪辑台)则先落盘编辑态,成片即所见;V1 视频阶段直接合成不传。
|
||||
const submitExport = useCallback(async (payload?: TimelineSavePayload) => {
|
||||
const projectId = activeProjectId;
|
||||
if (!projectId || exportingRef.current) return;
|
||||
exportingRef.current = true;
|
||||
setNotice(null);
|
||||
setExportResult({ status: "running", progress: 0, output_asset: null, output_url: "", error_message: "" });
|
||||
try {
|
||||
if (payload) await api.saveTimeline(projectId, payload);
|
||||
await api.submitExport(projectId);
|
||||
// 后端在后台线程跑 ffmpeg;这里轮询 poll-export 直到成片/失败,实时回填进度。
|
||||
// 上限 ~8 分钟:后端 ffmpeg 自身 15 分钟超时,轮询窗口耗尽只是停止盯着,任务仍在跑。
|
||||
for (let i = 0; i < 240; i += 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
const res = await api.pollExport(projectId);
|
||||
setExportResult(res);
|
||||
if (res.status === "succeeded") {
|
||||
setNotice({ type: "success", text: "成片已合成,可直接播放" });
|
||||
// 列表页的播放按钮读 final_video_url,合成完要刷一次才拿到
|
||||
void loadData();
|
||||
void refreshProjectDetail();
|
||||
return;
|
||||
}
|
||||
if (res.status === "failed") throw new Error(res.error_message || "合成失败,请重试");
|
||||
}
|
||||
throw new Error("合成用时超出预期,后台可能仍在拼接,稍后回到本页查看");
|
||||
} catch (error) {
|
||||
setNotice({ type: "error", text: error instanceof Error ? error.message : "合成失败" });
|
||||
await refreshExport();
|
||||
} finally {
|
||||
exportingRef.current = false;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [activeProjectId, refreshExport]);
|
||||
|
||||
function navigate(next: Page, options: NavigateOptions = {}) {
|
||||
// 已知为子账号时在发起导航前拦截;与直接访问 URL 的 layout guard 共用同一规则。
|
||||
if (role && !isOwner && isOwnerOnlyPage(next)) {
|
||||
@@ -1101,23 +1142,7 @@ export function App() {
|
||||
onUploadVideoSegment={(segmentId, file) => action(() => api.uploadVideoSegment(pipelineProject.id, segmentId, file), "视频已上传")}
|
||||
onUploadBgm={(file, volume) => action(() => api.uploadBgm(pipelineProject.id, file, volume), "BGM 已上传")}
|
||||
onSaveTimeline={(payload) => action(() => api.saveTimeline(pipelineProject.id, payload), "草稿已保存")}
|
||||
onSubmitExport={(payload) =>
|
||||
action(async () => {
|
||||
// 导出前先落盘当前编辑态(片段/字幕/转场/BGM),成片即所见
|
||||
if (payload) await api.saveTimeline(pipelineProject.id, payload);
|
||||
await api.submitExport(pipelineProject.id);
|
||||
// 后端在后台线程跑 ffmpeg 拼接,这里轮询 poll-export 直到成片/失败,实时回填进度
|
||||
for (let i = 0; i < 160; i += 1) {
|
||||
const res = await api.pollExport(pipelineProject.id);
|
||||
setExportResult(res);
|
||||
if (res.status === "succeeded") return res;
|
||||
if (res.status === "failed") throw new Error(res.error_message || "拼接导出失败,请重试");
|
||||
await new Promise((resolve) => setTimeout(resolve, 2500));
|
||||
}
|
||||
// 轮询窗口耗尽:如实报超时(后台 ffmpeg 可能仍在跑,进入拼接页会自动回填)
|
||||
throw new Error("导出超时,后台可能仍在拼接,稍后回到本页查看");
|
||||
}, "成片已导出")
|
||||
}
|
||||
onSubmitExport={submitExport}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user