|
|
<template> <div class="user-login-stats-container"> <div class="tab-header"> <div class="tab-item" :class="{ active: activeTab === 'loginData' }" @click="activeTab = 'loginData'" > 登录数据 </div> <div class="tab-item" :class="{ active: activeTab === 'regionalData' }" @click="activeTab = 'regionalData'" > 各地区登录数据 </div> </div>
<!-- 登录数据 Tab --> <div v-show="activeTab === 'loginData'" class="tab-content" v-loading="loading"> <!-- 搜索栏 --> <div class="search-bar"> <div class="search-label">地区</div> <el-select v-model="selectedRegion" placeholder="请选择所属地区" style="width: 200px; margin-right: 20px;"> <el-option label="全部" value="all" /> <el-option v-for="item in regionOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> <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" @click="handleSearch">搜索</el-button> <el-button type="primary" class="reset-btn" @click="handleReset">重置</el-button> <el-button type="danger" class="export-btn" @click="handleExportLogin">数据导出</el-button> </div>
<!-- 统计卡片 --> <div class="stats-row"> <!-- 今日登录用户数 --> <div class="stat-card purple-gradient big-card"> <div class="card-title"> <el-icon><UserFilled /></el-icon> {{ statsTitle }}登录用户数 </div> <div class="big-card-content"> <div class="card-value">{{ loginStats.total }}</div> <div v-if="loginStats.total_growth" class="card-tag" :class="getGrowthClass(loginStats.total_growth)"> {{ getGrowthText(loginStats.total_growth) }} </div> </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> {{ statsTitle }}登录会员用户数 </div> <div class="card-value-small">{{ loginStats.member }}</div> </div> <div class="card-tag-wrapper"> <div v-if="loginStats.member_growth" class="card-tag" :class="getGrowthClass(loginStats.member_growth)"> {{ getGrowthText(loginStats.member_growth) }} </div> </div> </div> <!-- 今日登录非网用户数 --> <div class="stat-card blue-gradient small-card"> <div class="top-row"> <div class="card-title"> <el-icon><User /></el-icon> {{ statsTitle }}登录非网用户数 </div> <div class="card-value-small">{{ loginStats.normal }}</div> </div> <div class="card-tag-wrapper"> <div v-if="loginStats.normal_growth" class="card-tag" :class="getGrowthClass(loginStats.normal_growth)"> {{ getGrowthText(loginStats.normal_growth) }} </div> </div> </div> </div> </div>
<!-- 近7天登录趋势 --> <div class="chart-section"> <div class="section-title"><el-icon><TrendCharts /></el-icon> {{ chartTrendTitle }}</div> <div ref="chartTrendRef" class="chart-box-large"></div> </div>
<!-- 表格1: 今日登录数据 --> <div class="detail-section"> <div class="section-title"><el-icon><DataLine /></el-icon> {{ statsTitle }}登录数据</div> <el-table :data="loginTableData1" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="channel" label="来源渠道" align="center" /> <el-table-column prop="total" :label="statsTitle + '登录总数'" align="center" /> <el-table-column prop="dailyNew" label="较昨日新增" align="center"> <template #default="scope"> <span :class="getValueColorClass(scope.row.dailyNew)">{{ scope.row.dailyNew }}</span> </template> </el-table-column> <el-table-column prop="percent" label="占比" align="center" /> </el-table> </div>
<!-- 表格2: 会员登录数据 --> <div class="detail-section"> <div class="section-title"><el-icon><Trophy /></el-icon> {{ statsTitle }}会员登录数据</div> <el-table :data="loginTableData2" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="channel" label="来源渠道" align="center" /> <el-table-column prop="total" :label="statsTitle + '登录会员数'" align="center" /> <el-table-column prop="dailyNew" label="较昨日新增" align="center"> <template #default="scope"> <span :class="getValueColorClass(scope.row.dailyNew)">{{ scope.row.dailyNew }}</span> </template> </el-table-column> <el-table-column prop="rate" label="会员登录率" align="center" /> </el-table> </div>
<!-- 表格3: 非网登录数据 --> <div class="detail-section"> <div class="section-title"><el-icon><User /></el-icon> {{ statsTitle }}非网登录数据</div> <el-table :data="loginTableData3" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="channel" label="来源渠道" align="center" /> <el-table-column prop="total" :label="statsTitle + '登录非网数'" align="center" /> <el-table-column prop="dailyNew" label="较昨日新增" align="center"> <template #default="scope"> <span :class="getValueColorClass(scope.row.dailyNew)">{{ scope.row.dailyNew }}</span> </template> </el-table-column> <el-table-column prop="rate" label="非网登录率" align="center" /> </el-table> </div> </div>
<!-- 各地区登录数据 Tab --> <div v-show="activeTab === 'regionalData'" class="tab-content" v-loading="loadingRegion"> <!-- 搜索栏 --> <div class="search-bar"> <!-- 移除地区查询 --> <div class="search-label">时间段查询</div> <el-date-picker v-model="dateRangeRegion" type="daterange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" size="default" /> <el-button type="primary" class="search-btn" @click="handleSearchRegion">搜索</el-button> <el-button type="primary" class="reset-btn" @click="handleResetRegion">重置</el-button> <el-button type="danger" class="export-btn" @click="handleExportRegion">数据导出</el-button> </div>
<!-- 表格1: 各地区活跃数据 --> <div class="detail-section"> <div class="section-title"><el-icon><Location /></el-icon> 各地区活跃数据</div> <el-table :data="regionalTableData1" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="region" label="地区" align="center" /> <el-table-column prop="dailyActive" label="日活跃用户" align="center" /> <el-table-column prop="weeklyActive" label="周活跃用户" align="center" /> <el-table-column prop="monthlyActive" label="月活跃用户" align="center" /> <el-table-column prop="periodActive" label="区间活跃用户" align="center" /> <el-table-column prop="percent" label="活跃度占比" align="center" /> </el-table> </div>
<!-- 表格2: 各地区会员活跃数据 --> <div class="detail-section"> <div class="section-title"><el-icon><Trophy /></el-icon> 各地区会员活跃数据</div> <el-table :data="regionalTableData2" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="region" label="地区" align="center" /> <el-table-column prop="dailyActive" label="日活跃用户" align="center" /> <el-table-column prop="weeklyActive" label="周活跃用户" align="center" /> <el-table-column prop="monthlyActive" label="月活跃用户" align="center" /> <el-table-column prop="periodActive" label="区间活跃用户" align="center" /> <el-table-column prop="percent" label="活跃度占比" align="center" /> </el-table> </div>
<!-- 表格3: 各地区非网活跃数据 --> <div class="detail-section"> <div class="section-title"><el-icon><User /></el-icon> 各地区非网活跃数据</div> <el-table :data="regionalTableData3" style="width: 100%" :header-cell-style="headerCellStyle"> <el-table-column prop="region" label="地区" align="center" /> <el-table-column prop="dailyActive" label="日活跃用户" align="center" /> <el-table-column prop="weeklyActive" label="周活跃用户" align="center" /> <el-table-column prop="monthlyActive" label="月活跃用户" align="center" /> <el-table-column prop="periodActive" label="区间活跃用户" align="center" /> <el-table-column prop="percent" label="活跃度占比" align="center" /> </el-table> </div>
<!-- 图表: 各地区日活跃用户 --> <div class="chart-section"> <div class="section-title"><el-icon><BarChart /></el-icon> 各地区日活跃用户 <el-button size="small" style="margin-left: auto;" @click="toggleBarChartMode">切换</el-button> </div> <div ref="chartRegionBarRef" class="chart-box-large"></div> </div>
<!-- 图表: 地区分布 --> <div class="chart-section"> <div class="section-title"><el-icon><PieChart /></el-icon> 各地区用户分布</div> <div ref="chartRegionPieRef" class="chart-box-large" style="height: 400px;"></div> </div>
<div class="charts-row"> <div class="chart-section half-width"> <div class="section-title">各地区会员用户分布</div> <div ref="chartRegionMemberPieRef" class="chart-box-medium"></div> </div> <div class="chart-section half-width"> <div class="section-title">各地区非网用户分布</div> <div ref="chartRegionNonMemberPieRef" class="chart-box-medium"></div> </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';import { getUserLoginList, getUserLoginTrend, getRegionActiveData, getRegionActiveDataHistogram, getUserLoginChannel, getUserLoginChannelMember, getUserLoginChannelNoMember, getRegionUserDistribution, exportRegionActiveData, getRegionsList, exportUserLoginPDF } from '../../api/platformData';import { ElMessage } from 'element-plus';
const route = useRoute();const router = useRouter();
const activeTab = ref(route.query.tab || 'loginData');const dateRange = ref('');const selectedRegion = ref('');const searchRegion = ref('');const dateRangeRegion = ref('');const loading = ref(false);const loadingRegion = ref(false);
const chartTrendRef = ref(null);let chartTrendInstance = null;const chartRegionBarRef = ref(null);let chartRegionBarInstance = null;const chartRegionPieRef = ref(null);const chartRegionMemberPieRef = ref(null);const chartRegionNonMemberPieRef = ref(null);
// 柱状图显示模式:'all' (全部用户), 'detail' (分身份用户)
const barChartMode = ref('all');
// 响应式数据:地区活跃数据
const regionalTableData1 = ref([]);const regionalTableData2 = ref([]);const regionalTableData3 = ref([]);const regionOptions = ref([]);
// 获取地区列表
const fetchRegionOptions = async () => { try { const res = await getRegionsList(); console.log("获取地区列表响应:", res); const data = res.data || res; if (data && data.list) { regionOptions.value = data.list.map(region => ({ label: region, value: region })); } } catch (e) { console.error('获取地区列表失败:', e); }};
// 初始化查询参数
const initQueryParams = () => { const { start_time, end_time, region, r_start_time, r_end_time } = route.query; // Tab 1 Params
if (start_time && end_time) { dateRange.value = [new Date(start_time), new Date(end_time)]; } if (region) { selectedRegion.value = region; }
// Tab 2 Params
if (r_start_time && r_end_time) { dateRangeRegion.value = [new Date(r_start_time), new Date(r_end_time)]; }};
initQueryParams();
// 响应式数据:登录数据统计
const loginStats = ref({ total: 0, total_growth: '0%', member: 0, member_growth: '0%', normal: 0, normal_growth: '0%'});
// 获取增长率的样式类
const chartTrendTitle = ref('近7天登录趋势');const statsTitle = ref('今日');
const getGrowthClass = (growthStr) => { if (!growthStr) return ''; return growthStr.startsWith('-') ? 'down' : 'up';};
// 获取增长率的显示文本(添加箭头)
const getGrowthText = (growthStr) => { if (!growthStr) return ''; const isDown = growthStr.startsWith('-'); const arrow = isDown ? '↓' : '↑'; const prefix = isDown ? '较昨日减少' : '较昨日增加'; const value = growthStr.replace('-', ''); return `${prefix}${arrow} ${value}`;};
// 获取表格数值颜色样式
const getValueColorClass = (val) => { if (!val || val === '-') return ''; const strVal = String(val); if (strVal.startsWith('+')) return 'text-green'; if (strVal.startsWith('-')) return 'text-red'; if (strVal === '0') return 'text-black'; // 如果是数字且大于0(不带+号的情况)
if (!isNaN(parseFloat(strVal)) && parseFloat(strVal) > 0) return 'text-green'; return '';};
// 格式化日期
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())}`;};
const fetchLoginData = async () => { let params = {}; if (dateRange.value && dateRange.value.length === 2) { params.start_time = formatDate(dateRange.value[0]); params.end_time = formatDate(dateRange.value[1]); statsTitle.value = `${params.start_time} 至 ${params.end_time}`; } else { statsTitle.value = '今日'; } if (selectedRegion.value && selectedRegion.value !== 'all') { params.region = selectedRegion.value; }
try { const res = await getUserLoginList(params); console.log("获取用户登录数据响应:", res); // 兼容处理拦截器
const data = res.list ? res : (res.data && res.data.list ? res.data : null);
if (data && data.list) { loginStats.value = data.list; } } catch (e) { console.error('获取用户登录数据失败:', e); }};
const fetchTrendData = async () => { let params = {}; if (dateRange.value && dateRange.value.length === 2) { params.start_time = formatDate(dateRange.value[0]); params.end_time = formatDate(dateRange.value[1]); chartTrendTitle.value = `${params.start_time} 至 ${params.end_time} 登录趋势`; } else { chartTrendTitle.value = '近7天登录趋势'; }
try { const res = await getUserLoginTrend(params); console.log("获取用户登录趋势响应:", res); // 兼容处理拦截器
const data = res.list ? res : (res.data && res.data.list ? res.data : null);
if (data && data.list) { updateTrendChart(data.list); } } catch (e) { console.error('获取用户登录趋势失败:', e); }};
const updateTrendChart = (list) => { if (!chartTrendRef.value) return; if (!chartTrendInstance) { chartTrendInstance = echarts.init(chartTrendRef.value); }
const dates = list.map(item => item.date); const allUser = list.map(item => item.all_user); const member = list.map(item => item.member);
const option = { tooltip: { trigger: 'axis' }, legend: { data: ['所有用户', '会员用户'], top: 'top' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: dates }, yAxis: { type: 'value' }, series: [ { name: '所有用户', type: 'line', data: allUser, smooth: true, itemStyle: { color: '#40a9ff' } }, { name: '会员用户', type: 'line', data: member, smooth: true, itemStyle: { color: '#52c41a' } } ] }; chartTrendInstance.setOption(option);};
const fetchLoginChannelData = async () => { let params = {}; if (dateRange.value && dateRange.value.length === 2) { params.start_time = formatDate(dateRange.value[0]); params.end_time = formatDate(dateRange.value[1]); } if (selectedRegion.value && selectedRegion.value !== 'all') { params.region = selectedRegion.value; }
try { const res = await getUserLoginChannel(params); console.log("获取今日登录渠道数据响应:", res); // 兼容处理拦截器
const data = res.list ? res : (res.data && res.data.list ? res.data : null);
if (data && data.list) { loginTableData1.value = data.list.map(item => ({ channel: item.source, total: item.today_count, dailyNew: item.growth_value, percent: item.ratio + '%' })); } } catch (e) { console.error('获取今日登录渠道数据失败:', e); }};
const fetchLoginChannelMemberData = async () => { let params = {}; if (dateRange.value && dateRange.value.length === 2) { params.start_time = formatDate(dateRange.value[0]); params.end_time = formatDate(dateRange.value[1]); } if (selectedRegion.value && selectedRegion.value !== 'all') { params.region = selectedRegion.value; }
try { const res = await getUserLoginChannelMember(params); console.log("获取会员登录渠道数据响应:", res); // 兼容处理拦截器
const data = res.list ? res : (res.data && res.data.list ? res.data : null);
if (data && data.list) { loginTableData2.value = data.list.map(item => ({ channel: item.source, total: item.today_count, dailyNew: item.growth_value, rate: item.ratio + '%' })); } } catch (e) { console.error('获取会员登录渠道数据失败:', e); }};
const fetchLoginChannelNoMemberData = async () => { let params = {}; if (dateRange.value && dateRange.value.length === 2) { params.start_time = formatDate(dateRange.value[0]); params.end_time = formatDate(dateRange.value[1]); } if (selectedRegion.value && selectedRegion.value !== 'all') { params.region = selectedRegion.value; }
try { const res = await getUserLoginChannelNoMember(params); console.log("获取非网登录渠道数据响应:", res); // 兼容处理拦截器
const data = res.list ? res : (res.data && res.data.list ? res.data : null);
if (data && data.list) { loginTableData3.value = data.list.map(item => ({ channel: item.source, total: item.today_count, dailyNew: item.growth_value, rate: item.ratio + '%' })); } } catch (e) { console.error('获取非网登录渠道数据失败:', e); }};
// 统一获取登录数据 Tab 的所有数据
const fetchAllLoginData = async () => { loading.value = true; try { // 使用 Promise.all 并行请求所有接口
await Promise.all([ fetchTrendData(), fetchLoginData(), fetchLoginChannelData(), fetchLoginChannelMemberData(), fetchLoginChannelNoMemberData() ]); } catch (error) { console.error('获取登录数据失败:', error); } finally { loading.value = false; }};
const handleExportLogin = 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]); } if (selectedRegion.value && selectedRegion.value !== 'all') { params.region = selectedRegion.value; }
try { const res = await exportUserLoginPDF(params); console.log("导出登录数据PDF响应(Blob):", res); // 创建Blob对象,处理二进制流下载
const blob = new Blob([res], { type: 'application/pdf' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; // 设置下载文件名,添加时间戳防止重名
const fileName = `用户登录数据_${formatDate(new Date())}.pdf`; link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); // 清理资源
document.body.removeChild(link); window.URL.revokeObjectURL(url); ElMessage.success('导出成功'); } catch (e) { console.error('导出登录数据失败:', e); ElMessage.error('导出请求发生错误'); } finally { loading.value = false; }};
const handleSearch = () => { // 更新 URL 参数
const query = { ...route.query }; if (dateRange.value && dateRange.value.length === 2) { query.start_time = formatDate(dateRange.value[0]); query.end_time = formatDate(dateRange.value[1]); } else { delete query.start_time; delete query.end_time; }
if (selectedRegion.value && selectedRegion.value !== 'all') { query.region = selectedRegion.value; } else { delete query.region; }
router.replace({ query });
fetchAllLoginData();};
const handleReset = () => { dateRange.value = ''; selectedRegion.value = ''; // 清除 URL 参数
const query = { ...route.query }; delete query.start_time; delete query.end_time; delete query.region; router.replace({ query });
fetchAllLoginData();};
// 统一获取地区数据 Tab 的所有数据
const fetchAllRegionData = async () => { loadingRegion.value = true; try { await Promise.all([ fetchRegionActiveData(), fetchRegionHistogramData(), fetchRegionDistributionData() ]); } catch (error) { console.error('获取地区数据失败:', error); } finally { loadingRegion.value = false; }};
const fetchRegionActiveData = async () => { let params = {}; if (dateRangeRegion.value && dateRangeRegion.value.length === 2) { params.start_time = formatDate(dateRangeRegion.value[0]); params.end_time = formatDate(dateRangeRegion.value[1]); }
try { // 并行请求三种身份的数据
const [resAll, resMember, resNormal] = await Promise.all([ getRegionActiveData({ ...params, identity: 0 }), getRegionActiveData({ ...params, identity: 1 }), getRegionActiveData({ ...params, identity: 2 }) ]);
console.log("获取地区活跃数据响应:", { resAll, resMember, resNormal }); // 检查是否选择了时间段
const isRangeSelected = !!(params.start_time && params.end_time);
const processData = (res) => { // 兼容处理拦截器
const data = res.region_active_data ? res : (res.data && res.data.region_active_data ? res.data : null); if (data && data.region_active_data) { return data.region_active_data.map(item => ({ region: item.region, dailyActive: item.daily_active_user, weeklyActive: item.weekly_active_user, monthlyActive: item.monthly_active_user, // 如果没有选择时间段,则显示 '-',否则显示数据
periodActive: isRangeSelected ? item.range_active_user : '-', percent: item.active_rate + '%' })); } return []; };
regionalTableData1.value = processData(resAll); regionalTableData2.value = processData(resMember); regionalTableData3.value = processData(resNormal);
} catch (e) { console.error('获取地区活跃数据失败:', e); }};
const fetchRegionHistogramData = async () => { let params = {}; if (dateRangeRegion.value && dateRangeRegion.value.length === 2) { params.start_time = formatDate(dateRangeRegion.value[0]); params.end_time = formatDate(dateRangeRegion.value[1]); }
try { const res = await getRegionActiveDataHistogram(params); console.log("获取地区活跃柱状图数据响应:", res); // 兼容处理拦截器
const data = res.region_active_data_histogram ? res : (res.data && res.data.region_active_data_histogram ? res.data : null);
if (data && data.region_active_data_histogram) { updateRegionBarChart(data.region_active_data_histogram); } } catch (e) { console.error('获取地区活跃柱状图数据失败:', e); }};
const updateRegionBarChart = (list) => { if (!chartRegionBarRef.value) return; if (!chartRegionBarInstance) { chartRegionBarInstance = echarts.init(chartRegionBarRef.value); }
const regions = list.map(item => item.region); const series = []; const legendData = [];
if (barChartMode.value === 'all') { legendData.push('日活跃用户', '周活跃用户', '月活跃用户'); series.push( { name: '日活跃用户', type: 'bar', data: list.map(item => item.daily_active_user), itemStyle: { color: '#40a9ff' } }, { name: '周活跃用户', type: 'bar', data: list.map(item => item.weekly_active_user), itemStyle: { color: '#52c41a' } }, { name: '月活跃用户', type: 'bar', data: list.map(item => item.monthly_active_user), itemStyle: { color: '#BC943D' } } ); } else { legendData.push('日活跃会员', '日活跃非网', '周活跃会员', '周活跃非网', '月活跃会员', '月活跃非网'); // 使用 stack 属性将会员和非会员数据堆叠在一起
// 日活跃
series.push( { name: '日活跃会员', type: 'bar', stack: 'daily', data: list.map(item => item.member_daily_active), itemStyle: { color: '#91D5FF' } // 浅蓝
}, { name: '日活跃非网', type: 'bar', stack: 'daily', data: list.map(item => item.no_member_daily_active), itemStyle: { color: '#40a9ff' } // 深蓝
} );
// 周活跃
series.push( { name: '周活跃会员', type: 'bar', stack: 'weekly', data: list.map(item => item.member_weekly_active), itemStyle: { color: '#95de64' } // 浅绿
}, { name: '周活跃非网', type: 'bar', stack: 'weekly', data: list.map(item => item.no_member_weekly_active), itemStyle: { color: '#52c41a' } // 深绿
} );
// 月活跃
series.push( { name: '月活跃会员', type: 'bar', stack: 'monthly', data: list.map(item => item.member_monthly_active), itemStyle: { color: '#FFC53D' } // 浅橙
}, { name: '月活跃非网', type: 'bar', stack: 'monthly', data: list.map(item => item.no_member_monthly_active), itemStyle: { color: '#BC943D' } // 深棕
} ); }
const option = { tooltip: { trigger: 'item' }, // 之前需求:取消十字线显示,改为item
legend: { data: legendData, top: 'top' }, xAxis: { type: 'category', data: regions, axisLabel: { interval: 0, rotate: 30 } // 防止标签重叠
}, yAxis: { type: 'value' }, series: series };
chartRegionBarInstance.setOption(option, true); // true: not merge, reset
};
const toggleBarChartMode = () => { barChartMode.value = barChartMode.value === 'all' ? 'detail' : 'all'; fetchRegionHistogramData();};
const fetchRegionDistributionData = async () => { let params = {}; if (dateRangeRegion.value && dateRangeRegion.value.length === 2) { params.start_time = formatDate(dateRangeRegion.value[0]); params.end_time = formatDate(dateRangeRegion.value[1]); }
try { const [resAll, resMember, resNormal] = await Promise.all([ getRegionUserDistribution({ ...params, identity: 0 }), getRegionUserDistribution({ ...params, identity: 1 }), getRegionUserDistribution({ ...params, identity: 2 }) ]);
console.log("获取地区分布数据响应:", { resAll, resMember, resNormal }); const processData = (res) => { const data = res.region_user_distribution ? res : (res.data && res.data.region_user_distribution ? res.data : null); if (data && data.region_user_distribution) { return data.region_user_distribution.map(item => ({ name: item.region, value: item.total })).filter(item => item.value > 0); // 过滤掉值为0的项,使饼图更美观
} return []; };
updatePieChart(chartRegionPieRef, chartRegionPieInstance, processData(resAll)); updatePieChart(chartRegionMemberPieRef, chartRegionMemberPieInstance, processData(resMember)); updatePieChart(chartRegionNonMemberPieRef, chartRegionNonMemberPieInstance, processData(resNormal));
} catch (e) { console.error('获取地区分布数据失败:', e); }};
let chartRegionPieInstance = null;let chartRegionMemberPieInstance = null;let chartRegionNonMemberPieInstance = null;
const updatePieChart = (chartRef, chartInstance, data) => { if (!chartRef.value) return; let instance = chartInstance; if (!instance) { // 如果没有传入实例,尝试获取或初始化 (注意:这里需要正确维护全局实例变量)
// 简单起见,这里总是重新获取echarts实例,或者通过DOM属性判断
instance = echarts.getInstanceByDom(chartRef.value); if (!instance) { instance = echarts.init(chartRef.value); } }
const option = { color: regionColors, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', right: '0%', top: 'center', itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 10 } }, series: [ { type: 'pie', radius: '70%', center: ['30%', '50%'], data: data, label: { show: false }, itemStyle: { borderRadius: 0, borderColor: '#fff', borderWidth: 2 }, } ] }; instance.setOption(option);};
const handleExportRegion = async () => { let params = {}; if (dateRangeRegion.value && dateRangeRegion.value.length === 2) { params.start_time = formatDate(dateRangeRegion.value[0]); params.end_time = formatDate(dateRangeRegion.value[1]); }
try { const res = await exportRegionActiveData(params); console.log("导出地区活跃数据响应(Blob):", res); // 创建Blob对象,处理二进制流下载
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; // 设置下载文件名,添加时间戳防止重名
const fileName = `各地区登录活跃数据_${formatDate(new Date())}.xlsx`; link.setAttribute('download', fileName); document.body.appendChild(link); link.click(); // 清理资源
document.body.removeChild(link); window.URL.revokeObjectURL(url); ElMessage.success('导出成功'); } catch (e) { console.error('导出地区活跃数据失败:', e); ElMessage.error('导出请求发生错误'); }};
const handleSearchRegion = () => { // 更新 URL 参数
const query = { ...route.query }; if (dateRangeRegion.value && dateRangeRegion.value.length === 2) { query.r_start_time = formatDate(dateRangeRegion.value[0]); query.r_end_time = formatDate(dateRangeRegion.value[1]); } else { delete query.r_start_time; delete query.r_end_time; }
// 移除 region 参数处理
router.replace({ query });
fetchAllRegionData();};
const handleResetRegion = () => { dateRangeRegion.value = ''; searchRegion.value = ''; // 保持变量重置,但不影响UI
// 清除 URL 参数
const query = { ...route.query }; delete query.r_start_time; delete query.r_end_time; // delete query.r_region; // 已移除参数
router.replace({ query });
fetchAllRegionData();};
// Tab 1 数据
const loginTableData1 = ref([]);
const loginTableData2 = ref([]);
const loginTableData3 = ref([]);
// Tab 2 数据:regionalTableData1, 2, 3 已改为响应式变量并在 fetchRegionActiveData 中更新
const headerCellStyle = { background: '#fff0f0', color: '#333', fontWeight: 'bold'};
const regionColors = ['#68B2FF', '#D94F41', '#69D2AF', '#FFD360', '#ADADAD', '#BE71DD'];
const initCharts = () => { nextTick(() => { if (activeTab.value === 'loginData') { // 趋势图已经在 fetchTrendData -> updateTrendChart 中初始化和更新
// 这里只需要处理初始无数据时的状态,或者等待 fetchTrendData 调用
fetchAllLoginData(); } else if (activeTab.value === 'regionalData') { fetchAllRegionData(); // 柱状图
if (chartRegionBarRef.value) { const chart = echarts.init(chartRegionBarRef.value); chart.setOption({ tooltip: { trigger: 'item' }, legend: { data: ['日活跃用户', '周活跃用户', '月活跃用户'], top: 'top' }, xAxis: { type: 'category', data: ['香港', '新加坡', '泰国', '越南', '马来西亚', '加拿大', '美国'] }, yAxis: { type: 'value' }, series: [ { name: '日活跃用户', type: 'bar', data: [400, 400, 400, 500, 400, 400, 400], itemStyle: { color: '#40a9ff' } }, { name: '周活跃用户', type: 'bar', data: [500, 500, 500, 600, 500, 500, 500], itemStyle: { color: '#52c41a' } }, { name: '月活跃用户', type: 'bar', data: [600, 600, 600, 700, 600, 600, 600], itemStyle: { color: '#BC943D' } } ] }); }
// 饼图 - 地区分布
if (chartRegionPieRef.value) { const chart = echarts.init(chartRegionPieRef.value); chart.setOption({ color: regionColors, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', right: '10%', top: 'center' }, series: [ { type: 'pie', radius: '80%', center: ['40%', '50%'], data: [ { value: 1048, name: '香港地区' }, { value: 735, name: '新加坡地区' }, { value: 580, name: '泰国地区' }, { value: 484, name: '越南地区' }, { value: 300, name: '马来西亚地区' }, { value: 300, name: '其他地区' } ], itemStyle: { borderRadius: 0, borderColor: '#fff', borderWidth: 2 }, label: { show: false } } ] }); }
// 饼图 - 会员分布
if (chartRegionMemberPieRef.value) { const chart = echarts.init(chartRegionMemberPieRef.value); chart.setOption({ color: regionColors, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', right: '0%', top: 'center', itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 10 } }, series: [ { type: 'pie', radius: '70%', center: ['30%', '50%'], data: [ { value: 1048, name: '香港地区' }, { value: 735, name: '新加坡地区' }, { value: 580, name: '泰国地区' }, { value: 484, name: '越南地区' }, { value: 300, name: '马来西亚地区' }, { value: 300, name: '其他地区' } ], label: { show: false } } ] }); }
// 饼图 - 非网分布
if (chartRegionNonMemberPieRef.value) { const chart = echarts.init(chartRegionNonMemberPieRef.value); chart.setOption({ color: regionColors, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', right: '0%', top: 'center', itemWidth: 10, itemHeight: 10, textStyle: { fontSize: 10 } }, series: [ { type: 'pie', radius: '70%', center: ['30%', '50%'], data: [ { value: 1048, name: '香港地区' }, { value: 735, name: '新加坡地区' }, { value: 580, name: '泰国地区' }, { value: 484, name: '越南地区' }, { value: 300, name: '马来西亚地区' }, { value: 300, name: '其他地区' } ], label: { show: false } } ] }); } } });};
watch(activeTab, (newVal) => { router.replace({ query: { ...route.query, tab: newVal } }); initCharts();});
onMounted(() => { if (activeTab.value === 'loginData') { fetchAllLoginData(); } fetchRegionOptions(); initCharts();});</script>
<style scoped>.user-login-stats-container { padding: 20px; background-color: #fee6e6; min-height: calc(100vh - 40px);}
/* 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;}
/* Search Bar */.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: #409eff; border-color: #409eff;}.export-btn { margin-left: auto; background-color: #ff7875; border-color: #ff7875;}
/* Cards */.stats-row { display: flex; gap: 20px; margin-bottom: 20px;}.big-card { flex: 1; height: 360px; border-radius: 12px; padding: 24px; display: flex; flex-direction: column; justify-content: flex-start; color: #fff; position: relative;}.right-stats-col { flex: 2; display: flex; flex-direction: column; gap: 20px;}.small-card { flex: 1; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: flex-start; color: #fff; position: relative;}.purple-gradient { background: linear-gradient(135deg, #a18cd1 0%, #fbc2eb 100%); }.orange-gradient { background: linear-gradient(90deg, #ff8c6d 0%, #ffcba4 100%); }.blue-gradient { background: linear-gradient(135deg, #9BB7FC 0%, #66a6ff 100%); }
.card-title { font-size: 24px; font-weight: bold; display: flex; align-items: center; gap: 8px; margin-bottom: 20px; width: 100%;}.big-card-content { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 10px;}.card-value { font-size: 64px; font-weight: bold; margin: 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: 8px 16px; border-radius: 4px; font-weight: bold; display: inline-block;}.big-card .card-tag { font-size: 18px; }.card-tag.up { color: #52c41a; }.card-tag.down { color: #ff4d4f; }
.text-red { color: #ff4d4f; font-weight: bold; }.text-green { color: #52c41a; font-weight: bold; }.text-black { color: #333; font-weight: bold; }
/* Sections */.chart-section, .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;}.chart-box-large { width: 100%; height: 400px;}.charts-row { display: flex; gap: 20px;}.half-width { flex: 1;}.chart-box-medium { width: 100%; height: 300px;}</style>
|