feat: 添加文稿版本管理、差异对比高亮和多项功能优化

- 修复AI重写输出过滤(过滤thinking/draft标签)
- 添加文稿保存与版本管理功能
- 新增插入差异功能(差异对比模式)
- 实现版本历史侧边抽屉(DocumentVersionPanel)
- 添加版本对比差异高亮显示
- 调整文稿编辑区高度适应屏幕
- 新增MarkdownEditor组件(暂未启用)
This commit is contained in:
empty
2026-01-09 02:44:13 +08:00
parent 5a22477075
commit e153e10d48
8 changed files with 1547 additions and 5 deletions

View File

@@ -153,7 +153,26 @@
<h2 class="text-sm font-medium text-blue-400 flex items-center gap-2">
写作内容
</h2>
<span class="text-xs text-slate-500">{{ rightParagraphs.length }} </span>
<div class="flex items-center gap-2">
<span class="text-xs text-slate-500">{{ rightParagraphs.length }} </span>
<!-- 保存按钮仅当内容来自文稿库时显示 -->
<button
v-if="rightSourceType === 'document' && rightSourceDocId"
@click="saveRightContent"
:disabled="isSaving || !hasContentChanged"
class="text-xs px-2 py-1 rounded flex items-center gap-1 transition"
:class="hasContentChanged
? 'bg-green-600 text-white hover:bg-green-500'
: 'bg-slate-700 text-slate-400 cursor-not-allowed'"
>
<span v-if="isSaving" class="animate-spin"></span>
<span v-else>💾</span>
{{ isSaving ? '保存中...' : (hasContentChanged ? '保存版本' : '已保存') }}
</button>
<span v-if="rightSourceType === 'document' && rightSourceDocTitle" class="text-xs text-blue-400">
📄 {{ rightSourceDocTitle }}
</span>
</div>
</div>
<!-- 来源选择器 -->
<div class="px-3 py-2 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
@@ -651,11 +670,18 @@
>取消</button>
<!-- 差异对比模式的应用选中按钮 -->
<button
v-if="rewriteViewMode !== 'partial' && diffSegments.length > 0 && acceptedChanges.size < reviewableChanges.length"
v-if="rewriteViewMode === 'diff' && diffSegments.length > 0 && acceptedChanges.size < reviewableChanges.length"
@click="applySelectedChangesToContent"
:disabled="rewritingSuggestionIdx !== null || acceptedChanges.size === 0"
class="px-4 py-2 text-sm bg-amber-600 text-white rounded-lg hover:bg-amber-500 disabled:opacity-50 disabled:cursor-not-allowed"
>应用选中 ({{ acceptedChanges.size }})</button>
<!-- 差异对比模式的插入差异按钮 -->
<button
v-if="rewriteViewMode === 'diff' && diffSegments.length > 0"
@click="insertSelectedChangesToContent"
:disabled="rewritingSuggestionIdx !== null || acceptedChanges.size === 0"
class="px-4 py-2 text-sm bg-cyan-600 text-white rounded-lg hover:bg-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed"
>插入差异</button>
<!-- 部分替换模式的应用按钮 -->
<button
v-if="rewriteViewMode === 'partial'"
@@ -680,6 +706,7 @@ import { ref, computed, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useAppStore } from '../stores/app'
import { useDatabaseStore } from '../stores/database'
import { updateDocument, saveDocumentVersion, getDocumentById } from '../db/index.js'
import { SECTION_TYPES, getSectionTypeById, getSectionTypeClasses } from '../config/sectionTypes'
import { getLogicParadigmById, buildLogicPrompt } from '../config/logicParadigms'
import { computeDiff, applySelectedChanges as applyDiffChanges, getDiffStats, splitIntoSentencesWithPosition } from '../utils/textDiff'
@@ -702,6 +729,10 @@ const showLeftDocSelector = ref(false)
// 右侧来源相关
const rightSourceType = ref('paste') // 'paste' | 'document'
const showRightDocSelector = ref(false)
const rightSourceDocId = ref(null) // 来源文稿ID
const rightSourceDocTitle = ref('') // 来源文稿标题
const rightOriginalContent = ref('') // 来源文稿原始内容(用于差异比对)
const isSaving = ref(false) // 保存中状态
// 段落类型识别相关
const isDetectingTypes = ref(false)
@@ -814,6 +845,12 @@ const canCompare = computed(() => {
return selectedLeftIdxs.value.length > 0 && selectedRightIdxs.value.length > 0
})
// 右侧内容是否已修改(与原始内容对比)
const hasContentChanged = computed(() => {
if (rightSourceType.value !== 'document' || !rightSourceDocId.value) return false
return rightContent.value !== rightOriginalContent.value
})
// 选择段落(多选切换)
const selectLeftParagraph = (idx) => {
const i = selectedLeftIdxs.value.indexOf(idx)
@@ -888,9 +925,55 @@ const handleLeftDocSelect = (doc) => {
const handleRightDocSelect = (doc) => {
rightContent.value = doc.content || ''
rightSourceType.value = 'document'
rightSourceDocId.value = doc.id // 保存文稿ID
rightSourceDocTitle.value = doc.title // 保存文稿标题
rightOriginalContent.value = doc.content || '' // 保存原始内容用于差异比对
showRightDocSelector.value = false
}
// 保存右侧内容到文稿
const saveRightContent = async () => {
if (rightSourceType.value !== 'document' || !rightSourceDocId.value) {
alert('当前内容不是来自文稿库,无需保存')
return
}
if (!hasContentChanged.value) {
alert('内容未修改,无需保存')
return
}
isSaving.value = true
try {
// 计算差异用于生成版本说明
const changes = computeDiff(rightOriginalContent.value, rightContent.value)
const stats = getDiffStats(changes)
// 生成简短的变更说明
let changeNote = `修改了 ${stats.modified}`
if (stats.added > 0) changeNote += `,新增了 ${stats.added}`
if (stats.removed > 0) changeNote += `,删除了 ${stats.removed}`
// 保存新版本
const versionNumber = saveDocumentVersion(rightSourceDocId.value, rightContent.value, changeNote)
// 同时更新文稿主内容
updateDocument(rightSourceDocId.value, { content: rightContent.value })
// 更新原始内容为当前内容(标记为已保存)
rightOriginalContent.value = rightContent.value
console.log(`文稿保存成功,版本号: ${versionNumber}${changeNote}`)
alert(`保存成功!新版本: v${versionNumber}\n${changeNote}`)
} catch (error) {
console.error('保存失败:', error)
alert('保存失败,请重试')
} finally {
isSaving.value = false
}
}
// 切换段落类型下拉菜单
const toggleTypeDropdown = (idx) => {
showTypeDropdown.value = showTypeDropdown.value === idx ? null : idx
@@ -1042,6 +1125,14 @@ const executeRewrite = async (suggestionIdx, suggestion) => {
finalContent = finalContent.replace(/<thinking>[\s\S]*?<\/thinking>/g, '').trim()
// 也移除可能残留的单独标签
finalContent = finalContent.replace(/<\/?thinking>/g, '').trim()
// 提取 <draft>...</draft> 中的内容,或移除标签
const draftMatch = finalContent.match(/<draft>([\s\S]*?)<\/draft>/)
if (draftMatch) {
finalContent = draftMatch[1].trim()
} else {
// 移除残留的 <draft> 或 </draft> 标签
finalContent = finalContent.replace(/<\/?draft>/g, '').trim()
}
rewritePreview.value = finalContent
// 重写完成后自动计算差异
@@ -1186,6 +1277,59 @@ const applySelectedChangesToContent = () => {
resetRewriteModal()
}
// 将选中的修改插入到原文(不替换原文内容)
const insertSelectedChangesToContent = () => {
if (diffSegments.value.length === 0) return
// 如果没有选中任何修改,提示用户并返回
if (acceptedChanges.value.size === 0) {
alert('请先选中需要插入的内容')
return
}
// 收集选中的修改片段的重写内容
const selectedSegments = diffSegments.value.filter(seg =>
acceptedChanges.value.has(seg.idx) &&
(seg.type === 'modified' || seg.type === 'added')
)
if (selectedSegments.length === 0) {
alert('没有可插入的内容(请选择"修改"或"新增"类型的差异)')
return
}
// 获取要插入的内容
const insertContent = selectedSegments.map(seg => seg.rewritten).join('')
// 找到第一个选中修改对应的原文位置,在其后插入
const firstSegment = selectedSegments[0]
let finalContent = originalContent.value
if (firstSegment.originalEnd !== undefined) {
// 在原文对应位置后插入新内容
const insertPosition = firstSegment.originalEnd
finalContent = originalContent.value.slice(0, insertPosition) +
insertContent +
originalContent.value.slice(insertPosition)
} else {
// 如果没有位置信息,追加到末尾
finalContent = originalContent.value + '\n\n' + insertContent
}
// 防止结果为空
if (!finalContent || finalContent.trim() === '') {
console.error('插入操作返回空结果,保持原内容不变')
alert('插入失败,请尝试其他方式')
return
}
// 执行更新
updateRightContent(targetParagraphIdxs.value, finalContent)
// 关闭浮窗并重置状态
resetRewriteModal()
}
// 重置重写浮窗状态
const resetRewriteModal = () => {
showRewriteModal.value = false