feat: 新增对照检查页面
- 创建 ComparePanel.vue:左右双栏布局 - 左侧:写作要求(可选中段落) - 右侧:写作内容(可选中段落) - 中间:对应关系可视化指示器 - 底部:对照检查按钮 + AI 分析结果展示 - 集成到导航系统,支持从写作页面跳转
This commit is contained in:
17
src/App.vue
17
src/App.vue
@@ -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)
|
||||
|
||||
311
src/components/ComparePanel.vue
Normal file
311
src/components/ComparePanel.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user