dev #1

Merged
tengxiangli merged 3 commits from dev into master 2025-07-18 07:12:58 +00:00
13 changed files with 5556 additions and 16916 deletions
Showing only changes of commit c98b49f7fc - Show all commits

20001
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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 || ''}`);
}
/**

View File

@ -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
View 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()}`);
}

View File

@ -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',

View 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>

View 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';
// APIDOM
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>

View 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>

View 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 // NaN0
})).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>

View 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>

View 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>

View File

@ -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)

View File

@ -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);