|
|
|
@ -92,7 +92,7 @@ |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 数据明细 --> |
|
|
|
<div v-show="activeTab === 'detail'" class="tab-content detail-content"> |
|
|
|
<div v-show="activeTab === 'detail'" class="tab-content detail-content" v-loading="loading" element-loading-text="数据加载中..."> |
|
|
|
<!-- 搜索栏 --> |
|
|
|
<div class="search-bar"> |
|
|
|
<div class="search-label">时间段查询</div> |
|
|
|
@ -104,8 +104,8 @@ |
|
|
|
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="primary" class="search-btn" @click="handleSearch">搜索</el-button> |
|
|
|
<el-button type="primary" class="reset-btn" @click="handleReset">重置</el-button> |
|
|
|
<el-button type="danger" class="export-btn">数据导出</el-button> |
|
|
|
</div> |
|
|
|
|
|
|
|
@ -137,7 +137,11 @@ |
|
|
|
<span class="text-green">{{ scope.row.monthlyNew }}</span> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
<el-table-column prop="periodNew" label="时间段新增" /> |
|
|
|
<el-table-column prop="periodNew" label="时间段新增"> |
|
|
|
<template #default="scope"> |
|
|
|
<span class="text-green">{{ scope.row.periodNew }}</span> |
|
|
|
</template> |
|
|
|
</el-table-column> |
|
|
|
</el-table> |
|
|
|
</div> |
|
|
|
|
|
|
|
@ -178,20 +182,26 @@ |
|
|
|
</template> |
|
|
|
|
|
|
|
<script setup> |
|
|
|
import { ref, onMounted, nextTick, watch } from 'vue'; |
|
|
|
import { ref, onMounted, nextTick, watch, computed } from 'vue'; |
|
|
|
import { useRoute, useRouter } from 'vue-router'; |
|
|
|
import * as echarts from 'echarts'; |
|
|
|
import { getUserOverviewList } from '../../api/platformData'; |
|
|
|
import { getUserOverviewList, getUserFullReportList } from '../../api/platformData'; |
|
|
|
|
|
|
|
const route = useRoute(); |
|
|
|
const router = useRouter(); |
|
|
|
|
|
|
|
const activeTab = ref(route.query.tab || 'overview'); |
|
|
|
const dateRange = ref(''); |
|
|
|
const loading = ref(false); |
|
|
|
|
|
|
|
const hasDateRange = computed(() => { |
|
|
|
return dateRange.value && dateRange.value.length === 2; |
|
|
|
}); |
|
|
|
|
|
|
|
const chartMemberRef = ref(null); |
|
|
|
const chartNewOldRef = ref(null); |
|
|
|
const chartBarRef = ref(null); |
|
|
|
let chartBarInstance = null; |
|
|
|
|
|
|
|
const overviewData = ref({ |
|
|
|
total: 0, |
|
|
|
@ -204,6 +214,17 @@ const overviewData = ref({ |
|
|
|
old_normal: 0 |
|
|
|
}); |
|
|
|
|
|
|
|
// 表格数据 - 使用 ref 响应式数据 |
|
|
|
const tableData1 = ref([]); |
|
|
|
const tableData2 = ref([]); |
|
|
|
const tableData3 = ref([]); |
|
|
|
|
|
|
|
const headerCellStyle = { |
|
|
|
background: '#fff0f0', |
|
|
|
color: '#333', |
|
|
|
fontWeight: 'bold' |
|
|
|
}; |
|
|
|
|
|
|
|
// 获取增长率的样式类 |
|
|
|
const getGrowthClass = (growthStr) => { |
|
|
|
if (!growthStr) return ''; |
|
|
|
@ -221,10 +242,18 @@ const getGrowthText = (growthStr) => { |
|
|
|
return `${prefix}${arrow} ${value}`; |
|
|
|
}; |
|
|
|
|
|
|
|
// 格式化日期 |
|
|
|
const formatDate = (date) => { |
|
|
|
if (!date) return ''; |
|
|
|
const d = new Date(date); |
|
|
|
const pad = (n) => n < 10 ? '0' + n : n; |
|
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; |
|
|
|
}; |
|
|
|
|
|
|
|
const fetchData = async () => { |
|
|
|
try { |
|
|
|
const res = await getUserOverviewList(); |
|
|
|
console.log("获取用户概览数据响应完成:",res.list); |
|
|
|
console.log("获取用户概览数据响应完成:",res); |
|
|
|
// 根据用户反馈,响应拦截器直接返回data部分,不再包含code |
|
|
|
if (res && res.list) { |
|
|
|
overviewData.value = res.list; |
|
|
|
@ -235,35 +264,82 @@ const fetchData = async () => { |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 表格数据 |
|
|
|
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: 'Link 注册过', 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 fetchDetailData = async () => { |
|
|
|
loading.value = true; |
|
|
|
let params = {}; |
|
|
|
if (dateRange.value && dateRange.value.length === 2) { |
|
|
|
params.start_time = formatDate(dateRange.value[0]); |
|
|
|
params.end_time = formatDate(dateRange.value[1]); |
|
|
|
} |
|
|
|
|
|
|
|
// Check if range search is active |
|
|
|
const isRangeSearch = !!(params.start_time && params.end_time); |
|
|
|
|
|
|
|
try { |
|
|
|
const res = await getUserFullReportList(params); |
|
|
|
console.log("获取数据明细响应:", res); |
|
|
|
|
|
|
|
// 兼容处理:如果拦截器返回了data,则直接使用res;如果没拦截,使用res.data |
|
|
|
const data = res.composition ? res : (res.data ? res.data : null); |
|
|
|
|
|
|
|
if (data) { |
|
|
|
// Map Table 1 |
|
|
|
if (data.composition) { |
|
|
|
tableData1.value = data.composition.map(item => ({ |
|
|
|
type: item.label, |
|
|
|
total: item.total.toLocaleString(), |
|
|
|
dailyNew: isRangeSearch ? '' : (item.growth_day > 0 ? '+' + item.growth_day : item.growth_day), |
|
|
|
weeklyNew: isRangeSearch ? '' : (item.growth_week > 0 ? '+' + item.growth_week : item.growth_week), |
|
|
|
monthlyNew: isRangeSearch ? '' : (item.growth_month > 0 ? '+' + item.growth_month : item.growth_month), |
|
|
|
periodNew: !isRangeSearch ? '' : item.growth_range |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
// Map Table 2 |
|
|
|
if (data.new_source) { |
|
|
|
tableData2.value = data.new_source.map(item => ({ |
|
|
|
channel: item.channel, |
|
|
|
dailyNew: isRangeSearch ? '' : item.today_add, |
|
|
|
weeklyNew: isRangeSearch ? '' : (item.week_add > 0 ? '+' + item.week_add : item.week_add), |
|
|
|
monthlyNew: isRangeSearch ? '' : (item.month_add > 0 ? '+' + item.month_add : item.month_add), |
|
|
|
periodNew: !isRangeSearch ? '' : item.range_add, |
|
|
|
percent: item.rate |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
// Map Table 3 |
|
|
|
if (data.old_source) { |
|
|
|
tableData3.value = data.old_source.map(item => ({ |
|
|
|
channel: item.channel, |
|
|
|
dailyNew: isRangeSearch ? '' : item.today_add, |
|
|
|
weeklyNew: isRangeSearch ? '' : (item.week_add > 0 ? '+' + item.week_add : item.week_add), |
|
|
|
monthlyNew: isRangeSearch ? '' : (item.month_add > 0 ? '+' + item.month_add : item.month_add), |
|
|
|
periodNew: !isRangeSearch ? '' : item.range_add, |
|
|
|
percent: item.rate |
|
|
|
})); |
|
|
|
} |
|
|
|
|
|
|
|
// Chart Data |
|
|
|
if (data.chart) { |
|
|
|
nextTick(() => { |
|
|
|
updateBarChart(data.chart, data.new_source || [], data.old_source || []); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
} catch(e) { |
|
|
|
console.error('获取数据明细失败:', e); |
|
|
|
} finally { |
|
|
|
loading.value = false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const headerCellStyle = { |
|
|
|
background: '#fff0f0', |
|
|
|
color: '#333', |
|
|
|
fontWeight: 'bold' |
|
|
|
const handleSearch = () => { |
|
|
|
fetchDetailData(); |
|
|
|
}; |
|
|
|
|
|
|
|
const handleReset = () => { |
|
|
|
dateRange.value = ''; |
|
|
|
fetchDetailData(); |
|
|
|
}; |
|
|
|
|
|
|
|
const initCharts = () => { |
|
|
|
@ -319,67 +395,104 @@ const initCharts = () => { |
|
|
|
} |
|
|
|
}); |
|
|
|
} else if (activeTab.value === 'detail') { |
|
|
|
nextTick(() => { |
|
|
|
// Chart 3: 柱状图 |
|
|
|
if (chartBarRef.value) { |
|
|
|
const chart3 = echarts.init(chartBarRef.value); |
|
|
|
chart3.setOption({ |
|
|
|
tooltip: { |
|
|
|
trigger: 'item', |
|
|
|
}, |
|
|
|
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', 'HC 注册过', 'Link 注册过', '海外 CRM', '其他'], |
|
|
|
axisTick: { alignWithLabel: true } |
|
|
|
} |
|
|
|
], |
|
|
|
yAxis: [ |
|
|
|
{ type: 'value' } |
|
|
|
], |
|
|
|
series: [ |
|
|
|
{ |
|
|
|
name: '新用户', |
|
|
|
type: 'bar', |
|
|
|
barWidth: '20%', |
|
|
|
color: '#40a9ff', |
|
|
|
data: [580, 1150, 650, 780, 0, 0, 0, 0], |
|
|
|
stack: 'total' |
|
|
|
}, |
|
|
|
{ |
|
|
|
name: '老用户', |
|
|
|
type: 'bar', |
|
|
|
barWidth: '20%', |
|
|
|
color: '#9287e7', |
|
|
|
data: [0, 0, 0, 0, 1245, 987, 543, 321], |
|
|
|
stack: 'total' |
|
|
|
} |
|
|
|
] |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
// 这里的初始化主要用于空状态或第一次渲染,数据更新由 updateBarChart 处理 |
|
|
|
// 如果没有数据,可以不初始化,或者初始化为空 |
|
|
|
// fetchDetailData 会被调用并初始化图表 |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
const updateBarChart = (chartData, newSources, oldSources) => { |
|
|
|
if (!chartBarRef.value) return; |
|
|
|
|
|
|
|
if (!chartBarInstance) { |
|
|
|
chartBarInstance = echarts.init(chartBarRef.value); |
|
|
|
} |
|
|
|
|
|
|
|
const xAxisData = chartData.x_axis || []; |
|
|
|
const yAxisData = chartData.y_axis || []; |
|
|
|
|
|
|
|
// 分离新老用户数据 |
|
|
|
const newUserData = []; |
|
|
|
const oldUserData = []; |
|
|
|
|
|
|
|
// 提取渠道名称用于匹配 |
|
|
|
const newSourceChannels = newSources.map(s => s.channel); |
|
|
|
|
|
|
|
xAxisData.forEach((label, index) => { |
|
|
|
const val = yAxisData[index] || 0; |
|
|
|
if (newSourceChannels.includes(label)) { |
|
|
|
newUserData.push(val); |
|
|
|
oldUserData.push(0); |
|
|
|
} else { |
|
|
|
newUserData.push(0); |
|
|
|
oldUserData.push(val); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
const option = { |
|
|
|
tooltip: { |
|
|
|
trigger: 'item', |
|
|
|
}, |
|
|
|
legend: { |
|
|
|
data: ['新用户', '老用户'], |
|
|
|
top: 'top', |
|
|
|
left: 'center' |
|
|
|
}, |
|
|
|
grid: { |
|
|
|
left: '3%', |
|
|
|
right: '4%', |
|
|
|
bottom: '3%', |
|
|
|
containLabel: true |
|
|
|
}, |
|
|
|
xAxis: [ |
|
|
|
{ |
|
|
|
type: 'category', |
|
|
|
data: xAxisData, |
|
|
|
axisTick: { alignWithLabel: true } |
|
|
|
} |
|
|
|
], |
|
|
|
yAxis: [ |
|
|
|
{ type: 'value' } |
|
|
|
], |
|
|
|
series: [ |
|
|
|
{ |
|
|
|
name: '新用户', |
|
|
|
type: 'bar', |
|
|
|
barWidth: '20%', |
|
|
|
color: '#40a9ff', |
|
|
|
data: newUserData, |
|
|
|
stack: 'total' |
|
|
|
}, |
|
|
|
{ |
|
|
|
name: '老用户', |
|
|
|
type: 'bar', |
|
|
|
barWidth: '20%', |
|
|
|
color: '#9287e7', |
|
|
|
data: oldUserData, |
|
|
|
stack: 'total' |
|
|
|
} |
|
|
|
] |
|
|
|
}; |
|
|
|
|
|
|
|
chartBarInstance.setOption(option); |
|
|
|
}; |
|
|
|
|
|
|
|
watch(activeTab, (newVal) => { |
|
|
|
// 同步 tab 状态到 URL |
|
|
|
router.replace({ query: { ...route.query, tab: newVal } }); |
|
|
|
initCharts(); |
|
|
|
if (newVal === 'overview') { |
|
|
|
fetchData(); |
|
|
|
} else if (newVal === 'detail') { |
|
|
|
fetchDetailData(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
onMounted(() => { |
|
|
|
fetchData(); |
|
|
|
if (activeTab.value === 'overview') { |
|
|
|
fetchData(); |
|
|
|
} else { |
|
|
|
fetchDetailData(); |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
</script> |
|
|
|
|