763 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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