Files
ai-write/src/components/AnalysisPanel.vue
empty f9f0785106 feat: 完善范式润色功能和相关组件
- 新增范式选择模态框 (ParadigmSelectorModal)
- 新增差异标注面板 (DiffAnnotationPanel)
- 新增精确差异处理工具 (preciseDiff.js)
- 更新各面板组件支持范式润色功能
- 更新范式配置文件 (paradigms.js)
- 更新依赖配置

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 03:02:21 +08:00

595 lines
21 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<aside class="w-[400px] flex flex-col border-r border-slate-700 bg-slate-800 shrink-0 h-screen">
<!-- 头部 -->
<header class="p-4 border-b border-slate-700 flex items-center justify-between shrink-0">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl">🎯</span> 写作范式分析
</h1>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
</header>
<!-- 内容区 - 添加 min-h-0 确保滚动正常 -->
<div class="flex-1 overflow-y-auto p-4 space-y-6 min-h-0">
<!-- 写作范式库 -->
<section>
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-medium text-slate-400">📚 写作范式库</h3>
<button
@click="openAddModal"
class="text-xs px-2 py-1 bg-green-600 text-white rounded hover:bg-green-500 transition flex items-center gap-1"
>
<span>+</span> 新增范式
</button>
</div>
<div class="space-y-3">
<div
v-for="paradigm in paradigms"
:key="paradigm.id"
@click="selectParadigm(paradigm)"
:class="['bg-slate-700/50 rounded-lg p-4 border transition cursor-pointer',
selectedParadigm?.id === paradigm.id
? 'border-blue-500 bg-blue-900/20'
: 'border-slate-600 hover:border-blue-500']"
>
<div class="flex justify-between items-start mb-2">
<h4 class="font-medium text-white flex items-center gap-2">
{{ paradigm.icon }} {{ paradigm.name }}
<span v-if="paradigm.isNew" class="text-[10px] px-1.5 py-0.5 rounded bg-orange-500 text-white font-bold animate-pulse">
NEW
</span>
<span v-if="paradigm.isCustom" class="text-[10px] px-1.5 py-0.5 rounded bg-purple-500 text-white font-bold">
自定义
</span>
</h4>
<div class="flex items-center gap-1">
<button
@click.stop="openEditModal(paradigm)"
class="text-xs px-2 py-1 bg-slate-600 text-white rounded hover:bg-slate-500 transition"
title="编辑范式"
>
</button>
<button
v-if="paradigm.isCustom"
@click.stop="deleteParadigm(paradigm)"
class="text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-500 transition"
title="删除范式"
>
🗑
</button>
<button
v-if="selectedParadigm?.id === paradigm.id"
@click.stop="applyParadigm(paradigm)"
class="text-xs px-2 py-1 bg-blue-600 text-white rounded hover:bg-blue-500 transition"
>
应用到写作
</button>
</div>
</div>
<p class="text-xs text-slate-400 mb-2">{{ paradigm.description }}</p>
<div class="flex flex-wrap gap-1">
<span
v-for="tag in paradigm.tags"
:key="tag"
:class="['text-xs px-2 py-1 rounded', paradigm.tagClass]"
>
{{ tag }}
</span>
</div>
</div>
</div>
</section>
<!-- 范式分析工具 -->
<section>
<h3 class="text-sm font-medium text-slate-400 mb-4">🔍 范式分析工具</h3>
<textarea
v-model="analysisText"
class="w-full h-32 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 transition placeholder-slate-600 resize-none"
placeholder="粘贴文章内容,分析其写作范式..."
></textarea>
<div class="mt-2 flex gap-2">
<button
@click="analyzeArticle"
:disabled="isAnalyzing || !analysisText"
class="flex-1 bg-blue-600 hover:bg-blue-500 text-white py-2 rounded-lg text-sm font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ isAnalyzing ? '正在分析...' : '分析文章范式' }}
</button>
<button
@click="clearAnalysis"
class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-sm font-medium transition"
>
清空
</button>
</div>
</section>
<!-- 分析历史 -->
<section v-if="analysisHistory.length > 0">
<h3 class="text-sm font-medium text-slate-400 mb-4">📝 分析历史</h3>
<div class="space-y-2">
<div
v-for="(item, index) in analysisHistory"
:key="index"
@click="loadHistoryItem(item)"
class="bg-slate-700/30 rounded p-3 cursor-pointer hover:bg-slate-700/50 transition text-xs"
>
<div class="flex justify-between items-center">
<span class="text-slate-300">{{ item.paradigm }}</span>
<span class="text-slate-500">{{ formatDate(item.timestamp) }}</span>
</div>
<p class="text-slate-500 mt-1 truncate">{{ item.preview }}</p>
</div>
</div>
</section>
</div>
<!-- 编辑/新增范式弹窗 -->
<div
v-if="showEditModal"
class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
@click.self="closeEditModal"
>
<div class="bg-slate-800 rounded-lg w-[500px] max-h-[80vh] overflow-y-auto border border-slate-600 shadow-xl">
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<h3 class="font-medium text-white">{{ isAddMode ? '新增写作范式' : '编辑写作范式' }}</h3>
<button @click="closeEditModal" class="text-slate-400 hover:text-white transition"></button>
</div>
<div class="p-4 space-y-4">
<!-- 图标选择 -->
<div>
<label class="block text-sm text-slate-400 mb-2">图标</label>
<div class="flex gap-2 flex-wrap">
<button
v-for="icon in iconOptions"
:key="icon"
@click="editForm.icon = icon"
:class="['w-10 h-10 rounded-lg text-xl flex items-center justify-center transition',
editForm.icon === icon ? 'bg-blue-600' : 'bg-slate-700 hover:bg-slate-600']"
>
{{ icon }}
</button>
</div>
</div>
<!-- 名称 -->
<div>
<label class="block text-sm text-slate-400 mb-2">范式名称</label>
<input
v-model="editForm.name"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
placeholder="如:技术博客范式"
/>
</div>
<!-- 描述 -->
<div>
<label class="block text-sm text-slate-400 mb-2">描述</label>
<textarea
v-model="editForm.description"
rows="2"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition resize-none"
placeholder="简要描述此范式的适用场景"
></textarea>
</div>
<!-- 标签 -->
<div>
<label class="block text-sm text-slate-400 mb-2">标签逗号分隔</label>
<input
v-model="editForm.tagsInput"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition"
placeholder="如:问题引入,解决方案,代码示例"
/>
<div class="flex flex-wrap gap-1 mt-2" v-if="editForm.tagsInput">
<span
v-for="tag in editForm.tagsInput.split(',').map(t => t.trim()).filter(t => t)"
:key="tag"
:class="['text-xs px-2 py-1 rounded', editForm.tagClass]"
>
{{ tag }}
</span>
</div>
</div>
<!-- 标签颜色 -->
<div>
<label class="block text-sm text-slate-400 mb-2">标签颜色</label>
<div class="flex gap-2 flex-wrap">
<button
v-for="color in colorOptions"
:key="color.class"
@click="editForm.tagClass = color.class"
:class="['px-3 py-1 rounded text-xs transition', color.class,
editForm.tagClass === color.class ? 'ring-2 ring-white' : '']"
>
{{ color.label }}
</button>
</div>
</div>
<!-- 核心字段完整 Prompt -->
<div class="border-t border-slate-700 pt-4 mt-4">
<label class="block text-sm text-slate-400 mb-2 flex items-center gap-2">
<span></span> 完整 Prompt核心字段
<span class="text-xs text-slate-500 font-normal">粘贴您的完整提示词</span>
</label>
<textarea
v-model="editForm.specializedPrompt"
rows="12"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition resize-y font-mono leading-relaxed"
placeholder="在此粘贴您的完整 Prompt...
示例:
# Role
你是一个具有较高政治敏感性的AI助手...
# 任务目标
你需要逐一分析文章中的每一条问题...
# 检查步骤
1. 检查问题表述的清晰度
2. 深度分析思想根源
..."
></textarea>
<p class="text-xs text-slate-500 mt-2">
💡 提示这是 AI 调用时的核心指令完整的 Prompt 应包含角色目标步骤和输出格式
</p>
</div>
</div>
<div class="p-4 border-t border-slate-700 flex justify-end gap-2">
<button
@click="closeEditModal"
class="px-4 py-2 bg-slate-700 text-white rounded-lg text-sm hover:bg-slate-600 transition"
>
取消
</button>
<button
@click="saveParadigm"
:disabled="!editForm.name"
class="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-500 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{{ isAddMode ? '添加' : '保存' }}
</button>
</div>
</div>
</div>
</aside>
</template>
<script setup>
import { ref, reactive, onMounted, computed, watch } from 'vue'
import { storeToRefs } from 'pinia'
import { useAppStore } from '../stores/app'
import { useDatabaseStore } from '../stores/database.js'
import DeepSeekAPI from '../api/deepseek.js'
import { getParadigmList } from '../config/paradigms.js'
const appStore = useAppStore()
const { analysisText, isAnalyzing } = storeToRefs(appStore)
// 数据库 Store
const dbStore = useDatabaseStore()
const { isInitialized: dbInitialized } = storeToRefs(dbStore)
// 选中的范式
const selectedParadigm = ref(null)
// 分析历史
const analysisHistory = ref([])
// 范式列表(响应式,支持编辑)
const paradigms = ref([])
// 编辑弹窗状态
const showEditModal = ref(false)
const isAddMode = ref(false)
const editingParadigmId = ref(null)
// 编辑表单
const editForm = reactive({
icon: '📝',
name: '',
description: '',
tagsInput: '',
tagClass: 'bg-blue-900/30 text-blue-300',
// ⭐ 核心字段:完整 Prompt
specializedPrompt: ''
})
// 图标选项
const iconOptions = ['📝', '💻', '📊', '🚀', '📚', '🏛️', '🔥', '🏢', '💡', '🎯', '📋', '✨']
// 颜色选项
const colorOptions = [
{ label: '蓝色', class: 'bg-blue-900/30 text-blue-300' },
{ label: '绿色', class: 'bg-green-900/30 text-green-300' },
{ label: '红色', class: 'bg-red-900/30 text-red-300' },
{ label: '紫色', class: 'bg-purple-900/30 text-purple-300' },
{ label: '橙色', class: 'bg-orange-900/30 text-orange-300' },
{ label: '青色', class: 'bg-cyan-900/30 text-cyan-300' }
]
// 初始化范式列表
const initParadigms = () => {
// 先加载默认范式
const defaultParadigms = getParadigmList()
// 从本地存储加载自定义修改
const savedCustomizations = localStorage.getItem('paradigmCustomizations')
const customizations = savedCustomizations ? JSON.parse(savedCustomizations) : {}
// 从本地存储加载自定义范式
const savedCustomParadigms = localStorage.getItem('customParadigms')
const customParadigms = savedCustomParadigms ? JSON.parse(savedCustomParadigms) : []
// 合并默认范式和自定义修改
const mergedParadigms = defaultParadigms.map(p => {
if (customizations[p.id]) {
return { ...p, ...customizations[p.id] }
}
return p
})
// 添加自定义范式
paradigms.value = [...mergedParadigms, ...customParadigms]
}
// 打开新增弹窗
const openAddModal = () => {
isAddMode.value = true
editingParadigmId.value = null
resetEditForm()
showEditModal.value = true
}
// 打开编辑弹窗
const openEditModal = (paradigm) => {
isAddMode.value = false
editingParadigmId.value = paradigm.id
editForm.icon = paradigm.icon || '📝'
editForm.name = paradigm.name || ''
editForm.description = paradigm.description || ''
editForm.tagsInput = (paradigm.tags || []).join(', ')
editForm.tagClass = paradigm.tagClass || 'bg-blue-900/30 text-blue-300'
// ⭐ 核心字段:加载完整 Prompt
editForm.specializedPrompt = paradigm.specializedPrompt || ''
showEditModal.value = true
}
// 关闭弹窗
const closeEditModal = () => {
showEditModal.value = false
resetEditForm()
}
// 重置表单
const resetEditForm = () => {
editForm.icon = '📝'
editForm.name = ''
editForm.description = ''
editForm.tagsInput = ''
editForm.tagClass = 'bg-blue-900/30 text-blue-300'
// ⭐ 核心字段重置
editForm.specializedPrompt = ''
}
// 保存范式
const saveParadigm = () => {
const tags = editForm.tagsInput.split(',').map(t => t.trim()).filter(t => t)
if (isAddMode.value) {
// 新增自定义范式
const newParadigm = {
id: `custom-${Date.now()}`,
icon: editForm.icon,
name: editForm.name,
description: editForm.description || editForm.specializedPrompt.substring(0, 100) + '...',
tags,
tagClass: editForm.tagClass,
isCustom: true,
// ⭐ 核心字段:完整 Prompt
specializedPrompt: editForm.specializedPrompt
}
paradigms.value.push(newParadigm)
// 保存到本地存储
const savedCustomParadigms = localStorage.getItem('customParadigms')
const customParadigms = savedCustomParadigms ? JSON.parse(savedCustomParadigms) : []
customParadigms.push(newParadigm)
localStorage.setItem('customParadigms', JSON.stringify(customParadigms))
} else {
// 编辑现有范式
const index = paradigms.value.findIndex(p => p.id === editingParadigmId.value)
if (index !== -1) {
const updatedParadigm = {
...paradigms.value[index],
icon: editForm.icon,
name: editForm.name,
description: editForm.description,
tags,
tagClass: editForm.tagClass,
// ⭐ 核心字段:完整 Prompt
specializedPrompt: editForm.specializedPrompt
}
paradigms.value[index] = updatedParadigm
// 根据是否是自定义范式决定存储位置
if (updatedParadigm.isCustom) {
// 更新自定义范式
const savedCustomParadigms = localStorage.getItem('customParadigms')
const customParadigms = savedCustomParadigms ? JSON.parse(savedCustomParadigms) : []
const customIndex = customParadigms.findIndex(p => p.id === editingParadigmId.value)
if (customIndex !== -1) {
customParadigms[customIndex] = updatedParadigm
localStorage.setItem('customParadigms', JSON.stringify(customParadigms))
}
} else {
// 保存对默认范式的自定义修改
const savedCustomizations = localStorage.getItem('paradigmCustomizations')
const customizations = savedCustomizations ? JSON.parse(savedCustomizations) : {}
customizations[editingParadigmId.value] = {
icon: editForm.icon,
name: editForm.name,
description: editForm.description,
tags,
tagClass: editForm.tagClass,
specializedPrompt: editForm.specializedPrompt
}
localStorage.setItem('paradigmCustomizations', JSON.stringify(customizations))
}
}
}
closeEditModal()
}
// 删除自定义范式
const deleteParadigm = (paradigm) => {
if (!paradigm.isCustom) return
if (!confirm(`确定要删除"${paradigm.name}"吗?`)) return
// 从列表中移除
paradigms.value = paradigms.value.filter(p => p.id !== paradigm.id)
// 从本地存储中移除
const savedCustomParadigms = localStorage.getItem('customParadigms')
const customParadigms = savedCustomParadigms ? JSON.parse(savedCustomParadigms) : []
const filtered = customParadigms.filter(p => p.id !== paradigm.id)
localStorage.setItem('customParadigms', JSON.stringify(filtered))
// 如果正在选中该范式,清除选中状态
if (selectedParadigm.value?.id === paradigm.id) {
selectedParadigm.value = null
}
}
// 选择范式
const selectParadigm = (paradigm) => {
selectedParadigm.value = paradigm
}
// 应用范式到写作(使用新的 loadParadigmPreset 方法)
const applyParadigm = (paradigm) => {
appStore.loadParadigmPreset(paradigm.id)
// 显示提示
alert(`已应用"${paradigm.name}"到写作任务`)
}
// 分析文章
const analyzeArticle = async () => {
try {
const result = await appStore.analyzeArticleAction(analysisText.value, detectParadigm)
// 添加到历史记录
addToHistory(result.paradigm, analysisText.value, result.content)
} catch (error) {
alert(error.message)
}
}
// 检测文章范式
const detectParadigm = (analysis) => {
const text = analysis.toLowerCase()
const list = paradigms.value
// 根据 id 查找范式
const findById = (id) => list.find(p => p.id === id)
if (text.includes('民主生活会') || text.includes('对照检查') || text.includes('整改') || text.includes('党性')) {
return findById('party-review') || list[0]
} else if (text.includes('政府') || text.includes('工作报告') || text.includes('述职')) {
return findById('gov-report') || list[0]
} else if (text.includes('技术') || text.includes('代码') || text.includes('编程')) {
return findById('tech') || list[0]
} else if (text.includes('商业') || text.includes('市场') || text.includes('数据分析')) {
return findById('business') || list[0]
} else if (text.includes('产品') || text.includes('营销') || text.includes('用户')) {
return findById('marketing') || list[0]
} else if (text.includes('学术') || text.includes('研究') || text.includes('文献')) {
return findById('academic') || list[0]
}
return list[0] // 默认第一个范式
}
// 添加到历史记录
const addToHistory = (paradigm, text, analysis) => {
const historyItem = {
paradigm,
text,
analysis,
preview: text.substring(0, 50) + '...',
timestamp: new Date()
}
// 限制历史记录数量
analysisHistory.value.unshift(historyItem)
if (analysisHistory.value.length > 10) {
analysisHistory.value = analysisHistory.value.slice(0, 10)
}
// 保存到本地存储
localStorage.setItem('analysisHistory', JSON.stringify(analysisHistory.value))
}
// 加载历史记录项
const loadHistoryItem = (item) => {
analysisText.value = item.text
appStore.analysisResult = {
paradigm: item.paradigm,
analysis: item.analysis,
timestamp: item.timestamp
}
}
// 清空分析
const clearAnalysis = () => {
analysisText.value = ''
appStore.analysisResult = null
selectedParadigm.value = null
}
// 格式化日期
const formatDate = (date) => {
const now = new Date()
const diff = now - new Date(date)
const minutes = Math.floor(diff / 60000)
const hours = Math.floor(diff / 3600000)
const days = Math.floor(diff / 86400000)
if (minutes < 1) return '刚刚'
if (minutes < 60) return `${minutes}分钟前`
if (hours < 24) return `${hours}小时前`
if (days < 7) return `${days}天前`
return new Date(date).toLocaleDateString()
}
// 组件挂载时加载历史记录和范式列表
onMounted(() => {
// 初始化范式列表
initParadigms()
// 加载分析历史
const saved = localStorage.getItem('analysisHistory')
if (saved) {
analysisHistory.value = JSON.parse(saved)
}
})
</script>