Add admin-frontend and user-frontend standalone projects, certification/invoice/discovery features, fix auth header and theme consistency
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="item in cards" :key="item.label">
|
||||
<el-card shadow="hover" style="text-align:center;margin-bottom:20px">
|
||||
<div class="card-value">{{ item.value }}</div>
|
||||
<div class="card-label">{{ item.label }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>客户状态分布</span></template>
|
||||
<div style="padding:20px;text-align:center">
|
||||
<div v-for="s in statusData" :key="s.label" style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
||||
<span style="width:80px;text-align:right">{{ s.label }}</span>
|
||||
<el-progress :percentage="s.pct" :color="s.color" :stroke-width="18" />
|
||||
<span style="width:40px">{{ s.count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>国家分布 Top 10</span></template>
|
||||
<div style="padding:20px">
|
||||
<div v-for="c in countryData" :key="c.country" style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
||||
<span style="width:100px">{{ c.country }}</span>
|
||||
<el-progress :percentage="c.pct" :stroke-width="16" />
|
||||
<span style="width:30px">{{ c.count }}</span>
|
||||
</div>
|
||||
<el-empty v-if="!countryData.length" description="暂无数据" :image-size="50" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getAnalyticsOverview } from '@/api'
|
||||
|
||||
const cards = ref([])
|
||||
const statusData = ref([])
|
||||
const countryData = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await getAnalyticsOverview()
|
||||
const d = res.data || res
|
||||
cards.value = [
|
||||
{ value: d.customers?.total || 0, label: '客户总数' },
|
||||
{ value: d.translations?.today || 0, label: '今日翻译' },
|
||||
{ value: d.quotations?.total || 0, label: '报价单数' },
|
||||
{ value: d.messages?.today || 0, label: '今日消息' },
|
||||
]
|
||||
const customers = d.customers || {}
|
||||
const statusCounts = customers.status_counts || customers.by_status || {}
|
||||
const total = Object.values(statusCounts).reduce((a, b) => a + b, 0) || 1
|
||||
const statusLabels = { lead: '潜在客户', negotiating: '洽谈中', customer: '已成交', lost: '流失' }
|
||||
const statusColors = { lead: '#909399', negotiating: '#e6a23c', customer: '#67c23a', lost: '#f56c6c' }
|
||||
statusData.value = Object.entries(statusCounts).map(([k, v]) => ({ label: statusLabels[k] || k, count: v, pct: Math.round(v / total * 100), color: statusColors[k] || '#409eff' }))
|
||||
|
||||
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) }))
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-value { font-size: 28px; font-weight: 700; color: #409eff; }
|
||||
.card-label { font-size: 13px; color: #999; margin-top: 4px; }
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<template #header><span>实名认证</span></template>
|
||||
<el-alert v-if="status === 'approved'" title="认证已通过" type="success" show-icon :closable="false" style="margin-bottom:16px" />
|
||||
<el-alert v-else-if="status === 'rejected'" :title="'认证被拒绝:' + (reason || '')" type="error" show-icon :closable="false" style="margin-bottom:16px" />
|
||||
<el-alert v-else-if="status === 'pending'" title="认证审核中" type="warning" show-icon :closable="false" style="margin-bottom:16px" />
|
||||
|
||||
<el-form :model="form" label-width="100" size="large" v-if="status !== 'approved'">
|
||||
<el-form-item label="认证类型">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio value="individual">个人实名</el-radio>
|
||||
<el-radio value="enterprise">企业认证</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="身份证号" v-if="form.type === 'individual'"><el-input v-model="form.id_number" /></el-form-item>
|
||||
<el-form-item label="公司名称" v-if="form.type === 'enterprise'"><el-input v-model="form.company_name" /></el-form-item>
|
||||
<el-form-item label="税号" v-if="form.type === 'enterprise'"><el-input v-model="form.tax_id" /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="submit">提交认证</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-empty v-if="!loading && status === null" description="点击底部按钮获取认证状态" :image-size="60" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { submitCertification, getCertificationStatus } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const status = ref(null)
|
||||
const reason = ref('')
|
||||
const form = reactive({ type: 'individual', name: '', id_number: '', company_name: '', tax_id: '' })
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await getCertificationStatus()
|
||||
const d = res.data || res
|
||||
status.value = d.status
|
||||
reason.value = d.reason || ''
|
||||
} catch { /* not certified yet */ }
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
loading.value = true
|
||||
try {
|
||||
await submitCertification(form)
|
||||
ElMessage.success('提交成功,等待审核')
|
||||
status.value = 'pending'
|
||||
} catch (e) { ElMessage.error(e?.detail || '提交失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
||||
<el-input v-model="searchQuery" placeholder="搜索客户名称/公司..." style="width:260px" clearable @clear="load" @keyup.enter="load" />
|
||||
<el-select v-model="statusFilter" placeholder="状态" clearable style="width:140px" @change="load">
|
||||
<el-option label="潜在客户" value="lead" />
|
||||
<el-option label="洽谈中" value="negotiating" />
|
||||
<el-option label="已成交" value="customer" />
|
||||
<el-option label="流失" value="lost" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="showCreate = true">新增客户</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="list" v-loading="loading" stripe style="width:100%">
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column prop="company" label="公司" min-width="160" />
|
||||
<el-table-column prop="country" label="国家" width="120" />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="health_grade" label="健康度" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="healthType(row.health_grade)" size="small">{{ row.health_grade || '-' }}</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>
|
||||
<el-button type="primary" link size="small" @click="editRow(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除?" @confirm="deleteRow(row.id)">
|
||||
<template #reference>
|
||||
<el-button type="danger" link size="small">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && !list.length" description="暂无客户" />
|
||||
<div style="margin-top:16px;text-align:right">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
:page-size="size"
|
||||
:total="total"
|
||||
layout="prev,pager,next"
|
||||
@current-change="load"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showCreate" :title="editing ? '编辑客户' : '新增客户'" width="500">
|
||||
<el-form :model="form" label-width="80">
|
||||
<el-form-item label="名称"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="公司"><el-input v-model="form.company" /></el-form-item>
|
||||
<el-form-item label="国家"><el-input v-model="form.country" /></el-form-item>
|
||||
<el-form-item label="手机"><el-input v-model="form.phone" /></el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width:100%">
|
||||
<el-option label="潜在客户" value="lead" />
|
||||
<el-option label="洽谈中" value="negotiating" />
|
||||
<el-option label="已成交" value="customer" />
|
||||
<el-option label="流失" value="lost" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showDetail" title="客户详情" width="600">
|
||||
<template v-if="detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="名称">{{ detail.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="公司">{{ detail.company }}</el-descriptions-item>
|
||||
<el-descriptions-item label="国家">{{ detail.country }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">{{ detail.phone }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ statusLabel(detail.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="健康度">{{ detail.health_grade || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { listCustomers, createCustomer, updateCustomer, deleteCustomer, getCustomer } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const page = ref(1)
|
||||
const size = ref(20)
|
||||
const total = ref(0)
|
||||
const searchQuery = ref('')
|
||||
const statusFilter = ref('')
|
||||
const showCreate = ref(false)
|
||||
const showDetail = ref(false)
|
||||
const editing = ref(false)
|
||||
const saving = ref(false)
|
||||
const detail = ref(null)
|
||||
const form = reactive({ name: '', company: '', country: '', phone: '', status: 'lead' })
|
||||
|
||||
function statusType(s) { return { lead: 'info', negotiating: 'warning', customer: 'success', lost: 'danger' }[s] || 'info' }
|
||||
function statusLabel(s) { return { lead: '潜在客户', negotiating: '洽谈中', customer: '已成交', lost: '流失' }[s] || s }
|
||||
function healthType(g) { return { A: 'success', B: 'warning', C: 'danger' }[g] || 'info' }
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: page.value, size: size.value }
|
||||
if (searchQuery.value) params.query = searchQuery.value
|
||||
if (statusFilter.value) params.status = statusFilter.value
|
||||
const res = await listCustomers(params)
|
||||
const d = res.data || res
|
||||
list.value = d.items || d.rows || d.data || d || []
|
||||
total.value = d.total || list.value.length
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await updateCustomer(editing.value, form)
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await createCustomer(form)
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
showCreate.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.detail || '操作失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
function editRow(row) {
|
||||
editing.value = row.id
|
||||
Object.assign(form, { name: row.name, company: row.company || '', country: row.country || '', phone: row.phone || '', status: row.status || 'lead' })
|
||||
showCreate.value = true
|
||||
}
|
||||
|
||||
async function viewDetail(row) {
|
||||
try {
|
||||
const res = await getCustomer(row.id)
|
||||
detail.value = res.data || res
|
||||
showDetail.value = true
|
||||
} catch { ElMessage.error('获取详情失败') }
|
||||
}
|
||||
|
||||
async function deleteRow(id) {
|
||||
try {
|
||||
await deleteCustomer(id)
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
} catch { ElMessage.error('删除失败') }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="客户挖掘" name="search">
|
||||
<el-card shadow="never">
|
||||
<el-form :model="form" label-width="100">
|
||||
<el-form-item label="产品"><el-input v-model="form.product" placeholder="你的产品名称" /></el-form-item>
|
||||
<el-form-item label="目标市场"><el-input v-model="form.market" placeholder="如:美国、德国" /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="search">挖掘</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="results.length">
|
||||
<el-card v-for="r in results" :key="r.id || r.name" shadow="hover" style="margin-top:12px">
|
||||
<h4>{{ r.name }} <el-tag v-if="r.match_score" size="small" :type="scoreType(r.match_score)">{{ r.match_score }}%</el-tag></h4>
|
||||
<p v-if="r.description" style="color:#666;font-size:13px">{{ r.description }}</p>
|
||||
<p v-if="r.contact" style="font-size:12px;color:#999">联系方式:{{ r.contact }}</p>
|
||||
<div style="margin-top:8px">
|
||||
<el-button size="small" type="primary" @click="addCustomer(r)">添加为客户</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-empty v-if="!loading && !results.length && searched" description="未找到匹配客户" :image-size="60" />
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="开发信生成" name="outreach">
|
||||
<el-card shadow="never">
|
||||
<el-form :model="outForm" label-width="100">
|
||||
<el-form-item label="目标公司"><el-input v-model="outForm.company" placeholder="公司名称" /></el-form-item>
|
||||
<el-form-item label="你的产品"><el-input v-model="outForm.product" placeholder="你的产品/服务" /></el-form-item>
|
||||
<el-form-item label="渠道">
|
||||
<el-select v-model="outForm.channel" style="width:160px">
|
||||
<el-option label="开发信" value="email" />
|
||||
<el-option label="LinkedIn" value="linkedin" />
|
||||
<el-option label="WhatsApp" value="whatsapp" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="outLoading" @click="generateOutreach">生成</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div v-if="outreachResult" style="padding:16px;background:#f5f5f5;border-radius:6px;margin-top:12px;white-space:pre-wrap">{{ outreachResult }}</div>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { discoverySearch, discoveryOutreach, createCustomer } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const tab = ref('search')
|
||||
const loading = ref(false)
|
||||
const searched = ref(false)
|
||||
const results = ref([])
|
||||
const form = ref({ product: '', market: '' })
|
||||
const outForm = ref({ company: '', product: '', channel: 'email' })
|
||||
const outLoading = ref(false)
|
||||
const outreachResult = ref('')
|
||||
|
||||
function scoreType(s) { if (s >= 80) return 'success'; if (s >= 60) return 'warning'; return 'info' }
|
||||
|
||||
async function search() {
|
||||
if (!form.value.product) { ElMessage.warning('请输入产品'); return }
|
||||
loading.value = true
|
||||
searched.value = true
|
||||
try {
|
||||
const res = await discoverySearch(form.value)
|
||||
const d = res.data || res
|
||||
results.value = d.companies || d.items || d.results || d || []
|
||||
} catch { ElMessage.error('挖掘失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function addCustomer(r) {
|
||||
try {
|
||||
await createCustomer({ name: r.name, company: r.name, country: r.country || '', description: r.description || '' })
|
||||
ElMessage.success('已添加为客户')
|
||||
} catch (e) { ElMessage.error(e?.detail || '添加失败') }
|
||||
}
|
||||
|
||||
async function generateOutreach() {
|
||||
if (!outForm.value.company || !outForm.value.product) { ElMessage.warning('请填写完整'); return }
|
||||
outLoading.value = true
|
||||
try {
|
||||
const res = await discoveryOutreach(outForm.value)
|
||||
outreachResult.value = res.data?.content || res.content || res.text || ''
|
||||
} catch { ElMessage.error('生成失败') }
|
||||
finally { outLoading.value = false }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<template #header><span>意见反馈</span></template>
|
||||
<el-form :model="form" label-width="80" size="large">
|
||||
<el-form-item label="类别">
|
||||
<el-select v-model="form.type" style="width:200px">
|
||||
<el-option label="功能建议" value="feature" />
|
||||
<el-option label="问题反馈" value="bug" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容">
|
||||
<el-input v-model="form.content" type="textarea" :rows="5" placeholder="请详细描述你的建议或问题..." />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系方式">
|
||||
<el-input v-model="form.contact" placeholder="手机号或邮箱(选填)" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="submit">提交</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" style="margin-top:16px">
|
||||
<template #header><span>常见问题</span></template>
|
||||
<el-collapse>
|
||||
<el-collapse-item title="如何使用智能翻译?" name="1">
|
||||
<p style="font-size:13px;color:#666">在工作台点击"智能翻译",输入需要翻译的外贸文本,选择目标语言即可。还支持回复建议和信息提取功能。</p>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="如何管理客户?" name="2">
|
||||
<p style="font-size:13px;color:#666">在"客户管理"页面可以新增、编辑、删除客户,支持按状态筛选和关键词搜索。还可以查看客户的健康度分析和跟进记录。</p>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item title="如何开具发票?" name="3">
|
||||
<p style="font-size:13px;color:#666">在"个人中心 - 发票管理"页面提交开票申请,填写抬头和金额。管理员审核通过后即可获取发票。</p>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { submitFeedback } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const form = reactive({ type: 'feature', content: '', contact: '' })
|
||||
|
||||
async function submit() {
|
||||
if (!form.content) { ElMessage.warning('请输入反馈内容'); return }
|
||||
loading.value = true
|
||||
try {
|
||||
await submitFeedback(form)
|
||||
ElMessage.success('感谢你的反馈!')
|
||||
form.content = ''
|
||||
form.contact = ''
|
||||
} catch (e) { ElMessage.error(e?.detail || '提交失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="s in statItems" :key="s.label">
|
||||
<el-card shadow="hover" style="text-align:center;margin-bottom:16px">
|
||||
<div class="stat-num">{{ s.value }}</div>
|
||||
<div class="stat-label">{{ s.label }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="待跟进" name="pending">
|
||||
<el-card shadow="never">
|
||||
<el-table :data="pendingList" v-loading="loading" stripe style="width:100%">
|
||||
<el-table-column prop="customer_name" label="客户" min-width="140" />
|
||||
<el-table-column label="沉默天数" width="100">
|
||||
<template #default="{ row }"><el-tag :type="row.silent_days > 7 ? 'danger' : 'warning'" size="small">{{ row.silent_days }}天</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column label="跟进内容" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<span style="font-size:13px;color:#666">{{ (row.content || row.suggested_content || '').slice(0, 80) }}{{ (row.content || row.suggested_content || '').length > 80 ? '...' : '' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" @click="sendFollowup(row)">发送</el-button>
|
||||
<el-button size="small" @click="editFollowup(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && !pendingList.length" description="暂无待跟进" />
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="历史记录" name="history">
|
||||
<el-card shadow="never">
|
||||
<el-table :data="logList" v-loading="loadingLogs" stripe style="width:100%">
|
||||
<el-table-column prop="customer_name" label="客户" min-width="140" />
|
||||
<el-table-column prop="content" label="内容" min-width="260">
|
||||
<template #default="{ row }">{{ (row.content || '').slice(0, 80) }}{{ row.content?.length > 80 ? '...' : '' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'sent' ? 'success' : 'info'" size="small">{{ row.status === 'sent' ? '已发送' : '草稿' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="时间" width="170" />
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="showEdit" title="编辑跟进内容" width="500">
|
||||
<el-input v-model="editContent" type="textarea" :rows="6" />
|
||||
<template #footer>
|
||||
<el-button @click="showEdit = false">取消</el-button>
|
||||
<el-button type="primary" :loading="editSaving" @click="saveEdit">保存并发送</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getFollowupStats, getFollowupPending, getFollowupLogs, markFollowupSent, editFollowup as editFollowupApi, scanFollowups } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const tab = ref('pending')
|
||||
const loading = ref(false)
|
||||
const loadingLogs = ref(false)
|
||||
const pendingList = ref([])
|
||||
const logList = ref([])
|
||||
const statItems = ref([{ label: '待跟进', value: 0 }, { label: '已发送', value: 0 }, { label: '已回复', value: 0 }, { label: '完成率', value: '0%' }])
|
||||
const showEdit = ref(false)
|
||||
const editContent = ref('')
|
||||
const editId = ref(null)
|
||||
const editSaving = ref(false)
|
||||
|
||||
onMounted(() => { loadStats(); loadPending(); loadLogs() })
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await getFollowupStats()
|
||||
const d = res.data || res
|
||||
statItems.value = [
|
||||
{ label: '待跟进', value: d.pending || d.total_pending || 0 },
|
||||
{ label: '已发送', value: d.sent || d.total_sent || 0 },
|
||||
{ label: '已回复', value: d.replied || d.total_replied || 0 },
|
||||
{ label: '完成率', value: (d.completion_rate || d.rate || 0) + '%' },
|
||||
]
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function loadPending() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getFollowupPending()
|
||||
const d = res.data || res
|
||||
pendingList.value = d.items || d || []
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadLogs() {
|
||||
loadingLogs.value = true
|
||||
try {
|
||||
const res = await getFollowupLogs({ page: 1, size: 50 })
|
||||
const d = res.data || res
|
||||
logList.value = d.items || d.rows || d.data || d || []
|
||||
} catch { /* ignore */ }
|
||||
finally { loadingLogs.value = false }
|
||||
}
|
||||
|
||||
async function sendFollowup(row) {
|
||||
try {
|
||||
await markFollowupSent(row.id)
|
||||
ElMessage.success('已发送')
|
||||
loadPending()
|
||||
loadLogs()
|
||||
} catch { ElMessage.error('发送失败') }
|
||||
}
|
||||
|
||||
function editFollowup(row) {
|
||||
editId.value = row.id
|
||||
editContent.value = row.content || row.suggested_content || ''
|
||||
showEdit.value = true
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
editSaving.value = true
|
||||
try {
|
||||
await editFollowupApi(editId.value, { content: editContent.value })
|
||||
ElMessage.success('已保存并发送')
|
||||
showEdit.value = false
|
||||
loadPending()
|
||||
loadLogs()
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
finally { editSaving.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.stat-num { font-size: 28px; font-weight: 700; color: #409eff; }
|
||||
.stat-label { font-size: 13px; color: #999; margin-top: 4px; }
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-tabs v-model="tab">
|
||||
<el-tab-pane label="开票申请" name="apply">
|
||||
<el-card shadow="never">
|
||||
<el-form :model="form" label-width="100" size="large">
|
||||
<el-form-item label="发票类型">
|
||||
<el-radio-group v-model="form.type">
|
||||
<el-radio value="individual">个人发票</el-radio>
|
||||
<el-radio value="enterprise">企业发票</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="抬头"><el-input v-model="form.title" /></el-form-item>
|
||||
<el-form-item label="税号" v-if="form.type === 'enterprise'"><el-input v-model="form.tax_id" /></el-form-item>
|
||||
<el-form-item label="金额"><el-input-number v-model="form.amount" :min="1" style="width:200px" /></el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="apply">提交申请</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="开票记录" name="history">
|
||||
<el-card shadow="never">
|
||||
<el-table :data="list" v-loading="loadingList" stripe style="width:100%">
|
||||
<el-table-column label="类型" width="100">
|
||||
<template #default="{ row }">{{ row.type === 'individual' ? '个人' : '企业' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="抬头" min-width="140" />
|
||||
<el-table-column label="金额" width="120">
|
||||
<template #default="{ row }">{{ row.amount }} 元</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="申请时间" width="170" />
|
||||
<el-table-column v-if="rejectedReason" label="驳回原因" min-width="140">
|
||||
<template #default="{ row }">{{ row.reason || '-' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loadingList && !list.length" description="暂无开票记录" />
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { applyInvoice, listInvoices } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const tab = ref('apply')
|
||||
const loading = ref(false)
|
||||
const loadingList = ref(false)
|
||||
const list = ref([])
|
||||
const form = ref({ type: 'individual', title: '', tax_id: '', amount: 0 })
|
||||
|
||||
function statusType(s) { return { pending: 'warning', issued: 'success', rejected: 'danger' }[s] || 'info' }
|
||||
function statusLabel(s) { return { pending: '处理中', issued: '已开票', rejected: '已驳回' }[s] || s }
|
||||
|
||||
onMounted(loadList)
|
||||
|
||||
async function apply() {
|
||||
if (!form.value.title || !form.value.amount) { ElMessage.warning('请填写完整'); return }
|
||||
loading.value = true
|
||||
try {
|
||||
await applyInvoice(form.value)
|
||||
ElMessage.success('申请已提交')
|
||||
form.value = { type: 'individual', title: '', tax_id: '', amount: 0 }
|
||||
loadList()
|
||||
} catch (e) { ElMessage.error(e?.detail || '提交失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
loadingList.value = true
|
||||
try {
|
||||
const res = await listInvoices({ page: 1, size: 50 })
|
||||
const d = res.data || res
|
||||
list.value = d.items || d.rows || d.data || d || []
|
||||
} catch { /* ignore */ }
|
||||
finally { loadingList.value = false }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<div class="login-card">
|
||||
<h2 class="login-title">TradeMate 工作台</h2>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" size="large" @keyup.enter="submit">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="form.username" placeholder="用户名" prefix-icon="User" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="密码" prefix-icon="Lock" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" style="width:100%" @click="submit">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p v-if="error" class="login-error">{{ error }}</p>
|
||||
<p class="login-hint">已在主站登录?请先退出后重新登录</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const formRef = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const form = reactive({ username: '', password: '' })
|
||||
const rules = { username: [{ required: true, message: '请输入用户名' }], password: [{ required: true, message: '请输入密码' }] }
|
||||
|
||||
async function submit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
await auth.login(form)
|
||||
await auth.fetchUser()
|
||||
const redirect = route.query.redirect || '/'
|
||||
router.push(redirect)
|
||||
} catch (e) {
|
||||
error.value = e?.detail || '登录失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-page { height: 100vh; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||||
.login-card { width: 400px; padding: 40px; background: #fff; border-radius: 12px; box-shadow: 0 8px 40px rgba(0,0,0,0.15); }
|
||||
.login-title { text-align: center; margin-bottom: 30px; font-size: 22px; color: #333; }
|
||||
.login-error { color: #f56c6c; text-align: center; font-size: 13px; margin-top: -10px; }
|
||||
.login-hint { text-align: center; margin-top: 16px; font-size: 12px; color: #999; }
|
||||
</style>
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<template #header><span>AI 营销素材生成</span></template>
|
||||
<el-form :model="form" label-width="100">
|
||||
<el-form-item label="产品名称"><el-input v-model="form.product_name" placeholder="你的产品名称" /></el-form-item>
|
||||
<el-form-item label="产品描述"><el-input v-model="form.product_desc" type="textarea" :rows="3" placeholder="产品特点、优势等" /></el-form-item>
|
||||
<el-form-item label="目标市场"><el-input v-model="form.target_market" placeholder="如:北美、欧洲、东南亚" /></el-form-item>
|
||||
<el-form-item label="写作风格">
|
||||
<el-select v-model="form.style" style="width:160px">
|
||||
<el-option label="专业" value="professional" />
|
||||
<el-option label="友好" value="friendly" />
|
||||
<el-option label="说服型" value="persuasive" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="生成类型">
|
||||
<el-select v-model="form.type" style="width:160px">
|
||||
<el-option label="开发信" value="cold_email" />
|
||||
<el-option label="WhatsApp话术" value="whatsapp" />
|
||||
<el-option label="产品描述" value="product_desc" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="generate">生成</el-button>
|
||||
<el-button @click="showKeywords = !showKeywords">关键词建议</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div v-if="result" style="padding:16px;background:#f5f5f5;border-radius:6px;margin-top:12px">
|
||||
<p style="white-space:pre-wrap">{{ result }}</p>
|
||||
<div style="margin-top:8px;display:flex;gap:8px">
|
||||
<el-button text type="primary" size="small" @click="copyText(result)">复制</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="showKeywords" shadow="never" style="margin-top:16px">
|
||||
<template #header><span>关键词建议</span></template>
|
||||
<el-input v-model="kwInput" placeholder="输入产品关键词描述" style="margin-bottom:12px" />
|
||||
<el-button type="primary" :loading="kwLoading" @click="fetchKeywords">生成</el-button>
|
||||
<div v-if="keywords.length" style="margin-top:12px">
|
||||
<el-tag v-for="k in keywords" :key="k" style="margin:4px">{{ k }}</el-tag>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { generateMarketing, getKeywords } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const form = ref({ product_name: '', product_desc: '', target_market: '', style: 'professional', type: 'cold_email' })
|
||||
const loading = ref(false)
|
||||
const result = ref('')
|
||||
const showKeywords = ref(false)
|
||||
const kwInput = ref('')
|
||||
const kwLoading = ref(false)
|
||||
const keywords = ref([])
|
||||
|
||||
async function generate() {
|
||||
if (!form.value.product_name) { ElMessage.warning('请输入产品名称'); return }
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await generateMarketing({ product_info: { name: form.value.product_name, description: form.value.product_desc }, target_market: form.value.target_market, style: form.value.style, type: form.value.type })
|
||||
result.value = res.data?.content || res.content || res.data?.text || res.text || ''
|
||||
} catch { ElMessage.error('生成失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function fetchKeywords() {
|
||||
if (!kwInput.value.trim()) return
|
||||
kwLoading.value = true
|
||||
try {
|
||||
const res = await getKeywords({ text: kwInput.value })
|
||||
keywords.value = res.data?.keywords || res.keywords || []
|
||||
} catch { ElMessage.error('生成失败') }
|
||||
finally { kwLoading.value = false }
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
navigator.clipboard.writeText(text).then(() => ElMessage.success('已复制'))
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<span>通知中心</span>
|
||||
<el-button v-if="list.length" size="small" @click="markAll">全部已读</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div v-for="n in list" :key="n.id" class="notif-item" :class="{ unread: !n.is_read }" @click="markRead(n)">
|
||||
<div style="display:flex;justify-content:space-between">
|
||||
<strong>{{ n.title }}</strong>
|
||||
<el-tag v-if="!n.is_read" size="small" type="danger">新</el-tag>
|
||||
</div>
|
||||
<p style="margin:4px 0;font-size:13px;color:#666">{{ n.content }}</p>
|
||||
<span style="font-size:11px;color:#999">{{ n.created_at }}</span>
|
||||
</div>
|
||||
<el-empty v-if="!list.length" description="暂无通知" :image-size="60" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { listNotifications, markNotificationRead, markAllRead } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const list = ref([])
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await listNotifications({ page: 1, size: 50 })
|
||||
const d = res.data || res
|
||||
list.value = d.items || d.rows || d.data || d || []
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function markRead(n) {
|
||||
if (n.is_read) return
|
||||
try {
|
||||
await markNotificationRead(n.id)
|
||||
n.is_read = true
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function markAll() {
|
||||
try {
|
||||
await markAllRead()
|
||||
list.value.forEach(n => n.is_read = true)
|
||||
ElMessage.success('已全部标记为已读')
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.notif-item { padding: 14px 16px; border-bottom: 1px solid #f0f0f0; cursor: pointer; transition: background 0.2s; }
|
||||
.notif-item:hover { background: #fafafa; }
|
||||
.notif-item.unread { background: #f0f5ff; }
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div style="display:flex;gap:12px;margin-bottom:16px">
|
||||
<el-input v-model="searchQuery" placeholder="搜索产品名称..." style="width:260px" clearable @clear="load" @keyup.enter="load" />
|
||||
<el-button type="primary" @click="showForm = true">新增产品</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" stripe style="width:100%">
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column prop="name_en" label="英文名" min-width="160" />
|
||||
<el-table-column prop="category" label="类别" width="120" />
|
||||
<el-table-column prop="price" label="价格" width="100">
|
||||
<template #default="{ row }">{{ row.price }} {{ row.currency || 'USD' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="moq" label="MOQ" width="80" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="editRow(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除?" @confirm="deleteRow(row.id)">
|
||||
<template #reference>
|
||||
<el-button type="danger" link size="small">删除</el-button>
|
||||
</template>
|
||||
</el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && !list.length" description="暂无产品" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showForm" :title="editing ? '编辑产品' : '新增产品'" width="520">
|
||||
<el-form :model="form" label-width="80">
|
||||
<el-form-item label="名称"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="英文名"><el-input v-model="form.name_en" /></el-form-item>
|
||||
<el-form-item label="类别"><el-input v-model="form.category" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="form.description" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="价格">
|
||||
<el-input-number v-model="form.price" :min="0" style="width:200px" />
|
||||
<el-select v-model="form.currency" style="width:100px;margin-left:8px">
|
||||
<el-option label="USD" value="USD" />
|
||||
<el-option label="CNY" value="CNY" />
|
||||
<el-option label="EUR" value="EUR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="MOQ"><el-input-number v-model="form.moq" :min="1" /></el-form-item>
|
||||
<el-form-item label="关键词"><el-input v-model="form.keywords" placeholder="逗号分隔" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showForm = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { listProducts, createProduct, updateProduct, deleteProduct } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const searchQuery = ref('')
|
||||
const showForm = ref(false)
|
||||
const editing = ref(false)
|
||||
const saving = ref(false)
|
||||
const form = reactive({ name: '', name_en: '', category: '', description: '', price: 0, currency: 'USD', moq: 1, keywords: '' })
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {}
|
||||
if (searchQuery.value) params.query = searchQuery.value
|
||||
const res = await listProducts(params)
|
||||
const d = res.data || res
|
||||
list.value = d.items || d.rows || d.data || d || []
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = { ...form }
|
||||
if (editing.value) {
|
||||
await updateProduct(editing.value, payload)
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await createProduct(payload)
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
showForm.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.detail || '操作失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
function editRow(row) {
|
||||
editing.value = row.id
|
||||
Object.assign(form, { name: row.name, name_en: row.name_en || '', category: row.category || '', description: row.description || '', price: row.price || 0, currency: row.currency || 'USD', moq: row.moq || 1, keywords: row.keywords || '' })
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
async function deleteRow(id) {
|
||||
try {
|
||||
await deleteProduct(id)
|
||||
ElMessage.success('已删除')
|
||||
load()
|
||||
} catch { ElMessage.error('删除失败') }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8">
|
||||
<el-card shadow="never">
|
||||
<div style="text-align:center;padding:20px 0">
|
||||
<el-avatar :size="72" style="background:#409eff;font-size:28px">{{ (user?.username || 'U')[0].toUpperCase() }}</el-avatar>
|
||||
<h3 style="margin:12px 0 4px">{{ user?.username || '用户' }}</h3>
|
||||
<el-tag size="small" :type="tierType(user?.tier)">{{ user?.tier || 'free' }}</el-tag>
|
||||
<el-tag v-if="user?.role === 'admin'" type="danger" size="small" style="margin-left:4px">管理员</el-tag>
|
||||
</div>
|
||||
<el-divider style="margin:8px 0" />
|
||||
<div class="profile-menu">
|
||||
<div class="menu-item" @click="$router.push('/upgrade')">
|
||||
<el-icon><Crown /></el-icon><span>升级会员</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/certification')">
|
||||
<el-icon><Stamp /></el-icon><span>实名认证</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/invoice')">
|
||||
<el-icon><List /></el-icon><span>发票管理</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/notifications')">
|
||||
<el-icon><Bell /></el-icon><span>通知中心</span>
|
||||
</div>
|
||||
<div class="menu-item" @click="$router.push('/feedback')">
|
||||
<el-icon><ChatDotSquare /></el-icon><span>意见反馈</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>编辑资料</span></template>
|
||||
<el-form :model="form" label-width="80" size="large">
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机">
|
||||
<el-input v-model="form.phone" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="saving" @click="saveProfile">保存</el-button>
|
||||
<el-button @click="showPassword = true">修改密码</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog v-model="showPassword" title="修改密码" width="400">
|
||||
<el-form :model="pwForm" label-width="80">
|
||||
<el-form-item label="旧密码"><el-input v-model="pwForm.old_password" type="password" /></el-form-item>
|
||||
<el-form-item label="新密码"><el-input v-model="pwForm.new_password" type="password" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showPassword = false">取消</el-button>
|
||||
<el-button type="primary" :loading="pwLoading" @click="changePw">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { updateProfile, changePassword } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const auth = useAuthStore()
|
||||
const user = auth.user
|
||||
const saving = ref(false)
|
||||
const showPassword = ref(false)
|
||||
const pwLoading = ref(false)
|
||||
const form = reactive({ username: '', email: '', phone: '' })
|
||||
const pwForm = reactive({ old_password: '', new_password: '' })
|
||||
|
||||
function tierType(t) { return { free: 'warning', pro: 'primary', enterprise: 'success' }[t] || 'info' }
|
||||
|
||||
onMounted(() => {
|
||||
if (user.value) {
|
||||
form.username = user.value.username || ''
|
||||
form.email = user.value.email || ''
|
||||
form.phone = user.value.phone || ''
|
||||
}
|
||||
})
|
||||
|
||||
async function saveProfile() {
|
||||
saving.value = true
|
||||
try {
|
||||
await updateProfile({ email: form.email })
|
||||
ElMessage.success('已保存')
|
||||
auth.fetchUser()
|
||||
} catch (e) { ElMessage.error(e?.detail || '保存失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
async function changePw() {
|
||||
if (!pwForm.old_password || !pwForm.new_password) { ElMessage.warning('请填写完整'); return }
|
||||
pwLoading.value = true
|
||||
try {
|
||||
await changePassword(pwForm)
|
||||
ElMessage.success('密码已修改')
|
||||
showPassword.value = false
|
||||
pwForm.old_password = ''
|
||||
pwForm.new_password = ''
|
||||
} catch (e) { ElMessage.error(e?.detail || '修改失败') }
|
||||
finally { pwLoading.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-menu { padding: 0; }
|
||||
.menu-item { display: flex; align-items: center; gap: 10px; padding: 12px 16px; cursor: pointer; border-radius: 6px; transition: background 0.2s; }
|
||||
.menu-item:hover { background: #f0f5ff; color: #409eff; }
|
||||
</style>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div style="display:flex;gap:12px;margin-bottom:16px;flex-wrap:wrap">
|
||||
<el-select v-model="statusFilter" placeholder="状态" clearable style="width:140px" @change="load">
|
||||
<el-option label="草稿" value="draft" />
|
||||
<el-option label="已发送" value="sent" />
|
||||
<el-option label="已接受" value="accepted" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="showForm = true">新建报价</el-button>
|
||||
<el-button @click="showInquiry = true">AI 智能报价</el-button>
|
||||
</div>
|
||||
<el-table :data="list" v-loading="loading" stripe style="width:100%">
|
||||
<el-table-column prop="title" label="标题" min-width="160" />
|
||||
<el-table-column label="客户" width="140">
|
||||
<template #default="{ row }">{{ row.customer_name || row.customer?.name || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="金额" width="120">
|
||||
<template #default="{ row }">{{ row.total_amount }} {{ row.currency || 'USD' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.status === 'draft'" type="primary" link size="small" @click="markSent(row)">标记已发</el-button>
|
||||
<el-button type="primary" link size="small" @click="editRow(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!loading && !list.length" description="暂无报价单" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showForm" :title="editing ? '编辑报价' : '新建报价'" width="600">
|
||||
<el-form :model="form" label-width="80">
|
||||
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>
|
||||
<el-form-item label="客户">
|
||||
<el-select v-model="form.customer_id" filterable style="width:100%">
|
||||
<el-option v-for="c in customerOptions" :key="c.id" :label="c.name" :value="c.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="金额"><el-input-number v-model="form.total_amount" :min="0" style="width:200px" /></el-form-item>
|
||||
<el-form-item label="币种">
|
||||
<el-select v-model="form.currency" style="width:120px">
|
||||
<el-option label="USD" value="USD" />
|
||||
<el-option label="CNY" value="CNY" />
|
||||
<el-option label="EUR" value="EUR" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注"><el-input v-model="form.notes" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showForm = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showInquiry" title="AI 智能报价" width="500">
|
||||
<el-input v-model="inquiryText" type="textarea" :rows="5" placeholder="输入客户询盘内容..." />
|
||||
<div style="margin-top:12px">
|
||||
<el-button type="primary" :loading="genLoading" @click="generateFromInquiry">生成报价</el-button>
|
||||
</div>
|
||||
<div v-if="genResult" style="margin-top:12px;padding:12px;background:#f5f5f5;border-radius:6px;white-space:pre-wrap">{{ genResult }}</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { listQuotations, createQuotation, updateQuotationStatus, generateQuoteFromInquiry, listCustomers } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const statusFilter = ref('')
|
||||
const showForm = ref(false)
|
||||
const showInquiry = ref(false)
|
||||
const editing = ref(false)
|
||||
const saving = ref(false)
|
||||
const customerOptions = ref([])
|
||||
const inquiryText = ref('')
|
||||
const genLoading = ref(false)
|
||||
const genResult = ref('')
|
||||
const form = reactive({ title: '', customer_id: null, total_amount: 0, currency: 'USD', notes: '' })
|
||||
|
||||
function statusType(s) { return { draft: 'info', sent: 'warning', accepted: 'success' }[s] || 'info' }
|
||||
function statusLabel(s) { return { draft: '草稿', sent: '已发送', accepted: '已接受' }[s] || s }
|
||||
|
||||
onMounted(() => { load(); loadCustomers() })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = { page: 1, size: 50 }
|
||||
if (statusFilter.value) params.status = statusFilter.value
|
||||
const res = await listQuotations(params)
|
||||
const d = res.data || res
|
||||
list.value = d.items || d.rows || d.data || d || []
|
||||
} catch { /* ignore */ }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadCustomers() {
|
||||
try {
|
||||
const res = await listCustomers({ page: 1, size: 200 })
|
||||
const d = res.data || res
|
||||
customerOptions.value = d.items || d.rows || d || []
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await createQuotation(form)
|
||||
ElMessage.success('已更新')
|
||||
} else {
|
||||
await createQuotation(form)
|
||||
ElMessage.success('已创建')
|
||||
}
|
||||
showForm.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.detail || '操作失败') }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
async function markSent(row) {
|
||||
try {
|
||||
await updateQuotationStatus(row.id, 'sent')
|
||||
ElMessage.success('已标记为已发送')
|
||||
load()
|
||||
} catch { ElMessage.error('操作失败') }
|
||||
}
|
||||
|
||||
async function generateFromInquiry() {
|
||||
if (!inquiryText.value.trim()) return
|
||||
genLoading.value = true
|
||||
try {
|
||||
const res = await generateQuoteFromInquiry({ text: inquiryText.value })
|
||||
genResult.value = res.data?.quotation || res.quotation || JSON.stringify(res.data || res, null, 2)
|
||||
} catch { ElMessage.error('生成失败') }
|
||||
finally { genLoading.value = false }
|
||||
}
|
||||
|
||||
function editRow(row) {
|
||||
editing.value = row.id
|
||||
Object.assign(form, { title: row.title, customer_id: row.customer_id || (row.customer?.id || null), total_amount: row.total_amount || 0, currency: row.currency || 'USD', notes: row.notes || '' })
|
||||
showForm.value = true
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<div style="display:flex;gap:12px;margin-bottom:16px">
|
||||
<el-button type="primary" @click="showCreate = true">创建团队</el-button>
|
||||
</div>
|
||||
<div v-for="t in teams" :key="t.id" class="team-card">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<div>
|
||||
<h4 style="margin:0">{{ t.name }}</h4>
|
||||
<p v-if="t.description" style="color:#999;font-size:13px;margin:4px 0">{{ t.description }}</p>
|
||||
<p style="font-size:12px;color:#666">成员 {{ t.members?.length || 0 }} 人</p>
|
||||
</div>
|
||||
<el-button v-if="t.role === 'owner' || t.role === 'admin'" size="small" @click="showInvite(t)">邀请成员</el-button>
|
||||
</div>
|
||||
<div v-if="t.members?.length" style="margin-top:12px;display:flex;gap:12px;flex-wrap:wrap">
|
||||
<el-tag v-for="m in t.members" :key="m.id" :type="m.role === 'owner' ? 'danger' : m.role === 'admin' ? 'warning' : 'info'" size="small">
|
||||
{{ m.username || m.user?.username }}({{ m.role }})
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-if="!teams.length" description="暂无团队" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="showCreate" title="创建团队" width="400">
|
||||
<el-form :model="createForm" label-width="80">
|
||||
<el-form-item label="名称"><el-input v-model="createForm.name" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="createForm.description" type="textarea" :rows="2" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreate = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="createTeam">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showInviteDialog" title="邀请成员" width="400">
|
||||
<p>团队:{{ inviteTeam?.name }}</p>
|
||||
<el-input v-model="inviteUserId" placeholder="输入用户ID" style="margin-top:12px" />
|
||||
<template #footer>
|
||||
<el-button @click="showInviteDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="inviting" @click="doInvite">邀请</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import { listTeams, createTeam as createTeamApi, inviteTeamMember } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const teams = ref([])
|
||||
const showCreate = ref(false)
|
||||
const creating = ref(false)
|
||||
const createForm = reactive({ name: '', description: '' })
|
||||
const showInviteDialog = ref(false)
|
||||
const inviteTeam = ref(null)
|
||||
const inviteUserId = ref('')
|
||||
const inviting = ref(false)
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await listTeams()
|
||||
const d = res.data || res
|
||||
teams.value = d.items || d.rows || d.data || d || []
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function createTeam() {
|
||||
if (!createForm.name) { ElMessage.warning('请输入团队名称'); return }
|
||||
creating.value = true
|
||||
try {
|
||||
await createTeamApi(createForm)
|
||||
ElMessage.success('已创建')
|
||||
showCreate.value = false
|
||||
createForm.name = ''
|
||||
createForm.description = ''
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.detail || '创建失败') }
|
||||
finally { creating.value = false }
|
||||
}
|
||||
|
||||
function showInvite(t) {
|
||||
inviteTeam.value = t
|
||||
inviteUserId.value = ''
|
||||
showInviteDialog.value = true
|
||||
}
|
||||
|
||||
async function doInvite() {
|
||||
if (!inviteUserId.value) { ElMessage.warning('请输入用户ID'); return }
|
||||
inviting.value = true
|
||||
try {
|
||||
await inviteTeamMember(inviteTeam.value.id, inviteUserId.value)
|
||||
ElMessage.success('已邀请')
|
||||
showInviteDialog.value = false
|
||||
load()
|
||||
} catch (e) { ElMessage.error(e?.detail || '邀请失败') }
|
||||
finally { inviting.value = false }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.team-card { background: #fff; border: 1px solid #e8e8e8; border-radius: 8px; padding: 20px; margin-bottom: 12px; }
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card shadow="never">
|
||||
<template #header><span>文本翻译</span></template>
|
||||
<el-input v-model="form.text" type="textarea" :rows="5" placeholder="输入需要翻译的外贸文本..." />
|
||||
<div style="margin:16px 0;display:flex;gap:12px;align-items:center">
|
||||
<el-select v-model="form.target_lang" style="width:160px">
|
||||
<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="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">
|
||||
<el-button text type="primary" size="small" @click="copyText(result)">复制</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="showReply" shadow="never" style="margin-top:16px">
|
||||
<template #header><span>回复建议</span></template>
|
||||
<el-input v-model="replyInquiry" type="textarea" :rows="3" placeholder="输入客户询盘内容..." />
|
||||
<div style="margin-top:12px;display:flex;gap:8px">
|
||||
<el-button type="primary" :loading="replyLoading" @click="getReply">生成建议</el-button>
|
||||
</div>
|
||||
<div v-if="suggestions.length" style="margin-top:12px">
|
||||
<el-card v-for="(s, i) in suggestions" :key="i" shadow="hover" style="margin-bottom:8px">
|
||||
<template #header>
|
||||
<span style="font-weight:500">{{ s.tone || s.style || '建议 ' + (i+1) }}</span>
|
||||
</template>
|
||||
<p style="white-space:pre-wrap">{{ s.content || s.text }}</p>
|
||||
<el-button text type="primary" size="small" style="margin-top:8px" @click="copyText(s.content || s.text)">复制</el-button>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card v-if="showExtract" shadow="never" style="margin-top:16px">
|
||||
<template #header><span>信息提取</span></template>
|
||||
<el-input v-model="extractText" type="textarea" :rows="3" placeholder="输入要提取信息的文本..." />
|
||||
<div style="margin-top:12px">
|
||||
<el-button type="primary" :loading="extractLoading" @click="doExtract">提取</el-button>
|
||||
</div>
|
||||
<pre v-if="extractResult" style="margin-top:12px;padding:12px;background:#f5f5f5;border-radius:6px;white-space:pre-wrap">{{ extractResult }}</pre>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { translate, translateReply, extractInfo } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const form = ref({ text: '', target_lang: 'en' })
|
||||
const loading = ref(false)
|
||||
const result = ref('')
|
||||
const showReply = ref(false)
|
||||
const showExtract = ref(false)
|
||||
const replyInquiry = ref('')
|
||||
const replyLoading = ref(false)
|
||||
const suggestions = ref([])
|
||||
const extractText = ref('')
|
||||
const extractLoading = ref(false)
|
||||
const extractResult = ref('')
|
||||
|
||||
async function doTranslate() {
|
||||
if (!form.value.text.trim()) return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await translate(form.value)
|
||||
result.value = res.data?.translated_text || res.translated_text || ''
|
||||
} catch { ElMessage.error('翻译失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function getReply() {
|
||||
if (!replyInquiry.value.trim()) return
|
||||
replyLoading.value = true
|
||||
try {
|
||||
const res = await translateReply({ text: replyInquiry.value })
|
||||
suggestions.value = res.data?.suggestions || res.suggestions || []
|
||||
} catch { ElMessage.error('生成建议失败') }
|
||||
finally { replyLoading.value = false }
|
||||
}
|
||||
|
||||
async function doExtract() {
|
||||
if (!extractText.value.trim()) return
|
||||
extractLoading.value = true
|
||||
try {
|
||||
const res = await extractInfo({ text: extractText.value })
|
||||
extractResult.value = JSON.stringify(res.data || res, null, 2)
|
||||
} catch { ElMessage.error('提取失败') }
|
||||
finally { extractLoading.value = false }
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
navigator.clipboard.writeText(text).then(() => ElMessage.success('已复制'))
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,64 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="8" v-for="p in plans" :key="p.id">
|
||||
<el-card shadow="hover" :class="{ 'plan-highlight': p.id === currentPlan }">
|
||||
<template #header>
|
||||
<div style="text-align:center">
|
||||
<h3 style="margin:0">{{ p.name }}</h3>
|
||||
<p style="font-size:28px;font-weight:700;color:#409eff;margin:12px 0">
|
||||
¥{{ p.price || 0 }}<span style="font-size:14px;font-weight:400;color:#999">/月</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<p v-for="f in p.features || []" :key="f" style="font-size:13px;color:#666;margin:8px 0">
|
||||
<el-icon color="#67c23a" style="margin-right:6px"><Check /></el-icon>{{ f }}
|
||||
</p>
|
||||
</div>
|
||||
<div style="text-align:center;margin-top:16px">
|
||||
<el-button v-if="p.id === currentPlan" type="default" disabled>当前套餐</el-button>
|
||||
<el-button v-else type="primary" :loading="loadingId === p.id" @click="upgrade(p.id)">升级</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-if="!plans.length" description="暂无套餐信息" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getPlans, getSubscription, createOrder } from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const plans = ref([])
|
||||
const currentPlan = ref(null)
|
||||
const loadingId = ref(null)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [plansRes, subRes] = await Promise.all([getPlans(), getSubscription().catch(() => null)])
|
||||
const pd = plansRes.data || plansRes
|
||||
plans.value = pd.plans || pd.items || pd || []
|
||||
if (subRes) {
|
||||
const sd = subRes.data || subRes
|
||||
currentPlan.value = sd.plan_id || sd.plan
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
})
|
||||
|
||||
async function upgrade(planId) {
|
||||
loadingId.value = planId
|
||||
try {
|
||||
const res = await createOrder(planId)
|
||||
ElMessage.success('订单已创建' + (res.pay_url ? ',正在跳转支付...' : ''))
|
||||
if (res.pay_url) window.open(res.pay_url)
|
||||
} catch (e) { ElMessage.error(e?.detail || '升级失败') }
|
||||
finally { loadingId.value = null }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plan-highlight { border: 2px solid #409eff; transform: scale(1.02); }
|
||||
</style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="item in stats" :key="item.label">
|
||||
<el-card shadow="hover" class="stat-card">
|
||||
<div class="stat-value">{{ item.value }}</div>
|
||||
<div class="stat-label">{{ item.label }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top:20px">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>快速翻译</span></template>
|
||||
<el-input v-model="quickText" type="textarea" :rows="3" placeholder="输入要翻译的文本..." />
|
||||
<div style="margin-top:12px;display:flex;gap:8px">
|
||||
<el-select v-model="quickLang" style="width:140px">
|
||||
<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" style="margin-top:12px;padding:12px;background:#f5f5f5;border-radius:6px">{{ quickResult }}</p>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never">
|
||||
<template #header><span>跟进提醒</span></template>
|
||||
<div v-if="followups.length">
|
||||
<div v-for="f in followups" :key="f.id" class="followup-item">
|
||||
<span class="followup-name">{{ f.customer_name }}</span>
|
||||
<span class="followup-days">{{ f.silent_days }}天未联系</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无跟进提醒" :image-size="60" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" style="margin-top:20px">
|
||||
<template #header><span>功能入口</span></template>
|
||||
<div class="feature-grid">
|
||||
<div v-for="f in features" :key="f.title" class="feature-item" @click="$router.push(f.route)">
|
||||
<el-icon :size="24" :color="f.color"><component :is="f.icon" /></el-icon>
|
||||
<span>{{ f.title }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { getAnalyticsOverview, translate, getFollowupPending } from '@/api'
|
||||
|
||||
const stats = ref([])
|
||||
const quickText = ref('')
|
||||
const quickLang = ref('en')
|
||||
const quickResult = ref('')
|
||||
const translating = ref(false)
|
||||
const followups = ref([])
|
||||
|
||||
const features = [
|
||||
{ title: '智能翻译', icon: 'ChatLineSquare', color: '#409eff', route: '/translate' },
|
||||
{ title: '客户管理', icon: 'User', color: '#67c23a', route: '/customers' },
|
||||
{ title: '产品库', icon: 'Goods', color: '#e6a23c', route: '/products' },
|
||||
{ title: '报价单', icon: 'DocumentCopy', color: '#f56c6c', route: '/quotations' },
|
||||
{ title: '营销素材', icon: 'Promotion', color: '#909399', route: '/marketing' },
|
||||
{ title: '挖掘新客', icon: 'Search', color: '#409eff', route: '/discovery' },
|
||||
{ title: '智能跟进', icon: 'Message', color: '#67c23a', route: '/followup' },
|
||||
{ title: '数据分析', icon: 'DataAnalysis', color: '#e6a23c', route: '/analytics' },
|
||||
{ title: '团队协作', icon: 'UserFilled', color: '#f56c6c', route: '/team' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [overview, fup] = await Promise.all([
|
||||
getAnalyticsOverview().catch(() => null),
|
||||
getFollowupPending().catch(() => [])
|
||||
])
|
||||
const d = overview?.data || overview || {}
|
||||
stats.value = [
|
||||
{ value: d.customers?.total || d.total_customers || 0, label: '客户总数' },
|
||||
{ value: d.translations?.today || d.today_translations || 0, label: '今日翻译' },
|
||||
{ value: d.quotations?.total || d.total_quotations || 0, label: '报价单数' },
|
||||
{ value: fup?.length || 0, label: '待跟进' },
|
||||
]
|
||||
followups.value = Array.isArray(fup) ? fup.slice(0, 5) : []
|
||||
} 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>
|
||||
.stat-card { cursor: default; text-align: center; }
|
||||
.stat-value { font-size: 32px; font-weight: 700; color: #409eff; }
|
||||
.stat-label { font-size: 14px; color: #999; margin-top: 4px; }
|
||||
.followup-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.followup-name { font-weight: 500; }
|
||||
.followup-days { color: #f56c6c; font-size: 12px; }
|
||||
.feature-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 16px; }
|
||||
.feature-item { display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 20px 12px; cursor: pointer; border-radius: 8px; transition: background 0.2s; }
|
||||
.feature-item:hover { background: #f0f5ff; }
|
||||
.feature-item span { font-size: 13px; color: #333; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user