Softhub软件下载站实战开发(十六):仪表盘前端设计与实现

文章目录

  • [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系列往期文章

  1. Softhub软件下载站实战开发(一):项目总览
  2. Softhub软件下载站实战开发(二):项目基础框架搭建
  3. Softhub软件下载站实战开发(三):平台管理模块实战
  4. Softhub软件下载站实战开发(四):代码生成器设计与实现
  5. Softhub软件下载站实战开发(五):分类模块实现
  6. Softhub软件下载站实战开发(六):软件配置面板实现
  7. Softhub软件下载站实战开发(七):集成MinIO实现文件存储功能
  8. Softhub软件下载站实战开发(八):编写软件后台管理
  9. Softhub软件下载站实战开发(九):编写软件配置管理界面
  10. Softhub软件下载站实战开发(十):实现图片视频上传下载接口
  11. Softhub软件下载站实战开发(十一):软件分片上传接口实现
  12. Softhub软件下载站实战开发(十二):软件管理编辑页面实现
  13. Softhub软件下载站实战开发(十三):软件管理前端分片上传实现
  14. Softhub软件下载站实战开发(十四):软件收藏集设计
  15. Softhub软件下载站实战开发(十五):仪表盘API设计
相关推荐
gnip5 分钟前
项目开发流程之技术调用流程
前端·javascript
转转技术团队18 分钟前
多代理混战?用 PAC(Proxy Auto-Config) 优雅切换代理场景
前端·后端·面试
南囝coding20 分钟前
这几个 Vibe Coding 经验,真的建议学!
前端·后端
gnip33 分钟前
SSE技术介绍
前端·javascript
yinke小琪1 小时前
JavaScript DOM节点操作(增删改)常用方法
前端·javascript
枣把儿1 小时前
Vercel 收购 NuxtLabs!Nuxt UI Pro 即将免费!
前端·vue.js·nuxt.js
望获linux1 小时前
【Linux基础知识系列】第四十三篇 - 基础正则表达式与 grep/sed
linux·运维·服务器·开发语言·前端·操作系统·嵌入式软件
爱编程的喵1 小时前
从XMLHttpRequest到Fetch:前端异步请求的演进之路
前端·javascript
喜欢吃豆1 小时前
深入企业内部的MCP知识(三):FastMCP工具转换(Tool Transformation)全解析:从适配到增强的工具进化指南
java·前端·人工智能·大模型·github·mcp
豆苗学前端1 小时前
手把手实现支持百万级数据量、高可用和可扩展性的穿梭框组件
前端·javascript·面试