feat: AI enhancements on translate/customers/analytics pages

Translate: auto-detect source language toggle, one-click reply button that auto-fills inquiry and generates suggestions. Customers: AI suggested action column with smart heuristics based on status/health/last_contact. Analytics: AI-generated analysis summary showing key insights (customer count, top market, daily activity).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
wlt
2026-06-17 10:48:19 +08:00
parent 45e98a9c82
commit 5590f81536
3 changed files with 78 additions and 3 deletions
+32
View File
@@ -1,5 +1,9 @@
<template>
<div v-loading="loading" element-loading-text="加载数据分析中...">
<transition name="el-fade-in">
<el-alert v-if="aiSummary" :title="aiSummary" type="success" :closable="false" show-icon style="margin-bottom:20px" />
</transition>
<el-row :gutter="20">
<el-col :xs="12" :sm="6" v-for="item in cards" :key="item.label">
<el-card shadow="hover" style="text-align:center;margin-bottom:20px">
@@ -48,6 +52,33 @@ const loading = ref(true)
const cards = ref([])
const statusData = ref([])
const countryData = ref([])
const aiSummary = ref('')
function generateAiSummary(d) {
const customers = d.customers || {}
const total = customers.total || 0
const statusCounts = customers.status_counts || customers.by_status || {}
const leads = statusCounts.lead || 0
const negotiating = statusCounts.negotiating || 0
const deals = statusCounts.customer || 0
const countries = customers.by_country || d.countries || []
const topCountry = countries.length ? (countries[0].country || countries[0].name) : '—'
const todayTrans = d.translations?.today || 0
const todayMsgs = d.messages?.today || 0
const parts = []
if (total > 0) {
parts.push(`共有 ${total} 个客户`)
if (leads > 0) parts.push(`${leads} 个潜在客户待开发`)
if (negotiating > 0) parts.push(`${negotiating} 个正在洽谈中`)
if (deals > 0) parts.push(`已成交 ${deals}`)
}
if (topCountry !== '—') parts.push(`主要市场集中在 ${topCountry}`)
if (todayTrans > 0) parts.push(`今日翻译 ${todayTrans}`)
if (todayMsgs > 0) parts.push(`今日消息 ${todayMsgs}`)
return parts.length > 0 ? '📊 ' + parts.join('') + '。' : ''
}
onMounted(async () => {
loading.value = true
@@ -70,6 +101,7 @@ onMounted(async () => {
const countries = customers.by_country || d.countries || []
const countryTotal = countries.reduce((a, b) => a + (b.count || b.value || 0), 0) || 1
countryData.value = (countries.slice ? countries.slice(0, 10) : []).map(c => ({ country: c.country || c.name, count: c.count || c.value || 0, pct: Math.round((c.count || c.value || 0) / countryTotal * 100) }))
aiSummary.value = generateAiSummary(d)
} catch { ElMessage.error('加载分析数据失败') }
finally { loading.value = false }
})
+24
View File
@@ -26,6 +26,13 @@
<el-tag :type="healthType(row.health_grade)" size="small">{{ row.health_grade || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="AI建议" width="130">
<template #default="{ row }">
<el-tag :type="aiSuggestionType(row)" size="small" effect="plain" style="cursor:default">
{{ aiSuggestion(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
@@ -109,6 +116,23 @@ function statusType(s) { return { lead: 'info', negotiating: 'warning', customer
function statusLabel(s) { return { lead: '潜在客户', negotiating: '洽谈中', customer: '已成交', lost: '流失' }[s] || s }
function healthType(g) { return { A: 'success', B: 'warning', C: 'danger' }[g] || 'info' }
function aiSuggestion(row) {
if (row.status === 'lost') return '流失客户'
if (row.status === 'customer' && row.health_grade === 'C') return '需要挽回'
if (row.status === 'customer') return '维护关系'
if (row.status === 'negotiating') return '尽快报价'
if (!row.last_contact || Date.now() - new Date(row.last_contact).getTime() > 30 * 24 * 60 * 60 * 1000) return '建议跟进'
return '可跟进'
}
function aiSuggestionType(row) {
if (row.status === 'lost') return 'danger'
if (row.status === 'customer' && row.health_grade === 'C') return 'danger'
if (row.status === 'customer') return 'success'
if (row.status === 'negotiating') return 'warning'
if (!row.last_contact || Date.now() - new Date(row.last_contact).getTime() > 30 * 24 * 60 * 60 * 1000) return 'warning'
return 'info'
}
onMounted(load)
async function load() {
+22 -3
View File
@@ -10,14 +10,18 @@
<el-option label="西班牙语" value="es" />
<el-option label="日语" value="ja" />
</el-select>
<el-switch v-model="autoDetect" active-text="自动检测" style="margin-right:4px" />
<el-button type="primary" :loading="loading" @click="doTranslate">翻译</el-button>
<el-button @click="showReply = !showReply">回复建议</el-button>
<el-button @click="showExtract = !showExtract">信息提取</el-button>
</div>
<div v-if="result" style="padding:16px;background:#f5f5f5;border-radius:6px">
<p style="white-space:pre-wrap">{{ result }}</p>
<div style="margin-top:8px;display:flex;gap:8px">
<div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap">
<el-button text type="primary" size="small" @click="copyText(result)">复制</el-button>
<el-button text type="success" size="small" @click="quickReply">
<el-icon style="margin-right:4px"><ChatLineSquare /></el-icon>一键回复
</el-button>
</div>
</div>
</el-card>
@@ -51,11 +55,13 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { translate, translateReply, extractInfo } from '@/api'
import { ElMessage } from 'element-plus'
import { ChatLineSquare } from '@element-plus/icons-vue'
const form = ref({ text: '', target_lang: 'en' })
const autoDetect = ref(true)
const loading = ref(false)
const result = ref('')
const showReply = ref(false)
@@ -67,12 +73,18 @@ const extractText = ref('')
const extractLoading = ref(false)
const extractResult = ref('')
watch(autoDetect, (v) => {
if (v) form.value.target_lang = 'en'
})
async function doTranslate() {
if (!form.value.text.trim()) return
loading.value = true
result.value = '翻译中...'
try {
const res = await translate(form.value)
const payload = { text: form.value.text, target_lang: form.value.target_lang }
if (autoDetect.value) payload.source_lang = 'auto'
const res = await translate(payload)
result.value = res.data?.translated_text || res.translated_text || ''
} catch {
result.value = ''
@@ -81,6 +93,13 @@ async function doTranslate() {
finally { loading.value = false }
}
function quickReply() {
if (!result.value) return
replyInquiry.value = form.value.text
showReply.value = true
getReply()
}
async function getReply() {
if (!replyInquiry.value.trim()) return
replyLoading.value = true