feat: 完成短剧项目空间与结构化资产创作闭环 (#38)

* feat(canvas): 短剧项目空间 - 建立项目章节与画布关联 MVP

* feat(canvas): 短剧项目空间 - 完善项目资产、镜头与流程闭环

Refs #30

* fix(deploy): 本地部署 - 修复前端构建并忽略本地 PostgreSQL 配置

* feat(*): 短剧项目空间 - 完成项目、章节、资产、角色与画布创作闭环

Refs #30

---------

Co-authored-by: ddCat <ddcat666@88.com>
This commit is contained in:
ddcat666
2026-07-27 10:45:11 +08:00
committed by GitHub
parent 431b9e3294
commit d2fbc413b3
208 changed files with 22655 additions and 4234 deletions

View File

@@ -11,6 +11,7 @@ type CanvasNodeSpec = {
export const NODE_DEFAULT_SIZE = {
[CanvasNodeType.Image]: { width: 340, height: 240, title: "New Generation" },
[CanvasNodeType.Text]: { width: 340, height: 240, title: "Note" },
[CanvasNodeType.Drawing]: { width: 440, height: 300, title: "绘图" },
[CanvasNodeType.Script]: { width: 920, height: 360, title: "分镜脚本" },
[CanvasNodeType.Skill]: { width: 360, height: 220, title: "技能" },
[CanvasNodeType.Config]: { width: 340, height: 300, title: "生成配置" },
@@ -28,6 +29,10 @@ export const NODE_SPECS = {
...NODE_DEFAULT_SIZE[CanvasNodeType.Text],
metadata: { content: "", status: "idle", fontSize: 14 },
},
[CanvasNodeType.Drawing]: {
...NODE_DEFAULT_SIZE[CanvasNodeType.Drawing],
metadata: { status: "success" },
},
[CanvasNodeType.Script]: {
...NODE_DEFAULT_SIZE[CanvasNodeType.Script],
metadata: {

View File

@@ -11,21 +11,24 @@ export function CreditSymbol({ className, ...props }: ComponentProps<"span">) {
export type ModelCreditCost = {
model: string;
billingMode: "fixed_request" | "per_second";
unitPriceMicrocredits: number;
};
function modelCreditCost(modelCosts: ModelCreditCost[] | undefined, model: string) {
const microcredits = modelCosts?.find((item) => item.model === model)?.unitPriceMicrocredits;
return microcredits === undefined ? null : microcredits / 1_000_000;
return modelCosts?.find((item) => item.model === model) || null;
}
export function formatCredits(value: number, maximumFractionDigits = 6) {
return (value / 1_000_000).toLocaleString("zh-CN", { maximumFractionDigits });
}
export function requestCreditCost(options: { channelMode: string; modelCosts?: ModelCreditCost[]; model: string; count?: string | number }) {
export function requestCreditCost(options: { channelMode: string; modelCosts?: ModelCreditCost[]; model: string; count?: string | number; seconds?: string | number }) {
if (options.channelMode !== "remote") return null;
const unitCredits = modelCreditCost(options.modelCosts, options.model);
const count = Math.max(1, Math.floor(Math.abs(Number(options.count)) || 1));
return unitCredits === null ? null : unitCredits * count;
const cost = modelCreditCost(options.modelCosts, options.model);
if (!cost) return null;
const quantity = cost.billingMode === "per_second"
? Math.max(1, Math.floor(Math.abs(Number(options.seconds)) || 1))
: Math.max(1, Math.floor(Math.abs(Number(options.count)) || 1));
return (cost.unitPriceMicrocredits / 1_000_000) * quantity;
}

View File

@@ -1,26 +1,26 @@
import { BookOpenCheck, Images, ListChecks, Maximize2 } from "lucide-react";
import { FolderKanban, Images, ListChecks, Maximize2 } from "lucide-react";
export const navigationTools = [
{
slug: "projects",
label: "短剧创作",
icon: FolderKanban,
},
{
slug: "canvas",
label: "我的画布",
label: "画布",
icon: Maximize2,
},
{
slug: "tasks",
label: "任务中心",
label: "任务",
icon: ListChecks,
},
{
slug: "assets",
label: "我的素材",
label: "素材",
icon: Images,
},
{
slug: "skills",
label: "技能库大厅",
icon: BookOpenCheck,
},
] as const;
export type NavigationToolSlug = (typeof navigationTools)[number]["slug"];