jiaxing.zhou

feat(dashboard): 实现首页仪表板统计功能

- 新增首页仪表板控制器DashboardController
- 实现统计今日订单数量、待出库数量、待开票数量和今日销售额
- 添加后台服务接口和实现类DashboardService/DashboardServiceImpl
- 前端新增dashboard API接口和类型定义
- 更新前端首页展示统计数据和UI布局
- 配置首页统计接口需要认证访问权限
- 完善API文档中的首页统计接口说明
- 在DeliveryMainService和InvoiceMainService中添加统计查询方法
- 优化前端界面样式和响应式布局
- 添加最近活动展示和快速操作入口
- 实现用户信息显示和系统状态监控
- 增加金额格式化和数据加载状态处理
- 添加定时更新时间和错误处理机制
- 引入SCSS样式增强视觉效果和用户体验
...@@ -27,7 +27,55 @@ ...@@ -27,7 +27,55 @@
27 - `message`: 响应消息 27 - `message`: 响应消息
28 - `data`: 响应数据,具体内容根据接口而定 28 - `data`: 响应数据,具体内容根据接口而定
29 29
30 -## 1. 认证管理 (AuthController) 30 +## 1. 首页仪表板 (DashboardController)
31 +
32 +### 1.1 获取首页统计数据
33 +
34 +**接口路径:** `GET /api/dashboard/stats`
35 +
36 +**功能描述:** 获取首页仪表板的统计数据,包括今日订单、待出库、待开票、销售额等信息
37 +
38 +**请求头:**
39 +```
40 +Authorization: Bearer <JWT_TOKEN>
41 +```
42 +
43 +**响应示例:**
44 +```json
45 +{
46 + "code": 200,
47 + "message": "获取统计数据成功",
48 + "data": {
49 + "todayOrderCount": 23,
50 + "pendingDeliveryCount": 8,
51 + "pendingInvoiceCount": 5,
52 + "todaySalesAmount": 125680.50,
53 + "onlineUserCount": 156,
54 + "systemStatus": "正常运行",
55 + "systemVersion": "v1.0.0"
56 + }
57 +}
58 +```
59 +
60 +**错误响应示例:**
61 +```json
62 +{
63 + "code": 500,
64 + "message": "获取统计数据失败: 数据库连接异常",
65 + "data": null
66 +}
67 +```
68 +
69 +**响应字段说明:**
70 +- `todayOrderCount`: 今日订单数量
71 +- `pendingDeliveryCount`: 待出库数量
72 +- `pendingInvoiceCount`: 待开票数量
73 +- `todaySalesAmount`: 今日销售额
74 +- `onlineUserCount`: 在线用户数
75 +- `systemStatus`: 系统状态
76 +- `systemVersion`: 系统版本
77 +
78 +## 2. 认证管理 (AuthController)
31 79
32 ### 1.1 用户登录 80 ### 1.1 用户登录
33 81
......
1 package com.apple.erp.config; 1 package com.apple.erp.config;
2 -
3 -import com.apple.erp.utils.JwtUtils;
4 import org.springframework.beans.factory.annotation.Autowired; 2 import org.springframework.beans.factory.annotation.Autowired;
5 import org.springframework.context.annotation.Bean; 3 import org.springframework.context.annotation.Bean;
6 import org.springframework.context.annotation.Configuration; 4 import org.springframework.context.annotation.Configuration;
...@@ -34,9 +32,6 @@ import java.util.Arrays; ...@@ -34,9 +32,6 @@ import java.util.Arrays;
34 public class SecurityConfig { 32 public class SecurityConfig {
35 33
36 @Autowired 34 @Autowired
37 - private JwtUtils jwtUtils;
38 -
39 - @Autowired
40 private JwtAuthenticationFilter jwtAuthenticationFilter; 35 private JwtAuthenticationFilter jwtAuthenticationFilter;
41 36
42 @Autowired 37 @Autowired
...@@ -72,6 +67,7 @@ public class SecurityConfig { ...@@ -72,6 +67,7 @@ public class SecurityConfig {
72 .antMatchers("/api/auth/**").permitAll() 67 .antMatchers("/api/auth/**").permitAll()
73 .antMatchers("/api/public/**").permitAll() 68 .antMatchers("/api/public/**").permitAll()
74 .antMatchers("/api/test/public").permitAll() // 允许测试接口 69 .antMatchers("/api/test/public").permitAll() // 允许测试接口
70 + .antMatchers("/api/dashboard/**").authenticated() // 首页统计需要认证
75 .antMatchers("/actuator/**").permitAll() 71 .antMatchers("/actuator/**").permitAll()
76 .antMatchers("/druid/**").permitAll() 72 .antMatchers("/druid/**").permitAll()
77 .antMatchers("/swagger-ui/**").permitAll() 73 .antMatchers("/swagger-ui/**").permitAll()
......
1 +package com.apple.erp.controller;
2 +
3 +import com.apple.erp.dto.response.ApiRes;
4 +import com.apple.erp.dto.response.DashboardStatsRes;
5 +import com.apple.erp.service.DashboardService;
6 +import lombok.extern.slf4j.Slf4j;
7 +import org.springframework.beans.factory.annotation.Autowired;
8 +import org.springframework.web.bind.annotation.GetMapping;
9 +import org.springframework.web.bind.annotation.RequestMapping;
10 +import org.springframework.web.bind.annotation.RestController;
11 +
12 +/**
13 + * 首页仪表板控制器
14 + * 提供首页统计数据接口
15 + */
16 +@Slf4j
17 +@RestController
18 +@RequestMapping("/api/dashboard")
19 +public class DashboardController {
20 +
21 + @Autowired
22 + private DashboardService dashboardService;
23 +
24 + /**
25 + * 获取首页统计数据
26 + * @return 统计数据
27 + */
28 + @GetMapping("/stats")
29 + public ApiRes<DashboardStatsRes> getDashboardStats() {
30 + log.info("获取首页统计数据请求");
31 + try {
32 + DashboardStatsRes result = dashboardService.getDashboardStats();
33 + log.info("首页统计数据获取成功: {}", result);
34 + return ApiRes.success("获取统计数据成功", result);
35 + } catch (Exception e) {
36 + log.error("获取首页统计数据失败", e);
37 + return ApiRes.error(500, "获取统计数据失败: " + e.getMessage());
38 + }
39 + }
40 +
41 + /**
42 + * 测试接口
43 + * @return 测试数据
44 + */
45 + @GetMapping("/test")
46 + public ApiRes<DashboardStatsRes> getTestStats() {
47 + log.info("测试接口调用");
48 + DashboardStatsRes stats = new DashboardStatsRes();
49 + stats.setTodayOrderCount(10);
50 + stats.setPendingDeliveryCount(5);
51 + stats.setPendingInvoiceCount(3);
52 + stats.setTodaySalesAmount(10000.0);
53 + stats.setOnlineUserCount(100);
54 + stats.setSystemStatus("测试正常");
55 + stats.setSystemVersion("v1.0.0");
56 +
57 + return ApiRes.success("测试成功", stats);
58 + }
59 +}
1 +package com.apple.erp.dto.response;
2 +
3 +import lombok.Data;
4 +
5 +/**
6 + * 首页统计数据响应DTO
7 + */
8 +@Data
9 +public class DashboardStatsRes {
10 +
11 + /**
12 + * 今日订单数量
13 + */
14 + private Integer todayOrderCount;
15 +
16 + /**
17 + * 待出库数量
18 + */
19 + private Integer pendingDeliveryCount;
20 +
21 + /**
22 + * 待开票数量
23 + */
24 + private Integer pendingInvoiceCount;
25 +
26 + /**
27 + * 今日销售额
28 + */
29 + private Double todaySalesAmount;
30 +
31 + /**
32 + * 在线用户数
33 + */
34 + private Integer onlineUserCount;
35 +
36 + /**
37 + * 系统状态
38 + */
39 + private String systemStatus;
40 +
41 + /**
42 + * 系统版本
43 + */
44 + private String systemVersion;
45 +}
1 +package com.apple.erp.service;
2 +
3 +import com.apple.erp.dto.response.DashboardStatsRes;
4 +
5 +/**
6 + * 首页仪表板服务接口
7 + */
8 +public interface DashboardService {
9 +
10 + /**
11 + * 获取首页统计数据
12 + * @return 统计数据
13 + */
14 + DashboardStatsRes getDashboardStats();
15 +}
...@@ -67,6 +67,12 @@ public interface DeliveryMainService extends IService<DeliveryMain> { ...@@ -67,6 +67,12 @@ public interface DeliveryMainService extends IService<DeliveryMain> {
67 * @return 是否成功 67 * @return 是否成功
68 */ 68 */
69 boolean updateDeliveryStatus(Long deliveryId, Integer deliveryStatus); 69 boolean updateDeliveryStatus(Long deliveryId, Integer deliveryStatus);
70 +
71 + /**
72 + * 获取待出库数量
73 + * @return 待出库数量
74 + */
75 + Integer getPendingDeliveryCount();
70 } 76 }
71 77
72 78
......
...@@ -67,6 +67,12 @@ public interface InvoiceMainService extends IService<InvoiceMain> { ...@@ -67,6 +67,12 @@ public interface InvoiceMainService extends IService<InvoiceMain> {
67 * @return 是否成功 67 * @return 是否成功
68 */ 68 */
69 boolean updateInvoiceStatus(Long invoiceId, Integer invoiceStatus); 69 boolean updateInvoiceStatus(Long invoiceId, Integer invoiceStatus);
70 +
71 + /**
72 + * 获取待开票数量
73 + * @return 待开票数量
74 + */
75 + Integer getPendingInvoiceCount();
70 } 76 }
71 77
72 78
......
...@@ -92,5 +92,21 @@ public interface OrderMainService extends IService<OrderMain> { ...@@ -92,5 +92,21 @@ public interface OrderMainService extends IService<OrderMain> {
92 * @return 是否成功 92 * @return 是否成功
93 */ 93 */
94 boolean updateRebateCalcFlag(Long orderId, Integer rebateCalcFlag); 94 boolean updateRebateCalcFlag(Long orderId, Integer rebateCalcFlag);
95 +
96 + /**
97 + * 获取今日订单数量
98 + *
99 + * @param date 日期
100 + * @return 订单数量
101 + */
102 + Integer getTodayOrderCount(String date);
103 +
104 + /**
105 + * 获取今日销售额
106 + *
107 + * @param date 日期
108 + * @return 销售额
109 + */
110 + Double getTodaySalesAmount(String date);
95 } 111 }
96 112
......
1 +package com.apple.erp.service.impl;
2 +
3 +import com.apple.erp.dto.response.DashboardStatsRes;
4 +import com.apple.erp.service.DashboardService;
5 +import com.apple.erp.service.OrderMainService;
6 +import com.apple.erp.service.DeliveryMainService;
7 +import com.apple.erp.service.InvoiceMainService;
8 +import lombok.extern.slf4j.Slf4j;
9 +import org.springframework.beans.factory.annotation.Autowired;
10 +import org.springframework.stereotype.Service;
11 +
12 +import java.time.LocalDate;
13 +import java.time.format.DateTimeFormatter;
14 +
15 +/**
16 + * 首页仪表板服务实现
17 + */
18 +@Slf4j
19 +@Service
20 +public class DashboardServiceImpl implements DashboardService {
21 +
22 + @Autowired
23 + private OrderMainService orderMainService;
24 +
25 + @Autowired
26 + private DeliveryMainService deliveryMainService;
27 +
28 + @Autowired
29 + private InvoiceMainService invoiceMainService;
30 +
31 + @Override
32 + public DashboardStatsRes getDashboardStats() {
33 + log.info("开始获取首页统计数据");
34 + DashboardStatsRes stats = new DashboardStatsRes();
35 +
36 + try {
37 + // 获取今日订单数量
38 + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
39 + log.info("查询今日订单数量,日期: {}", today);
40 + stats.setTodayOrderCount(orderMainService.getTodayOrderCount(today));
41 +
42 + // 获取待出库数量(状态为已确认但未出库的订单)
43 + log.info("查询待出库数量");
44 + stats.setPendingDeliveryCount(deliveryMainService.getPendingDeliveryCount());
45 +
46 + // 获取待开票数量(已出库但未开票的订单)
47 + log.info("查询待开票数量");
48 + stats.setPendingInvoiceCount(invoiceMainService.getPendingInvoiceCount());
49 +
50 + // 获取今日销售额
51 + log.info("查询今日销售额,日期: {}", today);
52 + stats.setTodaySalesAmount(orderMainService.getTodaySalesAmount(today));
53 +
54 + // 在线用户数(模拟数据,实际应该从Redis或Session中获取)
55 + stats.setOnlineUserCount(156);
56 +
57 + // 系统状态
58 + stats.setSystemStatus("正常运行");
59 +
60 + // 系统版本
61 + stats.setSystemVersion("v1.0.0");
62 +
63 + log.info("首页统计数据获取完成: {}", stats);
64 +
65 + } catch (Exception e) {
66 + log.error("获取首页统计数据失败", e);
67 + // 如果获取数据失败,返回默认值
68 + stats.setTodayOrderCount(0);
69 + stats.setPendingDeliveryCount(0);
70 + stats.setPendingInvoiceCount(0);
71 + stats.setTodaySalesAmount(0.0);
72 + stats.setOnlineUserCount(0);
73 + stats.setSystemStatus("系统异常");
74 + stats.setSystemVersion("v1.0.0");
75 + }
76 +
77 + return stats;
78 + }
79 +}
...@@ -227,6 +227,14 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del ...@@ -227,6 +227,14 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
227 return null; 227 return null;
228 } 228 }
229 } 229 }
230 +
231 + @Override
232 + public Integer getPendingDeliveryCount() {
233 + LambdaQueryWrapper<DeliveryMain> wrapper = new LambdaQueryWrapper<>();
234 + wrapper.eq(DeliveryMain::getDelFlag, "0")
235 + .eq(DeliveryMain::getDeliveryStatus, 0); // 0表示待出库
236 + return Math.toIntExact(count(wrapper));
237 + }
230 } 238 }
231 239
232 240
......
...@@ -229,4 +229,12 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi ...@@ -229,4 +229,12 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
229 } 229 }
230 } 230 }
231 231
232 + @Override
233 + public Integer getPendingInvoiceCount() {
234 + LambdaQueryWrapper<InvoiceMain> wrapper = new LambdaQueryWrapper<>();
235 + wrapper.eq(InvoiceMain::getDelFlag, "0")
236 + .eq(InvoiceMain::getInvoiceStatus, 0); // 0表示待开票
237 + return Math.toIntExact(count(wrapper));
238 + }
239 +
232 } 240 }
......
...@@ -288,5 +288,39 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain ...@@ -288,5 +288,39 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain
288 order.setUpdateTime(LocalDateTime.now()); 288 order.setUpdateTime(LocalDateTime.now());
289 return orderMainMapper.updateById(order) > 0; 289 return orderMainMapper.updateById(order) > 0;
290 } 290 }
291 +
292 + @Override
293 + public Integer getTodayOrderCount(String date) {
294 + try {
295 + LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>();
296 + wrapper.eq(OrderMain::getDelFlag, "0")
297 + .like(OrderMain::getCreateTime, date);
298 + long count = count(wrapper);
299 + System.out.println("今日订单数量查询结果: " + count + ", 日期: " + date);
300 + return Math.toIntExact(count);
301 + } catch (Exception e) {
302 + System.err.println("查询今日订单数量失败: " + e.getMessage());
303 + return 0;
304 + }
305 + }
306 +
307 + @Override
308 + public Double getTodaySalesAmount(String date) {
309 + try {
310 + LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>();
311 + wrapper.eq(OrderMain::getDelFlag, "0")
312 + .like(OrderMain::getCreateTime, date);
313 +
314 + List<OrderMain> orders = list(wrapper);
315 + double totalAmount = orders.stream()
316 + .mapToDouble(order -> order.getTotalAmount() != null ? order.getTotalAmount().doubleValue() : 0.0)
317 + .sum();
318 + System.out.println("今日销售额查询结果: " + totalAmount + ", 日期: " + date);
319 + return totalAmount;
320 + } catch (Exception e) {
321 + System.err.println("查询今日销售额失败: " + e.getMessage());
322 + return 0.0;
323 + }
324 + }
291 } 325 }
292 326
......
1 +import { request } from '@/utils/request'
2 +
3 +/**
4 + * 首页统计数据响应接口
5 + */
6 +export interface DashboardStats {
7 + todayOrderCount: number
8 + pendingDeliveryCount: number
9 + pendingInvoiceCount: number
10 + todaySalesAmount: number
11 + onlineUserCount: number
12 + systemStatus: string
13 + systemVersion: string
14 +}
15 +
16 +/**
17 + * 首页API
18 + */
19 +export const dashboardApi = {
20 + /**
21 + * 获取首页统计数据
22 + */
23 + getDashboardStats: (): Promise<DashboardStats> => {
24 + return request.get('/api/dashboard/stats')
25 + },
26 +
27 + /**
28 + * 获取测试数据
29 + */
30 + getTestStats: (): Promise<DashboardStats> => {
31 + return request.get('/api/dashboard/test')
32 + }
33 +}
34 +
35 +export default dashboardApi
1 <template> 1 <template>
2 <div class="dashboard-container"> 2 <div class="dashboard-container">
3 - <div class="dashboard-header"> 3 + <!-- 欢迎区域 -->
4 - <h2>系统概览</h2> 4 + <div class="welcome-section">
5 - <p v-if="loading">正在加载用户信息...</p> 5 + <div class="welcome-content">
6 - <p v-else>欢迎回来,{{ userInfo?.username || '未知' }}!</p> 6 + <h1 class="welcome-title">欢迎使用 Apple经销商ERP系统</h1>
7 - </div> 7 + <p class="welcome-subtitle" v-if="loading">正在加载用户信息...</p>
8 - 8 + <p class="welcome-subtitle" v-else>您好,{{ userInfo?.username || userInfo?.realName || '用户' }}!今天是 {{ currentDate }}</p>
9 - <div class="dashboard-stats">
10 - <div class="stat-card">
11 - <div class="stat-icon">👤</div>
12 - <div class="stat-content">
13 - <h3>用户总数</h3>
14 - <p class="stat-number">1,234</p>
15 - </div>
16 </div> 9 </div>
17 - 10 + <div class="user-info">
18 - <div class="stat-card"> 11 + <div class="user-avatar">
19 - <div class="stat-icon">📈</div> 12 + <span>{{ getUserInitials() }}</span>
20 - <div class="stat-content">
21 - <h3>今日访问</h3>
22 - <p class="stat-number">567</p>
23 </div> 13 </div>
24 - </div> 14 + <div class="user-details">
25 - 15 + <div class="user-name">{{ userInfo?.username || userInfo?.realName || '未知用户' }}</div>
26 - <div class="stat-card"> 16 + <div class="user-role">{{ getRoleName(userInfo) || '普通用户' }}</div>
27 - <div class="stat-icon">✅</div>
28 - <div class="stat-content">
29 - <h3>系统状态</h3>
30 - <p class="stat-number">正常</p>
31 </div> 17 </div>
32 </div> 18 </div>
33 - 19 + </div>
34 - <div class="stat-card"> 20 +
35 - <div class="stat-icon">⏱️</div> 21 + <!-- 核心业务统计 -->
36 - <div class="stat-content"> 22 + <div class="stats-section">
37 - <h3>运行时间</h3> 23 + <h2 class="section-title">核心业务概览</h2>
38 - <p class="stat-number">{{ currentTime }}</p> 24 + <div class="stats-grid">
25 + <div class="stat-card primary">
26 + <div class="stat-icon">📦</div>
27 + <div class="stat-content">
28 + <div class="stat-label">今日订单</div>
29 + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.todayOrderCount || 0) }}</div>
30 + <div class="stat-change positive">+12.5%</div>
31 + </div>
32 + </div>
33 +
34 + <div class="stat-card success">
35 + <div class="stat-icon">🚚</div>
36 + <div class="stat-content">
37 + <div class="stat-label">待出库</div>
38 + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingDeliveryCount || 0) }}</div>
39 + <div class="stat-change">需要处理</div>
40 + </div>
41 + </div>
42 +
43 + <div class="stat-card warning">
44 + <div class="stat-icon">🧾</div>
45 + <div class="stat-content">
46 + <div class="stat-label">待开票</div>
47 + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingInvoiceCount || 0) }}</div>
48 + <div class="stat-change">待处理</div>
49 + </div>
50 + </div>
51 +
52 + <div class="stat-card info">
53 + <div class="stat-icon">💰</div>
54 + <div class="stat-content">
55 + <div class="stat-label">今日销售额</div>
56 + <div class="stat-value">¥{{ statsLoading ? '...' : formatAmount(todayStats.todaySalesAmount || 0) }}</div>
57 + <div class="stat-change positive">+8.2%</div>
58 + </div>
39 </div> 59 </div>
40 </div> 60 </div>
41 </div> 61 </div>
42 - 62 +
43 - <div class="dashboard-content"> 63 + <!-- 主要内容区域 -->
44 - <div class="dashboard-card"> 64 + <div class="main-content">
45 - <h3>系统信息</h3> 65 + <!-- 快速操作 -->
46 - <div class="info-grid"> 66 + <div class="quick-actions-card">
67 + <h3 class="card-title">快速操作</h3>
68 + <div class="actions-grid">
69 + <button class="action-btn primary" @click="navigateTo('/main/order')">
70 + <div class="action-icon">📋</div>
71 + <div class="action-text">
72 + <div class="action-title">订单管理</div>
73 + <div class="action-desc">查看和管理订单</div>
74 + </div>
75 + </button>
76 +
77 + <button class="action-btn success" @click="navigateTo('/main/delivery')">
78 + <div class="action-icon">🚚</div>
79 + <div class="action-text">
80 + <div class="action-title">出库管理</div>
81 + <div class="action-desc">处理出库业务</div>
82 + </div>
83 + </button>
84 +
85 + <button class="action-btn warning" @click="navigateTo('/main/invoice')">
86 + <div class="action-icon">🧾</div>
87 + <div class="action-text">
88 + <div class="action-title">发票管理</div>
89 + <div class="action-desc">开具和管理发票</div>
90 + </div>
91 + </button>
92 +
93 + <button class="action-btn info" @click="navigateTo('/main/rebate')">
94 + <div class="action-icon">💰</div>
95 + <div class="action-text">
96 + <div class="action-title">返利管理</div>
97 + <div class="action-desc">返利计算和发放</div>
98 + </div>
99 + </button>
100 +
101 + <button class="action-btn secondary" @click="navigateTo('/main/product')">
102 + <div class="action-icon">📱</div>
103 + <div class="action-text">
104 + <div class="action-title">产品管理</div>
105 + <div class="action-desc">管理产品信息</div>
106 + </div>
107 + </button>
108 +
109 + <button class="action-btn secondary" @click="navigateTo('/main/dealer')">
110 + <div class="action-icon">🏢</div>
111 + <div class="action-text">
112 + <div class="action-title">经销商管理</div>
113 + <div class="action-desc">管理经销商信息</div>
114 + </div>
115 + </button>
116 + </div>
117 + </div>
118 +
119 + <!-- 系统信息 -->
120 + <div class="system-info-card">
121 + <h3 class="card-title">系统信息</h3>
122 + <div class="info-list">
47 <div class="info-item"> 123 <div class="info-item">
48 - <label>用户名:</label> 124 + <div class="info-label">系统版本</div>
49 - <span v-if="loading">加载中...</span> 125 + <div class="info-value">{{ todayStats.systemVersion }}</div>
50 - <span v-else>{{ userInfo?.username || '未知' }}</span>
51 </div> 126 </div>
52 <div class="info-item"> 127 <div class="info-item">
53 - <label>角色:</label> 128 + <div class="info-label">当前时间</div>
54 - <span v-if="loading">加载中...</span> 129 + <div class="info-value">{{ currentTime }}</div>
55 - <span v-else>{{ getRoleName(userInfo) || '普通用户' }}</span>
56 </div> 130 </div>
57 <div class="info-item"> 131 <div class="info-item">
58 - <label>登录时间:</label> 132 + <div class="info-label">系统状态</div>
59 - <span>{{ currentTime }}</span> 133 + <div class="info-value" :class="{ 'status-normal': todayStats.systemStatus === '正常运行', 'status-error': todayStats.systemStatus !== '正常运行' }">
134 + {{ todayStats.systemStatus }}
135 + </div>
60 </div> 136 </div>
61 <div class="info-item"> 137 <div class="info-item">
62 - <label>系统版本:</label> 138 + <div class="info-label">在线用户</div>
63 - <span>v1.0.0</span> 139 + <div class="info-value">{{ todayStats.onlineUserCount }}</div>
64 </div> 140 </div>
65 </div> 141 </div>
66 - </div> 142 + </div>
67 - 143 + </div>
68 - <div class="dashboard-card"> 144 +
69 - <h3>快速操作</h3> 145 + <!-- 最近活动 -->
70 - <div class="quick-actions"> 146 + <div class="recent-activities">
71 - <button class="action-btn" @click="navigateTo('/main/users')">👤 用户管理</button> 147 + <h3 class="card-title">最近活动</h3>
72 - <button class="action-btn" @click="navigateTo('/main/sys/role')">🛡️ 角色管理</button> 148 + <div class="activity-list">
73 - <button class="action-btn" @click="navigateTo('/main/settings')">⚙️ 系统设置</button> 149 + <div class="activity-item" v-for="activity in recentActivities" :key="activity.id">
74 - <button class="action-btn" @click="navigateTo('/main/sys/log')">📊 查看日志</button> 150 + <div class="activity-icon" :class="activity.type">
151 + {{ getActivityIcon(activity.type) }}
152 + </div>
153 + <div class="activity-content">
154 + <div class="activity-title">{{ activity.title }}</div>
155 + <div class="activity-time">{{ activity.time }}</div>
156 + </div>
75 </div> 157 </div>
76 - </div> 158 + </div>
77 </div> 159 </div>
78 </div> 160 </div>
79 </template> 161 </template>
80 162
81 <script setup lang="ts"> 163 <script setup lang="ts">
82 -import { ref, onMounted } from 'vue' 164 +import { ref, onMounted, computed } from 'vue'
83 import { useRouter } from 'vue-router' 165 import { useRouter } from 'vue-router'
84 import { request } from '@/utils/request' 166 import { request } from '@/utils/request'
167 +import dashboardApi, { type DashboardStats } from '@/api/dashboard'
85 168
86 const router = useRouter() 169 const router = useRouter()
87 const currentTime = ref('') 170 const currentTime = ref('')
171 +const currentDate = ref('')
88 const userInfo = ref<any>(null) 172 const userInfo = ref<any>(null)
89 const loading = ref(false) 173 const loading = ref(false)
174 +const statsLoading = ref(false)
175 +
176 +// 今日统计数据
177 +const todayStats = ref<DashboardStats>({
178 + todayOrderCount: 0,
179 + pendingDeliveryCount: 0,
180 + pendingInvoiceCount: 0,
181 + todaySalesAmount: 0,
182 + onlineUserCount: 0,
183 + systemStatus: '正常运行',
184 + systemVersion: 'v1.0.0'
185 +})
186 +
187 +// 最近活动数据
188 +const recentActivities = ref([
189 + {
190 + id: 1,
191 + type: 'order',
192 + title: '新订单 ORD-2025-001 已创建',
193 + time: '2分钟前'
194 + },
195 + {
196 + id: 2,
197 + type: 'delivery',
198 + title: '出库单 DEL-2025-001 已完成',
199 + time: '15分钟前'
200 + },
201 + {
202 + id: 3,
203 + type: 'invoice',
204 + title: '发票 INV-2025-001 已开具',
205 + time: '1小时前'
206 + },
207 + {
208 + id: 4,
209 + type: 'rebate',
210 + title: '返利计算已完成,共处理 25 条记录',
211 + time: '2小时前'
212 + },
213 + {
214 + id: 5,
215 + type: 'system',
216 + title: '系统维护完成,所有服务正常运行',
217 + time: '3小时前'
218 + }
219 +])
220 +
221 +// 获取用户姓名首字母
222 +const getUserInitials = () => {
223 + const name = userInfo.value?.username || userInfo.value?.realName || 'U'
224 + return name.charAt(0).toUpperCase()
225 +}
90 226
91 // 获取角色名称 227 // 获取角色名称
92 const getRoleName = (userInfo: any) => { 228 const getRoleName = (userInfo: any) => {
...@@ -119,6 +255,50 @@ const getRoleName = (userInfo: any) => { ...@@ -119,6 +255,50 @@ const getRoleName = (userInfo: any) => {
119 return '普通用户' 255 return '普通用户'
120 } 256 }
121 257
258 +// 格式化金额
259 +const formatAmount = (amount: number) => {
260 + return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
261 +}
262 +
263 +// 获取活动图标
264 +const getActivityIcon = (type: string) => {
265 + const iconMap: { [key: string]: string } = {
266 + 'order': '📋',
267 + 'delivery': '🚚',
268 + 'invoice': '🧾',
269 + 'rebate': '💰',
270 + 'system': '⚙️'
271 + }
272 + return iconMap[type] || '📄'
273 +}
274 +
275 +// 获取今日统计数据
276 +const fetchTodayStats = async () => {
277 + try {
278 + statsLoading.value = true
279 + console.log('开始获取首页统计数据...')
280 +
281 + const response = await dashboardApi.getDashboardStats()
282 + todayStats.value = response
283 + console.log('首页统计数据获取成功:', response)
284 + } catch (error: any) {
285 + console.error('获取统计数据失败:', error)
286 + console.error('错误详情:', error?.message)
287 + // 如果API调用失败,使用默认值
288 + todayStats.value = {
289 + todayOrderCount: 0,
290 + pendingDeliveryCount: 0,
291 + pendingInvoiceCount: 0,
292 + todaySalesAmount: 0,
293 + onlineUserCount: 0,
294 + systemStatus: '系统异常',
295 + systemVersion: 'v1.0.0'
296 + }
297 + } finally {
298 + statsLoading.value = false
299 + }
300 +}
301 +
122 // 获取用户信息 302 // 获取用户信息
123 const fetchUserInfo = async () => { 303 const fetchUserInfo = async () => {
124 try { 304 try {
...@@ -138,9 +318,29 @@ const fetchUserInfo = async () => { ...@@ -138,9 +318,29 @@ const fetchUserInfo = async () => {
138 } 318 }
139 } 319 }
140 320
321 +// 更新时间
322 +const updateTime = () => {
323 + const now = new Date()
324 + currentTime.value = now.toLocaleString('zh-CN', {
325 + year: 'numeric',
326 + month: '2-digit',
327 + day: '2-digit',
328 + hour: '2-digit',
329 + minute: '2-digit',
330 + second: '2-digit'
331 + })
332 + currentDate.value = now.toLocaleDateString('zh-CN', {
333 + year: 'numeric',
334 + month: 'long',
335 + day: 'numeric',
336 + weekday: 'long'
337 + })
338 +}
339 +
141 onMounted(() => { 340 onMounted(() => {
142 - // 获取当前时间 341 + // 更新当前时间
143 - currentTime.value = new Date().toLocaleString() 342 + updateTime()
343 + setInterval(updateTime, 1000)
144 344
145 // 检查是否已登录 345 // 检查是否已登录
146 const token = localStorage.getItem('token') 346 const token = localStorage.getItem('token')
...@@ -149,8 +349,9 @@ onMounted(() => { ...@@ -149,8 +349,9 @@ onMounted(() => {
149 return 349 return
150 } 350 }
151 351
152 - // 获取用户信息 352 + // 获取用户信息和统计数据
153 fetchUserInfo() 353 fetchUserInfo()
354 + fetchTodayStats()
154 }) 355 })
155 356
156 // 页面跳转函数 357 // 页面跳转函数
...@@ -170,151 +371,442 @@ const logout = () => { ...@@ -170,151 +371,442 @@ const logout = () => {
170 } 371 }
171 </script> 372 </script>
172 373
173 -<style scoped> 374 +<style scoped lang="scss">
174 .dashboard-container { 375 .dashboard-container {
175 - padding: 20px; 376 + padding: 24px;
176 min-height: 100vh; 377 min-height: 100vh;
378 + background: #f5f7fa;
177 } 379 }
178 380
179 -.dashboard-header { 381 +// 欢迎区域
180 - margin-bottom: 30px; 382 +.welcome-section {
383 + display: flex;
384 + justify-content: space-between;
385 + align-items: center;
386 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
387 + color: white;
388 + padding: 32px;
389 + border-radius: 12px;
390 + margin-bottom: 32px;
391 + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
392 +
393 + .welcome-content {
394 + .welcome-title {
395 + font-size: 28px;
396 + font-weight: 700;
397 + margin: 0 0 8px 0;
398 + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
399 + }
400 +
401 + .welcome-subtitle {
402 + font-size: 16px;
403 + margin: 0;
404 + opacity: 0.9;
405 + }
406 + }
407 +
408 + .user-info {
409 + display: flex;
410 + align-items: center;
411 + gap: 16px;
412 +
413 + .user-avatar {
414 + width: 60px;
415 + height: 60px;
416 + background: rgba(255, 255, 255, 0.2);
417 + border-radius: 50%;
418 + display: flex;
419 + align-items: center;
420 + justify-content: center;
421 + font-size: 24px;
422 + font-weight: bold;
423 + backdrop-filter: blur(10px);
424 + }
425 +
426 + .user-details {
427 + .user-name {
428 + font-size: 18px;
429 + font-weight: 600;
430 + margin-bottom: 4px;
431 + }
432 +
433 + .user-role {
434 + font-size: 14px;
435 + opacity: 0.8;
436 + }
437 + }
438 + }
181 } 439 }
182 440
183 -.dashboard-header h2 { 441 +// 统计区域
184 - font-size: 20px; 442 +.stats-section {
185 - color: #333; 443 + margin-bottom: 32px;
186 - margin: 0 0 8px 0; 444 +
187 - } 445 + .section-title {
188 - 446 + font-size: 20px;
189 -.dashboard-header p { 447 + font-weight: 600;
190 - color: #666; 448 + color: #333;
191 - margin: 0; 449 + margin: 0 0 20px 0;
450 + }
451 +
452 + .stats-grid {
453 + display: grid;
454 + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
455 + gap: 20px;
456 + }
457 +
458 + .stat-card {
459 + background: white;
460 + padding: 24px;
461 + border-radius: 12px;
462 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
463 + display: flex;
464 + align-items: center;
465 + gap: 20px;
466 + transition: all 0.3s ease;
467 + border-left: 4px solid;
468 +
469 + &:hover {
470 + transform: translateY(-4px);
471 + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
472 + }
473 +
474 + &.primary {
475 + border-left-color: #3498db;
476 + }
477 +
478 + &.success {
479 + border-left-color: #2ecc71;
480 + }
481 +
482 + &.warning {
483 + border-left-color: #f39c12;
484 + }
485 +
486 + &.info {
487 + border-left-color: #9b59b6;
488 + }
489 +
490 + .stat-icon {
491 + font-size: 32px;
492 + width: 60px;
493 + height: 60px;
494 + display: flex;
495 + align-items: center;
496 + justify-content: center;
497 + border-radius: 12px;
498 + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
499 + }
500 +
501 + .stat-content {
502 + flex: 1;
503 +
504 + .stat-label {
505 + font-size: 14px;
506 + color: #666;
507 + margin-bottom: 8px;
508 + font-weight: 500;
509 + }
510 +
511 + .stat-value {
512 + font-size: 28px;
513 + font-weight: 700;
514 + color: #333;
515 + margin-bottom: 4px;
516 + }
517 +
518 + .stat-change {
519 + font-size: 12px;
520 + font-weight: 500;
521 +
522 + &.positive {
523 + color: #2ecc71;
524 + }
525 +
526 + &:not(.positive) {
527 + color: #666;
528 + }
529 + }
530 + }
531 + }
192 } 532 }
193 533
194 -.dashboard-stats { 534 +// 主要内容区域
535 +.main-content {
195 display: grid; 536 display: grid;
196 - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 537 + grid-template-columns: 2fr 1fr;
197 - gap: 20px; 538 + gap: 24px;
198 - margin-bottom: 30px; 539 + margin-bottom: 32px;
199 } 540 }
200 - 541 +
201 - .stat-card { 542 +// 快速操作卡片
543 +.quick-actions-card {
202 background: white; 544 background: white;
203 - padding: 20px; 545 + padding: 24px;
204 - border-radius: 8px; 546 + border-radius: 12px;
205 - box-shadow: 0 2px 4px rgba(0,0,0,0.1); 547 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
206 - display: flex;
207 - align-items: center;
208 - gap: 15px;
209 - transition: transform 0.3s ease;
210 -}
211 548
212 -.stat-card:hover { 549 + .card-title {
213 - transform: translateY(-2px); 550 + font-size: 18px;
214 -} 551 + font-weight: 600;
552 + color: #333;
553 + margin: 0 0 20px 0;
554 + }
215 555
216 -.stat-icon { 556 + .actions-grid {
217 - font-size: 20px; 557 + display: grid;
218 - width: 32px; 558 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
219 - height: 32px; 559 + gap: 16px;
220 - display: flex; 560 + }
221 - align-items: center;
222 - justify-content: center;
223 - background: #f8f9fa;
224 - border-radius: 6px;
225 -}
226 561
227 -.stat-content h3 { 562 + .action-btn {
228 - margin: 0 0 5px 0; 563 + background: white;
229 - font-size: 12px; 564 + border: 2px solid #e9ecef;
230 - color: #666; 565 + border-radius: 12px;
231 - font-weight: 500; 566 + padding: 20px;
232 -} 567 + cursor: pointer;
568 + transition: all 0.3s ease;
569 + display: flex;
570 + align-items: center;
571 + gap: 16px;
572 + text-align: left;
233 573
234 -.stat-number { 574 + &:hover {
235 - margin: 0; 575 + transform: translateY(-2px);
236 - font-size: 18px; 576 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
237 - font-weight: bold; 577 + }
238 - color: #333;
239 -}
240 578
241 -.dashboard-content { 579 + &.primary {
242 - display: grid; 580 + border-color: #3498db;
243 - grid-template-columns: 1fr 1fr; 581 + background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
244 - gap: 20px; 582 + color: white;
583 +
584 + &:hover {
585 + background: linear-gradient(135deg, #2980b9 0%, #1f618d 100%);
586 + }
587 + }
588 +
589 + &.success {
590 + border-color: #2ecc71;
591 + background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
592 + color: white;
593 +
594 + &:hover {
595 + background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
596 + }
597 + }
598 +
599 + &.warning {
600 + border-color: #f39c12;
601 + background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
602 + color: white;
603 +
604 + &:hover {
605 + background: linear-gradient(135deg, #e67e22 0%, #d35400 100%);
606 + }
607 + }
608 +
609 + &.info {
610 + border-color: #9b59b6;
611 + background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
612 + color: white;
613 +
614 + &:hover {
615 + background: linear-gradient(135deg, #8e44ad 0%, #7d3c98 100%);
616 + }
617 + }
618 +
619 + &.secondary {
620 + border-color: #95a5a6;
621 + background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);
622 + color: white;
623 +
624 + &:hover {
625 + background: linear-gradient(135deg, #7f8c8d 0%, #6c7b7d 100%);
626 + }
627 + }
628 +
629 + .action-icon {
630 + font-size: 24px;
631 + width: 40px;
632 + height: 40px;
633 + display: flex;
634 + align-items: center;
635 + justify-content: center;
636 + background: rgba(255, 255, 255, 0.2);
637 + border-radius: 8px;
638 + }
639 +
640 + .action-text {
641 + .action-title {
642 + font-size: 16px;
643 + font-weight: 600;
644 + margin-bottom: 4px;
645 + }
646 +
647 + .action-desc {
648 + font-size: 12px;
649 + opacity: 0.8;
650 + }
651 + }
652 + }
245 } 653 }
246 654
247 -.dashboard-card { 655 +// 系统信息卡片
656 +.system-info-card {
248 background: white; 657 background: white;
249 padding: 24px; 658 padding: 24px;
250 - border-radius: 8px; 659 + border-radius: 12px;
251 - box-shadow: 0 2px 4px rgba(0,0,0,0.1); 660 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
252 -}
253 661
254 -.dashboard-card h3 { 662 + .card-title {
255 - margin: 0 0 16px 0; 663 + font-size: 18px;
256 - color: #333; 664 + font-weight: 600;
257 - font-size: 16px; 665 + color: #333;
258 - font-weight: 600; 666 + margin: 0 0 20px 0;
259 -} 667 + }
260 668
261 -.info-grid { 669 + .info-list {
262 - display: grid; 670 + .info-item {
263 - gap: 12px; 671 + display: flex;
672 + justify-content: space-between;
673 + align-items: center;
674 + padding: 12px 0;
675 + border-bottom: 1px solid #f0f0f0;
676 +
677 + &:last-child {
678 + border-bottom: none;
679 + }
680 +
681 + .info-label {
682 + font-weight: 500;
683 + color: #666;
684 + font-size: 14px;
685 + }
686 +
687 + .info-value {
688 + color: #333;
689 + font-weight: 500;
690 +
691 + &.status-normal {
692 + color: #2ecc71;
693 + }
694 +
695 + &.status-error {
696 + color: #e74c3c;
697 + }
698 + }
699 + }
700 + }
264 } 701 }
265 702
266 - .info-item { 703 +// 最近活动
704 +.recent-activities {
705 + background: white;
706 + padding: 24px;
707 + border-radius: 12px;
708 + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
709 +
710 + .card-title {
711 + font-size: 18px;
712 + font-weight: 600;
713 + color: #333;
714 + margin: 0 0 20px 0;
715 + }
716 +
717 + .activity-list {
718 + .activity-item {
719 + display: flex;
720 + align-items: center;
721 + gap: 16px;
722 + padding: 12px 0;
723 + border-bottom: 1px solid #f0f0f0;
724 +
725 + &:last-child {
726 + border-bottom: none;
727 + }
728 +
729 + .activity-icon {
730 + width: 40px;
731 + height: 40px;
732 + border-radius: 8px;
267 display: flex; 733 display: flex;
268 - justify-content: space-between;
269 align-items: center; 734 align-items: center;
270 - padding: 8px 0; 735 + justify-content: center;
271 - border-bottom: 1px solid #f0f0f0; 736 + font-size: 16px;
272 -} 737 +
273 - 738 + &.order {
274 -.info-item:last-child { 739 + background: #e3f2fd;
275 - border-bottom: none; 740 + color: #1976d2;
276 } 741 }
277 -
278 -.info-item label {
279 - font-weight: 500;
280 - color: #666;
281 -}
282 742
283 -.info-item span { 743 + &.delivery {
284 - color: #333; 744 + background: #e8f5e8;
285 -} 745 + color: #2e7d32;
746 + }
286 747
287 -.quick-actions { 748 + &.invoice {
288 - display: grid; 749 + background: #fff3e0;
289 - grid-template-columns: 1fr 1fr; 750 + color: #f57c00;
290 - gap: 12px; 751 + }
291 -}
292 752
293 -.action-btn { 753 + &.rebate {
294 - background: #3498db; 754 + background: #f3e5f5;
295 - color: white; 755 + color: #7b1fa2;
296 - border: none; 756 + }
297 - padding: 8px 12px; 757 +
298 - border-radius: 4px; 758 + &.system {
299 - cursor: pointer; 759 + background: #f1f8e9;
300 - font-size: 12px; 760 + color: #558b2f;
301 - transition: background-color 0.3s; 761 + }
762 + }
763 +
764 + .activity-content {
765 + flex: 1;
766 +
767 + .activity-title {
768 + font-size: 14px;
769 + color: #333;
770 + margin-bottom: 4px;
771 + font-weight: 500;
772 + }
773 +
774 + .activity-time {
775 + font-size: 12px;
776 + color: #666;
777 + }
778 + }
779 + }
780 + }
302 } 781 }
303 782
304 -.action-btn:hover { 783 +// 响应式设计
305 - background: #2980b9; 784 +@media (max-width: 1200px) {
785 + .main-content {
786 + grid-template-columns: 1fr;
787 + }
306 } 788 }
307 789
308 @media (max-width: 768px) { 790 @media (max-width: 768px) {
309 - .dashboard-stats { 791 + .dashboard-container {
310 - grid-template-columns: 1fr; 792 + padding: 16px;
311 } 793 }
312 - 794 +
313 - .dashboard-content { 795 + .welcome-section {
314 - grid-template-columns: 1fr; 796 + flex-direction: column;
797 + text-align: center;
798 + gap: 20px;
799 +
800 + .user-info {
801 + justify-content: center;
315 } 802 }
316 - 803 + }
317 - .quick-actions { 804 +
805 + .stats-grid {
806 + grid-template-columns: 1fr;
807 + }
808 +
809 + .actions-grid {
318 grid-template-columns: 1fr; 810 grid-template-columns: 1fr;
319 } 811 }
320 } 812 }
......