4 changed files with 576 additions and 4 deletions
-
26package-lock.json
-
1package.json
-
6src/layout/Layout.vue
-
547src/views/PlatformData/UserOverview.vue
@ -1,14 +1,559 @@ |
|||
<template> |
|||
<div class="user-overview-container"> |
|||
<h1>用户数据概览</h1> |
|||
<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 === '用户总数'}">{{ 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: ['新用户', '老用户'], |
|||
right: '10%' |
|||
}, |
|||
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; } |
|||
|
|||
.chart-section-bg { |
|||
/* background: #fff; already set by detail-section */ |
|||
} |
|||
.bar-chart-box { |
|||
width: 100%; |
|||
height: 350px; |
|||
} |
|||
</style> |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue