172 lines
5.4 KiB
JavaScript
172 lines
5.4 KiB
JavaScript
// 本地存储工具 - 使用持久化适配器管理核心数据 (章节、摘要)
|
||
// 优先服务端文件系统,fallback 到浏览器 IndexedDB
|
||
// 章节按作品(workId)隔离存储
|
||
|
||
import { persistGet, persistSet, persistDel } from './persistence';
|
||
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* 一次性迁移:将旧的全局 author-chapters 剪切到当前活跃作品
|
||
* 调用方在 page.js initData 中负责调用
|
||
*/
|
||
export async function migrateGlobalChapters(workId) {
|
||
if (typeof window === 'undefined' || !workId) return;
|
||
try {
|
||
const perWorkData = await persistGet(getStorageKey(workId));
|
||
if (perWorkData) return; // 该作品已有数据,不迁移
|
||
|
||
const globalData = await persistGet(LEGACY_STORAGE_KEY);
|
||
if (globalData && Array.isArray(globalData) && globalData.length > 0) {
|
||
await persistSet(getStorageKey(workId), globalData);
|
||
await persistDel(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 persistGet(key);
|
||
if (!chapters) {
|
||
chapters = [];
|
||
}
|
||
return chapters;
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 保存所有章节 (Async)
|
||
export async function saveChapters(chapters, workId) {
|
||
if (typeof window === 'undefined') return;
|
||
await persistSet(getStorageKey(workId), chapters);
|
||
}
|
||
|
||
// 创建新章节 (Async)
|
||
export async function createChapter(title = '未命名章节', workId) {
|
||
const chapters = await getChapters(workId);
|
||
const newChapter = {
|
||
id: generateId(),
|
||
title,
|
||
content: '',
|
||
wordCount: 0,
|
||
createdAt: new Date().toISOString(),
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
chapters.push(newChapter);
|
||
await saveChapters(chapters, workId);
|
||
return newChapter;
|
||
}
|
||
|
||
// 更新章节 (Async)
|
||
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;
|
||
|
||
chapters[index] = {
|
||
...chapters[index],
|
||
...updates,
|
||
updatedAt: new Date().toISOString(),
|
||
};
|
||
await saveChapters(chapters, workId);
|
||
return chapters[index];
|
||
}
|
||
|
||
// 删除章节 (Async)
|
||
export async function deleteChapter(id, workId) {
|
||
const chapters = await getChapters(workId);
|
||
const newChapters = chapters.filter(ch => ch.id !== id);
|
||
await saveChapters(newChapters, workId);
|
||
return newChapters;
|
||
}
|
||
|
||
// 获取单个章节 (Async)
|
||
export async function getChapter(id, workId) {
|
||
const chapters = await getChapters(workId);
|
||
return chapters.find(ch => ch.id === id) || null;
|
||
}
|
||
|
||
// 通用下载辅助:优先使用 File System Access API(弹出系统另存为对话框),
|
||
// 回退到 data URL(兼容旧浏览器)
|
||
async function downloadTextFile(content, fileName) {
|
||
// 方式一:File System Access API(Chrome/Edge 86+)
|
||
if (typeof window !== 'undefined' && window.showSaveFilePicker) {
|
||
try {
|
||
const ext = fileName.split('.').pop() || 'txt';
|
||
const handle = await window.showSaveFilePicker({
|
||
suggestedName: fileName,
|
||
types: [{
|
||
description: ext.toUpperCase() + ' File',
|
||
accept: { 'text/plain': ['.' + ext] },
|
||
}],
|
||
});
|
||
const writable = await handle.createWritable();
|
||
await writable.write(content);
|
||
await writable.close();
|
||
return;
|
||
} catch (e) {
|
||
if (e.name === 'AbortError') return; // 用户取消
|
||
// 降级到方式二
|
||
}
|
||
}
|
||
// 方式二:data URL
|
||
const a = document.createElement('a');
|
||
a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(content);
|
||
a.download = fileName;
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
}
|
||
|
||
// 导出为 Markdown
|
||
export function exportToMarkdown(chapter) {
|
||
const md = `# ${chapter.title}\n\n${chapter.content.replace(/<[^>]*>/g, '').replace(/ /g, ' ')}`;
|
||
downloadTextFile(md, `${chapter.title || '未命名'}.md`);
|
||
}
|
||
|
||
// 导出所有章节
|
||
export function exportAllToMarkdown(chapters) {
|
||
const md = chapters.map(ch => {
|
||
const text = ch.content.replace(/<[^>]*>/g, '').replace(/ /g, ' ');
|
||
return `# ${ch.title}\n\n${text}`;
|
||
}).join('\n\n---\n\n');
|
||
|
||
downloadTextFile(md, '全部章节.md');
|
||
}
|
||
|
||
// ==================== 章节摘要缓存 ====================
|
||
|
||
const SUMMARY_PREFIX = 'author-chapter-summary-';
|
||
|
||
// 获取章节摘要 (Async)
|
||
export async function getChapterSummary(id) {
|
||
if (typeof window === 'undefined') return null;
|
||
try {
|
||
const summary = await persistGet(SUMMARY_PREFIX + id);
|
||
return summary || null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// 保存章节摘要 (Async)
|
||
export async function saveChapterSummary(id, summary) {
|
||
if (typeof window === 'undefined') return;
|
||
await persistSet(SUMMARY_PREFIX + id, summary);
|
||
}
|