diff --git a/app/components/SettingsPanel.js b/app/components/SettingsPanel.js index 752425f..18124da 100644 --- a/app/components/SettingsPanel.js +++ b/app/components/SettingsPanel.js @@ -124,6 +124,8 @@ export default function SettingsPanel() { const handleSwitchWork = (workId) => { setActiveWorkIdState(workId); setActiveWorkId(workId); + // 同步 Zustand store,触发 page.js 重载章节 + useAppStore.getState().setActiveWorkId(workId); setSelectedNodeId(null); }; diff --git a/app/components/Sidebar.js b/app/components/Sidebar.js index 32ba01a..1bf2e30 100644 --- a/app/components/Sidebar.js +++ b/app/components/Sidebar.js @@ -3,14 +3,15 @@ import { useState, useCallback } from 'react'; import { useAppStore } from '../store/useAppStore'; import { useI18n } from '../lib/useI18n'; -import { createChapter, deleteChapter, updateChapter, exportToMarkdown, exportAllToMarkdown } from '../lib/storage'; -import { exportProject, importProject } from '../lib/project-io'; -import { WRITING_MODES } from '../lib/settings'; +import { createChapter, deleteChapter, updateChapter, exportToMarkdown, exportAllToMarkdown, saveChapters } from '../lib/storage'; +import { exportProject, importProject, importWork, exportWorkAsTxt } from '../lib/project-io'; +import { WRITING_MODES, getAllWorks, getSettingsNodes, createWorkNode, saveSettingsNodes, setActiveWorkId as setActiveWorkIdSetting } from '../lib/settings'; export default function Sidebar() { const { chapters, addChapter, setChapters, updateChapter: updateChapterStore, activeChapterId, setActiveChapterId, + activeWorkId, setActiveWorkId: setActiveWorkIdStore, sidebarOpen, setSidebarOpen, theme, setTheme, writingMode, @@ -22,6 +23,7 @@ export default function Sidebar() { const [renameId, setRenameId] = useState(null); const [renameTitle, setRenameTitle] = useState(''); const [contextMenu, setContextMenu] = useState(null); + const [importModal, setImportModal] = useState(null); // { chapters, totalWords, file } const { t } = useI18n(); // 切换主题 @@ -102,14 +104,14 @@ export default function Sidebar() { // 创建新章节 — 一键创建并进入重命名模式 const handleCreateChapter = useCallback(async () => { const title = getNextChapterTitle(); - const ch = await createChapter(title); + const ch = await createChapter(title, activeWorkId); addChapter(ch); setActiveChapterId(ch.id); // 立即进入重命名模式,方便用户修改标题 setRenameId(ch.id); setRenameTitle(title); showToast(t('sidebar.chapterCreated').replace('{title}', title), 'success'); - }, [getNextChapterTitle, showToast, addChapter, setActiveChapterId, t]); + }, [getNextChapterTitle, showToast, addChapter, setActiveChapterId, t, activeWorkId]); // 删除章节 const handleDeleteChapter = useCallback(async (id) => { @@ -118,24 +120,24 @@ export default function Sidebar() { return; } const ch = chapters.find(c => c.id === id); - const remaining = await deleteChapter(id); + const remaining = await deleteChapter(id, activeWorkId); setChapters(remaining); if (activeChapterId === id) { setActiveChapterId(remaining[0]?.id || null); } showToast(t('sidebar.chapterDeleted').replace('{title}', ch?.title), 'info'); setContextMenu(null); - }, [chapters, activeChapterId, showToast, setChapters, setActiveChapterId, t]); + }, [chapters, activeChapterId, showToast, setChapters, setActiveChapterId, t, activeWorkId]); // 重命名章节 const handleRename = useCallback((id) => { const title = renameTitle.trim(); if (!title) return; - const updated = updateChapter(id, { title }); + updateChapter(id, { title }, activeWorkId); updateChapterStore(id, { title }); setRenameId(null); setRenameTitle(''); - }, [renameTitle, updateChapterStore]); + }, [renameTitle, updateChapterStore, activeWorkId]); // 导出 const handleExport = useCallback((type) => { @@ -290,6 +292,12 @@ export default function Sidebar() { + + + { + const file = e.target.files?.[0]; + if (!file) return; + try { + const result = await importWork(file); + if (!result.success) { + const msg = result.message === 'noChapter' + ? t('sidebar.importWorkNoChapter') + : t('sidebar.importWorkFailed').replace('{error}', result.message); + showToast(msg, 'error'); + e.target.value = ''; + return; + } + // 弹出作品选择 + setImportModal({ chapters: result.chapters, totalWords: result.totalWords }); + } catch (err) { + showToast(t('sidebar.importWorkFailed').replace('{error}', err.message), 'error'); + } + e.target.value = ''; + }} + /> @@ -358,6 +392,116 @@ export default function Sidebar() { )} + {/* ===== 导入作品-选择目标作品弹窗 ===== */} + {importModal && ( + setImportModal(null)} + onImport={async (targetWorkId) => { + try { + await saveChapters(importModal.chapters, targetWorkId); + // 切换到目标作品 + setActiveWorkIdSetting(targetWorkId); + setActiveWorkIdStore(targetWorkId); + showToast(t('sidebar.importWorkSuccess').replace('{count}', importModal.chapters.length), 'success'); + setImportModal(null); + } catch (err) { + showToast(t('sidebar.importWorkFailed').replace('{error}', err.message), 'error'); + } + }} + t={t} + /> + )} ); } + +/** + * 导入作品时的目标作品选择弹窗 + */ +function ImportWorkModal({ chapters, totalWords, onClose, onImport, t }) { + const [works, setWorks] = useState([]); + const [newWorkName, setNewWorkName] = useState(''); + const [showNewInput, setShowNewInput] = useState(false); + + // 加载作品列表 + useState(() => { + (async () => { + const nodes = await getSettingsNodes(); + setWorks(getAllWorks(nodes)); + })(); + }); + + const handleCreateAndImport = async () => { + const name = newWorkName.trim(); + if (!name) return; + const { workNode, subNodes } = createWorkNode(name); + const allNodes = await getSettingsNodes(); + const updatedNodes = [...allNodes, workNode, ...subNodes]; + await saveSettingsNodes(updatedNodes); + onImport(workNode.id); + }; + + return ( +
+
e.stopPropagation()} style={{ + padding: '24px', maxWidth: 420, width: '90%', borderRadius: 'var(--radius-lg)', + display: 'flex', flexDirection: 'column', gap: 16, + }}> +

{t('sidebar.importWorkSelectTitle')}

+

+ {t('sidebar.importWorkSelectDesc') + .replace('{count}', chapters.length) + .replace('{words}', totalWords.toLocaleString())} +

+ +
+ {works.map(w => ( + + ))} + + {showNewInput ? ( +
+ setNewWorkName(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleCreateAndImport()} + placeholder={t('sidebar.importWorkNewPlaceholder')} + autoFocus + /> + +
+ ) : ( + + )} +
+ +
+ +
+
+
+ ); +} diff --git a/app/lib/context-engine.js b/app/lib/context-engine.js index 93288c9..fab7c2c 100644 --- a/app/lib/context-engine.js +++ b/app/lib/context-engine.js @@ -38,7 +38,7 @@ const PRIORITY = { */ export async function getContextItems(activeChapterId) { const settings = getProjectSettings(); - const chapters = await getChapters(); + const chapters = await getChapters(getActiveWorkId()); const currentIndex = chapters.findIndex(ch => ch.id === activeChapterId); const allNodes = await getSettingsNodes(); @@ -152,7 +152,7 @@ export async function getContextItems(activeChapterId) { */ export async function buildContext(activeChapterId, selectedText, selectedIds = null) { const settings = getProjectSettings(); - const chapters = await getChapters(); + const chapters = await getChapters(getActiveWorkId()); const currentChapter = chapters.find(ch => ch.id === activeChapterId); const currentIndex = chapters.findIndex(ch => ch.id === activeChapterId); @@ -307,7 +307,7 @@ function applyTokenBudget(modules) { */ export async function getContextPreview(activeChapterId, selectedText) { const settings = getProjectSettings(); - const chapters = await getChapters(); + const chapters = await getChapters(getActiveWorkId()); const currentChapter = chapters.find(ch => ch.id === activeChapterId); const currentIndex = chapters.findIndex(ch => ch.id === activeChapterId); diff --git a/app/lib/project-io.js b/app/lib/project-io.js index 7e78ca1..1b526df 100644 --- a/app/lib/project-io.js +++ b/app/lib/project-io.js @@ -108,6 +108,131 @@ export async function importProject(file) { } } +/** + * 导入作品 — 从 TXT 文件中自动识别章节并批量创建 + * 支持格式:第X章/回/节/卷(中文/阿拉伯数字)、Chapter X、纯数字、纯中文数字 + * @param {File} file - 用户选择的 TXT 文件 + * @returns {Promise<{ success: boolean, message: string, chapters?: Array, totalWords?: number }>} + */ +export async function importWork(file) { + if (typeof window === 'undefined') return { success: false, message: '环境不支持' }; + + try { + const text = await file.text(); + if (!text.trim()) { + return { success: false, message: '文件内容为空' }; + } + + // 章节标题正则 — 支持多种格式 + // 1. 第X章/回/节/卷(中文数字或阿拉伯数字)+ 可选标题 + // 2. Chapter X + 可选标题 + // 3. 纯阿拉伯数字行(如 "1"、"23") + // 4. 纯中文数字行(如 "一"、"三十三") + const CHAPTER_REGEX = /^(?:第[零一二三四五六七八九十百千万\d]+[章回节卷](?:\s+.*)?|Chapter\s+\d+(?:\s+.*)?|\d+|[零一二三四五六七八九十百千万]+)$/i; + + const lines = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n'); + const rawChapters = []; + let currentChapter = null; + + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + + if (CHAPTER_REGEX.test(trimmed)) { + // 找到章节标题 + if (currentChapter) { + rawChapters.push(currentChapter); + } + currentChapter = { title: trimmed, lines: [] }; + } else { + if (!currentChapter) { + // 第一个章节标题之前的内容归为序章 + currentChapter = { title: null, lines: [] }; + } + currentChapter.lines.push(lines[i]); + } + } + if (currentChapter) { + rawChapters.push(currentChapter); + } + + // 如果只有一个无标题章节且无内容,说明没识别到任何章节 + if (rawChapters.length === 0 || (rawChapters.length === 1 && !rawChapters[0].title && rawChapters[0].lines.join('').trim() === '')) { + return { success: false, message: 'noChapter' }; + } + + // 转换为章节对象 + const { generateId } = await import('./storage'); + const now = new Date().toISOString(); + const chapters = rawChapters.map((raw, idx) => { + const content = textToHtml(raw.lines); + const plainText = raw.lines.join('').replace(/\s/g, ''); + return { + id: generateId(), + title: raw.title || `序章`, + content, + wordCount: plainText.length, + createdAt: now, + updatedAt: now, + }; + }); + + const totalWords = chapters.reduce((sum, ch) => sum + ch.wordCount, 0); + return { success: true, chapters, totalWords, message: '' }; + } catch (err) { + return { success: false, message: err.message }; + } +} + +/** + * 将纯文本行数组转换为 HTML(匹配编辑器 insertText 格式) + * 规则:空行分段(

),段内换行用
,去掉多余空行 + */ +function textToHtml(lines) { + const normalized = lines.join('\n').trim(); + if (!normalized) return ''; + + // 按空行(连续换行)分段 + const blocks = normalized.split(/\n\n+/); + return blocks + .map(block => { + const blockLines = block.split('\n').map(l => l.trimEnd()).filter(l => l); + if (blockLines.length === 0) return ''; + return `

${blockLines.join('
')}

`; + }) + .filter(p => p && p !== '

') + .join(''); +} + +/** + * 导出章节为 TXT 文件 + * @param {Array} chapters - 章节数组 + * @param {string} [fileName] - 文件名(不含扩展名) + */ +export function exportWorkAsTxt(chapters, fileName) { + if (!chapters || chapters.length === 0) return; + const text = chapters.map(ch => { + const title = ch.title || ''; + const content = (ch.content || '') + .replace(/<\/p>/gi, '\n\n') + .replace(//gi, '\n') + .replace(/<[^>]*>/g, '') + .replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .trim(); + return `${title}\n\n${content}`; + }).join('\n\n\n'); + + const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${fileName || '导出作品'}.txt`; + a.click(); + URL.revokeObjectURL(url); +} + /** * 获取当前项目数据的概要信息(用于显示) */ diff --git a/app/lib/snapshots.js b/app/lib/snapshots.js index 0e78e57..73c3768 100644 --- a/app/lib/snapshots.js +++ b/app/lib/snapshots.js @@ -1,6 +1,6 @@ import { get, set } from 'idb-keyval'; import { getChapters, saveChapters } from './storage'; -import { getSettingsNodes, saveSettingsNodes } from './settings'; +import { getSettingsNodes, saveSettingsNodes, getActiveWorkId } from './settings'; const SNAPSHOTS_KEY = 'author-snapshots'; @@ -26,7 +26,7 @@ export async function getSnapshots() { */ export async function createSnapshot(label, type = 'auto') { try { - const chapters = await getChapters(); + const chapters = await getChapters(getActiveWorkId()); const settingsNodes = await getSettingsNodes(); const snapshot = { @@ -80,7 +80,7 @@ export async function restoreSnapshot(snapshotId) { await createSnapshot('恢复前的备份', 'auto'); // 覆盖现有数据 - await saveChapters(target.data.chapters || []); + await saveChapters(target.data.chapters || [], getActiveWorkId()); await saveSettingsNodes(target.data.settingsNodes || []); return true; diff --git a/app/lib/storage.js b/app/lib/storage.js index 4eee575..2f7ae76 100644 --- a/app/lib/storage.js +++ b/app/lib/storage.js @@ -1,27 +1,57 @@ // 本地存储工具 - 使用 IndexedDB 管理核心数据 (章节、摘要) -import { get, set } from 'idb-keyval'; +// 章节按作品(workId)隔离存储 +import { get, set, del } from 'idb-keyval'; -const STORAGE_KEY = 'author-chapters'; +const LEGACY_STORAGE_KEY = 'author-chapters'; + +function getStorageKey(workId) { + return workId ? `author-chapters-${workId}` : LEGACY_STORAGE_KEY; +} // 生成唯一ID export function generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 9); } -// 获取所有章节 (Async) -export async function getChapters() { - if (typeof window === 'undefined') return []; +/** + * 一次性迁移:将旧的全局 author-chapters 剪切到当前活跃作品 + * 调用方在 page.js initData 中负责调用 + */ +export async function migrateGlobalChapters(workId) { + if (typeof window === 'undefined' || !workId) return; try { - let chapters = await get(STORAGE_KEY); - if (!chapters) { - // Fallback: 第一次迁移,从 localStorage 拿 - const legacyData = localStorage.getItem(STORAGE_KEY); - if (legacyData) { - chapters = JSON.parse(legacyData); - await set(STORAGE_KEY, chapters); - } else { - chapters = []; + const perWorkData = await get(getStorageKey(workId)); + if (perWorkData) return; // 该作品已有数据,不迁移 + + const globalData = await get(LEGACY_STORAGE_KEY); + if (!globalData || !Array.isArray(globalData) || globalData.length === 0) { + // 也检查 localStorage fallback + const legacyLocal = localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacyLocal) { + const parsed = JSON.parse(legacyLocal); + if (Array.isArray(parsed) && parsed.length > 0) { + await set(getStorageKey(workId), parsed); + localStorage.removeItem(LEGACY_STORAGE_KEY); + } } + return; + } + // 剪切到新 key + await set(getStorageKey(workId), globalData); + await del(LEGACY_STORAGE_KEY); + } catch (e) { + console.warn('[迁移] 章节迁移失败:', e); + } +} + +// 获取所有章节 (Async) +export async function getChapters(workId) { + if (typeof window === 'undefined') return []; + const key = getStorageKey(workId); + try { + let chapters = await get(key); + if (!chapters) { + chapters = []; } return chapters; } catch { @@ -30,14 +60,14 @@ export async function getChapters() { } // 保存所有章节 (Async) -export async function saveChapters(chapters) { +export async function saveChapters(chapters, workId) { if (typeof window === 'undefined') return; - await set(STORAGE_KEY, chapters); + await set(getStorageKey(workId), chapters); } // 创建新章节 (Async) -export async function createChapter(title = '未命名章节') { - const chapters = await getChapters(); +export async function createChapter(title = '未命名章节', workId) { + const chapters = await getChapters(workId); const newChapter = { id: generateId(), title, @@ -47,13 +77,13 @@ export async function createChapter(title = '未命名章节') { updatedAt: new Date().toISOString(), }; chapters.push(newChapter); - await saveChapters(chapters); + await saveChapters(chapters, workId); return newChapter; } // 更新章节 (Async) -export async function updateChapter(id, updates) { - const chapters = await getChapters(); +export async function updateChapter(id, updates, workId) { + const chapters = await getChapters(workId); const index = chapters.findIndex(ch => ch.id === id); if (index === -1) return null; @@ -62,21 +92,21 @@ export async function updateChapter(id, updates) { ...updates, updatedAt: new Date().toISOString(), }; - await saveChapters(chapters); + await saveChapters(chapters, workId); return chapters[index]; } // 删除章节 (Async) -export async function deleteChapter(id) { - const chapters = await getChapters(); +export async function deleteChapter(id, workId) { + const chapters = await getChapters(workId); const newChapters = chapters.filter(ch => ch.id !== id); - await saveChapters(newChapters); + await saveChapters(newChapters, workId); return newChapters; } // 获取单个章节 (Async) -export async function getChapter(id) { - const chapters = await getChapters(); +export async function getChapter(id, workId) { + const chapters = await getChapters(workId); return chapters.find(ch => ch.id === id) || null; } diff --git a/app/locales/en.json b/app/locales/en.json index 1cb3a79..87e43ee 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -255,7 +255,20 @@ "clickToSwitchMode": "Click to switch writing mode", "exportedChapter": "Current chapter exported", "exportedAll": "All chapters exported", - "importSuccess": "Page will reload automatically." + "importSuccess": "Page will reload automatically.", + "btnImportWork": "📥 Import", + "btnImportWorkTitle": "Import work from TXT file (auto-detect chapters)", + "importWorkConfirm": "Importing will replace all current chapters. Detected {count} chapters, {words} words total. Confirm import?", + "importWorkSuccess": "Successfully imported {count} chapters", + "importWorkFailed": "Import failed: {error}", + "importWorkNoChapter": "No chapters detected. Please check the file format.", + "importWorkSelectTitle": "Select Import Target", + "importWorkSelectDesc": "Detected {count} chapters, {words} words total. Choose which work to import into:", + "importWorkReplaceConfirm": "Import chapters into \"{name}\"? This will replace all existing chapters in that work.", + "importWorkNewBtn": "New Work", + "importWorkNewPlaceholder": "Enter new work name", + "btnExportTxt": "📤 Export", + "btnExportTxtTitle": "Export all chapters as TXT file" }, "page": { "noChapterHint": "Select or create a chapter to start writing", diff --git a/app/locales/zh.json b/app/locales/zh.json index 40e7375..8f257fd 100644 --- a/app/locales/zh.json +++ b/app/locales/zh.json @@ -255,7 +255,20 @@ "clickToSwitchMode": "点击切换写作模式", "exportedChapter": "已导出当前章节", "exportedAll": "已导出全部章节", - "importSuccess": "页面将自动刷新。" + "importSuccess": "页面将自动刷新。", + "btnImportWork": "📥 导入", + "btnImportWorkTitle": "从 TXT 文件导入作品(自动识别章节)", + "importWorkConfirm": "导入作品将替换当前所有章节。已识别 {count} 个章节,共 {words} 字。确认导入?", + "importWorkSuccess": "成功导入 {count} 个章节", + "importWorkFailed": "导入失败:{error}", + "importWorkNoChapter": "未识别到任何章节,请检查文件格式", + "importWorkSelectTitle": "选择导入目标", + "importWorkSelectDesc": "已识别 {count} 个章节,共 {words} 字。请选择导入到哪个作品:", + "importWorkReplaceConfirm": "确定将章节导入到「{name}」?这将替换该作品的所有现有章节。", + "importWorkNewBtn": "新建作品", + "importWorkNewPlaceholder": "输入新作品名称", + "btnExportTxt": "📤 导出", + "btnExportTxtTitle": "将当前作品所有章节导出为 TXT 文件" }, "page": { "noChapterHint": "请选择或创建一个章节开始写作", diff --git a/app/page.js b/app/page.js index c69fb5f..ddc2527 100644 --- a/app/page.js +++ b/app/page.js @@ -11,6 +11,8 @@ import { deleteChapter, exportToMarkdown, exportAllToMarkdown, + migrateGlobalChapters, + saveChapters, } from './lib/storage'; import { buildContext, compileSystemPrompt, compileUserPrompt, OUTPUT_TOKEN_BUDGET, getContextItems, estimateTokens } from './lib/context-engine'; import { getProjectSettings, WRITING_MODES, getWritingMode, addSettingsNode, updateSettingsNode, deleteSettingsNode, getSettingsNodes, getActiveWorkId } from './lib/settings'; @@ -35,6 +37,7 @@ export default function Home() { const { chapters, setChapters, addChapter, updateChapter: updateChapterStore, activeChapterId, setActiveChapterId, + activeWorkId, setActiveWorkId: setActiveWorkIdStore, sidebarOpen, setSidebarOpen, toggleSidebar, aiSidebarOpen, setAiSidebarOpen, toggleAiSidebar, showSettings, setShowSettings, @@ -66,30 +69,41 @@ export default function Home() { const activeSession = useMemo(() => getActiveSession(sessionStore), [sessionStore]); const chatHistory = useMemo(() => activeSession?.messages || [], [activeSession]); + // 加载指定作品的章节 + const loadChaptersForWork = useCallback(async (workId) => { + let saved = await getChapters(workId); + // 自动修复:过滤掉损坏的章节数据 + if (Array.isArray(saved)) { + const cleaned = saved.filter(ch => ch && typeof ch === 'object' && ch.id); + if (cleaned.length !== saved.length) { + console.warn(`[数据修复] 发现 ${saved.length - cleaned.length} 条损坏的章节数据,已自动清理`); + saved = cleaned; + await saveChapters(saved, workId); + } + } else { + saved = []; + } + if (saved.length === 0) { + const first = await createChapter(t('page.firstChapterTitle'), workId); + setChapters([first]); + setActiveChapterId(first.id); + } else { + setChapters(saved); + setActiveChapterId(saved[0].id); + } + }, [t, setChapters, setActiveChapterId]); + // 初始化数据 useEffect(() => { const initData = async () => { - let saved = await getChapters(); - // 自动修复:过滤掉损坏的章节数据(如缺少 id 或非对象的条目) - if (Array.isArray(saved)) { - const cleaned = saved.filter(ch => ch && typeof ch === 'object' && ch.id); - if (cleaned.length !== saved.length) { - console.warn(`[数据修复] 发现 ${saved.length - cleaned.length} 条损坏的章节数据,已自动清理`); - saved = cleaned; - const { saveChapters } = await import('./lib/storage'); - await saveChapters(saved); - } - } else { - saved = []; - } - if (saved.length === 0) { - const first = await createChapter(t('page.firstChapterTitle')); - setChapters([first]); - setActiveChapterId(first.id); - } else { - setChapters(saved); - setActiveChapterId(saved[0].id); + const workId = getActiveWorkId(); + if (workId) { + setActiveWorkIdStore(workId); + // 一次性迁移旧全局章节 + await migrateGlobalChapters(workId); } + await loadChaptersForWork(workId); + const savedTheme = localStorage.getItem('author-theme') || 'light'; setTheme(savedTheme); setWritingMode(getWritingMode()); @@ -97,7 +111,6 @@ export default function Home() { // 加载会话数据 let store = await loadSessionStore(); if (store.sessions.length === 0) { - // 首次使用:创建一个空会话 store = createSession(store); } setSessionStore(store); @@ -105,6 +118,14 @@ export default function Home() { initData(); }, []); + // 切换作品时重新加载章节 + const prevWorkIdRef = useRef(activeWorkId); + useEffect(() => { + if (prevWorkIdRef.current === activeWorkId) return; + prevWorkIdRef.current = activeWorkId; + loadChaptersForWork(activeWorkId); + }, [activeWorkId, loadChaptersForWork]); + // 初始化上下文条目和勾选状态(设定集 + 章节 + 对话历史) useEffect(() => { if (!activeChapterId) return; @@ -167,11 +188,11 @@ export default function Home() { const updated = await updateChapter(activeChapterId, { content: html, wordCount, - }); + }, activeWorkId); if (updated) { updateChapterStore(activeChapterId, { content: html, wordCount }); } - }, [activeChapterId, updateChapterStore]); + }, [activeChapterId, activeWorkId, updateChapterStore]); // Inline AI 回调:编辑器调用此函数发起 AI 请求 const handleInlineAiRequest = useCallback(async ({ mode, text, instruction, signal, onChunk }) => { diff --git a/app/store/useAppStore.js b/app/store/useAppStore.js index a1df7ed..2261426 100644 --- a/app/store/useAppStore.js +++ b/app/store/useAppStore.js @@ -4,8 +4,10 @@ export const useAppStore = create((set, get) => ({ // --- Chapter State --- chapters: [], activeChapterId: null, + activeWorkId: typeof window !== 'undefined' ? localStorage.getItem('author-active-work') || null : null, setChapters: (chapters) => set({ chapters: Array.isArray(chapters) ? chapters.filter(ch => ch && typeof ch === 'object' && ch.id) : [] }), setActiveChapterId: (id) => set({ activeChapterId: id }), + setActiveWorkId: (id) => set({ activeWorkId: id }), addChapter: (chapter) => set((state) => ({ chapters: [...state.chapters, chapter] })), deleteChapter: (id) => set((state) => ({ chapters: state.chapters.filter((ch) => ch.id !== id) })), updateChapter: (id, updates) => set((state) => ({ diff --git a/package.json b/package.json index 93d9c8e..ae673ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "author-app", - "version": "1.1.1", + "version": "1.2.0", "private": true, "main": "electron/main.js", "scripts": {