feat: 完善范式润色功能和相关组件
- 新增范式选择模态框 (ParadigmSelectorModal) - 新增差异标注面板 (DiffAnnotationPanel) - 新增精确差异处理工具 (preciseDiff.js) - 更新各面板组件支持范式润色功能 - 更新范式配置文件 (paradigms.js) - 更新依赖配置 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col bg-slate-900">
|
||||
<div class="h-screen w-full flex flex-col bg-slate-900">
|
||||
<!-- 头部 -->
|
||||
<header class="p-4 border-b border-slate-700 bg-slate-800 flex items-center justify-between shrink-0">
|
||||
<div class="flex items-center gap-4">
|
||||
@@ -43,16 +43,39 @@
|
||||
</div>
|
||||
|
||||
<!-- 范式规则展开区 -->
|
||||
<div v-if="showParadigmRules && hasParadigmRules" class="px-4 py-3 bg-indigo-950/50 border-b border-indigo-800/30 max-h-48 overflow-y-auto shrink-0">
|
||||
<div v-if="showParadigmRules && hasParadigmRules" class="px-4 py-3 bg-indigo-950/50 border-b border-indigo-800/30 max-h-56 overflow-y-auto shrink-0">
|
||||
<!-- 操作栏 -->
|
||||
<div class="flex items-center justify-between mb-2 pb-2 border-b border-indigo-800/30">
|
||||
<span class="text-xs text-indigo-300">已启用 {{ enabledRulesCount }}/{{ paradigmRulesCount }} 条规则</span>
|
||||
<button
|
||||
@click="toggleAllRules"
|
||||
class="text-xs px-2 py-0.5 rounded bg-indigo-700/50 text-indigo-200 hover:bg-indigo-600/50 transition"
|
||||
>
|
||||
{{ enabledRulesCount === paradigmRulesCount ? '取消全选' : '全选' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 规则列表 -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div v-for="(rule, idx) in getFilteredRules(null)" :key="idx"
|
||||
:class="['text-xs px-2 py-1 rounded flex items-start gap-1',
|
||||
rule.scope === checkMode ? 'bg-indigo-800/50 text-indigo-100' : 'bg-slate-800/50 text-slate-400'
|
||||
<div v-for="rule in allRulesWithIdx" :key="rule.idx"
|
||||
@click="toggleRule(rule.idx)"
|
||||
:class="['text-xs px-2 py-1.5 rounded flex items-start gap-2 cursor-pointer transition border',
|
||||
enabledRuleIdxs.has(rule.idx)
|
||||
? (rule.scope === checkMode ? 'bg-indigo-800/50 text-indigo-100 border-indigo-600/50' : 'bg-slate-800/50 text-slate-300 border-slate-600/50')
|
||||
: 'bg-slate-900/50 text-slate-500 border-slate-700/30 opacity-60'
|
||||
]">
|
||||
<span :class="['shrink-0 px-1 rounded text-[10px]',
|
||||
rule.scope === 'paragraph' ? 'bg-amber-600/50 text-amber-200' : 'bg-blue-600/50 text-blue-200'
|
||||
]">{{ rule.scope === 'paragraph' ? '段' : '文' }}</span>
|
||||
<span>{{ rule.text }}</span>
|
||||
<!-- 勾选框 -->
|
||||
<div :class="['w-4 h-4 rounded border flex items-center justify-center shrink-0 mt-0.5 transition',
|
||||
enabledRuleIdxs.has(rule.idx) ? 'bg-indigo-600 border-indigo-500' : 'bg-slate-800 border-slate-600'
|
||||
]">
|
||||
<span v-if="enabledRuleIdxs.has(rule.idx)" class="text-white text-[10px]">✓</span>
|
||||
</div>
|
||||
<!-- 规则标签和文本 -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<span :class="['shrink-0 px-1 rounded text-[10px] mr-1',
|
||||
rule.scope === 'paragraph' ? 'bg-amber-600/50 text-amber-200' : 'bg-blue-600/50 text-blue-200'
|
||||
]">{{ rule.scope === 'paragraph' ? '段' : '文' }}</span>
|
||||
<span class="break-words">{{ rule.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -65,7 +88,26 @@
|
||||
<h2 class="text-sm font-medium text-amber-400 flex items-center gap-2">
|
||||
📋 要求原文
|
||||
</h2>
|
||||
<span class="text-xs text-slate-500">{{ leftParagraphs.length }} 段</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-slate-500">{{ leftParagraphs.length }} 段</span>
|
||||
<!-- 保存按钮(仅当内容来自文稿库时显示) -->
|
||||
<button
|
||||
v-if="leftSourceType === 'document' && leftSourceDocId"
|
||||
@click="saveLeftContent"
|
||||
:disabled="isLeftSaving || !hasLeftContentChanged"
|
||||
class="text-xs px-2 py-1 rounded flex items-center gap-1 transition"
|
||||
:class="hasLeftContentChanged
|
||||
? 'bg-green-600 text-white hover:bg-green-500'
|
||||
: 'bg-slate-700 text-slate-400 cursor-not-allowed'"
|
||||
>
|
||||
<span v-if="isLeftSaving" class="animate-spin">↻</span>
|
||||
<span v-else>💾</span>
|
||||
{{ isLeftSaving ? '保存中...' : (hasLeftContentChanged ? '保存版本' : '已保存') }}
|
||||
</button>
|
||||
<span v-if="leftSourceType === 'document' && leftSourceDocTitle" class="text-xs text-amber-400">
|
||||
📄 {{ leftSourceDocTitle }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 来源选择器 -->
|
||||
<div class="px-3 py-2 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
|
||||
@@ -77,11 +119,10 @@
|
||||
leftSourceType === 'paste' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>粘贴</button>
|
||||
<button
|
||||
v-if="activeParadigm?.defaultReference"
|
||||
@click="loadParadigmReference"
|
||||
@click="showParadigmSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
leftSourceType === 'paradigm' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>范式范文</button>
|
||||
>范式库</button>
|
||||
<button
|
||||
@click="showMaterialSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
@@ -373,6 +414,13 @@
|
||||
@select="handleMaterialSelect"
|
||||
/>
|
||||
|
||||
<!-- 范式选择弹窗 -->
|
||||
<ParadigmSelectorModal
|
||||
:visible="showParadigmSelector"
|
||||
@close="showParadigmSelector = false"
|
||||
@select="handleParadigmSelect"
|
||||
/>
|
||||
|
||||
<!-- 文稿选择弹窗(左侧) -->
|
||||
<DocumentSelectorModal
|
||||
:visible="showLeftDocSelector"
|
||||
@@ -712,6 +760,7 @@ import { getLogicParadigmById, buildLogicPrompt } from '../config/logicParadigms
|
||||
import { computeDiff, applySelectedChanges as applyDiffChanges, getDiffStats, splitIntoSentencesWithPosition } from '../utils/textDiff'
|
||||
import MaterialSelectorModal from './MaterialSelectorModal.vue'
|
||||
import DocumentSelectorModal from './DocumentSelectorModal.vue'
|
||||
import ParadigmSelectorModal from './ParadigmSelectorModal.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const dbStore = useDatabaseStore()
|
||||
@@ -723,8 +772,13 @@ const rightContent = ref('')
|
||||
|
||||
// 左侧来源相关
|
||||
const leftSourceType = ref('paste') // 'paste' | 'paradigm' | 'material' | 'document'
|
||||
const showParadigmSelector = ref(false)
|
||||
const showMaterialSelector = ref(false)
|
||||
const showLeftDocSelector = ref(false)
|
||||
const leftSourceDocId = ref(null) // 来源文稿ID
|
||||
const leftSourceDocTitle = ref('') // 来源文稿标题
|
||||
const leftOriginalContent = ref('') // 来源文稿原始内容(用于差异比对)
|
||||
const isLeftSaving = ref(false) // 左侧保存中状态
|
||||
|
||||
// 右侧来源相关
|
||||
const rightSourceType = ref('paste') // 'paste' | 'document'
|
||||
@@ -773,6 +827,7 @@ const rewrittenSentences = ref([]) // 重写后句子列表
|
||||
// 范式相关
|
||||
const showParadigmRules = ref(false)
|
||||
const checkMode = ref('paragraph') // 'paragraph' | 'document'
|
||||
const enabledRuleIdxs = ref(new Set()) // 已启用的规则索引集合
|
||||
|
||||
const hasParadigmRules = computed(() => {
|
||||
const p = activeParadigm.value
|
||||
@@ -783,46 +838,88 @@ const activeParadigmName = computed(() => {
|
||||
return activeParadigm.value?.name || ''
|
||||
})
|
||||
|
||||
// 提取范式规则列表(根据 scope 过滤)
|
||||
// 提取范式规则列表(根据 scope 过滤,添加索引用于勾选)
|
||||
const getFilteredRules = (scope) => {
|
||||
const paradigm = activeParadigm.value
|
||||
if (!paradigm) return []
|
||||
|
||||
const rules = []
|
||||
let idx = 0
|
||||
|
||||
if (paradigm.expertGuidelines) {
|
||||
paradigm.expertGuidelines
|
||||
.filter(g => !scope || g.scope === scope || !g.scope) // 无 scope 的规则默认都包含
|
||||
.forEach(g => {
|
||||
rules.push({ text: `【${g.title}】${g.description}`, scope: g.scope || 'both' })
|
||||
rules.push({
|
||||
idx: idx++,
|
||||
text: `【${g.title}】${g.description}`,
|
||||
scope: g.scope || 'both'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// systemConstraints 默认为全文级
|
||||
if (paradigm.systemConstraints && (scope === 'document' || !scope)) {
|
||||
paradigm.systemConstraints.forEach(c => {
|
||||
rules.push({ text: c, scope: 'document' })
|
||||
rules.push({ idx: idx++, text: c, scope: 'document' })
|
||||
})
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
// 获取所有规则(带索引)
|
||||
const allRulesWithIdx = computed(() => getFilteredRules(null))
|
||||
|
||||
// 初始化规则勾选状态(默认全部勾选)
|
||||
const initializeRuleSelection = () => {
|
||||
const rules = allRulesWithIdx.value
|
||||
enabledRuleIdxs.value = new Set(rules.map(r => r.idx))
|
||||
}
|
||||
|
||||
// 切换单个规则的勾选状态
|
||||
const toggleRule = (idx) => {
|
||||
if (enabledRuleIdxs.value.has(idx)) {
|
||||
enabledRuleIdxs.value.delete(idx)
|
||||
} else {
|
||||
enabledRuleIdxs.value.add(idx)
|
||||
}
|
||||
enabledRuleIdxs.value = new Set(enabledRuleIdxs.value) // 触发响应式更新
|
||||
}
|
||||
|
||||
// 全选/取消全选规则
|
||||
const toggleAllRules = () => {
|
||||
const allRules = allRulesWithIdx.value
|
||||
if (enabledRuleIdxs.value.size === allRules.length) {
|
||||
enabledRuleIdxs.value = new Set()
|
||||
} else {
|
||||
enabledRuleIdxs.value = new Set(allRules.map(r => r.idx))
|
||||
}
|
||||
}
|
||||
|
||||
// 监听范式变化,重新初始化勾选状态
|
||||
watch(() => activeParadigm.value, () => {
|
||||
initializeRuleSelection()
|
||||
}, { immediate: true })
|
||||
|
||||
// 当前模式下的规则列表
|
||||
const paradigmRulesList = computed(() => getFilteredRules(null).map(r => r.text))
|
||||
const paragraphRules = computed(() => getFilteredRules('paragraph'))
|
||||
const documentRules = computed(() => getFilteredRules('document'))
|
||||
|
||||
const paradigmRulesCount = computed(() => paradigmRulesList.value.length)
|
||||
const currentScopeRulesCount = computed(() =>
|
||||
checkMode.value === 'paragraph' ? paragraphRules.value.length : documentRules.value.length
|
||||
)
|
||||
const enabledRulesCount = computed(() => enabledRuleIdxs.value.size)
|
||||
const currentScopeRulesCount = computed(() => {
|
||||
const rules = checkMode.value === 'paragraph' ? paragraphRules.value : documentRules.value
|
||||
return rules.filter(r => enabledRuleIdxs.value.has(r.idx)).length
|
||||
})
|
||||
|
||||
// 获取范式规则文本(根据当前检查模式过滤)
|
||||
// 获取范式规则文本(根据当前检查模式过滤,只包含勾选的规则)
|
||||
const getParadigmRulesText = () => {
|
||||
const rules = checkMode.value === 'paragraph' ? paragraphRules.value : documentRules.value
|
||||
if (rules.length === 0) return ''
|
||||
return rules.map(r => r.text).join('\n')
|
||||
const enabledRules = rules.filter(r => enabledRuleIdxs.value.has(r.idx))
|
||||
if (enabledRules.length === 0) return ''
|
||||
return enabledRules.map(r => r.text).join('\n')
|
||||
}
|
||||
|
||||
// 解析段落
|
||||
@@ -851,6 +948,12 @@ const hasContentChanged = computed(() => {
|
||||
return rightContent.value !== rightOriginalContent.value
|
||||
})
|
||||
|
||||
// 左侧内容是否已修改(与原始内容对比)
|
||||
const hasLeftContentChanged = computed(() => {
|
||||
if (leftSourceType.value !== 'document' || !leftSourceDocId.value) return false
|
||||
return leftContent.value !== leftOriginalContent.value
|
||||
})
|
||||
|
||||
// 选择段落(多选切换)
|
||||
const selectLeftParagraph = (idx) => {
|
||||
const i = selectedLeftIdxs.value.indexOf(idx)
|
||||
@@ -897,12 +1000,21 @@ const goBack = () => {
|
||||
appStore.switchPage('writer')
|
||||
}
|
||||
|
||||
// 加载范式默认参考范文
|
||||
const loadParadigmReference = () => {
|
||||
if (activeParadigm.value?.defaultReference) {
|
||||
leftContent.value = activeParadigm.value.defaultReference.content
|
||||
leftSourceType.value = 'paradigm'
|
||||
// 加载范式范文或规则
|
||||
const handleParadigmSelect = (p) => {
|
||||
appStore.activeParadigm = p
|
||||
leftSourceType.value = 'paradigm'
|
||||
|
||||
// 填充内容:优先使用系统约束作为要求原文,如果没有则用范文内容
|
||||
if (p.systemConstraints && p.systemConstraints.length > 0) {
|
||||
leftContent.value = p.systemConstraints.join('\n\n')
|
||||
} else if (p.defaultReference?.content) {
|
||||
leftContent.value = p.defaultReference.content
|
||||
} else {
|
||||
leftContent.value = p.description || ''
|
||||
}
|
||||
|
||||
showParadigmSelector.value = false
|
||||
}
|
||||
|
||||
// 处理素材选择
|
||||
@@ -918,6 +1030,9 @@ const handleMaterialSelect = (material) => {
|
||||
const handleLeftDocSelect = (doc) => {
|
||||
leftContent.value = doc.content || ''
|
||||
leftSourceType.value = 'document'
|
||||
leftSourceDocId.value = doc.id // 保存文稿ID
|
||||
leftSourceDocTitle.value = doc.title // 保存文稿标题
|
||||
leftOriginalContent.value = doc.content || '' // 保存原始内容用于差异比对
|
||||
showLeftDocSelector.value = false
|
||||
}
|
||||
|
||||
@@ -974,6 +1089,49 @@ const saveRightContent = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 保存左侧内容到文稿
|
||||
const saveLeftContent = async () => {
|
||||
if (leftSourceType.value !== 'document' || !leftSourceDocId.value) {
|
||||
alert('当前内容不是来自文稿库,无需保存')
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasLeftContentChanged.value) {
|
||||
alert('内容未修改,无需保存')
|
||||
return
|
||||
}
|
||||
|
||||
isLeftSaving.value = true
|
||||
|
||||
try {
|
||||
// 计算差异用于生成版本说明
|
||||
const changes = computeDiff(leftOriginalContent.value, leftContent.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(leftSourceDocId.value, leftContent.value, changeNote)
|
||||
|
||||
// 同时更新文稿主内容
|
||||
updateDocument(leftSourceDocId.value, { content: leftContent.value })
|
||||
|
||||
// 更新原始内容为当前内容(标记为已保存)
|
||||
leftOriginalContent.value = leftContent.value
|
||||
|
||||
console.log(`左侧文稿保存成功,版本号: ${versionNumber},${changeNote}`)
|
||||
alert(`保存成功!新版本: v${versionNumber}\n${changeNote}`)
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
alert('保存失败,请重试')
|
||||
} finally {
|
||||
isLeftSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换段落类型下拉菜单
|
||||
const toggleTypeDropdown = (idx) => {
|
||||
showTypeDropdown.value = showTypeDropdown.value === idx ? null : idx
|
||||
|
||||
Reference in New Issue
Block a user