chore: 集成设计系统和完善范式相关组件

## 主要改进
- index.html:引入设计系统样式文件
- package.json:添加 playwright 依赖用于测试
- ParadigmSelectorModal.vue:添加"新建范式"按钮
- 其他组件:更新以支持自定义范式功能

## 技术改进
- 统一设计 token 和组件样式
- 完善范式选择和创建流程

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
empty
2026-01-11 14:03:01 +08:00
parent 5a3cec6600
commit d1c9a4a5dd
12 changed files with 2474 additions and 439 deletions

View File

@@ -1,76 +1,69 @@
<template>
<aside class="w-[400px] flex flex-col border-r border-slate-700 bg-slate-800 shrink-0 h-screen">
<aside class="analysis-panel">
<!-- 头部 -->
<header class="p-4 border-b border-slate-700 flex items-center justify-between shrink-0">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl">🎯</span> 写作范式分析
<header class="analysis-header">
<h1 class="analysis-header-title">
<span style="font-size: var(--text-xl)">🎯</span> 写作范式分析
</h1>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
<span class="badge badge-primary">Pro版</span>
</header>
<!-- 内容区 - 添加 min-h-0 确保滚动正常 -->
<div class="flex-1 overflow-y-auto p-4 space-y-6 min-h-0">
<!-- 内容区 -->
<div class="analysis-content">
<!-- 写作范式库 -->
<section>
<section class="analysis-section">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-medium text-slate-400">📚 写作范式库</h3>
<button
<h3 class="text-sm font-medium text-secondary">📚 写作范式库</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"
class="btn btn-analysis btn-success flex items-center gap-1"
>
<span>+</span> 新增范式
</button>
</div>
<div class="space-y-3">
<div
v-for="paradigm in paradigms"
<div
v-for="paradigm in paradigms"
:key="paradigm.id"
@click="selectParadigm(paradigm)"
:class="['bg-slate-700/50 rounded-lg p-4 border transition cursor-pointer',
selectedParadigm?.id === paradigm.id
? 'border-blue-500 bg-blue-900/20'
: 'border-slate-600 hover:border-blue-500']"
:class="['paradigm-card', { 'selected': selectedParadigm?.id === paradigm.id }]"
>
<div class="flex justify-between items-start mb-2">
<h4 class="font-medium text-white flex items-center gap-2">
<div class="paradigm-card-header">
<h4 class="paradigm-card-title">
{{ paradigm.icon }} {{ paradigm.name }}
<span v-if="paradigm.isNew" class="text-[10px] px-1.5 py-0.5 rounded bg-orange-500 text-white font-bold animate-pulse">
NEW
</span>
<span v-if="paradigm.isCustom" class="text-[10px] px-1.5 py-0.5 rounded bg-purple-500 text-white font-bold">
自定义
</span>
<span v-if="paradigm.isNew" class="badge-new animate-pulse">NEW</span>
<span v-if="paradigm.isCustom" class="badge-custom">自定义</span>
</h4>
<div class="flex items-center gap-1">
<button
<button
@click.stop="openEditModal(paradigm)"
class="text-xs px-2 py-1 bg-slate-600 text-white rounded hover:bg-slate-500 transition"
class="btn-action"
title="编辑范式"
>
</button>
<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"
class="btn-action danger"
title="删除范式"
>
🗑
</button>
<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"
class="btn-action primary"
>
应用到写作
</button>
</div>
</div>
<p class="text-xs text-slate-400 mb-2">{{ paradigm.description }}</p>
<p class="text-xs text-muted mb-2">{{ paradigm.description }}</p>
<div class="flex flex-wrap gap-1">
<span
v-for="tag in paradigm.tags"
<span
v-for="tag in paradigm.tags"
:key="tag"
:class="['text-xs px-2 py-1 rounded', paradigm.tagClass]"
>
@@ -82,24 +75,24 @@
</section>
<!-- 范式分析工具 -->
<section>
<h3 class="text-sm font-medium text-slate-400 mb-4">🔍 范式分析工具</h3>
<textarea
<section class="analysis-section">
<h3 class="text-sm font-medium text-secondary mb-4">🔍 范式分析工具</h3>
<textarea
v-model="analysisText"
class="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition placeholder-slate-600 resize-none"
class="analysis-textarea"
placeholder="粘贴文章内容,分析其写作范式..."
></textarea>
<div class="mt-2 flex gap-2">
<button
<div class="flex gap-2 mt-2">
<button
@click="analyzeArticle"
:disabled="isAnalyzing || !analysisText"
class="flex-1 bg-blue-600 hover:bg-blue-500 text-white py-2 rounded-lg text-sm font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"
class="btn btn-analysis primary flex-1"
>
{{ isAnalyzing ? '正在分析...' : '分析文章范式' }}
</button>
<button
<button
@click="clearAnalysis"
class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg text-sm font-medium transition"
class="btn btn-analysis secondary"
>
清空
</button>
@@ -107,48 +100,47 @@
</section>
<!-- 分析历史 -->
<section v-if="analysisHistory.length > 0">
<h3 class="text-sm font-medium text-slate-400 mb-4">📝 分析历史</h3>
<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"
<div
v-for="(item, index) in analysisHistory"
:key="index"
@click="loadHistoryItem(item)"
class="bg-slate-700/30 rounded p-3 cursor-pointer hover:bg-slate-700/50 transition text-xs"
class="history-item"
>
<div class="flex justify-between items-center">
<span class="text-slate-300">{{ item.paradigm }}</span>
<span class="text-slate-500">{{ formatDate(item.timestamp) }}</span>
<span class="text-secondary">{{ item.paradigm }}</span>
<span class="text-muted">{{ formatDate(item.timestamp) }}</span>
</div>
<p class="text-slate-500 mt-1 truncate">{{ item.preview }}</p>
<p class="text-muted mt-1 truncate">{{ item.preview }}</p>
</div>
</div>
</section>
</div>
<!-- 编辑/新增范式弹窗 -->
<div
<div
v-if="showEditModal"
class="fixed inset-0 bg-black/60 flex items-center justify-center z-50"
class="paradigm-modal-backdrop"
@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 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-slate-400 mb-2">图标</label>
<div class="flex gap-2 flex-wrap">
<button
v-for="icon in iconOptions"
<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="['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']"
:class="['icon-option', { 'selected': editForm.icon === icon }]"
>
{{ icon }}
</button>
@@ -157,36 +149,36 @@
<!-- 名称 -->
<div>
<label class="block text-sm text-slate-400 mb-2">范式名称</label>
<input
<label class="block text-sm text-secondary 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"
class="analysis-input"
placeholder="如:技术博客范式"
/>
</div>
<!-- 描述 -->
<div>
<label class="block text-sm text-slate-400 mb-2">描述</label>
<textarea
<label class="block text-sm text-secondary 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"
class="analysis-textarea"
placeholder="简要描述此范式的适用场景"
></textarea>
</div>
<!-- 标签 -->
<div>
<label class="block text-sm text-slate-400 mb-2">标签逗号分隔</label>
<input
<label class="block text-sm text-secondary 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"
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)"
<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]"
>
@@ -197,14 +189,13 @@
<!-- 标签颜色 -->
<div>
<label class="block text-sm text-slate-400 mb-2">标签颜色</label>
<label class="block text-sm text-secondary mb-2">标签颜色</label>
<div class="flex gap-2 flex-wrap">
<button
v-for="color in colorOptions"
<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' : '']"
:class="['color-option', color.class, { 'selected': editForm.tagClass === color.class }]"
>
{{ color.label }}
</button>
@@ -212,15 +203,15 @@
</div>
<!-- 核心字段完整 Prompt -->
<div class="border-t border-slate-700 pt-4 mt-4">
<label class="block text-sm text-slate-400 mb-2 flex items-center gap-2">
<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-slate-500 font-normal">粘贴您的完整提示词</span>
<span class="text-xs text-muted font-normal">粘贴您的完整提示词</span>
</label>
<textarea
<textarea
v-model="editForm.specializedPrompt"
rows="12"
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition resize-y font-mono leading-relaxed"
class="analysis-textarea resize-y font-mono leading-relaxed"
placeholder="在此粘贴您的完整 Prompt...
示例:
@@ -235,23 +226,23 @@
2. 深度分析思想根源
..."
></textarea>
<p class="text-xs text-slate-500 mt-2">
<p class="text-xs text-muted mt-2">
💡 提示这是 AI 调用时的核心指令完整的 Prompt 应包含角色目标步骤和输出格式
</p>
</div>
</div>
<div class="p-4 border-t border-slate-700 flex justify-end gap-2">
<button
<div class="p-4 border-t border-default 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"
class="btn btn-analysis secondary"
>
取消
</button>
<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"
class="btn btn-analysis primary"
>
{{ isAddMode ? '添加' : '保存' }}
</button>
@@ -592,3 +583,287 @@ onMounted(() => {
}
})
</script>
<style scoped>
/* ========== 使用设计令牌系统 ========== */
/* 侧边栏容器 */
.analysis-panel {
width: 400px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-default);
background: var(--bg-secondary);
flex-shrink: 0;
height: 100vh;
}
/* 头部 */
.analysis-header {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.analysis-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);
}
/* 内容区 */
.analysis-content {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
}
.analysis-section {
margin-bottom: var(--space-6);
}
.analysis-section:last-child {
margin-bottom: 0;
}
/* 范式卡片 */
.paradigm-card {
background: var(--bg-elevated);
border-radius: var(--radius-lg);
padding: var(--space-4);
border: 1px solid var(--border-default);
transition: all var(--transition-fast);
cursor: pointer;
}
.paradigm-card:hover {
border-color: var(--accent-primary);
}
.paradigm-card.selected {
border-color: var(--accent-primary);
background: var(--info-bg);
}
.paradigm-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--space-2);
}
.paradigm-card-title {
font-weight: var(--font-medium);
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2);
}
/* 徽章 */
.badge-new {
font-size: 10px;
padding: 2px 6px;
border-radius: var(--radius-sm);
background: var(--accent-warning);
color: var(--text-inverse);
font-weight: var(--font-semibold);
}
.badge-custom {
font-size: 10px;
padding: 2px 6px;
border-radius: var(--radius-sm);
background: #a855f7;
color: var(--text-inverse);
font-weight: var(--font-semibold);
}
/* 分析历史项 */
.history-item {
background: var(--bg-elevated);
border-radius: var(--radius-md);
padding: var(--space-3);
cursor: pointer;
transition: all var(--transition-fast);
}
.history-item:hover {
background: var(--bg-sunken);
}
/* 模态框 */
.paradigm-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal-backdrop);
}
.paradigm-modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
width: 500px;
max-height: 80vh;
overflow-y: auto;
border: 1px solid var(--border-default);
box-shadow: var(--shadow-lg);
}
/* 图标选择器 */
.icon-selector {
display: flex;
gap: var(--space-2);
flex-wrap: wrap;
}
.icon-option {
width: 40px;
height: 40px;
border-radius: var(--radius-lg);
font-size: var(--text-xl);
display: flex;
align-items: center;
justify-content: center;
transition: all var(--transition-fast);
}
.icon-option.selected {
background: var(--accent-primary);
}
.icon-option:not(.selected) {
background: var(--bg-elevated);
}
.icon-option:not(.selected):hover {
background: var(--bg-sunken);
}
/* 颜色选择器 */
.color-option {
padding: var(--space-1) var(--space-3);
border-radius: var(--radius-md);
font-size: var(--text-xs);
transition: all var(--transition-fast);
}
.color-option.selected {
box-shadow: 0 0 0 2px var(--text-primary);
}
/* 输入框 */
.analysis-input {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-3);
font-size: var(--text-sm);
}
.analysis-input:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--info-bg);
}
.analysis-textarea {
width: 100%;
height: 128px;
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-3);
font-size: var(--text-sm);
resize: none;
}
.analysis-textarea:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--info-bg);
}
/* 按钮 */
.btn-analysis {
font-size: var(--text-xs);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
border: none;
cursor: pointer;
}
.btn-analysis.primary {
background: var(--accent-primary);
color: var(--text-inverse);
}
.btn-analysis.primary:hover {
background: var(--accent-primary-hover);
}
.btn-analysis.secondary {
background: var(--bg-elevated);
color: var(--text-primary);
}
.btn-analysis.secondary:hover {
background: var(--bg-sunken);
}
.btn-analysis:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-action {
font-size: var(--text-xs);
padding: var(--space-2);
background: var(--bg-elevated);
color: var(--text-primary);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.btn-action:hover {
background: var(--bg-sunken);
}
.btn-action.primary {
background: var(--accent-primary);
color: var(--text-inverse);
}
.btn-action.primary:hover {
background: var(--accent-primary-hover);
}
.btn-action.danger {
background: var(--accent-danger);
color: var(--text-inverse);
}
.btn-action.danger:hover {
opacity: 0.9;
}
.btn-action.success {
background: var(--accent-success);
color: var(--text-inverse);
}
.btn-action.success:hover {
opacity: 0.9;
}
</style>

View File

@@ -1764,4 +1764,326 @@ const runCompare = async () => {
writing-mode: vertical-rl;
text-orientation: mixed;
}
/* ========== 使用设计令牌系统 ========== */
/* 页面容器 */
.compare-page {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
/* 头部 */
.compare-header {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
background: var(--bg-secondary);
display: flex;
align-items: center;
justify-content: between;
flex-shrink: 0;
}
.compare-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);
}
/* 范式上下文提示条 */
.paradigm-bar {
padding: var(--space-2) var(--space-4);
background: var(--info-bg);
border-bottom: 1px solid var(--accent-primary);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
/* 主体内容区 */
.compare-main {
flex: 1;
display: flex;
overflow: hidden;
}
/* 面板区域 */
.panel-section {
display: flex;
flex-direction: column;
min-width: 0;
}
.panel-left {
width: 40%;
border-right: 1px solid var(--border-default);
}
.panel-right {
flex: 1;
}
/* 面板头部 */
.panel-header {
padding: var(--space-3);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-title {
font-size: var(--text-sm);
font-weight: var(--font-medium);
display: flex;
align-items: center;
gap: var(--space-2);
}
.panel-title-left {
color: var(--accent-warning);
}
.panel-title-right {
color: var(--accent-primary);
}
/* 段落卡片 */
.paragraph-card {
padding: var(--space-3);
border-radius: var(--radius-lg);
border: 1px solid var(--border-default);
cursor: pointer;
transition: all var(--transition-fast);
}
.paragraph-card:hover {
border-color: var(--accent-primary);
}
.paragraph-card-selected-left {
background: var(--warning-bg);
border-color: var(--accent-warning);
box-shadow: 0 0 0 2px var(--accent-warning);
}
.paragraph-card-selected-right {
background: var(--info-bg);
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--accent-primary);
}
/* 底部操作区 */
.compare-footer {
padding: var(--space-4);
background: var(--bg-secondary);
border-top: 1px solid var(--border-default);
flex-shrink: 0;
}
/* 按钮统一使用 .btn 类 */
.btn-header {
font-size: var(--text-xs);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
background: var(--bg-elevated);
color: var(--text-secondary);
transition: all var(--transition-fast);
border: none;
cursor: pointer;
}
.btn-header:hover {
background: var(--bg-sunken);
color: var(--text-primary);
}
.btn-primary {
background: var(--accent-primary);
color: var(--text-inverse);
}
.btn-primary:hover {
background: var(--accent-primary-hover);
}
.btn-secondary {
background: var(--bg-elevated);
color: var(--text-primary);
}
/* 视图切换器 */
.view-toggle-group {
display: flex;
background: var(--bg-primary);
padding: var(--space-1);
border-radius: var(--radius-lg);
gap: var(--space-1);
}
.view-toggle-btn {
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
font-weight: var(--font-medium);
border-radius: var(--radius-md);
color: var(--text-secondary);
transition: all var(--transition-fast);
cursor: pointer;
border: none;
background: transparent;
}
.view-toggle-btn:hover {
color: var(--text-primary);
}
.view-toggle-btn.active {
background: var(--bg-secondary);
color: var(--accent-primary);
box-shadow: var(--shadow-xs);
}
/* 输入框 */
.panel-textarea {
width: 100%;
height: 96px;
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-3);
font-size: var(--text-sm);
color: var(--text-primary);
}
.panel-textarea:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--info-bg);
}
/* 模态框 */
.rewrite-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal-backdrop);
}
.rewrite-modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
width: 900px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: var(--shadow-lg);
}
.rewrite-modal-header {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
}
.rewrite-modal-body {
flex: 1;
overflow-y: auto;
min-height: 300px;
}
.rewrite-modal-footer {
padding: var(--space-4);
border-top: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
}
/* 差异对比区域 */
.diff-box {
background: var(--bg-primary);
border-radius: var(--radius-lg);
padding: var(--space-3);
border: 1px solid var(--border-default);
}
/* 差异片段样式 */
.diff-unchanged {
color: var(--text-muted);
}
.diff-removed {
background: var(--warning-bg);
color: var(--accent-warning);
padding: 2px 4px;
border-radius: var(--radius-sm);
text-decoration: line-through;
}
.diff-added {
background: var(--success-bg);
color: var(--accent-success);
padding: 2px 4px;
border-radius: var(--radius-sm);
}
.diff-modified {
background: var(--warning-bg);
color: var(--accent-warning);
padding: 2px 4px;
border-radius: var(--radius-sm);
}
.diff-selected-left {
background: rgba(245, 158, 11, 0.4);
color: #fef3c7;
box-shadow: 0 0 0 2px var(--accent-warning);
}
.diff-selected-right {
background: rgba(96, 165, 250, 0.4);
color: #dbeafe;
box-shadow: 0 0 0 2px var(--accent-primary);
}
/* 检查结果状态 */
.check-pass {
background: var(--success-bg);
color: var(--accent-success);
}
.check-warning {
background: var(--warning-bg);
color: var(--accent-warning);
}
.check-fail {
background: var(--danger-bg);
color: var(--accent-danger);
}
/* 徽章 */
.badge-count {
font-size: var(--text-xs);
color: var(--text-muted);
}
/* 进度指示器 */
.progress-text {
font-size: var(--text-xs);
color: var(--text-secondary);
}
</style>

View File

@@ -437,3 +437,309 @@ const exportToWord = async () => {
}
}
</script>
<style scoped>
/* ========== 使用设计令牌系统 ========== */
/* 页面容器 */
.diff-page {
height: 100vh;
width: 100%;
display: flex;
flex-direction: column;
background: var(--bg-primary);
}
/* 头部工具栏 */
.diff-header {
height: 56px;
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-6);
background: var(--bg-primary);
flex-shrink: 0;
}
.diff-header-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
display: flex;
align-items: center;
gap: var(--space-2);
}
/* 图例说明 */
.diff-legend {
font-size: var(--text-xs);
color: var(--text-muted);
display: flex;
align-items: center;
gap: var(--space-3);
}
.diff-legend-item {
display: flex;
align-items: center;
gap: var(--space-1);
}
.diff-legend-dot {
width: 12px;
height: 12px;
border-radius: var(--radius-md);
}
.diff-legend-dot.modified {
background: rgba(245, 158, 11, 0.5);
}
.diff-legend-dot.added {
background: rgba(34, 197, 94, 0.5);
}
.diff-legend-dot.removed {
background: rgba(239, 68, 68, 0.5);
}
/* 差异统计 */
.diff-stats {
font-size: var(--text-xs);
color: var(--text-secondary);
display: flex;
align-items: center;
gap: var(--space-3);
border-left: 1px solid var(--border-default);
padding-left: var(--space-4);
}
.diff-stats .modified {
color: var(--accent-warning);
}
.diff-stats .added {
color: var(--accent-success);
}
.diff-stats .removed {
color: var(--accent-danger);
}
/* 主内容区 */
.diff-main {
flex: 1;
display: flex;
width: 100%;
overflow: hidden;
}
/* 面板区域 */
.diff-panel {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
}
.diff-panel-left {
border-right: 1px solid var(--border-default);
}
/* 面板头部 */
.diff-panel-header {
padding: var(--space-3);
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.diff-panel-title {
font-size: var(--text-sm);
font-weight: var(--font-medium);
display: flex;
align-items: center;
gap: var(--space-2);
}
.diff-panel-title.left {
color: var(--accent-warning);
}
.diff-panel-title.right {
color: var(--accent-primary);
}
/* 差异显示区 */
.diff-display {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
background: rgba(15, 23, 42, 0.5);
min-height: 0;
}
.diff-content {
font-size: var(--text-sm);
line-height: 1.6;
white-space: pre-wrap;
}
/* 差异片段样式 */
.diff-unchanged {
color: var(--text-secondary);
}
.diff-modified {
background: rgba(245, 158, 11, 0.3);
color: #fed7aa;
padding: 2px 4px;
border-radius: var(--radius-sm);
}
.diff-removed {
background: rgba(239, 68, 68, 0.3);
color: #fecaca;
padding: 2px 4px;
border-radius: var(--radius-sm);
text-decoration: line-through;
}
.diff-added {
background: rgba(34, 197, 94, 0.3);
color: #bbf7d0;
padding: 2px 4px;
border-radius: var(--radius-sm);
}
/* 空状态 */
.diff-empty-state {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.diff-empty-icon {
font-size: 3rem;
margin-bottom: var(--space-3);
opacity: 0.3;
}
/* 可拖动分割线 */
.diff-resizer {
height: 8px;
background: var(--bg-elevated);
cursor: ns-resize;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: background var(--transition-fast);
}
.diff-resizer:hover {
background: var(--border-strong);
}
.diff-resizer-handle {
width: 48px;
height: 4px;
background: var(--text-muted);
border-radius: var(--radius-full);
}
/* 输入区 */
.diff-input-area {
padding: var(--space-3);
border-top: 1px solid var(--border-default);
background: var(--bg-secondary);
flex-shrink: 0;
}
.diff-textarea {
width: 100%;
height: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-3);
font-size: var(--text-sm);
color: var(--text-primary);
}
.diff-textarea:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--info-bg);
}
/* 按钮样式 */
.btn-diff {
font-size: var(--text-xs);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
border: none;
cursor: pointer;
}
.btn-diff-secondary {
background: var(--bg-elevated);
color: var(--text-secondary);
}
.btn-diff-secondary:hover {
background: var(--bg-sunken);
color: var(--text-primary);
}
.btn-diff-primary {
background: var(--accent-primary);
color: var(--text-inverse);
}
.btn-diff-primary:hover {
background: var(--accent-primary-hover);
}
.btn-diff:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 来源切换器 */
.source-toggle {
display: flex;
background: var(--bg-primary);
padding: var(--space-1);
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
}
.source-toggle-btn {
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.source-toggle-btn.active {
background: var(--accent-primary);
color: var(--text-inverse);
}
.source-toggle-btn:not(.active) {
color: var(--text-secondary);
}
.source-toggle-btn:not(.active):hover {
color: var(--text-primary);
}
</style>

View File

@@ -1,62 +1,56 @@
<template>
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
<aside class="docs-panel">
<!-- 头部 -->
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl">📂</span> 文稿管理
<header class="docs-header">
<h1 class="docs-header-title">
<span style="font-size: var(--text-xl)">📂</span> 文稿管理
</h1>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
<span class="badge badge-primary">Pro版</span>
</header>
<!-- 工具栏 -->
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<div class="docs-toolbar">
<div class="flex gap-2">
<button
v-for="filter in statusFilters"
<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']"
:class="['filter-btn', { 'active': currentFilter === filter.value }]"
>
{{ filter.label }} ({{ getCountByStatus(filter.value) }})
</button>
</div>
<button
<button
@click="createNewDocument"
class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition"
class="create-btn"
>
+ 新建文稿
</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>
<div class="docs-list">
<div v-if="filteredDocuments.length === 0" class="text-center text-muted py-8">
<span style="font-size: var(--text-2xl); display: block; margin-bottom: var(--space-2)">📄</span>
<p class="text-sm">暂无文稿</p>
</div>
<div
v-for="doc in filteredDocuments"
<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']"
:class="['doc-card', { 'selected': selectedDocId === doc.id }]"
>
<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]]">
<h3 class="font-medium text-primary text-sm truncate flex-1">{{ doc.title }}</h3>
<span :class="['status-badge', statusStyles[doc.status]]">
{{ statusLabels[doc.status] }}
</span>
</div>
<p class="text-xs text-slate-500 line-clamp-2 mb-2">
<p class="text-xs text-muted 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">
<div class="flex items-center justify-between text-xs text-muted">
<span>{{ doc.word_count || 0 }} </span>
<span>{{ formatDate(doc.updated_at) }}</span>
</div>
@@ -64,29 +58,29 @@
</div>
<!-- 底部操作区 -->
<div v-if="selectedDocId" class="p-4 border-t border-slate-700 space-y-2">
<div v-if="selectedDocId" class="docs-footer">
<div class="flex gap-2">
<button
<button
@click="openDocument"
class="flex-1 text-xs py-2 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
class="action-btn primary"
>
📝 编辑
</button>
<button
<button
@click="duplicateDocument"
class="flex-1 text-xs py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
class="action-btn secondary"
>
📋 复制
</button>
<button
<button
@click="toggleVersionPanel"
class="flex-1 text-xs py-2 rounded bg-cyan-600 text-white hover:bg-cyan-500 transition"
class="action-btn success"
>
🕰 版本
</button>
<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"
class="action-btn danger"
>
🗑
</button>
@@ -94,20 +88,20 @@
</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
<div v-if="showDeleteConfirm" class="confirm-modal-backdrop">
<div class="confirm-modal">
<h3 class="confirm-modal-title">确认删除</h3>
<p class="confirm-modal-text">确定要删除这篇文稿吗此操作不可恢复</p>
<div class="confirm-modal-actions">
<button
@click="showDeleteConfirm = false"
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
class="action-btn secondary"
>
取消
</button>
<button
<button
@click="deleteSelectedDocument"
class="flex-1 py-2 rounded bg-red-600 text-white hover:bg-red-500 transition"
class="action-btn danger"
>
删除
</button>
@@ -154,9 +148,9 @@ const statusLabels = {
// 状态样式
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'
draft: 'draft',
published: 'published',
archived: 'archived'
}
// 筛选后的文稿
@@ -285,3 +279,221 @@ onMounted(() => {
loadDocuments()
})
</script>
<style scoped>
/* ========== 使用设计令牌系统 ========== */
/* 侧边栏容器 */
.docs-panel {
width: 400px;
height: 100vh;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-default);
background: var(--bg-secondary);
flex-shrink: 0;
}
/* 头部 */
.docs-header {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
}
.docs-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);
}
/* 工具栏 */
.docs-toolbar {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
}
/* 筛选按钮 */
.filter-btn {
font-size: var(--text-xs);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.filter-btn.active {
background: var(--accent-primary);
color: var(--text-inverse);
}
.filter-btn:not(.active) {
background: var(--bg-elevated);
color: var(--text-secondary);
}
.filter-btn:not(.active):hover {
background: var(--bg-sunken);
color: var(--text-primary);
}
/* 文稿列表 */
.docs-list {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
}
/* 文稿卡片 */
.doc-card {
padding: var(--space-3);
border-radius: var(--radius-lg);
border: 1px solid var(--border-default);
cursor: pointer;
transition: all var(--transition-fast);
}
.doc-card.selected {
background: var(--info-bg);
border-color: var(--accent-primary);
}
.doc-card:not(.selected) {
background: var(--bg-primary);
}
.doc-card:not(.selected):hover {
border-color: var(--border-strong);
}
/* 状态徽章 */
.status-badge {
font-size: var(--text-xs);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.status-badge.draft {
background: var(--warning-bg);
color: var(--accent-warning);
}
.status-badge.published {
background: var(--success-bg);
color: var(--accent-success);
}
.status-badge.archived {
background: var(--bg-elevated);
color: var(--text-secondary);
}
/* 底部操作区 */
.docs-footer {
padding: var(--space-4);
border-top: 1px solid var(--border-default);
}
/* 操作按钮 */
.action-btn {
flex: 1;
font-size: var(--text-xs);
padding: var(--space-2);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.action-btn.primary {
background: var(--accent-primary);
color: var(--text-inverse);
}
.action-btn.primary:hover {
background: var(--accent-primary-hover);
}
.action-btn.secondary {
background: var(--bg-elevated);
color: var(--text-primary);
}
.action-btn.secondary:hover {
background: var(--bg-sunken);
}
.action-btn.danger {
background: var(--danger-bg);
color: var(--accent-danger);
}
.action-btn.danger:hover {
background: rgba(248, 113, 113, 0.2);
}
.action-btn.success {
background: var(--accent-success);
color: var(--text-inverse);
}
.action-btn.success:hover {
opacity: 0.9;
}
/* 新建按钮 */
.create-btn {
font-size: var(--text-xs);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
background: var(--accent-success);
color: var(--text-inverse);
transition: all var(--transition-fast);
}
.create-btn:hover {
background: #22c55e;
}
/* 确认弹窗 */
.confirm-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal-backdrop);
}
.confirm-modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: var(--space-6);
width: 320px;
border: 1px solid var(--border-default);
}
.confirm-modal-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: var(--space-4);
}
.confirm-modal-text {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-6);
}
.confirm-modal-actions {
display: flex;
gap: var(--space-3);
}
</style>

View File

@@ -1,56 +1,43 @@
<template>
<aside class="w-16 h-screen flex flex-col items-center py-6 bg-slate-900 border-r border-slate-800 shrink-0 z-50">
<aside class="sidebar">
<!-- Logo/Home -->
<div class="mb-8 text-2xl filter drop-shadow-[0_0_8px_rgba(59,130,246,0.5)]">
<div class="sidebar-logo">
🚀
</div>
<!-- 导航项 -->
<nav class="flex-1 w-full flex flex-col items-center gap-4">
<button
v-for="item in navItems"
<nav class="sidebar-nav">
<button
v-for="item in navItems"
:key="item.id"
@click="switchPage(item.id)"
:class="[
'group relative w-12 h-12 rounded-xl flex items-center justify-center transition-all duration-300',
currentPage === item.id
? 'bg-blue-600 text-white shadow-[0_0_15px_rgba(37,99,235,0.4)]'
: 'text-slate-500 hover:bg-slate-800 hover:text-slate-300'
]"
:class="['nav-item', { active: currentPage === item.id }]"
:title="item.label"
>
<span class="text-xl">{{ item.icon }}</span>
<span class="nav-icon">{{ item.icon }}</span>
<!-- Tooltip -->
<div class="absolute left-full ml-3 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 shadow-xl border border-slate-700 pointer-events-none">
<div class="nav-tooltip">
{{ item.label }}
<div class="absolute left-[-4px] top-1/2 -translate-y-1/2 w-2 h-2 bg-slate-800 border-l border-b border-slate-700 rotate-45"></div>
<div class="nav-tooltip-arrow"></div>
</div>
<!-- Active Indicator -->
<div
v-if="currentPage === item.id"
class="absolute -left-0 w-1 h-6 bg-blue-400 rounded-r-full"
></div>
<div v-if="currentPage === item.id" class="nav-indicator"></div>
</button>
</nav>
<!-- 底部设置 -->
<div class="mt-auto">
<button
<div class="sidebar-footer">
<button
@click="switchPage('settings')"
:class="[
'group relative w-12 h-12 rounded-xl flex items-center justify-center transition-all duration-300',
currentPage === 'settings'
? 'bg-slate-700 text-white'
: 'text-slate-500 hover:bg-slate-800 hover:text-slate-300'
]"
:class="['nav-item', { active: currentPage === 'settings' }]"
title="设置"
>
<span class="text-xl"></span>
<div class="absolute left-full ml-3 px-2 py-1 bg-slate-800 text-white text-xs rounded opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 shadow-xl border border-slate-700 pointer-events-none">
<span class="nav-icon"></span>
<div class="nav-tooltip">
设置
<div class="absolute left-[-4px] top-1/2 -translate-y-1/2 w-2 h-2 bg-slate-800 border-l border-b border-slate-700 rotate-45"></div>
<div class="nav-tooltip-arrow"></div>
</div>
</button>
</div>
@@ -78,3 +65,120 @@ const switchPage = (page) => {
appStore.setCurrentPage(page)
}
</script>
<style scoped>
/* 侧边栏容器 */
.sidebar {
width: 64px;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: var(--space-6) 0;
background: var(--bg-primary);
border-right: 1px solid var(--border-default);
flex-shrink: 0;
z-index: 50;
}
/* Logo */
.sidebar-logo {
margin-bottom: var(--space-8);
font-size: var(--text-2xl);
filter: drop-shadow(0 0 8px rgba(96, 165, 250, 0.5));
}
/* 导航区域 */
.sidebar-nav {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-4);
}
/* 导航项 */
.nav-item {
position: relative;
width: 48px;
height: 48px;
border-radius: var(--radius-xl);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
transition: all var(--transition-normal);
cursor: pointer;
background: transparent;
border: none;
}
.nav-item:hover {
background: var(--bg-elevated);
color: var(--text-secondary);
}
.nav-item.active {
background: var(--accent-primary);
color: var(--text-inverse);
box-shadow: 0 0 15px rgba(96, 165, 250, 0.4);
}
.nav-icon {
font-size: var(--text-xl);
}
/* Tooltip */
.nav-tooltip {
position: absolute;
left: 100%;
margin-left: var(--space-3);
padding: var(--space-1) var(--space-2);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: var(--text-xs);
border-radius: var(--radius-md);
opacity: 0;
visibility: hidden;
transition: all var(--transition-fast);
white-space: nowrap;
z-index: 50;
box-shadow: var(--shadow-md);
border: 1px solid var(--border-default);
pointer-events: none;
}
.nav-item:hover .nav-tooltip {
opacity: 1;
visibility: visible;
}
.nav-tooltip-arrow {
position: absolute;
left: -4px;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-default);
border-bottom: 1px solid var(--border-default);
rotate: 45deg;
}
/* 激活指示器 */
.nav-indicator {
position: absolute;
left: 0;
width: 4px;
height: 24px;
background: var(--accent-primary);
border-radius: 0 var(--radius-full) var(--radius-full) 0;
}
/* 底部区域 */
.sidebar-footer {
margin-top: auto;
}
</style>

View File

@@ -1,68 +1,62 @@
<template>
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
<aside class="docs-panel">
<!-- 头部 -->
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl">📚</span> 素材库
<header class="docs-header">
<h1 class="docs-header-title">
<span style="font-size: var(--text-xl)">📚</span> 素材库
</h1>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
<span class="badge badge-primary">Pro版</span>
</header>
<!-- 工具栏 -->
<div class="p-4 border-b border-slate-700 flex items-center justify-between">
<div class="docs-toolbar">
<div class="flex gap-1 flex-wrap">
<button
v-for="filter in typeFilters"
<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']"
:class="['filter-btn', { 'active': currentFilter === filter.value }]"
>
{{ filter.icon }} {{ filter.label }}
</button>
</div>
<button
<button
@click="openAddModal"
class="text-xs px-3 py-1.5 rounded bg-green-600 text-white hover:bg-green-500 transition"
class="create-btn"
>
+ 新增
</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>
<div class="docs-list">
<div v-if="filteredMaterials.length === 0" class="text-center text-muted py-8">
<span style="font-size: var(--text-2xl); display: block; margin-bottom: var(--space-2)">📄</span>
<p class="text-sm">暂无素材</p>
</div>
<div
v-for="material in filteredMaterials"
<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']"
:class="['doc-card', { 'selected': selectedId === material.id }]"
>
<div class="flex items-start justify-between mb-2">
<h3 class="font-medium text-white text-sm truncate flex-1">
<h3 class="font-medium text-primary 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 v-if="material.is_default" class="status-badge archived">
预置
</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">
<p class="text-xs text-muted mb-2">{{ material.source || '无来源' }}</p>
<div class="flex items-center justify-between text-xs text-muted">
<span>{{ material.excerpts?.length || 0 }} 条摘录</span>
<div class="flex gap-1">
<span
v-for="tag in (material.tags || []).slice(0, 2)"
<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"
class="px-1.5 py-0.5 rounded bg-elevated text-muted"
>
{{ tag }}
</span>
@@ -72,24 +66,24 @@
</div>
<!-- 底部操作区 -->
<div v-if="selectedId" class="p-4 border-t border-slate-700 space-y-2">
<div v-if="selectedId" class="docs-footer">
<div class="flex gap-2">
<button
<button
@click="openEditModal"
class="flex-1 text-xs py-2 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
class="action-btn primary"
>
编辑
</button>
<button
<button
@click="viewExcerpts"
class="flex-1 text-xs py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
class="action-btn secondary"
>
📋 查看摘录
</button>
<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"
class="action-btn danger"
>
🗑
</button>
@@ -97,26 +91,23 @@
</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 v-if="showEditModal" class="paradigm-modal-backdrop">
<div class="paradigm-modal flex flex-col" style="max-height: 80vh">
<div class="p-4 border-b border-default flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">{{ isAddMode ? '新增素材' : '编辑素材' }}</h3>
<button @click="closeEditModal" class="text-muted hover:text-primary"></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>
<label class="block text-xs text-secondary mb-2">素材类型</label>
<div class="flex flex-wrap gap-2">
<button
v-for="type in materialTypes"
<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']"
:class="['filter-btn', { 'active': editForm.type === type.value }]"
>
{{ type.icon }} {{ type.label }}
</button>
@@ -125,40 +116,40 @@
<!-- 标题 -->
<div>
<label class="block text-xs text-slate-400 mb-2">素材标题</label>
<input
<label class="block text-xs text-secondary 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"
class="analysis-input"
placeholder="如:二十届四中全会精神要点"
>
</div>
<!-- 来源 -->
<div>
<label class="block text-xs text-slate-400 mb-2">来源</label>
<input
<label class="block text-xs text-secondary 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"
class="analysis-input"
placeholder="如:人民日报 2024-10-15"
>
</div>
<!-- 标签 -->
<div>
<label class="block text-xs text-slate-400 mb-2">标签逗号分隔</label>
<input
<label class="block text-xs text-secondary 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"
class="analysis-input"
placeholder="如:全面深化改革, 制度建设"
>
</div>
<!-- 关联维度集 -->
<div>
<label class="block text-xs text-slate-400 mb-2">关联维度集</label>
<select
<label class="block text-xs text-secondary 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"
class="analysis-input"
>
<option value="">不关联</option>
<option v-for="ds in dimensionSets" :key="ds.id" :value="ds.id">
@@ -168,46 +159,46 @@
</div>
<!-- 摘录管理 -->
<div class="border-t border-slate-700 pt-4">
<div class="border-t border-default pt-4">
<div class="flex items-center justify-between mb-3">
<label class="text-xs text-slate-400">可引用摘录</label>
<button
<label class="text-xs text-secondary">可引用摘录</label>
<button
@click="addExcerpt"
class="text-xs text-blue-400 hover:text-blue-300"
class="text-xs text-accent hover:opacity-80"
>
+ 添加摘录
</button>
</div>
<div class="space-y-3 max-h-60 overflow-y-auto">
<div
v-for="(excerpt, index) in editForm.excerpts"
<div class="space-y-3" style="max-height: 240px; 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"
class="bg-primary rounded p-3 border border-default"
>
<div class="flex items-center justify-between mb-2">
<input
<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"
class="flex-1 bg-transparent border-b border-default text-sm text-primary outline-none focus:border-accent pb-1"
placeholder="摘录主题"
>
<button
<button
@click="removeExcerpt(index)"
class="text-red-400 hover:text-red-300 ml-2"
class="text-danger hover:opacity-80 ml-2"
>
</button>
</div>
<textarea
<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"
class="w-full bg-elevated border border-default rounded px-2 py-1.5 text-xs text-secondary outline-none focus:border-accent resize-none"
rows="2"
placeholder="摘录内容..."
></textarea>
<div class="flex items-center gap-2 mt-2">
<select
<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"
class="text-xs bg-elevated border border-default rounded px-2 py-1 text-secondary outline-none"
>
<option value="">通用</option>
<option value="positive">正面案例</option>
@@ -219,16 +210,16 @@
</div>
</div>
<div class="p-4 border-t border-slate-700 flex gap-3">
<button
<div class="p-4 border-t border-default flex gap-3">
<button
@click="closeEditModal"
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
class="flex-1 action-btn secondary"
>
取消
</button>
<button
<button
@click="saveMaterial"
class="flex-1 py-2 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
class="flex-1 action-btn primary"
>
{{ isAddMode ? '添加' : '保存' }}
</button>
@@ -237,33 +228,33 @@
</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 v-if="showExcerptsModal" class="paradigm-modal-backdrop">
<div class="paradigm-modal flex flex-col" style="max-height: 70vh">
<div class="p-4 border-b border-default flex items-center justify-between">
<h3 class="text-lg font-semibold text-primary">{{ selectedMaterial?.title }} - 摘录</h3>
<button @click="showExcerptsModal = false" class="text-muted hover:text-primary"></button>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-3">
<div
v-for="(excerpt, index) in selectedMaterial?.excerpts || []"
<div
v-for="(excerpt, index) in selectedMaterial?.excerpts || []"
:key="index"
class="bg-slate-900/50 rounded p-3 border border-slate-700"
class="bg-primary rounded p-3 border border-default"
>
<div class="flex items-center justify-between mb-2">
<span class="font-medium text-white text-sm">{{ excerpt.topic }}</span>
<span
<span class="font-medium text-primary 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' ? 'bg-success-bg text-success' : 'bg-danger-bg text-danger']"
>
{{ excerpt.useFor === 'positive' ? '正面' : '反面' }}
</span>
</div>
<p class="text-xs text-slate-400 leading-relaxed">{{ excerpt.content }}</p>
<p class="text-xs text-secondary leading-relaxed">{{ excerpt.content }}</p>
</div>
<div v-if="!selectedMaterial?.excerpts?.length" class="text-center text-slate-500 py-4">
<div v-if="!selectedMaterial?.excerpts?.length" class="text-center text-muted py-4">
暂无摘录
</div>
</div>
@@ -271,20 +262,20 @@
</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
<div v-if="showDeleteConfirm" class="confirm-modal-backdrop">
<div class="confirm-modal">
<h3 class="confirm-modal-title">确认删除</h3>
<p class="confirm-modal-text">确定要删除这条素材吗此操作不可恢复</p>
<div class="confirm-modal-actions">
<button
@click="showDeleteConfirm = false"
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
class="flex-1 action-btn secondary"
>
取消
</button>
<button
<button
@click="deleteMaterial"
class="flex-1 py-2 rounded bg-red-600 text-white hover:bg-red-500 transition"
class="flex-1 action-btn danger"
>
删除
</button>
@@ -496,3 +487,256 @@ onMounted(() => {
loadMaterials()
})
</script>
<style scoped>
/* ========== 使用设计令牌系统 ========== */
/* 侧边栏容器 */
.docs-panel {
width: 400px;
height: 100vh;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-default);
background: var(--bg-secondary);
flex-shrink: 0;
}
/* 头部 */
.docs-header {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
}
.docs-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);
}
/* 工具栏 */
.docs-toolbar {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--space-2);
}
/* 筛选按钮 */
.filter-btn {
font-size: var(--text-xs);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.filter-btn.active {
background: var(--accent-primary);
color: var(--text-inverse);
}
.filter-btn:not(.active) {
background: var(--bg-elevated);
color: var(--text-secondary);
}
.filter-btn:not(.active):hover {
background: var(--bg-sunken);
color: var(--text-primary);
}
/* 素材列表 */
.docs-list {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
}
/* 素材卡片 */
.doc-card {
padding: var(--space-3);
border-radius: var(--radius-lg);
border: 1px solid var(--border-default);
cursor: pointer;
transition: all var(--transition-fast);
margin-bottom: var(--space-3);
}
.doc-card.selected {
background: var(--info-bg);
border-color: var(--accent-primary);
}
.doc-card:not(.selected) {
background: var(--bg-primary);
}
.doc-card:not(.selected):hover {
border-color: var(--border-strong);
}
/* 状态徽章 */
.status-badge {
font-size: var(--text-xs);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.status-badge.archived {
background: var(--bg-elevated);
color: var(--text-secondary);
}
/* 底部操作区 */
.docs-footer {
padding: var(--space-4);
border-top: 1px solid var(--border-default);
}
/* 操作按钮 */
.action-btn {
flex: 1;
font-size: var(--text-xs);
padding: var(--space-2);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.action-btn.primary {
background: var(--accent-primary);
color: var(--text-inverse);
}
.action-btn.primary:hover {
background: var(--accent-primary-hover);
}
.action-btn.secondary {
background: var(--bg-elevated);
color: var(--text-primary);
}
.action-btn.secondary:hover {
background: var(--bg-sunken);
}
.action-btn.danger {
background: var(--danger-bg);
color: var(--accent-danger);
}
.action-btn.danger:hover {
background: rgba(248, 113, 113, 0.2);
}
/* 新建按钮 */
.create-btn {
font-size: var(--text-xs);
padding: var(--space-2) var(--space-3);
border-radius: var(--radius-md);
background: var(--accent-success);
color: var(--text-inverse);
transition: all var(--transition-fast);
}
.create-btn:hover {
background: #22c55e;
}
/* 确认弹窗 */
.confirm-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal-backdrop);
}
.confirm-modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: var(--space-6);
width: 320px;
border: 1px solid var(--border-default);
}
.confirm-modal-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: var(--space-4);
}
.confirm-modal-text {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-6);
}
.confirm-modal-actions {
display: flex;
gap: var(--space-3);
}
/* 范式模态框 */
.paradigm-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal-backdrop);
}
.paradigm-modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
width: 500px;
max-height: 80vh;
overflow: hidden;
border: 1px solid var(--border-default);
display: flex;
flex-direction: column;
}
/* 输入框 */
.analysis-input {
width: 100%;
background: var(--bg-primary);
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);
}
.analysis-input:focus {
border-color: var(--accent-primary);
}
/* 徽章 */
.badge {
font-size: var(--text-xs);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.badge-primary {
background: var(--accent-primary);
color: var(--text-inverse);
}
</style>

View File

@@ -78,19 +78,29 @@
<!-- 底部操作 -->
<div class="p-4 border-t border-slate-700 bg-slate-800 flex gap-3 shrink-0">
<button
@click="$emit('close')"
class="flex-1 py-2.5 rounded-lg bg-slate-700 text-slate-300 hover:bg-slate-600 transition font-medium"
<button
@click="$emit('create-custom')"
class="py-2.5 px-4 rounded-lg bg-purple-600 text-white hover:bg-purple-500 transition font-medium shadow-lg shadow-purple-900/20 flex items-center gap-2"
title="从需求文档创建自定义范式"
>
取消
</button>
<button
@click="confirmSelect"
:disabled="!selectedId"
class="flex-1 py-2.5 rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition font-medium disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-900/20"
>
确认作为对比要求
<span>🎯</span>
<span>新建范式</span>
</button>
<div class="flex-1 flex gap-3">
<button
@click="$emit('close')"
class="flex-1 py-2.5 rounded-lg bg-slate-700 text-slate-300 hover:bg-slate-600 transition font-medium"
>
取消
</button>
<button
@click="confirmSelect"
:disabled="!selectedId"
class="flex-1 py-2.5 rounded-lg bg-blue-600 text-white hover:bg-blue-500 transition font-medium disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-900/20"
>
确认作为对比要求
</button>
</div>
</div>
</div>
</div>
@@ -104,7 +114,7 @@ const props = defineProps({
visible: Boolean
})
const emit = defineEmits(['close', 'select'])
const emit = defineEmits(['close', 'select', 'create-custom'])
// 状态
const paradigms = ref([])

View File

@@ -1,57 +1,57 @@
<template>
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
<aside class="docs-panel">
<!-- 头部 -->
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl"></span> 设置中心
<header class="docs-header">
<h1 class="docs-header-title">
<span style="font-size: var(--text-xl)"></span> 设置中心
</h1>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
<span class="badge badge-primary">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">
<section class="settings-section">
<h3 class="settings-section-title">
<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 class="stats-grid">
<div class="stat-card">
<div class="stat-value blue">{{ stats.documents }}</div>
<div class="stat-label">文稿数量</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 class="stat-card">
<div class="stat-value green">{{ stats.materials }}</div>
<div class="stat-label">素材数量</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 class="stat-card">
<div class="stat-value purple">{{ stats.paradigms }}</div>
<div class="stat-label">范式数量</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 class="stat-card">
<div class="stat-value amber">{{ formatSize(stats.dbSize) }}</div>
<div class="stat-label">数据库大小</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">
<section class="settings-section">
<h3 class="settings-section-title">
<span>📚</span> 素材库管理
</h3>
<div class="space-y-3">
<button
<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"
class="settings-btn secondary"
>
<span>管理素材库</span>
<span class="text-slate-500">{{ stats.materials }} </span>
<span class="text-muted">{{ stats.materials }} </span>
</button>
<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"
class="settings-btn secondary"
>
📥 导入素材 (JSON)
</button>
@@ -59,26 +59,26 @@
</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">
<section class="settings-section">
<h3 class="settings-section-title">
<span>💾</span> 数据备份与恢复
</h3>
<div class="space-y-3">
<button
<button
@click="exportData"
class="w-full text-xs py-2.5 rounded bg-blue-600 text-white hover:bg-blue-500 transition"
class="settings-btn primary"
>
📤 导出所有数据 (JSON)
</button>
<button
<button
@click="triggerImport"
class="w-full text-xs py-2.5 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
class="settings-btn secondary"
>
📥 导入数据
</button>
<input
<input
ref="importInput"
type="file"
type="file"
accept=".json"
class="hidden"
@change="handleImport"
@@ -87,20 +87,20 @@
</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">
<section class="settings-section danger-section">
<h3 class="settings-section-title">
<span></span> 危险操作
</h3>
<div class="space-y-3">
<button
<button
@click="confirmClearDocuments"
class="w-full text-xs py-2 rounded bg-red-900/50 text-red-300 hover:bg-red-800/50 transition"
class="settings-btn danger-btn"
>
清空所有文稿
</button>
<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"
class="settings-btn danger-btn"
>
重置数据库恢复默认
</button>
@@ -108,11 +108,11 @@
</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">
<section class="settings-section">
<h3 class="settings-section-title">
<span></span> 关于
</h3>
<div class="text-xs text-slate-500 space-y-1">
<div class="text-xs text-muted space-y-1">
<p>AI 写作工坊 v1.0.0</p>
<p>数据存储浏览器 IndexedDB (SQLite)</p>
<p>© 2026 AI Writing Workshop</p>
@@ -121,20 +121,20 @@
</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
<div v-if="showConfirm" class="confirm-modal-backdrop">
<div class="confirm-modal">
<h3 class="confirm-modal-title">{{ confirmTitle }}</h3>
<p class="confirm-modal-text">{{ confirmMessage }}</p>
<div class="confirm-modal-actions">
<button
@click="showConfirm = false"
class="flex-1 py-2 rounded bg-slate-600 text-white hover:bg-slate-500 transition"
class="settings-btn secondary"
>
取消
</button>
<button
<button
@click="executeConfirmAction"
class="flex-1 py-2 rounded bg-red-600 text-white hover:bg-red-500 transition"
class="settings-btn danger-btn"
>
确认
</button>
@@ -143,7 +143,7 @@
</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">
<div v-if="showToast" class="toast">
{{ toastMessage }}
</div>
</aside>
@@ -307,3 +307,190 @@ onMounted(() => {
loadStats()
})
</script>
<style scoped>
/* ========== 使用设计令牌系统 ========== */
/* 侧边栏容器 */
.settings-panel {
width: 400px;
height: 100vh;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-default);
background: var(--bg-secondary);
flex-shrink: 0;
}
/* 头部 */
.settings-header {
padding: var(--space-4);
border-bottom: 1px solid var(--border-default);
display: flex;
align-items: center;
justify-content: space-between;
}
/* 内容区 */
.settings-content {
flex: 1;
overflow-y: auto;
padding: var(--space-4);
}
/* 设置区块 */
.settings-section {
background: var(--bg-primary);
border-radius: var(--radius-lg);
padding: var(--space-4);
border: 1px solid var(--border-default);
margin-bottom: var(--space-6);
}
.settings-section:last-child {
margin-bottom: 0;
}
.settings-section-title {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-primary);
margin-bottom: var(--space-4);
display: flex;
align-items: center;
gap: var(--space-2);
}
/* 统计卡片网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--space-3);
}
.stat-card {
background: var(--bg-elevated);
border-radius: var(--radius-md);
padding: var(--space-3);
text-align: center;
}
.stat-value {
font-size: var(--text-2xl);
font-weight: var(--font-semibold);
font-variant-numeric: tabular-nums;
}
.stat-value.blue { color: var(--accent-primary); }
.stat-value.green { color: var(--accent-success); }
.stat-value.purple { color: #a855f7; }
.stat-value.amber { color: var(--accent-warning); }
.stat-label {
font-size: var(--text-xs);
color: var(--text-muted);
}
/* 设置按钮 */
.settings-btn {
width: 100%;
text-align: left;
font-size: var(--text-xs);
padding: var(--space-3);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
display: flex;
align-items: center;
justify-content: space-between;
border: none;
cursor: pointer;
}
.settings-btn.secondary {
background: var(--bg-elevated);
color: var(--text-primary);
}
.settings-btn.secondary:hover {
background: var(--bg-sunken);
}
.settings-btn.primary {
background: var(--accent-primary);
color: var(--text-inverse);
text-align: center;
}
.settings-btn.primary:hover {
background: var(--accent-primary-hover);
}
/* 危险操作区块 */
.danger-section {
background: var(--danger-bg);
border: 1px solid rgba(248, 113, 113, 0.5);
}
.danger-section .settings-section-title {
color: var(--accent-danger);
}
.danger-btn {
background: var(--danger-bg);
color: var(--accent-danger);
}
.danger-btn:hover {
background: rgba(248, 113, 113, 0.2);
}
/* 确认弹窗 */
.confirm-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-modal-backdrop);
}
.confirm-modal {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: var(--space-6);
width: 320px;
border: 1px solid var(--border-default);
}
.confirm-modal-title {
font-size: var(--text-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: var(--space-4);
}
.confirm-modal-message {
font-size: var(--text-sm);
color: var(--text-secondary);
margin-bottom: var(--space-6);
}
.confirm-modal-actions {
display: flex;
gap: var(--space-3);
}
/* Toast 提示 */
.toast {
position: fixed;
bottom: var(--space-4);
right: var(--space-4);
background: var(--accent-success);
color: var(--text-inverse);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: var(--z-modal);
}
</style>

View File

@@ -1,87 +1,87 @@
<template>
<aside class="w-[400px] h-screen flex flex-col border-r border-slate-700 bg-slate-800 shrink-0">
<aside class="writer-panel">
<!-- 头部 -->
<header class="p-4 border-b border-slate-700 flex items-center justify-between">
<h1 class="font-bold text-lg text-white flex items-center gap-2">
<span class="text-2xl"></span> AI 写作工坊
<header class="writer-header">
<h1 class="writer-header-title">
<span style="font-size: var(--text-xl)"></span> AI 写作工坊
</h1>
<span class="text-xs px-2 py-1 rounded bg-blue-900 text-blue-300 border border-blue-700">Pro版</span>
<span class="badge badge-default">Pro版</span>
</header>
<!-- 内容区 -->
<div class="flex-1 overflow-y-auto p-4 space-y-6 min-h-0">
<div class="writer-content">
<!-- 写作任务 -->
<section>
<section class="writer-section">
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-slate-400">1. 写作任务 (User Input)</label>
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
<button
<label class="writer-label">1. 写作任务 (User Input)</label>
<div class="input-type-toggle">
<button
@click="inputType = 'text'"
:class="['text-xs px-2 py-0.5 rounded transition', inputType === 'text' ? 'bg-slate-700 text-white' : 'text-slate-500 hover:text-slate-300']"
:class="['input-type-btn', { active: inputType === 'text' }]"
>自由文本</button>
<button
<button
@click="inputType = 'outline'"
:class="['text-xs px-2 py-0.5 rounded transition', inputType === 'outline' ? 'bg-slate-700 text-white' : 'text-slate-500 hover:text-slate-300']"
:class="['input-type-btn', { active: inputType === 'outline' }]"
>大纲模式</button>
</div>
</div>
<div v-if="inputType === 'text'">
<textarea
v-model="inputTask"
class="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition placeholder-slate-600 resize-none"
<textarea
v-model="inputTask"
class="writer-textarea"
placeholder="请输入具体的写作要求、主题、核心观点..."
></textarea>
<div class="text-right mt-1">
<span class="text-xs text-slate-500">{{ inputTask.length }} </span>
<span class="text-xs text-muted">{{ inputTask.length }} </span>
</div>
</div>
<div v-else class="space-y-2">
<input v-model="outlinePoints.topic" placeholder="核心主题" class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs outline-none focus:border-blue-500">
<input v-model="outlinePoints.audience" placeholder="目标受众" class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs outline-none focus:border-blue-500">
<textarea v-model="outlinePoints.keyPoints" placeholder="关键观点(每行一个)" class="w-full h-20 bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs outline-none focus:border-blue-500 resize-none"></textarea>
<input v-model="outlinePoints.topic" placeholder="核心主题" class="writer-input">
<input v-model="outlinePoints.audience" placeholder="目标受众" class="writer-input">
<textarea v-model="outlinePoints.keyPoints" placeholder="关键观点(每行一个)" class="writer-textarea" style="height: 80px"></textarea>
</div>
</section>
<!-- 参考案例 -->
<section>
<section class="writer-section">
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-slate-400">2. 参考案例 (Style Ref)</label>
<button @click="showRefInput = !showRefInput" class="text-xs text-blue-400 hover:text-blue-300">
<label class="writer-label">2. 参考案例 (Style Ref)</label>
<button @click="showRefInput = !showRefInput" class="text-accent hover:opacity-80" style="font-size: var(--text-xs)">
{{ showRefInput ? '取消' : '+ 添加案例' }}
</button>
</div>
<div v-if="showRefInput" class="mb-3 p-3 bg-slate-900 rounded-lg border border-blue-500/30">
<input v-model="newRefTitle" placeholder="案例标题" class="w-full mb-2 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs outline-none">
<textarea v-model="newRefContent" placeholder="粘贴优秀的参考文本..." class="w-full h-24 bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs outline-none resize-none mb-2"></textarea>
<button @click="addReference" class="w-full bg-blue-600 hover:bg-blue-500 text-xs py-1.5 rounded text-white">确认添加</button>
<div v-if="showRefInput" class="mb-3 p-3 bg-primary border border-accent rounded-lg" style="border-color: var(--accent-primary)">
<input v-model="newRefTitle" placeholder="案例标题" class="writer-input mb-2">
<textarea v-model="newRefContent" placeholder="粘贴优秀的参考文本..." class="writer-textarea mb-2" style="height: 96px"></textarea>
<button @click="addReference" class="btn btn-primary w-full text-xs">确认添加</button>
</div>
<div class="space-y-2">
<div v-for="(ref, index) in references" :key="index" class="group flex flex-col bg-slate-700/50 p-2 rounded border border-slate-700 hover:border-slate-600 transition">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center gap-2 overflow-hidden">
<span class="text-lg">📄</span>
<span class="text-xs font-medium text-slate-200 truncate">{{ ref.title }}</span>
<div v-for="(ref, index) in references" :key="index" class="ref-card">
<div class="ref-card-header">
<div class="ref-card-title">
<span style="font-size: var(--text-base)">📄</span>
<span class="ref-card-title-text">{{ ref.title }}</span>
</div>
<button @click="removeReference(index)" class="text-slate-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2">×</button>
<button @click="removeReference(index)" class="ref-remove-btn px-2">×</button>
</div>
<div class="pl-7">
<p class="text-[10px] text-slate-500 truncate mb-1.5">{{ ref.content.substring(0, 30) }}...</p>
<div style="padding-left: var(--space-5)">
<p class="text-xs text-muted truncate mb-1">{{ ref.content.substring(0, 30) }}...</p>
<!-- 风格分析结果 -->
<div v-if="ref.isAnalyzing" class="flex items-center gap-1 text-[10px] text-indigo-400 animate-pulse">
<span class="w-1 h-1 rounded-full bg-indigo-400"></span>
<div v-if="ref.isAnalyzing" class="flex items-center gap-1 text-xs animate-pulse" style="color: var(--accent-primary)">
<span style="width: 4px; height: 4px; background: var(--accent-primary); border-radius: 50%"></span>
正在分析风格特征...
</div>
<div v-else-if="ref.styleTags && ref.styleTags.length > 0" class="flex flex-wrap gap-1">
<span
v-for="tag in ref.styleTags"
<span
v-for="tag in ref.styleTags"
:key="tag"
class="text-[10px] px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-300 border border-indigo-500/30"
class="style-tag"
>
{{ tag }}
</span>
@@ -92,33 +92,33 @@
</section>
<!-- 专家指令范式预设时显示 -->
<section v-if="activeParadigm">
<section v-if="activeParadigm" class="writer-section">
<div class="flex justify-between items-center mb-2">
<label class="text-sm font-medium text-amber-400 flex items-center gap-1">
<label class="writer-label writer-label-alt">
专家指令 (Expert Guidelines)
</label>
<button
<button
@click="clearParadigm"
class="text-[10px] text-slate-500 hover:text-red-400 transition"
class="text-xs text-muted hover:text-danger transition"
>
清除范式
</button>
</div>
<div class="bg-amber-950/20 border border-amber-500/30 rounded-lg p-3 space-y-2">
<div class="expert-section">
<div class="flex items-center gap-2 mb-2">
<span class="text-amber-400">{{ activeParadigm.icon }}</span>
<span class="text-xs font-medium text-amber-300">已加载{{ activeParadigm.name }}专家标准</span>
<span class="text-accent">{{ activeParadigm.icon }}</span>
<span class="text-xs font-medium text-accent" style="color: var(--accent-warning)">已加载{{ activeParadigm.name }}专家标准</span>
</div>
<div class="space-y-1.5">
<div
v-for="(guideline, idx) in expertGuidelines"
<div
v-for="(guideline, idx) in expertGuidelines"
:key="idx"
class="text-[11px] text-slate-400 flex items-start gap-2"
class="expert-item"
>
<span class="text-amber-500/70 shrink-0">{{ idx + 1 }}.</span>
<span style="color: var(--accent-warning); opacity: 0.7;" class="shrink-0">{{ idx + 1 }}.</span>
<div>
<span class="text-amber-300/80 font-medium">{{ guideline.title }}</span>
<span class="text-slate-500">{{ guideline.description }}</span>
<span class="font-medium" style="color: var(--accent-warning); opacity: 0.8;">{{ guideline.title }}</span>
<span class="text-secondary">{{ guideline.description }}</span>
</div>
</div>
</div>
@@ -126,78 +126,77 @@
</section>
<!-- 输出规范 -->
<section>
<label class="block text-sm font-medium text-slate-400 mb-2">3. 输出规范 (Constraints)</label>
<section class="writer-section">
<label class="writer-label block mb-2">3. 输出规范 (Constraints)</label>
<div class="flex flex-wrap gap-2 mb-3">
<button
v-for="tag in presetTags"
<button
v-for="tag in presetTags"
:key="tag"
@click="toggleTag(tag)"
:class="['px-2 py-1 rounded text-xs border transition',
selectedTags.includes(tag)
? 'bg-blue-600/20 border-blue-500 text-blue-300'
: 'bg-slate-900 border-slate-700 text-slate-500 hover:border-slate-500']"
:class="['tag-button', { selected: selectedTags.includes(tag) }]"
>
{{ tag }}
</button>
</div>
<input
v-model="customConstraint"
type="text"
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs focus:border-blue-500 outline-none"
<input
v-model="customConstraint"
type="text"
class="writer-input"
placeholder="补充其他要求"
>
<!-- 深度模式开关 -->
<div class="mt-4 flex items-center justify-between bg-slate-900/50 p-3 rounded border border-indigo-500/30">
<div class="deep-mode-toggle mt-4">
<div class="flex flex-col">
<span class="text-sm font-bold text-indigo-300 flex items-center gap-1">
<span class="text-sm font-bold flex items-center gap-1" style="color: var(--accent-primary)">
🧠 深度模式 (Deep Mode)
</span>
<span class="text-[10px] text-slate-500">模拟人类初稿-反思-润色的思维链</span>
<span class="text-[10px] text-muted">模拟人类"初稿-反思-润色"的思维链</span>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" v-model="isDeepMode" class="sr-only peer">
<div class="w-9 h-5 bg-slate-700 peer-focus:outline-none 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-indigo-600"></div>
<input type="checkbox" v-model="isDeepMode" class="hidden peer">
<div style="width: 36px; height: 20px; background: var(--bg-elevated); border-radius: 9999px; position: relative; transition: all var(--transition-normal);" class="peer-checked:bg-indigo-600">
<div style="width: 16px; height: 16px; background: white; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: all var(--transition-normal);" class="peer-checked:translate-x-4"></div>
</div>
</label>
</div>
</section>
</div>
<!-- 底部操作区 -->
<footer class="p-4 bg-slate-800 border-t border-slate-700 space-y-3">
<footer class="writer-footer">
<div class="flex items-center justify-between">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showPromptDebug" class="hidden">
<div class="w-8 h-4 bg-slate-900 rounded-full border border-slate-600 relative transition-colors" :class="{'bg-blue-900 border-blue-500': showPromptDebug}">
<div class="w-2 h-2 bg-slate-400 rounded-full absolute top-1 left-1 transition-transform" :class="{'translate-x-4 bg-blue-400': showPromptDebug}"></div>
<div class="toggle-switch" :class="{'active': showPromptDebug}">
<div class="toggle-thumb" :class="{'active': showPromptDebug}"></div>
</div>
<span class="text-xs text-slate-500 select-none">预览构建的 Prompt</span>
<span class="text-xs text-muted select-none">预览构建的 Prompt</span>
</label>
<span class="text-xs text-slate-600">deepseek</span>
<span class="text-xs text-muted">deepseek</span>
</div>
<!-- API配置 -->
<div class="space-y-2">
<input
v-model="apiUrl"
type="text"
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs focus:border-blue-500 outline-none"
<input
v-model="apiUrl"
type="text"
class="writer-input"
placeholder="API 地址"
>
<input
v-model="apiKey"
type="password"
class="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-xs focus:border-blue-500 outline-none"
<input
v-model="apiKey"
type="password"
class="writer-input"
placeholder="API Key"
>
</div>
<button
@click="generateContent"
<button
@click="generateContent"
:disabled="!canGenerate"
class="w-full py-3 rounded-lg font-bold text-white shadow-lg shadow-blue-900/20 flex items-center justify-center gap-2 transition-all transform active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:class="isGenerating ? 'bg-slate-700' : 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500'"
class="generate-button primary"
:class="{'generating': isGenerating}"
>
<span v-if="isGenerating" class="animate-spin text-lg"></span>
{{ isGenerating ? '正在思考与撰写...' : '开始生成文稿' }}
@@ -276,3 +275,325 @@ const canGenerate = computed(() => {
}
})
</script>
<style scoped>
/* ========== 使用设计令牌系统 ========== */
/* 侧边栏容器 */
.writer-panel {
width: 400px;
height: 100vh;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-default);
background: var(--bg-secondary);
flex-shrink: 0;
}
/* 头部 */
.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-section:last-child {
margin-bottom: 0;
}
/* 标签 */
.writer-label {
font-size: var(--text-sm);
font-weight: var(--font-medium);
color: var(--text-secondary);
margin-bottom: var(--space-2);
}
.writer-label-alt {
color: var(--accent-warning);
}
/* 输入框样式 */
.writer-textarea {
width: 100%;
height: 128px;
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
padding: var(--space-3);
font-size: var(--text-sm);
color: var(--text-primary);
transition: all var(--transition-fast);
}
.writer-textarea::placeholder {
color: var(--text-muted);
}
.writer-textarea:focus {
outline: none;
border-color: var(--accent-primary);
box-shadow: 0 0 0 2px var(--info-bg);
}
.writer-input {
width: 100%;
background: var(--bg-primary);
border: 1px solid var(--border-default);
border-radius: var(--radius-md);
padding: var(--space-3);
font-size: var(--text-xs);
color: var(--text-primary);
}
.writer-input::placeholder {
color: var(--text-muted);
}
.writer-input:focus {
outline: none;
border-color: var(--accent-primary);
}
/* 参考案例卡片 */
.ref-card {
display: flex;
flex-direction: column;
background: var(--bg-elevated);
padding: var(--space-2);
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
transition: all var(--transition-fast);
}
.ref-card:hover {
border-color: var(--border-strong);
}
.ref-card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--space-1);
}
.ref-card-title {
display: flex;
align-items: center;
gap: var(--space-2);
overflow: hidden;
}
.ref-card-title-text {
font-size: var(--text-xs);
font-weight: var(--font-medium);
color: var(--text-primary);
}
.ref-remove-btn {
color: var(--text-muted);
opacity: 0;
transition: all var(--transition-fast);
}
.ref-card:hover .ref-remove-btn {
opacity: 1;
}
.ref-remove-btn:hover {
color: var(--accent-danger);
}
/* 风格标签 */
.style-tag {
font-size: 10px;
padding: 2px 6px;
border-radius: var(--radius-sm);
background: var(--info-bg);
color: var(--accent-primary);
border: 1px solid rgba(96, 165, 250, 0.3);
}
/* 专家指令区域 */
.expert-section {
background: var(--warning-bg);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: var(--radius-lg);
padding: var(--space-3);
}
.expert-item {
font-size: 11px;
color: var(--text-secondary);
display: flex;
align-items: flex-start;
gap: var(--space-2);
}
/* 标签按钮 */
.tag-button {
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-md);
font-size: var(--text-xs);
border: 1px solid var(--border-default);
transition: all var(--transition-fast);
cursor: pointer;
}
.tag-button.selected {
background: var(--info-bg);
border-color: var(--accent-primary);
color: var(--accent-primary);
}
.tag-button:not(.selected) {
background: var(--bg-primary);
color: var(--text-muted);
}
.tag-button:not(.selected):hover {
border-color: var(--border-strong);
}
/* 深度模式开关 */
.deep-mode-toggle {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--bg-primary);
padding: var(--space-3);
border-radius: var(--radius-md);
border: 1px solid var(--accent-primary);
}
/* 底部操作区 */
.writer-footer {
padding: var(--space-4);
background: var(--bg-secondary);
border-top: 1px solid var(--border-default);
}
/* 切换开关 */
.toggle-switch {
width: 32px;
height: 16px;
background: var(--bg-primary);
border-radius: var(--radius-full);
border: 1px solid var(--border-default);
position: relative;
transition: all var(--transition-normal);
}
.toggle-switch.active {
background: var(--info-bg);
border-color: var(--accent-primary);
}
.toggle-thumb {
width: 10px;
height: 10px;
background: var(--text-muted);
border-radius: 50%;
position: absolute;
top: 2px;
left: 2px;
transition: all var(--transition-normal);
}
.toggle-thumb.active {
transform: translateX(16px);
background: var(--accent-primary);
}
/* 生成按钮 */
.generate-button {
width: 100%;
padding: var(--space-3);
border-radius: var(--radius-lg);
font-weight: var(--font-semibold);
color: var(--text-inverse);
box-shadow: var(--shadow-md);
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
transition: all var(--transition-normal);
transform: translateY(0);
border: none;
cursor: pointer;
}
.generate-button:not(:disabled):active {
transform: translateY(1px);
}
.generate-button.primary {
background: linear-gradient(135deg, var(--accent-primary), #6366f1);
}
.generate-button.primary:hover:not(:disabled) {
background: linear-gradient(135deg, var(--accent-primary-hover), #4f46e5);
}
.generate-button.generating {
background: var(--bg-elevated);
color: var(--text-muted);
cursor: wait;
}
.generate-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 切换器 */
.input-type-toggle {
display: flex;
background: var(--bg-primary);
padding: var(--space-1);
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
}
.input-type-btn {
padding: var(--space-2) var(--space-3);
font-size: var(--text-xs);
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
}
.input-type-btn.active {
background: var(--bg-elevated);
color: var(--text-primary);
}
.input-type-btn:not(.active) {
color: var(--text-muted);
}
.input-type-btn:not(.active):hover {
color: var(--text-secondary);
}
</style>