feat: 新增对照检查页面

- 创建 ComparePanel.vue:左右双栏布局
- 左侧:写作要求(可选中段落)
- 右侧:写作内容(可选中段落)
- 中间:对应关系可视化指示器
- 底部:对照检查按钮 + AI 分析结果展示
- 集成到导航系统,支持从写作页面跳转
This commit is contained in:
empty
2026-01-08 15:53:43 +08:00
parent 0662e93dbb
commit 97e4240308
4 changed files with 337 additions and 6 deletions

View File

@@ -1,11 +1,17 @@
<template>
<div class="flex h-full">
<!-- 左侧面板 -->
<WriterPanel v-if="currentPage === 'writer'" />
<AnalysisPanel v-else />
<!-- 对照检查页面全屏独占 -->
<ComparePanel v-if="currentPage === 'compare'" />
<!-- 右侧主内容区 -->
<MainContent />
<!-- 常规布局 -->
<template v-else>
<!-- 左侧面板 -->
<WriterPanel v-if="currentPage === 'writer'" />
<AnalysisPanel v-else-if="currentPage === 'analysis'" />
<!-- 右侧主内容区 -->
<MainContent />
</template>
</div>
</template>
@@ -15,6 +21,7 @@ import { useAppStore } from './stores/app'
import WriterPanel from './components/WriterPanel.vue'
import AnalysisPanel from './components/AnalysisPanel.vue'
import MainContent from './components/MainContent.vue'
import ComparePanel from './components/ComparePanel.vue'
const appStore = useAppStore()
const currentPage = computed(() => appStore.currentPage)

View File

@@ -0,0 +1,311 @@
<template>
<div class="h-screen 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">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl">🔍</span> 对照检查
</h1>
<span class="text-xs text-slate-500">选中左右两侧对应段落进行检查</span>
</div>
<button
@click="goBack"
class="text-xs px-3 py-1.5 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
>
返回写作
</button>
</header>
<!-- 主体内容区 -->
<div class="flex-1 flex min-h-0">
<!-- 左侧写作要求 -->
<div class="flex-1 flex flex-col border-r border-slate-700">
<div class="p-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between">
<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>
<div class="flex-1 overflow-y-auto p-4 space-y-3 min-h-0">
<div v-if="!leftContent" class="h-full flex items-center justify-center">
<div class="text-center">
<span class="text-4xl opacity-20 block mb-2">📝</span>
<p class="text-slate-600 text-sm">请在下方输入写作要求</p>
</div>
</div>
<div
v-for="(para, idx) in leftParagraphs"
:key="'left-' + idx"
@click="selectLeftParagraph(idx)"
:class="[
'p-3 rounded-lg border cursor-pointer transition-all',
selectedLeftIdx === idx
? 'bg-amber-900/30 border-amber-500 ring-2 ring-amber-500/30'
: 'bg-slate-800/50 border-slate-700 hover:border-amber-500/50'
]"
>
<div class="flex items-start gap-2">
<span class="text-xs text-amber-500/70 shrink-0 mt-0.5">{{ idx + 1 }}.</span>
<p class="text-sm text-slate-300 whitespace-pre-wrap">{{ para }}</p>
</div>
</div>
</div>
<div class="p-3 border-t border-slate-700 bg-slate-800">
<textarea
v-model="leftContent"
class="w-full h-24 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
placeholder="粘贴或输入写作要求(每段用空行分隔)..."
></textarea>
</div>
</div>
<!-- 中间对应关系指示器 -->
<div class="w-16 flex flex-col items-center justify-center bg-slate-800/50 shrink-0">
<div v-if="selectedLeftIdx !== null && selectedRightIdx !== null" class="flex flex-col items-center gap-2">
<div class="w-3 h-3 rounded-full bg-amber-500"></div>
<div class="w-0.5 h-8 bg-gradient-to-b from-amber-500 to-blue-500"></div>
<span class="text-xs text-slate-500 writing-mode-vertical">对应</span>
<div class="w-0.5 h-8 bg-gradient-to-b from-blue-500 to-blue-500"></div>
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
</div>
<div v-else class="text-center px-2">
<span class="text-xs text-slate-600">请选中<br/>两侧段落</span>
</div>
</div>
<!-- 右侧写作内容 -->
<div class="flex-1 flex flex-col">
<div class="p-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between">
<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>
<div class="flex-1 overflow-y-auto p-4 space-y-3 min-h-0">
<div v-if="!rightContent" class="h-full flex items-center justify-center">
<div class="text-center">
<span class="text-4xl opacity-20 block mb-2">📄</span>
<p class="text-slate-600 text-sm">请在下方输入写作内容</p>
</div>
</div>
<div
v-for="(para, idx) in rightParagraphs"
:key="'right-' + idx"
@click="selectRightParagraph(idx)"
:class="[
'p-3 rounded-lg border cursor-pointer transition-all',
selectedRightIdx === idx
? 'bg-blue-900/30 border-blue-500 ring-2 ring-blue-500/30'
: 'bg-slate-800/50 border-slate-700 hover:border-blue-500/50'
]"
>
<div class="flex items-start gap-2">
<span class="text-xs text-blue-500/70 shrink-0 mt-0.5">{{ idx + 1 }}.</span>
<p class="text-sm text-slate-300 whitespace-pre-wrap">{{ para }}</p>
</div>
<!-- 检查结果标记 -->
<div v-if="checkResults[idx]" class="mt-2 pt-2 border-t border-slate-700">
<div :class="[
'text-xs px-2 py-1 rounded inline-flex items-center gap-1',
checkResults[idx].status === 'pass' ? 'bg-green-900/30 text-green-400' :
checkResults[idx].status === 'warning' ? 'bg-yellow-900/30 text-yellow-400' :
'bg-red-900/30 text-red-400'
]">
<span>{{ checkResults[idx].status === 'pass' ? '✅' : checkResults[idx].status === 'warning' ? '⚠️' : '❌' }}</span>
{{ checkResults[idx].message }}
</div>
</div>
</div>
</div>
<div class="p-3 border-t border-slate-700 bg-slate-800">
<textarea
v-model="rightContent"
class="w-full h-24 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none"
placeholder="粘贴或输入写作内容(每段用空行分隔)..."
></textarea>
</div>
</div>
</div>
<!-- 底部操作区 -->
<footer class="p-4 bg-slate-800 border-t border-slate-700 shrink-0">
<div class="flex items-center justify-between gap-4">
<!-- 检查结果摘要 -->
<div v-if="lastCheckResult" class="flex-1">
<div :class="[
'p-3 rounded-lg border',
lastCheckResult.overall === 'pass' ? 'bg-green-900/20 border-green-500/30' :
lastCheckResult.overall === 'warning' ? 'bg-yellow-900/20 border-yellow-500/30' :
'bg-red-900/20 border-red-500/30'
]">
<div class="flex items-center gap-2 mb-1">
<span class="text-lg">
{{ lastCheckResult.overall === 'pass' ? '✅' : lastCheckResult.overall === 'warning' ? '⚠️' : '❌' }}
</span>
<span :class="[
'font-medium',
lastCheckResult.overall === 'pass' ? 'text-green-400' :
lastCheckResult.overall === 'warning' ? 'text-yellow-400' : 'text-red-400'
]">
{{ lastCheckResult.overall === 'pass' ? '符合要求' : lastCheckResult.overall === 'warning' ? '部分符合' : '不符合要求' }}
</span>
</div>
<p class="text-xs text-slate-400">{{ lastCheckResult.summary }}</p>
</div>
</div>
<div v-else class="flex-1 text-sm text-slate-500">
选中左侧要求和右侧内容点击"对照检查"按钮进行分析
</div>
<!-- 操作按钮 -->
<div class="flex gap-2 shrink-0">
<button
@click="clearSelection"
class="px-4 py-2 rounded-lg text-sm bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
>
清除选择
</button>
<button
@click="runCompare"
:disabled="!canCompare || isComparing"
class="px-6 py-2 rounded-lg text-sm font-medium text-white transition disabled:opacity-50 disabled:cursor-not-allowed"
:class="isComparing ? 'bg-slate-600' : 'bg-gradient-to-r from-amber-600 to-blue-600 hover:from-amber-500 hover:to-blue-500'"
>
<span v-if="isComparing" class="flex items-center gap-2">
<span class="animate-spin"></span> 检查中...
</span>
<span v-else>🔍 对照检查</span>
</button>
</div>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useAppStore } from '../stores/app'
const appStore = useAppStore()
// 内容
const leftContent = ref('')
const rightContent = ref('')
// 选中状态
const selectedLeftIdx = ref(null)
const selectedRightIdx = ref(null)
// 检查状态
const isComparing = ref(false)
const lastCheckResult = ref(null)
const checkResults = ref({})
// 解析段落
const leftParagraphs = computed(() => {
return leftContent.value
.split(/\n\s*\n/)
.map(p => p.trim())
.filter(p => p.length > 0)
})
const rightParagraphs = computed(() => {
return rightContent.value
.split(/\n\s*\n/)
.map(p => p.trim())
.filter(p => p.length > 0)
})
// 是否可以检查
const canCompare = computed(() => {
return selectedLeftIdx.value !== null && selectedRightIdx.value !== null
})
// 选择段落
const selectLeftParagraph = (idx) => {
selectedLeftIdx.value = selectedLeftIdx.value === idx ? null : idx
}
const selectRightParagraph = (idx) => {
selectedRightIdx.value = selectedRightIdx.value === idx ? null : idx
}
// 清除选择
const clearSelection = () => {
selectedLeftIdx.value = null
selectedRightIdx.value = null
lastCheckResult.value = null
}
// 返回写作页面
const goBack = () => {
appStore.switchPage('writer')
}
// 执行对照检查
const runCompare = async () => {
if (!canCompare.value) return
const requirement = leftParagraphs.value[selectedLeftIdx.value]
const content = rightParagraphs.value[selectedRightIdx.value]
isComparing.value = true
lastCheckResult.value = null
try {
const prompt = `你是一个严格的写作质检专家。请对比以下"写作要求"和"写作内容",判断内容是否符合要求。
# 写作要求
${requirement}
# 写作内容
${content}
# 输出要求
请严格按照以下 JSON 格式输出检查结果(不要输出其他内容):
{
"overall": "pass|warning|fail",
"summary": "一句话总结检查结果",
"details": [
{"aspect": "检查维度", "status": "pass|warning|fail", "message": "具体说明"}
],
"suggestions": ["改进建议1", "改进建议2"]
}`
let result = ''
await appStore.callApi(prompt, (chunk) => {
result += chunk
}, { temperature: 0.3 })
// 解析结果
const jsonMatch = result.match(/\{[\s\S]*\}/)
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0])
lastCheckResult.value = parsed
// 保存到对应段落的检查结果
checkResults.value[selectedRightIdx.value] = {
status: parsed.overall,
message: parsed.summary
}
}
} catch (error) {
console.error('对照检查失败:', error)
lastCheckResult.value = {
overall: 'warning',
summary: '检查过程出现异常,请重试',
details: [],
suggestions: []
}
} finally {
isComparing.value = false
}
}
</script>
<style scoped>
.writing-mode-vertical {
writing-mode: vertical-rl;
text-orientation: mixed;
}
</style>

View File

@@ -12,6 +12,12 @@
>
写作范式
</button>
<button
@click="switchPage('compare')"
class="text-xs px-2 py-1 rounded bg-amber-900/50 text-amber-300 border border-amber-700/50 hover:bg-amber-800/50 transition"
>
🔍 对照检查
</button>
</div>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
</header>

View File

@@ -393,6 +393,12 @@ ${draft}
qualityReport.value = null
}
// 通用 API 调用方法
const callApi = async (prompt, onContent, options = {}) => {
const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value })
return api.generateContent(prompt, onContent, options)
}
return {
// 状态
currentPage,
@@ -428,6 +434,7 @@ ${draft}
generateContentAction,
analyzeArticleAction,
loadParadigmPreset,
clearParadigm
clearParadigm,
callApi
}
})