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:
@@ -0,0 +1,4 @@
|
||||
import http from './http'
|
||||
export function chatWithAssistant(data) {
|
||||
return http.post('/assistant/chat', data)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 } })
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import http from './http'
|
||||
export function translateDocument(data) {
|
||||
return http.post('/document/translate', data)
|
||||
}
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user