宫格预览

This commit is contained in:
zhishi 2026-03-25 23:42:49 +08:00
parent 95dfc38f3a
commit 558e9b95e3
5 changed files with 233 additions and 94 deletions

2
env/.env.dev vendored
View File

@ -1,4 +1,4 @@
NODE_ENV=dev
PORT=10588
OSSURL=http://192.168.0.74:10588/
OSSURL=http://127.0.0.1:10588/

View File

@ -308,6 +308,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise<void> =>
table.text("lines");
table.text("state");
table.text("reason");
table.text("index");
table.integer("createTime");
table.primary(["id"]);
table.unique(["id"]);

View File

@ -1,4 +1,4 @@
// @routes-hash bf3c43509342cfaa6f58c3551570331d
// @routes-hash f5f78866e59979bf30af031c9ea0de82
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -49,42 +49,43 @@ import route45 from "./routes/production/getFlowData";
import route46 from "./routes/production/getProductionData";
import route47 from "./routes/production/getStoryboardData";
import route48 from "./routes/production/saveFlowData";
import route49 from "./routes/production/workbench/confirmSelection";
import route50 from "./routes/production/workbench/delVideo";
import route51 from "./routes/production/workbench/generateVideo";
import route52 from "./routes/production/workbench/getChatLines";
import route53 from "./routes/production/workbench/getVideoModelDetail";
import route54 from "./routes/production/workbench/videoPolling";
import route55 from "./routes/project/addProject";
import route56 from "./routes/project/delProject";
import route57 from "./routes/project/editProject";
import route58 from "./routes/project/getProject";
import route59 from "./routes/script/addScript";
import route60 from "./routes/script/delScript";
import route61 from "./routes/script/exportScript";
import route62 from "./routes/script/getScrptApi";
import route63 from "./routes/script/updateScript";
import route64 from "./routes/scriptAgent/getPlanData";
import route65 from "./routes/scriptAgent/setPlanData";
import route66 from "./routes/setting/agentDeploy/agentSetKey";
import route67 from "./routes/setting/agentDeploy/deployAgentModel";
import route68 from "./routes/setting/agentDeploy/getAgentDeploy";
import route69 from "./routes/setting/dbConfig/clearData";
import route70 from "./routes/setting/fileManagement/openFolder";
import route71 from "./routes/setting/getTextModel";
import route72 from "./routes/setting/loginConfig/getUser";
import route73 from "./routes/setting/loginConfig/updateUserPwd";
import route74 from "./routes/setting/memoryConfig/getMemory";
import route75 from "./routes/setting/memoryConfig/sureMemory";
import route76 from "./routes/setting/vendorConfig/addVendor";
import route77 from "./routes/setting/vendorConfig/deleteVendor";
import route78 from "./routes/setting/vendorConfig/getVendorList";
import route79 from "./routes/setting/vendorConfig/modelTest";
import route80 from "./routes/setting/vendorConfig/updateVendor";
import route81 from "./routes/task/getTaskApi";
import route82 from "./routes/task/getTaskCategories";
import route83 from "./routes/task/taskDetails";
import route84 from "./routes/test/test";
import route49 from "./routes/production/storyboard/previewImage";
import route50 from "./routes/production/workbench/confirmSelection";
import route51 from "./routes/production/workbench/delVideo";
import route52 from "./routes/production/workbench/generateVideo";
import route53 from "./routes/production/workbench/getChatLines";
import route54 from "./routes/production/workbench/getVideoModelDetail";
import route55 from "./routes/production/workbench/videoPolling";
import route56 from "./routes/project/addProject";
import route57 from "./routes/project/delProject";
import route58 from "./routes/project/editProject";
import route59 from "./routes/project/getProject";
import route60 from "./routes/script/addScript";
import route61 from "./routes/script/delScript";
import route62 from "./routes/script/exportScript";
import route63 from "./routes/script/getScrptApi";
import route64 from "./routes/script/updateScript";
import route65 from "./routes/scriptAgent/getPlanData";
import route66 from "./routes/scriptAgent/setPlanData";
import route67 from "./routes/setting/agentDeploy/agentSetKey";
import route68 from "./routes/setting/agentDeploy/deployAgentModel";
import route69 from "./routes/setting/agentDeploy/getAgentDeploy";
import route70 from "./routes/setting/dbConfig/clearData";
import route71 from "./routes/setting/fileManagement/openFolder";
import route72 from "./routes/setting/getTextModel";
import route73 from "./routes/setting/loginConfig/getUser";
import route74 from "./routes/setting/loginConfig/updateUserPwd";
import route75 from "./routes/setting/memoryConfig/getMemory";
import route76 from "./routes/setting/memoryConfig/sureMemory";
import route77 from "./routes/setting/vendorConfig/addVendor";
import route78 from "./routes/setting/vendorConfig/deleteVendor";
import route79 from "./routes/setting/vendorConfig/getVendorList";
import route80 from "./routes/setting/vendorConfig/modelTest";
import route81 from "./routes/setting/vendorConfig/updateVendor";
import route82 from "./routes/task/getTaskApi";
import route83 from "./routes/task/getTaskCategories";
import route84 from "./routes/task/taskDetails";
import route85 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -135,40 +136,41 @@ export default async (app: Express) => {
app.use("/api/production/getProductionData", route46);
app.use("/api/production/getStoryboardData", route47);
app.use("/api/production/saveFlowData", route48);
app.use("/api/production/workbench/confirmSelection", route49);
app.use("/api/production/workbench/delVideo", route50);
app.use("/api/production/workbench/generateVideo", route51);
app.use("/api/production/workbench/getChatLines", route52);
app.use("/api/production/workbench/getVideoModelDetail", route53);
app.use("/api/production/workbench/videoPolling", route54);
app.use("/api/project/addProject", route55);
app.use("/api/project/delProject", route56);
app.use("/api/project/editProject", route57);
app.use("/api/project/getProject", route58);
app.use("/api/script/addScript", route59);
app.use("/api/script/delScript", route60);
app.use("/api/script/exportScript", route61);
app.use("/api/script/getScrptApi", route62);
app.use("/api/script/updateScript", route63);
app.use("/api/scriptAgent/getPlanData", route64);
app.use("/api/scriptAgent/setPlanData", route65);
app.use("/api/setting/agentDeploy/agentSetKey", route66);
app.use("/api/setting/agentDeploy/deployAgentModel", route67);
app.use("/api/setting/agentDeploy/getAgentDeploy", route68);
app.use("/api/setting/dbConfig/clearData", route69);
app.use("/api/setting/fileManagement/openFolder", route70);
app.use("/api/setting/getTextModel", route71);
app.use("/api/setting/loginConfig/getUser", route72);
app.use("/api/setting/loginConfig/updateUserPwd", route73);
app.use("/api/setting/memoryConfig/getMemory", route74);
app.use("/api/setting/memoryConfig/sureMemory", route75);
app.use("/api/setting/vendorConfig/addVendor", route76);
app.use("/api/setting/vendorConfig/deleteVendor", route77);
app.use("/api/setting/vendorConfig/getVendorList", route78);
app.use("/api/setting/vendorConfig/modelTest", route79);
app.use("/api/setting/vendorConfig/updateVendor", route80);
app.use("/api/task/getTaskApi", route81);
app.use("/api/task/getTaskCategories", route82);
app.use("/api/task/taskDetails", route83);
app.use("/api/test/test", route84);
app.use("/api/production/storyboard/previewImage", route49);
app.use("/api/production/workbench/confirmSelection", route50);
app.use("/api/production/workbench/delVideo", route51);
app.use("/api/production/workbench/generateVideo", route52);
app.use("/api/production/workbench/getChatLines", route53);
app.use("/api/production/workbench/getVideoModelDetail", route54);
app.use("/api/production/workbench/videoPolling", route55);
app.use("/api/project/addProject", route56);
app.use("/api/project/delProject", route57);
app.use("/api/project/editProject", route58);
app.use("/api/project/getProject", route59);
app.use("/api/script/addScript", route60);
app.use("/api/script/delScript", route61);
app.use("/api/script/exportScript", route62);
app.use("/api/script/getScrptApi", route63);
app.use("/api/script/updateScript", route64);
app.use("/api/scriptAgent/getPlanData", route65);
app.use("/api/scriptAgent/setPlanData", route66);
app.use("/api/setting/agentDeploy/agentSetKey", route67);
app.use("/api/setting/agentDeploy/deployAgentModel", route68);
app.use("/api/setting/agentDeploy/getAgentDeploy", route69);
app.use("/api/setting/dbConfig/clearData", route70);
app.use("/api/setting/fileManagement/openFolder", route71);
app.use("/api/setting/getTextModel", route72);
app.use("/api/setting/loginConfig/getUser", route73);
app.use("/api/setting/loginConfig/updateUserPwd", route74);
app.use("/api/setting/memoryConfig/getMemory", route75);
app.use("/api/setting/memoryConfig/sureMemory", route76);
app.use("/api/setting/vendorConfig/addVendor", route77);
app.use("/api/setting/vendorConfig/deleteVendor", route78);
app.use("/api/setting/vendorConfig/getVendorList", route79);
app.use("/api/setting/vendorConfig/modelTest", route80);
app.use("/api/setting/vendorConfig/updateVendor", route81);
app.use("/api/task/getTaskApi", route82);
app.use("/api/task/getTaskCategories", route83);
app.use("/api/task/taskDetails", route84);
app.use("/api/test/test", route85);
}

View File

@ -154,31 +154,52 @@ export default router.post(
};
}),
);
// 将原有 flowData.storyboard 按 id 建立索引,以便后续合并保留旧字段
const existingStoryboardMap: Record<number, any> = {};
// 将数据库 storyboardData 按 id 建立索引
const dbStoryboardMap: Record<number, (typeof storyboardData)[number]> = {};
storyboardData.forEach((i) => {
dbStoryboardMap[i.id!] = i;
});
// 用于构造单条 storyboard 的辅助函数
const buildStoryboardItem = (i: (typeof storyboardData)[number], existing: any = {}) => ({
...existing,
id: i.id,
title: i.title,
description: i.description,
camera: i.camera,
duration: i.duration ? +i.duration : 0,
frameMode: i.frameMode,
prompt: i.prompt,
lines: i.lines,
sound: i.sound,
associateAssetsIds: assets2StoryboardMap[i.id!] ?? [],
src: i.filePath,
state: i.state,
});
// 保持旧数据顺序,新增的追加到最后
const usedIds = new Set<number>();
const orderedStoryboard: any[] = [];
// 1. 按旧数据顺序遍历,若数据库中仍存在则合并更新
if (Array.isArray(flowData.storyboard)) {
flowData.storyboard.forEach((s: any) => {
existingStoryboardMap[s.id] = s;
const dbItem = dbStoryboardMap[s.id];
if (dbItem) {
orderedStoryboard.push(buildStoryboardItem(dbItem, s));
usedIds.add(s.id);
}
});
}
flowData.storyboard = storyboardData.map((i) => {
const existing = existingStoryboardMap[i.id!] ?? {};
return {
...existing,
id: i.id,
title: i.title,
description: i.description,
camera: i.camera,
duration: i.duration ? +i.duration : 0,
frameMode: i.frameMode,
prompt: i.prompt,
lines: i.lines,
sound: i.sound,
associateAssetsIds: assets2StoryboardMap[i.id!] ?? [],
src: i.filePath,
state: i.state,
};
// 2. 数据库中新增的(旧数据中没有的)追加到最后
storyboardData.forEach((i) => {
if (!usedIds.has(i.id!)) {
orderedStoryboard.push(buildStoryboardItem(i));
}
});
flowData.storyboard = orderedStoryboard;
res.status(200).send(success(flowData));
} catch (err) {
res.status(400).send(error());

View File

@ -0,0 +1,115 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import sharp from "sharp";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
storyboardIds: z.array(z.number()),
}),
async (req, res) => {
const { storyboardIds } = req.body;
const storyboardImage = await u.db("o_storyboard").whereIn("id", storyboardIds).select("id", "filePath");
// 按 storyboardIds 顺序构建 filePath 映射
const filePathMap: Record<number, string> = {};
storyboardImage.forEach((i) => {
filePathMap[i.id!] = i.filePath || "";
});
const orderedFilePaths = storyboardIds.map((id: number) => filePathMap[id]);
// 读取所有图片 buffer 并获取元数据
const loaded = await Promise.all(
orderedFilePaths.map(async (filePath: string) => {
if (!filePath) return null;
const buffer = await u.oss.getFile(filePath);
const metadata = await sharp(buffer).metadata();
return { buffer, width: metadata.width || 0, height: metadata.height || 0 };
}),
);
// 过滤掉无效图片
const validImages = loaded.filter((img): img is NonNullable<typeof img> => img !== null && img.width > 0 && img.height > 0);
if (validImages.length === 0) {
return res.status(200).send(success(null));
}
// 计算网格布局
const cols = Math.min(5, validImages.length);
const rows = Math.ceil(validImages.length / cols);
const colWidths: number[] = Array(cols).fill(0);
const rowHeights: number[] = Array(rows).fill(0);
validImages.forEach((img, idx) => {
const c = idx % cols;
const r = Math.floor(idx / cols);
colWidths[c] = Math.max(colWidths[c], img.width);
rowHeights[r] = Math.max(rowHeights[r], img.height);
});
const canvasWidth = colWidths.reduce((a, b) => a + b, 0);
const canvasHeight = rowHeights.reduce((a, b) => a + b, 0);
// 为每张图片生成带标号的合成层
const compositeInputs: sharp.OverlayOptions[] = [];
for (let i = 0; i < validImages.length; i++) {
const img = validImages[i];
const c = i % cols;
const r = Math.floor(i / cols);
const x = colWidths.slice(0, c).reduce((a, b) => a + b, 0);
const y = rowHeights.slice(0, r).reduce((a, b) => a + b, 0);
// 添加图片层
compositeInputs.push({
input: img.buffer,
left: x,
top: y,
});
// 生成标号标签 SVG
const label = `S${String(i + 1).padStart(2, "0")}`;
const fontSize = Math.max(14, Math.min(img.width, img.height) * 0.06);
const padding = Math.round(fontSize * 0.4);
// 估算文字宽度(等宽近似)
const textWidth = Math.round(label.length * fontSize * 0.65);
const bgW = textWidth + padding * 2;
const bgH = Math.round(fontSize) + padding * 2;
const labelSvg = Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" width="${bgW}" height="${bgH}">
<rect x="0" y="0" width="${bgW}" height="${bgH}" rx="4" ry="4" fill="rgba(0,0,0,0.55)"/>
<text x="${padding}" y="${padding + fontSize * 0.85}" font-family="Arial, sans-serif" font-weight="bold" font-size="${fontSize}" fill="#fff">${label}</text>
</svg>`,
);
compositeInputs.push({
input: labelSvg,
left: x + 4,
top: y + 4,
});
}
// 使用 sharp 创建画布并合成
const resultBuffer = await sharp({
create: {
width: canvasWidth,
height: canvasHeight,
channels: 4,
background: { r: 255, g: 255, b: 255, alpha: 1 },
},
})
.composite(compositeInputs)
.png()
.toBuffer();
const base64 = resultBuffer.toString("base64");
const dataUrl = `data:image/png;base64,${base64}`;
return res.status(200).send(success(dataUrl));
},
);