Add admin-frontend and user-frontend standalone projects, certification/invoice/discovery features, fix auth header and theme consistency
This commit is contained in:
+5
-199
@@ -1,207 +1,13 @@
|
||||
<template>
|
||||
<div class="app-wrapper">
|
||||
<div class="app-nav" id="appNav">
|
||||
<div class="app-nav-brand">TradeMate</div>
|
||||
<div
|
||||
v-for="(item, index) in navList"
|
||||
:key="index"
|
||||
class="app-nav-item"
|
||||
:class="{ active: currentIndex === index }"
|
||||
@click="switchTab(index)"
|
||||
>
|
||||
<span class="app-nav-icon">{{ item.icon }}</span>
|
||||
<span class="app-nav-text">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<router-view />
|
||||
</div>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
const currentIndex = ref(0)
|
||||
|
||||
const navList = [
|
||||
{ pagePath: '/pages/index/index', text: '首页', icon: '🏠' },
|
||||
{ pagePath: '/pages/customers/customers', text: '客户', icon: '👥' },
|
||||
{ pagePath: '/pages/marketing/marketing', text: '营销', icon: '📢' },
|
||||
{ pagePath: '/pages/quotation/quotation', text: '报价', icon: '📄' },
|
||||
{ pagePath: '/pages/profile/profile', text: '我的', icon: '👤' },
|
||||
]
|
||||
|
||||
const updateCurrentIndex = () => {
|
||||
const hash = window.location.hash || ''
|
||||
for (let i = 0; i < navList.length; i++) {
|
||||
if (hash.includes(navList[i].pagePath)) {
|
||||
currentIndex.value = i
|
||||
return
|
||||
}
|
||||
}
|
||||
currentIndex.value = 0
|
||||
}
|
||||
|
||||
const switchTab = (index) => {
|
||||
if (currentIndex.value === index) return
|
||||
uni.switchTab({ url: navList[index].pagePath })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateCurrentIndex()
|
||||
window.addEventListener('hashchange', updateCurrentIndex)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('hashchange', updateCurrentIndex)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Global reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Let uni-app pages scroll */
|
||||
uni-page {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
uni-page-body {
|
||||
overflow-y: auto !important;
|
||||
min-height: 100% !important;
|
||||
}
|
||||
|
||||
/* ===== Nav: hidden on mobile (uni-app default tab bar is used) ===== */
|
||||
.app-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ===== Desktop responsive (≥1024px) ===== */
|
||||
@media (min-width: 1024px) {
|
||||
/* Sidebar nav */
|
||||
.app-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 220px;
|
||||
height: 100vh;
|
||||
background: #fff;
|
||||
box-shadow: 4px 0 16px rgba(0,0,0,0.08);
|
||||
z-index: 999999;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
.app-nav-brand {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1890ff;
|
||||
padding: 20px 24px 32px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-nav-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 14px 24px;
|
||||
margin: 2px 12px;
|
||||
border-radius: 12px;
|
||||
gap: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.app-nav-item:hover {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
.app-nav-item.active {
|
||||
background: #e6f0ff;
|
||||
}
|
||||
|
||||
.app-nav-icon {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
|
||||
}
|
||||
|
||||
.app-nav-text {
|
||||
font-size: 15px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.app-nav-item.active .app-nav-text {
|
||||
color: #1890ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Hide the built-in uni-app tab bar on desktop */
|
||||
uni-tabbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Sidebar — shift page content right */
|
||||
uni-page-body {
|
||||
margin-left: 220px !important;
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* Remove the tab-bar-height bottom padding that uni-app adds for tab pages */
|
||||
.uni-app--showtabbar uni-page-wrapper:after {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Constrain + center page content */
|
||||
uni-page-body > view {
|
||||
max-width: 1200px !important;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
padding: 40px 48px !important;
|
||||
}
|
||||
|
||||
/* Cards more breathing room */
|
||||
uni-page-body .card {
|
||||
padding: 32px !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
/* Buttons: reasonable desktop sizing */
|
||||
uni-page-body button,
|
||||
uni-page-body .btn-primary,
|
||||
uni-page-body .btn-secondary,
|
||||
uni-page-body .uni-btn,
|
||||
uni-page-body [class*="btn-"] {
|
||||
min-width: 120px;
|
||||
padding: 12px 32px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
uni-page-body input,
|
||||
uni-page-body textarea,
|
||||
uni-page-body .uni-input-input {
|
||||
font-size: 15px !important;
|
||||
padding: 10px 16px !important;
|
||||
}
|
||||
|
||||
/* Fix floating AI assistant (clear sidebar) */
|
||||
.ai-float-btn {
|
||||
right: 40px !important;
|
||||
bottom: 40px !important;
|
||||
}
|
||||
.ai-dialog {
|
||||
right: 40px !important;
|
||||
bottom: 100px !important;
|
||||
}
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body, #app { height: 100%; width: 100%; }
|
||||
uni-page { overflow-y: auto !important; }
|
||||
uni-page-body { overflow-y: auto !important; min-height: 100% !important; }
|
||||
</style>
|
||||
|
||||
@@ -22,8 +22,11 @@ export const PAGES = {
|
||||
FOLLOWUP: '/pages/followup/followup',
|
||||
NOTIFICATION: '/pages/notification/notification',
|
||||
ANALYTICS: '/pages/analytics/analytics',
|
||||
DISCOVERY: '/pages/discovery/discovery',
|
||||
TEAM: '/pages/team/team',
|
||||
ADMIN: '/pages/admin/admin',
|
||||
CERTIFICATION: '/pages/certification/certification',
|
||||
INVOICE: '/pages/invoice/invoice',
|
||||
AGREEMENT_PRIVACY: '/pages/agreement/privacy',
|
||||
AGREEMENT_TERMS: '/pages/agreement/terms',
|
||||
}
|
||||
|
||||
+19
-1
@@ -46,7 +46,7 @@
|
||||
{
|
||||
"path": "pages/admin/admin",
|
||||
"style": {
|
||||
"navigationBarTitleText": "管理后台"
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -102,6 +102,24 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "个人中心"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/discovery/discovery",
|
||||
"style": {
|
||||
"navigationBarTitleText": "挖掘新客"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/certification/certification",
|
||||
"style": {
|
||||
"navigationBarTitleText": "实名认证"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/invoice/invoice",
|
||||
"style": {
|
||||
"navigationBarTitleText": "发票管理"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
<template>
|
||||
<view class="admin-container">
|
||||
<scroll-view class="tabs" scroll-x>
|
||||
<view class="tab" :class="{ active: tab === 'overview' }" @click="tab = 'overview'">概览</view>
|
||||
<view class="tab" :class="{ active: tab === 'users' }" @click="tab = 'users'">用户</view>
|
||||
<view class="tab" :class="{ active: tab === 'stats' }" @click="tab = 'stats'">统计</view>
|
||||
<view class="tab" :class="{ active: tab === 'logs' }" @click="tab = 'logs'">日志</view>
|
||||
<view class="tab" :class="{ active: tab === 'config' }" @click="tab = 'config'">配置</view>
|
||||
<view class="tab" :class="{ active: tab === 'quota' }" @click="tab = 'quota'">翻译配额</view>
|
||||
</scroll-view>
|
||||
<view class="sidebar">
|
||||
<view class="sidebar-header">
|
||||
<text class="sidebar-title">管理后台</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'overview' }" @click="tab = 'overview'">
|
||||
<text class="tab-icon">📊</text><text>概览</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'users' }" @click="tab = 'users'">
|
||||
<text class="tab-icon">👥</text><text>用户</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'stats' }" @click="tab = 'stats'">
|
||||
<text class="tab-icon">📈</text><text>统计</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'logs' }" @click="tab = 'logs'">
|
||||
<text class="tab-icon">📋</text><text>日志</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'config' }" @click="tab = 'config'">
|
||||
<text class="tab-icon">⚙️</text><text>配置</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'quota' }" @click="tab = 'quota'">
|
||||
<text class="tab-icon">📄</text><text>翻译配额</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'cert' }" @click="tab = 'cert'; loadCerts()">
|
||||
<text class="tab-icon">🪪</text><text>认证审核</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: tab === 'invoice' }" @click="tab = 'invoice'; loadAdminInvoices()">
|
||||
<text class="tab-icon">🧾</text><text>发票管理</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="main-content">
|
||||
<scroll-view class="tabs mobile-only" scroll-x>
|
||||
<view class="tab" :class="{ active: tab === 'overview' }" @click="tab = 'overview'">概览</view>
|
||||
<view class="tab" :class="{ active: tab === 'users' }" @click="tab = 'users'">用户</view>
|
||||
<view class="tab" :class="{ active: tab === 'stats' }" @click="tab = 'stats'">统计</view>
|
||||
<view class="tab" :class="{ active: tab === 'logs' }" @click="tab = 'logs'">日志</view>
|
||||
<view class="tab" :class="{ active: tab === 'config' }" @click="tab = 'config'">配置</view>
|
||||
<view class="tab" :class="{ active: tab === 'quota' }" @click="tab = 'quota'">翻译配额</view>
|
||||
<view class="tab" :class="{ active: tab === 'cert' }" @click="tab = 'cert'; loadCerts()">认证审核</view>
|
||||
<view class="tab" :class="{ active: tab === 'invoice' }" @click="tab = 'invoice'; loadAdminInvoices()">发票管理</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 概览 -->
|
||||
<view v-if="tab === 'overview'">
|
||||
@@ -266,6 +298,92 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 认证审核 -->
|
||||
<view v-if="tab === 'cert'">
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">实名认证审核</text>
|
||||
</view>
|
||||
<view class="filter-bar">
|
||||
<text class="filter-btn" :class="{ active: certFilter === '' }" @click="certFilter = ''; loadCerts()">全部</text>
|
||||
<text class="filter-btn" :class="{ active: certFilter === 'pending' }" @click="certFilter = 'pending'; loadCerts()">待审核</text>
|
||||
<text class="filter-btn" :class="{ active: certFilter === 'approved' }" @click="certFilter = 'approved'; loadCerts()">已通过</text>
|
||||
<text class="filter-btn" :class="{ active: certFilter === 'rejected' }" @click="certFilter = 'rejected'; loadCerts()">已驳回</text>
|
||||
</view>
|
||||
<view class="cert-list" v-if="certs.length">
|
||||
<view class="cert-card" v-for="c in certs" :key="c.id">
|
||||
<view class="cert-header">
|
||||
<text class="cert-type">{{ c.cert_type === 'individual' ? '个人认证' : '企业认证' }}</text>
|
||||
<text class="cert-status" :class="c.status">{{ { pending: '待审核', approved: '已通过', rejected: '已驳回' }[c.status] }}</text>
|
||||
</view>
|
||||
<view class="cert-body">
|
||||
<text class="cert-field" v-if="c.personal_name">姓名:{{ c.personal_name }}</text>
|
||||
<text class="cert-field" v-if="c.company_name">企业:{{ c.company_name }}</text>
|
||||
<text class="cert-field" v-if="c.tax_id">税号:{{ c.tax_id }}</text>
|
||||
<text class="cert-field">用户ID:{{ c.user_id?.substring(0, 8) }}...</text>
|
||||
<text class="cert-date">{{ c.created_at?.substring(0, 10) }}</text>
|
||||
</view>
|
||||
<view class="cert-actions" v-if="c.status === 'pending'">
|
||||
<text class="action-btn approve" @click="reviewCert(c.id, 'approve')">通过</text>
|
||||
<text class="action-btn reject" @click="showRejectCert = c.id">驳回</text>
|
||||
</view>
|
||||
<text v-if="c.reject_reason" class="cert-reason">驳回原因:{{ c.reject_reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-else class="empty-text">暂无认证申请</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 发票管理 -->
|
||||
<view v-if="tab === 'invoice'">
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">开票管理</text>
|
||||
</view>
|
||||
<view class="filter-bar">
|
||||
<text class="filter-btn" :class="{ active: invFilter === '' }" @click="invFilter = ''; loadAdminInvoices()">全部</text>
|
||||
<text class="filter-btn" :class="{ active: invFilter === 'pending' }" @click="invFilter = 'pending'; loadAdminInvoices()">待开票</text>
|
||||
<text class="filter-btn" :class="{ active: invFilter === 'issued' }" @click="invFilter = 'issued'; loadAdminInvoices()">已开票</text>
|
||||
<text class="filter-btn" :class="{ active: invFilter === 'rejected' }" @click="invFilter = 'rejected'; loadAdminInvoices()">已驳回</text>
|
||||
</view>
|
||||
<view class="inv-list" v-if="adminInvoices.length">
|
||||
<view class="inv-card" v-for="inv in adminInvoices" :key="inv.id">
|
||||
<view class="inv-header">
|
||||
<text class="inv-title">{{ inv.title }}</text>
|
||||
<text class="inv-amount">¥{{ inv.amount }}</text>
|
||||
</view>
|
||||
<view class="inv-meta">
|
||||
<text class="inv-type">{{ inv.invoice_type === 'individual' ? '个人' : '企业' }}发票</text>
|
||||
<text class="inv-status" :class="inv.status">{{ { pending: '待开票', issued: '已开票', rejected: '已驳回' }[inv.status] }}</text>
|
||||
</view>
|
||||
<view class="inv-actions" v-if="inv.status === 'pending'">
|
||||
<text class="action-btn approve" @click="processInv(inv.id, 'issue')">确认已开</text>
|
||||
<text class="action-btn reject" @click="showRejectInv = inv.id">驳回</text>
|
||||
</view>
|
||||
<text v-if="inv.reject_reason" class="inv-reason">驳回原因:{{ inv.reject_reason }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-else class="empty-text">暂无开票申请</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 驳回弹窗 -->
|
||||
<view class="modal-mask" v-if="showRejectCert || showRejectInv" @click="showRejectCert = null; showRejectInv = null">
|
||||
<view class="modal-content" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">驳回原因</text>
|
||||
<text class="modal-close" @click="showRejectCert = null; showRejectInv = null">✕</text>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<input class="reject-input" v-model="rejectReason" placeholder="输入驳回原因" />
|
||||
<view class="modal-actions">
|
||||
<text class="action-btn cancel" @click="showRejectCert = null; showRejectInv = null">取消</text>
|
||||
<text class="action-btn reject" @click="confirmReject">确认驳回</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户详情弹窗 -->
|
||||
<view class="modal-mask" v-if="userDetail" @click="userDetail = null">
|
||||
<view class="modal-content" @click.stop>
|
||||
@@ -294,6 +412,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<AiAssistant />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -572,19 +691,110 @@ const resetQuota = async (version) => {
|
||||
}
|
||||
}
|
||||
|
||||
const certs = ref([])
|
||||
const certFilter = ref('pending')
|
||||
const showRejectCert = ref(null)
|
||||
const showRejectInv = ref(null)
|
||||
const rejectReason = ref('')
|
||||
const adminInvoices = ref([])
|
||||
const invFilter = ref('pending')
|
||||
|
||||
const loadCerts = async () => {
|
||||
try {
|
||||
const res = await adminApi.listCertifications(1, 50, certFilter.value || undefined)
|
||||
certs.value = res.items || []
|
||||
} catch (e) { certs.value = [] }
|
||||
}
|
||||
|
||||
const reviewCert = async (id, action) => {
|
||||
try {
|
||||
await adminApi.reviewCertification(id, action)
|
||||
uni.showToast({ title: action === 'approve' ? '已通过' : '已驳回', icon: 'success' })
|
||||
loadCerts()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const confirmReject = async () => {
|
||||
const id = showRejectCert.value || showRejectInv.value
|
||||
const type = showRejectCert.value ? 'cert' : 'inv'
|
||||
if (!rejectReason.value) { uni.showToast({ title: '请输入驳回原因', icon: 'none' }); return }
|
||||
showRejectCert.value = null
|
||||
showRejectInv.value = null
|
||||
try {
|
||||
if (type === 'cert') {
|
||||
await adminApi.reviewCertification(id, 'reject', rejectReason.value)
|
||||
} else {
|
||||
await adminApi.processInvoice(id, 'reject', rejectReason.value)
|
||||
}
|
||||
uni.showToast({ title: '已驳回', icon: 'success' })
|
||||
rejectReason.value = ''
|
||||
loadCerts()
|
||||
loadAdminInvoices()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const loadAdminInvoices = async () => {
|
||||
try {
|
||||
const res = await adminApi.listInvoices(1, 50, invFilter.value || undefined)
|
||||
adminInvoices.value = res.items || []
|
||||
} catch (e) { adminInvoices.value = [] }
|
||||
}
|
||||
|
||||
const processInv = async (id, action) => {
|
||||
try {
|
||||
await adminApi.processInvoice(id, action)
|
||||
uni.showToast({ title: action === 'issue' ? '已开票' : '已驳回', icon: 'success' })
|
||||
loadAdminInvoices()
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '操作失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
watch(tab, (val) => {
|
||||
if (val === 'stats') loadUsageStats()
|
||||
else if (val === 'logs') { logPage.value = 1; loadLogs() }
|
||||
else if (val === 'config') loadConfig()
|
||||
else if (val === 'quota') loadQuotas()
|
||||
else if (val === 'cert') loadCerts()
|
||||
else if (val === 'invoice') loadAdminInvoices()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.admin-container { min-height: 100vh; background: #f5f5f5; padding: 20rpx; }
|
||||
.tabs { width: 100%; background: #fff; border-radius: 16rpx; margin-bottom: 30rpx; white-space: nowrap; }
|
||||
.admin-container { min-height: 100vh; background: #f5f5f5; display: flex; }
|
||||
|
||||
.tabs { width: 100%; background: #fff; border-radius: 16rpx; margin-bottom: 30rpx; white-space: nowrap; overflow-x: auto; }
|
||||
.tab { display: inline-block; text-align: center; padding: 20rpx 28rpx; font-size: 26rpx; color: #666; font-weight: 500; }
|
||||
.tab.active { color: #667eea; border-bottom: 4rpx solid #667eea; }
|
||||
|
||||
.pc-only { display: none; }
|
||||
.mobile-only { display: block; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.admin-container { padding: 0; }
|
||||
.pc-only { display: block; }
|
||||
.mobile-only { display: none; }
|
||||
.sidebar { display: flex; flex-direction: column; width: 220px; min-height: 100vh; background: #001529; position: fixed; top: 0; left: 0; z-index: 9999; }
|
||||
.sidebar-header { display: flex; align-items: center; padding: 16px 20px; background: #002140; border-bottom: 1px solid #003a5c; }
|
||||
.sidebar-title { color: #fff; font-size: 16px; font-weight: 600; }
|
||||
.sidebar .tab { display: flex; align-items: center; gap: 10px; padding: 14px 24px; font-size: 14px; color: rgba(255,255,255,0.65); cursor: pointer; transition: all 0.2s; }
|
||||
.sidebar .tab:hover { color: #fff; background: #002140; }
|
||||
.sidebar .tab.active { color: #fff; background: #1890ff; }
|
||||
.tab-icon { font-size: 16px; }
|
||||
.main-content { margin-left: 220px; flex: 1; padding: 24px; min-height: 100vh; width: calc(100% - 220px); }
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.admin-container { padding: 20rpx; flex-direction: column; }
|
||||
.sidebar { display: none; }
|
||||
.main-content { flex: 1; width: 100%; }
|
||||
.tabs { width: 100%; background: #fff; border-radius: 16rpx; margin-bottom: 30rpx; white-space: nowrap; overflow-x: auto; }
|
||||
}
|
||||
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20rpx; margin-bottom: 30rpx; }
|
||||
.stat-card { background: #fff; border-radius: 16rpx; padding: 30rpx; text-align: center; }
|
||||
.stat-value { font-size: 48rpx; font-weight: bold; color: #667eea; display: block; }
|
||||
@@ -679,4 +889,34 @@ watch(tab, (val) => {
|
||||
.quota-input { width: 140rpx; height: 56rpx; background: #fff; border: 1rpx solid #e0e0e0; border-radius: 8rpx; padding: 0 12rpx; font-size: 24rpx; text-align: center; }
|
||||
.quota-reset-btn { font-size: 22rpx; padding: 6rpx 16rpx; background: #fff7e6; color: #fa8c16; border-radius: 6rpx; }
|
||||
.quota-save-btn { font-size: 22rpx; padding: 6rpx 16rpx; background: #52c41a; color: #fff; border-radius: 6rpx; }
|
||||
.filter-bar { display: flex; gap: 12rpx; margin-bottom: 16rpx; flex-wrap: wrap; }
|
||||
.filter-btn { font-size: 22rpx; padding: 6rpx 16rpx; background: #f5f5f5; color: #666; border-radius: 6rpx; }
|
||||
.filter-btn.active { background: #667eea; color: #fff; }
|
||||
.cert-list, .inv-list { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.cert-card, .inv-card { padding: 16rpx; background: #f9f9f9; border-radius: 10rpx; }
|
||||
.cert-header, .inv-header { display: flex; justify-content: space-between; margin-bottom: 8rpx; }
|
||||
.cert-type { font-size: 24rpx; color: #333; font-weight: 500; }
|
||||
.cert-status { font-size: 20rpx; padding: 2rpx 10rpx; border-radius: 4rpx; }
|
||||
.cert-status.pending { background: #fff7e6; color: #d46b08; }
|
||||
.cert-status.approved { background: #f6ffed; color: #389e0d; }
|
||||
.cert-status.rejected { background: #fff2f0; color: #cf1322; }
|
||||
.cert-body { }
|
||||
.cert-field { font-size: 22rpx; color: #666; display: block; margin: 4rpx 0; }
|
||||
.cert-date { font-size: 20rpx; color: #999; display: block; margin-top: 4rpx; }
|
||||
.cert-actions { display: flex; gap: 12rpx; margin-top: 8rpx; }
|
||||
.action-btn.approve { background: #f6ffed; color: #52c41a; }
|
||||
.action-btn.reject { background: #fff2f0; color: #f5222d; }
|
||||
.action-btn.cancel { background: #f5f5f5; color: #666; }
|
||||
.cert-reason, .inv-reason { font-size: 20rpx; color: #cf1322; display: block; margin-top: 4rpx; }
|
||||
.inv-title { font-size: 24rpx; color: #333; font-weight: 500; }
|
||||
.inv-amount { font-size: 24rpx; color: #f5222d; font-weight: 600; }
|
||||
.inv-meta { display: flex; justify-content: space-between; margin: 6rpx 0; }
|
||||
.inv-type { font-size: 20rpx; color: #666; }
|
||||
.inv-status { font-size: 20rpx; padding: 2rpx 10rpx; border-radius: 4rpx; }
|
||||
.inv-status.pending { background: #fff7e6; color: #d46b08; }
|
||||
.inv-status.issued { background: #f6ffed; color: #389e0d; }
|
||||
.inv-status.rejected { background: #fff2f0; color: #cf1322; }
|
||||
.inv-actions { display: flex; gap: 12rpx; margin-top: 8rpx; }
|
||||
.reject-input { width: 100%; height: 80rpx; border: 1rpx solid #d9d9d9; border-radius: 8rpx; padding: 0 16rpx; font-size: 26rpx; box-sizing: border-box; }
|
||||
.modal-actions { display: flex; gap: 16rpx; margin-top: 20rpx; justify-content: flex-end; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="header-card">
|
||||
<text class="title">实名认证</text>
|
||||
<text class="subtitle">认证后可申请发票</text>
|
||||
</view>
|
||||
|
||||
<view v-if="loading.status" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<view v-else-if="cert" class="section">
|
||||
<view class="status-bar" :class="cert.status">
|
||||
<text class="status-icon">{{ statusIcon }}</text>
|
||||
<text class="status-text">{{ statusText }}</text>
|
||||
<text v-if="cert.reject_reason" class="reject-reason">原因:{{ cert.reject_reason }}</text>
|
||||
</view>
|
||||
<view class="info-card">
|
||||
<view class="info-row">
|
||||
<text class="info-label">认证类型</text>
|
||||
<text class="info-value">{{ cert.cert_type === 'individual' ? '个人认证' : '企业认证' }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="cert.personal_name">
|
||||
<text class="info-label">姓名</text>
|
||||
<text class="info-value">{{ cert.personal_name }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="cert.company_name">
|
||||
<text class="info-label">企业名称</text>
|
||||
<text class="info-value">{{ cert.company_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="cert.status === 'rejected'" class="action-bar">
|
||||
<button class="primary-btn" @click="resetForm">重新提交</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="section">
|
||||
<view class="input-group">
|
||||
<text class="input-label">认证类型</text>
|
||||
<view class="type-selector">
|
||||
<text class="type-option" :class="{ active: form.cert_type === 'individual' }" @click="form.cert_type = 'individual'">个人认证</text>
|
||||
<text class="type-option" :class="{ active: form.cert_type === 'enterprise' }" @click="form.cert_type = 'enterprise'">企业认证</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="input-label">姓名</text>
|
||||
<input class="input-field" v-model="form.personal_name" placeholder="输入真实姓名" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<text class="input-label">身份证号</text>
|
||||
<input class="input-field" v-model="form.personal_id" placeholder="输入身份证号码" />
|
||||
</view>
|
||||
|
||||
<view v-if="form.cert_type === 'enterprise'" class="enterprise-fields">
|
||||
<view class="input-group">
|
||||
<text class="input-label">企业名称</text>
|
||||
<input class="input-field" v-model="form.company_name" placeholder="输入营业执照上的企业名称" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<text class="input-label">统一社会信用代码</text>
|
||||
<input class="input-field" v-model="form.tax_id" placeholder="输入税号" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="notice">
|
||||
<text class="notice-text">提交后预计 1-3 个工作日审核完成</text>
|
||||
</view>
|
||||
|
||||
<button class="primary-btn" :disabled="loading.submit" @click="doSubmit">
|
||||
<text v-if="loading.submit">提交中...</text>
|
||||
<text v-else>提交认证</text>
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { certificationApi } from '@/utils/api.js'
|
||||
|
||||
const loading = reactive({ status: false, submit: false })
|
||||
const cert = ref(null)
|
||||
const form = reactive({
|
||||
cert_type: 'individual',
|
||||
personal_name: '',
|
||||
personal_id: '',
|
||||
company_name: '',
|
||||
tax_id: '',
|
||||
})
|
||||
|
||||
const statusIcon = computed(() => ({ pending: '⏳', approved: '✅', rejected: '❌' })[cert.value?.status] || '')
|
||||
const statusText = computed(() => ({ pending: '审核中', approved: '已认证', rejected: '认证未通过' })[cert.value?.status] || '')
|
||||
|
||||
async function loadStatus() {
|
||||
loading.status = true
|
||||
try {
|
||||
const res = await certificationApi.status()
|
||||
if (res.success && res.data) {
|
||||
cert.value = res.data
|
||||
form.personal_name = res.data.personal_name || ''
|
||||
form.company_name = res.data.company_name || ''
|
||||
}
|
||||
} catch (e) {
|
||||
// not certified yet
|
||||
}
|
||||
loading.status = false
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
cert.value = null
|
||||
form.cert_type = 'individual'
|
||||
form.personal_name = ''
|
||||
form.personal_id = ''
|
||||
form.company_name = ''
|
||||
form.tax_id = ''
|
||||
}
|
||||
|
||||
async function doSubmit() {
|
||||
if (!form.personal_name) { uni.showToast({ title: '请填写姓名', icon: 'none' }); return }
|
||||
if (!form.personal_id) { uni.showToast({ title: '请填写身份证号', icon: 'none' }); return }
|
||||
if (form.cert_type === 'enterprise' && !form.company_name) {
|
||||
uni.showToast({ title: '请填写企业名称', icon: 'none' }); return
|
||||
}
|
||||
loading.submit = true
|
||||
try {
|
||||
const res = await certificationApi.submit(form)
|
||||
if (res.success) {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
loadStatus()
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
loading.submit = false
|
||||
}
|
||||
|
||||
onMounted(loadStatus)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page { background: #f5f5f5; }
|
||||
.page { padding: 16px; }
|
||||
.header-card { background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
|
||||
.header-card .title { font-size: 20px; font-weight: 600; color: #fff; display: block; }
|
||||
.header-card .subtitle { font-size: 13px; color: rgba(255,255,255,.8); display: block; margin-top: 4px; }
|
||||
.section { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
|
||||
.loading-wrap { text-align: center; padding: 40px; }
|
||||
.loading-text { color: #999; }
|
||||
.status-bar { text-align: center; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
|
||||
.status-bar.pending { background: #fff7e6; }
|
||||
.status-bar.approved { background: #f6ffed; }
|
||||
.status-bar.rejected { background: #fff2f0; }
|
||||
.status-icon { font-size: 32px; display: block; }
|
||||
.status-text { font-size: 16px; font-weight: 500; display: block; margin-top: 4px; }
|
||||
.reject-reason { font-size: 13px; color: #ff4d4f; display: block; margin-top: 8px; }
|
||||
.info-card { }
|
||||
.info-row { display: flex; justify-content: space-between; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.info-label { color: #666; font-size: 14px; }
|
||||
.info-value { color: #333; font-size: 14px; font-weight: 500; }
|
||||
.action-bar { margin-top: 16px; }
|
||||
.input-group { margin-bottom: 16px; }
|
||||
.input-label { font-size: 14px; color: #333; display: block; margin-bottom: 6px; font-weight: 500; }
|
||||
.input-field { width: 100%; height: 44px; border: 1px solid #d9d9d9; border-radius: 8px; padding: 0 12px; font-size: 14px; box-sizing: border-box; }
|
||||
.type-selector { display: flex; gap: 12px; }
|
||||
.type-option { flex: 1; text-align: center; padding: 10px; border: 2px solid #d9d9d9; border-radius: 8px; font-size: 14px; }
|
||||
.type-option.active { border-color: #667eea; color: #667eea; font-weight: 500; }
|
||||
.notice { background: #fffbe6; border: 1px solid #ffe58f; border-radius: 6px; padding: 10px; margin-bottom: 16px; }
|
||||
.notice-text { font-size: 13px; color: #ad8b00; }
|
||||
.primary-btn { width: 100%; height: 44px; background: #667eea; color: #fff; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; border: none; }
|
||||
.primary-btn:disabled { opacity: .6; }
|
||||
</style>
|
||||
@@ -0,0 +1,357 @@
|
||||
<template>
|
||||
<view class="discovery-container">
|
||||
<view class="header-card">
|
||||
<text class="title">挖掘新客</text>
|
||||
<text class="subtitle">AI 帮你找到潜在客户并生成开发信</text>
|
||||
</view>
|
||||
|
||||
<view class="tab-bar">
|
||||
<view class="tab-item" :class="{ active: activeTab === 'search' }" @click="activeTab = 'search'">
|
||||
<text class="tab-text">客户挖掘</text>
|
||||
</view>
|
||||
<view class="tab-item" :class="{ active: activeTab === 'outreach' }" @click="activeTab = 'outreach'">
|
||||
<text class="tab-text">开发信生成</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'search'" class="section">
|
||||
<view class="input-group">
|
||||
<text class="input-label">你的产品/服务</text>
|
||||
<textarea class="input-area" v-model="searchForm.product" placeholder="例如:太阳能电池板 200W 单晶硅" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<text class="input-label">目标市场</text>
|
||||
<input class="input-field" v-model="searchForm.market" placeholder="例如:美国、欧洲、东南亚" />
|
||||
</view>
|
||||
<button class="primary-btn" :disabled="loading.search" @click="doSearch">
|
||||
<text v-if="loading.search">搜索中...</text>
|
||||
<text v-else>开始挖掘</text>
|
||||
</button>
|
||||
|
||||
<view v-if="searchResult" class="result-area">
|
||||
|
||||
<view v-if="searchResult.companies && searchResult.companies.length > 0">
|
||||
<view class="result-card" v-for="(company, i) in searchResult.companies" :key="'c' + i">
|
||||
<text class="company-title">{{ company.title }}</text>
|
||||
<text class="company-url" @click="openUrl(company.url)">{{ company.url }}</text>
|
||||
<text class="company-snippet" v-if="company.snippet">{{ company.snippet }}</text>
|
||||
<view class="company-actions" v-if="analysisLoading !== i">
|
||||
<text class="action-btn-sm" @click="analyzeCompany(company, i)">🔍 分析</text>
|
||||
<text class="action-btn-sm add" @click="showAddCustomer(company)">➕ 加入客户</text>
|
||||
</view>
|
||||
<view class="company-actions" v-else>
|
||||
<text class="loading-text">分析中...</text>
|
||||
</view>
|
||||
|
||||
<view v-if="company.analysis" class="analysis-result">
|
||||
<view class="score-bar">
|
||||
<text class="score-label">匹配度</text>
|
||||
<view class="score-track">
|
||||
<view class="score-fill" :style="{ width: (company.analysis.match_score || 0) + '%' }" />
|
||||
</view>
|
||||
<text class="score-val">{{ company.analysis.match_score || '?' }}/100</text>
|
||||
</view>
|
||||
<text class="analysis-text" v-if="company.analysis.match_reason">{{ company.analysis.match_reason }}</text>
|
||||
<view class="contact-row" v-if="company.analysis.contact_info">
|
||||
<text class="contact-item" v-for="(emails, key) in company.analysis.contact_info" :key="key">
|
||||
{{ key }}: {{ Array.isArray(emails) ? emails.join(', ') : emails }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="searchResult.buyer_personas">
|
||||
<view class="strategy-card">
|
||||
<text class="section-label">由于搜索服务未配置,以下是 AI 推荐的发现策略:</text>
|
||||
</view>
|
||||
<view class="result-card" v-for="(persona, i) in searchResult.buyer_personas" :key="'p' + i">
|
||||
<text class="result-title">{{ persona.type }}</text>
|
||||
<text class="result-desc">{{ persona.description }}</text>
|
||||
<view class="tag-row">
|
||||
<text class="tag" v-for="(ch, ci) in persona.channels" :key="'c' + ci">{{ ch }}</text>
|
||||
</view>
|
||||
<view class="query-box" v-if="persona.search_queries">
|
||||
<text class="query-label">搜索关键词:</text>
|
||||
<text class="query-text" v-for="(q, qi) in persona.search_queries" :key="'q' + qi">{{ q }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="strategy-card" v-if="searchResult.strategy">
|
||||
<text class="strategy-title">策略建议</text>
|
||||
<text class="strategy-text">{{ searchResult.strategy }}</text>
|
||||
</view>
|
||||
|
||||
<view class="tips-card" v-if="searchResult.tips">
|
||||
<text class="tips-title">💡 实用建议</text>
|
||||
<text class="tip-item" v-for="(tip, ti) in searchResult.tips" :key="'t' + ti">{{ ti + 1 }}. {{ tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="activeTab === 'outreach'" class="section">
|
||||
<view class="input-group">
|
||||
<text class="input-label">目标公司名称</text>
|
||||
<input class="input-field" v-model="outreachForm.company" placeholder="例如:ABC Trading Co." />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<text class="input-label">公司简介(可选)</text>
|
||||
<textarea class="input-area" v-model="outreachForm.companyDesc" placeholder="对方的业务范围、主营产品等" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<text class="input-label">你的产品名称</text>
|
||||
<input class="input-field" v-model="outreachForm.product" placeholder="例如:太阳能电池板" />
|
||||
</view>
|
||||
<view class="input-group">
|
||||
<text class="input-label">产品优势(可选)</text>
|
||||
<textarea class="input-area" v-model="outreachForm.productAdv" placeholder="价格优势、品质认证、交期快等" />
|
||||
</view>
|
||||
<button class="primary-btn" :disabled="loading.outreach" @click="doOutreach">
|
||||
<text v-if="loading.outreach">生成中...</text>
|
||||
<text v-else>生成开发信</text>
|
||||
</button>
|
||||
|
||||
<view v-if="outreachResult" class="result-area">
|
||||
<view class="outreach-card" v-if="outreachResult.email_body">
|
||||
<text class="outreach-label">📧 邮件正文</text>
|
||||
<text class="outreach-content" style="white-space: pre-line">{{ outreachResult.email_body }}</text>
|
||||
<button class="copy-btn" @click="copyText(outreachResult.email_body)">复制</button>
|
||||
</view>
|
||||
<view class="outreach-card" v-if="outreachResult.linkedin_message">
|
||||
<text class="outreach-label">💼 LinkedIn 私信</text>
|
||||
<text class="outreach-content">{{ outreachResult.linkedin_message }}</text>
|
||||
<button class="copy-btn" @click="copyText(outreachResult.linkedin_message)">复制</button>
|
||||
</view>
|
||||
<view class="outreach-card" v-if="outreachResult.whatsapp_message">
|
||||
<text class="outreach-label">📱 WhatsApp 消息</text>
|
||||
<text class="outreach-content">{{ outreachResult.whatsapp_message }}</text>
|
||||
<button class="copy-btn" @click="copyText(outreachResult.whatsapp_message)">复制</button>
|
||||
</view>
|
||||
<view class="tips-card" v-if="outreachResult.tips">
|
||||
<text class="tips-title">💡 发送建议</text>
|
||||
<text class="tip-item" v-for="(tip, ti) in outreachResult.tips" :key="'ot' + ti">{{ ti + 1 }}. {{ tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const activeTab = ref('search')
|
||||
|
||||
const searchForm = reactive({
|
||||
product: '',
|
||||
market: 'US',
|
||||
})
|
||||
|
||||
const outreachForm = reactive({
|
||||
company: '',
|
||||
companyDesc: '',
|
||||
product: '',
|
||||
productAdv: '',
|
||||
})
|
||||
|
||||
const loading = reactive({ search: false, outreach: false })
|
||||
const analysisLoading = ref(-1)
|
||||
const searchResult = ref(null)
|
||||
const outreachResult = ref(null)
|
||||
|
||||
const apiBase = '/api/v1'
|
||||
|
||||
async function doSearch() {
|
||||
if (!searchForm.product.trim()) {
|
||||
uni.showToast({ title: '请填写产品描述', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loading.search = true
|
||||
searchResult.value = null
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: `${apiBase}/discovery/search`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
product_description: searchForm.product,
|
||||
target_market: searchForm.market,
|
||||
},
|
||||
})
|
||||
const body = res.data
|
||||
if (body.success && body.data) {
|
||||
searchResult.value = body.data
|
||||
} else {
|
||||
uni.showToast({ title: '搜索失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '网络错误', icon: 'none' })
|
||||
} finally {
|
||||
loading.search = false
|
||||
}
|
||||
}
|
||||
|
||||
async function analyzeCompany(company, index) {
|
||||
analysisLoading.value = index
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: `${apiBase}/discovery/analyze`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
company_url: company.url,
|
||||
product_description: searchForm.product,
|
||||
},
|
||||
})
|
||||
const body = res.data
|
||||
if (body.success && body.data) {
|
||||
company.analysis = body.data
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '分析失败', icon: 'none' })
|
||||
} finally {
|
||||
analysisLoading.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
function showAddCustomer(company) {
|
||||
uni.showModal({
|
||||
title: '加入客户列表',
|
||||
content: `将「${company.title}」添加到客户管理?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const token = uni.getStorageSync('token')
|
||||
await uni.request({
|
||||
url: `${apiBase}/customers`,
|
||||
method: 'POST',
|
||||
header: { Authorization: `Bearer ${token}` },
|
||||
data: {
|
||||
name: company.title,
|
||||
company: company.title,
|
||||
website: company.url,
|
||||
notes: company.snippet || '',
|
||||
status: 'potential',
|
||||
},
|
||||
})
|
||||
uni.showToast({ title: '已加入客户列表', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '添加失败,请手动添加', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openUrl(url) {
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
success: () => uni.showToast({ title: '网址已复制', icon: 'success' }),
|
||||
})
|
||||
}
|
||||
|
||||
async function doOutreach() {
|
||||
if (!outreachForm.company.trim()) {
|
||||
uni.showToast({ title: '请填写目标公司名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!outreachForm.product.trim()) {
|
||||
uni.showToast({ title: '请填写你的产品名称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
loading.outreach = true
|
||||
outreachResult.value = null
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: `${apiBase}/discovery/outreach`,
|
||||
method: 'POST',
|
||||
data: {
|
||||
company: {
|
||||
name: outreachForm.company,
|
||||
description: outreachForm.companyDesc,
|
||||
},
|
||||
product: {
|
||||
name: outreachForm.product,
|
||||
advantages: outreachForm.productAdv,
|
||||
},
|
||||
},
|
||||
})
|
||||
const body = res.data
|
||||
if (body.success && body.data) {
|
||||
outreachResult.value = body.data
|
||||
} else {
|
||||
uni.showToast({ title: '生成失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '网络错误', icon: 'none' })
|
||||
} finally {
|
||||
loading.outreach = false
|
||||
}
|
||||
}
|
||||
|
||||
function copyText(text) {
|
||||
uni.setClipboardData({
|
||||
data: text,
|
||||
success: () => uni.showToast({ title: '已复制', icon: 'success' }),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.discovery-container { padding: 30rpx; }
|
||||
.header-card { margin-bottom: 30rpx; }
|
||||
.header-card .title { font-size: 36rpx; font-weight: 700; color: #333; }
|
||||
.header-card .subtitle { font-size: 24rpx; color: #999; margin-top: 8rpx; display: block; }
|
||||
|
||||
.tab-bar { display: flex; background: #f5f5f5; border-radius: 12rpx; padding: 4rpx; margin-bottom: 30rpx; }
|
||||
.tab-item { flex: 1; text-align: center; padding: 16rpx 0; border-radius: 10rpx; }
|
||||
.tab-item.active { background: #fff; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06); }
|
||||
.tab-text { font-size: 26rpx; color: #666; }
|
||||
.tab-item.active .tab-text { color: #1890ff; font-weight: 600; }
|
||||
|
||||
.section { margin-bottom: 30rpx; }
|
||||
.input-group { margin-bottom: 24rpx; }
|
||||
.input-label { font-size: 26rpx; color: #333; font-weight: 500; margin-bottom: 10rpx; display: block; }
|
||||
.input-area { width: 100%; min-height: 120rpx; background: #fff; border: 2rpx solid #e8e8e8; border-radius: 12rpx; padding: 20rpx; font-size: 26rpx; color: #333; box-sizing: border-box; }
|
||||
.input-field { width: 100%; height: 72rpx; background: #fff; border: 2rpx solid #e8e8e8; border-radius: 12rpx; padding: 0 20rpx; font-size: 26rpx; color: #333; box-sizing: border-box; }
|
||||
.primary-btn { width: 100%; height: 80rpx; background: #1890ff; color: #fff; font-size: 28rpx; font-weight: 600; border-radius: 12rpx; display: flex; align-items: center; justify-content: center; border: none; margin-top: 10rpx; }
|
||||
.primary-btn[disabled] { opacity: 0.6; }
|
||||
.result-area { margin-top: 30rpx; }
|
||||
|
||||
.company-title { font-size: 28rpx; font-weight: 600; color: #333; display: block; margin-bottom: 6rpx; }
|
||||
.company-url { font-size: 22rpx; color: #1890ff; display: block; margin-bottom: 8rpx; }
|
||||
.company-snippet { font-size: 24rpx; color: #666; line-height: 1.5; display: block; margin-bottom: 14rpx; }
|
||||
.company-actions { display: flex; gap: 14rpx; margin-bottom: 12rpx; }
|
||||
.action-btn-sm { font-size: 24rpx; color: #1890ff; background: #eaf4ff; padding: 8rpx 20rpx; border-radius: 8rpx; }
|
||||
.action-btn-sm.add { color: #52c41a; background: #f0fff0; }
|
||||
.loading-text { font-size: 24rpx; color: #999; }
|
||||
|
||||
.analysis-result { background: #f9f9f9; border-radius: 10rpx; padding: 18rpx; margin-top: 8rpx; }
|
||||
.score-bar { display: flex; align-items: center; gap: 10rpx; margin-bottom: 10rpx; }
|
||||
.score-label { font-size: 22rpx; color: #666; flex-shrink: 0; }
|
||||
.score-track { flex: 1; height: 12rpx; background: #eee; border-radius: 6rpx; overflow: hidden; }
|
||||
.score-fill { height: 100%; background: #1890ff; border-radius: 6rpx; transition: width 0.3s; }
|
||||
.score-val { font-size: 22rpx; color: #1890ff; font-weight: 600; flex-shrink: 0; }
|
||||
.analysis-text { font-size: 22rpx; color: #666; display: block; margin-bottom: 8rpx; }
|
||||
.contact-row { display: flex; flex-wrap: wrap; gap: 6rpx; }
|
||||
.contact-item { font-size: 20rpx; color: #999; background: #fff; padding: 4rpx 10rpx; border-radius: 4rpx; }
|
||||
|
||||
.section-label { font-size: 24rpx; color: #fa8c16; background: #fff7e6; padding: 12rpx 18rpx; border-radius: 8rpx; display: block; margin-bottom: 16rpx; }
|
||||
|
||||
.result-card { background: #fff; border-radius: 16rpx; padding: 28rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
|
||||
.result-title { font-size: 28rpx; font-weight: 600; color: #1890ff; margin-bottom: 8rpx; display: block; }
|
||||
.result-desc { font-size: 24rpx; color: #666; line-height: 1.6; display: block; margin-bottom: 14rpx; }
|
||||
.tag-row { display: flex; flex-wrap: wrap; gap: 10rpx; margin-bottom: 12rpx; }
|
||||
.tag { font-size: 22rpx; color: #1890ff; background: #eaf4ff; padding: 6rpx 16rpx; border-radius: 6rpx; }
|
||||
.query-box { background: #f9f9f9; border-radius: 8rpx; padding: 14rpx; }
|
||||
.query-label { font-size: 22rpx; color: #999; display: block; margin-bottom: 6rpx; }
|
||||
.query-text { font-size: 22rpx; color: #333; background: #fff; padding: 4rpx 12rpx; border-radius: 4rpx; margin: 4rpx 6rpx 4rpx 0; display: inline-block; border: 1rpx solid #e8e8e8; }
|
||||
|
||||
.strategy-card, .tips-card, .outreach-card { background: #fff; border-radius: 16rpx; padding: 28rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04); }
|
||||
.strategy-title, .tips-title { font-size: 26rpx; font-weight: 600; color: #333; display: block; margin-bottom: 12rpx; }
|
||||
.strategy-text { font-size: 24rpx; color: #666; line-height: 1.6; display: block; }
|
||||
.tip-item { font-size: 24rpx; color: #666; line-height: 1.8; display: block; }
|
||||
|
||||
.outreach-label { font-size: 26rpx; font-weight: 600; color: #333; display: block; margin-bottom: 12rpx; }
|
||||
.outreach-content { font-size: 24rpx; color: #444; line-height: 1.7; display: block; margin-bottom: 14rpx; }
|
||||
.copy-btn { font-size: 22rpx; color: #1890ff; background: #eaf4ff; border: none; border-radius: 6rpx; padding: 8rpx 24rpx; }
|
||||
</style>
|
||||
@@ -136,6 +136,10 @@
|
||||
<text class="more-icon">🔤</text>
|
||||
<text class="more-text">翻译</text>
|
||||
</view>
|
||||
<view class="more-item" @click="goToPage(PAGES.DISCOVERY)">
|
||||
<text class="more-icon">🔍</text>
|
||||
<text class="more-text">挖掘新客</text>
|
||||
</view>
|
||||
<view class="more-item" @click="hasLogin ? goToPage(PAGES.PRODUCT) : goToLogin()">
|
||||
<text class="more-icon">📦</text>
|
||||
<text class="more-text">产品库</text>
|
||||
@@ -166,10 +170,6 @@
|
||||
<text class="more-icon">👨👩👧👦</text>
|
||||
<text class="more-text">团队</text>
|
||||
</view>
|
||||
<view class="more-item" v-if="isAdmin" @click="goToPage(PAGES.ADMIN)">
|
||||
<text class="more-icon">⚙️</text>
|
||||
<text class="more-text">管理</text>
|
||||
</view>
|
||||
<view class="more-item" @click="showWechatModal = true">
|
||||
<text class="more-icon">💁</text>
|
||||
<text class="more-text">联系客服</text>
|
||||
@@ -277,7 +277,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { ref, onUnmounted } from 'vue'
|
||||
import { onShow } from '@dcloudio/uni-app'
|
||||
import { authApi, customerApi, analyticsApi, onboardingApi, notificationApi, followupApi, translateApi, BASE_URL } from '@/utils/api.js'
|
||||
import AiAssistant from '@/components/ai-assistant.vue'
|
||||
@@ -292,14 +292,12 @@ const announcements = [
|
||||
]
|
||||
let announcementTimer = null
|
||||
|
||||
const hasLogin = computed(() => {
|
||||
const token = uni.getStorageSync('token')
|
||||
const isGuest = uni.getStorageSync('isGuest')
|
||||
return !!token && !isGuest
|
||||
})
|
||||
const isAdmin = computed(() => {
|
||||
return hasLogin.value && userInfo.value?.role === 'admin'
|
||||
})
|
||||
const hasLogin = ref(false)
|
||||
function checkLogin() {
|
||||
const token = uni.getStorageSync(STORAGE_KEYS.TOKEN)
|
||||
const isGuest = uni.getStorageSync(STORAGE_KEYS.IS_GUEST)
|
||||
hasLogin.value = !!token && !isGuest
|
||||
}
|
||||
const userInfo = ref(null)
|
||||
const stats = ref({
|
||||
customers: 0,
|
||||
@@ -331,9 +329,8 @@ onShow(() => {
|
||||
currentAnnouncement.value = (currentAnnouncement.value + 1) % announcements.length
|
||||
}, 4000)
|
||||
|
||||
const token = uni.getStorageSync('token')
|
||||
const isGuest = uni.getStorageSync('isGuest')
|
||||
if (token && !isGuest) {
|
||||
checkLogin()
|
||||
if (hasLogin.value) {
|
||||
uni.setStorageSync(STORAGE_KEYS.IS_GUEST, false)
|
||||
loadData()
|
||||
checkOnboarding()
|
||||
@@ -449,6 +446,24 @@ const goToLogin = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定退出当前账号?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.removeStorageSync(STORAGE_KEYS.TOKEN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
uni.removeStorageSync(STORAGE_KEYS.HAS_LOGIN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.IS_GUEST)
|
||||
hasLogin.value = false
|
||||
uni.switchTab({ url: PAGES.INDEX })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const doQuickTranslate = async () => {
|
||||
if (!quickTranslateText.value.trim()) return
|
||||
try {
|
||||
@@ -1237,4 +1252,11 @@ const playTryResult = () => {
|
||||
color: #bbb;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* On desktop, hide the feature matrix section (items are in the sidebar) */
|
||||
@media (min-width: 1024px) {
|
||||
.more-section {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="header-card">
|
||||
<text class="title">发票管理</text>
|
||||
<text class="subtitle">申请开票与开票记录</text>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="tab-bar">
|
||||
<text class="tab-item" :class="{ active: tab === 'apply' }" @click="tab = 'apply'">申请开票</text>
|
||||
<text class="tab-item" :class="{ active: tab === 'history' }" @click="tab = 'history'; loadInvoices()">开票记录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="tab === 'apply'" class="section">
|
||||
<view class="input-group">
|
||||
<text class="input-label">发票类型</text>
|
||||
<view class="type-selector">
|
||||
<text class="type-option" :class="{ active: form.invoice_type === 'individual' }" @click="form.invoice_type = 'individual'">个人发票</text>
|
||||
<text class="type-option" :class="{ active: form.invoice_type === 'enterprise' }" @click="form.invoice_type = 'enterprise'">企业发票</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="input-label">发票抬头</text>
|
||||
<input class="input-field" v-model="form.title" :placeholder="form.invoice_type === 'individual' ? '输入您的姓名' : '输入企业名称'" />
|
||||
</view>
|
||||
|
||||
<view class="input-group" v-if="form.invoice_type === 'enterprise'">
|
||||
<text class="input-label">税号</text>
|
||||
<input class="input-field" v-model="form.tax_id" placeholder="输入统一社会信用代码" />
|
||||
</view>
|
||||
|
||||
<view class="input-group">
|
||||
<text class="input-label">开票金额(元)</text>
|
||||
<input class="input-field" v-model="form.amount" type="number" placeholder="输入开票金额" />
|
||||
</view>
|
||||
|
||||
<view class="notice">
|
||||
<text class="notice-text">个人发票需先完成个人实名认证,企业发票需先完成企业认证</text>
|
||||
</view>
|
||||
|
||||
<button class="primary-btn" :disabled="loading.apply" @click="doApply">
|
||||
<text v-if="loading.apply">提交中...</text>
|
||||
<text v-else>提交开票申请</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-else class="section">
|
||||
<view v-if="loading.history" class="loading-wrap">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<view v-else-if="invoices.length === 0" class="empty-wrap">
|
||||
<text class="empty-text">暂无开票记录</text>
|
||||
</view>
|
||||
<view v-else class="invoice-list">
|
||||
<view class="invoice-item" v-for="inv in invoices" :key="inv.id">
|
||||
<view class="inv-header">
|
||||
<text class="inv-type">{{ inv.invoice_type === 'individual' ? '个人' : '企业' }}发票</text>
|
||||
<text class="inv-status" :class="inv.status">{{ statusLabel(inv.status) }}</text>
|
||||
</view>
|
||||
<view class="inv-body">
|
||||
<text class="inv-title">{{ inv.title }}</text>
|
||||
<text class="inv-amount">¥{{ inv.amount }}</text>
|
||||
</view>
|
||||
<text v-if="inv.reject_reason" class="inv-reject">驳回原因:{{ inv.reject_reason }}</text>
|
||||
<text class="inv-date">{{ inv.created_at ? inv.created_at.substring(0, 10) : '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { invoiceApi } from '@/utils/api.js'
|
||||
|
||||
const tab = ref('apply')
|
||||
const loading = reactive({ apply: false, history: false })
|
||||
const invoices = ref([])
|
||||
const form = reactive({
|
||||
invoice_type: 'individual',
|
||||
title: '',
|
||||
tax_id: '',
|
||||
amount: '',
|
||||
})
|
||||
|
||||
function statusLabel(s) {
|
||||
return { pending: '待开票', issued: '已开票', rejected: '已驳回' }[s] || s
|
||||
}
|
||||
|
||||
async function doApply() {
|
||||
if (!form.title) { uni.showToast({ title: '请填写发票抬头', icon: 'none' }); return }
|
||||
if (!form.amount || parseFloat(form.amount) <= 0) { uni.showToast({ title: '请填写有效金额', icon: 'none' }); return }
|
||||
if (form.invoice_type === 'enterprise' && !form.tax_id) { uni.showToast({ title: '请填写税号', icon: 'none' }); return }
|
||||
loading.apply = true
|
||||
try {
|
||||
const res = await invoiceApi.apply({
|
||||
invoice_type: form.invoice_type,
|
||||
title: form.title,
|
||||
tax_id: form.tax_id || undefined,
|
||||
amount: parseFloat(form.amount),
|
||||
})
|
||||
if (res.success) {
|
||||
uni.showToast({ title: '申请成功', icon: 'success' })
|
||||
form.title = ''; form.tax_id = ''; form.amount = ''
|
||||
tab.value = 'history'
|
||||
loadInvoices()
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
loading.apply = false
|
||||
}
|
||||
|
||||
async function loadInvoices() {
|
||||
loading.history = true
|
||||
try {
|
||||
const res = await invoiceApi.list()
|
||||
if (res.success) invoices.value = res.data || []
|
||||
} catch (e) { invoices.value = [] }
|
||||
loading.history = false
|
||||
}
|
||||
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
page { background: #f5f5f5; }
|
||||
.page { padding: 16px; }
|
||||
.header-card { background: linear-gradient(135deg, #52c41a, #1890ff); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
|
||||
.header-card .title { font-size: 20px; font-weight: 600; color: #fff; display: block; }
|
||||
.header-card .subtitle { font-size: 13px; color: rgba(255,255,255,.8); display: block; margin-top: 4px; }
|
||||
.section { background: #fff; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
|
||||
.tab-bar { display: flex; gap: 0; }
|
||||
.tab-item { flex: 1; text-align: center; padding: 10px; font-size: 15px; color: #666; border-bottom: 2px solid transparent; }
|
||||
.tab-item.active { color: #1890ff; border-bottom-color: #1890ff; font-weight: 500; }
|
||||
.input-group { margin-bottom: 16px; }
|
||||
.input-label { font-size: 14px; color: #333; display: block; margin-bottom: 6px; font-weight: 500; }
|
||||
.input-field { width: 100%; height: 44px; border: 1px solid #d9d9d9; border-radius: 8px; padding: 0 12px; font-size: 14px; box-sizing: border-box; }
|
||||
.type-selector { display: flex; gap: 12px; }
|
||||
.type-option { flex: 1; text-align: center; padding: 10px; border: 2px solid #d9d9d9; border-radius: 8px; font-size: 14px; }
|
||||
.type-option.active { border-color: #1890ff; color: #1890ff; font-weight: 500; }
|
||||
.notice { background: #fffbe6; border: 1px solid #ffe58f; border-radius: 6px; padding: 10px; margin-bottom: 16px; }
|
||||
.notice-text { font-size: 13px; color: #ad8b00; }
|
||||
.primary-btn { width: 100%; height: 44px; background: #1890ff; color: #fff; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; border: none; }
|
||||
.primary-btn:disabled { opacity: .6; }
|
||||
.loading-wrap { text-align: center; padding: 40px; }
|
||||
.loading-text { color: #999; }
|
||||
.empty-wrap { text-align: center; padding: 40px; }
|
||||
.empty-text { color: #999; }
|
||||
.invoice-list { }
|
||||
.invoice-item { padding: 12px 0; border-bottom: 1px solid #f0f0f0; }
|
||||
.inv-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
|
||||
.inv-type { font-size: 13px; color: #666; }
|
||||
.inv-status { font-size: 12px; padding: 2px 8px; border-radius: 4px; }
|
||||
.inv-status.pending { background: #fff7e6; color: #d46b08; }
|
||||
.inv-status.issued { background: #f6ffed; color: #389e0d; }
|
||||
.inv-status.rejected { background: #fff2f0; color: #cf1322; }
|
||||
.inv-body { display: flex; justify-content: space-between; }
|
||||
.inv-title { font-size: 15px; color: #333; font-weight: 500; flex: 1; }
|
||||
.inv-amount { font-size: 16px; color: #f5222d; font-weight: 600; }
|
||||
.inv-reject { font-size: 12px; color: #cf1322; display: block; margin-top: 4px; }
|
||||
.inv-date { font-size: 12px; color: #999; display: block; margin-top: 4px; }
|
||||
</style>
|
||||
@@ -28,6 +28,20 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" v-if="user.tier !== 'guest'">
|
||||
<view class="section-title">认证与发票</view>
|
||||
<view class="menu-item" @click="goCertification">
|
||||
<text class="menu-icon">🪪</text>
|
||||
<text class="menu-text">实名认证</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" @click="goInvoice">
|
||||
<text class="menu-icon">🧾</text>
|
||||
<text class="menu-text">发票管理</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">其他</view>
|
||||
<view class="menu-item" @click="goFeedback">
|
||||
@@ -174,6 +188,8 @@ const goLogin = () => uni.navigateTo({ url: PAGES.LOGIN })
|
||||
const goUpgrade = () => uni.navigateTo({ url: PAGES.UPGRADE })
|
||||
const goFeedback = () => uni.navigateTo({ url: PAGES.FEEDBACK })
|
||||
const goAgreement = (type) => uni.navigateTo({ url: `/pages/agreement/${type}` })
|
||||
const goCertification = () => uni.navigateTo({ url: PAGES.CERTIFICATION })
|
||||
const goInvoice = () => uni.navigateTo({ url: PAGES.INVOICE })
|
||||
|
||||
const logout = () => {
|
||||
uni.showModal({
|
||||
|
||||
@@ -183,6 +183,20 @@ export const adminApi = {
|
||||
request(`/admin/translation-quotas/${encodeURIComponent(version)}`, 'PUT', data),
|
||||
resetTranslationQuota: (version) =>
|
||||
request(`/admin/translation-quotas/${encodeURIComponent(version)}/reset`, 'POST'),
|
||||
listCertifications: (page = 1, size = 20, status) => {
|
||||
let url = `/admin/certifications?page=${page}&size=${size}`
|
||||
if (status) url += `&status=${status}`
|
||||
return request(url)
|
||||
},
|
||||
reviewCertification: (id, action, reason) =>
|
||||
request(`/admin/certifications/${id}/review`, 'POST', { action, reason }),
|
||||
listInvoices: (page = 1, size = 20, status) => {
|
||||
let url = `/admin/invoices?page=${page}&size=${size}`
|
||||
if (status) url += `&status=${status}`
|
||||
return request(url)
|
||||
},
|
||||
processInvoice: (id, action, reason) =>
|
||||
request(`/admin/invoices/${id}/process`, 'POST', { action, reason }),
|
||||
}
|
||||
|
||||
export const aiChatApi = {
|
||||
@@ -303,6 +317,16 @@ export const whatsappApi = {
|
||||
request('/whatsapp/send', 'POST', { to, text, template_name: templateName, template_params: templateParams, media_url: mediaUrl, media_type: mediaType }),
|
||||
}
|
||||
|
||||
export const certificationApi = {
|
||||
submit: (data) => request('/certification/submit', 'POST', data),
|
||||
status: () => request('/certification/status'),
|
||||
}
|
||||
|
||||
export const invoiceApi = {
|
||||
apply: (data) => request('/invoices/apply', 'POST', data),
|
||||
list: () => request('/invoices/list'),
|
||||
}
|
||||
|
||||
export const customerApi = {
|
||||
list: (page = 1, size = 20, status) => {
|
||||
let params = `page=${page}&size=${size}`
|
||||
|
||||
Reference in New Issue
Block a user