feat: 添加文稿管理、素材库、设置页面及对照检查重写功能

- 新增 DocumentsPanel.vue 文稿管理页面
- 新增 MaterialsPanel.vue 素材库管理页面
- 新增 SettingsPanel.vue 设置页面
- 新增 DocumentSelectorModal.vue 文稿选择弹窗
- 新增 MaterialSelectorModal.vue 素材选择弹窗
- 集成 SQLite 数据库持久化 (sql.js)
- 对照检查页面支持从文稿库选取内容
- 对照检查页面新增一键重写及差异对比功能
- 修复对照检查页面布局问题
- MainContent 支持文稿编辑功能
This commit is contained in:
empty
2026-01-09 00:21:52 +08:00
parent a0faaf4157
commit 1a1d7dabdf
23 changed files with 5808 additions and 64 deletions

859
src/db/index.js Normal file
View File

@@ -0,0 +1,859 @@
// ============================================
// SQLite 数据库服务层
// ============================================
// 使用 sql.js 在浏览器端运行 SQLite
// 数据持久化到 IndexedDB
// 数据库实例
let db = null
let SQL = null
// IndexedDB 配置
const DB_NAME = 'AIWriterWorkshop'
const DB_STORE = 'database'
const DB_KEY = 'sqlite_db'
// ============================================
// 数据库初始化
// ============================================
/**
* 从 CDN 加载 sql.js
*/
const loadSqlJs = () => {
return new Promise((resolve, reject) => {
// 如果已经加载过
if (window.initSqlJs) {
resolve(window.initSqlJs)
return
}
// 动态加载脚本
const script = document.createElement('script')
script.src = 'https://sql.js.org/dist/sql-wasm.js'
script.async = true
script.onload = () => {
if (window.initSqlJs) {
resolve(window.initSqlJs)
} else {
reject(new Error('sql.js 加载失败'))
}
}
script.onerror = () => reject(new Error('sql.js 脚本加载失败'))
document.head.appendChild(script)
})
}
export const initDatabase = async () => {
if (db) return db
try {
// 从 CDN 加载 sql.js
const initSqlJs = await loadSqlJs()
// 加载 sql.js WASM
SQL = await initSqlJs({
locateFile: file => `https://sql.js.org/dist/${file}`
})
// 尝试从 IndexedDB 加载现有数据库
const savedData = await loadFromIndexedDB()
if (savedData) {
db = new SQL.Database(savedData)
console.log('📦 从 IndexedDB 加载数据库成功')
} else {
db = new SQL.Database()
console.log('🆕 创建新数据库')
// 初始化表结构
await initTables()
// 导入默认数据
await importDefaultData()
}
return db
} catch (error) {
console.error('❌ 数据库初始化失败:', error)
throw error
}
}
/**
* 初始化数据表结构
*/
const initTables = async () => {
// 素材表
db.run(`
CREATE TABLE IF NOT EXISTS materials (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
title TEXT NOT NULL,
source TEXT,
date TEXT,
tags TEXT,
related_dimension_sets TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_default INTEGER DEFAULT 0
)
`)
// 素材摘录表
db.run(`
CREATE TABLE IF NOT EXISTS reference_excerpts (
id TEXT PRIMARY KEY,
reference_id TEXT NOT NULL,
topic TEXT NOT NULL,
content TEXT NOT NULL,
applicable_dimensions TEXT,
use_for TEXT,
FOREIGN KEY (reference_id) REFERENCES materials(id) ON DELETE CASCADE
)
`)
// 范式表
db.run(`
CREATE TABLE IF NOT EXISTS paradigms (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT,
description TEXT,
tags TEXT,
tag_class TEXT,
system_constraints TEXT,
dimension_set_id TEXT,
custom_dimensions TEXT,
logic_paradigms TEXT,
auto_match_refs INTEGER DEFAULT 1,
selected_refs TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_custom INTEGER DEFAULT 0
)
`)
// 维度集表
db.run(`
CREATE TABLE IF NOT EXISTS dimension_sets (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
applicable_for TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_custom INTEGER DEFAULT 0
)
`)
// 维度表
db.run(`
CREATE TABLE IF NOT EXISTS dimensions (
id TEXT PRIMARY KEY,
dimension_set_id TEXT NOT NULL,
name TEXT NOT NULL,
focus TEXT,
keywords TEXT,
negative_keywords TEXT,
positive_benchmark TEXT,
sort_order INTEGER DEFAULT 0,
FOREIGN KEY (dimension_set_id) REFERENCES dimension_sets(id) ON DELETE CASCADE
)
`)
// 用户配置表
db.run(`
CREATE TABLE IF NOT EXISTS user_config (
key TEXT PRIMARY KEY,
value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// 分析历史表
db.run(`
CREATE TABLE IF NOT EXISTS analysis_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
paradigm_id TEXT,
input_text TEXT,
result TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// 文稿记录表
db.run(`
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
paradigm_id TEXT,
dimension_set_id TEXT,
selected_refs TEXT,
status TEXT DEFAULT 'draft',
word_count INTEGER DEFAULT 0,
tags TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// 文稿版本历史表
db.run(`
CREATE TABLE IF NOT EXISTS document_versions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
document_id TEXT NOT NULL,
content TEXT,
version_number INTEGER,
change_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE
)
`)
console.log('✅ 数据表初始化完成')
}
// ============================================
// IndexedDB 持久化
// ============================================
/**
* 保存数据库到 IndexedDB
*/
export const saveToIndexedDB = async () => {
if (!db) return
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1)
request.onerror = () => reject(request.error)
request.onupgradeneeded = (event) => {
const idb = event.target.result
if (!idb.objectStoreNames.contains(DB_STORE)) {
idb.createObjectStore(DB_STORE)
}
}
request.onsuccess = (event) => {
const idb = event.target.result
const transaction = idb.transaction([DB_STORE], 'readwrite')
const store = transaction.objectStore(DB_STORE)
const data = db.export()
const buffer = new Uint8Array(data)
const putRequest = store.put(buffer, DB_KEY)
putRequest.onsuccess = () => {
console.log('💾 数据库已保存到 IndexedDB')
resolve()
}
putRequest.onerror = () => reject(putRequest.error)
}
})
}
/**
* 从 IndexedDB 加载数据库
*/
const loadFromIndexedDB = async () => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1)
request.onerror = () => reject(request.error)
request.onupgradeneeded = (event) => {
const idb = event.target.result
if (!idb.objectStoreNames.contains(DB_STORE)) {
idb.createObjectStore(DB_STORE)
}
}
request.onsuccess = (event) => {
const idb = event.target.result
const transaction = idb.transaction([DB_STORE], 'readonly')
const store = transaction.objectStore(DB_STORE)
const getRequest = store.get(DB_KEY)
getRequest.onsuccess = () => {
resolve(getRequest.result || null)
}
getRequest.onerror = () => resolve(null)
}
})
}
// ============================================
// 通用 CRUD 操作
// ============================================
/**
* 执行查询并返回结果
*/
export const query = (sql, params = []) => {
if (!db) throw new Error('数据库未初始化')
try {
const stmt = db.prepare(sql)
stmt.bind(params)
const results = []
while (stmt.step()) {
results.push(stmt.getAsObject())
}
stmt.free()
return results
} catch (error) {
console.error('查询失败:', sql, error)
throw error
}
}
/**
* 执行单条查询
*/
export const queryOne = (sql, params = []) => {
const results = query(sql, params)
return results.length > 0 ? results[0] : null
}
/**
* 执行更新/插入操作
*/
export const execute = (sql, params = []) => {
if (!db) throw new Error('数据库未初始化')
try {
db.run(sql, params)
// 自动保存到 IndexedDB
saveToIndexedDB()
return true
} catch (error) {
console.error('执行失败:', sql, error)
throw error
}
}
/**
* 批量执行
*/
export const executeBatch = (statements) => {
if (!db) throw new Error('数据库未初始化')
try {
statements.forEach(({ sql, params = [] }) => {
db.run(sql, params)
})
saveToIndexedDB()
return true
} catch (error) {
console.error('批量执行失败:', error)
throw error
}
}
// ============================================
// 素材库 CRUD
// ============================================
/**
* 获取所有素材
*/
export const getAllReferences = () => {
const refs = query('SELECT * FROM materials ORDER BY created_at DESC')
return refs.map(ref => ({
...ref,
tags: ref.tags ? JSON.parse(ref.tags) : [],
relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [],
excerpts: getExcerptsByReferenceId(ref.id)
}))
}
/**
* 根据ID获取素材
*/
export const getReferenceById = (id) => {
const ref = queryOne('SELECT * FROM materials WHERE id = ?', [id])
if (!ref) return null
return {
...ref,
tags: ref.tags ? JSON.parse(ref.tags) : [],
relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [],
excerpts: getExcerptsByReferenceId(ref.id)
}
}
/**
* 根据类型获取素材
*/
export const getReferencesByType = (type) => {
const refs = query('SELECT * FROM materials WHERE type = ? ORDER BY created_at DESC', [type])
return refs.map(ref => ({
...ref,
tags: ref.tags ? JSON.parse(ref.tags) : [],
relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [],
excerpts: getExcerptsByReferenceId(ref.id)
}))
}
/**
* 获取素材的摘录
*/
const getExcerptsByReferenceId = (referenceId) => {
const excerpts = query('SELECT * FROM reference_excerpts WHERE reference_id = ?', [referenceId])
return excerpts.map(e => ({
...e,
applicableDimensions: e.applicable_dimensions ? JSON.parse(e.applicable_dimensions) : []
}))
}
/**
* 添加素材
*/
export const addReference = (reference) => {
const id = reference.id || `ref-${Date.now()}`
execute(`
INSERT INTO materials (id, type, title, source, date, tags, related_dimension_sets, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
reference.type,
reference.title,
reference.source || null,
reference.date || null,
JSON.stringify(reference.tags || []),
JSON.stringify(reference.relatedDimensionSets || []),
reference.isDefault ? 1 : 0
])
// 添加摘录
if (reference.excerpts?.length) {
reference.excerpts.forEach((excerpt, index) => {
execute(`
INSERT INTO reference_excerpts (id, reference_id, topic, content, applicable_dimensions, use_for)
VALUES (?, ?, ?, ?, ?, ?)
`, [
excerpt.id || `${id}-excerpt-${index}`,
id,
excerpt.topic,
excerpt.content,
JSON.stringify(excerpt.applicableDimensions || []),
excerpt.useFor || null
])
})
}
return id
}
/**
* 更新素材
*/
export const updateReference = (id, updates) => {
const setClauses = []
const params = []
if (updates.type !== undefined) {
setClauses.push('type = ?')
params.push(updates.type)
}
if (updates.title !== undefined) {
setClauses.push('title = ?')
params.push(updates.title)
}
if (updates.source !== undefined) {
setClauses.push('source = ?')
params.push(updates.source)
}
if (updates.tags !== undefined) {
setClauses.push('tags = ?')
params.push(JSON.stringify(updates.tags))
}
if (updates.relatedDimensionSets !== undefined) {
setClauses.push('related_dimension_sets = ?')
params.push(JSON.stringify(updates.relatedDimensionSets))
}
setClauses.push('updated_at = CURRENT_TIMESTAMP')
params.push(id)
execute(`UPDATE materials SET ${setClauses.join(', ')} WHERE id = ?`, params)
return true
}
/**
* 删除素材
*/
export const deleteReference = (id) => {
execute('DELETE FROM reference_excerpts WHERE reference_id = ?', [id])
execute('DELETE FROM materials WHERE id = ?', [id])
return true
}
// ============================================
// 范式 CRUD
// ============================================
/**
* 获取所有范式
*/
export const getAllParadigms = () => {
const paradigms = query('SELECT * FROM paradigms ORDER BY is_custom ASC, created_at DESC')
return paradigms.map(p => ({
...p,
tags: p.tags ? JSON.parse(p.tags) : [],
systemConstraints: p.system_constraints ? JSON.parse(p.system_constraints) : [],
customDimensions: p.custom_dimensions ? JSON.parse(p.custom_dimensions) : null,
logicParadigms: p.logic_paradigms ? JSON.parse(p.logic_paradigms) : null,
selectedRefs: p.selected_refs ? JSON.parse(p.selected_refs) : [],
isCustom: p.is_custom === 1,
autoMatchRefs: p.auto_match_refs === 1
}))
}
/**
* 添加范式
*/
export const addParadigm = (paradigm) => {
const id = paradigm.id || `paradigm-${Date.now()}`
execute(`
INSERT INTO paradigms (id, name, icon, description, tags, tag_class, system_constraints,
dimension_set_id, custom_dimensions, logic_paradigms, auto_match_refs, selected_refs, is_custom)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
paradigm.name,
paradigm.icon || '📝',
paradigm.description || null,
JSON.stringify(paradigm.tags || []),
paradigm.tagClass || 'bg-blue-900/30 text-blue-300',
JSON.stringify(paradigm.systemConstraints || []),
paradigm.dimensionSetId || null,
paradigm.customDimensions ? JSON.stringify(paradigm.customDimensions) : null,
paradigm.logicParadigms ? JSON.stringify(paradigm.logicParadigms) : null,
paradigm.autoMatchRefs !== false ? 1 : 0,
JSON.stringify(paradigm.selectedRefs || []),
paradigm.isCustom ? 1 : 0
])
return id
}
/**
* 更新范式
*/
export const updateParadigm = (id, updates) => {
const setClauses = []
const params = []
const fieldMap = {
name: 'name',
icon: 'icon',
description: 'description',
tagClass: 'tag_class',
dimensionSetId: 'dimension_set_id',
autoMatchRefs: 'auto_match_refs'
}
Object.entries(updates).forEach(([key, value]) => {
if (fieldMap[key]) {
setClauses.push(`${fieldMap[key]} = ?`)
params.push(key === 'autoMatchRefs' ? (value ? 1 : 0) : value)
}
})
if (updates.tags !== undefined) {
setClauses.push('tags = ?')
params.push(JSON.stringify(updates.tags))
}
if (updates.systemConstraints !== undefined) {
setClauses.push('system_constraints = ?')
params.push(JSON.stringify(updates.systemConstraints))
}
if (updates.customDimensions !== undefined) {
setClauses.push('custom_dimensions = ?')
params.push(updates.customDimensions ? JSON.stringify(updates.customDimensions) : null)
}
if (updates.selectedRefs !== undefined) {
setClauses.push('selected_refs = ?')
params.push(JSON.stringify(updates.selectedRefs))
}
setClauses.push('updated_at = CURRENT_TIMESTAMP')
params.push(id)
execute(`UPDATE paradigms SET ${setClauses.join(', ')} WHERE id = ?`, params)
return true
}
/**
* 删除范式
*/
export const deleteParadigm = (id) => {
execute('DELETE FROM paradigms WHERE id = ?', [id])
return true
}
// ============================================
// 用户配置 CRUD
// ============================================
/**
* 获取配置
*/
export const getConfig = (key, defaultValue = null) => {
const result = queryOne('SELECT value FROM user_config WHERE key = ?', [key])
if (!result) return defaultValue
try {
return JSON.parse(result.value)
} catch {
return result.value
}
}
/**
* 设置配置
*/
export const setConfig = (key, value) => {
const valueStr = typeof value === 'string' ? value : JSON.stringify(value)
execute(`
INSERT INTO user_config (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
`, [key, valueStr, valueStr])
return true
}
// ============================================
// 文稿 CRUD
// ============================================
/**
* 获取所有文稿
*/
export const getAllDocuments = () => {
const docs = query('SELECT * FROM documents ORDER BY updated_at DESC')
return docs.map(doc => ({
...doc,
tags: doc.tags ? JSON.parse(doc.tags) : [],
selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : []
}))
}
/**
* 根据ID获取文稿
*/
export const getDocumentById = (id) => {
const doc = queryOne('SELECT * FROM documents WHERE id = ?', [id])
if (!doc) return null
return {
...doc,
tags: doc.tags ? JSON.parse(doc.tags) : [],
selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : [],
versions: getDocumentVersions(id)
}
}
/**
* 获取文稿版本历史
*/
export const getDocumentVersions = (documentId) => {
return query('SELECT * FROM document_versions WHERE document_id = ? ORDER BY version_number DESC', [documentId])
}
/**
* 创建文稿
*/
export const createDocument = (document) => {
const id = document.id || `doc-${Date.now()}`
execute(`
INSERT INTO documents (id, title, content, paradigm_id, dimension_set_id, selected_refs, status, word_count, tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
id,
document.title || '未命名文稿',
document.content || '',
document.paradigmId || null,
document.dimensionSetId || null,
JSON.stringify(document.selectedRefs || []),
document.status || 'draft',
document.wordCount || 0,
JSON.stringify(document.tags || [])
])
return id
}
/**
* 更新文稿
*/
export const updateDocument = (id, updates) => {
const setClauses = []
const params = []
if (updates.title !== undefined) {
setClauses.push('title = ?')
params.push(updates.title)
}
if (updates.content !== undefined) {
setClauses.push('content = ?')
params.push(updates.content)
// 自动计算字数
setClauses.push('word_count = ?')
params.push(updates.content.length)
}
if (updates.paradigmId !== undefined) {
setClauses.push('paradigm_id = ?')
params.push(updates.paradigmId)
}
if (updates.status !== undefined) {
setClauses.push('status = ?')
params.push(updates.status)
}
if (updates.tags !== undefined) {
setClauses.push('tags = ?')
params.push(JSON.stringify(updates.tags))
}
if (updates.selectedRefs !== undefined) {
setClauses.push('selected_refs = ?')
params.push(JSON.stringify(updates.selectedRefs))
}
setClauses.push('updated_at = CURRENT_TIMESTAMP')
params.push(id)
execute(`UPDATE documents SET ${setClauses.join(', ')} WHERE id = ?`, params)
return true
}
/**
* 保存文稿版本
*/
export const saveDocumentVersion = (documentId, content, changeNote = '') => {
// 获取当前最大版本号
const result = queryOne('SELECT MAX(version_number) as max_version FROM document_versions WHERE document_id = ?', [documentId])
const nextVersion = (result?.max_version || 0) + 1
execute(`
INSERT INTO document_versions (document_id, content, version_number, change_note)
VALUES (?, ?, ?, ?)
`, [documentId, content, nextVersion, changeNote])
return nextVersion
}
/**
* 删除文稿
*/
export const deleteDocument = (id) => {
execute('DELETE FROM document_versions WHERE document_id = ?', [id])
execute('DELETE FROM documents WHERE id = ?', [id])
return true
}
/**
* 根据状态筛选文稿
*/
export const getDocumentsByStatus = (status) => {
const docs = query('SELECT * FROM documents WHERE status = ? ORDER BY updated_at DESC', [status])
return docs.map(doc => ({
...doc,
tags: doc.tags ? JSON.parse(doc.tags) : [],
selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : []
}))
}
// ============================================
// 数据导入/导出
// ============================================
/**
* 导出数据库为二进制
*/
export const exportDatabase = () => {
if (!db) throw new Error('数据库未初始化')
return db.export()
}
/**
* 导入数据库
*/
export const importDatabase = async (data) => {
if (!SQL) throw new Error('SQL.js 未初始化')
db = new SQL.Database(new Uint8Array(data))
await saveToIndexedDB()
return true
}
/**
* 导出为 JSON
*/
export const exportAsJSON = () => {
return {
references: getAllReferences(),
paradigms: getAllParadigms(),
documents: getAllDocuments(),
config: query('SELECT * FROM user_config'),
exportedAt: new Date().toISOString()
}
}
/**
* 重置数据库
*/
export const resetDatabase = async () => {
if (!SQL) throw new Error('SQL.js 未初始化')
db = new SQL.Database()
await initTables()
await importDefaultData()
await saveToIndexedDB()
return true
}
// ============================================
// 默认数据导入
// ============================================
/**
* 导入默认数据(从静态配置文件)
*/
const importDefaultData = async () => {
// 导入默认素材
const { REFERENCES } = await import('../config/references.js')
Object.values(REFERENCES).forEach(ref => {
addReference({
...ref,
isDefault: true
})
})
console.log('✅ 默认素材导入完成')
}
// 导出数据库实例(用于调试)
export const getDb = () => db