feat: 新增文档翻译 + 文案校对 + 智能助手 + 批量字段提取四大功能

- 文档翻译:多语言翻译(中英日韩法德西),支持导出 Word/PPT/Excel/PDF
- 文案校对:错别字/语病/风格/数据检查,三级校对模式,质量评分
- 智能助手:对话/任务拆解/文案生成/批量提取 四个模式入口
- 批量提取:自定义字段配置,AI 结构化提取,CSV 导出

后端新增 API:/api/document/translate /api/copy/proofread /api/assistant/chat /api/batch/extract
前端新增页面:4 个新页面 + 模板入口 + API 模块
路由更新:tools/ 系列页面路由

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
eaiadmin
2026-09-13 22:56:40 +08:00
co-authored by Claude Code
parent 8d849ab4d9
commit 3c58b376b3
15 changed files with 1581 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
import http from './http'
export function chatWithAssistant(data) {
return http.post('/assistant/chat', data)
}
+7
View File
@@ -0,0 +1,7 @@
import http from './http'
export function extractBatch(data) {
return http.post('/batch/extract', data)
}
export function extractFromFiles(data) {
return http.post('/batch/files', data)
}
+7
View File
@@ -0,0 +1,7 @@
import http from './http'
export function proofreadCopy(data) {
return http.post('/copy/proofread', data)
}
export function quickProofread(q) {
return http.get('/copy/quick', { params: { q } })
}
+4
View File
@@ -0,0 +1,4 @@
import http from './http'
export function translateDocument(data) {
return http.post('/document/translate', data)
}
+5
View File
@@ -24,6 +24,11 @@ const routes = [
{ path: 'apps/process-coordination', name: 'ProcessCoordinationApp', meta: { workspaceKey: 'process-coordination' }, component: () => import('@/views/workbench/BusinessAppPage.vue') },
{ path: 'apps/report-generation', name: 'ReportGenerationApp', meta: { workspaceKey: 'report-generation' }, component: () => import('@/views/workbench/BusinessAppPage.vue') },
{ path: 'report-gen', name: 'ReportGeneration', meta: { requiresAdmin: true }, component: () => import('@/views/report/ReportGenerationPage.vue') },
{ path: 'tools/document-translate', name: 'DocumentTranslate', component: () => import('@/views/tools/DocumentTranslatePage.vue) },
{ path: 'tools/copy-proofreading', name: 'CopyProofreading', component: () => import('@/views/tools/CopyProofreadingPage.vue) },
{ path: 'tools/smart-assistant', name: 'SmartAssistant', component: () => import('@/views/tools/SmartAssistantPage.vue) },
{ path: 'tools/batch-extract', name: 'BatchExtract', component: () => import('@/views/tools/BatchExtractPage.vue) },
{ path: 'apps/wechat-official-account', name: 'WechatOfficialAccountApp', meta: { workspaceKey: 'wechat-official-account' }, component: () => import('@/views/workbench/OfficialAccountWorkflowPage.vue') },
{ path: 'apps/logistics-fulfillment', name: 'LogisticsFulfillmentApp', meta: { workspaceKey: 'logistics-fulfillment' }, component: () => import('@/views/workbench/BusinessAppPage.vue') },
{ path: 'apps/hr-email-sorter', name: 'HrEmailSorter', meta: { workspaceKey: 'hr-email-sorter' }, component: () => import('@/views/workbench/BusinessAppPage.vue') },
@@ -0,0 +1,156 @@
<template>
<div class="batch-extract-page">
<header class="page-header">
<div class="header-left">
<h1>批量字段提取</h1>
<p>从多份文档中自动提取结构化字段,导出为表格</p>
</div>
</header>
<div class="main-layout">
<section class="input-panel">
<div class="card">
<div class="card-title">字段配置</div>
<div class="field-config" v-for="(field, idx) in form.fields" :key="idx">
<el-input v-model="field.name" placeholder="字段名" />
<el-select v-model="field.type" style="width: 120px">
<el-option label="文本" value="text" />
<el-option label="数字" value="number" />
<el-option label="日期" value="date" />
<el-option label="百分比" value="percent" />
</el-select>
<el-checkbox v-model="field.required">必填</el-checkbox>
<el-button type="danger" text @click="removeField(idx)">✕</el-button>
</div>
<el-button @click="addField" style="margin-top: 12px">+ 添加字段</el-button>
<div class="card-title" style="margin-top: 24px">文档内容</div>
<el-input v-model="form.content" type="textarea" :rows="8" placeholder="粘贴文档内容,AI 将自动提取字段" />
<el-button type="primary" :loading="extracting" @click="extract" style="width:100%; margin-top: 12px">
<el-icon v-if="!extracting"><DataAnalysis /></el-icon>
{{ extracting ? '提取中...' : '开始提取' }}
</el-button>
</div>
</section>
<section class="preview-panel">
<div class="card">
<div class="card-header">
<div class="card-title">提取结果</div>
<el-button size="small" @click="exportCSV" v-if="result">导出 CSV</el-button>
</div>
<div v-if="!result" class="empty-preview">
<span class="icon-item">📊</span>
<h3>提取结果预览区</h3>
<p>在左侧配置字段并粘贴文档内容</p>
</div>
<div v-else class="result-table">
<table>
<thead>
<tr>
<th>#</th>
<th v-for="f in form.fields" :key="f.name">{{ f.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, idx) in result.rows" :key="idx">
<td>{{ idx + 1 }}</td>
<td v-for="f in form.fields" :key="f.name">{{ row[f.name] || '-' }}</td>
</tr>
</tbody>
</table>
<div class="result-summary">共提取 {{ result.total }} 条记录</div>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { DataAnalysis } from '@element-plus/icons-vue'
import { extractBatch } from '@/api/batch'
const extracting = ref(false)
const result = ref(null)
const form = ref({
content: '',
fields: [
{ name: '字段1', type: 'text', required: true },
],
})
function addField() {
form.value.fields.push({ name: '新字段', type: 'text', required: false })
}
function removeField(idx) {
form.value.fields.splice(idx, 1)
}
async function extract() {
if (!form.value.content.trim()) {
ElMessage.warning('请输入文档内容')
return
}
if (form.value.fields.length === 0) {
ElMessage.warning('请至少添加一个字段')
return
}
extracting.value = true
try {
const res = await extractBatch(form.value)
result.value = res.data
ElMessage.success('提取完成')
} catch (error) {
ElMessage.error(error.message || '提取失败')
} finally {
extracting.value = false
}
}
function exportCSV() {
if (!result.value || result.value.rows.length === 0) return
const fields = form.value.fields.map(f => f.name).join(',')
const rows = result.value.rows.map(r =>
form.value.fields.map(f => '"' + (r[f.name] || '') + '"').join(',')
).join('\n')
const csv = fields + '\n' + rows
const blob = new Blob([csv], { type: 'text/csv' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = '提取结果.csv'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
</script>
<style scoped>
.batch-extract-page { max-width: 1400px; margin: 0 auto; padding: 24px; }
.page-header { margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #eef1f6; }
.header-left h1 { margin: 0 0 6px; font-size: 24px; color: #1f2d3d; font-weight: 700; }
.header-left p { margin: 0; font-size: 14px; color: #6b7785; }
.main-layout { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 24px; }
.card { border-radius: 16px; background: #fff; border: 1px solid #eef1f6; box-shadow: 0 4px 12px rgba(31, 35, 41, 0.04); padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.card-title { font-size: 16px; font-weight: 700; color: #1f2d3d; margin-bottom: 12px; }
.field-config { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; }
.field-config .el-input { flex: 1; }
.empty-preview { text-align: center; padding: 60px 20px; }
.empty-preview h3 { margin: 16px 0 8px; font-size: 18px; color: #1f2d3d; font-weight: 600; }
.empty-preview p { font-size: 13px; color: #6b7785; }
.icon-item { font-size: 48px; }
.result-table { overflow-x: auto; }
.result-table table { width: 100%; border-collapse: collapse; font-size: 13px; }
.result-table th, .result-table td { padding: 8px 12px; border: 1px solid #eef1f6; text-align: left; }
.result-table th { background: #f8f9fb; font-weight: 600; color: #1f2d3d; }
.result-summary { margin-top: 12px; font-size: 13px; color: #6b7785; text-align: right; }
@media (max-width: 1200px) { .main-layout { grid-template-columns: 1fr; } }
</style>
@@ -0,0 +1,154 @@
<template>
<div class="copy-proofreading-page">
<header class="page-header">
<div class="header-left">
<h1>文案校对</h1>
<p>智能检查错别字、语病、风格问题,给出修改建议</p>
</div>
</header>
<div class="main-layout">
<section class="input-panel">
<div class="card">
<div class="card-title">校对设置</div>
<el-form :model="form" label-position="top">
<el-form-item label="校对模式">
<el-select v-model="form.mode" placeholder="选择校对模式">
<el-option label="基础 - 错别字" value="basic" />
<el-option label="高级 - 语病 + 风格" value="advanced" />
<el-option label="严格 - 全部检查" value="strict" />
</el-select>
</el-form-item>
<el-form-item label="文案内容">
<el-input v-model="form.text" type="textarea" :rows="12" placeholder="粘贴需要校对的文案" />
</el-form-item>
<el-button type="primary" :loading="proofreading" @click="proofread" style="width:100%">
<el-icon v-if="!proofreading"><Search /></el-icon>
{{ proofreading ? '校对中...' : '开始校对' }}
</el-button>
</el-form>
</div>
</section>
<section class="preview-panel">
<div class="card">
<div class="card-header">
<div class="card-title">校对结果</div>
<div class="card-actions" v-if="result">
<el-button size="small" @click="applyFixes"><el-icon><EditPen /></el-icon> 一键修复</el-button>
</div>
</div>
<div v-if="!result" class="empty-preview">
<span class="icon-item">🔍</span>
<h3>校对结果预览区</h3>
<p>在左侧粘贴文案后点击「开始校对」</p>
</div>
<div v-else>
<div class="score-bar">
<div class="score-label">质量评分</div>
<div class="score-value" :class="scoreClass">{{ result.score }} / 100</div>
<div class="score-stats">
<el-tag size="small" type="danger">错误 {{ result.error_count }}</el-tag>
<el-tag size="small" type="warning">警告 {{ result.warning_count }}</el-tag>
<el-tag size="small">建议 {{ result.total_issues }}</el-tag>
</div>
</div>
<div class="items-list">
<div v-for="(item, idx) in result.items" :key="idx" class="item-card" :class="item.level">
<div class="item-type">{{ typeLabel(item.type) }}</div>
<div class="item-content">
<span class="bad-text">{{ item.bad_text }}</span>
<span class="arrow">→</span>
<span class="good-text">{{ item.good_text }}</span>
</div>
<div class="item-desc">{{ item.explanation }}</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Search, EditPen } from '@element-plus/icons-vue'
import { proofreadCopy } from '@/api/copy'
const proofreading = ref(false)
const result = ref(null)
const form = ref({
text: '',
mode: 'basic',
})
const typeLabel = (t) => {
const map = { typo: '错别字', grammar: '语病', style: '风格', number: '数据' }
return map[t] || t
}
const scoreClass = computed(() => {
if (!result.value) return ''
const s = result.value.score
if (s >= 80) return 'score-good'
if (s >= 60) return 'score-warn'
return 'score-bad'
})
async function proofread() {
if (!form.value.text.trim()) {
ElMessage.warning('请输入文案内容')
return
}
proofreading.value = true
try {
const res = await proofreadCopy(form.value)
result.value = res.data
ElMessage.success('校对完成')
} catch (error) {
ElMessage.error(error.message || '校对失败')
} finally {
proofreading.value = false
}
}
import { computed } from 'vue'
</script>
<style scoped>
.copy-proofreading-page { max-width: 1400px; margin: 0 auto; padding: 24px; }
.page-header { margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #eef1f6; }
.header-left h1 { margin: 0 0 6px; font-size: 24px; color: #1f2d3d; font-weight: 700; }
.header-left p { margin: 0; font-size: 14px; color: #6b7785; }
.main-layout { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 24px; }
.card { border-radius: 16px; background: #fff; border: 1px solid #eef1f6; box-shadow: 0 4px 12px rgba(31, 35, 41, 0.04); padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.card-title { font-size: 16px; font-weight: 700; color: #1f2d3d; }
.card-actions { display: flex; gap: 8px; }
.input-panel .card .el-form-item { margin-bottom: 16px; }
.input-panel .el-input__inner, .input-panel .el-textarea__inner { font-size: 13px; }
.empty-preview { text-align: center; padding: 60px 20px; }
.empty-preview h3 { margin: 16px 0 8px; font-size: 18px; color: #1f2d3d; font-weight: 600; }
.empty-preview p { font-size: 13px; color: #6b7785; }
.icon-item { font-size: 48px; }
.score-bar { display: flex; align-items: center; gap: 16px; margin-bottom: 16px; padding: 12px; background: #f8f9fb; border-radius: 8px; }
.score-label { font-size: 13px; color: #6b7785; }
.score-value { font-size: 28px; font-weight: 700; }
.score-good { color: #67c23a; }
.score-warn { color: #e6a23c; }
.score-bad { color: #f56c6c; }
.score-stats { margin-left: auto; display: flex; gap: 8px; }
.items-list { max-height: 500px; overflow-y: auto; }
.item-card { padding: 12px; border-radius: 8px; margin-bottom: 8px; border: 1px solid #eef1f6; }
.item-card.error { border-left: 3px solid #f56c6c; background: #fef0f0; }
.item-card.warning { border-left: 3px solid #e6a23c; background: #fdf6ec; }
.item-card.info { border-left: 3px solid #909399; background: #f4f4f5; }
.item-type { font-size: 12px; color: #909399; margin-bottom: 4px; }
.item-content { font-size: 14px; margin-bottom: 4px; }
.bad-text { color: #f56c6c; text-decoration: line-through; }
.arrow { color: #909399; margin: 0 8px; }
.good-text { color: #67c23a; font-weight: 600; }
.item-desc { font-size: 12px; color: #909399; }
@media (max-width: 1200px) { .main-layout { grid-template-columns: 1fr; } }
</style>
@@ -0,0 +1,170 @@
<template>
<div class="doc-translate-page">
<header class="page-header">
<div class="header-left">
<h1>文档翻译</h1>
<p>支持多语言翻译,可导出为 Word / PPT / Excel / PDF 文档</p>
</div>
</header>
<div class="main-layout">
<section class="input-panel">
<div class="card">
<div class="card-title">翻译设置</div>
<el-form :model="form" label-position="top">
<el-form-item label="源语言">
<el-select v-model="form.source_lang" placeholder="选择源语言">
<el-option label="中文" value="zh" />
<el-option label="English" value="en" />
<el-option label="日本語" value="ja" />
<el-option label="한국어" value="ko" />
<el-option label="Français" value="fr" />
<el-option label="Deutsch" value="de" />
<el-option label="Español" value="es" />
</el-select>
</el-form-item>
<el-form-item label="目标语言">
<el-select v-model="form.target_lang" placeholder="选择目标语言">
<el-option label="中文" value="zh" />
<el-option label="English" value="en" />
<el-option label="日本語" value="ja" />
<el-option label="한국어" value="ko" />
<el-option label="Français" value="fr" />
<el-option label="Deutsch" value="de" />
<el-option label="Español" value="es" />
</el-select>
</el-form-item>
<el-form-item label="翻译内容">
<el-input v-model="form.content" type="textarea" :rows="10" placeholder="粘贴需要翻译的文本内容" />
</el-form-item>
<el-form-item label="导出格式">
<el-select v-model="form.format" placeholder="选择导出格式">
<el-option label="TXT 文本" value="txt" />
<el-option label="Word 文档" value="docx" />
<el-option label="PPT 演示文稿" value="pptx" />
<el-option label="Excel 表格" value="xlsx" />
<el-option label="PDF 文档" value="pdf" />
</el-select>
</el-form-item>
<el-button type="primary" :loading="translating" @click="translateDocument" style="width:100%">
<el-icon v-if="!translating"><Translate /></el-icon>
{{ translating ? '翻译中...' : '开始翻译' }}
</el-button>
</el-form>
</div>
</section>
<section class="preview-panel">
<div class="card">
<div class="card-header">
<div class="card-title">翻译结果</div>
<div class="card-actions" v-if="translatedContent">
<el-button size="small" @click="downloadResult"><el-icon><Download /></el-icon> 下载</el-button>
<el-button size="small" @click="copyResult"><el-icon><DocumentCopy /></el-icon> 复制</el-button>
</div>
</div>
<div v-if="!translatedContent" class="empty-preview">
<span class="icon-item">📄</span>
<h3>翻译结果预览区</h3>
<p>在左侧输入文本并选择语言后点击「开始翻译」</p>
</div>
<div v-else class="result-preview">
<div class="result-info">
<el-tag size="small">{{ form.source_lang }} → {{ form.target_lang }}</el-tag>
<el-tag size="small" type="success">{{ form.format }}</el-tag>
</div>
<div class="result-text">{{ translatedContent }}</div>
</div>
</div>
</section>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Translate, Download, DocumentCopy } from '@element-plus/icons-vue'
import { translateDocument as apiTranslate } from '@/api/document'
const translating = ref(false)
const translatedContent = ref('')
const resultBlob = ref(null)
const resultFormat = ref('txt')
const form = ref({
content: '',
source_lang: 'zh',
target_lang: 'en',
format: 'txt',
})
async function translateDocument() {
if (!form.value.content.trim()) {
ElMessage.warning('请输入翻译内容')
return
}
translating.value = true
try {
const res = await apiTranslate(form.value)
if (res?.data?.result) {
translatedContent.value = res.data.result.translated_content
resultFormat.value = res.data.format
if (res.data.data) {
resultBlob.value = res.data.data
}
ElMessage.success('翻译完成')
}
} catch (error) {
ElMessage.error(error.message || '翻译失败')
} finally {
translating.value = false
}
}
async function downloadResult() {
if (!resultBlob.value) {
ElMessage.warning('暂无可下载的文件')
return
}
const exts = { txt: '.txt', docx: '.docx', pptx: '.pptx', xlsx: '.xlsx', pdf: '.pdf' }
const ext = exts[resultFormat.value] || '.txt'
const url = URL.createObjectURL(resultBlob.value)
const a = document.createElement('a')
a.href = url
a.download = '翻译文档' + ext
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
async function copyResult() {
try {
await navigator.clipboard.writeText(translatedContent.value)
ElMessage.success('已复制到剪贴板')
} catch {
ElMessage.error('复制失败')
}
}
</script>
<style scoped>
.doc-translate-page { max-width: 1400px; margin: 0 auto; padding: 24px; }
.page-header { margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid #eef1f6; }
.header-left h1 { margin: 0 0 6px; font-size: 24px; color: #1f2d3d; font-weight: 700; }
.header-left p { margin: 0; font-size: 14px; color: #6b7785; }
.main-layout { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 24px; }
.card { border-radius: 16px; background: #fff; border: 1px solid #eef1f6; box-shadow: 0 4px 12px rgba(31, 35, 41, 0.04); padding: 20px; }
.card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; }
.card-title { font-size: 16px; font-weight: 700; color: #1f2d3d; }
.card-actions { display: flex; gap: 8px; }
.input-panel .card .el-form-item { margin-bottom: 16px; }
.input-panel .el-input__inner, .input-panel .el-textarea__inner { font-size: 13px; }
.empty-preview { text-align: center; padding: 60px 20px; }
.empty-preview h3 { margin: 16px 0 8px; font-size: 18px; color: #1f2d3d; font-weight: 600; }
.empty-preview p { font-size: 13px; color: #6b7785; }
.icon-item { font-size: 48px; }
.result-preview { max-height: 600px; overflow-y: auto; }
.result-info { display: flex; gap: 8px; margin-bottom: 12px; }
.result-text { white-space: pre-wrap; word-break: break-all; font-size: 14px; line-height: 1.8; color: #3d4854; background: #f8f9fb; border-radius: 8px; padding: 16px; }
@media (max-width: 1200px) { .main-layout { grid-template-columns: 1fr; } }
</style>
@@ -0,0 +1,108 @@
<template>
<div class="smart-assistant-page">
<header class="page-header">
<div class="header-left">
<h1>智能助手</h1>
<p>AI 智能助手 — 对话 · 任务拆解 · 文案生成 · 批量提取</p>
</div>
</header>
<div class="mode-tabs">
<el-radio-group v-model="mode" size="large">
<el-radio-button value="chat">💬 对话</el-radio-button>
<el-radio-button value="task">📋 任务拆解</el-radio-button>
<el-radio-button value="copy">✏️ 文案生成</el-radio-button>
<el-radio-button value="extract">📊 批量提取</el-radio-button>
</el-radio-group>
</div>
<div class="chat-layout">
<div class="chat-panel">
<div class="messages" ref="messagesEl">
<div v-for="(msg, idx) in messages" :key="idx" class="message" :class="msg.role">
<div class="msg-avatar">{{ msg.role === 'user' ? '👤' : '🤖' }}</div>
<div class="msg-body">
<div class="msg-content">{{ msg.content }}</div>
<div v-if="msg.task_plan" class="msg-task-plan">
<div class="plan-title">{{ msg.task_plan.title }}</div>
<div v-for="step in msg.task_plan.steps" :key="step.id" class="plan-step">
<span class="step-num">{{ step.order }}</span>
<span class="step-title">{{ step.title }}</span>
<span class="step-desc">{{ step.desc }}</span>
</div>
</div>
<div class="msg-time">{{ msg.timestamp }}</div>
</div>
</div>
</div>
<div class="input-area">
<el-input v-model="inputText" :rows="3" type="textarea" :placeholder="placeholder" @keydown.enter.ctrl="send" />
<el-button type="primary" @click="send" :loading="sending">发送</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { chatWithAssistant } from '@/api/assistant'
const mode = ref('chat')
const inputText = ref('')
const messages = ref([
{ role: 'assistant', content: '你好!我是你的AI助手,可以帮你文档翻译、文案校对、任务拆解和批量字段提取。', timestamp: new Date().toLocaleTimeString(), type: 'text' }
])
const sending = ref(false)
const messagesEl = ref(null)
const placeholder = computed(() => {
const map = { chat: '输入你的问题...', task: '输入需要拆解的任务...', copy: '输入需要生成的文案类型...', extract: '输入包含字段的文档内容...' }
return map[mode.value] || '输入你的问题...'
})
async function send() {
if (!inputText.value.trim()) return
const userMsg = { role: 'user', content: inputText.value, timestamp: new Date().toLocaleTimeString() }
messages.value.push(userMsg)
inputText.value = ''
sending.value = true
try {
const res = await chatWithAssistant({ message: userMsg.content, mode: mode.value })
const reply = res.data.message
reply.timestamp = new Date().toLocaleTimeString()
messages.value.push(reply)
} catch (error) {
ElMessage.error(error.message || '发送失败')
} finally {
sending.value = false
}
}
</script>
<style scoped>
.smart-assistant-page { max-width: 1200px; margin: 0 auto; padding: 24px; }
.page-header { margin-bottom: 16px; padding-bottom: 16px; border-bottom: 1px solid #eef1f6; }
.page-header h1 { margin: 0 0 4px; font-size: 24px; color: #1f2d3d; }
.page-header p { margin: 0; font-size: 14px; color: #6b7785; }
.mode-tabs { margin-bottom: 16px; }
.chat-layout { display: flex; gap: 24px; }
.chat-panel { flex: 1; background: #fff; border-radius: 16px; border: 1px solid #eef1f6; box-shadow: 0 4px 12px rgba(31, 35, 41, 0.04); overflow: hidden; }
.messages { padding: 16px; max-height: 500px; overflow-y: auto; }
.message { display: flex; gap: 12px; margin-bottom: 16px; }
.message.user { flex-direction: row-reverse; }
.msg-avatar { width: 36px; height: 36px; border-radius: 50%; background: #f0f0f0; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
.msg-body { max-width: 70%; }
.msg-content { background: #f5f5f5; padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.6; }
.message.user .msg-content { background: #2b63d9; color: #fff; }
.msg-time { font-size: 11px; color: #909399; margin-top: 4px; }
.msg-task-plan { margin-top: 8px; background: #f0f7ff; padding: 10px; border-radius: 8px; border: 1px solid #d6e8ff; }
.plan-title { font-weight: 600; font-size: 14px; color: #2b63d9; margin-bottom: 8px; }
.plan-step { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; font-size: 13px; }
.step-num { width: 22px; height: 22px; border-radius: 50%; background: #2b63d9; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; flex-shrink: 0; }
.step-title { font-weight: 600; color: #1f2d3d; }
.step-desc { color: #6b7785; }
.input-area { padding: 16px; border-top: 1px solid #eef1f6; display: flex; gap: 8px; }
.input-area .el-input { flex: 1; }
</style>
@@ -555,6 +555,23 @@ const templateCards = [
actions: ['章节规划', 'LLM 撰写', 'PDF/Word/PPT/Excel 导出'],
results: ['PDF 报告', 'Word 文档', 'PPT 演示文稿', 'Excel 表格'],
},
{
key: 'document-translate',
label: '文档翻译 DW',
summary: 多语言文档翻译,支持 Word/PPT/Excel/PDF 格式导出。',
inputs: ['原文内容', 源语言', 目标语言'],
actions: ['AI 翻译', 格式保留', 文档生成'],
results: ['翻译文档', 翻译文本'],
},
{
key: 'copy-proofreading',
label: '文案校对 DW',
summary: AI 文案校对,检查错别字、语病、风格问题,一键修复。',
inputs: ['文案内容', 校对模式'],
actions: ['错别字检查', 语病检查', 风格建议'],
results: ['校对报告', 修改建议'],
}
},
]
const toolboxItems = [