完善仪表盘统计

This commit is contained in:
李志强 2026-02-05 15:23:37 +08:00
parent eec987041c
commit 858f7500bd
3 changed files with 235 additions and 116 deletions

View File

@ -166,3 +166,30 @@ export function deleteUser(id) {
method: "delete", method: "delete",
}); });
} }
/*************************************************
****************** 仪表盘相关接口 ******************
*************************************************/
/**
* 获取用户列表
* @returns {Promise}
*/
export function getUserCounts() {
return request({
url: "/admin/babyhealthDashborad/users",
method: "get",
});
}
/**
* 获取宝贝列表
* @returns {Promise}
*/
export function getBabyCounts() {
return request({
url: "/admin/babyhealthDashborad/babys",
method: "get",
});
}

View File

@ -1,114 +1,152 @@
<template> <template>
<div class="statistics-container"> <div class="statistics-container">
<!-- 第一行宝贝统计 -->
<el-row :gutter="20" class="data-overview"> <el-row :gutter="20" class="data-overview">
<el-col :span="6" v-for="item in summaryData" :key="item.title"> <el-col :span="8" v-for="item in babyData" :key="item.title">
<el-card shadow="hover" class="data-card"> <el-card shadow="hover" class="data-card">
<div class="card-content"> <div class="card-content">
<div class="icon-box" :style="{ backgroundColor: item.color }"> <div class="icon-box" :style="{ backgroundColor: item.color }">
<el-icon><component :is="item.icon" /></el-icon> <i :class="item.icon"></i>
</div> </div>
<div class="text-box"> <div class="text-box">
<div class="title">{{ item.title }}</div> <div class="title">{{ item.title }}</div>
<div class="value">{{ item.value.toLocaleString() }}</div> <div class="value">{{ item.value.toLocaleString() }}</div>
<div class="trend" :class="item.isUp ? 'up' : 'down'">
{{ item.isUp ? '↑' : '↓' }} {{ item.percentage }}%
<span>较上月</span>
</div>
</div> </div>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" class="charts-row"> <!-- 第二行用户统计 -->
<el-col :span="16"> <el-row :gutter="20" class="data-overview" style="margin-top: 20px;">
<el-card shadow="hover" header="用户增长趋势"> <el-col :span="8" v-for="item in userData" :key="item.title">
<div ref="lineChartRef" class="chart-box"></div> <el-card shadow="hover" class="data-card">
<div class="card-content">
<div class="icon-box" :style="{ backgroundColor: item.color }">
<i :class="item.icon"></i>
</div>
<div class="text-box">
<div class="title">{{ item.title }}</div>
<div class="value">{{ item.value.toLocaleString() }}</div>
</div>
</div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="8"> </el-row>
<el-card shadow="hover" header="用户等级分布">
<div ref="pieChartRef" class="chart-box"></div> <!-- 图表区域 -->
<el-row :gutter="20" class="charts-row" style="margin-top: 20px;">
<!-- 宝贝增长趋势柱状图 -->
<el-col :span="12">
<el-card shadow="hover" header="宝贝增长趋势">
<div ref="babyChartRef" class="chart-box"></div>
</el-card>
</el-col>
<!-- 用户增长趋势柱状图 -->
<el-col :span="12">
<el-card shadow="hover" header="用户增长趋势">
<div ref="userChartRef" class="chart-box"></div>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, shallowRef } from 'vue'; import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { User, Pointer, Connection, Histogram } from '@element-plus/icons-vue'; import { getBabyCounts, getUserCounts } from '@/api/babyhealth';
// --- --- // --- ---
interface SummaryItem { interface SummaryItem {
title: string; title: string;
value: number; value: number;
icon: any; icon: string;
color: string; color: string;
percentage: number; percentage: number;
isUp: boolean; isUp: boolean;
} }
// --- --- // --- ---
const lineChartRef = ref<HTMLElement | null>(null); const babyChartRef = ref<HTMLElement | null>(null);
const pieChartRef = ref<HTMLElement | null>(null); const userChartRef = ref<HTMLElement | null>(null);
const lineChartInstance = shallowRef<echarts.ECharts | null>(null); const babyChartInstance = shallowRef<echarts.ECharts | null>(null);
const pieChartInstance = shallowRef<echarts.ECharts | null>(null); const userChartInstance = shallowRef<echarts.ECharts | null>(null);
const summaryData = ref<SummaryItem[]>([ const babyData = ref<SummaryItem[]>([
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true }, { title: '总宝贝数', value: 0, icon: 'fa-solid fa-baby', color: '#67C23A', percentage: 0, isUp: false },
{ title: '今日新增', value: 156, icon: Pointer, color: '#67C23A', percentage: 5, isUp: true }, { title: '男宝宝数', value: 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false }, { title: '女宝宝数', value: 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
]); ]);
// --- --- const userData = ref<SummaryItem[]>([
const initCharts = () => { { title: '总用户数', value: 0, icon: 'fa-solid fa-users', color: '#3973FF', percentage: 0, isUp: false },
// 线 { title: '父亲数', value: 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
if (lineChartRef.value) { { title: '母亲数', value: 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
lineChartInstance.value = echarts.init(lineChartRef.value); ]);
lineChartInstance.value.setOption({
tooltip: { trigger: 'axis' }, //
async function fetchGetBabyDatas() {
const res = await getBabyCounts();
if (res.code === 200 && res.data) {
const { total, male, female } = res.data;
// babyData
babyData.value = [
{ title: '总宝贝数', value: total, icon: 'fa-solid fa-baby', color: '#67C23A', percentage: 0, isUp: false },
{ title: '男宝宝数', value: male !== undefined ? male : 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
{ title: '女宝宝数', value: female !== undefined ? female : 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
];
//
updateBabyChart(total, male, female);
}
}
//
async function fetchGetUserDatas() {
const res = await getUserCounts();
if (res.code === 200 && res.data) {
const { total, father, mother } = res.data;
// userData
userData.value = [
{ title: '总用户数', value: total, icon: 'fa-solid fa-users', color: '#3973FF', percentage: 0, isUp: false },
{ title: '父亲数', value: father !== undefined ? father : 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
{ title: '母亲数', value: mother !== undefined ? mother : 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
];
//
updateUserChart(total, father, mother);
}
}
//
const initBabyChart = () => {
if (babyChartRef.value) {
babyChartInstance.value = echarts.init(babyChartRef.value);
babyChartInstance.value.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, data: ['总宝贝数', '男宝宝数', '女宝宝数'],
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] axisTick: { alignWithLabel: true }
}, },
yAxis: { type: 'value' }, yAxis: { type: 'value' },
series: [ series: [
{ {
name: '新增用户', name: '数量',
type: 'line', type: 'bar',
smooth: true, barWidth: '60%',
data: [120, 132, 101, 134, 90, 230, 210],
areaStyle: { opacity: 0.3 },
itemStyle: { color: '#3973FF' }
}
]
});
}
//
if (pieChartRef.value) {
pieChartInstance.value = echarts.init(pieChartRef.value);
pieChartInstance.value.setOption({
tooltip: { trigger: 'item' },
legend: { bottom: '0%', left: 'center' },
series: [
{
name: '等级分布',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
label: { show: false },
data: [ data: [
{ value: 1048, name: '普通用户' }, { value: 0, itemStyle: { color: '#67C23A' } },
{ value: 735, name: 'VIP会员' }, { value: 0, itemStyle: { color: '#409EFF' } },
{ value: 580, name: '超级管理员' }, { value: 0, itemStyle: { color: '#F56C6C' } }
{ value: 484, name: '运营人员' }
] ]
} }
] ]
@ -116,81 +154,135 @@ const initCharts = () => {
} }
}; };
// --- --- //
const initUserChart = () => {
if (userChartRef.value) {
userChartInstance.value = echarts.init(userChartRef.value);
userChartInstance.value.setOption({
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: {
type: 'category',
data: ['总用户数', '父亲数', '母亲数'],
axisTick: { alignWithLabel: true }
},
yAxis: { type: 'value' },
series: [
{
name: '数量',
type: 'bar',
barWidth: '60%',
data: [
{ value: 0, itemStyle: { color: '#3973FF' } },
{ value: 0, itemStyle: { color: '#409EFF' } },
{ value: 0, itemStyle: { color: '#F56C6C' } }
]
}
]
});
}
};
//
const updateBabyChart = (total: number, male: number, female: number) => {
if (babyChartInstance.value) {
babyChartInstance.value.setOption({
series: [{
data: [
{ value: total, itemStyle: { color: '#67C23A' } },
{ value: male, itemStyle: { color: '#409EFF' } },
{ value: female, itemStyle: { color: '#F56C6C' } }
]
}]
});
}
};
//
const updateUserChart = (total: number, father: number, mother: number) => {
if (userChartInstance.value) {
userChartInstance.value.setOption({
series: [{
data: [
{ value: total, itemStyle: { color: '#3973FF' } },
{ value: father, itemStyle: { color: '#409EFF' } },
{ value: mother, itemStyle: { color: '#F56C6C' } }
]
}]
});
}
};
//
const handleResize = () => { const handleResize = () => {
lineChartInstance.value?.resize(); babyChartInstance.value?.resize();
pieChartInstance.value?.resize(); userChartInstance.value?.resize();
}; };
onMounted(() => { onMounted(() => {
initCharts(); initBabyChart();
initUserChart();
fetchGetBabyDatas();
fetchGetUserDatas();
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
babyChartInstance.value?.dispose();
userChartInstance.value?.dispose();
}); });
</script> </script>
<style scoped lang="scss"> <style lang="scss" scoped>
.statistics-container { .statistics-container {
padding: 20px; padding: 20px;
min-height: 100vh; min-height: 100vh;
}
.data-overview { .data-overview {
margin-bottom: 20px; margin-bottom: 20px;
}
.data-card { .data-card {
.card-content { .card-content {
display: flex; display: flex;
align-items: center; align-items: center;
.icon-box { .icon-box {
width: 56px; width: 56px;
height: 56px; height: 56px;
border-radius: 12px; border-radius: 12px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 15px; margin-right: 15px;
color: #fff; color: #fff;
font-size: 24px; font-size: 24px;
}
.text-box {
.title {
font-size: 14px;
color: #909399;
}
.value {
font-size: 24px;
font-weight: bold;
margin: 4px 0;
color: var(--el-text-color-primary);
}
.trend {
font-size: 12px;
&.up { color: #67c23a; }
&.down { color: #f56c6c; }
span { color: #909399; margin-left: 4px; }
}
}
}
} }
} }
.charts-row { .text-box {
.chart-box { .title {
height: 350px; font-size: 14px;
width: 100%; color: #909399;
}
.value {
font-size: 24px;
font-weight: bold;
margin: 4px 0;
color: var(--el-text-color-primary);
} }
} }
} }
// Element Plus .charts-row {
:deep(.el-card__header) { margin-top: 20px;
font-weight: bold;
font-size: 16px; .chart-box {
border-bottom: 1px solid #ebeef5; height: 350px;
width: 100%;
}
} }
</style> </style>

View File

@ -106,7 +106,7 @@ import { ElMessage } from "element-plus";
import { Plus } from '@element-plus/icons-vue'; import { Plus } from '@element-plus/icons-vue';
import type { UploadProps, UploadRequestOptions } from 'element-plus'; import type { UploadProps, UploadRequestOptions } from 'element-plus';
import { createUser, updateUser, getUserDetail } from "@/api/babyhealth"; import { createUser, updateUser, getUserDetail } from "@/api/babyhealth";
import { uploadAvatar } from '@/api/upload'; import { uploadAvatar } from '@/api/file';
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {