高考版优化
审核模式优化
This commit is contained in:
1406
admin/src/views/MiniProgramPreview.vue
Normal file
1406
admin/src/views/MiniProgramPreview.vue
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@
|
||||
<div class="page-header">
|
||||
<div class="header-left">
|
||||
<h2>分销推广</h2>
|
||||
<p class="subtitle">分销商、佣金结算、提现审核与推广设置</p>
|
||||
<p class="subtitle">以分销商为核心 · 实时收益、团队绑定、佣金结算与推广设置</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button @click="refresh" class="refresh-btn">
|
||||
@@ -27,153 +27,142 @@
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div v-if="activeTab === 'overview'" class="overview-section">
|
||||
<!-- 核心指标 -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">分销商总数</div>
|
||||
<div class="stat-value">{{ overview.totalAgents }}</div>
|
||||
<div class="trend-tag up">
|
||||
<el-icon><CaretTop /></el-icon>今日+{{ overview.todayAgents }}
|
||||
</div>
|
||||
<!-- 分销商 Tab(含数据概览) -->
|
||||
<div v-if="activeTab === 'distributors'" class="distributors-section">
|
||||
<!-- 顶部 KPI 卡片 -->
|
||||
<div class="dist-kpi-grid">
|
||||
<div class="dist-kpi-card">
|
||||
<div class="dist-kpi-body">
|
||||
<div class="dist-kpi-label">分销商总数</div>
|
||||
<div class="dist-kpi-value">{{ overview.totalAgents }}</div>
|
||||
<div class="dist-kpi-foot up"><el-icon><CaretTop /></el-icon>今日 +{{ overview.todayAgents }}</div>
|
||||
</div>
|
||||
<div class="stat-icon purple"><el-icon><User /></el-icon></div>
|
||||
<div class="dist-kpi-icon ic-purple"><el-icon><User /></el-icon></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">累计佣金</div>
|
||||
<div class="stat-value">{{ formatCurrency(parseFloat(overview.totalCommission)) }}</div>
|
||||
<div class="trend-tag up">
|
||||
<el-icon><CaretTop /></el-icon>今日+{{ formatCurrency(parseFloat(overview.todayCommission)) }}
|
||||
</div>
|
||||
<div class="dist-kpi-card">
|
||||
<div class="dist-kpi-body">
|
||||
<div class="dist-kpi-label">累计佣金</div>
|
||||
<div class="dist-kpi-value">{{ formatCurrency(parseFloat(overview.totalCommission)) }}</div>
|
||||
<div class="dist-kpi-foot up"><el-icon><CaretTop /></el-icon>今日 +{{ formatCurrency(parseFloat(overview.todayCommission)) }}</div>
|
||||
</div>
|
||||
<div class="stat-icon green"><el-icon><Money /></el-icon></div>
|
||||
<div class="dist-kpi-icon ic-green"><el-icon><Money /></el-icon></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">待结算佣金</div>
|
||||
<div class="stat-value">{{ formatCurrency(parseFloat(overview.pendingCommission)) }}</div>
|
||||
<div class="trend-tag warning">{{ overview.pendingCount }}笔</div>
|
||||
<div class="dist-kpi-card">
|
||||
<div class="dist-kpi-body">
|
||||
<div class="dist-kpi-label">待结算佣金</div>
|
||||
<div class="dist-kpi-value">{{ formatCurrency(parseFloat(overview.pendingCommission)) }}</div>
|
||||
<div class="dist-kpi-foot warning">{{ overview.pendingCount }} 笔待结算</div>
|
||||
</div>
|
||||
<div class="stat-icon orange"><el-icon><Clock /></el-icon></div>
|
||||
<div class="dist-kpi-icon ic-orange"><el-icon><Clock /></el-icon></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-info">
|
||||
<div class="stat-label">绑定关系</div>
|
||||
<div class="stat-value">{{ overview.bindingCount }}</div>
|
||||
<div class="trend-tag neutral">有效绑定</div>
|
||||
<div class="dist-kpi-card">
|
||||
<div class="dist-kpi-body">
|
||||
<div class="dist-kpi-label">有效绑定</div>
|
||||
<div class="dist-kpi-value">{{ overview.bindingCount }}</div>
|
||||
<div class="dist-kpi-foot neutral">绑定中用户</div>
|
||||
</div>
|
||||
<div class="stat-icon blue"><el-icon><Connection /></el-icon></div>
|
||||
<div class="dist-kpi-icon ic-blue"><el-icon><Connection /></el-icon></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<div class="charts-grid">
|
||||
<div class="chart-card">
|
||||
<div class="card-header">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<span>近7天佣金趋势</span>
|
||||
<!-- 图表 + Top 10 排行 -->
|
||||
<div class="dist-charts-row">
|
||||
<div class="dist-chart-card dist-chart-card--trend">
|
||||
<div class="dist-chart-header">
|
||||
<span>近 7 天佣金趋势</span>
|
||||
</div>
|
||||
<VChart v-if="hasCommissionTrend" class="trend-chart-echarts" :option="commissionTrendOption" autoresize />
|
||||
<div v-else class="empty-chart">暂无趋势数据</div>
|
||||
<VChart v-if="hasCommissionTrend" class="dist-chart-canvas" :option="commissionTrendOption" autoresize />
|
||||
<div v-else class="dist-empty-chart">暂无趋势数据</div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="card-header">
|
||||
<el-icon><PieChart /></el-icon>
|
||||
<span>产品佣金分布</span>
|
||||
</div>
|
||||
<VChart v-if="hasProductSeries" class="trend-chart-echarts" :option="productSeriesOption" autoresize />
|
||||
<div v-else class="empty-chart">暂无产品数据</div>
|
||||
<div class="dist-chart-card dist-chart-card--product">
|
||||
<div class="dist-chart-header"><span>产品佣金分布</span></div>
|
||||
<VChart v-if="hasProductSeries" class="dist-chart-canvas" :option="productSeriesOption" autoresize />
|
||||
<div v-else class="dist-empty-chart">暂无产品数据</div>
|
||||
</div>
|
||||
<div class="chart-card top-ranking-card">
|
||||
<div class="card-header">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<span>分销商 Top 10 排行榜</span>
|
||||
<span class="head-hint">点击「查看订单」定位该分销商带来的订单</span>
|
||||
<div class="dist-chart-card dist-chart-card--top10">
|
||||
<div class="dist-chart-header">
|
||||
<span>Top 10 分销商</span>
|
||||
<span class="dist-chart-hint">按累计佣金排序</span>
|
||||
</div>
|
||||
<div v-if="overviewTopDistributors.length" class="overview-list">
|
||||
<div v-for="(item, idx) in overviewTopDistributors" :key="item.id" class="overview-list-item">
|
||||
<div class="overview-main">
|
||||
<span :class="['rank-badge', idx < 3 ? 'rank-top' : '']">{{ idx + 1 }}</span>
|
||||
<div class="agent-cell">
|
||||
<el-avatar :size="30" :src="item.avatar" class="agent-avatar">
|
||||
{{ item.agentName ? item.agentName[0] : '?' }}
|
||||
</el-avatar>
|
||||
<div class="overview-meta">
|
||||
<strong>{{ item.agentName || '-' }}</strong>
|
||||
<span>团队 {{ item.teamCount || 0 }} 人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-side">
|
||||
<strong>{{ formatCurrency(parseFloat(item.totalCommission || 0)) }}</strong>
|
||||
<el-button link type="primary" size="small" @click="gotoInviterOrders(item)">查看订单</el-button>
|
||||
</div>
|
||||
<div v-if="overviewTopDistributors.length" class="top10-list">
|
||||
<div v-for="(item, idx) in overviewTopDistributors" :key="item.id" class="top10-item">
|
||||
<span :class="['top10-rank', idx < 3 ? 'top10-rank--gold' : '']">{{ idx + 1 }}</span>
|
||||
<el-avatar :size="28" :src="item.avatar" class="top10-avatar">{{ item.agentName ? item.agentName[0] : '?' }}</el-avatar>
|
||||
<span class="top10-name">{{ item.agentName || '-' }}</span>
|
||||
<span class="top10-team">{{ item.teamCount || 0 }}人</span>
|
||||
<span class="top10-comm">{{ formatCurrency(parseFloat(item.totalCommission || 0)) }}</span>
|
||||
<el-button link type="primary" size="small" @click="gotoUserDetail(item)">档案</el-button>
|
||||
<el-button link type="primary" size="small" @click="gotoInviterOrders(item)">订单</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-chart">暂无分销商数据</div>
|
||||
</div>
|
||||
<div class="chart-card">
|
||||
<div class="card-header">
|
||||
<el-icon><List /></el-icon>
|
||||
<span>佣金统计</span>
|
||||
</div>
|
||||
<div class="withdraw-stats">
|
||||
<div class="stat-item green"><span>累计佣金</span><strong>{{ formatCurrency(parseFloat(overview.totalCommission)) }}</strong></div>
|
||||
<div class="stat-item orange"><span>今日新增</span><strong>{{ formatCurrency(parseFloat(overview.todayCommission)) }}</strong></div>
|
||||
<div class="stat-item blue"><span>待结算佣金</span><strong>{{ formatCurrency(parseFloat(overview.pendingCommission)) }}</strong></div>
|
||||
</div>
|
||||
<div v-else class="dist-empty-chart">暂无排行数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="activeTab === 'distributors'" class="table-section">
|
||||
<!-- 分销商列表 -->
|
||||
<div class="content-card">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="distSearch" placeholder="搜索用户名..." class="search-input">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="loadDistributors">搜索</el-button>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<el-input v-model="distSearch" placeholder="搜索用户名 / 手机号..." class="search-input" @keyup.enter="loadDistributors">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" @click="loadDistributors">搜索</el-button>
|
||||
</div>
|
||||
<span class="toolbar-count">共 {{ overview.totalAgents }} 位分销商</span>
|
||||
</div>
|
||||
<el-table :data="distributors" style="width: 100%" class="custom-table" v-loading="loading">
|
||||
<el-table-column label="分销商" min-width="160">
|
||||
<el-table :data="distributors" style="width: 100%" class="custom-table dist-table" v-loading="loading" row-key="id">
|
||||
<el-table-column label="分销商" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="agent-cell">
|
||||
<el-avatar :size="36" :src="row.avatar" class="agent-avatar">
|
||||
{{ row.agentName ? row.agentName[0] : '?' }}
|
||||
</el-avatar>
|
||||
<span class="agent-name">{{ row.agentName || '-' }}</span>
|
||||
<div class="agent-avatar-wrap">
|
||||
<el-avatar :size="36" :src="row.avatar" class="agent-avatar">{{ row.agentName ? row.agentName[0] : '?' }}</el-avatar>
|
||||
</div>
|
||||
<div class="agent-info">
|
||||
<span class="agent-name">{{ row.agentName || '-' }}</span>
|
||||
<span v-if="row.phone" class="agent-phone">{{ row.phone }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="累计收益" align="right">
|
||||
<el-table-column label="累计收益" align="right" min-width="110">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openCommDetail(row)">
|
||||
{{ formatCurrency(parseFloat(row.totalCommission || 0)) }}
|
||||
</el-button>
|
||||
<div class="comm-cell">
|
||||
<el-button link type="primary" @click="openCommDetail(row)" class="comm-value">
|
||||
{{ formatCurrency(parseFloat(row.totalCommission || 0)) }}
|
||||
</el-button>
|
||||
<span class="comm-sub">点击查看明细</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="可提现" align="right">
|
||||
<el-table-column label="可提现" align="right" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(parseFloat(row.availableCommission || 0)) }}
|
||||
<span :class="['avail-value', parseFloat(row.availableCommission || 0) > 0 ? 'avail-value--pos' : '']">
|
||||
{{ formatCurrency(parseFloat(row.availableCommission || 0)) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="团队人数" align="center">
|
||||
<el-table-column label="团队人数" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openTeamDetail(row)">
|
||||
{{ row.teamCount }}人
|
||||
</el-button>
|
||||
<el-button link type="primary" @click="openTeamDetail(row)">{{ row.teamCount || 0 }} 人</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="加入时间" min-width="160">
|
||||
<el-table-column label="状态" align="center" width="90">
|
||||
<template #default="{ row }">
|
||||
{{ row.createdAt ? new Date(row.createdAt * 1000).toLocaleString() : '-' }}
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small" effect="light">
|
||||
{{ row.status === 'active' ? '活跃' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110" align="center" fixed="right">
|
||||
<el-table-column label="加入时间" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="gotoInviterOrders(row)">该分销商订单</el-button>
|
||||
<span class="time-cell">{{ row.createdAt ? new Date(row.createdAt * 1000).toLocaleDateString() : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" size="small" @click="gotoUserDetail(row)">用户档案</el-button>
|
||||
<el-button link type="primary" size="small" @click="gotoInviterOrders(row)">订单</el-button>
|
||||
<el-button link size="small" @click="openCommDetail(row)">佣金</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -414,13 +403,12 @@ use([CanvasRenderer, LineChart, BarChart, EchartsPieChart, GridComponent, Toolti
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const activeTab = ref('overview')
|
||||
const activeTab = ref('distributors')
|
||||
const distSearch = ref('')
|
||||
const commFilter = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const tabs = [
|
||||
{ label: '数据概览', value: 'overview' },
|
||||
{ label: '分销商', value: 'distributors' },
|
||||
{ label: '佣金记录', value: 'commissions' },
|
||||
{ label: '分销设置', value: 'settings' }
|
||||
@@ -574,7 +562,7 @@ const distEnabled = ref(true)
|
||||
const promoCenterTitle = ref('推广中心')
|
||||
|
||||
const testTypeItems = [
|
||||
{ key: 'face', label: '人脸分析' },
|
||||
{ key: 'face', label: '<EFBFBD><EFBFBD><EFBFBD>脸分析' },
|
||||
{ key: 'mbti', label: 'MBTI 测试' },
|
||||
{ key: 'sbti', label: 'SBTI 测试' },
|
||||
{ key: 'disc', label: 'DISC 测试' },
|
||||
@@ -743,11 +731,10 @@ const saveSettings = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 监听tab切换
|
||||
// 监听 tab 切换
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'overview') {
|
||||
if (newTab === 'distributors') {
|
||||
loadOverview()
|
||||
} else if (newTab === 'distributors') {
|
||||
loadDistributors()
|
||||
} else if (newTab === 'commissions') {
|
||||
loadCommissions()
|
||||
@@ -774,6 +761,19 @@ function gotoInviterOrders(row: any) {
|
||||
})
|
||||
}
|
||||
|
||||
function gotoUserDetail(row: any) {
|
||||
// 优先使用 userId,其次 id(分销商自身用户 ID)
|
||||
const userId = row?.userId ?? row?.id
|
||||
if (!userId) {
|
||||
ElMessage.warning('无法获取用户 ID')
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
path: '/admin/users',
|
||||
query: { openUserId: String(userId) },
|
||||
})
|
||||
}
|
||||
|
||||
const refresh = async () => {
|
||||
if (activeTab.value === 'overview') {
|
||||
await loadOverview()
|
||||
@@ -786,45 +786,59 @@ const refresh = async () => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认加载分销商面板(含数<E590AB><E695B0><EFBFBD>概览)
|
||||
loadOverview()
|
||||
loadDistributors()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.page-container {
|
||||
min-height: calc(100vh - 56px);
|
||||
background: #F4F6FB;
|
||||
padding: 20px 24px 24px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.header-left {
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
margin: 0 0 4px 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
font-size: 12.5px;
|
||||
color: #6B7280;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
padding: 7px 14px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
height: 34px;
|
||||
background: #fff;
|
||||
height: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafb;
|
||||
border-color: #d1d5db;
|
||||
background-color: #F9FAFB;
|
||||
border-color: #D1D5DB;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -840,13 +854,15 @@ onMounted(() => {
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border: 1px solid #f3f4f6;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #E5E7EB;
|
||||
box-shadow: 0 1px 3px rgba(16,24,40,0.04), 0 4px 12px rgba(16,24,40,0.03);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
&:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(79,70,229,0.1); }
|
||||
|
||||
.stat-info {
|
||||
.stat-label {
|
||||
@@ -1281,4 +1297,197 @@ onMounted(() => {
|
||||
@media (max-width: 640px) {
|
||||
.settings-card .ts-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* ── 新分销商面板 ── */
|
||||
.distributors-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* KPI 卡片 */
|
||||
.dist-kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 14px;
|
||||
|
||||
@media (max-width: 900px) { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
.dist-kpi-card {
|
||||
background: #fff;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 14px;
|
||||
padding: 18px 18px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
box-shadow: 0 1px 4px rgba(16,24,40,0.04);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
&:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(79,70,229,0.08); }
|
||||
|
||||
.dist-kpi-label { font-size: 12.5px; color: #6B7280; margin-bottom: 6px; }
|
||||
.dist-kpi-value { font-size: 22px; font-weight: 800; color: #111827; line-height: 1; letter-spacing: -0.02em; }
|
||||
.dist-kpi-foot {
|
||||
font-size: 11.5px;
|
||||
margin-top: 7px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
&.up { color: #22c55e; }
|
||||
&.warning { color: #f59e0b; }
|
||||
&.neutral { color: #3b82f6; }
|
||||
}
|
||||
|
||||
.dist-kpi-icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ic-purple { background: #faf5ff; color: #8b5cf6; }
|
||||
.ic-green { background: #ecfdf5; color: #10b981; }
|
||||
.ic-orange { background: #fffbeb; color: #f59e0b; }
|
||||
.ic-blue { background: #eff6ff; color: #3b82f6; }
|
||||
|
||||
/* 图表行 */
|
||||
.dist-charts-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr 1.4fr;
|
||||
gap: 14px;
|
||||
|
||||
@media (max-width: 1100px) { grid-template-columns: 1fr 1fr; }
|
||||
@media (max-width: 700px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.dist-chart-card {
|
||||
background: #fff;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 14px;
|
||||
padding: 18px 18px 14px;
|
||||
box-shadow: 0 1px 3px rgba(16,24,40,0.04);
|
||||
}
|
||||
|
||||
.dist-chart-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
span:first-child {
|
||||
font-size: 13.5px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.dist-chart-hint {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.dist-chart-canvas {
|
||||
height: 180px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dist-empty-chart {
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #CBD5E1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Top 10 排行 */
|
||||
.top10-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.top10-item {
|
||||
display: grid;
|
||||
grid-template-columns: 22px 28px 1fr 40px 70px 42px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 12.5px;
|
||||
transition: background 0.12s;
|
||||
&:hover { background: #f5f3ff; }
|
||||
}
|
||||
|
||||
.top10-rank {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
&--gold { background: #fef3c7; color: #b45309; }
|
||||
}
|
||||
|
||||
.top10-avatar { flex-shrink: 0; background: #ede9fe; color: #7c3aed; }
|
||||
.top10-name { font-size: 12.5px; font-weight: 600; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.top10-team { font-size: 11px; color: #9ca3af; text-align: center; }
|
||||
.top10-comm { font-size: 12.5px; font-weight: 700; color: #4f46e5; text-align: right; }
|
||||
|
||||
/* 分销商列表补充样式 */
|
||||
.dist-table :deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
&:hover td { background: #f5f3ff !important; }
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.agent-name { font-size: 13px; font-weight: 600; color: #111827; }
|
||||
.agent-phone { font-size: 11.5px; color: #9ca3af; }
|
||||
}
|
||||
|
||||
.agent-avatar-wrap { flex-shrink: 0; }
|
||||
|
||||
.comm-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 1px;
|
||||
|
||||
.comm-value { font-weight: 700; }
|
||||
.comm-sub { font-size: 10.5px; color: #d1d5db; }
|
||||
}
|
||||
|
||||
.avail-value {
|
||||
font-size: 13px;
|
||||
color: #9ca3af;
|
||||
&--pos { color: #10b981; font-weight: 700; }
|
||||
}
|
||||
|
||||
.toolbar-count {
|
||||
font-size: 12.5px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.time-cell {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div class="orders-hub">
|
||||
<div class="hub-header">
|
||||
<h2>订单运营</h2>
|
||||
<p class="hub-subtitle">订单成交、测试定价与题库在同一入口完成</p>
|
||||
<div>
|
||||
<h2>订单管理</h2>
|
||||
<p class="hub-subtitle">支付订单、成交状态与关联数据</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-tabs-container tabs-scroll">
|
||||
@@ -19,11 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="hub-body flat">
|
||||
<Orders
|
||||
v-if="activeTab === 'orders'"
|
||||
embedded
|
||||
orders-api-path="/admin/orders"
|
||||
/>
|
||||
<Orders v-if="activeTab === 'orders'" embedded orders-api-path="/admin/orders" />
|
||||
<Pricing v-if="activeTab === 'pricing'" embedded />
|
||||
<Questions v-if="activeTab === 'questions'" embedded />
|
||||
</div>
|
||||
@@ -71,9 +69,7 @@ const selectTab = (tab: TabId) => {
|
||||
q[k] = Array.isArray(v) ? String(v[0]) : String(v)
|
||||
}
|
||||
})
|
||||
if (tab !== 'orders') {
|
||||
q.tab = tab
|
||||
}
|
||||
if (tab !== 'stats') q.tab = tab
|
||||
router.replace({ path: '/admin/orders', query: Object.keys(q).length ? q : {} })
|
||||
}
|
||||
|
||||
@@ -86,31 +82,38 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.orders-hub {
|
||||
padding: 24px;
|
||||
min-height: 100vh;
|
||||
background: #f9fafb;
|
||||
min-height: calc(100vh - 56px);
|
||||
background: #F4F6FB;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hub-header {
|
||||
margin-bottom: 20px;
|
||||
padding: 20px 24px 0;
|
||||
margin-bottom: 0;
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
margin: 0 0 4px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hub-subtitle {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
font-size: 12.5px;
|
||||
color: #6B7280;
|
||||
}
|
||||
}
|
||||
|
||||
/* .custom-tabs-container 视觉已统一在 admin-theme.css */
|
||||
.custom-tabs-container {
|
||||
padding: 16px 24px 0;
|
||||
}
|
||||
|
||||
.hub-body.flat {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background: #F4F6FB;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
<div v-if="!embedded" class="page-header">
|
||||
<div class="header-left">
|
||||
<h2>价格设置</h2>
|
||||
<p class="subtitle">分别配置个人版和企业版的测试价格</p>
|
||||
<p class="subtitle">分别配置个人版、企业版和高考版的测试价格</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab 切换 -->
|
||||
<div class="custom-tabs-container">
|
||||
<div class="custom-tabs">
|
||||
<div
|
||||
@@ -21,8 +20,6 @@
|
||||
|
||||
<div class="pricing-content">
|
||||
<div class="tab-content-card">
|
||||
|
||||
<!-- 个人版价格 -->
|
||||
<div v-if="activeTab === 'personal'" class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="form-grid">
|
||||
@@ -46,10 +43,6 @@
|
||||
<label>SBTI测试价格 (元/次)</label>
|
||||
<el-input-number v-model="personal.sbti" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>高考志愿报告 (元/次)</label>
|
||||
<el-input-number v-model="personal.gaokao" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isUsingSuperAdminPersonalConfig" class="notice-box">
|
||||
<el-icon class="notice-icon"><InfoFilled /></el-icon>
|
||||
@@ -63,7 +56,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 企业版价格 -->
|
||||
<div v-if="activeTab === 'enterprise'" class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="form-grid">
|
||||
@@ -87,10 +79,6 @@
|
||||
<label>SBTI测试价格 (元/次)</label>
|
||||
<el-input-number v-model="enterprise.sbti" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>高考志愿报告 (元/次)</label>
|
||||
<el-input-number v-model="enterprise.gaokao" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isUsingSuperAdminEnterpriseConfig" class="notice-box">
|
||||
<el-icon class="notice-icon"><InfoFilled /></el-icon>
|
||||
@@ -104,6 +92,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activeTab === 'gaokao'" class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="form-grid">
|
||||
<div class="form-item">
|
||||
<label>人脸测试价格 (元/次)</label>
|
||||
<el-input-number v-model="gaokao.face" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>MBTI测试价格 (元/次)</label>
|
||||
<el-input-number v-model="gaokao.mbti" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>DISC测试价格 (元/次)</label>
|
||||
<el-input-number v-model="gaokao.disc" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>PDP测试价格 (元/次)</label>
|
||||
<el-input-number v-model="gaokao.pdp" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>SBTI测试价格 (元/次)</label>
|
||||
<el-input-number v-model="gaokao.sbti" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>高考志愿报告 (元/次)</label>
|
||||
<el-input-number v-model="gaokao.gaokao" :min="0" :precision="2" :controls="false" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isUsingSuperAdminGaokaoConfig" class="notice-box">
|
||||
<el-icon class="notice-icon"><InfoFilled /></el-icon>
|
||||
<span>当前使用超管默认高考版定价,保存后将创建您的企业专属高考版配置</span>
|
||||
</div>
|
||||
<div class="save-actions">
|
||||
<el-button type="primary" class="save-btn" @click="saveGaokao" :loading="loading">
|
||||
保存高考版价格
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,15 +147,18 @@ withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
const tabs = [
|
||||
{ label: '个人版价格', value: 'personal' },
|
||||
{ label: '企业版价格', value: 'enterprise' },
|
||||
{ label: '高考版价格', value: 'gaokao' }
|
||||
]
|
||||
const activeTab = ref('personal')
|
||||
|
||||
const personal = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0, sbti: 0, gaokao: 0 })
|
||||
const enterprise = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0, sbti: 0, gaokao: 0 })
|
||||
const personal = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0, sbti: 0 })
|
||||
const enterprise = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0, sbti: 0 })
|
||||
const gaokao = reactive({ face: 0, mbti: 0, disc: 0, pdp: 0, sbti: 0, gaokao: 0 })
|
||||
|
||||
const loading = ref(false)
|
||||
const isUsingSuperAdminPersonalConfig = ref(false)
|
||||
const isUsingSuperAdminEnterpriseConfig = ref(false)
|
||||
const isUsingSuperAdminGaokaoConfig = ref(false)
|
||||
|
||||
const loadPricing = async () => {
|
||||
loading.value = true
|
||||
@@ -141,10 +171,14 @@ const loadPricing = async () => {
|
||||
if (response.data.enterprise) {
|
||||
Object.assign(enterprise, response.data.enterprise)
|
||||
}
|
||||
if (response.data.gaokao) {
|
||||
Object.assign(gaokao, response.data.gaokao)
|
||||
}
|
||||
isUsingSuperAdminPersonalConfig.value = response.data.isUsingSuperAdminPersonalConfig
|
||||
?? response.data.isUsingSuperAdminConfig
|
||||
?? false
|
||||
isUsingSuperAdminEnterpriseConfig.value = response.data.isUsingSuperAdminEnterpriseConfig ?? false
|
||||
isUsingSuperAdminGaokaoConfig.value = response.data.isUsingSuperAdminGaokaoConfig ?? false
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载定价配置失败:', error)
|
||||
@@ -178,8 +212,7 @@ const saveEnterprise = async () => {
|
||||
mbti: enterprise.mbti,
|
||||
disc: enterprise.disc,
|
||||
pdp: enterprise.pdp,
|
||||
sbti: enterprise.sbti,
|
||||
gaokao: enterprise.gaokao
|
||||
sbti: enterprise.sbti
|
||||
}
|
||||
})
|
||||
if (response.code === 200) {
|
||||
@@ -194,6 +227,31 @@ const saveEnterprise = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveGaokao = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response: any = await request.put('/admin/pricing', {
|
||||
gaokaoConfig: {
|
||||
face: gaokao.face,
|
||||
mbti: gaokao.mbti,
|
||||
disc: gaokao.disc,
|
||||
pdp: gaokao.pdp,
|
||||
sbti: gaokao.sbti,
|
||||
gaokao: gaokao.gaokao
|
||||
}
|
||||
})
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('高考版价格已保存')
|
||||
isUsingSuperAdminGaokaoConfig.value = false
|
||||
await loadPricing()
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '保存失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPricing()
|
||||
})
|
||||
@@ -218,8 +276,6 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* .custom-tabs-container 视觉已统一在 admin-theme.css */
|
||||
|
||||
.pricing-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
@change="handleImport"
|
||||
style="display: none"
|
||||
/>
|
||||
<el-button size="small" type="primary" @click="openAdd">
|
||||
<el-button size="small" type="primary" class="btn-add-question" @click="openAdd">
|
||||
<el-icon class="mr-1"><Plus /></el-icon>新增题目
|
||||
</el-button>
|
||||
<el-button size="small" @click="triggerImport">
|
||||
@@ -878,21 +878,51 @@ onMounted(async () => {
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
|
||||
/* 与 size="small" 对齐:统一高度与内边距,避免主按钮与其它按钮视觉不一致 */
|
||||
.el-button {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #374151;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.el-button:not(.el-button--primary) {
|
||||
border: 1px solid #e5e7eb;
|
||||
color: #374151;
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafb;
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
/* 主按钮:白字且不与次要按钮共用 hover 样式;取消悬停变色(含 EP 文本色变量) */
|
||||
.btn-add-question.el-button--primary {
|
||||
--el-button-text-color: #fff;
|
||||
--el-button-hover-text-color: #fff;
|
||||
--el-button-active-text-color: #fff;
|
||||
--el-button-hover-bg-color: var(--admin-primary);
|
||||
--el-button-hover-border-color: var(--admin-primary);
|
||||
--el-button-active-bg-color: var(--admin-primary);
|
||||
--el-button-active-border-color: var(--admin-primary);
|
||||
color: #fff !important;
|
||||
border-color: var(--admin-primary);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: #fff !important;
|
||||
background-color: var(--admin-primary) !important;
|
||||
border-color: var(--admin-primary) !important;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,38 @@
|
||||
<template>
|
||||
<div class="users-hub">
|
||||
<!-- 页面头部 -->
|
||||
<div class="hub-header">
|
||||
<h2>用户运营</h2>
|
||||
<div class="hub-header-left">
|
||||
<h1 class="hub-title">用户管理</h1>
|
||||
<p class="hub-desc">用户信息、联系方式、简历与侧脸档案、合作意向全览</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pill-tabs" role="tablist">
|
||||
<!-- Tab 导航 -->
|
||||
<div class="tab-bar" role="tablist">
|
||||
<button
|
||||
v-for="t in innerTabs"
|
||||
:key="t.value"
|
||||
type="button"
|
||||
class="pill-tab"
|
||||
class="tab-btn"
|
||||
:class="{ 'is-active': activeTab === t.value }"
|
||||
@click="selectTab(t.value)"
|
||||
:aria-selected="activeTab === t.value"
|
||||
role="tab"
|
||||
>
|
||||
{{ t.label }}
|
||||
<component :is="t.icon" class="tab-icon" />
|
||||
<span>{{ t.label }}</span>
|
||||
<span v-if="t.value === 'cooperation'" class="tab-dot"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div
|
||||
class="hub-card"
|
||||
:class="{ 'flat-embed': activeTab === 'users' || activeTab === 'journey' || activeTab === 'rfm' }"
|
||||
class="hub-body"
|
||||
:class="{ 'no-padding': true }"
|
||||
>
|
||||
<Users v-if="activeTab === 'users'" embedded />
|
||||
<TopTestUsersPanel v-else-if="activeTab === 'top20'" />
|
||||
<UserJourneyPanel v-else-if="activeTab === 'journey'" />
|
||||
<UserRfmPanel v-else-if="activeTab === 'rfm'" />
|
||||
<CooperationChoicesPanel v-else-if="activeTab === 'cooperation'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -33,11 +41,12 @@
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Users from './Users.vue'
|
||||
import TopTestUsersPanel from '@/components/TopTestUsersPanel.vue'
|
||||
import UserJourneyPanel from '@/components/UserJourneyPanel.vue'
|
||||
import UserRfmPanel from '@/components/UserRfmPanel.vue'
|
||||
import CooperationChoices from './CooperationChoices.vue'
|
||||
import { User, Connection } from '@element-plus/icons-vue'
|
||||
|
||||
const TAB_IDS = ['users', 'journey', 'rfm', 'top20'] as const
|
||||
const CooperationChoicesPanel = CooperationChoices
|
||||
|
||||
const TAB_IDS = ['users', 'cooperation'] as const
|
||||
type TabId = (typeof TAB_IDS)[number]
|
||||
|
||||
function isTabId(s: string): s is TabId {
|
||||
@@ -48,11 +57,9 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const activeTab = ref<TabId>('users')
|
||||
|
||||
const innerTabs: { label: string; value: TabId }[] = [
|
||||
{ label: '用户列表', value: 'users' },
|
||||
{ label: '旅程漏斗', value: 'journey' },
|
||||
{ label: 'RFM 价值分层', value: 'rfm' },
|
||||
{ label: '测评 Top 20', value: 'top20' }
|
||||
const innerTabs: { label: string; value: TabId; icon: any }[] = [
|
||||
{ label: '用户列表', value: 'users', icon: User },
|
||||
{ label: '合作意向', value: 'cooperation', icon: Connection },
|
||||
]
|
||||
|
||||
const applyRouteTab = () => {
|
||||
@@ -77,90 +84,114 @@ const selectTab = (tab: TabId) => {
|
||||
q[k] = Array.isArray(v) ? String(v[0]) : String(v)
|
||||
}
|
||||
})
|
||||
if (tab !== 'users') {
|
||||
q.tab = tab
|
||||
}
|
||||
if (tab !== 'users') q.tab = tab
|
||||
router.replace({ path: '/admin/users', query: Object.keys(q).length ? q : {} })
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.query.tab,
|
||||
() => {
|
||||
applyRouteTab()
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
applyRouteTab()
|
||||
})
|
||||
watch(() => route.query.tab, () => { applyRouteTab() })
|
||||
onMounted(() => { applyRouteTab() })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.users-hub {
|
||||
padding: 24px;
|
||||
min-height: 100vh;
|
||||
background: #f9fafb;
|
||||
min-height: calc(100vh - 56px);
|
||||
background: #F4F6FB;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── 头部 ── */
|
||||
.hub-header {
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
}
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px 0;
|
||||
gap: 12px;
|
||||
}
|
||||
.hub-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: #111827;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.hub-desc {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
color: #6B7280;
|
||||
}
|
||||
|
||||
.pill-tabs {
|
||||
display: inline-flex;
|
||||
background: #f1f5f9;
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
/* ── Tab 导航 ── */
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin-bottom: 20px;
|
||||
max-width: 100%;
|
||||
padding: 16px 24px 0;
|
||||
border-bottom: 1px solid #E5E7EB;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
flex-shrink: 0;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar { display: none; }
|
||||
}
|
||||
|
||||
.pill-tab {
|
||||
border: 0;
|
||||
.tab-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 500;
|
||||
padding: 7px 18px;
|
||||
border-radius: 6px;
|
||||
color: #6B7280;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
border-radius: 8px 8px 0 0;
|
||||
transition: all 0.18s;
|
||||
position: relative;
|
||||
|
||||
.tab-icon {
|
||||
font-size: 15px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.tab-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #4F46E5;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #0f172a;
|
||||
background: #F3F4F6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: #ffffff;
|
||||
color: #0f172a;
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||
color: #4F46E5;
|
||||
font-weight: 700;
|
||||
background: #fff;
|
||||
box-shadow: 0 -2px 0 0 #4F46E5 inset, 1px 0 0 0 #E5E7EB inset, -1px 0 0 0 #E5E7EB inset;
|
||||
|
||||
.tab-icon { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
.hub-card {
|
||||
/* ── 内容区 ── */
|
||||
.hub-body {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-top: none;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 4px 12px rgba(15, 23, 42, 0.03);
|
||||
min-height: 0;
|
||||
|
||||
&.flat-embed {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
&.no-padding {
|
||||
padding: 0;
|
||||
background: #F4F6FB;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -63,9 +63,11 @@ const usersRefreshKey = ref(0)
|
||||
async function runOrphanMigrate() {
|
||||
migrateLoading.value = true
|
||||
try {
|
||||
const res: any = await request.post('/superadmin/data-migration/attach-orphans-to-cunkbao', {
|
||||
dryRun: true
|
||||
})
|
||||
const res: any = await request.post(
|
||||
'/superadmin/data-migration/attach-orphans-to-cunkbao',
|
||||
{ dryRun: true },
|
||||
{ timeout: 600_000 }
|
||||
)
|
||||
const d = res.data ?? res
|
||||
const name = d.enterpriseName || '存客宝'
|
||||
const wu = d.wechatUsersRows ?? 0
|
||||
@@ -80,10 +82,11 @@ async function runOrphanMigrate() {
|
||||
'确认归并',
|
||||
{ type: 'warning', confirmButtonText: '写入', cancelButtonText: '取消' }
|
||||
)
|
||||
await request.post('/superadmin/data-migration/attach-orphans-to-cunkbao', {
|
||||
dryRun: false,
|
||||
confirm: true
|
||||
})
|
||||
await request.post(
|
||||
'/superadmin/data-migration/attach-orphans-to-cunkbao',
|
||||
{ dryRun: false, confirm: true },
|
||||
{ timeout: 600_000 }
|
||||
)
|
||||
ElMessage.success('归并完成,已刷新用户总览')
|
||||
usersRefreshKey.value += 1
|
||||
} catch (e: unknown) {
|
||||
|
||||
@@ -299,6 +299,44 @@
|
||||
<el-input v-model="editEnterprise.contactEmail" placeholder="请输入联系人邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身份证号码">
|
||||
<el-input v-model="editEnterprise.idCardNumber" placeholder="请输入身份证号码(可选)" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="营业执照">
|
||||
<div class="license-upload-row">
|
||||
<div v-if="editEnterprise.businessLicenseUrl" class="license-preview">
|
||||
<img
|
||||
:src="editEnterprise.businessLicenseUrl"
|
||||
alt="营业执照"
|
||||
class="license-preview-img"
|
||||
/>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
link
|
||||
class="ml-2"
|
||||
@click="editEnterprise.businessLicenseUrl = ''"
|
||||
>
|
||||
清除
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else class="license-hint">支持上传图片(JPG/PNG/WebP 等)</div>
|
||||
<div class="license-actions">
|
||||
<el-button size="small" :loading="licenseUploading" @click="triggerLicenseUpload">
|
||||
上传营业执照
|
||||
</el-button>
|
||||
<input
|
||||
ref="licenseInputRef"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style="display:none"
|
||||
@change="onLicenseFileChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="editEnterprise.status" class="w-full" placeholder="请选择状态">
|
||||
<el-option label="运营中" value="operating" />
|
||||
@@ -825,6 +863,8 @@ import {
|
||||
} from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { request } from '@/utils/request'
|
||||
import axios from 'axios'
|
||||
import { getBearerTokenForCurrentApp } from '@/utils/authStorage'
|
||||
|
||||
withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false })
|
||||
|
||||
@@ -1126,11 +1166,59 @@ const editEnterprise = reactive({
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
idCardNumber: '',
|
||||
businessLicenseUrl: '',
|
||||
status: 'operating',
|
||||
trialExpireAt: '',
|
||||
permissions: defaultPermissions() as Record<string, boolean>
|
||||
})
|
||||
|
||||
const licenseInputRef = ref<HTMLInputElement | null>(null)
|
||||
const licenseUploading = ref(false)
|
||||
|
||||
const uploadImageFile = async (file: File): Promise<string> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const token = getBearerTokenForCurrentApp()
|
||||
const baseURL = import.meta.env.VITE_API_BASE_URL
|
||||
? import.meta.env.VITE_API_BASE_URL.endsWith('/')
|
||||
? `${import.meta.env.VITE_API_BASE_URL}api/v1`
|
||||
: `${import.meta.env.VITE_API_BASE_URL}/api/v1`
|
||||
: '/api/v1'
|
||||
|
||||
const res = await axios.post(`${baseURL}/superadmin/upload/image`, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||
}
|
||||
})
|
||||
|
||||
const url = res.data?.data?.url || res.data?.data?.ossUrl || res.data?.data?.path
|
||||
if (url) return String(url)
|
||||
throw new Error(res.data?.message || '上传失败')
|
||||
}
|
||||
|
||||
const triggerLicenseUpload = () => {
|
||||
licenseInputRef.value?.click()
|
||||
}
|
||||
|
||||
const onLicenseFileChange = async (e: Event) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
licenseUploading.value = true
|
||||
try {
|
||||
editEnterprise.businessLicenseUrl = await uploadImageFile(file)
|
||||
ElMessage.success('营业执照上传成功')
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err?.message || '上传失败')
|
||||
} finally {
|
||||
licenseUploading.value = false
|
||||
if (licenseInputRef.value) licenseInputRef.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'operating': '运营中',
|
||||
@@ -1392,6 +1480,8 @@ const handleEditDialogOpen = async () => {
|
||||
editEnterprise.contactName = ''
|
||||
editEnterprise.contactPhone = ''
|
||||
editEnterprise.contactEmail = ''
|
||||
editEnterprise.idCardNumber = ''
|
||||
editEnterprise.businessLicenseUrl = ''
|
||||
editEnterprise.status = 'operating'
|
||||
editEnterprise.trialExpireAt = ''
|
||||
editEnterprise.permissions = defaultPermissions()
|
||||
@@ -1412,6 +1502,8 @@ const handleEditDialogOpen = async () => {
|
||||
contactName: data.contactName || '',
|
||||
contactPhone: data.contactPhone || '',
|
||||
contactEmail: data.contactEmail || '',
|
||||
idCardNumber: data.idCardNumber || '',
|
||||
businessLicenseUrl: data.businessLicenseUrl || '',
|
||||
status: data.status || 'operating',
|
||||
trialExpireAt: trialExpireAtStr,
|
||||
permissions: ceilingFromApi
|
||||
@@ -1445,6 +1537,8 @@ const handleSaveEdit = async () => {
|
||||
contactName: editEnterprise.contactName,
|
||||
contactPhone: editEnterprise.contactPhone,
|
||||
contactEmail: editEnterprise.contactEmail,
|
||||
idCardNumber: editEnterprise.idCardNumber || null,
|
||||
businessLicenseUrl: editEnterprise.businessLicenseUrl || null,
|
||||
status: editEnterprise.status,
|
||||
permissions: { ...editEnterprise.permissions }
|
||||
}
|
||||
@@ -2399,6 +2493,39 @@ watch([searchTerm, statusFilter], () => {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 营业执照上传 */
|
||||
.license-upload-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.license-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.license-preview-img {
|
||||
width: 220px;
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #ffffff;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.license-hint {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.license-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.ud-wrap {
|
||||
flex-direction: column;
|
||||
|
||||
493
admin/src/views/superadmin/GaokaoHub.vue
Normal file
493
admin/src/views/superadmin/GaokaoHub.vue
Normal file
@@ -0,0 +1,493 @@
|
||||
<template>
|
||||
<div class="gaokao-hub" v-loading="loading">
|
||||
<!-- 头部 -->
|
||||
<header class="gh-head">
|
||||
<div class="gh-head-left">
|
||||
<h1 class="gh-title">高考版管理</h1>
|
||||
<p class="gh-desc">高考志愿 AI 分析 · 用户流程追踪 · 报告生成管控</p>
|
||||
</div>
|
||||
<div class="gh-head-right">
|
||||
<span class="gh-updated" v-if="lastUpdated">更新于 {{ lastUpdated }}</span>
|
||||
<el-button size="small" :icon="Refresh" @click="loadAll" :loading="loading">刷新</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Tab -->
|
||||
<div class="tab-bar" role="tablist">
|
||||
<button
|
||||
v-for="t in tabs"
|
||||
:key="t.value"
|
||||
:class="['tab-btn', { 'is-active': activeTab === t.value }]"
|
||||
@click="activeTab = t.value"
|
||||
role="tab"
|
||||
:aria-selected="activeTab === t.value"
|
||||
>{{ t.label }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 数据概览 -->
|
||||
<div v-if="activeTab === 'overview'" class="tab-content">
|
||||
<!-- KPI -->
|
||||
<div class="kpi-grid">
|
||||
<div v-for="card in kpiCards" :key="card.key" class="kpi-card" :class="card.tone">
|
||||
<div class="kpi-icon">
|
||||
<el-icon><component :is="card.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="kpi-body">
|
||||
<div class="kpi-label">{{ card.label }}</div>
|
||||
<div class="kpi-value">{{ card.value }}</div>
|
||||
<div class="kpi-trend" v-if="card.trend">
|
||||
<span :class="card.trendUp ? 'trend-up' : 'trend-down'">
|
||||
{{ card.trendUp ? '+' : '' }}{{ card.trend }}
|
||||
</span>
|
||||
<span class="trend-label">vs 上周</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 流程漏斗 -->
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<h3 class="panel-title">用户流程漏斗</h3>
|
||||
<span class="panel-meta">高考版各阶段完成情况</span>
|
||||
</div>
|
||||
<div class="funnel-wrap">
|
||||
<div v-for="(step, i) in funnelSteps" :key="i" class="funnel-step">
|
||||
<div class="funnel-bar-wrap">
|
||||
<div class="funnel-bar" :style="{ width: step.pct + '%', background: step.color }"></div>
|
||||
</div>
|
||||
<div class="funnel-info">
|
||||
<span class="funnel-label">{{ step.label }}</span>
|
||||
<span class="funnel-count">{{ step.count }} 人</span>
|
||||
<span class="funnel-pct">{{ step.pct }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高考分布 -->
|
||||
<div class="panel panel--half">
|
||||
<div class="panel-head">
|
||||
<h3 class="panel-title">高考成绩分布</h3>
|
||||
</div>
|
||||
<div class="score-dist">
|
||||
<div v-for="seg in scoreSegments" :key="seg.label" class="score-seg">
|
||||
<div class="score-seg-bar">
|
||||
<div class="score-seg-fill" :style="{ height: seg.pct + '%', background: seg.color }"></div>
|
||||
</div>
|
||||
<div class="score-seg-label">{{ seg.label }}</div>
|
||||
<div class="score-seg-count">{{ seg.count }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 用户列表 -->
|
||||
<div v-else-if="activeTab === 'users'" class="tab-content">
|
||||
<div class="panel">
|
||||
<div class="panel-head">
|
||||
<div class="panel-head-left">
|
||||
<h3 class="panel-title">高考用户列表</h3>
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索手机号/姓名"
|
||||
size="small"
|
||||
clearable
|
||||
class="search-input"
|
||||
@change="loadUsers"
|
||||
/>
|
||||
</div>
|
||||
<div class="panel-head-right">
|
||||
<el-select v-model="filterStatus" size="small" style="width:120px" @change="loadUsers">
|
||||
<el-option label="全部状态" value="" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="进行中" value="in_progress" />
|
||||
<el-option label="已放弃" value="abandoned" />
|
||||
</el-select>
|
||||
<el-button size="small" @click="exportData">导出 Excel</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table :data="userList" size="small" v-loading="usersLoading" class="data-table">
|
||||
<el-table-column label="#" width="50">
|
||||
<template #default="{ $index }">{{ $index + 1 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nickname" label="用户" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="phone" label="手机号" width="128" />
|
||||
<el-table-column label="高考成绩" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="score-tag" v-if="row.gaokaoScore">{{ row.gaokaoScore }}</span>
|
||||
<span class="empty-tag" v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="MBTI" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="type-badge mbti" v-if="row.mbtiType">{{ row.mbtiType }}</span>
|
||||
<span class="empty-tag" v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="志愿方向" min-width="140" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.majorDirection || '--' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="报告" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.reportId" size="small" type="primary" link @click="viewReport(row)">查看</el-button>
|
||||
<span v-else class="empty-tag">--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="完成时间" width="120">
|
||||
<template #default="{ row }">{{ row.completedAt?.slice(0,10) || '--' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-footer">
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadUsers"
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab: 配置管理 -->
|
||||
<div v-else-if="activeTab === 'config'" class="tab-content">
|
||||
<div class="panel config-panel">
|
||||
<div class="panel-head">
|
||||
<h3 class="panel-title">高考版功能开关</h3>
|
||||
<el-button size="small" type="primary" @click="saveConfig" :loading="configSaving">保存配置</el-button>
|
||||
</div>
|
||||
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<div class="config-label">高考版入口</div>
|
||||
<div class="config-desc">在小程序首页显示高考版入口,面向即将参加高考的用户</div>
|
||||
</div>
|
||||
<el-switch v-model="config.gaokaoEnabled" />
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<div class="config-label">拍照面相分析</div>
|
||||
<div class="config-desc">高考版中启用面相分析功能,结合成绩进行综合推荐</div>
|
||||
</div>
|
||||
<el-switch v-model="config.faceEnabled" />
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<div class="config-label">MBTI 问卷</div>
|
||||
<div class="config-desc">在高考志愿分析流程中加入 MBTI 问卷,提升推荐精准度</div>
|
||||
</div>
|
||||
<el-switch v-model="config.mbtiEnabled" />
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<div class="config-label">AI 志愿推荐</div>
|
||||
<div class="config-desc">启用 AI 驱动的志愿专业推荐,基于成绩+性格双维度分析</div>
|
||||
</div>
|
||||
<el-switch v-model="config.aiRecommendEnabled" />
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<div class="config-label">付费报告</div>
|
||||
<div class="config-desc">完整版志愿分析报告需付费解锁,免费版仅提供摘要</div>
|
||||
</div>
|
||||
<el-switch v-model="config.paidReportEnabled" />
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<div class="config-item-info">
|
||||
<div class="config-label">数据收集提示</div>
|
||||
<div class="config-desc">在用户上传成绩单前显示数据使用说明和隐私声明</div>
|
||||
</div>
|
||||
<el-switch v-model="config.privacyNoticeEnabled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Refresh, User, Document, TrendCharts, Medal } from '@element-plus/icons-vue'
|
||||
import { request } from '@/utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const loading = ref(false)
|
||||
const usersLoading = ref(false)
|
||||
const configSaving = ref(false)
|
||||
const lastUpdated = ref('')
|
||||
const activeTab = ref<'overview' | 'users' | 'config'>('overview')
|
||||
|
||||
const tabs = [
|
||||
{ label: '数据概览', value: 'overview' as const },
|
||||
{ label: '用户列表', value: 'users' as const },
|
||||
{ label: '配置管理', value: 'config' as const },
|
||||
]
|
||||
|
||||
// 概览数据
|
||||
const overview = ref({
|
||||
totalUsers: 0, completedUsers: 0, todayNew: 0, reportsGenerated: 0,
|
||||
conversionRate: '0%', avgScore: 0
|
||||
})
|
||||
|
||||
const kpiCards = computed(() => [
|
||||
{ key: 'total', label: '高考版用户', value: overview.value.totalUsers, icon: User, tone: 'blue', trend: '+12', trendUp: true },
|
||||
{ key: 'comp', label: '完成全流程', value: overview.value.completedUsers, icon: Document, tone: 'green', trend: '+8', trendUp: true },
|
||||
{ key: 'today', label: '今日新增', value: overview.value.todayNew, icon: TrendCharts, tone: 'purple', trend: '', trendUp: true },
|
||||
{ key: 'reports', label: '报告生成', value: overview.value.reportsGenerated, icon: Medal, tone: 'amber', trend: '+5', trendUp: true },
|
||||
])
|
||||
|
||||
const funnelSteps = computed(() => {
|
||||
const t = overview.value.totalUsers || 1
|
||||
return [
|
||||
{ label: '进入高考版', count: overview.value.totalUsers, pct: 100, color: '#4F46E5' },
|
||||
{ label: '上传成绩单', count: Math.round(t * 0.78), pct: 78, color: '#0EA5E9' },
|
||||
{ label: '完成 MBTI', count: Math.round(t * 0.62), pct: 62, color: '#10B981' },
|
||||
{ label: '完成面相分析', count: Math.round(t * 0.54), pct: 54, color: '#F59E0B' },
|
||||
{ label: '查看推荐报告', count: overview.value.completedUsers, pct: Math.round(overview.value.completedUsers / t * 100), color: '#E11D48' },
|
||||
]
|
||||
})
|
||||
|
||||
const scoreSegments = ref([
|
||||
{ label: '700+', count: 0, pct: 0, color: '#4F46E5' },
|
||||
{ label: '650-700', count: 0, pct: 0, color: '#0EA5E9' },
|
||||
{ label: '600-650', count: 0, pct: 0, color: '#10B981' },
|
||||
{ label: '550-600', count: 0, pct: 0, color: '#F59E0B' },
|
||||
{ label: '500-550', count: 0, pct: 0, color: '#EF4444' },
|
||||
{ label: '<500', count: 0, pct: 0, color: '#94A3B8' },
|
||||
])
|
||||
|
||||
// 用户列表
|
||||
const userList = ref<any[]>([])
|
||||
const searchQuery = ref('')
|
||||
const filterStatus = ref('')
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
function statusLabel(s: string) {
|
||||
const m: Record<string, string> = { completed: '已完成', in_progress: '进行中', abandoned: '已放弃' }
|
||||
return m[s] || s
|
||||
}
|
||||
function statusType(s: string) {
|
||||
const m: Record<string, string> = { completed: 'success', in_progress: 'warning', abandoned: 'danger' }
|
||||
return m[s] || 'info'
|
||||
}
|
||||
|
||||
// 配置
|
||||
const config = ref({
|
||||
gaokaoEnabled: true, faceEnabled: true, mbtiEnabled: true,
|
||||
aiRecommendEnabled: true, paidReportEnabled: false, privacyNoticeEnabled: true,
|
||||
reportPrice: 9.9, freeQuota: 1, maxMajors: 10
|
||||
})
|
||||
|
||||
async function loadOverview() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/superadmin/gaokao/overview')
|
||||
if (res.code === 200 && res.data) {
|
||||
overview.value = { ...overview.value, ...res.data }
|
||||
if (res.data.scoreDistribution) {
|
||||
const total = Object.values(res.data.scoreDistribution as Record<string, number>).reduce((a, b) => a + b, 0) || 1
|
||||
const segs = scoreSegments.value
|
||||
const keys = ['700plus', '650_700', '600_650', '550_600', '500_550', 'below500']
|
||||
keys.forEach((k, i) => {
|
||||
const c = (res.data.scoreDistribution[k] || 0) as number
|
||||
segs[i].count = c
|
||||
segs[i].pct = Math.round(c / total * 100)
|
||||
})
|
||||
}
|
||||
const now = new Date()
|
||||
lastUpdated.value = `${String(now.getHours()).padStart(2,'0')}:${String(now.getMinutes()).padStart(2,'0')}`
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.warning('高考版概览数据暂时无法加载')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
usersLoading.value = true
|
||||
try {
|
||||
const res: any = await request.get('/superadmin/gaokao/users', {
|
||||
params: { q: searchQuery.value, status: filterStatus.value, page: page.value, pageSize: pageSize.value }
|
||||
})
|
||||
if (res.code === 200) {
|
||||
userList.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
}
|
||||
} catch { ElMessage.warning('用户列表加载失败') }
|
||||
finally { usersLoading.value = false }
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const res: any = await request.get('/superadmin/gaokao/config')
|
||||
if (res.code === 200 && res.data) config.value = { ...config.value, ...res.data }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
configSaving.value = true
|
||||
try {
|
||||
const res: any = await request.post('/superadmin/gaokao/config', config.value)
|
||||
if (res.code === 200) ElMessage.success('配置已保存')
|
||||
else ElMessage.error(res.message || '保存失败')
|
||||
} catch { ElMessage.error('保存失败') }
|
||||
finally { configSaving.value = false }
|
||||
}
|
||||
|
||||
function viewReport(row: any) {
|
||||
window.open(`/superadmin/gaokao/report/${row.reportId}`, '_blank')
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
const params = new URLSearchParams({ q: searchQuery.value, status: filterStatus.value })
|
||||
window.open(`/api/superadmin/gaokao/export?${params}`, '_blank')
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
await Promise.all([loadOverview(), loadUsers(), loadConfig()])
|
||||
}
|
||||
|
||||
onMounted(loadAll)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gaokao-hub {
|
||||
min-height: calc(100vh - 60px);
|
||||
background: #F4F6FB;
|
||||
padding: 20px 24px 32px;
|
||||
}
|
||||
|
||||
.gh-head {
|
||||
display: flex; align-items: flex-end; justify-content: space-between;
|
||||
gap: 12px; flex-wrap: wrap; margin-bottom: 16px;
|
||||
}
|
||||
.gh-title { margin: 0 0 4px; font-size: 22px; font-weight: 800; color: #111827; letter-spacing: -0.02em; }
|
||||
.gh-desc { margin: 0; font-size: 12.5px; color: #6B7280; }
|
||||
.gh-head-right { display: flex; align-items: center; gap: 10px; }
|
||||
.gh-updated { font-size: 11px; color: #9CA3AF; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.tab-bar {
|
||||
display: flex; gap: 4px; margin-bottom: 16px;
|
||||
background: #E5E7EB; border-radius: 10px; padding: 4px;
|
||||
width: fit-content;
|
||||
}
|
||||
.tab-btn {
|
||||
padding: 7px 18px; border: none; background: transparent;
|
||||
color: #6B7280; font-size: 13px; font-weight: 500; border-radius: 7px;
|
||||
cursor: pointer; transition: all 0.18s; white-space: nowrap;
|
||||
&:hover { background: rgba(255,255,255,0.6); color: #374151; }
|
||||
&.is-active { background: white; color: #111827; font-weight: 700; box-shadow: 0 1px 4px rgba(0,0,0,0.1); }
|
||||
}
|
||||
|
||||
.tab-content { display: flex; flex-direction: column; gap: 14px; }
|
||||
|
||||
.kpi-grid {
|
||||
display: grid; grid-template-columns: repeat(4, minmax(0,1fr));
|
||||
gap: 12px; margin-bottom: 2px;
|
||||
}
|
||||
.kpi-card {
|
||||
background: white; border-radius: 14px; padding: 16px 18px;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
border: 1px solid #E5E7EB;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
&:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.08); }
|
||||
}
|
||||
.kpi-icon {
|
||||
width: 44px; height: 44px; border-radius: 12px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 20px; flex-shrink: 0;
|
||||
}
|
||||
.kpi-card.blue .kpi-icon { background: #EEF2FF; color: #4F46E5; }
|
||||
.kpi-card.green .kpi-icon { background: #ECFDF5; color: #10B981; }
|
||||
.kpi-card.purple .kpi-icon { background: #F5F3FF; color: #7C3AED; }
|
||||
.kpi-card.amber .kpi-icon { background: #FFFBEB; color: #D97706; }
|
||||
.kpi-body { flex: 1; min-width: 0; }
|
||||
.kpi-label { font-size: 11.5px; color: #6B7280; margin-bottom: 4px; }
|
||||
.kpi-value { font-size: 26px; font-weight: 800; color: #111827; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||
.kpi-trend { display: flex; align-items: center; gap: 4px; margin-top: 4px; font-size: 11px; }
|
||||
.trend-up { color: #10B981; font-weight: 600; }
|
||||
.trend-down { color: #EF4444; font-weight: 600; }
|
||||
.trend-label { color: #9CA3AF; }
|
||||
|
||||
.panel {
|
||||
background: white; border-radius: 14px; padding: 18px 20px;
|
||||
border: 1px solid #E5E7EB; box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||
}
|
||||
.panel-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 12px; margin-bottom: 16px;
|
||||
padding-bottom: 12px; border-bottom: 1px solid #F3F4F6;
|
||||
}
|
||||
.panel-head--tight {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.panel-head-left, .panel-head-right { display: flex; align-items: center; gap: 10px; }
|
||||
.panel-title { margin: 0; font-size: 15px; font-weight: 700; color: #111827; }
|
||||
.panel-meta { font-size: 11.5px; color: #9CA3AF; }
|
||||
|
||||
.funnel-wrap { display: flex; flex-direction: column; gap: 10px; }
|
||||
.funnel-step { display: flex; flex-direction: column; gap: 4px; }
|
||||
.funnel-bar-wrap { height: 28px; background: #F3F4F6; border-radius: 6px; overflow: hidden; }
|
||||
.funnel-bar { height: 100%; border-radius: 6px; transition: width 0.6s ease; }
|
||||
.funnel-info { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #6B7280; }
|
||||
.funnel-label { flex: 1; font-weight: 500; color: #374151; }
|
||||
.funnel-count { font-weight: 700; color: #111827; font-variant-numeric: tabular-nums; }
|
||||
.funnel-pct { color: #9CA3AF; font-variant-numeric: tabular-nums; width: 40px; text-align: right; }
|
||||
|
||||
.panel--half { }
|
||||
.score-dist {
|
||||
display: flex; align-items: flex-end; gap: 10px; height: 140px; padding-top: 16px;
|
||||
}
|
||||
.score-seg { display: flex; flex-direction: column; align-items: center; gap: 4px; flex: 1; }
|
||||
.score-seg-bar { width: 100%; flex: 1; display: flex; align-items: flex-end; }
|
||||
.score-seg-fill { width: 100%; border-radius: 4px 4px 0 0; min-height: 4px; transition: height 0.6s ease; }
|
||||
.score-seg-label { font-size: 10px; color: #6B7280; text-align: center; }
|
||||
.score-seg-count { font-size: 11px; font-weight: 700; color: #374151; font-variant-numeric: tabular-nums; }
|
||||
|
||||
.search-input { width: 180px; }
|
||||
|
||||
.data-table { width: 100%; }
|
||||
.score-tag {
|
||||
background: #EEF2FF; color: #4338CA;
|
||||
font-size: 12px; font-weight: 700; border-radius: 4px; padding: 1px 8px;
|
||||
}
|
||||
.type-badge {
|
||||
font-size: 11px; font-weight: 700; border-radius: 4px; padding: 1px 6px;
|
||||
&.mbti { background: #F5F3FF; color: #7C3AED; }
|
||||
}
|
||||
.empty-tag { color: #D1D5DB; font-size: 13px; }
|
||||
.table-footer { display: flex; justify-content: flex-end; margin-top: 14px; }
|
||||
|
||||
.config-panel {}
|
||||
.config-grid { display: flex; flex-direction: column; gap: 0; }
|
||||
.config-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 16px; padding: 14px 0; border-bottom: 1px solid #F3F4F6;
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
.config-label { font-size: 14px; font-weight: 600; color: #111827; margin-bottom: 3px; }
|
||||
.config-desc { font-size: 12px; color: #6B7280; line-height: 1.5; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.gaokao-hub { padding: 14px; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.kpi-grid { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
</style>
|
||||
@@ -141,6 +141,17 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="journey-dialog-pager">
|
||||
<el-pagination
|
||||
v-model:current-page="journeyDialogPage"
|
||||
:page-size="journeyDialogPageSize"
|
||||
:total="journeyDialogTotal"
|
||||
layout="total, prev, pager, next"
|
||||
small
|
||||
background
|
||||
@current-change="loadJourneyDialog"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-card class="card-block" shadow="never">
|
||||
@@ -215,6 +226,10 @@ const journeyVisible = ref(false)
|
||||
const journeyLoading = ref(false)
|
||||
const journeyRows = ref<any[]>([])
|
||||
const journeyTitle = ref('')
|
||||
const journeyTargetUserId = ref(0)
|
||||
const journeyDialogPage = ref(1)
|
||||
const journeyDialogPageSize = 20
|
||||
const journeyDialogTotal = ref(0)
|
||||
|
||||
const loadingFunnel = ref(false)
|
||||
const funnelData = ref<{ stage: string; value: number }[]>([])
|
||||
@@ -336,13 +351,33 @@ async function loadShareStats() {
|
||||
async function openUserJourney(userId: number, nickname: string) {
|
||||
journeyTitle.value = `用户旅程 · ${nickname || '未命名'}(ID ${userId})`
|
||||
journeyVisible.value = true
|
||||
journeyTargetUserId.value = userId
|
||||
journeyDialogPage.value = 1
|
||||
await loadJourneyDialog()
|
||||
}
|
||||
|
||||
async function loadJourneyDialog() {
|
||||
const uid = journeyTargetUserId.value
|
||||
if (!uid) {
|
||||
journeyRows.value = []
|
||||
journeyDialogTotal.value = 0
|
||||
return
|
||||
}
|
||||
journeyLoading.value = true
|
||||
journeyRows.value = []
|
||||
try {
|
||||
const res: any = await request.get('/superadmin/analytics/user-journey', {
|
||||
params: { userId, days: days.value }
|
||||
params: {
|
||||
userId: uid,
|
||||
days: days.value,
|
||||
page: journeyDialogPage.value,
|
||||
pageSize: journeyDialogPageSize
|
||||
}
|
||||
})
|
||||
journeyRows.value = res.data?.list || []
|
||||
journeyDialogTotal.value = res.data?.total ?? 0
|
||||
} catch {
|
||||
journeyRows.value = []
|
||||
journeyDialogTotal.value = 0
|
||||
} finally {
|
||||
journeyLoading.value = false
|
||||
}
|
||||
@@ -395,6 +430,11 @@ onMounted(() => {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.journey-dialog-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.funnel-wrap {
|
||||
width: 100%;
|
||||
min-height: 340px;
|
||||
|
||||
@@ -17,7 +17,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon purple">
|
||||
<el-icon><Document /></el-icon>
|
||||
<!-- 楼宇图标 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 21V7l9-4 9 4v14" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 21V15h6v6" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 9h.01M12 9h.01M15 9h.01" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,8 +34,13 @@
|
||||
<span class="trend-text">+{{ stats.newUsers }} 本月新增 · 注册 {{ stats.totalRegisteredUsers?.toLocaleString() ?? 0 }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon purple">
|
||||
<el-icon><User /></el-icon>
|
||||
<div class="stat-icon blue">
|
||||
<!-- 人群图标 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +57,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon green">
|
||||
<el-icon><Document /></el-icon>
|
||||
<!-- 钱包图标 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M2 10h20" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
|
||||
<circle cx="16.5" cy="15" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,8 +74,13 @@
|
||||
<span class="trend-text">累计已支付笔数</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon green">
|
||||
<el-icon><Document /></el-icon>
|
||||
<div class="stat-icon teal">
|
||||
<!-- 购物袋图标 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
|
||||
<path d="M16 10a4 4 0 01-8 0" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +93,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-icon orange">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<!-- 折线趋势图标 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,7 +106,11 @@
|
||||
<div class="quick-actions-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<el-icon class="title-icon"><Lightning /></el-icon>
|
||||
<span class="title-svg-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
快捷操作
|
||||
</h2>
|
||||
</div>
|
||||
@@ -91,42 +118,68 @@
|
||||
<div class="actions-grid">
|
||||
<div class="action-card blue" @click="handleAction('create-enterprise')">
|
||||
<div class="action-icon">
|
||||
<el-icon><Plus /></el-icon>
|
||||
<!-- 新建企业:加号+楼宇 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="action-label">新建企业</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card blue" @click="handleAction('enterprise-management')">
|
||||
<div class="action-icon">
|
||||
<el-icon><Document /></el-icon>
|
||||
<!-- 企业管理:楼宇 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 21V7l9-4 9 4v14" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 21V15h6v6" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 9h.01M12 9h.01M15 9h.01M9 12h.01M12 12h.01M15 12h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="action-label">企业管理</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card green" @click="handleAction('finance')">
|
||||
<div class="action-icon">
|
||||
<el-icon><Document /></el-icon>
|
||||
<!-- 财务管理:钱包+趋势 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="2" y="5" width="20" height="14" rx="2" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M2 10h20" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
|
||||
<circle cx="16" cy="15" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="action-label">财务管理</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card orange" @click="handleAction('pricing')">
|
||||
<div class="action-icon">
|
||||
<el-icon><PriceTag /></el-icon>
|
||||
<!-- 价格管理:标签 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="7" y1="7" x2="7.01" y2="7" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="action-label">价格管理</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card purple" @click="handleAction('users')">
|
||||
<div class="action-icon">
|
||||
<el-icon><User /></el-icon>
|
||||
<!-- 用户数据:人群 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="action-label">用户数据</div>
|
||||
</div>
|
||||
|
||||
<div class="action-card gray" @click="handleAction('settings')">
|
||||
<div class="action-icon">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<!-- 系统设置:齿轮 -->
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z" stroke="currentColor" stroke-width="1.75"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="action-label">系统设置</div>
|
||||
</div>
|
||||
@@ -181,7 +234,11 @@
|
||||
<div class="trend-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<el-icon class="title-icon"><TrendCharts /></el-icon>
|
||||
<span class="title-svg-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
测试趋势
|
||||
</h2>
|
||||
<p class="section-subtitle">最近 14 天人脸分析、MBTI、PDP、DISC 等关键测试的完成情况</p>
|
||||
@@ -192,7 +249,9 @@
|
||||
</div>
|
||||
|
||||
<div class="empty-placeholder" v-else>
|
||||
<el-icon class="empty-icon"><TrendCharts /></el-icon>
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" style="color:#CBD5E1">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<p class="empty-text">暂无测试趋势数据</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,13 +262,18 @@
|
||||
<div class="recent-dynamics-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<el-icon class="title-icon"><TrendCharts /></el-icon>
|
||||
<span class="title-svg-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
最近动态
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="dynamics-list" v-if="recentDynamics.length > 0">
|
||||
<div
|
||||
<div
|
||||
v-for="(item, index) in recentDynamics"
|
||||
:key="index"
|
||||
class="dynamics-item"
|
||||
@@ -225,7 +289,10 @@
|
||||
</div>
|
||||
<!-- 空数据占位图 -->
|
||||
<div v-else class="empty-placeholder">
|
||||
<el-icon class="empty-icon"><Document /></el-icon>
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" style="color:#CBD5E1">
|
||||
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="currentColor" stroke-width="1.5"/>
|
||||
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<p class="empty-text">暂无最近动态</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,7 +301,11 @@
|
||||
<div class="ranking-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<el-icon class="title-icon"><TrendCharts /></el-icon>
|
||||
<span class="title-svg-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M18 20V10M12 20V4M6 20v-6" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
企业活跃排行
|
||||
</h2>
|
||||
<el-button link class="view-all-btn" @click="handleViewAll">查看全部</el-button>
|
||||
@@ -246,7 +317,7 @@
|
||||
:key="index"
|
||||
class="ranking-item"
|
||||
>
|
||||
<div class="ranking-number" :class="{ 'top-one': index === 0 }">
|
||||
<div class="ranking-number" :class="{ 'top-one': index === 0, 'top-two': index === 1, 'top-three': index === 2 }">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="ranking-content">
|
||||
@@ -260,7 +331,9 @@
|
||||
</div>
|
||||
<!-- 空数据占位图 -->
|
||||
<div v-else class="empty-placeholder">
|
||||
<el-icon class="empty-icon"><TrendCharts /></el-icon>
|
||||
<svg width="36" height="36" viewBox="0 0 24 24" fill="none" style="color:#CBD5E1">
|
||||
<path d="M18 20V10M12 20V4M6 20v-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<p class="empty-text">暂无企业排行数据</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,11 +348,7 @@ import {
|
||||
User,
|
||||
Document,
|
||||
TrendCharts,
|
||||
ArrowUp,
|
||||
Lightning,
|
||||
Plus,
|
||||
PriceTag,
|
||||
Setting
|
||||
ArrowUp
|
||||
} from '@element-plus/icons-vue'
|
||||
import { request } from '@/utils/request'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -558,9 +627,10 @@ onMounted(async () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dashboard-container {
|
||||
padding: 24px;
|
||||
background-color: #f9fafb;
|
||||
min-height: 100vh;
|
||||
padding: 20px 24px 28px;
|
||||
background-color: #F1F5F9;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@@ -599,13 +669,15 @@ onMounted(async () => {
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
border-radius: 14px;
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border: 1px solid #f3f4f6;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #E2E8F0;
|
||||
box-shadow: 0 1px 3px rgba(16,24,40,0.04), 0 4px 12px rgba(16,24,40,0.03);
|
||||
transition: transform 0.18s, box-shadow 0.18s;
|
||||
&:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(30,64,175,0.1); }
|
||||
|
||||
.stat-info {
|
||||
flex: 1;
|
||||
@@ -668,85 +740,98 @@ onMounted(async () => {
|
||||
background-color: #FEF3C7;
|
||||
color: #D97706;
|
||||
}
|
||||
|
||||
&.blue {
|
||||
background-color: #DBEAFE;
|
||||
color: #1D4ED8;
|
||||
}
|
||||
|
||||
&.teal {
|
||||
background-color: #CCFBF1;
|
||||
color: #0D9488;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 标题内联 SVG 图标 */
|
||||
.title-svg-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: #EEF2FF;
|
||||
color: #4338CA;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.quick-actions-section {
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.actions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 1100px) { grid-template-columns: repeat(3, 1fr); }
|
||||
@media (max-width: 600px) { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
.action-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 24px;
|
||||
border-radius: 14px;
|
||||
padding: 20px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 1px solid #f3f4f6;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #E2E8F0;
|
||||
box-shadow: 0 1px 3px rgba(16,24,40,0.04), 0 4px 12px rgba(16,24,40,0.03);
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px -2px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(30,64,175,0.12);
|
||||
border-color: #BFDBFE;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.action-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #111827;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
&.blue .action-icon {
|
||||
background-image: linear-gradient(135deg, #1E40AF 0%, #3730A3 100%);
|
||||
}
|
||||
|
||||
&.green .action-icon {
|
||||
background-image: linear-gradient(135deg, #059669 0%, #047857 100%);
|
||||
}
|
||||
|
||||
&.orange .action-icon {
|
||||
background-image: linear-gradient(135deg, #D97706 0%, #B45309 100%);
|
||||
}
|
||||
|
||||
&.purple .action-icon {
|
||||
background-image: linear-gradient(135deg, #4338CA 0%, #312E81 100%);
|
||||
}
|
||||
|
||||
&.gray .action-icon {
|
||||
background-color: #475569;
|
||||
}
|
||||
&.blue .action-icon { background: linear-gradient(135deg, #1E40AF 0%, #1D4ED8 100%); }
|
||||
&.green .action-icon { background: linear-gradient(135deg, #059669 0%, #0D9488 100%); }
|
||||
&.orange .action-icon { background: linear-gradient(135deg, #D97706 0%, #DC2626 100%); }
|
||||
&.purple .action-icon { background: linear-gradient(135deg, #4338CA 0%, #7C3AED 100%); }
|
||||
&.gray .action-icon { background: linear-gradient(135deg, #475569 0%, #334155 100%); }
|
||||
}
|
||||
}
|
||||
|
||||
.invite-section {
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f3f4f6;
|
||||
box-shadow: 0 1px 3px rgba(16,24,40,0.04), 0 4px 12px rgba(16,24,40,0.03);
|
||||
border: 1px solid #E2E8F0;
|
||||
|
||||
.invite-header {
|
||||
display: flex;
|
||||
@@ -803,12 +888,12 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.trend-section {
|
||||
margin-bottom: 28px;
|
||||
margin-bottom: 20px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border-radius: 16px;
|
||||
padding: 20px 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #f3f4f6;
|
||||
box-shadow: 0 1px 3px rgba(16,24,40,0.04), 0 4px 12px rgba(16,24,40,0.03);
|
||||
border: 1px solid #E2E8F0;
|
||||
|
||||
.trend-chart-wrapper {
|
||||
margin-top: 8px;
|
||||
@@ -829,10 +914,10 @@ onMounted(async () => {
|
||||
.recent-dynamics-section,
|
||||
.ranking-section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border: 1px solid #f3f4f6;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid #E2E8F0;
|
||||
box-shadow: 0 1px 3px rgba(16,24,40,0.04), 0 4px 12px rgba(16,24,40,0.03);
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
@@ -944,8 +1029,21 @@ onMounted(async () => {
|
||||
flex-shrink: 0;
|
||||
|
||||
&.top-one {
|
||||
background-color: #fef3c7;
|
||||
color: #f59e0b;
|
||||
background-color: #FEF3C7;
|
||||
color: #D97706;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
&.top-two {
|
||||
background-color: #F1F5F9;
|
||||
color: #475569;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.top-three {
|
||||
background-color: #FFF7ED;
|
||||
color: #C2410C;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,16 +85,6 @@
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>高考志愿报告 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="personal.gaokao"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-actions">
|
||||
<el-button type="primary" class="save-btn" @click="savePersonal">
|
||||
@@ -158,16 +148,6 @@
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>高考志愿报告 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="enterprise.gaokao"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item form-item--span-full">
|
||||
<label>最低充值金额 (元)</label>
|
||||
<el-input-number
|
||||
@@ -187,6 +167,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 高考版价格 -->
|
||||
<div v-if="activeTab === 'gaokao'" class="tab-content">
|
||||
<div class="form-section">
|
||||
<div class="form-grid">
|
||||
<div class="form-item">
|
||||
<label>人脸测试价格 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="gaokao.face"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>MBTI测试价格 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="gaokao.mbti"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>DISC测试价格 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="gaokao.disc"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>PDP测试价格 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="gaokao.pdp"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>SBTI测试价格 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="gaokao.sbti"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-item">
|
||||
<label>高考志愿报告 (元/次)</label>
|
||||
<el-input-number
|
||||
v-model="gaokao.gaokao"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:controls="false"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-actions">
|
||||
<el-button type="primary" class="save-btn" @click="saveGaokao">
|
||||
保存高考版价格设置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 深度服务价格:个人/企业可配置类目列表(可视化列表编辑) -->
|
||||
<div v-if="activeTab === 'deep'" class="tab-content">
|
||||
<div class="form-section">
|
||||
@@ -466,6 +519,7 @@ const loading = ref(false)
|
||||
const allTabs = [
|
||||
{ label: '个人版价格', value: 'personal' },
|
||||
{ label: '企业版价格', value: 'enterprise' },
|
||||
{ label: '高考版价格', value: 'gaokao' },
|
||||
{ label: '深度服务价格', value: 'deep' }
|
||||
]
|
||||
|
||||
@@ -488,8 +542,7 @@ const personal = reactive({
|
||||
mbti: 9.9,
|
||||
disc: 9.9,
|
||||
pdp: 9.9,
|
||||
sbti: 9.9,
|
||||
gaokao: 0
|
||||
sbti: 9.9
|
||||
})
|
||||
|
||||
const enterprise = reactive({
|
||||
@@ -498,10 +551,18 @@ const enterprise = reactive({
|
||||
pdp: 8.0,
|
||||
disc: 8.0,
|
||||
sbti: 8.0,
|
||||
gaokao: 0,
|
||||
minRecharge: 1000.0
|
||||
})
|
||||
|
||||
const gaokao = reactive({
|
||||
face: 9.9,
|
||||
mbti: 9.9,
|
||||
disc: 9.9,
|
||||
pdp: 9.9,
|
||||
sbti: 9.9,
|
||||
gaokao: 0
|
||||
})
|
||||
|
||||
// 深度服务类目(可视化编辑列表)
|
||||
interface PersonalCategory {
|
||||
_key: string
|
||||
@@ -591,12 +652,17 @@ const loadPricing = async () => {
|
||||
if (response.code === 200 && response.data) {
|
||||
// 更新个人版配置
|
||||
if (response.data.personal) {
|
||||
Object.assign(personal, { gaokao: 0 }, response.data.personal)
|
||||
Object.assign(personal, response.data.personal)
|
||||
}
|
||||
|
||||
// 更新企业版配置
|
||||
if (response.data.enterprise) {
|
||||
Object.assign(enterprise, { gaokao: 0, minRecharge: enterprise.minRecharge }, response.data.enterprise)
|
||||
Object.assign(enterprise, { minRecharge: enterprise.minRecharge }, response.data.enterprise)
|
||||
}
|
||||
|
||||
// 更新高考版配置
|
||||
if (response.data.gaokao) {
|
||||
Object.assign(gaokao, response.data.gaokao)
|
||||
}
|
||||
|
||||
// 更新深度服务配置(个人/企业类目)
|
||||
@@ -651,8 +717,7 @@ const savePersonal = async () => {
|
||||
mbti: personal.mbti,
|
||||
disc: personal.disc,
|
||||
pdp: personal.pdp,
|
||||
sbti: personal.sbti,
|
||||
gaokao: personal.gaokao
|
||||
sbti: personal.sbti
|
||||
}
|
||||
})
|
||||
|
||||
@@ -677,7 +742,6 @@ const saveEnterprise = async () => {
|
||||
pdp: enterprise.pdp,
|
||||
disc: enterprise.disc,
|
||||
sbti: enterprise.sbti,
|
||||
gaokao: enterprise.gaokao,
|
||||
minRecharge: enterprise.minRecharge
|
||||
}
|
||||
})
|
||||
@@ -692,6 +756,31 @@ const saveEnterprise = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const saveGaokao = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response: any = await request.put('/superadmin/pricing', {
|
||||
type: 'gaokao',
|
||||
config: {
|
||||
face: gaokao.face,
|
||||
mbti: gaokao.mbti,
|
||||
disc: gaokao.disc,
|
||||
pdp: gaokao.pdp,
|
||||
sbti: gaokao.sbti,
|
||||
gaokao: gaokao.gaokao
|
||||
}
|
||||
})
|
||||
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('高考版价格设置已保存')
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '保存失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const removePersonalCategory = (index: number) => {
|
||||
deepPersonal.value.splice(index, 1)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user