文章目录
- [Softhub软件下载站实战开发(十六):仪表盘前端设计与实现 🎛️](#Softhub软件下载站实战开发(十六):仪表盘前端设计与实现 🎛️)
-
- 前言
- [主要功能点 ✨](#主要功能点 ✨)
- [技术实现 🛠️](#技术实现 🛠️)
-
- [1. 接口设计与类型定义](#1. 接口设计与类型定义)
- [2. API请求封装](#2. API请求封装)
- [3. 核心组件实现](#3. 核心组件实现)
-
- [数据统计卡片 📊](#数据统计卡片 📊)
- [最新软件与待办事项 📋](#最新软件与待办事项 📋)
- [存储空间分布 💾](#存储空间分布 💾)
- [4. 数据处理逻辑](#4. 数据处理逻辑)
Softhub软件下载站实战开发(十六):仪表盘前端设计与实现 🎛️
前言
在Softhub软件下载站的管理后台中,仪表盘(Dashboard) 是管理员查看系统运行状态的核心界面。本文将详细介绍我们如何基于Vue3+Element Plus实现一个功能丰富、数据可视化的管理仪表盘。
效果展示

主要功能点 ✨
- 多维度数据统计:软件总数、分类数、平台数等核心指标一目了然
- 实时数据展示:最新软件、待处理事项等动态信息
- 存储空间可视化:直观展示各类别软件占用空间比例
技术实现 🛠️
1. 接口设计与类型定义
我们首先在types.ts
中定义了严谨的TypeScript类型:
typescript
// Dashboard API 类型定义
// 基础统计数据
export interface DashboardBasicStats {
software: {
total: number;
monthlyNew: number;
};
category: {
total: number;
};
platform: {
total: number;
};
resource: {
total: number;
monthlyNew: number;
totalSize: string;
};
downloads: {
total: number;
};
}
// 分类分布数据项
export interface CategoryDistributionItem {
categoryName: string;
count: number;
percentage: number;
}
// 平台分布数据项
export interface PlatformDistributionItem {
platformName: string;
count: number;
percentage: number;
}
// 最新软件列表数据
export interface LatestSoftwareList {
list: LatestSoftwareItem[];
}
// 最新软件数据项
export interface LatestSoftwareItem {
id: string;
name: string;
categoryName: string;
platformName: string;
createTime: string;
}
// 待处理事项数据
export interface TodoItems {
list: TodoItem[];
}
export interface TodoItem {
id: string;
name: string;
categoryName: string;
platformName: string;
type: string; // 问题类型:noCategory, noResource, noCover
typeText: string; // 问题类型文本
createTime: string;
}
// 存储空间分布数据项
export interface StorageDistributionItem {
categoryName: string;
size: string;
percentage: number;
}
// 存储空间列表数据
export interface StorageDistributionList {
list: StorageDistributionItem[];
}
// API 响应类型
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// Dashboard API 响应类型
export type BasicStatsResponse = ApiResponse<DashboardBasicStats>;
export type CategoryDistributionResponse = ApiResponse<CategoryDistributionItem[]>;
export type PlatformDistributionResponse = ApiResponse<PlatformDistributionItem[]>;
export type LatestSoftwareResponse = ApiResponse<LatestSoftwareList>;
export type TodoItemsResponse = ApiResponse<TodoItems>;
export type StorageDistributionResponse = ApiResponse<StorageDistributionList>;
2. API请求封装
在index.ts
中封装了所有仪表盘相关的API请求:
typescript
import request from '/@/utils/request';
import type {
BasicStatsResponse,
CategoryDistributionResponse,
PlatformDistributionResponse,
LatestSoftwareResponse,
TodoItemsResponse,
StorageDistributionResponse,
DashboardBasicStats,
CategoryDistributionItem,
PlatformDistributionItem,
LatestSoftwareItem,
TodoItems,
StorageDistributionItem
} from './types';
// 获取基础统计数据
export function getBasicStats(): Promise<BasicStatsResponse> {
return request({
url: '/api/v1/admin/ds/dashboard/basic-stats',
method: 'get'
})
}
// 获取分类分布数据
export function getCategoryDistribution(): Promise<CategoryDistributionResponse> {
return request({
url: '/api/v1/admin/ds/dashboard/category-distribution',
method: 'get'
})
}
// 获取平台分布数据
export function getPlatformDistribution(): Promise<PlatformDistributionResponse> {
return request({
url: '/api/v1/admin/ds/dashboard/platform-distribution',
method: 'get'
})
}
// 获取最新软件数据
export function getLatestSoftware(limit?: number): Promise<LatestSoftwareResponse> {
return request({
url: '/api/v1/admin/ds/dashboard/latest-software',
method: 'get',
params: { limit }
})
}
// 获取待处理事项数据
export function getTodoItems(): Promise<TodoItemsResponse> {
return request({
url: '/api/v1/admin/ds/dashboard/todo-items',
method: 'get'
})
}
// 获取存储空间分布数据
export function getStorageDistribution(): Promise<StorageDistributionResponse> {
return request({
url: '/api/v1/admin/ds/dashboard/storage-distribution',
method: 'get'
})
}
// 导出类型
export type {
DashboardBasicStats,
CategoryDistributionItem,
PlatformDistributionItem,
LatestSoftwareItem,
TodoItems,
StorageDistributionItem,
BasicStatsResponse,
CategoryDistributionResponse,
PlatformDistributionResponse,
LatestSoftwareResponse,
TodoItemsResponse,
StorageDistributionResponse
};
3. 核心组件实现
仪表盘主要包含三大区域:
数据统计卡片 📊
vue
<!-- 数据统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :xs="24" :sm="12" :md="4" :lg="4" :xl="4" v-for="(item, index) in statsData" :key="index">
<div class="stats-card" :class="`stats-card-${index}`">
<div class="stats-content">
<div class="stats-icon">
<component :is="item.icon" />
</div>
<div class="stats-info">
<div class="stats-number">{{ item.value }}</div>
<div class="stats-label">{{ item.label }}</div>
<div v-if="item.subValue" class="stats-sub-value">{{ item.subValue }}</div>
</div>
</div>
</div>
</el-col>
</el-row>
最新软件与待办事项 📋
采用左右布局展示最新上传的软件和待处理事项:
vue
<!-- 最新软件列表 -->
<el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12">
<div class="content-card">
<div class="card-header">
<h3>最新软件</h3>
<el-button type="primary" size="small" @click="goToSoftware">查看更多</el-button>
</div>
<div class="card-content">
<div v-if="latestSoftware.length === 0" class="empty-state">
<i class="el-icon-folder-opened"></i>
<p>暂无软件数据</p>
</div>
<div v-else class="software-list">
<div v-for="software in latestSoftware" :key="software.id" class="software-item">
<div class="software-info">
<div class="software-name">{{ software.name }}</div>
<div class="software-meta">
<span class="category">{{ software.categoryName }}</span>
<span class="platform">{{ software.platformName }}</span>
</div>
</div>
<div class="software-actions">
<el-button link size="small" @click="viewSoftware(software.id)">
查看
</el-button>
</div>
</div>
</div>
</div>
</div>
</el-col>
存储空间分布 💾
vue
<el-row :gutter="20" class="storage-row">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<div class="content-card">
<div class="card-header">
<h3>存储空间分布</h3>
</div>
<div class="card-content">
<div v-if="storageData.breakdown.length === 0" class="empty-state">
<i class="el-icon-folder-opened"></i>
<p>暂无存储空间数据</p>
</div>
<div v-else class="storage-overview">
<div class="storage-total">
<div class="storage-used">
<span class="used-size">总使用空间: {{ formatFileSize(storageData.used) }}</span>
</div>
</div>
<div class="storage-breakdown">
<div v-for="item in storageData.breakdown" :key="item.name" class="storage-item">
<div class="storage-item-info">
<div class="storage-item-name">{{ item.name }}</div>
<div class="storage-item-size">{{ formatFileSize(item.size) }}</div>
</div>
<div class="storage-item-bar">
<div class="storage-item-progress" :style="{ width: item.percentage + '%', backgroundColor: item.color }"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
4. 数据处理逻辑
在组件中实现了数据获取与转换逻辑:
typescript
// 获取仪表板数据
const fetchDashboardData = async () => {
try {
// 获取基础统计数据
console.log('开始获取基础统计数据...');
const basicStatsResponse = await getBasicStats();
console.log('基础统计数据响应:', basicStatsResponse);
if (basicStatsResponse.code === 0) {
const data = basicStatsResponse.data;
console.log('基础统计数据:', data);
// 更新统计数据
statsData.value[0].value = data.software.total || 0;
statsData.value[0].subValue = `本月新增: ${data.software.monthlyNew || 0}`;
statsData.value[1].value = data.category.total || 0;
statsData.value[2].value = data.platform.total || 0;
statsData.value[3].value = data.resource.total || 0;
statsData.value[3].subValue = `本月新增: ${data.resource.monthlyNew || 0}`;
statsData.value[4].value = data.downloads.total || 0;
console.log('更新后的统计数据:', statsData.value);
console.log('✅ 基础统计数据获取成功!');
} else {
console.error('❌ 基础统计数据接口返回错误:', basicStatsResponse.message);
}
// 获取最新软件列表
console.log('开始获取最新软件列表...');
const latestSoftwareResponse = await getLatestSoftware(10);
console.log('最新软件列表响应:', latestSoftwareResponse);
if (latestSoftwareResponse.code === 0) {
latestSoftware.value = latestSoftwareResponse.data.list || [];
console.log('最新软件列表数据:', latestSoftware.value);
console.log('✅ 最新软件列表获取成功!');
} else {
console.error('❌ 最新软件列表接口返回错误:', latestSoftwareResponse.message);
}
// 获取待处理事项
console.log('开始获取待处理事项...');
const todoItemsResponse = await getTodoItems();
console.log('待处理事项响应:', todoItemsResponse);
if (todoItemsResponse.code === 0) {
tasksData.value = todoItemsResponse.data.list || [];
console.log('待处理事项数据:', tasksData.value);
console.log('✅ 待处理事项获取成功!');
} else {
console.error('❌ 待处理事项接口返回错误:', todoItemsResponse.message);
}
// 获取存储空间数据
console.log('开始获取存储空间数据...');
const storageResponse = await getStorageDistribution();
console.log('存储空间响应:', storageResponse);
if (storageResponse.code === 0) {
const storageItems = storageResponse.data.list;
console.log('存储空间原始数据:', storageItems);
// 计算总存储空间
let totalSize = 0;
storageItems.forEach(item => {
// 解析size字段,支持GB和MB单位
const sizeStr = item.size;
let sizeInBytes = 0;
if (sizeStr.includes('GB')) {
sizeInBytes = parseFloat(sizeStr.replace('GB', '')) * 1024 * 1024 * 1024;
} else if (sizeStr.includes('MB')) {
sizeInBytes = parseFloat(sizeStr.replace('MB', '')) * 1024 * 1024;
} else {
sizeInBytes = parseFloat(sizeStr) * 1024 * 1024 * 1024; // 默认GB
}
totalSize += sizeInBytes;
});
storageData.used = totalSize;
storageData.total = totalSize; // 总容量等于已使用空间
storageData.percentage = 100; // 显示100%
// 转换存储空间分布数据
storageData.breakdown = storageItems.map(item => {
// 解析size字段
const sizeStr = item.size;
let sizeInBytes = 0;
if (sizeStr.includes('GB')) {
sizeInBytes = parseFloat(sizeStr.replace('GB', '')) * 1024 * 1024 * 1024;
} else if (sizeStr.includes('MB')) {
sizeInBytes = parseFloat(sizeStr.replace('MB', '')) * 1024 * 1024;
} else {
sizeInBytes = parseFloat(sizeStr) * 1024 * 1024 * 1024; // 默认GB
}
return {
name: item.categoryName,
size: sizeInBytes,
percentage: item.percentage,
color: getRandomColor()
};
});
console.log('处理后的存储空间数据:', storageData);
console.log('✅ 存储空间数据获取成功!');
} else {
console.error('❌ 存储空间接口返回错误:', storageResponse.message);
}
} catch (error) {
console.error('获取仪表板数据失败:', error);
}
};
格式化大小
js
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
获取任务类型
js
// 获取任务类型类
const getTaskTypeClass = (type: string): string => {
switch (type) {
case 'noCategory':
return 'task-type-urgent'; // 未分类 - 高优先级
case 'noResource':
return 'task-type-urgent'; // 缺少资源 - 高优先级
case 'noCover':
return 'task-type-low'; // 缺少封面 - 低优先级
case 'multiple':
return 'task-type-urgent'; // 多个问题 - 高优先级
case 'urgent':
return 'task-type-urgent';
case 'normal':
return 'task-type-normal';
case 'low':
return 'task-type-low';
default:
return 'task-type-normal';
}
};
softhub系列往期文章
- Softhub软件下载站实战开发(一):项目总览
- Softhub软件下载站实战开发(二):项目基础框架搭建
- Softhub软件下载站实战开发(三):平台管理模块实战
- Softhub软件下载站实战开发(四):代码生成器设计与实现
- Softhub软件下载站实战开发(五):分类模块实现
- Softhub软件下载站实战开发(六):软件配置面板实现
- Softhub软件下载站实战开发(七):集成MinIO实现文件存储功能
- Softhub软件下载站实战开发(八):编写软件后台管理
- Softhub软件下载站实战开发(九):编写软件配置管理界面
- Softhub软件下载站实战开发(十):实现图片视频上传下载接口
- Softhub软件下载站实战开发(十一):软件分片上传接口实现
- Softhub软件下载站实战开发(十二):软件管理编辑页面实现
- Softhub软件下载站实战开发(十三):软件管理前端分片上传实现
- Softhub软件下载站实战开发(十四):软件收藏集设计
- Softhub软件下载站实战开发(十五):仪表盘API设计