feat: 添加文稿管理、素材库、设置页面及对照检查重写功能
- 新增 DocumentsPanel.vue 文稿管理页面 - 新增 MaterialsPanel.vue 素材库管理页面 - 新增 SettingsPanel.vue 设置页面 - 新增 DocumentSelectorModal.vue 文稿选择弹窗 - 新增 MaterialSelectorModal.vue 素材选择弹窗 - 集成 SQLite 数据库持久化 (sql.js) - 对照检查页面支持从文稿库选取内容 - 对照检查页面新增一键重写及差异对比功能 - 修复对照检查页面布局问题 - MainContent 支持文稿编辑功能
This commit is contained in:
@@ -20,7 +20,15 @@
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6 min-h-0">
|
||||
<!-- 写作范式库 -->
|
||||
<section>
|
||||
<h3 class="text-sm font-medium text-slate-400 mb-4">📚 写作范式库</h3>
|
||||
<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
|
||||
@@ -38,14 +46,34 @@
|
||||
<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>
|
||||
<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 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">
|
||||
@@ -105,27 +133,628 @@
|
||||
</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>
|
||||
|
||||
<!-- 写作约束(高级) -->
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-2">写作约束(每行一条)</label>
|
||||
<textarea
|
||||
v-model="editForm.constraintsInput"
|
||||
rows="4"
|
||||
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 font-mono"
|
||||
placeholder="如: 开篇必须明确阐述问题 结尾需总结核心要点"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 维度配置(新架构) -->
|
||||
<div class="border-t border-slate-700 pt-4 mt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm text-slate-400 font-medium">📐 维度配置(高级)</label>
|
||||
<button
|
||||
@click="toggleDimensionEditor"
|
||||
class="text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
{{ showDimensionEditor ? '收起' : '展开' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showDimensionEditor" class="space-y-3">
|
||||
<!-- 维度集选择 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 mb-1">选择维度集模板</label>
|
||||
<select
|
||||
v-model="editForm.dimensionSetId"
|
||||
@change="onDimensionSetChange"
|
||||
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 outline-none"
|
||||
>
|
||||
<option :value="null">不使用维度集</option>
|
||||
<option v-for="ds in dimensionSetOptions" :key="ds.id" :value="ds.id">
|
||||
{{ ds.name }} - {{ ds.description }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 维度列表编辑 -->
|
||||
<div v-if="editForm.dimensions.length > 0">
|
||||
<label class="block text-xs text-slate-500 mb-2">维度列表(可编辑)</label>
|
||||
<div class="space-y-2 max-h-60 overflow-y-auto">
|
||||
<div
|
||||
v-for="(dim, index) in editForm.dimensions"
|
||||
:key="dim.id || index"
|
||||
class="bg-slate-900/50 rounded p-2 border border-slate-700"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<input
|
||||
v-model="dim.name"
|
||||
class="flex-1 bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs"
|
||||
placeholder="维度名称"
|
||||
/>
|
||||
<button
|
||||
@click="removeDimension(index)"
|
||||
class="text-red-400 hover:text-red-300 text-xs px-1"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="dim.focus"
|
||||
class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-400"
|
||||
placeholder="关注重点(如:理想信念、宗旨意识)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="addDimension"
|
||||
class="mt-2 text-xs text-green-400 hover:text-green-300 flex items-center gap-1"
|
||||
>
|
||||
<span>+</span> 添加维度
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 快速添加维度 -->
|
||||
<div v-else class="text-center py-4 text-slate-500 text-xs">
|
||||
<p>选择一个维度集模板,或手动添加维度</p>
|
||||
<button
|
||||
@click="addDimension"
|
||||
class="mt-2 text-green-400 hover:text-green-300"
|
||||
>
|
||||
+ 添加自定义维度
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 素材库配置(新增) -->
|
||||
<div class="border-t border-slate-700 pt-4 mt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm text-slate-400 font-medium">📚 素材库配置</label>
|
||||
<button
|
||||
@click="toggleReferenceEditor"
|
||||
class="text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
{{ showReferenceEditor ? '收起' : '展开' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="showReferenceEditor" class="space-y-3">
|
||||
<!-- 自动匹配开关 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-slate-500">自动匹配相关素材</span>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" v-model="editForm.autoMatchRefs" class="sr-only peer">
|
||||
<div class="w-9 h-5 bg-slate-600 peer-focus:ring-2 peer-focus:ring-blue-500 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 素材类型筛选 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-500 mb-2">手动选择素材(按类型)</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
<button
|
||||
v-for="type in referenceTypes"
|
||||
:key="type.id"
|
||||
@click="filterReferencesByType(type.id)"
|
||||
:class="['text-xs px-2 py-1 rounded transition',
|
||||
selectedRefType === type.id
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600']"
|
||||
>
|
||||
{{ type.icon }} {{ type.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 素材列表 -->
|
||||
<div class="max-h-40 overflow-y-auto space-y-1">
|
||||
<div
|
||||
v-for="ref in filteredReferences"
|
||||
:key="ref.id"
|
||||
@click="toggleReference(ref.id)"
|
||||
:class="['p-2 rounded cursor-pointer border transition text-xs',
|
||||
editForm.selectedRefs.includes(ref.id)
|
||||
? 'bg-blue-900/30 border-blue-500'
|
||||
: 'bg-slate-900/50 border-slate-700 hover:border-slate-500']"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-white">{{ ref.title }}</span>
|
||||
<span v-if="editForm.selectedRefs.includes(ref.id)" class="text-green-400">✓</span>
|
||||
</div>
|
||||
<div class="text-slate-500 text-[10px] mt-1">{{ ref.excerptCount }} 条可引用内容</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已选素材摘要 -->
|
||||
<div v-if="editForm.selectedRefs.length > 0" class="text-xs text-slate-400">
|
||||
已选择 {{ editForm.selectedRefs.length }} 个素材
|
||||
</div>
|
||||
</div>
|
||||
</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, onMounted } from 'vue'
|
||||
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'
|
||||
import { getParadigmList, getParadigmDimensions } from '../config/paradigms.js'
|
||||
import { getDimensionSetList, getDimensionSetById } from '../config/dimensionSets.js'
|
||||
import { getLogicParadigmList } from '../config/logicParadigms.js'
|
||||
import { REFERENCE_TYPES } from '../config/references.js'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { analysisText, isAnalyzing } = storeToRefs(appStore)
|
||||
|
||||
// 数据库 Store
|
||||
const dbStore = useDatabaseStore()
|
||||
const { references: dbReferences, isInitialized: dbInitialized } = storeToRefs(dbStore)
|
||||
|
||||
// 选中的范式
|
||||
const selectedParadigm = ref(null)
|
||||
|
||||
// 分析历史
|
||||
const analysisHistory = ref([])
|
||||
|
||||
// 从配置文件获取范式列表
|
||||
const paradigms = getParadigmList()
|
||||
// 范式列表(响应式,支持编辑)
|
||||
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',
|
||||
constraintsInput: '',
|
||||
// 新架构:维度配置
|
||||
dimensionSetId: null,
|
||||
dimensions: [],
|
||||
// 素材库配置
|
||||
autoMatchRefs: true,
|
||||
selectedRefs: []
|
||||
})
|
||||
|
||||
// 维度编辑器状态
|
||||
const showDimensionEditor = ref(false)
|
||||
|
||||
// 素材编辑器状态
|
||||
const showReferenceEditor = ref(false)
|
||||
const selectedRefType = ref('all')
|
||||
|
||||
// 使用数据库中的素材数据
|
||||
const allReferences = computed(() => {
|
||||
return dbReferences.value.map(ref => ({
|
||||
id: ref.id,
|
||||
type: ref.type,
|
||||
title: ref.title,
|
||||
source: ref.source,
|
||||
tags: ref.tags,
|
||||
excerptCount: ref.excerpts?.length || 0
|
||||
}))
|
||||
})
|
||||
|
||||
const filteredReferences = computed(() => {
|
||||
if (selectedRefType.value === 'all') {
|
||||
return allReferences.value
|
||||
}
|
||||
return allReferences.value.filter(ref => ref.type === selectedRefType.value)
|
||||
})
|
||||
|
||||
// 素材类型选项
|
||||
const referenceTypes = [
|
||||
{ id: 'all', name: '全部', icon: '📚' },
|
||||
{ id: REFERENCE_TYPES.POLICY, name: '政策文件', icon: '📄' },
|
||||
{ id: REFERENCE_TYPES.SPEECH, name: '领导讲话', icon: '🎙️' },
|
||||
{ id: REFERENCE_TYPES.CASE, name: '典型案例', icon: '📌' },
|
||||
{ id: REFERENCE_TYPES.QUOTE, name: '金句警句', icon: '✨' },
|
||||
{ id: REFERENCE_TYPES.REGULATION, name: '党规党纪', icon: '📖' }
|
||||
]
|
||||
|
||||
// 维度集选项
|
||||
const dimensionSetOptions = getDimensionSetList()
|
||||
|
||||
// 图标选项
|
||||
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'
|
||||
editForm.constraintsInput = (paradigm.systemConstraints || []).join('\n')
|
||||
|
||||
// 加载维度配置
|
||||
editForm.dimensionSetId = paradigm.dimensionSetId || null
|
||||
if (paradigm.dimensionSetId) {
|
||||
const dimSet = getDimensionSetById(paradigm.dimensionSetId)
|
||||
editForm.dimensions = dimSet ? JSON.parse(JSON.stringify(dimSet.dimensions)) : []
|
||||
} else if (paradigm.customDimensions) {
|
||||
editForm.dimensions = JSON.parse(JSON.stringify(paradigm.customDimensions))
|
||||
} else {
|
||||
editForm.dimensions = []
|
||||
}
|
||||
|
||||
// 加载素材配置
|
||||
editForm.autoMatchRefs = paradigm.autoMatchRefs !== false // 默认为true
|
||||
editForm.selectedRefs = paradigm.selectedRefs ? [...paradigm.selectedRefs] : []
|
||||
|
||||
showDimensionEditor.value = false
|
||||
showReferenceEditor.value = false
|
||||
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.constraintsInput = ''
|
||||
editForm.dimensionSetId = null
|
||||
editForm.dimensions = []
|
||||
// 素材配置重置
|
||||
editForm.autoMatchRefs = true
|
||||
editForm.selectedRefs = []
|
||||
showDimensionEditor.value = false
|
||||
showReferenceEditor.value = false
|
||||
}
|
||||
|
||||
// 切换维度编辑器显示
|
||||
const toggleDimensionEditor = () => {
|
||||
showDimensionEditor.value = !showDimensionEditor.value
|
||||
}
|
||||
|
||||
// 维度集变更时更新维度列表
|
||||
const onDimensionSetChange = () => {
|
||||
if (editForm.dimensionSetId) {
|
||||
const dimSet = getDimensionSetById(editForm.dimensionSetId)
|
||||
if (dimSet) {
|
||||
// 深拷贝维度列表,允许用户编辑
|
||||
editForm.dimensions = JSON.parse(JSON.stringify(dimSet.dimensions))
|
||||
}
|
||||
} else {
|
||||
editForm.dimensions = []
|
||||
}
|
||||
}
|
||||
|
||||
// 添加自定义维度
|
||||
const addDimension = () => {
|
||||
editForm.dimensions.push({
|
||||
id: `custom-dim-${Date.now()}`,
|
||||
name: '',
|
||||
focus: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除维度
|
||||
const removeDimension = (index) => {
|
||||
editForm.dimensions.splice(index, 1)
|
||||
}
|
||||
|
||||
// 切换素材编辑器显示
|
||||
const toggleReferenceEditor = () => {
|
||||
showReferenceEditor.value = !showReferenceEditor.value
|
||||
}
|
||||
|
||||
// 按类型筛选素材(通过修改selectedRefType触发computed重新计算)
|
||||
const filterReferencesByType = (typeId) => {
|
||||
selectedRefType.value = typeId
|
||||
}
|
||||
|
||||
// 切换素材选中状态
|
||||
const toggleReference = (refId) => {
|
||||
const index = editForm.selectedRefs.indexOf(refId)
|
||||
if (index === -1) {
|
||||
editForm.selectedRefs.push(refId)
|
||||
} else {
|
||||
editForm.selectedRefs.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 保存范式
|
||||
const saveParadigm = () => {
|
||||
const tags = editForm.tagsInput.split(',').map(t => t.trim()).filter(t => t)
|
||||
const constraints = editForm.constraintsInput.split('\n').map(c => c.trim()).filter(c => c)
|
||||
|
||||
// 处理维度配置
|
||||
const dimensionConfig = {
|
||||
dimensionSetId: editForm.dimensionSetId,
|
||||
customDimensions: editForm.dimensions.length > 0 ? editForm.dimensions : null
|
||||
}
|
||||
|
||||
// 处理素材配置
|
||||
const referenceConfig = {
|
||||
autoMatchRefs: editForm.autoMatchRefs,
|
||||
selectedRefs: editForm.selectedRefs
|
||||
}
|
||||
|
||||
if (isAddMode.value) {
|
||||
// 新增自定义范式
|
||||
const newParadigm = {
|
||||
id: `custom-${Date.now()}`,
|
||||
icon: editForm.icon,
|
||||
name: editForm.name,
|
||||
description: editForm.description,
|
||||
tags,
|
||||
tagClass: editForm.tagClass,
|
||||
systemConstraints: constraints,
|
||||
isCustom: true,
|
||||
// 新架构:维度配置
|
||||
...dimensionConfig,
|
||||
// 素材配置
|
||||
...referenceConfig,
|
||||
logicParadigms: {
|
||||
problemSection: 'progressive-problem',
|
||||
analysisSection: 'deep-attribution',
|
||||
remediationSection: 'remediation'
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
systemConstraints: constraints,
|
||||
// 新架构:维度配置
|
||||
...dimensionConfig,
|
||||
// 素材配置
|
||||
...referenceConfig
|
||||
}
|
||||
|
||||
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,
|
||||
systemConstraints: constraints,
|
||||
// 新架构:维度配置
|
||||
...dimensionConfig,
|
||||
// 素材配置
|
||||
...referenceConfig
|
||||
}
|
||||
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) => {
|
||||
@@ -154,22 +783,26 @@ const analyzeArticle = async () => {
|
||||
// 检测文章范式
|
||||
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 paradigms[4] // 民主生活会对照检查
|
||||
return findById('party-review') || list[0]
|
||||
} else if (text.includes('政府') || text.includes('工作报告') || text.includes('述职')) {
|
||||
return paradigms[5] // 政府工作报告
|
||||
return findById('gov-report') || list[0]
|
||||
} else if (text.includes('技术') || text.includes('代码') || text.includes('编程')) {
|
||||
return paradigms[0] // 技术博客
|
||||
return findById('tech') || list[0]
|
||||
} else if (text.includes('商业') || text.includes('市场') || text.includes('数据分析')) {
|
||||
return paradigms[1] // 商业分析
|
||||
return findById('business') || list[0]
|
||||
} else if (text.includes('产品') || text.includes('营销') || text.includes('用户')) {
|
||||
return paradigms[2] // 产品文案
|
||||
return findById('marketing') || list[0]
|
||||
} else if (text.includes('学术') || text.includes('研究') || text.includes('文献')) {
|
||||
return paradigms[3] // 学术论文
|
||||
return findById('academic') || list[0]
|
||||
}
|
||||
|
||||
return paradigms[0] // 默认技术博客
|
||||
return list[0] // 默认第一个范式
|
||||
}
|
||||
|
||||
// 添加到历史记录
|
||||
@@ -225,8 +858,12 @@ const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString()
|
||||
}
|
||||
|
||||
// 组件挂载时加载历史记录
|
||||
// 组件挂载时加载历史记录和范式列表
|
||||
onMounted(() => {
|
||||
// 初始化范式列表
|
||||
initParadigms()
|
||||
|
||||
// 加载分析历史
|
||||
const saved = localStorage.getItem('analysisHistory')
|
||||
if (saved) {
|
||||
analysisHistory.value = JSON.parse(saved)
|
||||
|
||||
@@ -58,15 +58,42 @@
|
||||
</div>
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<div class="flex-1 flex min-h-0">
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- 左侧:要求原文 -->
|
||||
<div class="flex-1 flex flex-col border-r border-slate-700">
|
||||
<div class="w-[40%] flex flex-col border-r border-slate-700 min-w-0">
|
||||
<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="px-3 py-2 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
|
||||
<span class="text-xs text-slate-500">来源:</span>
|
||||
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
|
||||
<button
|
||||
@click="leftSourceType = 'paste'"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
leftSourceType === 'paste' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>粘贴</button>
|
||||
<button
|
||||
v-if="activeParadigm?.defaultReference"
|
||||
@click="loadParadigmReference"
|
||||
: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
|
||||
@click="showMaterialSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
leftSourceType === 'material' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>素材库</button>
|
||||
<button
|
||||
@click="showLeftDocSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
leftSourceType === 'document' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>文稿库</button>
|
||||
</div>
|
||||
</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">
|
||||
@@ -121,13 +148,29 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧:写作内容 -->
|
||||
<div class="flex-1 flex flex-col">
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<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="px-3 py-2 bg-slate-800/50 border-b border-slate-700 flex items-center gap-2">
|
||||
<span class="text-xs text-slate-500">来源:</span>
|
||||
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
|
||||
<button
|
||||
@click="rightSourceType = 'paste'"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
rightSourceType === 'paste' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>粘贴</button>
|
||||
<button
|
||||
@click="showRightDocSelector = true"
|
||||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||||
rightSourceType === 'document' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>文稿库</button>
|
||||
</div>
|
||||
</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">
|
||||
@@ -147,12 +190,41 @@
|
||||
]"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<span :class="['text-xs shrink-0 mt-0.5 w-5 h-5 rounded flex items-center justify-center',
|
||||
<span :class="['text-xs shrink-0 mt-0.5 w-5 h-5 rounded flex items-center justify-center',
|
||||
selectedRightIdxs.includes(idx) ? 'bg-blue-500 text-white' : 'text-blue-500/70']">
|
||||
{{ selectedRightIdxs.includes(idx) ? '✓' : (idx + 1) }}
|
||||
</span>
|
||||
<p class="text-sm text-slate-300 whitespace-pre-wrap">{{ para }}</p>
|
||||
</div>
|
||||
<!-- 段落类型标签 -->
|
||||
<div v-if="paragraphTypes[idx]" class="mt-2 flex items-center gap-2 relative">
|
||||
<button
|
||||
@click.stop="toggleTypeDropdown(idx)"
|
||||
:class="['text-xs px-2 py-0.5 rounded flex items-center gap-1',
|
||||
getSectionTypeClasses(paragraphTypes[idx].sectionType)]"
|
||||
>
|
||||
<span>{{ paragraphTypes[idx].sectionLabel }}</span>
|
||||
<span v-if="!paragraphTypes[idx].userConfirmed" class="text-[10px] opacity-70">
|
||||
({{ Math.round(paragraphTypes[idx].confidence * 100) }}%)
|
||||
</span>
|
||||
<span class="text-[10px]">▼</span>
|
||||
</button>
|
||||
<!-- 类型选择下拉菜单 -->
|
||||
<div
|
||||
v-if="showTypeDropdown === idx"
|
||||
class="absolute top-full left-0 mt-1 bg-slate-800 border border-slate-600 rounded-lg shadow-lg z-10 py-1"
|
||||
>
|
||||
<button
|
||||
v-for="type in Object.values(SECTION_TYPES)"
|
||||
:key="type.id"
|
||||
@click.stop="selectParagraphType(idx, type)"
|
||||
:class="['w-full text-left text-xs px-3 py-1.5 hover:bg-slate-700 flex items-center gap-2',
|
||||
paragraphTypes[idx].sectionType === type.id ? 'bg-slate-700' : '']"
|
||||
>
|
||||
<span :class="[type.bgClass, type.textClass, 'px-1.5 py-0.5 rounded text-[10px]']">{{ type.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 检查结果标记 -->
|
||||
<div v-if="checkResults[idx]" class="mt-2 pt-2 border-t border-slate-700">
|
||||
<div :class="[
|
||||
@@ -201,6 +273,41 @@
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400">{{ lastCheckResult.summary }}</p>
|
||||
<!-- 展开/收起按钮 -->
|
||||
<button
|
||||
v-if="lastCheckResult.suggestions?.length"
|
||||
@click="showDetailedSuggestions = !showDetailedSuggestions"
|
||||
class="text-xs text-indigo-400 hover:text-indigo-300 mt-2"
|
||||
>
|
||||
{{ showDetailedSuggestions ? '收起详情 ▲' : '展开详情 ▼' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 详细建议区域 -->
|
||||
<div v-if="showDetailedSuggestions && lastCheckResult.suggestions?.length"
|
||||
class="mt-3 p-3 bg-slate-800/50 rounded-lg border border-slate-700">
|
||||
<h4 class="text-xs text-slate-400 mb-2 flex items-center gap-1">
|
||||
<span>💡</span> 改进建议
|
||||
</h4>
|
||||
<div class="space-y-2">
|
||||
<div v-for="(suggestion, idx) in lastCheckResult.suggestions" :key="idx"
|
||||
class="flex items-center justify-between bg-slate-900/50 rounded px-3 py-2">
|
||||
<span class="text-xs text-slate-300 flex-1">{{ idx + 1 }}. {{ suggestion }}</span>
|
||||
<button
|
||||
@click="executeRewrite(idx, suggestion)"
|
||||
:disabled="rewritingSuggestionIdx !== null || selectedRightIdxs.length === 0"
|
||||
class="text-xs px-2 py-1 rounded bg-indigo-600/50 text-indigo-200
|
||||
hover:bg-indigo-500 transition flex items-center gap-1
|
||||
disabled:opacity-50 disabled:cursor-not-allowed ml-2 shrink-0">
|
||||
<span v-if="rewritingSuggestionIdx === idx" class="animate-spin">↻</span>
|
||||
<span v-else>🔄</span>
|
||||
{{ rewritingSuggestionIdx === idx ? '重写中...' : '一键重写' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="selectedRightIdxs.length === 0"
|
||||
class="text-xs text-amber-400/70 mt-2">
|
||||
提示:请先选中右侧要重写的段落
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex-1 text-sm text-slate-500">
|
||||
@@ -209,13 +316,23 @@
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<button
|
||||
<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
|
||||
<button
|
||||
@click="detectParagraphTypes"
|
||||
:disabled="rightParagraphs.length === 0 || isDetectingTypes"
|
||||
class="px-4 py-2 rounded-lg text-sm bg-purple-700 text-white hover:bg-purple-600 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="isDetectingTypes" class="flex items-center gap-2">
|
||||
<span class="animate-spin">↻</span> 识别中...
|
||||
</span>
|
||||
<span v-else>🧠 智能识别</span>
|
||||
</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"
|
||||
@@ -229,21 +346,290 @@
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- 素材选择弹窗 -->
|
||||
<MaterialSelectorModal
|
||||
:visible="showMaterialSelector"
|
||||
@close="showMaterialSelector = false"
|
||||
@select="handleMaterialSelect"
|
||||
/>
|
||||
|
||||
<!-- 文稿选择弹窗(左侧) -->
|
||||
<DocumentSelectorModal
|
||||
:visible="showLeftDocSelector"
|
||||
@close="showLeftDocSelector = false"
|
||||
@select="handleLeftDocSelect"
|
||||
/>
|
||||
|
||||
<!-- 文稿选择弹窗(右侧) -->
|
||||
<DocumentSelectorModal
|
||||
:visible="showRightDocSelector"
|
||||
@close="showRightDocSelector = false"
|
||||
@select="handleRightDocSelect"
|
||||
/>
|
||||
|
||||
<!-- 重写预览浮窗 -->
|
||||
<div
|
||||
v-if="showRewriteModal"
|
||||
class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
|
||||
>
|
||||
<div class="bg-slate-800 rounded-lg w-[900px] max-h-[85vh] flex flex-col shadow-2xl">
|
||||
<!-- 浮窗头部 -->
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-white flex items-center gap-2">
|
||||
<span>🔄</span> AI 重写预览
|
||||
</h3>
|
||||
<button
|
||||
@click="resetRewriteModal"
|
||||
class="text-slate-400 hover:text-white text-xl"
|
||||
>✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 建议信息 + 视图切换 -->
|
||||
<div class="px-4 py-2 bg-indigo-900/30 border-b border-indigo-700/50 flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-xs text-indigo-300">📋 根据建议:</span>
|
||||
<span class="text-xs text-white ml-1">{{ currentSuggestion }}</span>
|
||||
</div>
|
||||
<!-- 视图切换按钮 -->
|
||||
<div v-if="rewritingSuggestionIdx === null && diffSegments.length > 0" class="flex bg-slate-900/50 rounded p-0.5">
|
||||
<button
|
||||
@click="rewriteViewMode = 'result'"
|
||||
:class="['text-xs px-2 py-1 rounded transition', rewriteViewMode === 'result' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>仅看结果</button>
|
||||
<button
|
||||
@click="rewriteViewMode = 'diff'"
|
||||
:class="['text-xs px-2 py-1 rounded transition', rewriteViewMode === 'diff' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>差异对比</button>
|
||||
<button
|
||||
@click="rewriteViewMode = 'review'; currentReviewIdx = 0"
|
||||
:class="['text-xs px-2 py-1 rounded transition', rewriteViewMode === 'review' ? 'bg-indigo-600 text-white' : 'text-slate-400 hover:text-white']"
|
||||
>逐句审核</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异统计信息 -->
|
||||
<div v-if="rewritingSuggestionIdx === null && diffStats.total > 0" class="px-4 py-2 bg-slate-900/30 border-b border-slate-700 flex items-center gap-4 text-xs">
|
||||
<span class="text-slate-400">共 {{ diffStats.total }} 处</span>
|
||||
<span v-if="diffStats.modified > 0" class="text-amber-400">{{ diffStats.modified }} 处修改</span>
|
||||
<span v-if="diffStats.added > 0" class="text-green-400">{{ diffStats.added }} 处新增</span>
|
||||
<span v-if="diffStats.removed > 0" class="text-red-400">{{ diffStats.removed }} 处删除</span>
|
||||
<span class="text-slate-500">|</span>
|
||||
<span class="text-indigo-400">已选中 {{ acceptedChanges.size }} 处修改</span>
|
||||
</div>
|
||||
|
||||
<!-- 重写内容区域 -->
|
||||
<div class="flex-1 overflow-y-auto min-h-[300px]">
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="rewritingSuggestionIdx !== null" class="p-4">
|
||||
<div class="flex items-center gap-2 text-indigo-300 mb-3">
|
||||
<span class="animate-spin">↻</span>
|
||||
<span class="text-sm">AI 正在重写中...</span>
|
||||
</div>
|
||||
<div class="bg-slate-900 rounded-lg p-4 border border-slate-700">
|
||||
<p class="text-sm text-slate-200 whitespace-pre-wrap leading-relaxed">
|
||||
{{ rewritePreview || '等待生成...' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 仅看结果视图 -->
|
||||
<div v-else-if="rewriteViewMode === 'result'" class="p-4">
|
||||
<div class="bg-slate-900 rounded-lg p-4 border border-slate-700">
|
||||
<p class="text-sm text-slate-200 whitespace-pre-wrap leading-relaxed">
|
||||
{{ rewritePreview || '等待生成...' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 差异对比视图 -->
|
||||
<div v-else-if="rewriteViewMode === 'diff'" class="p-4">
|
||||
<div class="flex gap-4">
|
||||
<!-- 左边:原文 -->
|
||||
<div class="flex-1">
|
||||
<div class="text-xs text-amber-400 mb-2 font-medium">📄 原文(点击选中需要替换的部分)</div>
|
||||
<div class="bg-slate-900 rounded-lg p-3 border border-slate-700">
|
||||
<template v-for="segment in diffSegments" :key="'orig-' + segment.idx">
|
||||
<span
|
||||
v-if="segment.type === 'unchanged'"
|
||||
class="text-sm text-slate-400"
|
||||
>{{ segment.original }}</span>
|
||||
<span
|
||||
v-else-if="segment.type === 'modified' || segment.type === 'removed'"
|
||||
@click="toggleChangeAccepted(segment.idx)"
|
||||
:class="[
|
||||
'text-sm px-0.5 rounded cursor-pointer transition-all',
|
||||
acceptedChanges.has(segment.idx)
|
||||
? 'bg-amber-500/40 text-amber-100 ring-2 ring-amber-400'
|
||||
: 'bg-amber-900/30 text-amber-200 hover:bg-amber-800/40',
|
||||
segment.type === 'removed' ? 'line-through' : ''
|
||||
]"
|
||||
>{{ segment.original }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右边:重写后 -->
|
||||
<div class="flex-1">
|
||||
<div class="text-xs text-blue-400 mb-2 font-medium">✨ 重写后(选中部分将替换为此内容)</div>
|
||||
<div class="bg-slate-900 rounded-lg p-3 border border-slate-700">
|
||||
<template v-for="segment in diffSegments" :key="'new-' + segment.idx">
|
||||
<span
|
||||
v-if="segment.type === 'unchanged'"
|
||||
class="text-sm text-slate-400"
|
||||
>{{ segment.rewritten }}</span>
|
||||
<span
|
||||
v-else-if="segment.type === 'modified' || segment.type === 'added'"
|
||||
:class="[
|
||||
'text-sm px-0.5 rounded transition-all',
|
||||
acceptedChanges.has(segment.idx)
|
||||
? 'bg-blue-500/40 text-blue-100 ring-2 ring-blue-400'
|
||||
: 'bg-blue-900/30 text-blue-200'
|
||||
]"
|
||||
>{{ segment.rewritten }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mt-3 text-center">在左边原文中点击句子可选中/取消,选中的部分将被右边对应内容替换</p>
|
||||
</div>
|
||||
|
||||
<!-- 逐句审核视图 -->
|
||||
<div v-else-if="rewriteViewMode === 'review'" class="p-4">
|
||||
<div v-if="currentReviewItem" class="space-y-4">
|
||||
<!-- 进度指示 -->
|
||||
<div class="flex items-center justify-between text-xs text-slate-400">
|
||||
<span>第 {{ currentReviewIdx + 1 }} / {{ reviewableChanges.length }} 处修改</span>
|
||||
<span :class="[
|
||||
'px-2 py-0.5 rounded',
|
||||
currentReviewItem.type === 'modified' ? 'bg-amber-900/30 text-amber-300' :
|
||||
currentReviewItem.type === 'added' ? 'bg-green-900/30 text-green-300' :
|
||||
'bg-red-900/30 text-red-300'
|
||||
]">
|
||||
{{ currentReviewItem.type === 'modified' ? '修改' : currentReviewItem.type === 'added' ? '新增' : '删除' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 原文 -->
|
||||
<div v-if="currentReviewItem.original">
|
||||
<div class="text-xs text-amber-400 mb-1">原文:</div>
|
||||
<div class="bg-slate-900 rounded-lg p-3 border border-amber-700/30">
|
||||
<p class="text-sm text-slate-300">{{ currentReviewItem.original }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 重写后 -->
|
||||
<div v-if="currentReviewItem.rewritten">
|
||||
<div class="text-xs text-blue-400 mb-1">重写后:</div>
|
||||
<div class="bg-slate-900 rounded-lg p-3 border border-blue-700/30">
|
||||
<p class="text-sm text-slate-200">{{ currentReviewItem.rewritten }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<button
|
||||
@click="prevReviewItem"
|
||||
:disabled="currentReviewIdx === 0"
|
||||
class="text-xs px-3 py-1.5 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 disabled:opacity-50"
|
||||
>← 上一处</button>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="toggleChangeAccepted(currentReviewItem.idx)"
|
||||
:class="[
|
||||
'text-xs px-3 py-1.5 rounded transition',
|
||||
acceptedChanges.has(currentReviewItem.idx)
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||
]"
|
||||
>
|
||||
{{ acceptedChanges.has(currentReviewItem.idx) ? '✓ 已接受' : '接受修改' }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="nextReviewItem"
|
||||
:disabled="currentReviewIdx >= reviewableChanges.length - 1"
|
||||
class="text-xs px-3 py-1.5 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 disabled:opacity-50"
|
||||
>下一处 →</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center text-slate-500 py-8">
|
||||
没有需要审核的修改
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 浮窗底部操作 -->
|
||||
<div class="p-4 border-t border-slate-700 flex items-center justify-between">
|
||||
<!-- 左侧:全选/取消全选 -->
|
||||
<div v-if="rewritingSuggestionIdx === null && diffSegments.length > 0" class="flex gap-2">
|
||||
<button
|
||||
@click="toggleAllChanges(true)"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600"
|
||||
>全选修改</button>
|
||||
<button
|
||||
@click="toggleAllChanges(false)"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600"
|
||||
>取消全选</button>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
|
||||
<!-- 右侧:主操作按钮 -->
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="resetRewriteModal"
|
||||
class="px-4 py-2 text-sm bg-slate-700 text-slate-300 rounded-lg hover:bg-slate-600"
|
||||
>取消</button>
|
||||
<button
|
||||
v-if="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
|
||||
@click="confirmRewrite"
|
||||
:disabled="rewritingSuggestionIdx !== null || !rewritePreview"
|
||||
class="px-4 py-2 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>全部替换</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useDatabaseStore } from '../stores/database'
|
||||
import { SECTION_TYPES, getSectionTypeById, getSectionTypeClasses } from '../config/sectionTypes'
|
||||
import { getLogicParadigmById, buildLogicPrompt } from '../config/logicParadigms'
|
||||
import { computeDiff, applySelectedChanges as applyDiffChanges, getDiffStats } from '../utils/textDiff'
|
||||
import MaterialSelectorModal from './MaterialSelectorModal.vue'
|
||||
import DocumentSelectorModal from './DocumentSelectorModal.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const dbStore = useDatabaseStore()
|
||||
const { activeParadigm } = storeToRefs(appStore)
|
||||
|
||||
// 内容
|
||||
const leftContent = ref('')
|
||||
const rightContent = ref('')
|
||||
|
||||
// 左侧来源相关
|
||||
const leftSourceType = ref('paste') // 'paste' | 'paradigm' | 'material' | 'document'
|
||||
const showMaterialSelector = ref(false)
|
||||
const showLeftDocSelector = ref(false)
|
||||
|
||||
// 右侧来源相关
|
||||
const rightSourceType = ref('paste') // 'paste' | 'document'
|
||||
const showRightDocSelector = ref(false)
|
||||
|
||||
// 段落类型识别相关
|
||||
const isDetectingTypes = ref(false)
|
||||
const paragraphTypes = ref({}) // { [idx]: { sectionType, sectionLabel, confidence, userConfirmed } }
|
||||
const showTypeDropdown = ref(null) // 当前显示下拉菜单的段落索引
|
||||
|
||||
// 选中状态(改为数组支持多选)
|
||||
const selectedLeftIdxs = ref([])
|
||||
const selectedRightIdxs = ref([])
|
||||
@@ -253,6 +639,22 @@ const isComparing = ref(false)
|
||||
const lastCheckResult = ref(null)
|
||||
const checkResults = ref({})
|
||||
|
||||
// 重写相关状态
|
||||
const showDetailedSuggestions = ref(false) // 是否展开详细建议
|
||||
const rewritingParagraphIdx = ref(null) // 正在重写的段落索引
|
||||
const rewritingSuggestionIdx = ref(null) // 正在处理的建议索引
|
||||
const rewritePreview = ref('') // 重写预览内容
|
||||
const showRewriteModal = ref(false) // 是否显示重写预览浮窗
|
||||
const currentSuggestion = ref('') // 当前处理的建议文本
|
||||
const targetParagraphIdxs = ref([]) // 目标段落索引
|
||||
|
||||
// 差异对比相关状态
|
||||
const rewriteViewMode = ref('result') // 'result' | 'diff' | 'review' - 视图模式
|
||||
const diffSegments = ref([]) // 差异片段数组
|
||||
const currentReviewIdx = ref(0) // 当前审核的片段索引
|
||||
const acceptedChanges = ref(new Set()) // 已接受的修改索引集合
|
||||
const originalContent = ref('') // 原始内容(用于对比)
|
||||
|
||||
// 范式相关
|
||||
const showParadigmRules = ref(false)
|
||||
const checkMode = ref('paragraph') // 'paragraph' | 'document'
|
||||
@@ -374,64 +776,439 @@ const goBack = () => {
|
||||
appStore.switchPage('writer')
|
||||
}
|
||||
|
||||
// 执行对照检查
|
||||
const runCompare = async () => {
|
||||
if (!canCompare.value) return
|
||||
|
||||
const requirement = getSelectedLeftText()
|
||||
const content = getSelectedRightText()
|
||||
const paradigmRules = getParadigmRulesText()
|
||||
|
||||
isComparing.value = true
|
||||
lastCheckResult.value = null
|
||||
|
||||
// 加载范式默认参考范文
|
||||
const loadParadigmReference = () => {
|
||||
if (activeParadigm.value?.defaultReference) {
|
||||
leftContent.value = activeParadigm.value.defaultReference.content
|
||||
leftSourceType.value = 'paradigm'
|
||||
}
|
||||
}
|
||||
|
||||
// 处理素材选择
|
||||
const handleMaterialSelect = (material) => {
|
||||
if (material.excerpts?.length > 0) {
|
||||
leftContent.value = material.excerpts.map(e => e.content).join('\n\n')
|
||||
}
|
||||
leftSourceType.value = 'material'
|
||||
showMaterialSelector.value = false
|
||||
}
|
||||
|
||||
// 处理左侧文稿选择
|
||||
const handleLeftDocSelect = (doc) => {
|
||||
leftContent.value = doc.content || ''
|
||||
leftSourceType.value = 'document'
|
||||
showLeftDocSelector.value = false
|
||||
}
|
||||
|
||||
// 处理右侧文稿选择
|
||||
const handleRightDocSelect = (doc) => {
|
||||
rightContent.value = doc.content || ''
|
||||
rightSourceType.value = 'document'
|
||||
showRightDocSelector.value = false
|
||||
}
|
||||
|
||||
// 切换段落类型下拉菜单
|
||||
const toggleTypeDropdown = (idx) => {
|
||||
showTypeDropdown.value = showTypeDropdown.value === idx ? null : idx
|
||||
}
|
||||
|
||||
// 手动选择段落类型
|
||||
const selectParagraphType = (idx, type) => {
|
||||
paragraphTypes.value[idx] = {
|
||||
...paragraphTypes.value[idx],
|
||||
sectionType: type.id,
|
||||
sectionLabel: type.label,
|
||||
userConfirmed: true
|
||||
}
|
||||
showTypeDropdown.value = null
|
||||
}
|
||||
|
||||
// 智能识别段落类型
|
||||
const detectParagraphTypes = async () => {
|
||||
if (rightParagraphs.value.length === 0) return
|
||||
|
||||
isDetectingTypes.value = true
|
||||
try {
|
||||
// 构建包含要求原文 + 范式规则的完整对照依据
|
||||
let prompt = `你是一个严格的写作质检专家。请对比以下"对照依据"和"写作内容",判断内容是否符合要求。
|
||||
const prompt = buildParagraphTypeDetectionPrompt(rightParagraphs.value)
|
||||
|
||||
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])
|
||||
parsed.results?.forEach(r => {
|
||||
paragraphTypes.value[r.paragraphIdx] = {
|
||||
sectionType: r.sectionType,
|
||||
sectionLabel: r.sectionLabel,
|
||||
confidence: r.confidence || 0.8,
|
||||
userConfirmed: false
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('段落类型识别失败:', error)
|
||||
} finally {
|
||||
isDetectingTypes.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 构建重写 Prompt
|
||||
const buildRewritePrompt = (originalParagraph, suggestion, paragraphTypeInfo) => {
|
||||
const paradigm = activeParadigm.value
|
||||
let prompt = `你是一名专业的写作润色专家。请根据以下检查建议,重写这段内容。
|
||||
|
||||
# 原文内容
|
||||
${originalParagraph}
|
||||
|
||||
# 检查建议
|
||||
${suggestion}
|
||||
|
||||
# 重写要求
|
||||
1. 保持原文的核心观点和信息不变
|
||||
2. 针对检查建议进行针对性改进
|
||||
3. 保持与上下文的连贯性
|
||||
`
|
||||
|
||||
// 注入段落类型对应的逻辑范式
|
||||
if (paragraphTypeInfo?.sectionType && paradigm) {
|
||||
const sectionType = getSectionTypeById(paragraphTypeInfo.sectionType)
|
||||
if (sectionType?.logicKey && paradigm.logicParadigms?.[sectionType.logicKey]) {
|
||||
const logicId = paradigm.logicParadigms[sectionType.logicKey]
|
||||
const logic = getLogicParadigmById(logicId)
|
||||
if (logic) {
|
||||
prompt += `
|
||||
# 写作规范(${logic.name})
|
||||
**结构公式**:${logic.structureFormula}
|
||||
**必须遵循的层次**:
|
||||
${logic.layers?.map((l, i) => `${i + 1}. ${l.name}:${l.question}`).join('\n') || ''}
|
||||
`
|
||||
if (logic.languageStyle?.vocabulary) {
|
||||
prompt += `**推荐术语**:${logic.languageStyle.vocabulary.slice(0, 8).join('、')}\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt += `
|
||||
# 输出要求
|
||||
请直接输出重写后的段落内容,不要包含任何解释或前缀。`
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// 执行重写(显示浮窗)
|
||||
const executeRewrite = async (suggestionIdx, suggestion) => {
|
||||
if (selectedRightIdxs.value.length === 0) {
|
||||
alert('请先选中要重写的段落')
|
||||
return
|
||||
}
|
||||
|
||||
// 保存目标信息
|
||||
targetParagraphIdxs.value = [...selectedRightIdxs.value].sort((a, b) => a - b)
|
||||
currentSuggestion.value = suggestion
|
||||
|
||||
// 保存原始内容到状态变量(用于差异对比)
|
||||
originalContent.value = targetParagraphIdxs.value.map(i => rightParagraphs.value[i]).join('\n\n')
|
||||
const paragraphTypeInfo = paragraphTypes.value[targetParagraphIdxs.value[0]]
|
||||
|
||||
// 重置差异相关状态
|
||||
rewriteViewMode.value = 'result'
|
||||
diffSegments.value = []
|
||||
currentReviewIdx.value = 0
|
||||
acceptedChanges.value = new Set()
|
||||
|
||||
// 显示浮窗
|
||||
showRewriteModal.value = true
|
||||
rewritingSuggestionIdx.value = suggestionIdx
|
||||
rewritePreview.value = ''
|
||||
|
||||
try {
|
||||
const prompt = buildRewritePrompt(originalContent.value, suggestion, paragraphTypeInfo)
|
||||
|
||||
await appStore.callApi(prompt, (chunk) => {
|
||||
rewritePreview.value += chunk
|
||||
}, { temperature: 0.5 })
|
||||
|
||||
// 重写完成后自动计算差异
|
||||
computeDiffSegments()
|
||||
|
||||
} catch (error) {
|
||||
console.error('重写失败:', error)
|
||||
alert('重写失败,请重试')
|
||||
showRewriteModal.value = false
|
||||
} finally {
|
||||
rewritingSuggestionIdx.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 更新右侧段落内容
|
||||
const updateRightContent = (targetIdxs, newContent) => {
|
||||
const paragraphs = rightContent.value.split(/\n\s*\n/).filter(p => p.trim())
|
||||
const firstIdx = targetIdxs[0]
|
||||
|
||||
// 替换目标段落
|
||||
paragraphs.splice(firstIdx, targetIdxs.length, newContent.trim())
|
||||
|
||||
// 更新 rightContent
|
||||
rightContent.value = paragraphs.join('\n\n')
|
||||
|
||||
// 清除被替换段落的检查结果和类型信息
|
||||
targetIdxs.forEach(idx => {
|
||||
delete checkResults.value[idx]
|
||||
delete paragraphTypes.value[idx]
|
||||
})
|
||||
|
||||
// 清除选中状态
|
||||
selectedRightIdxs.value = []
|
||||
}
|
||||
|
||||
// 确认重写(替换内容)
|
||||
const confirmRewrite = () => {
|
||||
if (!rewritePreview.value || targetParagraphIdxs.value.length === 0) return
|
||||
|
||||
// 执行替换
|
||||
updateRightContent(targetParagraphIdxs.value, rewritePreview.value)
|
||||
|
||||
// 关闭浮窗
|
||||
showRewriteModal.value = false
|
||||
rewritePreview.value = ''
|
||||
currentSuggestion.value = ''
|
||||
targetParagraphIdxs.value = []
|
||||
}
|
||||
|
||||
// 计算差异片段
|
||||
const computeDiffSegments = () => {
|
||||
if (!originalContent.value || !rewritePreview.value) return
|
||||
diffSegments.value = computeDiff(originalContent.value, rewritePreview.value)
|
||||
// 默认全部接受修改
|
||||
acceptedChanges.value = new Set(
|
||||
diffSegments.value
|
||||
.filter(s => s.type !== 'unchanged')
|
||||
.map(s => s.idx)
|
||||
)
|
||||
}
|
||||
|
||||
// 获取差异统计信息
|
||||
const diffStats = computed(() => {
|
||||
return getDiffStats(diffSegments.value)
|
||||
})
|
||||
|
||||
// 获取需要审核的修改(排除 unchanged)
|
||||
const reviewableChanges = computed(() => {
|
||||
return diffSegments.value.filter(s => s.type !== 'unchanged')
|
||||
})
|
||||
|
||||
// 当前审核的修改项
|
||||
const currentReviewItem = computed(() => {
|
||||
return reviewableChanges.value[currentReviewIdx.value] || null
|
||||
})
|
||||
|
||||
// 切换修改的接受状态
|
||||
const toggleChangeAccepted = (idx) => {
|
||||
if (acceptedChanges.value.has(idx)) {
|
||||
acceptedChanges.value.delete(idx)
|
||||
} else {
|
||||
acceptedChanges.value.add(idx)
|
||||
}
|
||||
// 触发响应式更新
|
||||
acceptedChanges.value = new Set(acceptedChanges.value)
|
||||
}
|
||||
|
||||
// 全选/取消全选修改
|
||||
const toggleAllChanges = (accept) => {
|
||||
if (accept) {
|
||||
acceptedChanges.value = new Set(
|
||||
diffSegments.value
|
||||
.filter(s => s.type !== 'unchanged')
|
||||
.map(s => s.idx)
|
||||
)
|
||||
} else {
|
||||
acceptedChanges.value = new Set()
|
||||
}
|
||||
}
|
||||
|
||||
// 逐句审核:下一个
|
||||
const nextReviewItem = () => {
|
||||
if (currentReviewIdx.value < reviewableChanges.value.length - 1) {
|
||||
currentReviewIdx.value++
|
||||
}
|
||||
}
|
||||
|
||||
// 逐句审核:上一个
|
||||
const prevReviewItem = () => {
|
||||
if (currentReviewIdx.value > 0) {
|
||||
currentReviewIdx.value--
|
||||
}
|
||||
}
|
||||
|
||||
// 应用选中的修改
|
||||
const applySelectedChangesToContent = () => {
|
||||
if (diffSegments.value.length === 0) return
|
||||
|
||||
const finalContent = applyDiffChanges(
|
||||
originalContent.value,
|
||||
diffSegments.value,
|
||||
acceptedChanges.value
|
||||
)
|
||||
|
||||
// 执行替换
|
||||
updateRightContent(targetParagraphIdxs.value, finalContent)
|
||||
|
||||
// 关闭浮窗并重置状态
|
||||
resetRewriteModal()
|
||||
}
|
||||
|
||||
// 重置重写浮窗状态
|
||||
const resetRewriteModal = () => {
|
||||
showRewriteModal.value = false
|
||||
rewritePreview.value = ''
|
||||
currentSuggestion.value = ''
|
||||
targetParagraphIdxs.value = []
|
||||
rewriteViewMode.value = 'result'
|
||||
diffSegments.value = []
|
||||
currentReviewIdx.value = 0
|
||||
acceptedChanges.value = new Set()
|
||||
originalContent.value = ''
|
||||
}
|
||||
|
||||
// 构建段落类型识别 Prompt
|
||||
const buildParagraphTypeDetectionPrompt = (paragraphs) => {
|
||||
return `你是一名专业的公文写作分析专家。请分析以下段落,识别每个段落属于哪种章节类型。
|
||||
|
||||
# 可识别的章节类型
|
||||
1. **problem** (存在问题):问题查摆、不足之处,通常采用"定性判断+具体表现+后果"结构
|
||||
2. **analysis** (原因剖析):原因分析、根源探究,通常从思想/政治/作风/能力/纪律等维度溯源
|
||||
3. **remediation** (整改措施):整改方案、改进措施,通常包含具体行动和量化指标
|
||||
4. **case** (典型案例):案例剖析,通常采用"以案说德/纪/法/责"结构
|
||||
5. **intro** (开篇引言):背景介绍、会议说明等
|
||||
6. **conclusion** (结尾表态):表态发言、决心表述等
|
||||
|
||||
# 待分析的段落
|
||||
${paragraphs.map((p, i) => `【段落${i}】\n${p.substring(0, 500)}${p.length > 500 ? '...' : ''}`).join('\n\n')}
|
||||
|
||||
# 输出要求
|
||||
请严格按照以下 JSON 格式输出(不要输出其他内容):
|
||||
{
|
||||
"results": [
|
||||
{"paragraphIdx": 0, "sectionType": "problem", "sectionLabel": "存在问题", "confidence": 0.85}
|
||||
]
|
||||
}`
|
||||
}
|
||||
|
||||
// 构建段落级精确检查 Prompt
|
||||
const buildParagraphCheckPrompt = (requirement, selectedParagraphs) => {
|
||||
const paradigm = activeParadigm.value
|
||||
let prompt = `你是一个严格的写作质检专家。请根据以下检查标准,对写作内容进行精确检查。
|
||||
|
||||
# 对照依据
|
||||
|
||||
## 一、要求原文(共 ${selectedLeftIdxs.value.length} 段)
|
||||
## 一、要求原文
|
||||
${requirement}
|
||||
`
|
||||
|
||||
// 如果有范式规则,追加到对照依据中
|
||||
if (paradigmRules) {
|
||||
prompt += `
|
||||
## 二、写作范式专家规则(共 ${paradigmRulesCount.value} 条)
|
||||
${paradigmRules}
|
||||
`
|
||||
// 根据段落类型注入对应的逻辑范式规则
|
||||
const typeGroups = {}
|
||||
selectedParagraphs.forEach(p => {
|
||||
const type = p.typeInfo?.sectionType || 'unknown'
|
||||
if (!typeGroups[type]) typeGroups[type] = []
|
||||
typeGroups[type].push(p)
|
||||
})
|
||||
|
||||
// 为每种类型注入对应的逻辑范式
|
||||
if (paradigm && Object.keys(typeGroups).length > 0) {
|
||||
prompt += `\n## 二、段落级写作规范\n`
|
||||
|
||||
for (const [type, paragraphs] of Object.entries(typeGroups)) {
|
||||
const sectionType = getSectionTypeById(type)
|
||||
if (!sectionType) continue
|
||||
|
||||
prompt += `\n### ${sectionType.label}类段落(共${paragraphs.length}段)\n`
|
||||
|
||||
// 获取对应的逻辑范式
|
||||
const logicKey = sectionType.logicKey
|
||||
if (logicKey && paradigm.logicParadigms?.[logicKey]) {
|
||||
const logicId = paradigm.logicParadigms[logicKey]
|
||||
const logic = getLogicParadigmById(logicId)
|
||||
if (logic) {
|
||||
prompt += `**适用逻辑范式**:${logic.name}\n`
|
||||
prompt += `**结构公式**:${logic.structureFormula}\n`
|
||||
prompt += `**必须遵循的层次**:\n`
|
||||
logic.layers?.forEach((l, i) => {
|
||||
prompt += `${i + 1}. ${l.name}:${l.question}\n`
|
||||
})
|
||||
if (logic.languageStyle?.vocabulary) {
|
||||
prompt += `**推荐术语**:${logic.languageStyle.vocabulary.slice(0, 5).join('、')}\n`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt += `
|
||||
# 写作内容(共 ${selectedRightIdxs.value.length} 段)
|
||||
${content}
|
||||
// 注入段落级专家规则
|
||||
const paragraphRules = paradigm?.expertGuidelines?.filter(g => g.scope === 'paragraph') || []
|
||||
if (paragraphRules.length > 0) {
|
||||
prompt += `\n## 三、专家评价标准(段落级)\n`
|
||||
paragraphRules.forEach((r, i) => {
|
||||
prompt += `${i + 1}. 【${r.title}】${r.description}\n`
|
||||
})
|
||||
}
|
||||
|
||||
# 检查说明
|
||||
请综合"要求原文"${paradigmRules ? '和"写作范式专家规则"' : ''},对写作内容进行全面检查。
|
||||
// 添加待检查内容
|
||||
prompt += `\n# 待检查的写作内容\n`
|
||||
selectedParagraphs.forEach(p => {
|
||||
const typeLabel = p.typeInfo?.sectionLabel || '未识别'
|
||||
prompt += `\n【段落${p.idx + 1}】(${typeLabel})\n${p.content}\n`
|
||||
})
|
||||
|
||||
prompt += `
|
||||
# 输出要求
|
||||
请严格按照以下 JSON 格式输出检查结果(不要输出其他内容):
|
||||
{
|
||||
"overall": "pass|warning|fail",
|
||||
"summary": "一句话总结检查结果",
|
||||
"details": [
|
||||
{"aspect": "检查维度(如:内容完整性/篇幅占比/关键词覆盖)", "status": "pass|warning|fail", "message": "具体说明"}
|
||||
{"aspect": "检查维度", "status": "pass|warning|fail", "message": "具体说明"}
|
||||
],
|
||||
"suggestions": ["改进建议1", "改进建议2"]
|
||||
}`
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// 执行对照检查
|
||||
const runCompare = async () => {
|
||||
if (!canCompare.value) return
|
||||
|
||||
const requirement = getSelectedLeftText()
|
||||
isComparing.value = true
|
||||
lastCheckResult.value = null
|
||||
|
||||
try {
|
||||
// 获取选中的右侧段落信息
|
||||
const selectedParagraphs = selectedRightIdxs.value.map(idx => ({
|
||||
idx,
|
||||
content: rightParagraphs.value[idx],
|
||||
typeInfo: paragraphTypes.value[idx] || null
|
||||
}))
|
||||
|
||||
// 构建段落级精确检查 Prompt
|
||||
const prompt = buildParagraphCheckPrompt(requirement, selectedParagraphs)
|
||||
|
||||
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
|
||||
|
||||
|
||||
// 保存到所有选中段落的检查结果
|
||||
selectedRightIdxs.value.forEach(idx => {
|
||||
checkResults.value[idx] = {
|
||||
|
||||
153
src/components/DocumentSelectorModal.vue
Normal file
153
src/components/DocumentSelectorModal.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div v-if="visible" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg w-[500px] max-h-[70vh] overflow-hidden border border-slate-600 flex flex-col">
|
||||
<!-- 头部 -->
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||||
<h3 class="text-lg font-bold text-white">📂 选择文稿</h3>
|
||||
<button @click="$emit('close')" class="text-slate-400 hover:text-white">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="px-4 py-2 border-b border-slate-700 flex gap-2 shrink-0">
|
||||
<button
|
||||
v-for="filter in statusFilters"
|
||||
:key="filter.value"
|
||||
@click="currentFilter = filter.value"
|
||||
:class="['text-xs px-2 py-1 rounded transition',
|
||||
currentFilter === filter.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600']"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 文稿列表 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-2 min-h-0">
|
||||
<div v-if="filteredDocuments.length === 0" class="text-center text-slate-500 py-8">
|
||||
<span class="text-4xl block mb-2">📄</span>
|
||||
<p class="text-sm">暂无文稿</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="doc in filteredDocuments"
|
||||
:key="doc.id"
|
||||
@click="selectDocument(doc)"
|
||||
:class="['p-3 rounded-lg border cursor-pointer transition',
|
||||
selectedDocId === doc.id
|
||||
? 'bg-blue-900/30 border-blue-500'
|
||||
: 'bg-slate-900/50 border-slate-700 hover:border-slate-500']"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-1">
|
||||
<h4 class="font-medium text-white text-sm truncate flex-1">{{ doc.title }}</h4>
|
||||
<span :class="['text-xs px-1.5 py-0.5 rounded ml-2',
|
||||
doc.status === 'draft' ? 'bg-yellow-900/50 text-yellow-300' :
|
||||
doc.status === 'published' ? 'bg-green-900/50 text-green-300' : 'bg-slate-700 text-slate-400']">
|
||||
{{ doc.status === 'draft' ? '草稿' : doc.status === 'published' ? '已发布' : '已归档' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 line-clamp-2">{{ doc.content?.substring(0, 100) || '无内容' }}...</p>
|
||||
<div class="flex items-center justify-between text-xs text-slate-600 mt-2">
|
||||
<span>{{ doc.word_count || doc.content?.length || 0 }} 字</span>
|
||||
<span>{{ formatDate(doc.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="p-4 border-t border-slate-700 flex gap-3 shrink-0">
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="confirmSelect"
|
||||
:disabled="!selectedDocId"
|
||||
class="flex-1 py-2 rounded bg-blue-600 text-white hover:bg-blue-500 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
确认选择
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'select'])
|
||||
|
||||
// 状态
|
||||
const documents = ref([])
|
||||
const selectedDocId = ref(null)
|
||||
const currentFilter = ref('all')
|
||||
|
||||
// 筛选选项
|
||||
const statusFilters = [
|
||||
{ value: 'all', label: '全部' },
|
||||
{ value: 'draft', label: '草稿' },
|
||||
{ value: 'published', label: '已发布' },
|
||||
{ value: 'archived', label: '已归档' }
|
||||
]
|
||||
|
||||
// 筛选后的文稿
|
||||
const filteredDocuments = computed(() => {
|
||||
if (currentFilter.value === 'all') {
|
||||
return documents.value
|
||||
}
|
||||
return documents.value.filter(d => d.status === currentFilter.value)
|
||||
})
|
||||
|
||||
// 加载文稿
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
const { getAllDocuments } = await import('../db/index.js')
|
||||
documents.value = getAllDocuments()
|
||||
} catch (error) {
|
||||
console.error('加载文稿失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择文稿
|
||||
const selectDocument = (doc) => {
|
||||
selectedDocId.value = selectedDocId.value === doc.id ? null : doc.id
|
||||
}
|
||||
|
||||
// 确认选择
|
||||
const confirmSelect = () => {
|
||||
const doc = documents.value.find(d => d.id === selectedDocId.value)
|
||||
if (doc) {
|
||||
emit('select', doc)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
// 监听可见性变化
|
||||
watch(() => props.visible, (val) => {
|
||||
if (val) {
|
||||
selectedDocId.value = null
|
||||
loadDocuments()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.line-clamp-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
273
src/components/DocumentsPanel.vue
Normal file
273
src/components/DocumentsPanel.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
|
||||
<!-- 头部 -->
|
||||
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<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>
|
||||
<button
|
||||
@click="switchPage('writer')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 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>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="filter in statusFilters"
|
||||
:key="filter.value"
|
||||
@click="currentFilter = filter.value"
|
||||
:class="['text-xs px-2 py-1 rounded transition',
|
||||
currentFilter === filter.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600']"
|
||||
>
|
||||
{{ filter.label }} ({{ getCountByStatus(filter.value) }})
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="createNewDocument"
|
||||
class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition"
|
||||
>
|
||||
+ 新建文稿
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 文稿列表 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-3 min-h-0">
|
||||
<div v-if="filteredDocuments.length === 0" class="text-center text-slate-500 py-8">
|
||||
<span class="text-4xl block mb-2">📄</span>
|
||||
<p class="text-sm">暂无文稿</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="doc in filteredDocuments"
|
||||
:key="doc.id"
|
||||
@click="selectDocument(doc)"
|
||||
:class="['p-3 rounded-lg border cursor-pointer transition',
|
||||
selectedDocId === doc.id
|
||||
? 'bg-blue-900/30 border-blue-500'
|
||||
: 'bg-slate-900/50 border-slate-700 hover:border-slate-500']"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-medium text-white text-sm truncate flex-1">{{ doc.title }}</h3>
|
||||
<span :class="['text-xs px-1.5 py-0.5 rounded', statusStyles[doc.status]]">
|
||||
{{ statusLabels[doc.status] }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 line-clamp-2 mb-2">
|
||||
{{ doc.content ? doc.content.substring(0, 100) + '...' : '空白文稿' }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between text-xs text-slate-600">
|
||||
<span>{{ doc.word_count || 0 }} 字</span>
|
||||
<span>{{ formatDate(doc.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div v-if="selectedDocId" class="p-4 border-t border-slate-700 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="openDocument"
|
||||
class="flex-1 text-xs py-2 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
|
||||
>
|
||||
📝 编辑
|
||||
</button>
|
||||
<button
|
||||
@click="duplicateDocument"
|
||||
class="flex-1 text-xs py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
📋 复制
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDelete"
|
||||
class="text-xs px-3 py-2 rounded bg-red-900/50 text-red-300 hover:bg-red-800/50 transition"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div v-if="showDeleteConfirm" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg p-6 w-80 border border-slate-600">
|
||||
<h3 class="text-lg font-bold text-white mb-4">确认删除</h3>
|
||||
<p class="text-sm text-slate-400 mb-6">确定要删除这篇文稿吗?此操作不可恢复。</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="showDeleteConfirm = false"
|
||||
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="deleteSelectedDocument"
|
||||
class="flex-1 py-2 rounded bg-red-600 text-white hover:bg-red-500 transition"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useDatabaseStore } from '../stores/database.js'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const dbStore = useDatabaseStore()
|
||||
|
||||
// 状态
|
||||
const selectedDocId = ref(null)
|
||||
const currentFilter = ref('all')
|
||||
const showDeleteConfirm = ref(false)
|
||||
|
||||
// 文稿数据
|
||||
const documents = ref([])
|
||||
|
||||
// 状态筛选选项
|
||||
const statusFilters = [
|
||||
{ label: '全部', value: 'all' },
|
||||
{ label: '草稿', value: 'draft' },
|
||||
{ label: '已发布', value: 'published' },
|
||||
{ label: '已归档', value: 'archived' }
|
||||
]
|
||||
|
||||
// 状态标签
|
||||
const statusLabels = {
|
||||
draft: '草稿',
|
||||
published: '已发布',
|
||||
archived: '已归档'
|
||||
}
|
||||
|
||||
// 状态样式
|
||||
const statusStyles = {
|
||||
draft: 'bg-yellow-900/50 text-yellow-300',
|
||||
published: 'bg-green-900/50 text-green-300',
|
||||
archived: 'bg-slate-700 text-slate-400'
|
||||
}
|
||||
|
||||
// 筛选后的文稿
|
||||
const filteredDocuments = computed(() => {
|
||||
if (currentFilter.value === 'all') {
|
||||
return documents.value
|
||||
}
|
||||
return documents.value.filter(doc => doc.status === currentFilter.value)
|
||||
})
|
||||
|
||||
// 按状态统计数量
|
||||
const getCountByStatus = (status) => {
|
||||
if (status === 'all') return documents.value.length
|
||||
return documents.value.filter(doc => doc.status === status).length
|
||||
}
|
||||
|
||||
// 加载文稿列表
|
||||
const loadDocuments = async () => {
|
||||
try {
|
||||
const { getAllDocuments } = await import('../db/index.js')
|
||||
documents.value = getAllDocuments()
|
||||
} catch (error) {
|
||||
console.error('加载文稿失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择文稿
|
||||
const selectDocument = (doc) => {
|
||||
selectedDocId.value = selectedDocId.value === doc.id ? null : doc.id
|
||||
}
|
||||
|
||||
// 创建新文稿
|
||||
const createNewDocument = async () => {
|
||||
try {
|
||||
const { createDocument } = await import('../db/index.js')
|
||||
const id = createDocument({
|
||||
title: '未命名文稿',
|
||||
content: '',
|
||||
status: 'draft'
|
||||
})
|
||||
await loadDocuments()
|
||||
selectedDocId.value = id
|
||||
} catch (error) {
|
||||
console.error('创建文稿失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑
|
||||
const openDocument = () => {
|
||||
if (!selectedDocId.value) return
|
||||
const doc = documents.value.find(d => d.id === selectedDocId.value)
|
||||
if (doc) {
|
||||
// 设置当前文稿到 store,让 MainContent 可以读取
|
||||
appStore.currentDocument = doc
|
||||
}
|
||||
}
|
||||
|
||||
// 复制文稿
|
||||
const duplicateDocument = async () => {
|
||||
if (!selectedDocId.value) return
|
||||
const doc = documents.value.find(d => d.id === selectedDocId.value)
|
||||
if (!doc) return
|
||||
|
||||
try {
|
||||
const { createDocument } = await import('../db/index.js')
|
||||
createDocument({
|
||||
title: doc.title + ' (副本)',
|
||||
content: doc.content,
|
||||
paradigmId: doc.paradigm_id,
|
||||
status: 'draft',
|
||||
tags: doc.tags
|
||||
})
|
||||
await loadDocuments()
|
||||
} catch (error) {
|
||||
console.error('复制文稿失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = () => {
|
||||
if (!selectedDocId.value) return
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
// 删除文稿
|
||||
const deleteSelectedDocument = async () => {
|
||||
if (!selectedDocId.value) return
|
||||
|
||||
try {
|
||||
const { deleteDocument } = await import('../db/index.js')
|
||||
deleteDocument(selectedDocId.value)
|
||||
selectedDocId.value = null
|
||||
showDeleteConfirm.value = false
|
||||
await loadDocuments()
|
||||
} catch (error) {
|
||||
console.error('删除文稿失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
const switchPage = (page) => {
|
||||
appStore.setCurrentPage(page)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDocuments()
|
||||
})
|
||||
</script>
|
||||
@@ -17,8 +17,9 @@
|
||||
<!-- 顶部工具栏 -->
|
||||
<header class="h-14 border-b border-slate-800 flex items-center justify-between px-6 bg-slate-950 shrink-0">
|
||||
<span class="text-sm font-medium text-slate-400">
|
||||
{{ currentPage === 'writer' ? '输出预览' : '范式分析结果' }}
|
||||
{{ headerTitle }}
|
||||
</span>
|
||||
<!-- 写作页面工具栏 -->
|
||||
<div class="flex items-center gap-4" v-if="currentPage === 'writer'">
|
||||
<span class="text-xs text-slate-600">
|
||||
字数: {{ generatedContent.length }} {{ isGenerating ? '(生成中...)' : '' }}
|
||||
@@ -32,6 +33,20 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 文稿页面工具栏 -->
|
||||
<div class="flex items-center gap-4" v-if="currentPage === 'documents' && currentDocument">
|
||||
<span class="text-xs text-slate-600">
|
||||
字数: {{ documentContent.length }}
|
||||
</span>
|
||||
<div class="flex gap-3">
|
||||
<button @click="saveDocument" class="text-xs text-green-400 hover:text-green-300 flex items-center gap-1 transition">
|
||||
💾 保存
|
||||
</button>
|
||||
<button @click="copyDocContent" class="text-xs text-slate-400 hover:text-white flex items-center gap-1 transition">
|
||||
📋 复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
@@ -142,6 +157,62 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文稿编辑页面内容 -->
|
||||
<div v-else-if="currentPage === 'documents'" class="h-full">
|
||||
<div v-if="!currentDocument" class="h-full flex flex-col items-center justify-center text-slate-700 mt-20">
|
||||
<span class="text-6xl mb-4 opacity-20">📄</span>
|
||||
<p>在左侧选择文稿进行编辑</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="h-full flex flex-col">
|
||||
<!-- 文稿标题 -->
|
||||
<div class="mb-4">
|
||||
<input
|
||||
v-model="documentTitle"
|
||||
class="w-full text-2xl font-bold bg-transparent border-b border-slate-700 pb-2 text-white outline-none focus:border-blue-500 transition"
|
||||
placeholder="文稿标题..."
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 文稿内容编辑器 -->
|
||||
<div class="flex-1 min-h-[400px]">
|
||||
<textarea
|
||||
v-model="documentContent"
|
||||
class="w-full h-full bg-slate-900/50 border border-slate-700 rounded-lg p-4 text-slate-200 outline-none focus:border-blue-500 resize-none font-mono text-sm leading-relaxed"
|
||||
placeholder="开始写作..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 文稿信息 -->
|
||||
<div class="mt-4 flex items-center justify-between text-xs text-slate-500">
|
||||
<div class="flex items-center gap-4">
|
||||
<span>状态: {{ currentDocument.status === 'draft' ? '草稿' : currentDocument.status === 'published' ? '已发布' : '已归档' }}</span>
|
||||
<span>创建: {{ formatDate(currentDocument.created_at) }}</span>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="changeDocStatus('draft')"
|
||||
:class="['px-2 py-1 rounded', currentDocument.status === 'draft' ? 'bg-yellow-900/50 text-yellow-300' : 'bg-slate-700 text-slate-400 hover:bg-slate-600']"
|
||||
>
|
||||
草稿
|
||||
</button>
|
||||
<button
|
||||
@click="changeDocStatus('published')"
|
||||
:class="['px-2 py-1 rounded', currentDocument.status === 'published' ? 'bg-green-900/50 text-green-300' : 'bg-slate-700 text-slate-400 hover:bg-slate-600']"
|
||||
>
|
||||
发布
|
||||
</button>
|
||||
<button
|
||||
@click="changeDocStatus('archived')"
|
||||
:class="['px-2 py-1 rounded', currentDocument.status === 'archived' ? 'bg-slate-600 text-slate-300' : 'bg-slate-700 text-slate-400 hover:bg-slate-600']"
|
||||
>
|
||||
归档
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 范式分析页面内容 -->
|
||||
<div v-else class="space-y-8">
|
||||
<div v-if="!analysisResult" class="h-full flex flex-col items-center justify-center text-slate-700 mt-20">
|
||||
@@ -218,7 +289,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { buildPrompt } from '../utils/promptBuilder.js'
|
||||
@@ -241,10 +312,91 @@ const {
|
||||
} = storeToRefs(appStore)
|
||||
|
||||
// AI 思考过程折叠状态
|
||||
import { ref } from 'vue'
|
||||
const isThinkingExpanded = ref(false)
|
||||
const isQualityReportExpanded = ref(true) // 质检报告默认展开
|
||||
|
||||
// 文稿编辑状态
|
||||
const currentDocument = ref(null)
|
||||
const documentTitle = ref('')
|
||||
const documentContent = ref('')
|
||||
|
||||
// 头部标题
|
||||
const headerTitle = computed(() => {
|
||||
const titles = {
|
||||
'writer': '输出预览',
|
||||
'analysis': '范式分析结果',
|
||||
'documents': currentDocument.value ? `编辑: ${currentDocument.value.title}` : '文稿编辑',
|
||||
'materials': '素材详情',
|
||||
'settings': '设置'
|
||||
}
|
||||
return titles[currentPage.value] || ''
|
||||
})
|
||||
|
||||
// 监听当前文稿变化(从 DocumentsPanel 传入)
|
||||
watch(() => appStore.currentDocument, (doc) => {
|
||||
if (doc) {
|
||||
currentDocument.value = doc
|
||||
documentTitle.value = doc.title || ''
|
||||
documentContent.value = doc.content || ''
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 保存文稿
|
||||
const saveDocument = async () => {
|
||||
if (!currentDocument.value) return
|
||||
|
||||
try {
|
||||
const { updateDocument } = await import('../db/index.js')
|
||||
updateDocument(currentDocument.value.id, {
|
||||
title: documentTitle.value,
|
||||
content: documentContent.value
|
||||
})
|
||||
currentDocument.value.title = documentTitle.value
|
||||
currentDocument.value.content = documentContent.value
|
||||
alert('保存成功')
|
||||
} catch (error) {
|
||||
console.error('保存失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制文稿内容
|
||||
const copyDocContent = () => {
|
||||
navigator.clipboard.writeText(documentContent.value)
|
||||
alert('已复制到剪贴板')
|
||||
}
|
||||
|
||||
// 修改文稿状态
|
||||
const changeDocStatus = async (status) => {
|
||||
if (!currentDocument.value) return
|
||||
|
||||
try {
|
||||
const { updateDocument } = await import('../db/index.js')
|
||||
updateDocument(currentDocument.value.id, { status })
|
||||
currentDocument.value.status = status
|
||||
} catch (error) {
|
||||
console.error('修改状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// 暴露设置当前文稿的方法(供 DocumentsPanel 调用)
|
||||
const setCurrentDocument = (doc) => {
|
||||
currentDocument.value = doc
|
||||
if (doc) {
|
||||
documentTitle.value = doc.title || ''
|
||||
documentContent.value = doc.content || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给父组件
|
||||
defineExpose({ setCurrentDocument })
|
||||
|
||||
// 范式定义
|
||||
const paradigms = [
|
||||
{
|
||||
|
||||
117
src/components/MaterialSelectorModal.vue
Normal file
117
src/components/MaterialSelectorModal.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||
@click.self="$emit('close')"
|
||||
>
|
||||
<div class="bg-slate-800 rounded-lg w-[600px] max-h-[80vh] flex flex-col">
|
||||
<!-- 头部 -->
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h3 class="text-lg font-medium text-white">选择素材</h3>
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="text-slate-400 hover:text-white"
|
||||
>✕</button>
|
||||
</div>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div class="p-3 border-b border-slate-700 flex gap-2">
|
||||
<button
|
||||
v-for="type in materialTypes"
|
||||
:key="type.id"
|
||||
@click="selectedType = type.id"
|
||||
:class="['text-xs px-3 py-1 rounded transition',
|
||||
selectedType === type.id
|
||||
? 'bg-amber-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600']"
|
||||
>{{ type.label }}</button>
|
||||
</div>
|
||||
|
||||
<!-- 素材列表 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
<div
|
||||
v-for="material in filteredMaterials"
|
||||
:key="material.id"
|
||||
@click="selectMaterial(material)"
|
||||
:class="['p-3 rounded-lg border cursor-pointer transition',
|
||||
selectedMaterial?.id === material.id
|
||||
? 'bg-amber-900/30 border-amber-500'
|
||||
: 'bg-slate-900 border-slate-700 hover:border-amber-500/50']"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm font-medium text-white">{{ material.title }}</span>
|
||||
<span class="text-xs text-slate-500">{{ material.type }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 line-clamp-2">
|
||||
{{ getMaterialPreview(material) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="filteredMaterials.length === 0" class="text-center py-8">
|
||||
<span class="text-slate-500 text-sm">暂无素材</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作 -->
|
||||
<div class="p-4 border-t border-slate-700 flex justify-end gap-2">
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="px-4 py-2 text-sm bg-slate-700 text-slate-300 rounded hover:bg-slate-600"
|
||||
>取消</button>
|
||||
<button
|
||||
@click="confirmSelect"
|
||||
:disabled="!selectedMaterial"
|
||||
class="px-4 py-2 text-sm bg-amber-600 text-white rounded hover:bg-amber-500 disabled:opacity-50"
|
||||
>确认选择</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useDatabaseStore } from '../stores/database'
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close', 'select'])
|
||||
|
||||
const dbStore = useDatabaseStore()
|
||||
|
||||
const selectedType = ref('all')
|
||||
const selectedMaterial = ref(null)
|
||||
|
||||
const materialTypes = [
|
||||
{ id: 'all', label: '全部' },
|
||||
{ id: 'policy', label: '政策文件' },
|
||||
{ id: 'speech', label: '领导讲话' },
|
||||
{ id: 'case', label: '典型案例' },
|
||||
{ id: 'reference', label: '参考范文' }
|
||||
]
|
||||
|
||||
const filteredMaterials = computed(() => {
|
||||
const materials = dbStore.references || []
|
||||
if (selectedType.value === 'all') return materials
|
||||
return materials.filter(m => m.type === selectedType.value)
|
||||
})
|
||||
|
||||
const getMaterialPreview = (material) => {
|
||||
if (material.excerpts?.length > 0) {
|
||||
return material.excerpts[0].content?.substring(0, 100) + '...'
|
||||
}
|
||||
return '暂无内容预览'
|
||||
}
|
||||
|
||||
const selectMaterial = (material) => {
|
||||
selectedMaterial.value = material
|
||||
}
|
||||
|
||||
const confirmSelect = () => {
|
||||
if (selectedMaterial.value) {
|
||||
emit('select', selectedMaterial.value)
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
506
src/components/MaterialsPanel.vue
Normal file
506
src/components/MaterialsPanel.vue
Normal file
@@ -0,0 +1,506 @@
|
||||
<template>
|
||||
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
|
||||
<!-- 头部 -->
|
||||
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<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>
|
||||
<button
|
||||
@click="switchPage('writer')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 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>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<div class="flex gap-1 flex-wrap">
|
||||
<button
|
||||
v-for="filter in typeFilters"
|
||||
:key="filter.value"
|
||||
@click="currentFilter = filter.value"
|
||||
:class="['text-xs px-2 py-1 rounded transition',
|
||||
currentFilter === filter.value
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-slate-700 text-slate-300 hover:bg-slate-600']"
|
||||
>
|
||||
{{ filter.icon }} {{ filter.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="openAddModal"
|
||||
class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition"
|
||||
>
|
||||
+ 新增
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 素材列表 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-3 min-h-0">
|
||||
<div v-if="filteredMaterials.length === 0" class="text-center text-slate-500 py-8">
|
||||
<span class="text-4xl block mb-2">📄</span>
|
||||
<p class="text-sm">暂无素材</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="material in filteredMaterials"
|
||||
:key="material.id"
|
||||
@click="selectMaterial(material)"
|
||||
:class="['p-3 rounded-lg border cursor-pointer transition',
|
||||
selectedId === material.id
|
||||
? 'bg-blue-900/30 border-blue-500'
|
||||
: 'bg-slate-900/50 border-slate-700 hover:border-slate-500']"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<h3 class="font-medium text-white text-sm truncate flex-1">
|
||||
{{ getTypeIcon(material.type) }} {{ material.title }}
|
||||
</h3>
|
||||
<span v-if="material.is_default" class="text-xs px-1.5 py-0.5 rounded bg-slate-700 text-slate-400">
|
||||
预置
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500 mb-2">{{ material.source || '无来源' }}</p>
|
||||
<div class="flex items-center justify-between text-xs text-slate-600">
|
||||
<span>{{ material.excerpts?.length || 0 }} 条摘录</span>
|
||||
<div class="flex gap-1">
|
||||
<span
|
||||
v-for="tag in (material.tags || []).slice(0, 2)"
|
||||
:key="tag"
|
||||
class="px-1.5 py-0.5 rounded bg-slate-800 text-slate-500"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<div v-if="selectedId" class="p-4 border-t border-slate-700 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="openEditModal"
|
||||
class="flex-1 text-xs py-2 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
|
||||
>
|
||||
✏️ 编辑
|
||||
</button>
|
||||
<button
|
||||
@click="viewExcerpts"
|
||||
class="flex-1 text-xs py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
📋 查看摘录
|
||||
</button>
|
||||
<button
|
||||
v-if="!selectedMaterial?.is_default"
|
||||
@click="confirmDelete"
|
||||
class="text-xs px-3 py-2 rounded bg-red-900/50 text-red-300 hover:bg-red-800/50 transition"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<div v-if="showEditModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg w-[500px] max-h-[80vh] overflow-hidden border border-slate-600 flex flex-col">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold text-white">{{ isAddMode ? '新增素材' : '编辑素材' }}</h3>
|
||||
<button @click="closeEditModal" class="text-slate-400 hover:text-white">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
<!-- 素材类型 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-2">素材类型</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="type in materialTypes"
|
||||
:key="type.value"
|
||||
@click="editForm.type = type.value"
|
||||
:class="['text-xs px-3 py-1.5 rounded border transition',
|
||||
editForm.type === type.value
|
||||
? 'bg-blue-600 border-blue-500 text-white'
|
||||
: 'bg-slate-900 border-slate-700 text-slate-300 hover:border-slate-500']"
|
||||
>
|
||||
{{ type.icon }} {{ type.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标题 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-2">素材标题</label>
|
||||
<input
|
||||
v-model="editForm.title"
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm text-white outline-none focus:border-blue-500"
|
||||
placeholder="如:二十届四中全会精神要点"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 来源 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-2">来源</label>
|
||||
<input
|
||||
v-model="editForm.source"
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm text-white outline-none focus:border-blue-500"
|
||||
placeholder="如:人民日报 2024-10-15"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-2">标签(逗号分隔)</label>
|
||||
<input
|
||||
v-model="editForm.tagsInput"
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm text-white outline-none focus:border-blue-500"
|
||||
placeholder="如:全面深化改革, 制度建设"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 关联维度集 -->
|
||||
<div>
|
||||
<label class="block text-xs text-slate-400 mb-2">关联维度集</label>
|
||||
<select
|
||||
v-model="editForm.dimensionSetId"
|
||||
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm text-white outline-none focus:border-blue-500"
|
||||
>
|
||||
<option value="">不关联</option>
|
||||
<option v-for="ds in dimensionSets" :key="ds.id" :value="ds.id">
|
||||
{{ ds.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 摘录管理 -->
|
||||
<div class="border-t border-slate-700 pt-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-xs text-slate-400">可引用摘录</label>
|
||||
<button
|
||||
@click="addExcerpt"
|
||||
class="text-xs text-blue-400 hover:text-blue-300"
|
||||
>
|
||||
+ 添加摘录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 max-h-60 overflow-y-auto">
|
||||
<div
|
||||
v-for="(excerpt, index) in editForm.excerpts"
|
||||
:key="index"
|
||||
class="bg-slate-900/50 rounded p-3 border border-slate-700"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<input
|
||||
v-model="excerpt.topic"
|
||||
class="flex-1 bg-transparent border-b border-slate-600 text-sm text-white outline-none focus:border-blue-500 pb-1"
|
||||
placeholder="摘录主题"
|
||||
>
|
||||
<button
|
||||
@click="removeExcerpt(index)"
|
||||
class="text-red-400 hover:text-red-300 ml-2"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="excerpt.content"
|
||||
class="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-xs text-slate-300 outline-none focus:border-blue-500 resize-none"
|
||||
rows="2"
|
||||
placeholder="摘录内容..."
|
||||
></textarea>
|
||||
<div class="flex items-center gap-2 mt-2">
|
||||
<select
|
||||
v-model="excerpt.useFor"
|
||||
class="text-xs bg-slate-800 border border-slate-700 rounded px-2 py-1 text-slate-300 outline-none"
|
||||
>
|
||||
<option value="">通用</option>
|
||||
<option value="positive">正面案例</option>
|
||||
<option value="negative">反面案例</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-slate-700 flex gap-3">
|
||||
<button
|
||||
@click="closeEditModal"
|
||||
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="saveMaterial"
|
||||
class="flex-1 py-2 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
|
||||
>
|
||||
{{ isAddMode ? '添加' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 摘录查看弹窗 -->
|
||||
<div v-if="showExcerptsModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg w-[500px] max-h-[70vh] overflow-hidden border border-slate-600 flex flex-col">
|
||||
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold text-white">{{ selectedMaterial?.title }} - 摘录</h3>
|
||||
<button @click="showExcerptsModal = false" class="text-slate-400 hover:text-white">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-3">
|
||||
<div
|
||||
v-for="(excerpt, index) in selectedMaterial?.excerpts || []"
|
||||
:key="index"
|
||||
class="bg-slate-900/50 rounded p-3 border border-slate-700"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="font-medium text-white text-sm">{{ excerpt.topic }}</span>
|
||||
<span
|
||||
v-if="excerpt.useFor"
|
||||
:class="['text-xs px-1.5 py-0.5 rounded',
|
||||
excerpt.useFor === 'positive' ? 'bg-green-900/50 text-green-300' : 'bg-red-900/50 text-red-300']"
|
||||
>
|
||||
{{ excerpt.useFor === 'positive' ? '正面' : '反面' }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 leading-relaxed">{{ excerpt.content }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="!selectedMaterial?.excerpts?.length" class="text-center text-slate-500 py-4">
|
||||
暂无摘录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<div v-if="showDeleteConfirm" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg p-6 w-80 border border-slate-600">
|
||||
<h3 class="text-lg font-bold text-white mb-4">确认删除</h3>
|
||||
<p class="text-sm text-slate-400 mb-6">确定要删除这条素材吗?此操作不可恢复。</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="showDeleteConfirm = false"
|
||||
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="deleteMaterial"
|
||||
class="flex-1 py-2 rounded bg-red-600 text-white hover:bg-red-500 transition"
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { REFERENCE_TYPES } from '../config/references.js'
|
||||
import { getDimensionSetList } from '../config/dimensionSets.js'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 状态
|
||||
const selectedId = ref(null)
|
||||
const currentFilter = ref('all')
|
||||
const showEditModal = ref(false)
|
||||
const showExcerptsModal = ref(false)
|
||||
const showDeleteConfirm = ref(false)
|
||||
const isAddMode = ref(false)
|
||||
|
||||
// 素材数据
|
||||
const materials = ref([])
|
||||
|
||||
// 维度集列表
|
||||
const dimensionSets = getDimensionSetList()
|
||||
|
||||
// 素材类型选项
|
||||
const materialTypes = [
|
||||
{ value: REFERENCE_TYPES.POLICY, label: '政策文件', icon: '📄' },
|
||||
{ value: REFERENCE_TYPES.SPEECH, label: '领导讲话', icon: '🎙️' },
|
||||
{ value: REFERENCE_TYPES.CASE, label: '典型案例', icon: '📌' },
|
||||
{ value: REFERENCE_TYPES.QUOTE, label: '金句警句', icon: '✨' },
|
||||
{ value: REFERENCE_TYPES.REGULATION, label: '党规党纪', icon: '📖' }
|
||||
]
|
||||
|
||||
// 类型筛选选项
|
||||
const typeFilters = [
|
||||
{ value: 'all', label: '全部', icon: '📚' },
|
||||
...materialTypes
|
||||
]
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
id: null,
|
||||
type: REFERENCE_TYPES.POLICY,
|
||||
title: '',
|
||||
source: '',
|
||||
tagsInput: '',
|
||||
dimensionSetId: '',
|
||||
excerpts: []
|
||||
})
|
||||
|
||||
// 获取选中的素材
|
||||
const selectedMaterial = computed(() => {
|
||||
return materials.value.find(m => m.id === selectedId.value)
|
||||
})
|
||||
|
||||
// 筛选后的素材
|
||||
const filteredMaterials = computed(() => {
|
||||
if (currentFilter.value === 'all') {
|
||||
return materials.value
|
||||
}
|
||||
return materials.value.filter(m => m.type === currentFilter.value)
|
||||
})
|
||||
|
||||
// 获取类型图标
|
||||
const getTypeIcon = (type) => {
|
||||
const found = materialTypes.find(t => t.value === type)
|
||||
return found?.icon || '📄'
|
||||
}
|
||||
|
||||
// 加载素材列表
|
||||
const loadMaterials = async () => {
|
||||
try {
|
||||
const { getAllReferences } = await import('../db/index.js')
|
||||
materials.value = getAllReferences()
|
||||
} catch (error) {
|
||||
console.error('加载素材失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 选择素材
|
||||
const selectMaterial = (material) => {
|
||||
selectedId.value = selectedId.value === material.id ? null : material.id
|
||||
}
|
||||
|
||||
// 打开新增弹窗
|
||||
const openAddModal = () => {
|
||||
isAddMode.value = true
|
||||
editForm.id = null
|
||||
editForm.type = REFERENCE_TYPES.POLICY
|
||||
editForm.title = ''
|
||||
editForm.source = ''
|
||||
editForm.tagsInput = ''
|
||||
editForm.dimensionSetId = ''
|
||||
editForm.excerpts = []
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
// 打开编辑弹窗
|
||||
const openEditModal = () => {
|
||||
if (!selectedMaterial.value) return
|
||||
|
||||
isAddMode.value = false
|
||||
const m = selectedMaterial.value
|
||||
editForm.id = m.id
|
||||
editForm.type = m.type
|
||||
editForm.title = m.title
|
||||
editForm.source = m.source || ''
|
||||
editForm.tagsInput = (m.tags || []).join(', ')
|
||||
editForm.dimensionSetId = m.relatedDimensionSets?.[0] || ''
|
||||
editForm.excerpts = (m.excerpts || []).map(e => ({
|
||||
topic: e.topic,
|
||||
content: e.content,
|
||||
useFor: e.useFor || ''
|
||||
}))
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
// 关闭编辑弹窗
|
||||
const closeEditModal = () => {
|
||||
showEditModal.value = false
|
||||
}
|
||||
|
||||
// 添加摘录
|
||||
const addExcerpt = () => {
|
||||
editForm.excerpts.push({
|
||||
topic: '',
|
||||
content: '',
|
||||
useFor: ''
|
||||
})
|
||||
}
|
||||
|
||||
// 删除摘录
|
||||
const removeExcerpt = (index) => {
|
||||
editForm.excerpts.splice(index, 1)
|
||||
}
|
||||
|
||||
// 保存素材
|
||||
const saveMaterial = async () => {
|
||||
if (!editForm.title.trim()) {
|
||||
alert('请输入素材标题')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const { addReference, updateReference } = await import('../db/index.js')
|
||||
|
||||
const materialData = {
|
||||
type: editForm.type,
|
||||
title: editForm.title.trim(),
|
||||
source: editForm.source.trim(),
|
||||
tags: editForm.tagsInput.split(',').map(t => t.trim()).filter(Boolean),
|
||||
relatedDimensionSets: editForm.dimensionSetId ? [editForm.dimensionSetId] : [],
|
||||
excerpts: editForm.excerpts.filter(e => e.topic && e.content)
|
||||
}
|
||||
|
||||
if (isAddMode.value) {
|
||||
addReference(materialData)
|
||||
} else {
|
||||
updateReference(editForm.id, materialData)
|
||||
}
|
||||
|
||||
await loadMaterials()
|
||||
closeEditModal()
|
||||
} catch (error) {
|
||||
console.error('保存素材失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看摘录
|
||||
const viewExcerpts = () => {
|
||||
if (!selectedMaterial.value) return
|
||||
showExcerptsModal.value = true
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
const confirmDelete = () => {
|
||||
if (!selectedId.value) return
|
||||
showDeleteConfirm.value = true
|
||||
}
|
||||
|
||||
// 删除素材
|
||||
const deleteMaterial = async () => {
|
||||
if (!selectedId.value) return
|
||||
|
||||
try {
|
||||
const { deleteReference } = await import('../db/index.js')
|
||||
deleteReference(selectedId.value)
|
||||
selectedId.value = null
|
||||
showDeleteConfirm.value = false
|
||||
await loadMaterials()
|
||||
} catch (error) {
|
||||
console.error('删除素材失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
const switchPage = (page) => {
|
||||
appStore.setCurrentPage(page)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMaterials()
|
||||
})
|
||||
</script>
|
||||
317
src/components/SettingsPanel.vue
Normal file
317
src/components/SettingsPanel.vue
Normal file
@@ -0,0 +1,317 @@
|
||||
<template>
|
||||
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
|
||||
<!-- 头部 -->
|
||||
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<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>
|
||||
<button
|
||||
@click="switchPage('writer')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 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>
|
||||
|
||||
<!-- 设置内容 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6 min-h-0">
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<section class="bg-slate-900/50 rounded-lg p-4 border border-slate-700">
|
||||
<h3 class="text-sm font-medium text-slate-300 mb-4 flex items-center gap-2">
|
||||
<span>📊</span> 数据统计
|
||||
</h3>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-slate-800 rounded p-3 text-center">
|
||||
<div class="text-2xl font-bold text-blue-400">{{ stats.documents }}</div>
|
||||
<div class="text-xs text-slate-500">文稿数量</div>
|
||||
</div>
|
||||
<div class="bg-slate-800 rounded p-3 text-center">
|
||||
<div class="text-2xl font-bold text-green-400">{{ stats.materials }}</div>
|
||||
<div class="text-xs text-slate-500">素材数量</div>
|
||||
</div>
|
||||
<div class="bg-slate-800 rounded p-3 text-center">
|
||||
<div class="text-2xl font-bold text-purple-400">{{ stats.paradigms }}</div>
|
||||
<div class="text-xs text-slate-500">范式数量</div>
|
||||
</div>
|
||||
<div class="bg-slate-800 rounded p-3 text-center">
|
||||
<div class="text-2xl font-bold text-amber-400">{{ formatSize(stats.dbSize) }}</div>
|
||||
<div class="text-xs text-slate-500">数据库大小</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 素材库管理 -->
|
||||
<section class="bg-slate-900/50 rounded-lg p-4 border border-slate-700">
|
||||
<h3 class="text-sm font-medium text-slate-300 mb-4 flex items-center gap-2">
|
||||
<span>📚</span> 素材库管理
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
@click="openMaterialsEditor"
|
||||
class="w-full text-left text-xs py-2 px-3 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 transition flex items-center justify-between"
|
||||
>
|
||||
<span>管理素材库</span>
|
||||
<span class="text-slate-500">{{ stats.materials }} 条</span>
|
||||
</button>
|
||||
<button
|
||||
@click="importMaterials"
|
||||
class="w-full text-left text-xs py-2 px-3 rounded bg-slate-800 text-slate-300 hover:bg-slate-700 transition"
|
||||
>
|
||||
📥 导入素材 (JSON)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 数据备份 -->
|
||||
<section class="bg-slate-900/50 rounded-lg p-4 border border-slate-700">
|
||||
<h3 class="text-sm font-medium text-slate-300 mb-4 flex items-center gap-2">
|
||||
<span>💾</span> 数据备份与恢复
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
@click="exportData"
|
||||
class="w-full text-xs py-2.5 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
|
||||
>
|
||||
📤 导出所有数据 (JSON)
|
||||
</button>
|
||||
<button
|
||||
@click="triggerImport"
|
||||
class="w-full text-xs py-2.5 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
📥 导入数据
|
||||
</button>
|
||||
<input
|
||||
ref="importInput"
|
||||
type="file"
|
||||
accept=".json"
|
||||
class="hidden"
|
||||
@change="handleImport"
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 危险操作 -->
|
||||
<section class="bg-red-950/30 rounded-lg p-4 border border-red-900/50">
|
||||
<h3 class="text-sm font-medium text-red-400 mb-4 flex items-center gap-2">
|
||||
<span>⚠️</span> 危险操作
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
@click="confirmClearDocuments"
|
||||
class="w-full text-xs py-2 rounded bg-red-900/50 text-red-300 hover:bg-red-800/50 transition"
|
||||
>
|
||||
清空所有文稿
|
||||
</button>
|
||||
<button
|
||||
@click="confirmResetDatabase"
|
||||
class="w-full text-xs py-2 rounded bg-red-900/50 text-red-300 hover:bg-red-800/50 transition"
|
||||
>
|
||||
重置数据库(恢复默认)
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 关于 -->
|
||||
<section class="bg-slate-900/50 rounded-lg p-4 border border-slate-700">
|
||||
<h3 class="text-sm font-medium text-slate-300 mb-3 flex items-center gap-2">
|
||||
<span>ℹ️</span> 关于
|
||||
</h3>
|
||||
<div class="text-xs text-slate-500 space-y-1">
|
||||
<p>AI 写作工坊 v1.0.0</p>
|
||||
<p>数据存储:浏览器 IndexedDB (SQLite)</p>
|
||||
<p>© 2026 AI Writing Workshop</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 确认弹窗 -->
|
||||
<div v-if="showConfirm" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-800 rounded-lg p-6 w-80 border border-slate-600">
|
||||
<h3 class="text-lg font-bold text-white mb-4">{{ confirmTitle }}</h3>
|
||||
<p class="text-sm text-slate-400 mb-6">{{ confirmMessage }}</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
@click="showConfirm = false"
|
||||
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="executeConfirmAction"
|
||||
class="flex-1 py-2 rounded bg-red-600 text-white hover:bg-red-500 transition"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成功提示 -->
|
||||
<div v-if="showToast" class="fixed bottom-4 right-4 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg z-50">
|
||||
{{ toastMessage }}
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useAppStore } from '../stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
// 状态
|
||||
const stats = reactive({
|
||||
documents: 0,
|
||||
materials: 0,
|
||||
paradigms: 0,
|
||||
dbSize: 0
|
||||
})
|
||||
|
||||
const showConfirm = ref(false)
|
||||
const confirmTitle = ref('')
|
||||
const confirmMessage = ref('')
|
||||
const confirmAction = ref(null)
|
||||
|
||||
const showToast = ref(false)
|
||||
const toastMessage = ref('')
|
||||
|
||||
const importInput = ref(null)
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const { getAllDocuments, getAllReferences, getAllParadigms, exportDatabase } = await import('../db/index.js')
|
||||
|
||||
stats.documents = getAllDocuments().length
|
||||
stats.materials = getAllReferences().length
|
||||
stats.paradigms = getAllParadigms().length
|
||||
|
||||
const dbData = exportDatabase()
|
||||
stats.dbSize = dbData.length
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化大小
|
||||
const formatSize = (bytes) => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = async () => {
|
||||
try {
|
||||
const { exportAsJSON } = await import('../db/index.js')
|
||||
const data = exportAsJSON()
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `ai-writer-backup-${new Date().toISOString().split('T')[0]}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
showToastMessage('数据导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 触发导入
|
||||
const triggerImport = () => {
|
||||
importInput.value?.click()
|
||||
}
|
||||
|
||||
// 处理导入
|
||||
const handleImport = async (event) => {
|
||||
const file = event.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
try {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
|
||||
// TODO: 实现导入逻辑
|
||||
console.log('导入数据:', data)
|
||||
showToastMessage('数据导入成功')
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开素材编辑器
|
||||
const openMaterialsEditor = () => {
|
||||
// TODO: 打开素材管理弹窗
|
||||
showToastMessage('素材管理功能开发中...')
|
||||
}
|
||||
|
||||
// 导入素材
|
||||
const importMaterials = () => {
|
||||
// TODO: 导入素材
|
||||
showToastMessage('素材导入功能开发中...')
|
||||
}
|
||||
|
||||
// 确认清空文稿
|
||||
const confirmClearDocuments = () => {
|
||||
confirmTitle.value = '清空所有文稿'
|
||||
confirmMessage.value = '确定要删除所有文稿吗?此操作不可恢复!'
|
||||
confirmAction.value = 'clearDocuments'
|
||||
showConfirm.value = true
|
||||
}
|
||||
|
||||
// 确认重置数据库
|
||||
const confirmResetDatabase = () => {
|
||||
confirmTitle.value = '重置数据库'
|
||||
confirmMessage.value = '确定要重置数据库吗?所有数据将被删除并恢复默认设置!'
|
||||
confirmAction.value = 'resetDatabase'
|
||||
showConfirm.value = true
|
||||
}
|
||||
|
||||
// 执行确认操作
|
||||
const executeConfirmAction = async () => {
|
||||
showConfirm.value = false
|
||||
|
||||
try {
|
||||
if (confirmAction.value === 'clearDocuments') {
|
||||
const { query, execute } = await import('../db/index.js')
|
||||
execute('DELETE FROM document_versions')
|
||||
execute('DELETE FROM documents')
|
||||
showToastMessage('文稿已清空')
|
||||
} else if (confirmAction.value === 'resetDatabase') {
|
||||
const { resetDatabase } = await import('../db/index.js')
|
||||
await resetDatabase()
|
||||
showToastMessage('数据库已重置')
|
||||
}
|
||||
|
||||
await loadStats()
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 显示提示
|
||||
const showToastMessage = (message) => {
|
||||
toastMessage.value = message
|
||||
showToast.value = true
|
||||
setTimeout(() => {
|
||||
showToast.value = false
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 切换页面
|
||||
const switchPage = (page) => {
|
||||
appStore.setCurrentPage(page)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
@@ -12,12 +12,30 @@
|
||||
>
|
||||
写作范式
|
||||
</button>
|
||||
<button
|
||||
@click="switchPage('documents')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
📂 文稿
|
||||
</button>
|
||||
<button
|
||||
@click="switchPage('materials')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||||
>
|
||||
📚 素材
|
||||
</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>
|
||||
<button
|
||||
@click="switchPage('settings')"
|
||||
class="text-xs px-2 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 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>
|
||||
|
||||
Reference in New Issue
Block a user