763 lines
18 KiB
Vue
763 lines
18 KiB
Vue
<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>
|