a95e8b2b73
Admin: - New CreditManagement.vue (tabs: rates, packages, plans, user credits, consumptions, stats) - Sidebar menu + router entry - Full CRUD for credit packages and subscription plans - User credit balance adjustment - Consumption log viewer User: - Credits.vue replaces Upgrade.vue (package purchase, subscription, history tabs) - Credit balance display in topbar + dashboard header CTA card - Navigation restructured: discovery first - Profile redirects to /credits - Dashboard upgrade dialog simplified to redirect to /credits
300 lines
15 KiB
Vue
300 lines
15 KiB
Vue
<template>
|
||
<div class="workspace">
|
||
<div class="welcome-section">
|
||
<div class="welcome-left">
|
||
<h2>你好,{{ auth.user?.username || '用户' }}</h2>
|
||
<p class="welcome-desc">欢迎回来,以下是你的业务概览</p>
|
||
</div>
|
||
<div class="welcome-right">
|
||
<el-button type="primary" @click="$router.push('/translate')">快速翻译</el-button>
|
||
<el-button @click="$router.push('/customers')">客户管理</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-alert
|
||
v-if="usageStats.tier === 'pro' && usageStats.trial_days_left > 0"
|
||
:title="'Pro 试用中,剩余 ' + usageStats.trial_days_left + ' 天'"
|
||
type="success"
|
||
show-icon
|
||
:closable="false"
|
||
style="margin-bottom:16px"
|
||
>
|
||
<template #default>
|
||
<span>试用结束后将自动恢复为免费版。 <el-button text type="primary" size="small" @click="$router.push('/credits')">购买更多次数</el-button></span>
|
||
</template>
|
||
</el-alert>
|
||
|
||
<el-card shadow="never" style="margin-bottom:16px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 0">
|
||
<div>
|
||
<div style="font-size:14px;opacity:0.9">信用余额</div>
|
||
<div style="font-size:28px;font-weight:700">{{ creditBalance ?? '...' }} 次</div>
|
||
</div>
|
||
<div style="text-align:right">
|
||
<el-button type="primary" @click="$router.push('/discovery')" style="margin-bottom:4px">发现新客户</el-button>
|
||
<br>
|
||
<el-button text style="color:#fff;opacity:0.8" @click="$router.push('/credits')">购买次数 →</el-button>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-row :gutter="20">
|
||
<el-col :xs="12" :sm="6" v-for="item in stats" :key="item.label">
|
||
<el-card shadow="hover" class="stat-card" @click="item.route && $router.push(item.route)">
|
||
<div class="stat-value" :style="{ color: item.color }">{{ item.value }}</div>
|
||
<div class="stat-label">{{ item.label }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-card shadow="never" class="section-card">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span class="section-title">本月用量</span>
|
||
<el-button v-if="usageStats.tier !== 'enterprise'" text type="primary" size="small" @click="$router.push('/credits')">购买更多次数</el-button>
|
||
</div>
|
||
</template>
|
||
<div class="usage-grid">
|
||
<div v-for="u in usageItems" :key="u.key" class="usage-item">
|
||
<div class="usage-label">
|
||
<span>{{ u.label }}</span>
|
||
<span class="usage-value">{{ u.used }} / {{ u.limit === 999999999 ? '∞' : u.limit }}</span>
|
||
</div>
|
||
<el-progress :percentage="u.pct" :color="u.pct > 80 ? '#ff4d4f' : u.pct > 50 ? '#faad14' : '#52c41a'" :stroke-width="12" />
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-card shadow="never" class="section-card">
|
||
<template #header><span class="section-title">功能矩阵</span></template>
|
||
<div class="feature-grid">
|
||
<div v-for="f in features" :key="f.title" class="feature-card" @click="$router.push(f.route)">
|
||
<div class="feature-icon" :style="{ background: f.color + '15' }">
|
||
<el-icon :size="26" :color="f.color"><component :is="f.icon" /></el-icon>
|
||
</div>
|
||
<div class="feature-info">
|
||
<h4>{{ f.title }}</h4>
|
||
<p>{{ f.desc }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
|
||
<el-row :gutter="20" style="margin-top:20px">
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card shadow="never">
|
||
<template #header><span class="section-title">快速翻译</span></template>
|
||
<el-input v-model="quickText" type="textarea" :rows="4" placeholder="输入要翻译的文本..." />
|
||
<div style="margin-top:12px;display:flex;gap:8px">
|
||
<el-select v-model="quickLang" style="width:130px" size="default">
|
||
<el-option label="英语" value="en" />
|
||
<el-option label="中文" value="zh" />
|
||
<el-option label="西语" value="es" />
|
||
<el-option label="日语" value="ja" />
|
||
</el-select>
|
||
<el-button type="primary" :loading="translating" @click="doQuickTranslate">翻译</el-button>
|
||
</div>
|
||
<p v-if="quickResult" class="quick-result">{{ quickResult }}</p>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span class="section-title">跟进提醒</span>
|
||
<el-button text type="primary" size="small" @click="$router.push('/followup')">查看全部</el-button>
|
||
</div>
|
||
</template>
|
||
<div v-if="followups.length">
|
||
<div v-for="f in followups" :key="f.id" class="list-item">
|
||
<div class="list-item-left">
|
||
<span class="list-item-name">{{ f.customer_name }}</span>
|
||
<span class="list-item-meta">{{ f.silent_days }}天未联系</span>
|
||
</div>
|
||
<el-button text type="primary" size="small" @click="$router.push('/followup')">去跟进</el-button>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无跟进提醒" :image-size="50" />
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :xs="24" :sm="12" :md="8">
|
||
<el-card shadow="never">
|
||
<template #header>
|
||
<div class="card-header">
|
||
<span class="section-title">沉默客户</span>
|
||
<el-button text type="primary" size="small" @click="$router.push('/customers')">查看全部</el-button>
|
||
</div>
|
||
</template>
|
||
<div v-if="silentCustomers.length">
|
||
<div v-for="c in silentCustomers" :key="c.id" class="list-item">
|
||
<div class="list-item-left">
|
||
<span class="list-item-name">{{ c.name }}</span>
|
||
<span class="list-item-meta">{{ c.silent_days || '?' }}天未联系</span>
|
||
</div>
|
||
<el-tag :type="(c.silent_days || 0) > 14 ? 'danger' : 'warning'" size="small">{{ c.silent_days || 0 }}天</el-tag>
|
||
</div>
|
||
</div>
|
||
<el-empty v-else description="暂无沉默客户" :image-size="50" />
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-dialog v-model="showUpgrade" title="升级套餐" width="700">
|
||
<el-table :data="planData" border>
|
||
<el-table-column label="功能" prop="feature" width="140" />
|
||
<el-table-column label="免费版" width="160">
|
||
<template #default="{ row }"><span>{{ row.free }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="Pro ¥99/月" width="160">
|
||
<template #default="{ row }"><span>{{ row.pro }}</span></template>
|
||
</el-table-column>
|
||
<el-table-column label="企业 ¥399/月" width="160">
|
||
<template #default="{ row }"><span>{{ row.enterprise }}</span></template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div style="text-align:center;margin-top:20px">
|
||
<el-button type="primary" size="large" @click="$router.push('/credits')">购买次数</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, onMounted } from 'vue'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
import { getCreditBalance } from '@/api'
|
||
import { getAnalyticsOverview, translate, getFollowupPending, getSilentCustomers, getUsageStats } from '@/api'
|
||
|
||
const auth = useAuthStore()
|
||
const stats = ref([])
|
||
const quickText = ref('')
|
||
const quickLang = ref('en')
|
||
const quickResult = ref('')
|
||
const translating = ref(false)
|
||
const followups = ref([])
|
||
const silentCustomers = ref([])
|
||
const usageStats = ref({ tier: 'free', limits: {}, usage: {} })
|
||
const showUpgrade = ref(false)
|
||
const creditBalance = ref(null)
|
||
|
||
const usageItems = computed(() => {
|
||
const u = usageStats.value.usage || {}
|
||
const l = usageStats.value.limits || {}
|
||
const tier = usageStats.value.tier || 'free'
|
||
const items = [
|
||
{ key: 'translate_chars', label: '翻译字符', used: u.translate_chars || 0, limit: l.translate_chars || 0 },
|
||
{ key: 'replies', label: '回复建议', used: u.replies || 0, limit: l.replies || 0 },
|
||
{ key: 'marketing', label: '营销生成', used: u.marketing || 0, limit: l.marketing || 0 },
|
||
{ key: 'customers', label: '客户数', used: u.customers || 0, limit: l.customers || 0 },
|
||
{ key: 'products', label: '产品数', used: u.products || 0, limit: l.products || 0 },
|
||
{ key: 'quotations', label: '报价单', used: u.quotations || 0, limit: l.quotations || 0 },
|
||
]
|
||
return items.map(i => ({
|
||
...i,
|
||
pct: i.limit >= 999999 ? 0 : Math.min(100, Math.round((i.used / (i.limit || 1)) * 100)),
|
||
}))
|
||
})
|
||
|
||
const planData = [
|
||
{ feature: '翻译字符/天', free: '5,000', pro: '50,000', enterprise: '∞' },
|
||
{ feature: '回复建议/天', free: '20', pro: '200', enterprise: '∞' },
|
||
{ feature: '营销生成/天', free: '5', pro: '50', enterprise: '∞' },
|
||
{ feature: '客户管理', free: '最多5个', pro: '最多100个', enterprise: '∞' },
|
||
{ feature: '产品管理', free: '最多1个', pro: '最多20个', enterprise: '∞' },
|
||
{ feature: '报价单/天', free: '3', pro: '30', enterprise: '∞' },
|
||
{ feature: '跟进提醒', free: '—', pro: '✓', enterprise: '✓' },
|
||
{ feature: 'WhatsApp 集成', free: '—', pro: '✓', enterprise: '✓' },
|
||
{ feature: '挖掘新客', free: '—', pro: '✓', enterprise: '✓' },
|
||
{ feature: '团队协作', free: '—', pro: '—', enterprise: '✓' },
|
||
]
|
||
|
||
const features = [
|
||
{ title: '智能翻译', desc: '多语言翻译 + 回复建议 + 信息提取', icon: 'ChatLineSquare', color: '#1890ff', route: '/translate' },
|
||
{ title: '客户管理', desc: 'CRM + 健康评分 + 跟进记录', icon: 'User', color: '#52c41a', route: '/customers' },
|
||
{ title: '产品库', desc: '双语产品管理 + 关键词标签', icon: 'Goods', color: '#faad14', route: '/products' },
|
||
{ title: '报价单', desc: 'AI 智能报价 + PDF 导出 + 状态追踪', icon: 'DocumentCopy', color: '#ff4d4f', route: '/quotations' },
|
||
{ title: '营销素材', desc: 'AI 生成开发信/WhatsApp话术/产品描述', icon: 'Promotion', color: '#722ed1', route: '/marketing' },
|
||
{ title: '挖掘新客', desc: 'AI 搜索潜在客户 + 开发信生成', icon: 'Search', color: '#13c2c2', route: '/discovery' },
|
||
{ title: '智能跟进', desc: '自动生成跟进话术 + 一键发送', icon: 'Message', color: '#eb2f96', route: '/followup' },
|
||
{ title: '数据分析', desc: '客户/翻译/报价多维度统计', icon: 'DataAnalysis', color: '#1890ff', route: '/analytics' },
|
||
{ title: '团队协作', desc: '团队管理 + 角色权限 + 成员邀请', icon: 'UserFilled', color: '#fa8c16', route: '/team' },
|
||
]
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
getCreditBalance().then(res => { creditBalance.value = res.balance }).catch(() => {})
|
||
const [overview, fup, silent, usage] = await Promise.all([
|
||
getAnalyticsOverview().catch(() => null),
|
||
getFollowupPending().catch(() => []),
|
||
getSilentCustomers(7).catch(() => []),
|
||
getUsageStats().catch(() => null),
|
||
])
|
||
if (usage) {
|
||
usageStats.value = usage.data || usage
|
||
}
|
||
const d = overview?.data || overview || {}
|
||
stats.value = [
|
||
{ value: d.customers?.total || 0, label: '客户总数', color: '#1890ff', route: '/customers' },
|
||
{ value: d.translations?.today || 0, label: '今日翻译', color: '#52c41a', route: '/translate' },
|
||
{ value: d.quotations?.total || 0, label: '报价单数', color: '#faad14', route: '/quotations' },
|
||
{ value: fup?.length || 0, label: '待跟进', color: '#ff4d4f', route: '/followup' },
|
||
]
|
||
followups.value = Array.isArray(fup) ? fup.slice(0, 5) : []
|
||
silentCustomers.value = Array.isArray(silent) ? silent.slice(0, 5) : (silent?.items || silent?.data || [])
|
||
} catch { /* ignore */ }
|
||
})
|
||
|
||
async function doQuickTranslate() {
|
||
if (!quickText.value.trim()) return
|
||
translating.value = true
|
||
try {
|
||
const res = await translate({ text: quickText.value, target_lang: quickLang.value })
|
||
quickResult.value = res.data?.translated_text || res.translated_text || ''
|
||
} catch { quickResult.value = '翻译失败' }
|
||
finally { translating.value = false }
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.workspace { max-width: 1200px; margin: 0 auto; }
|
||
.welcome-section { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
|
||
.welcome-left h2 { font-size: 24px; font-weight: 700; color: #333; margin: 0 0 4px; }
|
||
.welcome-desc { font-size: 14px; color: #999; margin: 0; }
|
||
.welcome-right { display: flex; gap: 12px; }
|
||
.stat-card { cursor: pointer; text-align: center; transition: all 0.25s; }
|
||
.stat-card:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.08); }
|
||
.stat-value { font-size: 32px; font-weight: 700; }
|
||
.stat-label { font-size: 14px; color: #999; margin-top: 4px; }
|
||
.section-card { margin-top: 20px; }
|
||
.section-title { font-weight: 600; font-size: 15px; }
|
||
.card-header { display: flex; justify-content: space-between; align-items: center; }
|
||
.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
|
||
@media (max-width: 768px) { .feature-grid { grid-template-columns: repeat(2, 1fr); } }
|
||
.feature-card { display: flex; gap: 16px; padding: 20px; border-radius: 10px; cursor: pointer; transition: all 0.25s; border: 1px solid #f0f0f0; }
|
||
.feature-card:hover { border-color: #d9d9d9; box-shadow: 0 4px 16px rgba(0,0,0,0.06); transform: translateY(-2px); }
|
||
.feature-icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||
.feature-info { flex: 1; min-width: 0; }
|
||
.feature-info h4 { font-size: 14px; font-weight: 600; color: #333; margin: 0 0 4px; }
|
||
.feature-info p { font-size: 12px; color: #999; margin: 0; line-height: 1.4; }
|
||
.list-item { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #f5f5f5; }
|
||
.list-item:last-child { border-bottom: none; }
|
||
.list-item-left { display: flex; flex-direction: column; gap: 2px; }
|
||
.list-item-name { font-size: 14px; font-weight: 500; color: #333; }
|
||
.list-item-meta { font-size: 12px; color: #999; }
|
||
.quick-result { margin-top: 12px; padding: 12px; background: #f5f5f5; border-radius: 6px; font-size: 13px; line-height: 1.5; }
|
||
.usage-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
||
.usage-item { }
|
||
.usage-label { display: flex; justify-content: space-between; font-size: 13px; color: #666; margin-bottom: 6px; }
|
||
.usage-value { color: #999; }
|
||
|
||
@media (max-width: 768px) {
|
||
.welcome-section { flex-direction: column; align-items: flex-start; gap: 12px; }
|
||
.welcome-left h2 { font-size: 20px; }
|
||
.feature-card { padding: 14px; gap: 12px; }
|
||
.feature-icon { width: 40px; height: 40px; }
|
||
.feature-icon :deep(.el-icon) { font-size: 20px !important; }
|
||
.feature-info h4 { font-size: 13px; }
|
||
.feature-info p { font-size: 11px; display: none; }
|
||
}
|
||
</style>
|