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:
@@ -1,6 +1,3 @@
|
||||
import type { JSONContent } from "@tiptap/core";
|
||||
import type { CanvasDocumentChapter, CanvasRichDocument } from "@/types/canvas";
|
||||
|
||||
const maxChapterHeadingLength = 120;
|
||||
const chineseChapterNumber = "[零〇○一二两三四五六七八九十百千万亿壹贰叁肆伍陆柒捌玖拾佰仟\\d]+";
|
||||
const englishChapterNumber = "(?:\\d+|[ivxlcdm]+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve)";
|
||||
@@ -14,44 +11,7 @@ const chapterHeadingPatterns = [
|
||||
new RegExp(`^(?:prologue|preface|introduction|epilogue|afterword|appendix)${chapterTitleSuffix}$`, "i"),
|
||||
];
|
||||
|
||||
export function emptyDocument(): JSONContent {
|
||||
return { type: "doc", content: [{ type: "paragraph" }] };
|
||||
}
|
||||
|
||||
export function textToDocument(text: string): JSONContent {
|
||||
const content = text.split(/\n{2,}/).map((paragraph) => {
|
||||
const value = paragraph.trim();
|
||||
if (!value) return { type: "paragraph" };
|
||||
const markdownHeading = /^(#{1,3})\s+(.+)$/.exec(value);
|
||||
if (markdownHeading) return { type: "heading", attrs: { level: markdownHeading[1].length }, content: [{ type: "text", text: markdownHeading[2] }] };
|
||||
return { type: "paragraph", content: [{ type: "text", text: value }] };
|
||||
});
|
||||
return { type: "doc", content: content.length ? content : [{ type: "paragraph" }] };
|
||||
}
|
||||
|
||||
export function createDocumentChapter(title: string, plainText = "", order = 0): CanvasDocumentChapter {
|
||||
return {
|
||||
id: `chapter-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
title,
|
||||
order,
|
||||
json: textToDocument(plainText) as unknown as Record<string, unknown>,
|
||||
plainText: plainText.trim(),
|
||||
characterCount: Array.from(plainText.trim()).length,
|
||||
storyboardStatus: "idle",
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDocumentChapters(document: CanvasRichDocument | undefined, fallbackText = "") {
|
||||
if (document?.chapters?.length) {
|
||||
return document.chapters
|
||||
.map((chapter, index) => ({ ...chapter, title: chapter.title.trim() || `第 ${index + 1} 章`, order: index }))
|
||||
.sort((left, right) => left.order - right.order);
|
||||
}
|
||||
const chapter = createDocumentChapter("第 1 章", document?.plainText || fallbackText, 0);
|
||||
if (document?.json) chapter.json = document.json;
|
||||
return [chapter];
|
||||
}
|
||||
export type ImportedNovelChapter = { title: string; plainText: string };
|
||||
|
||||
function chapterTitleFromLine(line: string) {
|
||||
const trimmed = line.trim();
|
||||
@@ -87,7 +47,7 @@ export function decodeNovelText(buffer: ArrayBuffer) {
|
||||
|
||||
export function splitTextIntoChapters(text: string) {
|
||||
const normalized = text.replace(/\r\n?/g, "\n").trim();
|
||||
if (!normalized) return [createDocumentChapter("第 1 章")];
|
||||
if (!normalized) return [{ title: "第 1 章", plainText: "" }];
|
||||
const lines = normalized.split("\n");
|
||||
const sections: Array<{ title: string; lines: string[] }> = [];
|
||||
let current: { title: string; lines: string[] } | null = null;
|
||||
@@ -104,29 +64,7 @@ export function splitTextIntoChapters(text: string) {
|
||||
else preface.push(line);
|
||||
}
|
||||
if (current) sections.push(current);
|
||||
if (!sections.length) return [createDocumentChapter("第 1 章", normalized, 0)];
|
||||
if (!sections.length) return [{ title: "第 1 章", plainText: normalized }];
|
||||
if (preface.some((line) => line.trim())) sections.unshift({ title: "序章", lines: preface });
|
||||
return sections.map((section, index) => createDocumentChapter(section.title, section.lines.join("\n").trim(), index));
|
||||
}
|
||||
|
||||
export function buildRichDocument(
|
||||
previous: CanvasRichDocument | undefined,
|
||||
chapters: CanvasDocumentChapter[],
|
||||
activeChapterId: string,
|
||||
sourceFileName?: string,
|
||||
): CanvasRichDocument {
|
||||
const ordered = chapters.map((chapter, index) => ({ ...chapter, order: index }));
|
||||
const active = ordered.find((chapter) => chapter.id === activeChapterId) || ordered[0];
|
||||
const plainText = ordered.map((chapter) => `${chapter.title}\n\n${chapter.plainText}`.trim()).filter(Boolean).join("\n\n");
|
||||
return {
|
||||
kind: previous?.kind || "novel",
|
||||
format: "tiptap-json",
|
||||
json: active?.json || (emptyDocument() as unknown as Record<string, unknown>),
|
||||
plainText,
|
||||
characterCount: ordered.reduce((total, chapter) => total + chapter.characterCount, 0),
|
||||
chapters: ordered,
|
||||
activeChapterId: active?.id,
|
||||
sourceFileName,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
return sections.map((section): ImportedNovelChapter => ({ title: section.title, plainText: section.lines.join("\n").trim() }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user