feat: 重构布局与模型配置
- 写作范式分析页面改为左中右三栏布局 - 范式分段写作页面改为左中右三栏布局 - 模型设置移至设置中心,支持多服务商选择 - API 配置通过 .env 文件管理,提升安全性 - 支持 DeepSeek、OpenAI、Claude、自定义服务商
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
<!-- 左侧/中间配置侧边栏 -->
|
||||
<WriterPanel v-if="currentPage === 'writer'" />
|
||||
<AnalysisPanel v-else-if="currentPage === 'analysis'" />
|
||||
<ParadigmWriterPanel v-else-if="currentPage === 'paradigmWriter'" />
|
||||
<DocumentsPanel
|
||||
v-else-if="currentPage === 'documents'"
|
||||
@toggle-version-panel="toggleVersionPanel"
|
||||
@@ -58,6 +59,7 @@ import ComparePanel from './components/ComparePanel.vue'
|
||||
import DocumentVersionPanel from './components/DocumentVersionPanel.vue'
|
||||
import DiffAnnotationPanel from './components/DiffAnnotationPanel.vue'
|
||||
import ArticleRewritePanel from './components/ArticleRewritePanel.vue'
|
||||
import ParadigmWriterPanel from './components/ParadigmWriterPanel.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const currentPage = computed(() => appStore.currentPage)
|
||||
|
||||
@@ -8,22 +8,47 @@
|
||||
<span class="badge badge-primary">Pro版</span>
|
||||
</header>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="analysis-content">
|
||||
<!-- 写作范式库 -->
|
||||
<section class="analysis-section">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-sm font-medium text-secondary">📚 写作范式库</h3>
|
||||
<button
|
||||
@click="openAddModal"
|
||||
class="btn btn-analysis btn-success flex items-center gap-1"
|
||||
>
|
||||
<span>+</span> 新增范式
|
||||
</button>
|
||||
</div>
|
||||
<!-- 标签页切换 -->
|
||||
<div class="analysis-tabs">
|
||||
<button
|
||||
:class="['analysis-tab', activeTab === 'paradigms' ? 'active' : '']"
|
||||
@click="activeTab = 'paradigms'"
|
||||
>
|
||||
📚 范式库
|
||||
</button>
|
||||
<button
|
||||
:class="['analysis-tab', activeTab === 'tools' ? 'active' : '']"
|
||||
@click="activeTab = 'tools'"
|
||||
>
|
||||
🔍 分析工具
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
<!-- 内容区 -->
|
||||
<div class="analysis-content">
|
||||
<!-- 写作范式库 -->
|
||||
<section v-show="activeTab === 'paradigms'" class="analysis-section">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<span class="text-xs text-muted">{{ paradigms.length }} 个范式</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
@click="openRequirementParser"
|
||||
class="btn btn-analysis btn-primary flex items-center gap-1"
|
||||
title="根据需求文档自动提取范式"
|
||||
>
|
||||
<span>🎯</span> AI 提取
|
||||
</button>
|
||||
<button
|
||||
@click="openAddModal"
|
||||
class="btn btn-analysis btn-success flex items-center gap-1"
|
||||
>
|
||||
<span>+</span> 新增
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="paradigm in paradigms"
|
||||
:key="paradigm.id"
|
||||
@click="selectParadigm(paradigm)"
|
||||
@@ -33,7 +58,10 @@
|
||||
<h4 class="paradigm-card-title">
|
||||
{{ paradigm.icon }} {{ paradigm.name }}
|
||||
<span v-if="paradigm.isNew" class="badge-new animate-pulse">NEW</span>
|
||||
<span v-if="paradigm.isCustom" class="badge-custom">自定义</span>
|
||||
<span v-if="isCustomParadigm(paradigm)" class="badge-custom">自定义</span>
|
||||
<span v-if="paradigm.createdAt" class="text-[10px] text-muted ml-auto opacity-70">
|
||||
{{ formatDate(paradigm.createdAt) }}
|
||||
</span>
|
||||
</h4>
|
||||
<div class="flex items-center gap-1">
|
||||
<button
|
||||
@@ -44,7 +72,7 @@
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
v-if="paradigm.isCustom"
|
||||
v-if="isCustomParadigm(paradigm)"
|
||||
@click.stop="deleteParadigm(paradigm)"
|
||||
class="btn-action danger"
|
||||
title="删除范式"
|
||||
@@ -54,7 +82,7 @@
|
||||
<button
|
||||
v-if="selectedParadigm?.id === paradigm.id"
|
||||
@click.stop="applyParadigm(paradigm)"
|
||||
class="btn-action primary"
|
||||
class="btn-action btn-action-text primary"
|
||||
>
|
||||
应用到写作
|
||||
</button>
|
||||
@@ -71,194 +99,71 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 范式分析工具 -->
|
||||
<section class="analysis-section">
|
||||
<h3 class="text-sm font-medium text-secondary mb-4">🔍 范式分析工具</h3>
|
||||
<textarea
|
||||
v-model="analysisText"
|
||||
class="analysis-textarea"
|
||||
placeholder="粘贴文章内容,分析其写作范式..."
|
||||
></textarea>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button
|
||||
@click="analyzeArticle"
|
||||
:disabled="isAnalyzing || !analysisText"
|
||||
class="btn btn-analysis primary flex-1"
|
||||
>
|
||||
{{ isAnalyzing ? '正在分析...' : '分析文章范式' }}
|
||||
</button>
|
||||
<button
|
||||
@click="clearAnalysis"
|
||||
class="btn btn-analysis secondary"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 分析历史 -->
|
||||
<section v-if="analysisHistory.length > 0" class="analysis-section">
|
||||
<h3 class="text-sm font-medium text-secondary mb-4">📝 分析历史</h3>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in analysisHistory"
|
||||
:key="index"
|
||||
@click="loadHistoryItem(item)"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-secondary">{{ item.paradigm }}</span>
|
||||
<span class="text-muted">{{ formatDate(item.timestamp) }}</span>
|
||||
</div>
|
||||
<p class="text-muted mt-1 truncate">{{ item.preview }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- 范式分析工具 -->
|
||||
<section v-show="activeTab === 'tools'" class="analysis-section">
|
||||
<h3 class="text-sm font-medium text-secondary mb-4">🔍 范式分析工具</h3>
|
||||
<textarea
|
||||
v-model="analysisText"
|
||||
class="analysis-textarea"
|
||||
placeholder="粘贴文章内容,分析其写作范式..."
|
||||
></textarea>
|
||||
<div class="flex gap-2 mt-2">
|
||||
<button
|
||||
@click="analyzeArticle"
|
||||
:disabled="isAnalyzing || !analysisText"
|
||||
class="btn btn-analysis primary flex-1"
|
||||
>
|
||||
{{ isAnalyzing ? '正在分析...' : '分析文章范式' }}
|
||||
</button>
|
||||
<button
|
||||
@click="clearAnalysis"
|
||||
class="btn btn-analysis secondary"
|
||||
>
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 分析历史 -->
|
||||
<div v-if="analysisHistory.length > 0" class="mt-6">
|
||||
<h4 class="text-xs font-medium text-muted mb-3">📝 分析历史</h4>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="(item, index) in analysisHistory"
|
||||
:key="index"
|
||||
@click="loadHistoryItem(item)"
|
||||
class="history-item"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-secondary">{{ item.paradigm }}</span>
|
||||
<span class="text-muted">{{ formatDate(item.timestamp) }}</span>
|
||||
</div>
|
||||
<p class="text-muted mt-1 truncate">{{ item.preview }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 编辑/新增范式弹窗 -->
|
||||
<div
|
||||
v-if="showEditModal"
|
||||
class="paradigm-modal-backdrop"
|
||||
@click.self="closeEditModal"
|
||||
>
|
||||
<div class="paradigm-modal">
|
||||
<div class="p-4 border-b border-default flex items-center justify-between">
|
||||
<h3 class="font-medium text-primary">{{ isAddMode ? '新增写作范式' : '编辑写作范式' }}</h3>
|
||||
<button @click="closeEditModal" class="text-muted hover:text-primary transition">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="p-4 space-y-4">
|
||||
<!-- 图标选择 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">图标</label>
|
||||
<div class="icon-selector">
|
||||
<button
|
||||
v-for="icon in iconOptions"
|
||||
:key="icon"
|
||||
@click="editForm.icon = icon"
|
||||
:class="['icon-option', { 'selected': editForm.icon === icon }]"
|
||||
>
|
||||
{{ icon }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 名称 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">范式名称</label>
|
||||
<input
|
||||
v-model="editForm.name"
|
||||
class="analysis-input"
|
||||
placeholder="如:技术博客范式"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">描述</label>
|
||||
<textarea
|
||||
v-model="editForm.description"
|
||||
rows="2"
|
||||
class="analysis-textarea"
|
||||
placeholder="简要描述此范式的适用场景"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm text-secondary mb-2">标签(逗号分隔)</label>
|
||||
<input
|
||||
v-model="editForm.tagsInput"
|
||||
class="analysis-input"
|
||||
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-secondary 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="['color-option', color.class, { 'selected': editForm.tagClass === color.class }]"
|
||||
>
|
||||
{{ color.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⭐ 核心字段:完整 Prompt -->
|
||||
<div class="border-t border-default pt-4 mt-4">
|
||||
<label class="block text-sm text-secondary mb-2 flex items-center gap-2">
|
||||
<span>✨</span> 完整 Prompt(核心字段)
|
||||
<span class="text-xs text-muted font-normal">粘贴您的完整提示词</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="editForm.specializedPrompt"
|
||||
rows="12"
|
||||
class="analysis-textarea resize-y font-mono leading-relaxed"
|
||||
placeholder="在此粘贴您的完整 Prompt...
|
||||
|
||||
示例:
|
||||
# Role
|
||||
你是一个具有较高政治敏感性的AI助手...
|
||||
|
||||
# 任务目标
|
||||
你需要逐一分析文章中的每一条问题...
|
||||
|
||||
# 检查步骤
|
||||
1. 检查问题表述的清晰度
|
||||
2. 深度分析思想根源
|
||||
..."
|
||||
></textarea>
|
||||
<p class="text-xs text-muted mt-2">
|
||||
💡 提示:这是 AI 调用时的核心指令。完整的 Prompt 应包含角色、目标、步骤和输出格式。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 border-t border-default flex justify-end gap-2">
|
||||
<button
|
||||
@click="closeEditModal"
|
||||
class="btn btn-analysis secondary"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="saveParadigm"
|
||||
:disabled="!editForm.name"
|
||||
class="btn btn-analysis primary"
|
||||
>
|
||||
{{ isAddMode ? '添加' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 需求解析面板 -->
|
||||
<RequirementParserPanel
|
||||
v-if="showRequirementParser"
|
||||
@close="showRequirementParser = false"
|
||||
@paradigm-created="handleParadigmCreated"
|
||||
/>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { ref, reactive, onMounted, onUnmounted, 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 RequirementParserPanel from './RequirementParserPanel.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { analysisText, isAnalyzing } = storeToRefs(appStore)
|
||||
@@ -276,10 +181,11 @@ const analysisHistory = ref([])
|
||||
// 范式列表(响应式,支持编辑)
|
||||
const paradigms = ref([])
|
||||
|
||||
// 编辑弹窗状态
|
||||
const showEditModal = ref(false)
|
||||
const isAddMode = ref(false)
|
||||
const editingParadigmId = ref(null)
|
||||
const showRequirementParser = ref(false)
|
||||
const activeTab = ref('paradigms') // 'paradigms' or 'tools'
|
||||
|
||||
// 编辑表单
|
||||
const editForm = reactive({
|
||||
@@ -331,28 +237,55 @@ const initParadigms = () => {
|
||||
paradigms.value = [...mergedParadigms, ...customParadigms]
|
||||
}
|
||||
|
||||
// 打开新增弹窗
|
||||
// 打开新增(在右侧面板显示)
|
||||
const openAddModal = () => {
|
||||
isAddMode.value = true
|
||||
editingParadigmId.value = null
|
||||
resetEditForm()
|
||||
showEditModal.value = true
|
||||
// 更新 app store 中的编辑状态
|
||||
appStore.paradigmEditState.isEditing = true
|
||||
appStore.paradigmEditState.isAddMode = true
|
||||
appStore.paradigmEditState.editingParadigmId = null
|
||||
|
||||
// 重置表单
|
||||
const form = appStore.paradigmEditState.editForm
|
||||
form.icon = '📝'
|
||||
form.name = ''
|
||||
form.description = ''
|
||||
form.tagsInput = ''
|
||||
form.tagClass = 'bg-blue-900/30 text-blue-300'
|
||||
form.specializedPrompt = ''
|
||||
}
|
||||
|
||||
// 打开编辑弹窗
|
||||
// 打开需求解析面板
|
||||
const openRequirementParser = () => {
|
||||
showRequirementParser.value = true
|
||||
}
|
||||
|
||||
// 处理 AI 解析创建的范式
|
||||
const handleParadigmCreated = (paradigm) => {
|
||||
// 刷新列表以显示新范式
|
||||
initParadigms()
|
||||
showRequirementParser.value = false
|
||||
// 自动选中新创建的范式
|
||||
const newRef = paradigms.value.find(p => p.id === paradigm.id)
|
||||
if (newRef) {
|
||||
selectParadigm(newRef)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开编辑(在右侧面板显示)
|
||||
const openEditModal = (paradigm) => {
|
||||
isAddMode.value = false
|
||||
editingParadigmId.value = paradigm.id
|
||||
// 更新 app store 中的编辑状态
|
||||
appStore.paradigmEditState.isEditing = true
|
||||
appStore.paradigmEditState.isAddMode = false
|
||||
appStore.paradigmEditState.editingParadigmId = paradigm.id
|
||||
|
||||
editForm.icon = paradigm.icon || '📝'
|
||||
editForm.name = paradigm.name || ''
|
||||
editForm.description = paradigm.description || ''
|
||||
editForm.tagsInput = (paradigm.tags || []).join(', ')
|
||||
editForm.tagClass = paradigm.tagClass || 'bg-blue-900/30 text-blue-300'
|
||||
// ⭐ 核心字段:加载完整 Prompt
|
||||
editForm.specializedPrompt = paradigm.specializedPrompt || ''
|
||||
|
||||
showEditModal.value = true
|
||||
// 填充表单数据
|
||||
const form = appStore.paradigmEditState.editForm
|
||||
form.icon = paradigm.icon || '📝'
|
||||
form.name = paradigm.name || ''
|
||||
form.description = paradigm.description || ''
|
||||
form.tagsInput = (paradigm.tags || []).join(', ')
|
||||
form.tagClass = paradigm.tagClass || 'bg-blue-900/30 text-blue-300'
|
||||
form.specializedPrompt = paradigm.specializedPrompt || ''
|
||||
}
|
||||
|
||||
|
||||
@@ -389,6 +322,7 @@ const saveParadigm = () => {
|
||||
tags,
|
||||
tagClass: editForm.tagClass,
|
||||
isCustom: true,
|
||||
createdAt: new Date().toISOString(),
|
||||
// ⭐ 核心字段:完整 Prompt
|
||||
specializedPrompt: editForm.specializedPrompt
|
||||
}
|
||||
@@ -447,10 +381,19 @@ const saveParadigm = () => {
|
||||
closeEditModal()
|
||||
}
|
||||
|
||||
// 判断范式是否为自定义(兼容旧数据)
|
||||
const isCustomParadigm = (paradigm) => {
|
||||
// 优先检查 isCustom 字段
|
||||
if (paradigm.isCustom) return true
|
||||
// 兼容旧数据:检查 type 字段或 id 前缀
|
||||
if (paradigm.type === 'custom') return true
|
||||
if (paradigm.id && paradigm.id.startsWith('custom-')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// 删除自定义范式
|
||||
const deleteParadigm = (paradigm) => {
|
||||
if (!paradigm.isCustom) return
|
||||
if (!isCustomParadigm(paradigm)) return
|
||||
|
||||
if (!confirm(`确定要删除"${paradigm.name}"吗?`)) return
|
||||
|
||||
@@ -571,6 +514,16 @@ const formatDate = (date) => {
|
||||
return new Date(date).toLocaleDateString()
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
appStore.setCurrentPage('writer')
|
||||
}
|
||||
|
||||
// 监听保存事件并刷新
|
||||
const handleParadigmSaved = () => {
|
||||
initParadigms()
|
||||
}
|
||||
|
||||
// 组件挂载时加载历史记录和范式列表
|
||||
onMounted(() => {
|
||||
// 初始化范式列表
|
||||
@@ -581,13 +534,18 @@ onMounted(() => {
|
||||
if (saved) {
|
||||
analysisHistory.value = JSON.parse(saved)
|
||||
}
|
||||
|
||||
// 监听保存事件
|
||||
window.addEventListener('paradigm-saved', handleParadigmSaved)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('paradigm-saved', handleParadigmSaved)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ========== 使用设计令牌系统 ========== */
|
||||
|
||||
/* 侧边栏容器 */
|
||||
/* ========== 侧边栏面板 ========== */
|
||||
.analysis-panel {
|
||||
width: 400px;
|
||||
display: flex;
|
||||
@@ -617,6 +575,52 @@ onMounted(() => {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
/* 返回按钮 */
|
||||
.back-btn {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-sunken);
|
||||
}
|
||||
|
||||
/* 标签页 */
|
||||
.analysis-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.analysis-tab {
|
||||
flex: 1;
|
||||
padding: var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.analysis-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.analysis-tab.active {
|
||||
color: var(--accent-primary);
|
||||
background: var(--bg-primary);
|
||||
border-bottom: 2px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.analysis-content {
|
||||
flex: 1;
|
||||
@@ -632,6 +636,144 @@ onMounted(() => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 右侧编辑面板内容 */
|
||||
.edit-panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.edit-panel-header {
|
||||
padding: var(--space-4) var(--space-6);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.edit-panel-title {
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
padding: var(--space-2);
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.edit-panel-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.edit-field {
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.edit-field.prompt-field {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.edit-label {
|
||||
display: block;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.edit-input {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.edit-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.edit-textarea {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.edit-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* 模态弹窗 */
|
||||
.paradigm-modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.paradigm-modal {
|
||||
width: 560px;
|
||||
max-width: 90vw;
|
||||
max-height: 85vh;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
padding: var(--space-4);
|
||||
border-top: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* 范式卡片 */
|
||||
.paradigm-card {
|
||||
background: var(--bg-elevated);
|
||||
@@ -830,14 +972,21 @@ onMounted(() => {
|
||||
.btn-action {
|
||||
font-size: var(--text-xs);
|
||||
padding: var(--space-2);
|
||||
background: var(--bg-elevated);
|
||||
background: var(--bg-sunken); /* 统一使用较深背景,确保在所有状态下可见 */
|
||||
color: var(--text-primary);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
border: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.btn-action:hover {
|
||||
background: var(--bg-sunken);
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.btn-action.primary {
|
||||
@@ -849,6 +998,14 @@ onMounted(() => {
|
||||
background: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
/* 带文字的动作按钮(自适应宽度) */
|
||||
.btn-action.btn-action-text {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-action.danger {
|
||||
background: var(--accent-danger);
|
||||
color: var(--text-inverse);
|
||||
|
||||
@@ -54,6 +54,7 @@ const currentPage = computed(() => appStore.currentPage)
|
||||
const navItems = [
|
||||
{ id: 'writer', label: 'AI 写作', icon: '✍️' },
|
||||
{ id: 'analysis', label: '范式库', icon: '🎯' },
|
||||
{ id: 'paradigmWriter', label: '范式写作', icon: '📝' },
|
||||
{ id: 'documents', label: '文稿库', icon: '📂' },
|
||||
{ id: 'materials', label: '素材库', icon: '📚' },
|
||||
{ id: 'rewrite', label: '范式润色', icon: '🎨' },
|
||||
|
||||
@@ -213,75 +213,252 @@
|
||||
</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">
|
||||
<span class="text-6xl mb-4 opacity-20">📊</span>
|
||||
<p>选择左侧的写作范式或使用分析工具</p>
|
||||
<!-- 范式写作页面内容 -->
|
||||
<div v-else-if="currentPage === 'paradigmWriter'" class="h-full flex flex-col">
|
||||
<div v-if="!totalGeneratedContent" 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>
|
||||
<p class="text-xs text-slate-500 mt-2">选择章节并点击「生成本节」开始</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div v-if="analysisResult.error" class="bg-red-900/20 rounded-lg p-6 border border-red-700">
|
||||
<h3 class="text-lg font-bold text-red-400 mb-4">分析失败</h3>
|
||||
<p class="text-red-300">{{ analysisResult.message }}</p>
|
||||
<!-- 统计信息 -->
|
||||
<div class="flex items-center justify-between text-sm text-slate-400">
|
||||
<span>{{ totalWordCount }} 字</span>
|
||||
<span>预计阅读 {{ Math.ceil(totalWordCount / 300) }} 分钟</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<!-- 分析结果头部 -->
|
||||
<div class="bg-slate-900 rounded-lg p-6 border border-slate-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-white">分析结果</h3>
|
||||
<span class="text-xs px-3 py-1 rounded-full bg-blue-900/30 text-blue-300 border border-blue-700">
|
||||
{{ analysisResult.paradigm }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 分析内容 -->
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div v-html="marked.parse(analysisResult.analysis)"></div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button
|
||||
@click="copyAnalysis"
|
||||
class="text-xs px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded transition"
|
||||
<!-- 生成内容预览 -->
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div
|
||||
v-for="(section, index) in paradigmWriterState.sections"
|
||||
:key="index"
|
||||
v-show="section.generatedContent"
|
||||
class="mb-6"
|
||||
>
|
||||
<h3 :class="section.type === 'h2' ? 'text-xl font-bold text-white mb-3' : 'text-lg font-semibold text-slate-200 mb-2'">
|
||||
{{ section.title }}
|
||||
</h3>
|
||||
<div v-html="renderSectionMarkdown(section.generatedContent)"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex gap-3 pt-4 border-t border-slate-700">
|
||||
<button @click="copyParadigmContent" class="text-xs px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded transition">
|
||||
📋 复制全文
|
||||
</button>
|
||||
<button @click="exportParadigmToWord" class="text-xs px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded transition">
|
||||
📥 导出 Word
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 范式分析页面内容 -->
|
||||
<div v-else-if="currentPage === 'analysis'" class="space-y-8">
|
||||
<!-- 范式编辑表单 -->
|
||||
<div v-if="paradigmEditState.isEditing" class="bg-slate-900 rounded-lg border border-slate-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-slate-700 flex items-center justify-between">
|
||||
<h3 class="text-lg font-bold text-white">
|
||||
{{ paradigmEditState.isAddMode ? '➕ 新增写作范式' : '✏️ 编辑写作范式' }}
|
||||
</h3>
|
||||
<button
|
||||
@click="closeParadigmEdit"
|
||||
class="text-slate-400 hover:text-white transition"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6 space-y-5">
|
||||
<!-- 图标选择 -->
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-2">图标</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
v-for="icon in iconOptions"
|
||||
:key="icon"
|
||||
@click="paradigmEditState.editForm.icon = icon"
|
||||
:class="['w-10 h-10 text-xl rounded border transition',
|
||||
paradigmEditState.editForm.icon === icon
|
||||
? 'border-blue-500 bg-blue-500/20'
|
||||
: 'border-slate-600 hover:border-slate-500']"
|
||||
>
|
||||
📋 复制分析结果
|
||||
</button>
|
||||
<button
|
||||
@click="applyToWriting"
|
||||
class="text-xs px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded transition"
|
||||
>
|
||||
✍️ 应用到写作
|
||||
{{ icon }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 范式详情卡片 -->
|
||||
<div v-if="getParadigmDetails()" class="bg-slate-900/50 rounded-lg p-6 border border-slate-700">
|
||||
<h4 class="text-md font-bold text-white mb-4">{{ getParadigmDetails().name }}特点</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h5 class="text-xs font-medium text-slate-400 mb-2">结构要素</h5>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="tag in getParadigmDetails().tags"
|
||||
:key="tag"
|
||||
:class="['text-xs px-2 py-1 rounded', getParadigmDetails().tagClass]"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 名称 -->
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-2">范式名称</label>
|
||||
<input
|
||||
v-model="paradigmEditState.editForm.name"
|
||||
class="w-full px-4 py-3 bg-slate-800 border border-slate-600 rounded-lg text-white outline-none focus:border-blue-500"
|
||||
placeholder="如:技术博客范式"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 描述 -->
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-2">描述</label>
|
||||
<textarea
|
||||
v-model="paradigmEditState.editForm.description"
|
||||
rows="2"
|
||||
class="w-full px-4 py-3 bg-slate-800 border border-slate-600 rounded-lg text-white outline-none focus:border-blue-500 resize-none"
|
||||
placeholder="简要描述此范式的适用场景"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-2">标签(逗号分隔)</label>
|
||||
<input
|
||||
v-model="paradigmEditState.editForm.tagsInput"
|
||||
class="w-full px-4 py-3 bg-slate-800 border border-slate-600 rounded-lg text-white outline-none focus:border-blue-500"
|
||||
placeholder="如:问题引入,解决方案,代码示例"
|
||||
/>
|
||||
<div class="flex flex-wrap gap-1 mt-2" v-if="paradigmEditState.editForm.tagsInput">
|
||||
<span
|
||||
v-for="tag in paradigmEditState.editForm.tagsInput.split(',').map(t => t.trim()).filter(t => t)"
|
||||
:key="tag"
|
||||
:class="['text-xs px-2 py-1 rounded', paradigmEditState.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="paradigmEditState.editForm.tagClass = color.class"
|
||||
:class="['px-3 py-1 text-xs rounded border transition', color.class,
|
||||
paradigmEditState.editForm.tagClass === color.class
|
||||
? 'ring-2 ring-white/50'
|
||||
: 'opacity-60 hover:opacity-100']"
|
||||
>
|
||||
{{ color.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ⭐ 核心字段:完整 Prompt -->
|
||||
<div>
|
||||
<label class="block text-sm text-slate-400 mb-2 flex items-center gap-2">
|
||||
<span>✨</span> 完整 Prompt(核心字段)
|
||||
<span class="text-xs text-slate-500">粘贴您的完整提示词</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="paradigmEditState.editForm.specializedPrompt"
|
||||
rows="16"
|
||||
class="w-full px-4 py-3 bg-slate-800 border border-slate-600 rounded-lg text-white outline-none focus:border-blue-500 resize-y font-mono text-sm leading-relaxed"
|
||||
placeholder="在此粘贴您的完整 Prompt..."
|
||||
></textarea>
|
||||
<p class="text-xs text-slate-500 mt-2">
|
||||
💡 提示:这是 AI 调用时的核心指令。完整的 Prompt 应包含角色、目标、步骤和输出格式。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-6 py-4 border-t border-slate-700 flex justify-end gap-3">
|
||||
<button
|
||||
@click="closeParadigmEdit"
|
||||
class="px-4 py-2 text-sm text-slate-400 hover:text-white transition"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
@click="saveParadigmEdit"
|
||||
:disabled="!paradigmEditState.editForm.name"
|
||||
class="px-6 py-2 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ paradigmEditState.isAddMode ? '添加范式' : '保存修改' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 默认显示或分析结果 -->
|
||||
<template v-else>
|
||||
<div v-if="!analysisResult" 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="space-y-6">
|
||||
<div v-if="analysisResult.error" class="bg-red-900/20 rounded-lg p-6 border border-red-700">
|
||||
<h3 class="text-lg font-bold text-red-400 mb-4">分析失败</h3>
|
||||
<p class="text-red-300">{{ analysisResult.message }}</p>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<!-- 分析结果头部 -->
|
||||
<div class="bg-slate-900 rounded-lg p-6 border border-slate-700">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-white">分析结果</h3>
|
||||
<span class="text-xs px-3 py-1 rounded-full bg-blue-900/30 text-blue-300 border border-blue-700">
|
||||
{{ analysisResult.paradigm }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="text-xs font-medium text-slate-400 mb-2">适用场景</h5>
|
||||
<p class="text-xs text-slate-300">{{ getParadigmDetails().description }}</p>
|
||||
|
||||
<!-- 分析内容 -->
|
||||
<div class="prose prose-invert max-w-none">
|
||||
<div v-html="marked.parse(analysisResult.analysis)"></div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button
|
||||
@click="copyAnalysis"
|
||||
class="text-xs px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded transition"
|
||||
>
|
||||
📋 复制分析结果
|
||||
</button>
|
||||
<button
|
||||
@click="applyToWriting"
|
||||
class="text-xs px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded transition"
|
||||
>
|
||||
✍️ 应用到写作
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 范式详情卡片 -->
|
||||
<div v-if="getParadigmDetails()" class="bg-slate-900/50 rounded-lg p-6 border border-slate-700">
|
||||
<h4 class="text-md font-bold text-white mb-4">{{ getParadigmDetails().name }}特点</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h5 class="text-xs font-medium text-slate-400 mb-2">结构要素</h5>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="tag in getParadigmDetails().tags"
|
||||
:key="tag"
|
||||
:class="['text-xs px-2 py-1 rounded', getParadigmDetails().tagClass]"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="text-xs font-medium text-slate-400 mb-2">适用场景</h5>
|
||||
<p class="text-xs text-slate-300">{{ getParadigmDetails().description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 其他页面默认内容 -->
|
||||
<div v-else 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>
|
||||
</div>
|
||||
@@ -308,13 +485,144 @@ const {
|
||||
inputTask,
|
||||
selectedTags,
|
||||
customConstraint,
|
||||
references
|
||||
references,
|
||||
paradigmEditState
|
||||
} = storeToRefs(appStore)
|
||||
|
||||
// AI 思考过程折叠状态
|
||||
const isThinkingExpanded = ref(false)
|
||||
const isQualityReportExpanded = ref(true) // 质检报告默认展开
|
||||
|
||||
// 范式写作预览相关
|
||||
const { paradigmWriterState } = storeToRefs(appStore)
|
||||
|
||||
// 获取总生成内容
|
||||
const totalGeneratedContent = computed(() => {
|
||||
if (!paradigmWriterState.value?.sections) return ''
|
||||
return paradigmWriterState.value.sections
|
||||
.filter(s => s.generatedContent)
|
||||
.map(s => `## ${s.title}\n\n${s.generatedContent}`)
|
||||
.join('\n\n')
|
||||
})
|
||||
|
||||
// 字数统计
|
||||
const totalWordCount = computed(() => {
|
||||
return totalGeneratedContent.value.length
|
||||
})
|
||||
|
||||
// 渲染章节 Markdown
|
||||
const renderSectionMarkdown = (content) => {
|
||||
return marked.parse(content || '')
|
||||
}
|
||||
|
||||
// 复制范式写作内容
|
||||
const copyParadigmContent = () => {
|
||||
if (totalGeneratedContent.value) {
|
||||
navigator.clipboard.writeText(totalGeneratedContent.value)
|
||||
alert('已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出 Word
|
||||
const exportParadigmToWord = async () => {
|
||||
if (!totalGeneratedContent.value) return
|
||||
|
||||
try {
|
||||
const { Document, Paragraph, TextRun, Packer, HeadingLevel } = await import('docx')
|
||||
const { saveAs } = await import('file-saver')
|
||||
|
||||
const children = []
|
||||
paradigmWriterState.value.sections.forEach(section => {
|
||||
if (section.generatedContent) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: section.title,
|
||||
heading: section.type === 'h2' ? HeadingLevel.HEADING_2 : HeadingLevel.HEADING_3
|
||||
}),
|
||||
new Paragraph({
|
||||
children: [new TextRun(section.generatedContent)]
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const doc = new Document({
|
||||
sections: [{ properties: {}, children }]
|
||||
})
|
||||
|
||||
const blob = await Packer.toBlob(doc)
|
||||
saveAs(blob, `范式写作_${new Date().toLocaleDateString()}.docx`)
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
alert('导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 图标选项
|
||||
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 closeParadigmEdit = () => {
|
||||
paradigmEditState.value.isEditing = false
|
||||
paradigmEditState.value.isAddMode = false
|
||||
paradigmEditState.value.editingParadigmId = null
|
||||
}
|
||||
|
||||
// 保存范式编辑
|
||||
const saveParadigmEdit = () => {
|
||||
const form = paradigmEditState.value.editForm
|
||||
|
||||
// 构建范式对象
|
||||
const paradigm = {
|
||||
id: paradigmEditState.value.isAddMode
|
||||
? `custom_${Date.now()}`
|
||||
: paradigmEditState.value.editingParadigmId,
|
||||
icon: form.icon,
|
||||
name: form.name,
|
||||
description: form.description,
|
||||
tags: form.tagsInput.split(',').map(t => t.trim()).filter(t => t),
|
||||
tagClass: form.tagClass,
|
||||
specializedPrompt: form.specializedPrompt,
|
||||
isCustom: true,
|
||||
createdAt: paradigmEditState.value.isAddMode ? new Date().toISOString() : undefined
|
||||
}
|
||||
|
||||
// 保存到 localStorage
|
||||
const customParadigms = JSON.parse(localStorage.getItem('customParadigms') || '[]')
|
||||
|
||||
if (paradigmEditState.value.isAddMode) {
|
||||
customParadigms.push(paradigm)
|
||||
} else {
|
||||
const index = customParadigms.findIndex(p => p.id === paradigm.id)
|
||||
if (index >= 0) {
|
||||
customParadigms[index] = { ...customParadigms[index], ...paradigm }
|
||||
} else {
|
||||
// 可能是系统范式的修改,添加为新的自定义范式
|
||||
customParadigms.push(paradigm)
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('customParadigms', JSON.stringify(customParadigms))
|
||||
|
||||
// 关闭编辑
|
||||
closeParadigmEdit()
|
||||
|
||||
// 触发刷新事件
|
||||
window.dispatchEvent(new CustomEvent('paradigm-saved'))
|
||||
|
||||
alert('范式保存成功!')
|
||||
}
|
||||
|
||||
// 文稿编辑状态
|
||||
const currentDocument = ref(null)
|
||||
const documentTitle = ref('')
|
||||
|
||||
644
src/components/ParadigmWriterPanel.vue
Normal file
644
src/components/ParadigmWriterPanel.vue
Normal file
@@ -0,0 +1,644 @@
|
||||
<template>
|
||||
<aside class="paradigm-writer-panel">
|
||||
<!-- 头部 -->
|
||||
<header class="writer-header">
|
||||
<h1 class="writer-header-title">
|
||||
<span style="font-size: var(--text-xl)">📝</span> 范式分段写作
|
||||
</h1>
|
||||
<span class="badge badge-primary">新功能</span>
|
||||
</header>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="writer-content">
|
||||
<!-- 1. 选择范式 -->
|
||||
<section class="writer-section">
|
||||
<label class="writer-label">1. 选择写作范式</label>
|
||||
<select
|
||||
v-model="selectedParadigmId"
|
||||
@change="handleParadigmChange"
|
||||
class="writer-select"
|
||||
>
|
||||
<option value="" disabled>请选择范式...</option>
|
||||
<option
|
||||
v-for="p in paradigmList"
|
||||
:key="p.id"
|
||||
:value="p.id"
|
||||
>
|
||||
{{ p.icon }} {{ p.name }}
|
||||
</option>
|
||||
</select>
|
||||
<p v-if="activeParadigm" class="text-xs text-muted mt-2">
|
||||
{{ activeParadigm.description }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- 2. 分段编写 -->
|
||||
<section v-if="paradigmWriterState.selectedParadigmId" class="writer-section">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="writer-label">2. 细化大纲与生成</label>
|
||||
<span class="text-[10px] text-muted">{{ paradigmWriterState.sections.length }} 个章节</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
v-for="(section, index) in paradigmWriterState.sections"
|
||||
:key="index"
|
||||
:class="['section-card', { 'active': currentSectionIndex === index }]"
|
||||
@click="currentSectionIndex = index"
|
||||
>
|
||||
<div class="section-card-header">
|
||||
<span :class="['section-type-badge', section.type === 'h2' ? 'primary' : 'secondary']">
|
||||
{{ section.type === 'h2' ? '主' : '子' }}
|
||||
</span>
|
||||
<h4 class="section-title text-sm truncate">{{ section.title }}</h4>
|
||||
|
||||
<div class="flex items-center gap-1">
|
||||
<span v-if="section.generatedContent" class="text-[10px] text-success">已生成</span>
|
||||
<span v-if="section.isGenerating" class="animate-spin text-xs">↻</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="currentSectionIndex === index" class="section-card-body mt-2 space-y-3">
|
||||
<!-- 输入类型选择 -->
|
||||
<div class="flex gap-2 mb-2">
|
||||
<button
|
||||
@click.stop="section.inputType = 'material'"
|
||||
:class="['input-type-btn', section.inputType === 'material' ? 'active' : '']"
|
||||
>
|
||||
📊 素材型
|
||||
</button>
|
||||
<button
|
||||
@click.stop="section.inputType = 'idea'"
|
||||
:class="['input-type-btn', section.inputType === 'idea' || !section.inputType ? 'active' : '']"
|
||||
>
|
||||
💡 思路型
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 结构化输入:核心论点 -->
|
||||
<div>
|
||||
<label class="text-[10px] text-muted block mb-1">📌 核心论点(必填)</label>
|
||||
<textarea
|
||||
v-model="section.corePoint"
|
||||
class="writer-textarea min-h-[50px]"
|
||||
placeholder="本章节的核心观点或主题..."
|
||||
@click.stop
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 结构化输入:数据/案例 -->
|
||||
<div>
|
||||
<label class="text-[10px] text-muted block mb-1">
|
||||
📊 数据/案例
|
||||
<span class="text-warning">({{ section.inputType === 'material' ? '严格引用' : '可润色' }})</span>
|
||||
</label>
|
||||
<textarea
|
||||
v-model="section.materialData"
|
||||
class="writer-textarea min-h-[50px]"
|
||||
placeholder="具体的数据、案例、引用内容..."
|
||||
@click.stop
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- 结构化输入:补充说明 -->
|
||||
<div>
|
||||
<label class="text-[10px] text-muted block mb-1">💡 补充说明(可自由发挥)</label>
|
||||
<textarea
|
||||
v-model="section.supplementNote"
|
||||
class="writer-textarea min-h-[40px]"
|
||||
:placeholder="section.placeholder || '写作风格、语气要求等...'"
|
||||
@click.stop
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center gap-2 pt-1">
|
||||
<button
|
||||
@click.stop="generateSection(index)"
|
||||
:disabled="section.isGenerating"
|
||||
class="btn btn-primary flex-1 text-xs py-1.5"
|
||||
>
|
||||
{{ section.isGenerating ? '正在生成...' : (section.generatedContent ? '重新生成本节' : '生成本节') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="section.generatedContent"
|
||||
@click.stop="copySectionContent(section)"
|
||||
class="btn btn-secondary text-[10px] py-1.5"
|
||||
title="复制内容"
|
||||
>
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 提示:未选择范式 -->
|
||||
<div v-else class="empty-state">
|
||||
<div class="empty-icon text-center">📂</div>
|
||||
<p class="text-sm text-muted text-center">请先选择一个写作范式以加载大纲</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部:全局操作 -->
|
||||
<footer class="writer-footer">
|
||||
<div v-if="paradigmWriterState.selectedParadigmId" class="space-y-3">
|
||||
<div class="flex items-center justify-between text-xs mb-1">
|
||||
<span class="text-muted">总进度</span>
|
||||
<span class="text-accent">{{ generatedSectionsCount }} / {{ paradigmWriterState.sections.length }}</span>
|
||||
</div>
|
||||
<div class="progress-bar-bg">
|
||||
<div
|
||||
class="progress-bar-fill"
|
||||
:style="{ width: `${(generatedSectionsCount / paradigmWriterState.sections.length) * 100}%` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="generateAllSections"
|
||||
:disabled="paradigmWriterState.isGeneratingOverall"
|
||||
class="generate-button primary"
|
||||
>
|
||||
{{ paradigmWriterState.isGeneratingOverall ? '正在批量生成中...' : '一键生成全文 (Beta)' }}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { getParadigmList } from '../config/paradigms.js'
|
||||
import { marked } from 'marked'
|
||||
import { Document, Packer, Paragraph, TextRun, HeadingLevel } from 'docx'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { paradigmWriterState, activeParadigm, generatedContent } = storeToRefs(appStore)
|
||||
|
||||
const selectedParadigmId = ref('')
|
||||
const currentSectionIndex = ref(0)
|
||||
const paradigmList = ref([])
|
||||
|
||||
// 加载完整的范式列表(包含自定义范式)
|
||||
const loadAllParadigms = () => {
|
||||
const defaultList = getParadigmList()
|
||||
const savedCustomParadigms = localStorage.getItem('customParadigms')
|
||||
const customList = savedCustomParadigms ? JSON.parse(savedCustomParadigms) : []
|
||||
paradigmList.value = [...defaultList, ...customList]
|
||||
}
|
||||
|
||||
// 已生成章节数
|
||||
const generatedSectionsCount = computed(() => {
|
||||
return paradigmWriterState.value.sections.filter(s => s.generatedContent).length
|
||||
})
|
||||
|
||||
// 总生成内容
|
||||
const totalGeneratedContent = computed(() => {
|
||||
return paradigmWriterState.value.sections
|
||||
.filter(s => s.generatedContent)
|
||||
.map(s => `## ${s.title}\n\n${s.generatedContent}`)
|
||||
.join('\n\n')
|
||||
})
|
||||
|
||||
// 总字数
|
||||
const totalWordCount = computed(() => {
|
||||
return totalGeneratedContent.value.replace(/[#\n\s]/g, '').length
|
||||
})
|
||||
|
||||
// 渲染 Markdown
|
||||
const renderMarkdown = (text) => {
|
||||
if (!text) return ''
|
||||
return marked(text)
|
||||
}
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
appStore.setCurrentPage('writer')
|
||||
}
|
||||
|
||||
// 处理范式选择切换
|
||||
const handleParadigmChange = () => {
|
||||
if (selectedParadigmId.value) {
|
||||
appStore.loadParadigmForWriting(selectedParadigmId.value)
|
||||
currentSectionIndex.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 生成单个章节
|
||||
const generateSection = async (index) => {
|
||||
await appStore.generateSectionContentAction(index)
|
||||
}
|
||||
|
||||
// 批量生成所有章节 (串行执行以保证连贯性)
|
||||
const generateAllSections = async () => {
|
||||
if (paradigmWriterState.value.isGeneratingOverall) return
|
||||
|
||||
paradigmWriterState.value.isGeneratingOverall = true
|
||||
try {
|
||||
for (let i = 0; i < paradigmWriterState.value.sections.length; i++) {
|
||||
currentSectionIndex.value = i
|
||||
await appStore.generateSectionContentAction(i)
|
||||
}
|
||||
} finally {
|
||||
paradigmWriterState.value.isGeneratingOverall = false
|
||||
}
|
||||
}
|
||||
|
||||
// 复制章节内容
|
||||
const copySectionContent = (section) => {
|
||||
navigator.clipboard.writeText(section.generatedContent)
|
||||
.then(() => alert('已复制到剪贴板'))
|
||||
}
|
||||
|
||||
// 复制全文
|
||||
const copyFullContent = () => {
|
||||
navigator.clipboard.writeText(totalGeneratedContent.value)
|
||||
.then(() => alert('全文已复制到剪贴板'))
|
||||
}
|
||||
|
||||
// 导出为 Word
|
||||
const exportToWord = async () => {
|
||||
const sections = paradigmWriterState.value.sections.filter(s => s.generatedContent)
|
||||
|
||||
const children = []
|
||||
|
||||
sections.forEach(section => {
|
||||
// 添加标题
|
||||
children.push(
|
||||
new Paragraph({
|
||||
text: section.title,
|
||||
heading: section.type === 'h2' ? HeadingLevel.HEADING_2 : HeadingLevel.HEADING_3,
|
||||
})
|
||||
)
|
||||
|
||||
// 添加内容段落
|
||||
const paragraphs = section.generatedContent.split('\n\n')
|
||||
paragraphs.forEach(p => {
|
||||
if (p.trim()) {
|
||||
children.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun(p.trim())],
|
||||
spacing: { after: 200 },
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const doc = new Document({
|
||||
sections: [{
|
||||
properties: {},
|
||||
children: children
|
||||
}]
|
||||
})
|
||||
|
||||
const blob = await Packer.toBlob(doc)
|
||||
saveAs(blob, `${activeParadigm.value?.name || '范式写作'}_${new Date().toLocaleDateString()}.docx`)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadAllParadigms()
|
||||
|
||||
if (paradigmWriterState.value.selectedParadigmId) {
|
||||
selectedParadigmId.value = paradigmWriterState.value.selectedParadigmId
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 侧边面板 */
|
||||
.paradigm-writer-panel {
|
||||
width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border-default);
|
||||
background: var(--bg-secondary);
|
||||
flex-shrink: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* 右侧预览面板 */
|
||||
.writer-preview-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-primary);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preview-stats {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.preview-empty-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: var(--space-4);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.preview-markdown {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.preview-h2 {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-3);
|
||||
padding-bottom: var(--space-2);
|
||||
border-bottom: 2px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.preview-h3 {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.8;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preview-text :deep(p) {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.preview-footer {
|
||||
padding: var(--space-4);
|
||||
border-top: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
justify-content: flex-end;
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.export-btn:hover {
|
||||
background: var(--bg-sunken);
|
||||
}
|
||||
|
||||
.export-btn.primary {
|
||||
background: var(--accent-primary);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.export-btn.primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 输入类型按钮 */
|
||||
.input-type-btn {
|
||||
flex: 1;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
font-size: 10px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.input-type-btn.active {
|
||||
background: var(--accent-primary);
|
||||
color: var(--text-inverse);
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
/* 原有样式保留 */
|
||||
.writer-header {
|
||||
padding: var(--space-4);
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.writer-header-title {
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--text-lg);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.writer-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.writer-section {
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.writer-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--space-2);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.writer-select {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--text-primary);
|
||||
font-size: var(--text-sm);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.writer-select:focus {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.section-card {
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-3);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.section-card:hover {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.section-card.active {
|
||||
border-color: var(--accent-primary);
|
||||
background: var(--bg-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.section-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.section-type-badge {
|
||||
font-size: 8px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.section-type-badge.primary {
|
||||
background: var(--info-bg);
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.section-type-badge.secondary {
|
||||
background: var(--bg-sunken);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
flex: 1;
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.section-card-body {
|
||||
border-top: 1px solid var(--border-default);
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
.writer-textarea {
|
||||
width: 100%;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-2);
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-primary);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.writer-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: var(--space-20);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: var(--space-4);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.progress-bar-bg {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: var(--bg-elevated);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: var(--accent-primary);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.writer-footer {
|
||||
padding: var(--space-4);
|
||||
border-top: 1px solid var(--border-default);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.generate-button {
|
||||
width: 100%;
|
||||
padding: var(--space-3);
|
||||
border-radius: var(--radius-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-inverse);
|
||||
background: linear-gradient(135deg, var(--accent-primary), #6366f1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
transition: all var(--transition-normal);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.generate-button:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.generate-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -11,6 +11,52 @@
|
||||
<!-- 设置内容 -->
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-6 min-h-0">
|
||||
|
||||
<!-- AI 模型设置 -->
|
||||
<section class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
<span>🤖</span> AI 模型设置
|
||||
</h3>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="settings-label">模型服务商</label>
|
||||
<select
|
||||
v-model="selectedProviderId"
|
||||
class="settings-select"
|
||||
>
|
||||
<option
|
||||
v-for="provider in availableProviders"
|
||||
:key="provider.id"
|
||||
:value="provider.id"
|
||||
>
|
||||
{{ provider.name }} - {{ provider.description }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 当前配置状态 -->
|
||||
<div class="provider-status">
|
||||
<div class="status-row">
|
||||
<span class="status-label">API 端点</span>
|
||||
<span class="status-value truncate">{{ currentProvider?.apiUrl || '未配置' }}</span>
|
||||
</div>
|
||||
<div class="status-row">
|
||||
<span class="status-label">API Key</span>
|
||||
<span :class="['status-value', currentProvider?.apiKey ? 'configured' : 'not-configured']">
|
||||
{{ currentProvider?.apiKey ? '✓ 已配置' : '✗ 未配置' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-row" v-if="currentProvider?.model">
|
||||
<span class="status-label">默认模型</span>
|
||||
<span class="status-value">{{ currentProvider.model }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-muted">
|
||||
💡 模型配置通过 <code>.env</code> 文件设置,详见项目文档
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 数据统计 -->
|
||||
<section class="settings-section">
|
||||
<h3 class="settings-section-title">
|
||||
@@ -150,10 +196,18 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { modelProviders, getConfiguredProviders } from '../utils/config.js'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const { selectedProviderId, currentProvider } = storeToRefs(appStore)
|
||||
|
||||
// 获取所有可选的服务商(包括已配置和未配置的)
|
||||
const availableProviders = computed(() => {
|
||||
return Object.values(modelProviders)
|
||||
})
|
||||
|
||||
// 状态
|
||||
const stats = reactive({
|
||||
@@ -391,6 +445,99 @@ onMounted(() => {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 设置输入框和标签 */
|
||||
.settings-label {
|
||||
display: block;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.settings-input {
|
||||
width: 100%;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.settings-input:focus {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.settings-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 选择框 */
|
||||
.settings-select {
|
||||
width: 100%;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-primary);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.settings-select:focus {
|
||||
border-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.settings-select option {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 服务商状态显示 */
|
||||
.provider-status {
|
||||
background: var(--bg-elevated);
|
||||
border-radius: var(--radius-md);
|
||||
padding: var(--space-3);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.status-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-1) 0;
|
||||
}
|
||||
|
||||
.status-row:not(:last-child) {
|
||||
border-bottom: 1px solid var(--border-default);
|
||||
}
|
||||
|
||||
.status-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-value {
|
||||
color: var(--text-secondary);
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.status-value.configured {
|
||||
color: var(--accent-success);
|
||||
}
|
||||
|
||||
.status-value.not-configured {
|
||||
color: var(--accent-danger);
|
||||
}
|
||||
|
||||
.status-value.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 设置按钮 */
|
||||
.settings-btn {
|
||||
width: 100%;
|
||||
|
||||
@@ -165,7 +165,7 @@
|
||||
|
||||
<!-- 底部操作区 -->
|
||||
<footer class="writer-footer">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="showPromptDebug" class="hidden">
|
||||
<div class="toggle-switch" :class="{'active': showPromptDebug}">
|
||||
@@ -176,22 +176,6 @@
|
||||
<span class="text-xs text-muted">deepseek</span>
|
||||
</div>
|
||||
|
||||
<!-- API配置 -->
|
||||
<div class="space-y-2">
|
||||
<input
|
||||
v-model="apiUrl"
|
||||
type="text"
|
||||
class="writer-input"
|
||||
placeholder="API 地址"
|
||||
>
|
||||
<input
|
||||
v-model="apiKey"
|
||||
type="password"
|
||||
class="writer-input"
|
||||
placeholder="API Key"
|
||||
>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="generateContent"
|
||||
:disabled="!canGenerate"
|
||||
@@ -223,8 +207,6 @@ const {
|
||||
expertGuidelines,
|
||||
isGenerating,
|
||||
showPromptDebug,
|
||||
apiUrl,
|
||||
apiKey,
|
||||
isDeepMode,
|
||||
inputType,
|
||||
outlinePoints
|
||||
|
||||
@@ -803,9 +803,26 @@ export const getParadigmList = () => {
|
||||
return Object.values(PARADIGMS)
|
||||
}
|
||||
|
||||
// 根据ID获取范式详情
|
||||
// 根据ID获取范式详情(同时查找内置和自定义范式)
|
||||
export const getParadigmById = (id) => {
|
||||
return PARADIGMS[id] || null
|
||||
// 1. 先从内置范式中查找
|
||||
if (PARADIGMS[id]) {
|
||||
return PARADIGMS[id]
|
||||
}
|
||||
|
||||
// 2. 从 localStorage 的自定义范式中查找
|
||||
try {
|
||||
const savedCustomParadigms = localStorage.getItem('customParadigms')
|
||||
if (savedCustomParadigms) {
|
||||
const customParadigms = JSON.parse(savedCustomParadigms)
|
||||
const found = customParadigms.find(p => p.id === id)
|
||||
if (found) return found
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取自定义范式失败:', e)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// 构建范式专用的 System Prompt 约束
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { config } from '../utils/config.js'
|
||||
import { ref, computed } from 'vue'
|
||||
import { config, modelProviders, getConfiguredProviders, getDefaultProvider } from '../utils/config.js'
|
||||
import DeepSeekAPI from '../api/deepseek.js'
|
||||
import { buildPrompt, createStreamParser, parseGhostwriterOutput } from '../utils/promptBuilder.js'
|
||||
import { PARADIGMS, getParadigmById, buildParadigmConstraints } from '../config/paradigms.js'
|
||||
@@ -8,11 +8,19 @@ import { PARADIGMS, getParadigmById, buildParadigmConstraints } from '../config/
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
// 页面状态
|
||||
const currentPage = ref('writer') // 'writer' 或 'analysis'
|
||||
|
||||
// API配置 - 从环境变量读取
|
||||
const apiUrl = ref(config.apiUrl)
|
||||
const apiKey = ref(config.apiKey)
|
||||
|
||||
|
||||
// 模型服务商选择
|
||||
const selectedProviderId = ref(getDefaultProvider().id)
|
||||
|
||||
// 动态获取当前服务商配置
|
||||
const currentProvider = computed(() => {
|
||||
return modelProviders[selectedProviderId.value] || getDefaultProvider()
|
||||
})
|
||||
|
||||
// API 配置(基于选择的服务商)
|
||||
const apiUrl = computed(() => currentProvider.value.apiUrl)
|
||||
const apiKey = computed(() => currentProvider.value.apiKey)
|
||||
|
||||
// 写作相关
|
||||
const inputTask = ref('请帮我写一篇关于"AI 辅助编程如何改变软件开发流程"的博客文章,面向中级程序员。')
|
||||
const inputType = ref('text') // 'text' 或 'outline'
|
||||
@@ -22,25 +30,25 @@ export const useAppStore = defineStore('app', () => {
|
||||
keyPoints: ''
|
||||
})
|
||||
const references = ref([
|
||||
{
|
||||
title: '示例:技术博客风格.txt',
|
||||
{
|
||||
title: '示例:技术博客风格.txt',
|
||||
content: '本文深入探讨了...(此处省略2000字,这是为了让AI模仿这种干练的技术风格)...',
|
||||
styleTags: ['#技术干货', '#逻辑严密', '#客观中立']
|
||||
styleTags: ['#技术干货', '#逻辑严密', '#客观中立']
|
||||
}
|
||||
])
|
||||
const selectedTags = ref(['Markdown格式', '总分总结构'])
|
||||
const customConstraint = ref('')
|
||||
|
||||
|
||||
// 范式相关
|
||||
const activeParadigm = ref(null) // 当前激活的范式配置
|
||||
const expertGuidelines = ref([]) // 专家指令列表
|
||||
|
||||
|
||||
// 当前编辑的文稿
|
||||
const currentDocument = ref(null)
|
||||
|
||||
|
||||
// 质检报告(深度模式)
|
||||
const qualityReport = ref(null) // { checks: [{key, title, status, message}], overall: 'pass'|'warning'|'fail' }
|
||||
|
||||
|
||||
// 生成相关
|
||||
const isGenerating = ref(false)
|
||||
const isDeepMode = ref(false)
|
||||
@@ -48,13 +56,36 @@ export const useAppStore = defineStore('app', () => {
|
||||
const generatedContent = ref('')
|
||||
const thinkingContent = ref('') // AI 思考过程(风格分析 + 大纲规划)
|
||||
const rawStreamBuffer = ref('') // 原始流式输出缓冲
|
||||
|
||||
|
||||
// 分析相关
|
||||
const analysisText = ref('')
|
||||
const analysisResult = ref(null)
|
||||
const isAnalyzing = ref(false)
|
||||
const styleAnalysis = ref('') // 存储具体的风格分析内容
|
||||
|
||||
// 范式写作相关 (Paradigm Writing)
|
||||
const paradigmWriterState = ref({
|
||||
selectedParadigmId: null,
|
||||
sections: [], // 解析后的章节列表 [{title, placeholder, userInput, generatedContent, isGenerating}]
|
||||
currentSectionIndex: 0,
|
||||
isGeneratingOverall: false
|
||||
})
|
||||
|
||||
// 范式编辑状态 (Paradigm Editing)
|
||||
const paradigmEditState = ref({
|
||||
isEditing: false, // 是否正在编辑
|
||||
isAddMode: false, // 是否为新增模式
|
||||
editingParadigmId: null, // 正在编辑的范式ID
|
||||
editForm: {
|
||||
icon: '📝',
|
||||
name: '',
|
||||
description: '',
|
||||
tagsInput: '',
|
||||
tagClass: 'bg-blue-900/30 text-blue-300',
|
||||
specializedPrompt: ''
|
||||
}
|
||||
})
|
||||
|
||||
// UI状态
|
||||
const showPromptDebug = ref(false)
|
||||
const showRefInput = ref(false)
|
||||
@@ -79,10 +110,10 @@ export const useAppStore = defineStore('app', () => {
|
||||
if (!ref || !ref.content) return
|
||||
|
||||
if (!apiUrl.value || !apiKey.value || apiKey.value === 'YOUR_KEY') {
|
||||
console.warn('Store: Missing API Key for style analysis')
|
||||
return
|
||||
console.warn('Store: Missing API Key for style analysis')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果已经有标签,跳过(或者强制刷新?这里假设自动触发仅一次)
|
||||
if (ref.styleTags && ref.styleTags.length > 0) return
|
||||
|
||||
@@ -90,12 +121,12 @@ export const useAppStore = defineStore('app', () => {
|
||||
try {
|
||||
const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value })
|
||||
let fullTags = ''
|
||||
|
||||
|
||||
console.log(`Store: Analyzing style for reference [${index}]...`)
|
||||
await api.extractStyle(ref.content, (content) => {
|
||||
fullTags += content
|
||||
fullTags += content
|
||||
})
|
||||
|
||||
|
||||
// 提取标签 (假设返回格式为 "#标签1 #标签2")
|
||||
const tags = fullTags.match(/#[\w\u4e00-\u9fa5]+/g) || []
|
||||
ref.styleTags = tags
|
||||
@@ -123,29 +154,29 @@ export const useAppStore = defineStore('app', () => {
|
||||
try {
|
||||
const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value })
|
||||
const streamParser = createStreamParser()
|
||||
|
||||
|
||||
// 构建 Prompt(XML 结构化数据)
|
||||
const taskContent = inputType.value === 'outline'
|
||||
const taskContent = inputType.value === 'outline'
|
||||
? `核心主题:${outlinePoints.value.topic}\n目标受众:${outlinePoints.value.audience}\n关键观点:\n${outlinePoints.value.keyPoints}`
|
||||
: inputTask.value
|
||||
|
||||
const userMessage = buildPrompt(
|
||||
taskContent,
|
||||
[...selectedTags.value, customConstraint.value].filter(Boolean),
|
||||
taskContent,
|
||||
[...selectedTags.value, customConstraint.value].filter(Boolean),
|
||||
references.value
|
||||
)
|
||||
|
||||
console.log('Store: 开始 Ghostwriter Protocol 生成流程...', {
|
||||
console.log('Store: 开始 Ghostwriter Protocol 生成流程...', {
|
||||
hasParadigm: !!activeParadigm.value,
|
||||
paradigmName: activeParadigm.value?.name
|
||||
paradigmName: activeParadigm.value?.name
|
||||
})
|
||||
|
||||
|
||||
// 流式接收并实时解析 XML 结构,同时实时显示内容
|
||||
// 传入范式配置以使用专用 System Prompt
|
||||
await api.generateContent(userMessage, (chunk) => {
|
||||
rawStreamBuffer.value += chunk
|
||||
const { section, buffer } = streamParser.process(chunk)
|
||||
|
||||
|
||||
// 根据当前 section 更新 UI 状态
|
||||
if (section === 'thinking' && generationStage.value !== 'thinking') {
|
||||
generationStage.value = 'thinking'
|
||||
@@ -155,19 +186,19 @@ export const useAppStore = defineStore('app', () => {
|
||||
generationStage.value = 'draft'
|
||||
console.log('Store: 进入草稿生成阶段')
|
||||
}
|
||||
|
||||
|
||||
// 实时更新 UI 显示(流式输出)
|
||||
// 提取当前已接收的 thinking 和 draft 内容
|
||||
const thinkingMatch = buffer.match(/<thinking>([\s\S]*?)(?:<\/thinking>|$)/)
|
||||
const draftMatch = buffer.match(/<draft>([\s\S]*?)(?:<\/draft>|$)/)
|
||||
|
||||
|
||||
if (thinkingMatch) {
|
||||
thinkingContent.value = thinkingMatch[1].trim()
|
||||
}
|
||||
if (draftMatch) {
|
||||
generatedContent.value = draftMatch[1].trim()
|
||||
}
|
||||
|
||||
|
||||
// 如果 AI 没有按 XML 格式输出,直接显示原始内容
|
||||
if (!thinkingMatch && !draftMatch && buffer.length > 50) {
|
||||
generatedContent.value = buffer
|
||||
@@ -177,7 +208,7 @@ export const useAppStore = defineStore('app', () => {
|
||||
// 流式完成后,最终解析确保内容完整
|
||||
const { thinking, draft, hasStructuredOutput } = streamParser.getResult()
|
||||
console.log('Store: XML 解析完成', { hasStructuredOutput, thinkingLength: thinking.length, draftLength: draft.length })
|
||||
|
||||
|
||||
thinkingContent.value = thinking
|
||||
generatedContent.value = draft
|
||||
|
||||
@@ -187,12 +218,12 @@ export const useAppStore = defineStore('app', () => {
|
||||
if (activeParadigm.value && expertGuidelines.value.length > 0) {
|
||||
generationStage.value = 'inspect'
|
||||
console.log('Store: 进入质检阶段...')
|
||||
|
||||
|
||||
// 构建质检 Prompt
|
||||
const guidelinesText = expertGuidelines.value
|
||||
.map((g, i) => `${i + 1}. 【${g.title}】${g.description}`)
|
||||
.join('\n')
|
||||
|
||||
|
||||
const inspectPrompt = `你是一名严格的质检专家。请根据以下专家评价标准,逐条检查这篇文章的质量。
|
||||
|
||||
# 专家评价标准
|
||||
@@ -215,7 +246,7 @@ ${draft}
|
||||
await api.generateContent(inspectPrompt, (content) => {
|
||||
inspectResult += content
|
||||
}, { temperature: 0.3 })
|
||||
|
||||
|
||||
// 解析质检结果
|
||||
try {
|
||||
// 提取 JSON(可能被包裹在 markdown 代码块中)
|
||||
@@ -243,9 +274,9 @@ ${draft}
|
||||
generationStage.value = 'critique'
|
||||
console.log('Store: 进入批判阶段...')
|
||||
generatedContent.value += '\n\n---\n\n💡 **AI 深度反思 (Critique Stage)**:\n正在分析逻辑漏洞与改进空间...\n\n'
|
||||
|
||||
|
||||
const critiquePrompt = `你是一个严厉的主编。请分析以下文章的逻辑漏洞、论证强度和风格一致性。请列出3-5条具体的修改建议。不要重写文章,只提供建议。\n\n文章内容:\n${draft}`
|
||||
|
||||
|
||||
let critiqueContent = ''
|
||||
await api.generateContent(critiquePrompt, (content) => {
|
||||
generatedContent.value += content
|
||||
@@ -256,9 +287,9 @@ ${draft}
|
||||
generationStage.value = 'refine'
|
||||
console.log('Store: 进入修正阶段...')
|
||||
generatedContent.value += '\n\n---\n\n✨ **深度润色 (Refinement Stage)**:\n正在根据反思意见重写...\n\n'
|
||||
|
||||
|
||||
const refinePrompt = `你是一个专业的写作专家。请根据以下修改建议,重写这篇文章。保持原文的优秀风格,同时修复提到的问题。\n\n原文:\n${draft}\n\n修改建议:\n${critiqueContent}\n\n请直接输出重写后的正文:`
|
||||
|
||||
|
||||
await api.generateContent(refinePrompt, (content) => {
|
||||
generatedContent.value += content
|
||||
})
|
||||
@@ -293,7 +324,7 @@ ${draft}
|
||||
try {
|
||||
const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value })
|
||||
let fullContent = ''
|
||||
|
||||
|
||||
console.log('Store: 调用 API 分析文章...')
|
||||
await api.analyzeContent(text, (content) => {
|
||||
fullContent += content
|
||||
@@ -310,7 +341,7 @@ ${draft}
|
||||
// 分析完成后检测范式类型
|
||||
const detectedParadigm = detectParadigmFn(fullContent)
|
||||
console.log('Store: 检测到范式类型:', detectedParadigm.name)
|
||||
|
||||
|
||||
analysisResult.value = {
|
||||
paradigm: detectedParadigm.name,
|
||||
paradigmType: detectedParadigm.type,
|
||||
@@ -336,12 +367,12 @@ ${draft}
|
||||
const switchPage = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
|
||||
// 设置当前页面(别名)
|
||||
const setCurrentPage = (page) => {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
|
||||
// 设置当前文稿(用于从文稿列表打开编辑)
|
||||
const setCurrentDocument = (doc) => {
|
||||
if (doc) {
|
||||
@@ -360,11 +391,11 @@ ${draft}
|
||||
}
|
||||
|
||||
console.log('Store: Loading paradigm preset:', paradigm.name)
|
||||
|
||||
|
||||
// 1. 设置当前范式
|
||||
activeParadigm.value = paradigm
|
||||
expertGuidelines.value = paradigm.expertGuidelines || []
|
||||
|
||||
|
||||
// 2. 预填充大纲模板(如果有)
|
||||
if (paradigm.outlineTemplate) {
|
||||
inputType.value = 'outline'
|
||||
@@ -373,7 +404,7 @@ ${draft}
|
||||
inputType.value = 'text'
|
||||
inputTask.value = ''
|
||||
}
|
||||
|
||||
|
||||
// 3. 自动挂载默认参考案例
|
||||
if (paradigm.defaultReference) {
|
||||
references.value = [{
|
||||
@@ -383,22 +414,22 @@ ${draft}
|
||||
isAnalyzing: false
|
||||
}]
|
||||
}
|
||||
|
||||
|
||||
// 4. 自动选中推荐标签
|
||||
if (paradigm.recommendedTags) {
|
||||
selectedTags.value = [...paradigm.recommendedTags]
|
||||
}
|
||||
|
||||
|
||||
// 5. 注入系统约束到自定义要求(简化版,完整版在 specializedPrompt)
|
||||
if (paradigm.systemConstraints?.length > 0) {
|
||||
customConstraint.value = paradigm.systemConstraints.join(';')
|
||||
}
|
||||
|
||||
|
||||
// 6. 清空之前的生成内容
|
||||
generatedContent.value = ''
|
||||
thinkingContent.value = ''
|
||||
qualityReport.value = null
|
||||
|
||||
|
||||
// 7. 跳转到写作页面
|
||||
currentPage.value = 'writer'
|
||||
}
|
||||
@@ -416,6 +447,301 @@ ${draft}
|
||||
return api.generateContent(prompt, onContent, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* 范式写作:解析大纲模板为章节
|
||||
*/
|
||||
const parseOutlineToSections = (paradigmId) => {
|
||||
const paradigm = getParadigmById(paradigmId)
|
||||
if (!paradigm) return []
|
||||
|
||||
// 如果有 outlineTemplate,则解析它
|
||||
if (paradigm.outlineTemplate) {
|
||||
const template = paradigm.outlineTemplate
|
||||
const sections = []
|
||||
const lines = template.split('\n')
|
||||
|
||||
let currentMainSection = null
|
||||
|
||||
lines.forEach(line => {
|
||||
const h2Match = line.match(/^##\s+(.+)/)
|
||||
const h3Match = line.match(/^###\s+(.+)/)
|
||||
const listItemMatch = line.match(/^[-*]\s+(.+)/)
|
||||
|
||||
if (h2Match) {
|
||||
currentMainSection = h2Match[1].trim()
|
||||
sections.push({
|
||||
title: currentMainSection,
|
||||
type: 'h2',
|
||||
userInput: '',
|
||||
// 新增结构化字段
|
||||
inputType: 'idea', // 默认思路型
|
||||
corePoint: '',
|
||||
materialData: '',
|
||||
supplementNote: '',
|
||||
generatedContent: '',
|
||||
isGenerating: false
|
||||
})
|
||||
} else if (h3Match) {
|
||||
sections.push({
|
||||
title: h3Match[1].trim(),
|
||||
type: 'h3',
|
||||
parentTitle: currentMainSection,
|
||||
userInput: '',
|
||||
// 新增结构化字段
|
||||
inputType: 'idea',
|
||||
corePoint: '',
|
||||
materialData: '',
|
||||
supplementNote: '',
|
||||
generatedContent: '',
|
||||
isGenerating: false
|
||||
})
|
||||
} else if (listItemMatch) {
|
||||
// 如果是列表项,通常是具体的提示或占位符
|
||||
const content = listItemMatch[1].trim()
|
||||
if (sections.length > 0) {
|
||||
const lastSection = sections[sections.length - 1]
|
||||
if (!lastSection.placeholder) {
|
||||
lastSection.placeholder = content
|
||||
} else {
|
||||
lastSection.placeholder += '\n' + content
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
// 自定义范式没有 outlineTemplate:根据 expertGuidelines 生成基础章节
|
||||
const sections = []
|
||||
|
||||
// 添加默认的开篇章节
|
||||
sections.push({
|
||||
title: '开篇引言',
|
||||
type: 'h2',
|
||||
userInput: '',
|
||||
inputType: 'idea',
|
||||
corePoint: '',
|
||||
materialData: '',
|
||||
supplementNote: '',
|
||||
placeholder: '请输入文章的背景、目的或主题介绍...',
|
||||
generatedContent: '',
|
||||
isGenerating: false
|
||||
})
|
||||
|
||||
// 如果有 expertGuidelines,根据其内容生成章节
|
||||
if (paradigm.expertGuidelines && paradigm.expertGuidelines.length > 0) {
|
||||
// 分析 expertGuidelines 中是否有结构性建议
|
||||
const guidelines = paradigm.expertGuidelines
|
||||
|
||||
// 按 scope 分组,document 级别的可能包含结构要求
|
||||
const docLevelGuidelines = guidelines.filter(g =>
|
||||
(typeof g === 'object' && g.scope === 'document') ||
|
||||
(typeof g === 'string' && /章节|结构|部分|篇幅/.test(g))
|
||||
)
|
||||
|
||||
if (docLevelGuidelines.length > 0) {
|
||||
// 有文档级别指令,尝试提取章节建议
|
||||
docLevelGuidelines.forEach((g, i) => {
|
||||
const title = typeof g === 'object' ? g.title : `核心内容 ${i + 1}`
|
||||
sections.push({
|
||||
title: title,
|
||||
type: 'h2',
|
||||
userInput: '',
|
||||
inputType: 'idea',
|
||||
corePoint: '',
|
||||
materialData: '',
|
||||
supplementNote: '',
|
||||
placeholder: typeof g === 'object' ? g.description : g,
|
||||
generatedContent: '',
|
||||
isGenerating: false
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// 没有明确的结构要求,生成通用章节
|
||||
sections.push({
|
||||
title: '核心内容',
|
||||
type: 'h2',
|
||||
userInput: '',
|
||||
inputType: 'idea',
|
||||
corePoint: '',
|
||||
materialData: '',
|
||||
supplementNote: '',
|
||||
placeholder: '请输入主要论点、分析或内容...',
|
||||
generatedContent: '',
|
||||
isGenerating: false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// 没有 expertGuidelines,生成最基础的章节
|
||||
sections.push({
|
||||
title: '主体内容',
|
||||
type: 'h2',
|
||||
userInput: '',
|
||||
inputType: 'idea',
|
||||
corePoint: '',
|
||||
materialData: '',
|
||||
supplementNote: '',
|
||||
placeholder: '请输入文章的主要内容...',
|
||||
generatedContent: '',
|
||||
isGenerating: false
|
||||
})
|
||||
}
|
||||
|
||||
// 添加默认的结尾章节
|
||||
sections.push({
|
||||
title: '总结与展望',
|
||||
type: 'h2',
|
||||
userInput: '',
|
||||
inputType: 'idea',
|
||||
corePoint: '',
|
||||
materialData: '',
|
||||
supplementNote: '',
|
||||
placeholder: '请输入总结、建议或下一步计划...',
|
||||
generatedContent: '',
|
||||
isGenerating: false
|
||||
})
|
||||
|
||||
return sections
|
||||
}
|
||||
|
||||
/**
|
||||
* 范式写作:加载范式
|
||||
*/
|
||||
const loadParadigmForWriting = (paradigmId) => {
|
||||
const paradigm = getParadigmById(paradigmId)
|
||||
if (!paradigm) return
|
||||
|
||||
paradigmWriterState.value.selectedParadigmId = paradigmId
|
||||
paradigmWriterState.value.sections = parseOutlineToSections(paradigmId)
|
||||
paradigmWriterState.value.currentSectionIndex = 0
|
||||
|
||||
// 同时设置全局激活范式以便在预览中使用专家指令
|
||||
activeParadigm.value = paradigm
|
||||
expertGuidelines.value = paradigm.expertGuidelines || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 范式写作:生成单个章节内容
|
||||
*/
|
||||
const generateSectionContentAction = async (index) => {
|
||||
const section = paradigmWriterState.value.sections[index]
|
||||
if (!section) return
|
||||
|
||||
const paradigm = getParadigmById(paradigmWriterState.value.selectedParadigmId)
|
||||
if (!paradigm) return
|
||||
|
||||
section.isGenerating = true
|
||||
section.generatedContent = ''
|
||||
|
||||
try {
|
||||
const api = new DeepSeekAPI({ url: apiUrl.value, key: apiKey.value })
|
||||
const streamParser = createStreamParser()
|
||||
|
||||
// 构建上文内容,保持连贯性
|
||||
const previousContent = paradigmWriterState.value.sections
|
||||
.slice(0, index)
|
||||
.filter(s => s.generatedContent)
|
||||
.map(s => `## ${s.title}\n${s.generatedContent}`)
|
||||
.join('\n\n')
|
||||
|
||||
// 判断输入类型
|
||||
const isMaterialType = section.inputType === 'material'
|
||||
|
||||
// 构建结构化输入内容
|
||||
const corePointText = section.corePoint?.trim() || ''
|
||||
const materialDataText = section.materialData?.trim() || ''
|
||||
const supplementNoteText = section.supplementNote?.trim() || section.placeholder || ''
|
||||
|
||||
// 兼容旧数据:如果新字段都为空,则使用旧的 userInput
|
||||
const hasStructuredInput = corePointText || materialDataText || supplementNoteText
|
||||
const userInputFallback = section.userInput?.trim() || ''
|
||||
|
||||
// 根据输入类型构建不同的 Prompt
|
||||
let inputSection = ''
|
||||
if (hasStructuredInput) {
|
||||
inputSection = `
|
||||
【用户输入】
|
||||
|
||||
📌 核心论点:
|
||||
${corePointText || '(未提供,请根据章节主题自由发挥)'}
|
||||
|
||||
📊 数据/案例:
|
||||
${materialDataText ? (isMaterialType
|
||||
? `⚠️ 【严格模式】以下内容必须100%完整保留,不可改写、删除或替换任何数据、数字、名称:\n${materialDataText}`
|
||||
: `以下内容可进行适当润色,但核心事实需保留:\n${materialDataText}`)
|
||||
: '(未提供)'}
|
||||
|
||||
💡 补充说明:
|
||||
${supplementNoteText || '(无特殊要求)'}
|
||||
`
|
||||
} else if (userInputFallback) {
|
||||
// 兼容旧数据
|
||||
inputSection = `
|
||||
【用户提供的写作要点】:
|
||||
${userInputFallback}
|
||||
`
|
||||
} else {
|
||||
inputSection = `
|
||||
【用户提供的写作要点】:
|
||||
请根据章节标题自主发挥
|
||||
`
|
||||
}
|
||||
|
||||
const prompt = `你正在按照「${paradigm.name}」范式进行写作。
|
||||
现在请撰写章节:${section.title}
|
||||
|
||||
${section.parentTitle ? `属于:${section.parentTitle}` : ''}
|
||||
${inputSection}
|
||||
---
|
||||
${previousContent ? `【已生成的上文参考】:\n${previousContent}\n\n---` : ''}
|
||||
|
||||
【写作要求】:
|
||||
1. 严格遵循 "${paradigm.name}" 的专家标准和文风。
|
||||
2. 仅输出当前章节 "${section.title}" 的内容,不要包含标题。
|
||||
3. 如果上文已提及相关背景,请简洁衔接,避免重复。
|
||||
${isMaterialType && materialDataText ? '4. ⚠️ 特别注意:用户提供的数据/案例必须原文引用,不可改动任何数字或事实。' : ''}
|
||||
|
||||
开始写作:`
|
||||
|
||||
// 使用流式解析,仅提取 <draft> 内容
|
||||
await api.generateContent(prompt, (chunk) => {
|
||||
const { buffer } = streamParser.process(chunk)
|
||||
|
||||
// 实时更新当前章节内容 - 只要 draft
|
||||
const draftMatch = buffer.match(/<draft>([\s\S]*?)(?:<\/draft>|$)/)
|
||||
if (draftMatch) {
|
||||
section.generatedContent = draftMatch[1].trim()
|
||||
} else if (!buffer.includes('<thinking>') && buffer.length > 50) {
|
||||
// fallback: 如果 AI 没吐格式,且吐了一定长度了,则认为是直接内容
|
||||
section.generatedContent = buffer
|
||||
}
|
||||
|
||||
updateTotalGeneratedContent()
|
||||
}, { paradigm })
|
||||
|
||||
} catch (error) {
|
||||
console.error('Section generation failed:', error)
|
||||
section.generatedContent = `[生成失败: ${error.message}]`
|
||||
} finally {
|
||||
section.isGenerating = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将所有章节内容合并到主预览区
|
||||
*/
|
||||
const updateTotalGeneratedContent = () => {
|
||||
const fullContent = paradigmWriterState.value.sections
|
||||
.map(s => {
|
||||
const titlePrefix = s.type === 'h2' ? '## ' : '### '
|
||||
return `${titlePrefix}${s.title}\n\n${s.generatedContent}`
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
generatedContent.value = fullContent
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
currentPage,
|
||||
@@ -446,6 +772,19 @@ ${draft}
|
||||
newRefTitle,
|
||||
newRefContent,
|
||||
|
||||
// 模型配置
|
||||
selectedProviderId,
|
||||
currentProvider,
|
||||
|
||||
// 范式写作
|
||||
paradigmWriterState,
|
||||
loadParadigmForWriting,
|
||||
generateSectionContentAction,
|
||||
updateTotalGeneratedContent,
|
||||
|
||||
// 范式编辑
|
||||
paradigmEditState,
|
||||
|
||||
// 方法
|
||||
switchPage,
|
||||
setCurrentPage,
|
||||
|
||||
@@ -1,9 +1,61 @@
|
||||
// 环境变量配置
|
||||
// 模型服务商配置列表
|
||||
export const modelProviders = {
|
||||
deepseek: {
|
||||
id: 'deepseek',
|
||||
name: 'DeepSeek',
|
||||
description: '深度求索',
|
||||
apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL || 'https://api.deepseek.com/chat/completions',
|
||||
apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY || '',
|
||||
model: import.meta.env.VITE_DEEPSEEK_MODEL || 'deepseek-chat'
|
||||
},
|
||||
openai: {
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
description: 'GPT 系列',
|
||||
apiUrl: import.meta.env.VITE_OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
|
||||
apiKey: import.meta.env.VITE_OPENAI_API_KEY || '',
|
||||
model: import.meta.env.VITE_OPENAI_MODEL || 'gpt-4o'
|
||||
},
|
||||
claude: {
|
||||
id: 'claude',
|
||||
name: 'Claude',
|
||||
description: 'Anthropic Claude',
|
||||
apiUrl: import.meta.env.VITE_CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
|
||||
apiKey: import.meta.env.VITE_CLAUDE_API_KEY || '',
|
||||
model: import.meta.env.VITE_CLAUDE_MODEL || 'claude-3-5-sonnet'
|
||||
},
|
||||
custom: {
|
||||
id: 'custom',
|
||||
name: '自定义',
|
||||
description: '自定义 API 端点',
|
||||
apiUrl: import.meta.env.VITE_CUSTOM_API_URL || '',
|
||||
apiKey: import.meta.env.VITE_CUSTOM_API_KEY || '',
|
||||
model: import.meta.env.VITE_CUSTOM_MODEL || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 获取已配置的服务商列表(有 API Key 的)
|
||||
export const getConfiguredProviders = () => {
|
||||
return Object.values(modelProviders).filter(p => p.apiKey && p.apiKey.length > 0)
|
||||
}
|
||||
|
||||
// 获取默认服务商
|
||||
export const getDefaultProvider = () => {
|
||||
const configured = getConfiguredProviders()
|
||||
return configured.length > 0 ? configured[0] : modelProviders.deepseek
|
||||
}
|
||||
|
||||
// 兼容旧版配置
|
||||
export const config = {
|
||||
// API 配置
|
||||
apiUrl: import.meta.env.VITE_API_URL || 'https://api.deepseek.com/chat/completions',
|
||||
apiKey: import.meta.env.VITE_API_KEY || 'YOUR_KEY',
|
||||
|
||||
// 向后兼容:使用第一个已配置的服务商
|
||||
get apiUrl() {
|
||||
return getDefaultProvider().apiUrl
|
||||
},
|
||||
get apiKey() {
|
||||
return getDefaultProvider().apiKey
|
||||
},
|
||||
|
||||
// 应用配置
|
||||
appVersion: '1.0.0',
|
||||
isDev: import.meta.env.DEV,
|
||||
@@ -13,23 +65,20 @@ export const config = {
|
||||
// 验证必需的环境变量
|
||||
export const validateConfig = () => {
|
||||
const errors = []
|
||||
|
||||
if (!config.apiUrl) {
|
||||
errors.push('API URL 未配置')
|
||||
const configured = getConfiguredProviders()
|
||||
|
||||
if (configured.length === 0) {
|
||||
errors.push('未配置任何模型服务商的 API Key')
|
||||
}
|
||||
|
||||
if (!config.apiKey || config.apiKey === 'YOUR_KEY') {
|
||||
errors.push('API Key 未配置或使用默认值')
|
||||
}
|
||||
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
// 获取配置摘要(用于调试)
|
||||
export const getConfigSummary = () => {
|
||||
return {
|
||||
apiUrl: config.apiUrl,
|
||||
hasApiKey: config.apiKey !== 'YOUR_KEY',
|
||||
configuredProviders: getConfiguredProviders().map(p => p.name),
|
||||
defaultProvider: getDefaultProvider().name,
|
||||
mode: config.mode,
|
||||
isDev: config.isDev
|
||||
}
|
||||
|
||||
@@ -131,7 +131,8 @@ export function buildParadigmObject(parsedConfig, sourceDocPath = null) {
|
||||
id,
|
||||
name: parsedConfig.metadata.name,
|
||||
description: parsedConfig.metadata.description,
|
||||
type: 'custom', // 标记为自定义范式
|
||||
type: 'custom',
|
||||
isCustom: true, // 标记为自定义范式
|
||||
createdAt: new Date().toISOString(),
|
||||
sourceDoc: sourceDocPath,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user