|
|
<template> <div class="user-overview-container"> <div class="tab-header"> <div class="tab-item" :class="{ active: activeTab === 'overview' }" @click="activeTab = 'overview'" > 数据概览 </div> <div class="tab-item" :class="{ active: activeTab === 'detail' }" @click="activeTab = 'detail'" > 数据明细 </div> </div>
<!-- 数据概览 --> <div v-show="activeTab === 'overview'" class="tab-content overview-content"> <div class="stats-row"> <!-- 用户总数 --> <div class="stat-card purple-gradient big-card"> <div class="card-title"> <el-icon><UserFilled /></el-icon> 用户总数 </div> <div class="card-value">154,838</div> <div class="card-tag up"> 较昨日增加↑ 5.22% </div> </div>
<div class="right-stats-col"> <!-- 会员总数 --> <div class="stat-card orange-gradient small-card"> <div class="top-row"> <div class="card-title"> <el-icon><Trophy /></el-icon> 会员总数 </div> <div class="card-value-small">154,838</div> </div> <div class="card-tag-wrapper"> <div class="card-tag up">较昨日增加↑ 15.22%</div> </div> </div> <!-- 非会员总数 --> <div class="stat-card blue-gradient small-card"> <div class="top-row"> <div class="card-title"> <el-icon><User /></el-icon> 非会员总数 </div> <div class="card-value-small">154,838</div> </div> <div class="card-tag-wrapper"> <div class="card-tag down">较昨日减少↓ 1.22%</div> </div> </div> </div> </div>
<!-- 用户构成比例 --> <div class="composition-section"> <div class="section-header"> <el-icon><PieChart /></el-icon> 用户构成比例 </div> <div class="charts-row"> <div class="chart-wrapper"> <div ref="chartMemberRef" class="chart-box"></div> <div class="legend-custom"> <div class="legend-item"><span class="dot green"></span>会员用户</div> <div class="legend-item"><span class="dot red"></span>非会员用户</div> </div> </div> <div class="chart-wrapper"> <div ref="chartNewOldRef" class="chart-box"></div> <div class="legend-custom"> <div class="legend-item"><span class="dot green"></span>会员用户</div> <div class="legend-item"><span class="dot red"></span>新非网数量</div> <div class="legend-item"><span class="dot blue"></span>老非网数量</div> </div> </div> </div> </div> </div>
<!-- 数据明细 --> <div v-show="activeTab === 'detail'" class="tab-content detail-content"> <!-- 搜索栏 --> <div class="search-bar"> <div class="search-label">时间段查询</div> <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" size="default" /> <el-button type="primary" class="search-btn">搜索</el-button> <el-button type="primary" class="reset-btn">重置</el-button> <el-button type="danger" class="export-btn">数据导出</el-button> </div>
<!-- 表格1: 用户构成明细 --> <div class="detail-section"> <div class="section-title"><el-icon><User /></el-icon> 用户构成明细</div> <el-table :data="tableData1" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="type" label="用户类型" width="180"> <template #default="scope"> <span :class="{ 'text-red': scope.row.type === '用户总数', 'sub-item-text': ['新非网总数', '老非网总数'].includes(scope.row.type) }">{{ scope.row.type }}</span> </template> </el-table-column> <el-table-column prop="total" label="当前总数" /> <el-table-column prop="dailyNew" label="较昨日新增"> <template #default="scope"> <span class="text-green">{{ scope.row.dailyNew }}</span> </template> </el-table-column> <el-table-column prop="weeklyNew" label="较上周新增"> <template #default="scope"> <span class="text-green">{{ scope.row.weeklyNew }}</span> </template> </el-table-column> <el-table-column prop="monthlyNew" label="较上月新增"> <template #default="scope"> <span class="text-green">{{ scope.row.monthlyNew }}</span> </template> </el-table-column> <el-table-column prop="periodNew" label="时间段新增" /> </el-table> </div>
<!-- 表格2: 新注册用户来源 --> <div class="detail-section"> <div class="section-title"><el-icon><User /></el-icon> 新注册用户来源</div> <el-table :data="tableData2" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="channel" label="来源渠道" /> <el-table-column prop="dailyNew" label="今日新增" /> <el-table-column prop="weeklyNew" label="本周新增" /> <el-table-column prop="monthlyNew" label="本月新增" /> <el-table-column prop="periodNew" label="时间段新增" /> <el-table-column prop="percent" label="占比" /> </el-table> </div>
<!-- 表格3: 老用户来源 --> <div class="detail-section"> <div class="section-title"><el-icon><User /></el-icon> 老用户来源</div> <el-table :data="tableData3" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="channel" label="来源渠道" /> <el-table-column prop="dailyNew" label="今日新增" /> <el-table-column prop="weeklyNew" label="本周新增" /> <el-table-column prop="monthlyNew" label="本月新增" /> <el-table-column prop="periodNew" label="时间段新增" /> <el-table-column prop="percent" label="占比" /> </el-table> </div>
<!-- 图表: 用户来源渠道分布 --> <div class="detail-section chart-section-bg"> <div class="section-title"><el-icon><PieChart /></el-icon> 用户来源渠道分布</div> <div ref="chartBarRef" class="bar-chart-box"></div> </div>
</div> </div></template>
<script setup>import { ref, onMounted, nextTick, watch } from 'vue';import { useRoute, useRouter } from 'vue-router';import * as echarts from 'echarts';
const route = useRoute();const router = useRouter();
const activeTab = ref(route.query.tab || 'overview');const dateRange = ref('');
const chartMemberRef = ref(null);const chartNewOldRef = ref(null);const chartBarRef = ref(null);
// 表格数据
const tableData1 = [ { type: '用户总数', total: '154,832', dailyNew: '+3.44', weeklyNew: '+21,379', monthlyNew: '+21,379', periodNew: '' }, { type: '会员总数', total: '42,567', dailyNew: '+5.56', weeklyNew: '+2,379', monthlyNew: '+2,379', periodNew: '' }, { type: '非会员总数', total: '112,265', dailyNew: '+9.32', weeklyNew: '+92,123', monthlyNew: '+92,123', periodNew: '' }, { type: '新非网总数', total: '68,420', dailyNew: '+35.34', weeklyNew: '+12,689', monthlyNew: '+12,689', periodNew: '' }, { type: '老非网总数', total: '68,420', dailyNew: '+23.45', weeklyNew: '+12,033', monthlyNew: '+12,033', periodNew: '' },];
const tableData2 = [ { channel: 'App Store', dailyNew: '154,832', weeklyNew: '+3.44', monthlyNew: '+21,379', periodNew: '', percent: '38%' }, { channel: 'Play Store', dailyNew: '42,567', weeklyNew: '+5.56', monthlyNew: '+2,379', periodNew: '', percent: '30%' }, { channel: 'H5', dailyNew: '112,265', weeklyNew: '+9.32', monthlyNew: '+92,123', periodNew: '', percent: '17%' }, { channel: 'APK', dailyNew: '68,420', weeklyNew: '+35.34', monthlyNew: '+12,689', periodNew: '', percent: '10%' }, { channel: '总计', dailyNew: '68,420', weeklyNew: '+23.45', monthlyNew: '+12,033', periodNew: '', percent: '100%' },];
const tableData3 = [ { channel: 'HC 注册过', dailyNew: '1,245', weeklyNew: '8,742', monthlyNew: '32,567', periodNew: '', percent: '38%' }, { channel: 'HC 注册过', dailyNew: '987', weeklyNew: '6,912', monthlyNew: '25,432', periodNew: '', percent: '30%' }, { channel: '海外 CRM', dailyNew: '543', weeklyNew: '3,801', monthlyNew: '14,567', periodNew: '', percent: '17%' }, { channel: '其他', dailyNew: '321', weeklyNew: '2,247', monthlyNew: '8,654', periodNew: '', percent: '10%' }, { channel: '总计', dailyNew: '3,096', weeklyNew: '21,702', monthlyNew: '81,220', periodNew: '', percent: '100%' },];
const headerCellStyle = { background: '#fff0f0', color: '#333', fontWeight: 'bold'};
const initCharts = () => { if (activeTab.value === 'overview') { nextTick(() => { // Chart 1: 会员/非会员
if (chartMemberRef.value) { const chart1 = echarts.init(chartMemberRef.value); chart1.setOption({ color: ['#ff4d4f', '#52c41a'], series: [ { type: 'pie', radius: ['50%', '70%'], avoidLabelOverlap: false, label: { show: false }, data: [ { value: 112265, name: '非会员用户' }, { value: 42567, name: '会员用户' } ] } ] }); } // Chart 2: 三色环形图
if (chartNewOldRef.value) { const chart2 = echarts.init(chartNewOldRef.value); chart2.setOption({ color: ['#ff4d4f', '#52c41a', '#409eff'], // 红 绿 蓝
series: [ { type: 'pie', radius: ['50%', '70%'], avoidLabelOverlap: false, label: { show: false }, data: [ { value: 300, name: '新非网数量' }, { value: 500, name: '会员用户' }, { value: 400, name: '老非网数量' } ] } ] }); } }); } else if (activeTab.value === 'detail') { nextTick(() => { // Chart 3: 柱状图
if (chartBarRef.value) { const chart3 = echarts.init(chartBarRef.value); chart3.setOption({ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { data: ['新用户', '老用户'], top: 'top', left: 'center' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: [ { type: 'category', data: ['App Store', 'Play Store', 'H5', 'APK', 'CRM系统', '其他'], axisTick: { alignWithLabel: true } } ], yAxis: [ { type: 'value' } ], series: [ { name: '新用户', type: 'bar', barWidth: '20%', color: '#40a9ff', data: [580, 1150, 650, 780, 0, 0] }, { name: '老用户', type: 'bar', barWidth: '20%', color: '#9287e7', data: [0, 0, 0, 0, 1100, 1300] } ] }); } }); }};
watch(activeTab, (newVal) => { // 同步 tab 状态到 URL
router.replace({ query: { ...route.query, tab: newVal } }); initCharts();});
onMounted(() => { initCharts();});
</script>
<style scoped>.user-overview-container { padding: 20px; background-color: #fee6e6;}
/* Tabs */.tab-header { display: flex; margin-bottom: 20px;}.tab-item { padding: 6px 16px; margin-right: 10px; background-color: #fff; border: 1px solid #ffcccc; border-radius: 4px; cursor: pointer; color: #ff4d4f; font-size: 14px;}.tab-item.active { background-color: #ff4d4f; color: #fff;}
/* Overview Tab */.stats-row { display: flex; gap: 20px; margin-bottom: 20px;}
.big-card { flex: 1; height: 260px; border-radius: 12px; padding: 24px; display: flex; flex-direction: column; justify-content: center; color: #fff;}.purple-gradient { background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%);}.orange-gradient { background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 99%, #fecfef 100%); background: linear-gradient(to right, #ffafbd, #ffc3a0); /* approximate orange */ background: linear-gradient(90deg, #ff8c6d 0%, #ffcba4 100%);}.blue-gradient { background: linear-gradient(135deg, #9BB7FC 0%, #66a6ff 100%);}
.right-stats-col { flex: 1; display: flex; flex-direction: column; gap: 20px;}.small-card { flex: 1; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: center; color: #fff;}
.card-title { font-size: 20px; display: flex; align-items: center; gap: 8px; margin-bottom: 10px;}.card-value { font-size: 64px; font-weight: bold; margin: 10px 0; text-align: center;}.card-value-small { font-size: 48px; font-weight: bold; margin-left: auto;}.top-row { display: flex; align-items: center; justify-content: space-between;}.card-tag-wrapper { display: flex; justify-content: flex-end; margin-top: 10px;}.card-tag { background-color: #fff; padding: 4px 12px; border-radius: 4px; font-weight: bold; display: inline-block; margin: 0 auto; /* Center for big card */}.big-card .card-tag { font-size: 18px;}.card-tag.up { color: #52c41a;}.card-tag.down { color: #ff4d4f;}
/* Composition Section */.composition-section { background: linear-gradient(135deg, #b8c6db 0%, #f5f7fa 100%); background-color: #b8c4f9; /* Fallback */ background: linear-gradient(to right, #a18cd1, #c2e9fb); border-radius: 12px; padding: 20px; /* background-image: linear-gradient(120deg, #a1c4fd 0%, #c2e9fb 100%); */ background: #a3b1ff; /* approximate purple-ish */}.section-header { color: #fff; font-size: 20px; margin-bottom: 15px; display: flex; align-items: center; gap: 8px;}.charts-row { display: flex; gap: 20px;}.chart-wrapper { flex: 1; background: #e6e9f5; border-radius: 12px; border: 2px solid #fff; height: 220px; display: flex; align-items: center; padding: 20px;}.chart-box { flex: 1; height: 100%;}.legend-custom { display: flex; flex-direction: column; gap: 10px; min-width: 120px;}.legend-item { display: flex; align-items: center; font-size: 14px; color: #333; font-weight: bold;}.dot { width: 12px; height: 12px; border-radius: 50%; margin-right: 8px;}.dot.green { background-color: #52c41a; }.dot.red { background-color: #ff4d4f; }.dot.blue { background-color: #409eff; }
/* Detail Tab */.search-bar { background: #fff; padding: 15px; border-radius: 8px; display: flex; align-items: center; gap: 10px; margin-bottom: 20px; border: 1px solid #f0f0f0;}.search-label { font-weight: bold; margin-right: 5px;}.search-btn, .reset-btn { width: 80px;}.search-btn { background-color: #409eff;}.reset-btn { background-color: #a0cfff; border-color: #a0cfff;}.export-btn { margin-left: auto; background-color: #ff7875; border-color: #ff7875;}
.detail-section { background: #fff; border-radius: 8px; padding: 15px; margin-bottom: 20px; border: 1px solid #f0f0f0;}.section-title { color: #409eff; font-size: 16px; font-weight: bold; margin-bottom: 15px; display: flex; align-items: center; gap: 5px;}
.text-red { color: #ff4d4f; font-weight: bold; }.text-green { color: #52c41a; font-weight: bold; }
.sub-item-text { font-size: 12px; padding-left: 20px; color: #606266; display: inline-block;}
.chart-section-bg { /* background: #fff; already set by detail-section */}.bar-chart-box { width: 100%; height: 350px;}</style>
|