dev #1
20001
package-lock.json
generated
20001
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,8 +1,8 @@
|
||||
import weChatHttp from './weChatHttp.js'
|
||||
|
||||
// 打卡信息
|
||||
export function GetPersonalGoalInfo(userId, month) {
|
||||
return weChatHttp.get(`/Server/GetPersonalGoalInfo?userId=${userId}&month=${month}`);
|
||||
// 打卡信息(使用微信小程序用户ID)
|
||||
export function GetPersonalGoalInfo(pageIndex, pageSize, userId, month) {
|
||||
return weChatHttp.get(`/Server/GetPersonalGoalInfo?pageIndex=${pageIndex}&pageSize=${pageSize}&userId=${userId}&month=${month || ''}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,8 +1,9 @@
|
||||
import http from './http'
|
||||
import weChatHttp from './weChatHttp.js'
|
||||
|
||||
// 获取用户列表
|
||||
// 获取用户列表(调用微信小程序API的员工数据)
|
||||
export function getUserList(pageIndex, pageSize, userTrueName, grade, gender) {
|
||||
return http.get(`/api/User/GetUserPageList?pageIndex=${pageIndex}&pageSize=${pageSize}&userTrueName=${userTrueName || ''}&grade=${grade || ''}&gender=${gender || ''}`);
|
||||
return weChatHttp.get(`/Server/GetSmartSportsUserPageList?pageIndex=${pageIndex}&pageSize=${pageSize}&userTrueName=${userTrueName || ''}&gradeId=${grade || ''}&gender=${gender || ''}`);
|
||||
}
|
||||
|
||||
// 获取用户详情
|
||||
@ -10,7 +11,7 @@ export function getUserDetails(userId) {
|
||||
return http.get(`/api/User/GetUserDetails?userId=${userId}`);
|
||||
}
|
||||
|
||||
// 获取用户训练记录
|
||||
// 获取用户训练记录(调用微信小程序API)
|
||||
export function getUserTrainingRecords(userId, pageIndex, pageSize, type, mode, startTime, endTime) {
|
||||
return http.get(`/api/User/GetUserTrainingRecords?userId=${userId}&pageIndex=${pageIndex}&pageSize=${pageSize}&type=${type || ''}&mode=${mode || ''}&startTime=${startTime || ''}&endTime=${endTime || ''}`);
|
||||
return weChatHttp.get(`/Server/GetUserTrainingRecords?pageIndex=${pageIndex}&pageSize=${pageSize}&userId=${userId}&type=${type || ''}&mode=${mode || ''}&startTime=${startTime || ''}&endTime=${endTime || ''}`);
|
||||
}
|
104
src/api/userActivity.js
Normal file
104
src/api/userActivity.js
Normal file
@ -0,0 +1,104 @@
|
||||
import weChatHttp from './weChatHttp.js'
|
||||
|
||||
/**
|
||||
* 获取用户活跃度概览数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @returns {Promise} 概览数据
|
||||
*/
|
||||
export function getUserActivityOverview(params) {
|
||||
return weChatHttp.get(`/Server/GetUserActivityOverview?timeRange=${params.timeRange}&date=${params.date.toISOString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃用户数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @returns {Promise} 活跃用户数据
|
||||
*/
|
||||
export function getActiveUsersData(params) {
|
||||
return weChatHttp.get(`/Server/GetActiveUsersData?timeRange=${params.timeRange}&date=${params.date.toISOString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取地域分析数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @returns {Promise} 地域分析数据
|
||||
*/
|
||||
export function getRegionData(params) {
|
||||
return weChatHttp.get(`/Server/GetRegionData?timeRange=${params.timeRange}&date=${params.date.toISOString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取人口统计数据(年龄和性别)
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @returns {Promise} 人口统计数据
|
||||
*/
|
||||
export function getDemographicsData(params) {
|
||||
return weChatHttp.get(`/Server/GetDemographicsData?timeRange=${params.timeRange}&date=${params.date.toISOString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取功能使用量统计数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @param {String} params.module 模块名称(可选)
|
||||
* @param {String} params.function 功能名称(可选)
|
||||
* @param {String} params.button 按钮名称(可选)
|
||||
* @returns {Promise} 功能使用量数据
|
||||
*/
|
||||
export function getFeatureUsageData(params) {
|
||||
let url = `/Server/GetFeatureUsageData?timeRange=${params.timeRange}&date=${params.date.toISOString()}`;
|
||||
|
||||
if (params.module) {
|
||||
url += `&module=${encodeURIComponent(params.module)}`;
|
||||
}
|
||||
if (params.function) {
|
||||
url += `&function=${encodeURIComponent(params.function)}`;
|
||||
}
|
||||
if (params.button) {
|
||||
url += `&button=${encodeURIComponent(params.button)}`;
|
||||
}
|
||||
|
||||
return weChatHttp.get(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取新增用户统计数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @returns {Promise} 新增用户数据
|
||||
*/
|
||||
export function getNewUsersData(params) {
|
||||
return weChatHttp.get(`/Server/GetNewUsersData?timeRange=${params.timeRange}&date=${params.date.toISOString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户总数统计数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @returns {Promise} 用户总数数据
|
||||
*/
|
||||
export function getTotalUsersData(params) {
|
||||
return weChatHttp.get(`/Server/GetTotalUsersData?timeRange=${params.timeRange}&date=${params.date.toISOString()}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平均使用时长数据
|
||||
* @param {Object} params 查询参数
|
||||
* @param {String} params.timeRange 时间范围:day, week, month
|
||||
* @param {Date} params.date 选择的日期
|
||||
* @returns {Promise} 平均使用时长数据
|
||||
*/
|
||||
export function getAvgUsageTimeData(params) {
|
||||
return weChatHttp.get(`/Server/GetAvgUsageTimeData?timeRange=${params.timeRange}&date=${params.date.toISOString()}`);
|
||||
}
|
@ -552,6 +552,14 @@ const routes = [{
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/userActivity', //用户活跃度统计
|
||||
name: 'userActivity',
|
||||
component: () => import('@/views/userActivity/index.vue'),
|
||||
meta: {
|
||||
keepAlive: false
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/resourceLibrary',
|
||||
name: 'resourceLibrary',
|
||||
|
315
src/views/userActivity/components/AgeGenderChart.vue
Normal file
315
src/views/userActivity/components/AgeGenderChart.vue
Normal file
@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<div class="ageGenderChart">
|
||||
<p class="title">年龄/性别比例</p>
|
||||
<div class="content">
|
||||
<div class="leftSection">
|
||||
<div class="sectionTitle">年龄</div>
|
||||
<div class="ageChart" id="ageChart"></div>
|
||||
</div>
|
||||
<div class="rightSection">
|
||||
<div class="sectionTitle">性别比例</div>
|
||||
<div class="genderChart" id="genderChart"></div>
|
||||
<div class="genderIcons">
|
||||
<div class="iconGroup male">
|
||||
<div class="icons">
|
||||
<span class="icon" v-for="n in Math.round(propsData.genderData.male / 10)" :key="'male-' + n">👨</span>
|
||||
</div>
|
||||
<div class="percentage">{{ propsData.genderData.male }}%</div>
|
||||
</div>
|
||||
<div class="iconGroup female">
|
||||
<div class="icons">
|
||||
<span class="icon" v-for="n in Math.round(propsData.genderData.female / 10)" :key="'female-' + n">👩</span>
|
||||
</div>
|
||||
<div class="percentage">{{ propsData.genderData.female }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
propsData: any
|
||||
}>();
|
||||
|
||||
const propsData = ref(props.propsData);
|
||||
|
||||
let ageChart: echarts.ECharts | null = null;
|
||||
let genderChart: echarts.ECharts | null = null;
|
||||
|
||||
const initAgeChart = () => {
|
||||
const chartDom = document.getElementById('ageChart');
|
||||
if (chartDom) {
|
||||
ageChart = echarts.init(chartDom);
|
||||
updateAgeChart();
|
||||
} else {
|
||||
console.error('找不到 ageChart 元素');
|
||||
}
|
||||
};
|
||||
|
||||
const initGenderChart = () => {
|
||||
const chartDom = document.getElementById('genderChart');
|
||||
if (chartDom) {
|
||||
genderChart = echarts.init(chartDom);
|
||||
updateGenderChart();
|
||||
} else {
|
||||
console.error('找不到 genderChart 元素');
|
||||
}
|
||||
};
|
||||
|
||||
const updateAgeChart = () => {
|
||||
if (!ageChart) return;
|
||||
|
||||
const ageData = [
|
||||
{ name: '18-25', value: 30 },
|
||||
{ name: '26-35', value: 40 },
|
||||
{ name: '36-45', value: 20 },
|
||||
{ name: '46-55', value: 8 },
|
||||
{ name: '55+', value: 2 }
|
||||
];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '10%',
|
||||
right: '10%',
|
||||
bottom: '15%',
|
||||
top: '10%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: ageData.map(item => item.name),
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 10
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 10,
|
||||
formatter: '{value}%'
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '年龄分布',
|
||||
type: 'bar',
|
||||
data: ageData.map(item => item.value),
|
||||
itemStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: '#4da6ff'
|
||||
}, {
|
||||
offset: 1, color: '#1a87ff'
|
||||
}]
|
||||
},
|
||||
borderRadius: [4, 4, 0, 0]
|
||||
},
|
||||
barWidth: '60%'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
ageChart.setOption(option);
|
||||
};
|
||||
|
||||
const updateGenderChart = () => {
|
||||
if (!genderChart) return;
|
||||
|
||||
const genderData = [
|
||||
{ name: '男', value: propsData.value.genderData.male, itemStyle: { color: '#4da6ff' } },
|
||||
{ name: '女', value: propsData.value.genderData.female, itemStyle: { color: '#ff6b9d' } }
|
||||
];
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '性别分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
data: genderData,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{b}\n{c}%',
|
||||
fontSize: 10,
|
||||
color: '#fff'
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#8690a5'
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
genderChart.setOption(option);
|
||||
};
|
||||
|
||||
watch(() => props.propsData, (newData) => {
|
||||
propsData.value = newData;
|
||||
updateAgeChart();
|
||||
updateGenderChart();
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
initAgeChart();
|
||||
initGenderChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ageGenderChart {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
background: url("@/assets/images/allImg/内容背景1.png") no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-left: 10px;
|
||||
padding-top: 15px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
height: calc(100% - 45px);
|
||||
padding: 10px;
|
||||
|
||||
.leftSection {
|
||||
width: 50%;
|
||||
padding-right: 10px;
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 12px;
|
||||
color: #8690a5;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ageChart {
|
||||
width: 100%;
|
||||
height: calc(100% - 30px);
|
||||
}
|
||||
}
|
||||
|
||||
.rightSection {
|
||||
width: 50%;
|
||||
padding-left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.sectionTitle {
|
||||
font-size: 12px;
|
||||
color: #8690a5;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.genderChart {
|
||||
width: 100%;
|
||||
height: 60%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.genderIcons {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-start;
|
||||
height: 30%;
|
||||
|
||||
.iconGroup {
|
||||
text-align: center;
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
max-width: 80px;
|
||||
|
||||
.icon {
|
||||
font-size: 14px;
|
||||
margin: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.percentage {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
&.male .percentage {
|
||||
color: #4da6ff;
|
||||
}
|
||||
|
||||
&.female .percentage {
|
||||
color: #ff6b9d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
762
src/views/userActivity/components/FunctionUsageChart.vue
Normal file
762
src/views/userActivity/components/FunctionUsageChart.vue
Normal file
@ -0,0 +1,762 @@
|
||||
<template>
|
||||
<div class="functionUsageChart">
|
||||
<p class="title">功能使用情况统计</p>
|
||||
<div class="content">
|
||||
<div class="chartContainer">
|
||||
<div class="chart" id="functionChart"></div>
|
||||
<!-- 移动到图表右上角的功能选择器 -->
|
||||
<div class="functionSelector">
|
||||
<div class="cascaderContainer">
|
||||
<div class="selectGroup">
|
||||
<label class="selectLabel">模块</label>
|
||||
<el-select
|
||||
v-model="selectedModule"
|
||||
placeholder="请选择模块"
|
||||
class="custom-select"
|
||||
@change="onModuleChange"
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="module in moduleOptions"
|
||||
:key="module.value"
|
||||
:label="module.label"
|
||||
:value="module.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="selectGroup">
|
||||
<label class="selectLabel">功能</label>
|
||||
<el-select
|
||||
v-model="selectedFunction"
|
||||
placeholder="请选择功能"
|
||||
class="custom-select"
|
||||
:disabled="!selectedModule"
|
||||
@change="onFunctionChange"
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="func in functionOptions"
|
||||
:key="func.value"
|
||||
:label="func.label"
|
||||
:value="func.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="selectGroup">
|
||||
<label class="selectLabel">按钮</label>
|
||||
<el-select
|
||||
v-model="selectedButton"
|
||||
placeholder="请选择按钮"
|
||||
class="custom-select"
|
||||
:disabled="!selectedFunction"
|
||||
@change="onButtonChange"
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="button in buttonOptions"
|
||||
:key="button.value"
|
||||
:label="button.label"
|
||||
:value="button.value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
// @ts-ignore
|
||||
import { getFeatureUsageData } from '@/api/userActivity.js';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const props = defineProps<{
|
||||
propsData: any
|
||||
}>();
|
||||
|
||||
const propsData = ref(props.propsData);
|
||||
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
// 三级数据结构
|
||||
const dataStructure = ref({
|
||||
training: {
|
||||
label: '训练',
|
||||
functions: {
|
||||
personal: {
|
||||
label: '个人',
|
||||
buttons: {
|
||||
fitness: { label: '健身减肥', usage: 16159 },
|
||||
primaryTest: { label: '小学体测', usage: 12500 },
|
||||
middleExam: { label: '中学考试', usage: 9800 }
|
||||
}
|
||||
},
|
||||
team: {
|
||||
label: '团队',
|
||||
buttons: {
|
||||
createGroup: { label: '创建群组', usage: 10010 },
|
||||
createTask: { label: '创建任务', usage: 8500 }
|
||||
}
|
||||
},
|
||||
checkin: {
|
||||
label: '打卡',
|
||||
buttons: {
|
||||
setGoal: { label: '设置目标', usage: 7562 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
teaching: {
|
||||
label: '教学',
|
||||
functions: {
|
||||
banner: {
|
||||
label: 'banner图',
|
||||
buttons: {
|
||||
bannerManage: { label: 'banner管理', usage: 5200 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 选择器状态
|
||||
const selectedModule = ref('');
|
||||
const selectedFunction = ref('');
|
||||
const selectedButton = ref('');
|
||||
|
||||
// 模块选项
|
||||
const moduleOptions = computed(() => {
|
||||
return Object.keys(dataStructure.value).map(key => ({
|
||||
value: key,
|
||||
label: dataStructure.value[key].label
|
||||
}));
|
||||
});
|
||||
|
||||
// 功能选项(根据选中的模块动态变化)
|
||||
const functionOptions = computed(() => {
|
||||
if (!selectedModule.value) return [];
|
||||
const module = dataStructure.value[selectedModule.value];
|
||||
if (!module) return [];
|
||||
|
||||
return Object.keys(module.functions).map(key => ({
|
||||
value: key,
|
||||
label: module.functions[key].label
|
||||
}));
|
||||
});
|
||||
|
||||
// 按钮选项(根据选中的功能动态变化)
|
||||
const buttonOptions = computed(() => {
|
||||
if (!selectedModule.value || !selectedFunction.value) return [];
|
||||
const module = dataStructure.value[selectedModule.value];
|
||||
if (!module) return [];
|
||||
|
||||
const func = module.functions[selectedFunction.value];
|
||||
if (!func) return [];
|
||||
|
||||
return Object.keys(func.buttons).map(key => ({
|
||||
value: key,
|
||||
label: func.buttons[key].label
|
||||
}));
|
||||
});
|
||||
|
||||
// 联动事件处理
|
||||
const onModuleChange = () => {
|
||||
selectedFunction.value = '';
|
||||
selectedButton.value = '';
|
||||
updateChart();
|
||||
};
|
||||
|
||||
const onFunctionChange = () => {
|
||||
selectedButton.value = '';
|
||||
updateChart();
|
||||
};
|
||||
|
||||
const onButtonChange = () => {
|
||||
// 当按钮选择完成后,调用API获取真实数据
|
||||
if (selectedModule.value && selectedFunction.value && selectedButton.value) {
|
||||
fetchFeatureUsageData();
|
||||
} else {
|
||||
updateChart();
|
||||
}
|
||||
};
|
||||
|
||||
// 获取当前选择的数据
|
||||
const getCurrentData = () => {
|
||||
if (!selectedModule.value || !selectedFunction.value || !selectedButton.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const module = dataStructure.value[selectedModule.value];
|
||||
if (!module) return null;
|
||||
|
||||
const func = module.functions[selectedFunction.value];
|
||||
if (!func) return null;
|
||||
|
||||
const button = func.buttons[selectedButton.value];
|
||||
if (!button) return null;
|
||||
|
||||
return button;
|
||||
} catch (error) {
|
||||
console.error('获取数据时出错:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 从API获取功能使用数据
|
||||
const fetchFeatureUsageData = async () => {
|
||||
if (!selectedModule.value || !selectedFunction.value || !selectedButton.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
timeRange: 'week',
|
||||
date: new Date(),
|
||||
module: selectedModule.value,
|
||||
function: selectedFunction.value,
|
||||
button: selectedButton.value
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
const response = await getFeatureUsageData(params);
|
||||
|
||||
if (response.data && response.data.XAxisData && response.data.SeriesData) {
|
||||
// 更新图表数据
|
||||
updateChartWithApiData(response.data.XAxisData, response.data.SeriesData);
|
||||
} else {
|
||||
// 如果API返回的数据格式不正确,使用本地数据
|
||||
updateChart();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取功能使用数据失败:', error);
|
||||
ElMessage.error('获取数据失败,使用本地数据');
|
||||
updateChart();
|
||||
}
|
||||
};
|
||||
|
||||
// 使用API数据更新图表
|
||||
const updateChartWithApiData = (xAxisData: string[], seriesData: number[]) => {
|
||||
if (!chart) return;
|
||||
|
||||
const currentData = getCurrentData();
|
||||
if (!currentData) return;
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: `${selectedModule.value ? dataStructure.value[selectedModule.value].label : ''} - ${selectedFunction.value ? dataStructure.value[selectedModule.value]?.functions[selectedFunction.value]?.label : ''} - ${currentData.label}`,
|
||||
left: 'left',
|
||||
top: '5%',
|
||||
textStyle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 13
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
return `${params[0].name}<br/>${params[0].seriesName}: ${params[0].value.toLocaleString()}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '8%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: xAxisData,
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 12,
|
||||
interval: 0
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 12,
|
||||
formatter: function(value: number) {
|
||||
if (value >= 10000) {
|
||||
return (value / 10000) + '万';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '使用量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: seriesData,
|
||||
itemStyle: {
|
||||
color: '#4da6ff'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#4da6ff',
|
||||
width: 3
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: 'rgba(77, 166, 255, 0.4)'
|
||||
}, {
|
||||
offset: 1, color: 'rgba(77, 166, 255, 0.1)'
|
||||
}]
|
||||
}
|
||||
},
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: '#ffffff',
|
||||
borderColor: '#4da6ff',
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById('functionChart');
|
||||
if (chartDom) {
|
||||
chart = echarts.init(chartDom);
|
||||
updateChart();
|
||||
} else {
|
||||
console.error('找不到 functionChart 元素');
|
||||
}
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chart) return;
|
||||
|
||||
const currentData = getCurrentData();
|
||||
|
||||
if (!currentData) {
|
||||
// 如果没有完整选择,显示提示
|
||||
chart.setOption({
|
||||
title: {
|
||||
text: '请选择模块、功能和按钮',
|
||||
left: 'center',
|
||||
top: 'middle',
|
||||
textStyle: {
|
||||
color: '#8690a5',
|
||||
fontSize: 14
|
||||
}
|
||||
},
|
||||
xAxis: { data: [] },
|
||||
yAxis: {},
|
||||
series: [{ data: [] }]
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成7天的模拟数据,基于选中按钮的使用量
|
||||
const dates = [];
|
||||
const data = [];
|
||||
const baseUsage = currentData.usage;
|
||||
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dates.push(`${date.getMonth() + 1}-${date.getDate()}`);
|
||||
// 基于基础使用量生成变化数据
|
||||
const variation = Math.floor(Math.random() * (baseUsage * 0.3)) - (baseUsage * 0.15);
|
||||
data.push(Math.max(100, Math.floor(baseUsage + variation)));
|
||||
}
|
||||
|
||||
const option = {
|
||||
title: {
|
||||
text: `${selectedModule.value ? dataStructure.value[selectedModule.value].label : ''} - ${selectedFunction.value ? dataStructure.value[selectedModule.value]?.functions[selectedFunction.value]?.label : ''} - ${currentData.label}`,
|
||||
left: 'left',
|
||||
top: '5%',
|
||||
textStyle: {
|
||||
color: '#ffffff',
|
||||
fontSize: 13
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'shadow'
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
formatter: function(params: any) {
|
||||
return `${params[0].name}<br/>${params[0].seriesName}: ${params[0].value.toLocaleString()}`;
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '8%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 12,
|
||||
interval: 0
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 12,
|
||||
formatter: function(value: number) {
|
||||
if (value >= 10000) {
|
||||
return (value / 10000) + '万';
|
||||
}
|
||||
return value;
|
||||
}
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '使用量',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: data,
|
||||
itemStyle: {
|
||||
color: '#4da6ff'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#4da6ff',
|
||||
width: 3
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: 'rgba(77, 166, 255, 0.4)'
|
||||
}, {
|
||||
offset: 1, color: 'rgba(77, 166, 255, 0.1)'
|
||||
}]
|
||||
}
|
||||
},
|
||||
symbol: 'circle',
|
||||
symbolSize: 8,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
color: '#ffffff',
|
||||
borderColor: '#4da6ff',
|
||||
borderWidth: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
// 监听选择器变化
|
||||
watch([selectedModule, selectedFunction, selectedButton], () => {
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
|
||||
watch(() => props.propsData, (newData) => {
|
||||
propsData.value = newData;
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
|
||||
// 设置默认选择
|
||||
selectedModule.value = 'training';
|
||||
selectedFunction.value = 'personal';
|
||||
selectedButton.value = 'fitness';
|
||||
|
||||
// 延迟调用API获取真实数据,确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
fetchFeatureUsageData();
|
||||
}, 100);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.functionUsageChart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background: url("@/assets/images/allImg/内容背景1.png") no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-left: 10px;
|
||||
padding-top: 15px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: calc(100% - 45px);
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
|
||||
.chartContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.functionSelector {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: 5px;
|
||||
z-index: 10;
|
||||
background: rgba(30, 40, 60, 0.9);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(77, 166, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.cascaderContainer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
|
||||
.selectGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-width: 100px;
|
||||
|
||||
.selectLabel {
|
||||
font-size: 11px;
|
||||
color: #8690a5;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.custom-select) {
|
||||
width: 110px;
|
||||
|
||||
.el-input {
|
||||
.el-input__wrapper {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(77, 166, 255, 0.3);
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
height: 30px;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(77, 166, 255, 0.6);
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
border-color: #4da6ff;
|
||||
box-shadow: 0 0 0 2px rgba(77, 166, 255, 0.2);
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0 8px;
|
||||
|
||||
&::placeholder {
|
||||
color: #8690a5;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__suffix {
|
||||
.el-input__suffix-inner {
|
||||
.el-select__caret {
|
||||
color: #8690a5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
.el-input__wrapper {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
.el-input__inner {
|
||||
color: #5a6c7d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1200px) {
|
||||
top: 3px;
|
||||
right: 3px;
|
||||
padding: 8px;
|
||||
|
||||
.cascaderContainer {
|
||||
gap: 8px;
|
||||
|
||||
.selectGroup {
|
||||
min-width: 90px;
|
||||
|
||||
:deep(.custom-select) {
|
||||
width: 90px;
|
||||
}
|
||||
|
||||
.selectLabel {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: relative;
|
||||
top: auto;
|
||||
right: auto;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
|
||||
.cascaderContainer {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
.selectGroup {
|
||||
flex: 1;
|
||||
min-width: 90px;
|
||||
|
||||
:deep(.custom-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 全局下拉框样式 */
|
||||
:deep(.el-select-dropdown) {
|
||||
background: rgba(30, 40, 60, 0.95);
|
||||
border: 1px solid rgba(77, 166, 255, 0.3);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
.el-select-dropdown__item {
|
||||
color: #ffffff;
|
||||
font-size: 11px;
|
||||
padding: 6px 10px;
|
||||
min-height: 28px;
|
||||
line-height: 16px;
|
||||
|
||||
&:hover {
|
||||
background: rgba(77, 166, 255, 0.2);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: rgba(77, 166, 255, 0.3);
|
||||
color: #4da6ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: #5a6c7d;
|
||||
}
|
||||
}
|
||||
|
||||
.el-popper__arrow {
|
||||
&::before {
|
||||
border-bottom-color: rgba(30, 40, 60, 0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 小尺寸下拉框的特殊样式 */
|
||||
:deep(.el-select--small) {
|
||||
.el-select-dropdown__item {
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
min-height: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
283
src/views/userActivity/components/OverviewCard.vue
Normal file
283
src/views/userActivity/components/OverviewCard.vue
Normal file
@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="overviewCard">
|
||||
<p class="title">综合情况</p>
|
||||
<div class="content">
|
||||
<div class="dataSection">
|
||||
<div class="dataItem">
|
||||
<div class="label">新增用户数</div>
|
||||
<div class="valueRow">
|
||||
<span class="value">{{ propsData.newUsers.today.toLocaleString() }}</span>
|
||||
<span class="unit">较昨日</span>
|
||||
<span class="growth" :class="propsData.newUsers.growth >= 0 ? 'positive' : 'negative'">
|
||||
{{ propsData.newUsers.growth >= 0 ? '+' : '' }}{{ propsData.newUsers.growth }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dataItem">
|
||||
<div class="label">活跃用户数</div>
|
||||
<div class="valueRow">
|
||||
<span class="value">{{ propsData.activeUsers.today.toLocaleString() }}</span>
|
||||
<span class="unit">较昨日</span>
|
||||
<span class="growth" :class="propsData.activeUsers.growth >= 0 ? 'positive' : 'negative'">
|
||||
{{ propsData.activeUsers.growth >= 0 ? '+' : '' }}{{ propsData.activeUsers.growth }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chartSection">
|
||||
<div class="chart" id="overviewChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const props = defineProps<{
|
||||
propsData: any
|
||||
}>();
|
||||
|
||||
const propsData = ref(props.propsData);
|
||||
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
const chartDom = document.getElementById('overviewChart');
|
||||
if (chartDom) {
|
||||
chart = echarts.init(chartDom);
|
||||
updateChart();
|
||||
} else {
|
||||
console.error('找不到 overviewChart 元素');
|
||||
}
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
if (!chart) return;
|
||||
|
||||
// 生成24小时的模拟数据
|
||||
const hours = [];
|
||||
const newUsersData = [];
|
||||
const activeUsersData = [];
|
||||
|
||||
for (let i = 0; i < 24; i++) {
|
||||
hours.push(`${i.toString().padStart(2, '0')}:00`);
|
||||
newUsersData.push(Math.floor(Math.random() * 500) + 200);
|
||||
activeUsersData.push(Math.floor(Math.random() * 800) + 400);
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross'
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: ['新增用户', '活跃用户'],
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
},
|
||||
top: 10
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: hours,
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 10
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#8690a5',
|
||||
fontSize: 10
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: '#2c3e50'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: newUsersData,
|
||||
itemStyle: {
|
||||
color: '#ff6b9d'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#ff6b9d',
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: 'rgba(255, 107, 157, 0.3)'
|
||||
}, {
|
||||
offset: 1, color: 'rgba(255, 107, 157, 0.1)'
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '活跃用户',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: activeUsersData,
|
||||
itemStyle: {
|
||||
color: '#4ecdc4'
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#4ecdc4',
|
||||
width: 2
|
||||
},
|
||||
areaStyle: {
|
||||
color: {
|
||||
type: 'linear',
|
||||
x: 0,
|
||||
y: 0,
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: 'rgba(78, 205, 196, 0.3)'
|
||||
}, {
|
||||
offset: 1, color: 'rgba(78, 205, 196, 0.1)'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
};
|
||||
|
||||
watch(() => props.propsData, (newData) => {
|
||||
propsData.value = newData;
|
||||
updateChart();
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.overviewCard {
|
||||
width: 100%;
|
||||
height: 380px;
|
||||
background: url("@/assets/images/allImg/内容背景1.png") no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-left: 10px;
|
||||
padding-top: 15px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 45px);
|
||||
padding: 10px;
|
||||
|
||||
.dataSection {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.dataItem {
|
||||
text-align: center;
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: #8690a5;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.valueRow {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
|
||||
.value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-size: 10px;
|
||||
color: #8690a5;
|
||||
}
|
||||
|
||||
.growth {
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
|
||||
&.positive {
|
||||
color: #4ecdc4;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: #ff6b9d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chartSection {
|
||||
flex: 1;
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
379
src/views/userActivity/components/RegionAnalysis.vue
Normal file
379
src/views/userActivity/components/RegionAnalysis.vue
Normal file
@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="regionAnalysis">
|
||||
<p class="title">地域分析(用户)</p>
|
||||
<div class="content">
|
||||
<div class="mapContainer">
|
||||
<div class="map" id="regionMap"></div>
|
||||
</div>
|
||||
<div class="ranking">
|
||||
<div class="rankingItem" v-for="(item, index) in rankingData" :key="index">
|
||||
<div class="rank">{{ index + 1 }}</div>
|
||||
<div class="name">{{ item.name }}</div>
|
||||
<div class="bar">
|
||||
<div class="progress" :style="{ width: getProgressWidth(item.value) + '%' }"></div>
|
||||
</div>
|
||||
<div class="value">{{ (Number(item.value) || 0).toLocaleString() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
// @ts-ignore
|
||||
import chinaMap from "@/assets/json/china.json";
|
||||
|
||||
const props = defineProps<{
|
||||
propsData: any
|
||||
}>();
|
||||
|
||||
const propsData = ref(props.propsData);
|
||||
|
||||
let mapChart: echarts.ECharts | null = null;
|
||||
|
||||
// 默认地区列表(主要省市)
|
||||
const defaultRegions = [
|
||||
{ name: '北京', value: 0 },
|
||||
{ name: '上海', value: 0 },
|
||||
{ name: '广东', value: 0 },
|
||||
{ name: '浙江', value: 0 },
|
||||
{ name: '江苏', value: 0 },
|
||||
{ name: '山东', value: 0 },
|
||||
{ name: '河南', value: 0 },
|
||||
{ name: '四川', value: 0 },
|
||||
{ name: '湖北', value: 0 },
|
||||
{ name: '福建', value: 0 },
|
||||
{ name: '湖南', value: 0 },
|
||||
{ name: '安徽', value: 0 },
|
||||
{ name: '河北', value: 0 },
|
||||
{ name: '江西', value: 0 },
|
||||
{ name: '重庆', value: 0 },
|
||||
{ name: '辽宁', value: 0 },
|
||||
{ name: '陕西', value: 0 },
|
||||
{ name: '广西', value: 0 },
|
||||
{ name: '云南', value: 0 },
|
||||
{ name: '山西', value: 0 },
|
||||
{ name: '贵州', value: 0 },
|
||||
{ name: '吉林', value: 0 },
|
||||
{ name: '黑龙江', value: 0 },
|
||||
{ name: '甘肃', value: 0 },
|
||||
{ name: '内蒙古', value: 0 },
|
||||
{ name: '新疆', value: 0 },
|
||||
{ name: '青海', value: 0 },
|
||||
{ name: '宁夏', value: 0 },
|
||||
{ name: '西藏', value: 0 },
|
||||
{ name: '海南', value: 0 },
|
||||
{ name: '天津', value: 0 }
|
||||
];
|
||||
|
||||
// 数据处理函数:确保数值类型正确
|
||||
const processRegionData = (data: any[]) => {
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return defaultRegions;
|
||||
}
|
||||
|
||||
return data.map(item => ({
|
||||
name: item.name || '',
|
||||
value: Number(item.value) || 0 // 确保转换为数字,NaN转为0
|
||||
})).filter(item => item.name); // 过滤掉名称为空的项
|
||||
};
|
||||
|
||||
// 合并API数据和默认数据
|
||||
const mergeRegionData = (apiData: any[]) => {
|
||||
const processedApiData = processRegionData(apiData);
|
||||
|
||||
// 如果API数据为空,返回默认数据
|
||||
if (processedApiData.length === 0) {
|
||||
return defaultRegions;
|
||||
}
|
||||
|
||||
// 创建API数据的映射
|
||||
const apiDataMap = new Map();
|
||||
processedApiData.forEach(item => {
|
||||
apiDataMap.set(item.name, item.value);
|
||||
});
|
||||
|
||||
// 合并数据:API数据优先,没有的地区使用默认值0
|
||||
return defaultRegions.map(defaultItem => ({
|
||||
name: defaultItem.name,
|
||||
value: apiDataMap.has(defaultItem.name) ? apiDataMap.get(defaultItem.name) : 0
|
||||
}));
|
||||
};
|
||||
|
||||
// 排行榜数据(取前10名,确保有数据显示)
|
||||
const rankingData = computed(() => {
|
||||
const mergedData = mergeRegionData(propsData.value.rankingData || propsData.value.mapData);
|
||||
|
||||
// 按值排序,取前10名
|
||||
return mergedData
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 10);
|
||||
});
|
||||
|
||||
// 地图数据(包含所有地区)
|
||||
const mapData = computed(() => {
|
||||
return mergeRegionData(propsData.value.mapData || propsData.value.rankingData);
|
||||
});
|
||||
|
||||
const getProgressWidth = (value: number) => {
|
||||
const values = rankingData.value.map(item => Number(item.value) || 0);
|
||||
const maxValue = Math.max(...values, 1); // 确保最大值至少为1,避免除零
|
||||
const numValue = Number(value) || 0;
|
||||
return (numValue / maxValue) * 100;
|
||||
};
|
||||
|
||||
const initMap = () => {
|
||||
const chartDom = document.getElementById('regionMap');
|
||||
if (chartDom) {
|
||||
mapChart = echarts.init(chartDom);
|
||||
updateMap();
|
||||
} else {
|
||||
console.error('找不到 regionMap 元素');
|
||||
}
|
||||
};
|
||||
|
||||
const updateMap = () => {
|
||||
if (!mapChart) return;
|
||||
|
||||
// 使用处理后的地图数据
|
||||
const currentMapData = mapData.value;
|
||||
|
||||
try {
|
||||
// 注册中国地图
|
||||
echarts.registerMap('china', chinaMap);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params: any) {
|
||||
const value = Number(params.value) || 0;
|
||||
return `${params.name}: ${value.toLocaleString()}人`;
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
visualMap: {
|
||||
min: 0,
|
||||
max: 20000,
|
||||
left: 5,
|
||||
bottom: 10,
|
||||
text: ['高', '低'],
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 8
|
||||
},
|
||||
inRange: {
|
||||
color: ['#0f4c75', '#3282b8', '#4da6ff', '#7ec8e3', '#a8e6cf']
|
||||
},
|
||||
itemWidth: 10,
|
||||
itemHeight: 40,
|
||||
show: true
|
||||
},
|
||||
geo: {
|
||||
map: 'china',
|
||||
roam: false,
|
||||
zoom: 1.1,
|
||||
center: [104, 35],
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: '#4da6ff',
|
||||
borderWidth: 1,
|
||||
areaColor: '#0f2027'
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
areaColor: '#4da6ff',
|
||||
shadowBlur: 10,
|
||||
shadowColor: 'rgba(77, 166, 255, 0.5)'
|
||||
}
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用户数量',
|
||||
type: 'map',
|
||||
geoIndex: 0,
|
||||
data: currentMapData,
|
||||
itemStyle: {
|
||||
borderColor: '#4da6ff',
|
||||
borderWidth: 1
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
areaColor: '#4da6ff'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mapChart.setOption(option);
|
||||
} catch (error) {
|
||||
console.error('地图加载失败,使用饼图作为备选方案:', error);
|
||||
|
||||
// 降级方案:使用饼图
|
||||
const fallbackOption = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params: any) {
|
||||
const value = Number(params.value) || 0;
|
||||
return `${params.name}: ${value.toLocaleString()}人`;
|
||||
},
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
textStyle: {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '用户分布',
|
||||
type: 'pie',
|
||||
radius: ['30%', '70%'],
|
||||
center: ['50%', '50%'],
|
||||
data: currentMapData.slice(0, 8).map((item: any, index: number) => ({
|
||||
...item,
|
||||
itemStyle: {
|
||||
color: ['#4da6ff', '#ff6b9d', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57', '#ff9ff3', '#54a0ff'][index]
|
||||
}
|
||||
})),
|
||||
label: {
|
||||
show: true,
|
||||
position: 'outside',
|
||||
formatter: '{b}\n{c}',
|
||||
fontSize: 10,
|
||||
color: '#fff'
|
||||
},
|
||||
labelLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#8690a5'
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mapChart.setOption(fallbackOption);
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.propsData, (newData) => {
|
||||
propsData.value = newData;
|
||||
updateMap();
|
||||
}, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
initMap();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.regionAnalysis {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
background: url("@/assets/images/allImg/内容背景1.png") no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-left: 10px;
|
||||
padding-top: 15px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
height: calc(100% - 45px);
|
||||
padding: 10px;
|
||||
|
||||
.mapContainer {
|
||||
width: 60%;
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking {
|
||||
width: 40%;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
.rankingItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.rank {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #4da6ff 0%, #1a87ff 100%);
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.name {
|
||||
width: 40px;
|
||||
font-size: 12px;
|
||||
color: #ffffff;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
position: relative;
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4da6ff 0%, #1a87ff 100%);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 40px;
|
||||
font-size: 10px;
|
||||
color: #8690a5;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
126
src/views/userActivity/components/UserCountCard.vue
Normal file
126
src/views/userActivity/components/UserCountCard.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<div class="userCountCard">
|
||||
<p class="title">用户总数</p>
|
||||
<div class="content">
|
||||
<div class="digitalDisplay">
|
||||
<div class="digit" v-for="(digit, index) in displayDigits" :key="index">
|
||||
{{ digit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<p>地域分析(用户)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
propsData: any
|
||||
}>();
|
||||
|
||||
const propsData = ref(props.propsData);
|
||||
|
||||
// 将总数转换为9位数字显示
|
||||
const displayDigits = computed(() => {
|
||||
const total = propsData.value.total || "000000000";
|
||||
return total.toString().padStart(9, '0').split('');
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 可以在这里添加数字滚动动画
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.userCountCard {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: url("@/assets/images/allImg/内容背景1.png") no-repeat;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
margin-bottom: 18px;
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
margin-left: 10px;
|
||||
padding-top: 15px;
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
height: calc(100% - 45px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.digitalDisplay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.digit {
|
||||
width: 35px;
|
||||
height: 50px;
|
||||
background: linear-gradient(135deg, #1a87ff 0%, #0066cc 100%);
|
||||
border: 2px solid #4da6ff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
margin: 0 2px;
|
||||
box-shadow: 0 4px 8px rgba(26, 135, 255, 0.3);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
height: 20px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
// 添加千位分隔符样式
|
||||
&:nth-child(3n):not(:last-child)::after {
|
||||
content: ',';
|
||||
position: absolute;
|
||||
right: -12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: #8690a5;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
463
src/views/userActivity/index.vue
Normal file
463
src/views/userActivity/index.vue
Normal file
@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div class="userActivity" ref="userActivity" id="userActivity" :style="{ backgroundImage: `url(${imageUrls[0]})` }"
|
||||
v-if="preloadedImages[imageUrls[0]]">
|
||||
<div class="userActivityHeader" :style="{ backgroundImage: `url(${imageUrls[1]})` }"
|
||||
v-if="preloadedImages[imageUrls[1]]">
|
||||
<div class="headerLeft">
|
||||
<p>用户活跃度统计</p>
|
||||
</div>
|
||||
|
||||
<div class="fullScreen" v-if="!isFullScreen" @click="toFullScreen">
|
||||
<img src="@/assets/images/testImg/全屏.png" alt="" />
|
||||
<span class="fullScreenText">全屏</span>
|
||||
</div>
|
||||
<div class="fullScreen" v-else @click="toFullScreen">
|
||||
<img src="@/assets/images/testImg/退出全屏.png" alt="" />
|
||||
<span class="fullScreenText">退出全屏</span>
|
||||
</div>
|
||||
<div class="time">
|
||||
{{ _date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="userActivityMain" v-if="apiEnd">
|
||||
<div class="userActivityMainLeft">
|
||||
<OverviewCard :propsData="overviewData"></OverviewCard>
|
||||
<FunctionUsageChart :propsData="functionUsageData"></FunctionUsageChart>
|
||||
</div>
|
||||
<div class="userActivityMainRight">
|
||||
<UserCountCard :propsData="userCountData"></UserCountCard>
|
||||
<RegionAnalysis :propsData="regionData"></RegionAnalysis>
|
||||
<AgeGenderChart :propsData="ageGenderData"></AgeGenderChart>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="userActivityFoot" :style="{ backgroundImage: `url(${imageUrls[2]})` }"
|
||||
v-if="preloadedImages[imageUrls[2]]">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, watch, watchEffect, nextTick } from "vue";
|
||||
import screenfull from "screenfull";
|
||||
import OverviewCard from "@/views/userActivity/components/OverviewCard.vue";
|
||||
import UserCountCard from "@/views/userActivity/components/UserCountCard.vue";
|
||||
import RegionAnalysis from "@/views/userActivity/components/RegionAnalysis.vue";
|
||||
import AgeGenderChart from "@/views/userActivity/components/AgeGenderChart.vue";
|
||||
import FunctionUsageChart from "@/views/userActivity/components/FunctionUsageChart.vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
// @ts-ignore
|
||||
import * as userActivityApi from "@/api/userActivity.js";
|
||||
|
||||
const imageUrls = [
|
||||
require("@/assets/images/allImg/大背景.png"),
|
||||
require('@/assets/images/allImg/标题背景二号方案.png'),
|
||||
require("@/assets/images/allImg/底部.png"),
|
||||
];
|
||||
|
||||
// 创建一个数组用于存储预加载完成的图片 URLs
|
||||
const preloadedImages = ref<{ [key: string]: boolean }>({});
|
||||
|
||||
const week = new Array(
|
||||
"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"
|
||||
);
|
||||
|
||||
const _date = ref("");
|
||||
const timeRange = ref("30days");
|
||||
const isFullScreen = ref(false);
|
||||
const apiEnd = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
const timeOptions = ref([
|
||||
{ label: "近7天", value: "7days" },
|
||||
{ label: "近30天", value: "30days" },
|
||||
{ label: "近3个月", value: "3months" },
|
||||
{ label: "近1年", value: "1year" }
|
||||
]);
|
||||
|
||||
const overviewData = ref({
|
||||
newUsers: {
|
||||
today: 7000,
|
||||
growth: 12,
|
||||
chartData: []
|
||||
},
|
||||
activeUsers: {
|
||||
today: 1821,
|
||||
growth: 11,
|
||||
chartData: []
|
||||
}
|
||||
});
|
||||
|
||||
const userCountData = ref({
|
||||
total: "002100304"
|
||||
});
|
||||
|
||||
const regionData = ref({
|
||||
mapData: [],
|
||||
rankingData: []
|
||||
});
|
||||
|
||||
const ageGenderData = ref({
|
||||
ageData: [],
|
||||
genderData: {
|
||||
male: 60,
|
||||
female: 40
|
||||
}
|
||||
});
|
||||
|
||||
const functionUsageData = ref({
|
||||
chartData: [],
|
||||
categories: ["功能使用情况统计"]
|
||||
});
|
||||
// 时间显示函数
|
||||
const showTime = () => {
|
||||
const date = new Date();
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
const day = date.getDate();
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
const seconds = date.getSeconds();
|
||||
const weekDay = week[date.getDay()];
|
||||
|
||||
return `${year}年${month}月${day}日 ${weekDay} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 全屏切换
|
||||
const toFullScreen = () => {
|
||||
isFullScreen.value = !isFullScreen.value;
|
||||
try {
|
||||
(screenfull as any).toggle(document.getElementById('userActivity'));
|
||||
} catch (error) {
|
||||
console.log('全屏功能不支持');
|
||||
}
|
||||
};
|
||||
|
||||
// 时间选择变化
|
||||
const onTimeRangeChange = (value: string) => {
|
||||
console.log('时间选择变化:', value);
|
||||
initApi(value);
|
||||
};
|
||||
|
||||
// 初始化API数据
|
||||
const initApi = async (timeRangeValue: string) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const currentDate = new Date();
|
||||
const params = {
|
||||
timeRange: timeRangeValue === "7days" ? "week" : timeRangeValue === "30days" ? "month" : "day",
|
||||
date: currentDate
|
||||
};
|
||||
|
||||
// 并行调用所有API
|
||||
const [
|
||||
overviewResponse,
|
||||
activeUsersResponse,
|
||||
regionResponse,
|
||||
demographicsResponse,
|
||||
totalUsersResponse,
|
||||
newUsersResponse
|
||||
] = await Promise.all([
|
||||
userActivityApi.getUserActivityOverview(params),
|
||||
userActivityApi.getActiveUsersData(params),
|
||||
userActivityApi.getRegionData(params),
|
||||
userActivityApi.getDemographicsData(params),
|
||||
userActivityApi.getTotalUsersData(params),
|
||||
userActivityApi.getNewUsersData(params)
|
||||
]);
|
||||
|
||||
// 更新概览数据
|
||||
overviewData.value = {
|
||||
newUsers: {
|
||||
today: overviewResponse.data?.NewUsers || 0,
|
||||
growth: overviewResponse.data?.NewUsersTrend || 0,
|
||||
chartData: newUsersResponse.data?.SeriesData?.map((value, index) => ({
|
||||
time: newUsersResponse.data.XAxisData[index],
|
||||
value: value
|
||||
})) || []
|
||||
},
|
||||
activeUsers: {
|
||||
today: overviewResponse.data?.ActiveUsers || 0,
|
||||
growth: overviewResponse.data?.ActiveUsersTrend || 0,
|
||||
chartData: activeUsersResponse.data?.SeriesData?.map((value, index) => ({
|
||||
time: activeUsersResponse.data.XAxisData[index],
|
||||
value: value
|
||||
})) || []
|
||||
}
|
||||
};
|
||||
|
||||
// 更新用户总数数据
|
||||
userCountData.value = {
|
||||
total: (totalUsersResponse.data?.TotalUsers || 0).toString().padStart(9, '0')
|
||||
};
|
||||
|
||||
// 更新地区数据
|
||||
regionData.value = {
|
||||
mapData: regionResponse.data?.RegionUsers || [],
|
||||
rankingData: regionResponse.data?.RegionUsers?.slice(0, 5) || []
|
||||
};
|
||||
|
||||
// 更新年龄性别数据
|
||||
ageGenderData.value = {
|
||||
ageData: demographicsResponse.data?.AgeData || [],
|
||||
genderData: {
|
||||
male: demographicsResponse.data?.GenderData?.find(g => g.Name === '男')?.Value || 50,
|
||||
female: demographicsResponse.data?.GenderData?.find(g => g.Name === '女')?.Value || 50
|
||||
}
|
||||
};
|
||||
|
||||
// 功能使用数据将由FunctionUsageChart组件自己获取
|
||||
functionUsageData.value = {
|
||||
chartData: [],
|
||||
categories: ["功能使用情况统计"]
|
||||
};
|
||||
|
||||
apiEnd.value = true;
|
||||
} catch (error) {
|
||||
console.error("Error occurred while calling API:", error);
|
||||
ElMessage.error("数据加载失败,使用模拟数据");
|
||||
|
||||
// 如果API调用失败,使用模拟数据
|
||||
overviewData.value = {
|
||||
newUsers: {
|
||||
today: 7000,
|
||||
growth: 12,
|
||||
chartData: generateMockChartData()
|
||||
},
|
||||
activeUsers: {
|
||||
today: 1821,
|
||||
growth: 11,
|
||||
chartData: generateMockChartData()
|
||||
}
|
||||
};
|
||||
|
||||
regionData.value = {
|
||||
mapData: generateMockMapData(),
|
||||
rankingData: generateMockRankingData()
|
||||
};
|
||||
|
||||
ageGenderData.value = {
|
||||
ageData: generateMockAgeData(),
|
||||
genderData: { male: 60, female: 40 }
|
||||
};
|
||||
|
||||
functionUsageData.value = {
|
||||
chartData: generateMockFunctionData(),
|
||||
categories: ["功能使用情况统计"]
|
||||
};
|
||||
|
||||
apiEnd.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 生成模拟数据的函数
|
||||
const generateMockChartData = () => {
|
||||
const data = [];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
data.push({
|
||||
time: `${i.toString().padStart(2, '0')}:00`,
|
||||
value: Math.floor(Math.random() * 10000) + 1000
|
||||
});
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const generateMockMapData = () => {
|
||||
return [
|
||||
{ name: '北京', value: 15000 },
|
||||
{ name: '上海', value: 12000 },
|
||||
{ name: '广东', value: 18000 },
|
||||
{ name: '浙江', value: 8000 },
|
||||
{ name: '江苏', value: 9000 }
|
||||
];
|
||||
};
|
||||
|
||||
const generateMockRankingData = () => {
|
||||
return [
|
||||
{ name: '广东', value: 18000 },
|
||||
{ name: '北京', value: 15000 },
|
||||
{ name: '上海', value: 12000 },
|
||||
{ name: '江苏', value: 9000 },
|
||||
{ name: '浙江', value: 8000 }
|
||||
];
|
||||
};
|
||||
|
||||
const generateMockAgeData = () => {
|
||||
return [
|
||||
{ name: '18-25', value: 30 },
|
||||
{ name: '26-35', value: 40 },
|
||||
{ name: '36-45', value: 20 },
|
||||
{ name: '46-55', value: 8 },
|
||||
{ name: '55+', value: 2 }
|
||||
];
|
||||
};
|
||||
|
||||
const generateMockFunctionData = () => {
|
||||
const dates = [];
|
||||
const data = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - i);
|
||||
dates.push(`2024-01-${12 + i}`);
|
||||
data.push(Math.floor(Math.random() * 40000) + 10000);
|
||||
}
|
||||
return { dates, data };
|
||||
};
|
||||
|
||||
// 图片预加载函数
|
||||
const preloadImages = () => {
|
||||
imageUrls.forEach((url) => {
|
||||
const image = new Image();
|
||||
image.src = url;
|
||||
image.onload = () => {
|
||||
preloadedImages.value[url] = true;
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// 数据刷新机制
|
||||
const refreshData = () => {
|
||||
console.log('刷新数据...');
|
||||
initApi(timeRange.value);
|
||||
};
|
||||
|
||||
// 在组件挂载时调用
|
||||
onMounted(() => {
|
||||
initApi(timeRange.value);
|
||||
preloadImages();
|
||||
|
||||
// 时间显示更新
|
||||
setInterval(function () {
|
||||
_date.value = showTime();
|
||||
}, 1000);
|
||||
|
||||
// 数据自动刷新(每5分钟刷新一次)
|
||||
setInterval(() => {
|
||||
refreshData();
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
window.onresize = () => {
|
||||
try {
|
||||
if (!(screenfull as any).isFullscreen) {
|
||||
isFullScreen.value = false;
|
||||
}
|
||||
} catch (error) {
|
||||
isFullScreen.value = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.userActivity {
|
||||
width: 100vw;
|
||||
height: 1080px;
|
||||
transform-origin: left top;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
|
||||
.userActivityHeader {
|
||||
width: 100%;
|
||||
height: 84px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.headerLeft {
|
||||
width: 75%;
|
||||
height: 20px;
|
||||
|
||||
p {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-family: zhuiguang;
|
||||
margin-left: 25px;
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.timeSelect {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
margin-right: 20px;
|
||||
|
||||
:deep(.el-input) {
|
||||
width: 140px;
|
||||
--el-input-bg-color: transparent;
|
||||
z-index: 9999;
|
||||
.el-input__inner {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fullScreen {
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
margin-top: 40px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.fullScreenText {
|
||||
font-size: 14px;
|
||||
font-family: PingFangSC;
|
||||
color: #ffffff;
|
||||
margin-right: 40px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
color: #fff;
|
||||
margin-top: 38px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.userActivityMain {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
margin-top: 80px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
z-index: 1;
|
||||
padding: 20px;
|
||||
|
||||
.userActivityMainLeft {
|
||||
width: 60%;
|
||||
height: 800px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.userActivityMainRight {
|
||||
width: 40%;
|
||||
height: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
.userActivityFoot {
|
||||
width: 100%;
|
||||
height: 452px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -243,14 +243,14 @@ const loadData = async () => {
|
||||
props.userId,
|
||||
searchForm.month
|
||||
)
|
||||
|
||||
if (res && res.status) {
|
||||
goalData.value = res.data.datas || []
|
||||
pagination.total = res.data.total || 0
|
||||
|
||||
if (res && res.datas) {
|
||||
goalData.value = res.datas || []
|
||||
pagination.total = res.total || 0
|
||||
} else {
|
||||
goalData.value = []
|
||||
pagination.total = 0
|
||||
//ElMessage.warning(res?.message || '获取打卡数据失败')
|
||||
//ElMessage.warning('获取打卡数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取打卡数据失败:', error)
|
||||
|
@ -138,14 +138,14 @@ const getTrainingRecords = async () => {
|
||||
endTime
|
||||
);
|
||||
|
||||
if (res.data) {
|
||||
data.tableNeed.tableData = res.data.datas.map((item, index) => {
|
||||
if (res && res.datas) {
|
||||
data.tableNeed.tableData = res.datas.map((item, index) => {
|
||||
return {
|
||||
...item,
|
||||
ind: (data.tableNeed.pageIndex - 1) * data.tableNeed.pageSize + index + 1
|
||||
};
|
||||
});
|
||||
data.tableNeed.total = res.data.total;
|
||||
data.tableNeed.total = res.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取训练记录失败', error);
|
||||
|
Loading…
x
Reference in New Issue
Block a user