feat: 添加文稿版本管理、差异对比高亮和多项功能优化
- 修复AI重写输出过滤(过滤thinking/draft标签) - 添加文稿保存与版本管理功能 - 新增插入差异功能(差异对比模式) - 实现版本历史侧边抽屉(DocumentVersionPanel) - 添加版本对比差异高亮显示 - 调整文稿编辑区高度适应屏幕 - 新增MarkdownEditor组件(暂未启用)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user