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:
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user