jiaxing.zhou

Merge remote-tracking branch 'origin/dev'

...@@ -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()
......
...@@ -9,6 +9,7 @@ import com.apple.erp.dto.response.UserInfoRes; ...@@ -9,6 +9,7 @@ import com.apple.erp.dto.response.UserInfoRes;
9 import com.apple.erp.entity.SysUser; 9 import com.apple.erp.entity.SysUser;
10 import com.apple.erp.service.SysUserService; 10 import com.apple.erp.service.SysUserService;
11 import com.apple.erp.service.SysLogService; 11 import com.apple.erp.service.SysLogService;
12 +import com.apple.erp.service.SessionService;
12 import com.apple.erp.utils.JwtUtils; 13 import com.apple.erp.utils.JwtUtils;
13 import io.swagger.v3.oas.annotations.Operation; 14 import io.swagger.v3.oas.annotations.Operation;
14 import io.swagger.v3.oas.annotations.Parameter; 15 import io.swagger.v3.oas.annotations.Parameter;
...@@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.*; ...@@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.*;
24 25
25 import javax.servlet.http.HttpServletRequest; 26 import javax.servlet.http.HttpServletRequest;
26 import javax.validation.Valid; 27 import javax.validation.Valid;
28 +import java.util.HashMap;
27 import java.util.List; 29 import java.util.List;
30 +import java.util.Map;
28 31
29 /** 32 /**
30 * 认证控制器 33 * 认证控制器
...@@ -51,6 +54,9 @@ public class AuthController { ...@@ -51,6 +54,9 @@ public class AuthController {
51 54
52 @Autowired 55 @Autowired
53 private SysLogService sysLogService; 56 private SysLogService sysLogService;
57 +
58 + @Autowired
59 + private SessionService sessionService;
54 60
55 /** 61 /**
56 * 用户登录 62 * 用户登录
...@@ -95,6 +101,21 @@ public class AuthController { ...@@ -95,6 +101,21 @@ public class AuthController {
95 // 生成JWT令牌 101 // 生成JWT令牌
96 String token = jwtUtils.generateToken(username); 102 String token = jwtUtils.generateToken(username);
97 String refreshToken = jwtUtils.generateRefreshToken(username); 103 String refreshToken = jwtUtils.generateRefreshToken(username);
104 +
105 + // 将用户会话信息存储到Redis
106 + try {
107 + Map<String, Object> sessionData = new HashMap<>();
108 + sessionData.put("username", username);
109 + sessionData.put("loginTime", System.currentTimeMillis());
110 + sessionData.put("clientIp", getClientIp(request));
111 + sessionData.put("token", token);
112 +
113 + sessionService.storeUserSession(username, sessionData);
114 + log.info("用户会话已存储: {}", username);
115 + } catch (Exception e) {
116 + log.error("存储用户会话失败", e);
117 + // Redis失败不影响登录流程
118 + }
98 119
99 LoginRes loginResponse = new LoginRes(token, refreshToken, username); 120 LoginRes loginResponse = new LoginRes(token, refreshToken, username);
100 ApiRes<LoginRes> response = ApiRes.success("登录成功", loginResponse); 121 ApiRes<LoginRes> response = ApiRes.success("登录成功", loginResponse);
...@@ -192,6 +213,16 @@ public class AuthController { ...@@ -192,6 +213,16 @@ public class AuthController {
192 } 213 }
193 } 214 }
194 215
216 + // 清除Redis中的用户会话
217 + if (username != null) {
218 + try {
219 + sessionService.removeUserSession(username);
220 + log.info("用户会话已清除: {}", username);
221 + } catch (Exception e) {
222 + log.error("清除用户会话失败", e);
223 + }
224 + }
225 +
195 // 清除Spring Security上下文 226 // 清除Spring Security上下文
196 SecurityContextHolder.clearContext(); 227 SecurityContextHolder.clearContext();
197 228
......
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 +import java.util.List;
13 +import java.util.Map;
14 +
15 +/**
16 + * 首页仪表板控制器
17 + * 提供首页统计数据接口
18 + */
19 +@Slf4j
20 +@RestController
21 +@RequestMapping("/api/dashboard")
22 +public class DashboardController {
23 +
24 + @Autowired
25 + private DashboardService dashboardService;
26 +
27 + /**
28 + * 获取首页统计数据
29 + * @return 统计数据
30 + */
31 + @GetMapping("/stats")
32 + public ApiRes<DashboardStatsRes> getDashboardStats() {
33 + log.info("获取首页统计数据请求");
34 + try {
35 + DashboardStatsRes result = dashboardService.getDashboardStats();
36 + log.info("首页统计数据获取成功: {}", result);
37 + return ApiRes.success("获取统计数据成功", result);
38 + } catch (Exception e) {
39 + log.error("获取首页统计数据失败", e);
40 + return ApiRes.error(500, "获取统计数据失败: " + e.getMessage());
41 + }
42 + }
43 +
44 + /**
45 + * 测试接口
46 + * @return 测试数据
47 + */
48 + @GetMapping("/test")
49 + public ApiRes<DashboardStatsRes> getTestStats() {
50 + log.info("测试接口调用");
51 + DashboardStatsRes stats = new DashboardStatsRes();
52 + stats.setTodayOrderCount(10);
53 + stats.setPendingDeliveryCount(5);
54 + stats.setPendingInvoiceCount(3);
55 + stats.setTodaySalesAmount(10000.0);
56 + stats.setOnlineUserCount(100);
57 + stats.setSystemStatus("测试正常");
58 + stats.setSystemVersion("v1.0.0");
59 +
60 + return ApiRes.success("测试成功", stats);
61 + }
62 +
63 + /**
64 + * 获取最近活动
65 + * @return 最近活动列表
66 + */
67 + @GetMapping("/activities")
68 + public ApiRes<List<Map<String, Object>>> getRecentActivities() {
69 + log.info("获取最近活动请求");
70 + try {
71 + List<Map<String, Object>> activities = dashboardService.getRecentActivities();
72 + return ApiRes.success("获取最近活动成功", activities);
73 + } catch (Exception e) {
74 + log.error("获取最近活动失败", e);
75 + return ApiRes.error(500, "获取最近活动失败: " + e.getMessage());
76 + }
77 + }
78 +}
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 +
46 + /**
47 + * 今日订单增长率
48 + */
49 + private String orderGrowthRate;
50 +
51 + /**
52 + * 今日销售额增长率
53 + */
54 + private String salesGrowthRate;
55 +}
1 +package com.apple.erp.service;
2 +
3 +import com.apple.erp.dto.response.DashboardStatsRes;
4 +
5 +import java.util.List;
6 +import java.util.Map;
7 +
8 +/**
9 + * 首页仪表板服务接口
10 + */
11 +public interface DashboardService {
12 +
13 + /**
14 + * 获取首页统计数据
15 + * @return 统计数据
16 + */
17 + DashboardStatsRes getDashboardStats();
18 +
19 + /**
20 + * 获取最近活动
21 + * @return 最近活动列表
22 + */
23 + List<Map<String, Object>> getRecentActivities();
24 +}
...@@ -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;
2 +
3 +import java.util.Map;
4 +
5 +/**
6 + * 会话管理服务接口
7 + *
8 + * @author Apple ERP Team
9 + * @version 1.0.0
10 + * @since 2024-01-01
11 + */
12 +public interface SessionService {
13 +
14 + /**
15 + * 存储用户会话信息
16 + * @param username 用户名
17 + * @param sessionData 会话数据
18 + */
19 + void storeUserSession(String username, Map<String, Object> sessionData);
20 +
21 + /**
22 + * 获取用户会话信息
23 + * @param username 用户名
24 + * @return 会话数据
25 + */
26 + Map<String, Object> getUserSession(String username);
27 +
28 + /**
29 + * 删除用户会话
30 + * @param username 用户名
31 + */
32 + void removeUserSession(String username);
33 +
34 + /**
35 + * 获取在线用户数
36 + * @return 在线用户数
37 + */
38 + Integer getOnlineUserCount();
39 +
40 + /**
41 + * 清理过期会话
42 + */
43 + void cleanupExpiredSessions();
44 +}
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 com.apple.erp.service.SessionService;
9 +import lombok.extern.slf4j.Slf4j;
10 +import org.springframework.beans.factory.annotation.Autowired;
11 +import org.springframework.stereotype.Service;
12 +
13 +import java.time.LocalDate;
14 +import java.time.format.DateTimeFormatter;
15 +import java.util.ArrayList;
16 +import java.util.HashMap;
17 +import java.util.List;
18 +import java.util.Map;
19 +import java.util.Set;
20 +
21 +/**
22 + * 首页仪表板服务实现
23 + */
24 +@Slf4j
25 +@Service
26 +public class DashboardServiceImpl implements DashboardService {
27 +
28 + @Autowired
29 + private OrderMainService orderMainService;
30 +
31 + @Autowired
32 + private DeliveryMainService deliveryMainService;
33 +
34 + @Autowired
35 + private InvoiceMainService invoiceMainService;
36 +
37 + @Autowired
38 + private SessionService sessionService;
39 +
40 + @Override
41 + public DashboardStatsRes getDashboardStats() {
42 + log.info("开始获取首页统计数据");
43 + DashboardStatsRes stats = new DashboardStatsRes();
44 +
45 + try {
46 + // 获取今日订单数量
47 + String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
48 + log.info("查询今日订单数量,日期: {}", today);
49 + stats.setTodayOrderCount(orderMainService.getTodayOrderCount(today));
50 +
51 + // 获取待出库数量(状态为已确认但未出库的订单)
52 + log.info("查询待出库数量");
53 + stats.setPendingDeliveryCount(deliveryMainService.getPendingDeliveryCount());
54 +
55 + // 获取待开票数量(已出库但未开票的订单)
56 + log.info("查询待开票数量");
57 + stats.setPendingInvoiceCount(invoiceMainService.getPendingInvoiceCount());
58 +
59 + // 获取今日销售额
60 + log.info("查询今日销售额,日期: {}", today);
61 + stats.setTodaySalesAmount(orderMainService.getTodaySalesAmount(today));
62 +
63 + // 获取在线用户数(从Redis或Session中获取)
64 + stats.setOnlineUserCount(getOnlineUserCount());
65 +
66 + // 系统状态
67 + stats.setSystemStatus(getSystemStatus());
68 +
69 + // 系统版本
70 + stats.setSystemVersion("v1.0.0");
71 +
72 + // 计算增长率数据
73 + stats.setOrderGrowthRate(calculateOrderGrowthRate(today));
74 + stats.setSalesGrowthRate(calculateSalesGrowthRate(today));
75 +
76 + log.info("首页统计数据获取完成: {}", stats);
77 +
78 + } catch (Exception e) {
79 + log.error("获取首页统计数据失败", e);
80 + // 如果获取数据失败,返回默认值
81 + stats.setTodayOrderCount(0);
82 + stats.setPendingDeliveryCount(0);
83 + stats.setPendingInvoiceCount(0);
84 + stats.setTodaySalesAmount(0.0);
85 + stats.setOnlineUserCount(0);
86 + stats.setSystemStatus("系统异常");
87 + stats.setSystemVersion("v1.0.0");
88 + stats.setOrderGrowthRate("0%");
89 + stats.setSalesGrowthRate("0%");
90 + }
91 +
92 + return stats;
93 + }
94 +
95 + @Override
96 + public List<Map<String, Object>> getRecentActivities() {
97 + log.info("开始获取最近活动数据");
98 + List<Map<String, Object>> activities = new ArrayList<>();
99 +
100 + try {
101 + // 从数据库获取最近的活动数据
102 + activities.addAll(getRecentOrders());
103 + activities.addAll(getRecentDeliveries());
104 + activities.addAll(getRecentInvoices());
105 + activities.addAll(getRecentRebates());
106 +
107 + // 按时间排序,取最新的5条
108 + activities.sort((a, b) -> {
109 + String timeA = (String) a.get("time");
110 + String timeB = (String) b.get("time");
111 + return timeB.compareTo(timeA);
112 + });
113 +
114 + if (activities.size() > 5) {
115 + activities = activities.subList(0, 5);
116 + }
117 +
118 + log.info("最近活动数据获取完成,共{}条记录", activities.size());
119 +
120 + } catch (Exception e) {
121 + log.error("获取最近活动数据失败", e);
122 + }
123 +
124 + return activities;
125 + }
126 +
127 + /**
128 + * 获取最近的订单活动
129 + */
130 + private List<Map<String, Object>> getRecentOrders() {
131 + List<Map<String, Object>> activities = new ArrayList<>();
132 + try {
133 + // 这里应该从数据库查询最近的订单
134 + // 暂时返回模拟数据
135 + Map<String, Object> activity = new HashMap<>();
136 + activity.put("id", 1);
137 + activity.put("type", "order");
138 + activity.put("title", "新订单 ORD-2025-001 已创建");
139 + activity.put("time", "2分钟前");
140 + activities.add(activity);
141 + } catch (Exception e) {
142 + log.error("获取最近订单活动失败", e);
143 + }
144 + return activities;
145 + }
146 +
147 + /**
148 + * 获取最近的出库活动
149 + */
150 + private List<Map<String, Object>> getRecentDeliveries() {
151 + List<Map<String, Object>> activities = new ArrayList<>();
152 + try {
153 + // 这里应该从数据库查询最近的出库记录
154 + // 暂时返回模拟数据
155 + Map<String, Object> activity = new HashMap<>();
156 + activity.put("id", 2);
157 + activity.put("type", "delivery");
158 + activity.put("title", "出库单 DEL-2025-001 已完成");
159 + activity.put("time", "15分钟前");
160 + activities.add(activity);
161 + } catch (Exception e) {
162 + log.error("获取最近出库活动失败", e);
163 + }
164 + return activities;
165 + }
166 +
167 + /**
168 + * 获取最近的发票活动
169 + */
170 + private List<Map<String, Object>> getRecentInvoices() {
171 + List<Map<String, Object>> activities = new ArrayList<>();
172 + try {
173 + // 这里应该从数据库查询最近的发票记录
174 + // 暂时返回模拟数据
175 + Map<String, Object> activity = new HashMap<>();
176 + activity.put("id", 3);
177 + activity.put("type", "invoice");
178 + activity.put("title", "发票 INV-2025-001 已开具");
179 + activity.put("time", "1小时前");
180 + activities.add(activity);
181 + } catch (Exception e) {
182 + log.error("获取最近发票活动失败", e);
183 + }
184 + return activities;
185 + }
186 +
187 + /**
188 + * 获取最近的返利活动
189 + */
190 + private List<Map<String, Object>> getRecentRebates() {
191 + List<Map<String, Object>> activities = new ArrayList<>();
192 + try {
193 + // 这里应该从数据库查询最近的返利记录
194 + // 暂时返回模拟数据
195 + Map<String, Object> activity = new HashMap<>();
196 + activity.put("id", 4);
197 + activity.put("type", "rebate");
198 + activity.put("title", "返利计算已完成,共处理 25 条记录");
199 + activity.put("time", "2小时前");
200 + activities.add(activity);
201 + } catch (Exception e) {
202 + log.error("获取最近返利活动失败", e);
203 + }
204 + return activities;
205 + }
206 +
207 + /**
208 + * 获取在线用户数
209 + */
210 + private Integer getOnlineUserCount() {
211 + try {
212 + // 使用SessionService获取在线用户数
213 + Integer onlineCount = sessionService.getOnlineUserCount();
214 + log.info("获取在线用户数: {}", onlineCount);
215 + return onlineCount;
216 + } catch (Exception e) {
217 + log.error("获取在线用户数失败", e);
218 + // 如果Redis不可用,尝试从数据库获取活跃用户数
219 + return getActiveUserCountFromDatabase();
220 + }
221 + }
222 +
223 + /**
224 + * 从数据库获取活跃用户数(Redis不可用时的备用方案)
225 + */
226 + private Integer getActiveUserCountFromDatabase() {
227 + try {
228 + // 查询最近30分钟内有登录记录的用户数
229 + // 这里需要根据实际的用户表结构调整
230 + log.info("从数据库获取活跃用户数");
231 + // TODO: 实现数据库查询逻辑
232 + return 0;
233 + } catch (Exception e) {
234 + log.error("从数据库获取活跃用户数失败", e);
235 + return 0;
236 + }
237 + }
238 +
239 + /**
240 + * 获取系统状态
241 + */
242 + private String getSystemStatus() {
243 + try {
244 + // 检查数据库连接状态
245 + // 检查关键服务状态
246 + // 这里暂时返回正常运行,后续可以添加健康检查
247 + return "正常运行";
248 + } catch (Exception e) {
249 + log.error("获取系统状态失败", e);
250 + return "系统异常";
251 + }
252 + }
253 +
254 + /**
255 + * 计算订单增长率
256 + */
257 + private String calculateOrderGrowthRate(String today) {
258 + try {
259 + // 获取昨日订单数量
260 + String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
261 + Integer yesterdayCount = orderMainService.getTodayOrderCount(yesterday);
262 + Integer todayCount = orderMainService.getTodayOrderCount(today);
263 +
264 + if (yesterdayCount == null || yesterdayCount == 0) {
265 + return todayCount > 0 ? "+100%" : "0%";
266 + }
267 +
268 + double growthRate = ((double) (todayCount - yesterdayCount) / yesterdayCount) * 100;
269 + return String.format("%+.1f%%", growthRate);
270 + } catch (Exception e) {
271 + log.error("计算订单增长率失败", e);
272 + return "0%";
273 + }
274 + }
275 +
276 + /**
277 + * 计算销售额增长率
278 + */
279 + private String calculateSalesGrowthRate(String today) {
280 + try {
281 + // 获取昨日销售额
282 + String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
283 + Double yesterdayAmount = orderMainService.getTodaySalesAmount(yesterday);
284 + Double todayAmount = orderMainService.getTodaySalesAmount(today);
285 +
286 + if (yesterdayAmount == null || yesterdayAmount == 0) {
287 + return todayAmount > 0 ? "+100%" : "0%";
288 + }
289 +
290 + double growthRate = ((todayAmount - yesterdayAmount) / yesterdayAmount) * 100;
291 + return String.format("%+.1f%%", growthRate);
292 + } catch (Exception e) {
293 + log.error("计算销售额增长率失败", e);
294 + return "0%";
295 + }
296 + }
297 +}
...@@ -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 +package com.apple.erp.service.impl;
2 +
3 +import com.apple.erp.service.SessionService;
4 +import lombok.extern.slf4j.Slf4j;
5 +import org.springframework.beans.factory.annotation.Autowired;
6 +import org.springframework.data.redis.core.RedisTemplate;
7 +import org.springframework.scheduling.annotation.Scheduled;
8 +import org.springframework.stereotype.Service;
9 +
10 +import java.util.HashMap;
11 +import java.util.Map;
12 +import java.util.Set;
13 +
14 +/**
15 + * 会话管理服务实现
16 + *
17 + * @author Apple ERP Team
18 + * @version 1.0.0
19 + * @since 2024-01-01
20 + */
21 +@Slf4j
22 +@Service
23 +public class SessionServiceImpl implements SessionService {
24 +
25 + private static final String SESSION_KEY_PREFIX = "user:session:";
26 + private static final long SESSION_TIMEOUT = 1800; // 30分钟
27 +
28 + @Autowired
29 + private RedisTemplate<String, Object> redisTemplate;
30 +
31 + @Override
32 + public void storeUserSession(String username, Map<String, Object> sessionData) {
33 + try {
34 + String sessionKey = SESSION_KEY_PREFIX + username;
35 + redisTemplate.opsForValue().set(sessionKey, sessionData, SESSION_TIMEOUT);
36 + log.debug("用户会话已存储: {}", username);
37 + } catch (Exception e) {
38 + log.error("存储用户会话失败: {}", username, e);
39 + }
40 + }
41 +
42 + @Override
43 + public Map<String, Object> getUserSession(String username) {
44 + try {
45 + String sessionKey = SESSION_KEY_PREFIX + username;
46 + Object sessionData = redisTemplate.opsForValue().get(sessionKey);
47 + if (sessionData instanceof Map) {
48 + return (Map<String, Object>) sessionData;
49 + }
50 + } catch (Exception e) {
51 + log.error("获取用户会话失败: {}", username, e);
52 + }
53 + return new HashMap<>();
54 + }
55 +
56 + @Override
57 + public void removeUserSession(String username) {
58 + try {
59 + String sessionKey = SESSION_KEY_PREFIX + username;
60 + redisTemplate.delete(sessionKey);
61 + log.debug("用户会话已删除: {}", username);
62 + } catch (Exception e) {
63 + log.error("删除用户会话失败: {}", username, e);
64 + }
65 + }
66 +
67 + @Override
68 + public Integer getOnlineUserCount() {
69 + try {
70 + String pattern = SESSION_KEY_PREFIX + "*";
71 + Set<String> sessionKeys = redisTemplate.keys(pattern);
72 +
73 + if (sessionKeys != null) {
74 + int onlineCount = sessionKeys.size();
75 + log.debug("当前在线用户数: {}", onlineCount);
76 + return onlineCount;
77 + } else {
78 + log.warn("Redis中未找到用户会话数据");
79 + return 0;
80 + }
81 + } catch (Exception e) {
82 + log.error("获取在线用户数失败", e);
83 + return 0;
84 + }
85 + }
86 +
87 + @Override
88 + @Scheduled(fixedRate = 300000) // 每5分钟执行一次
89 + public void cleanupExpiredSessions() {
90 + try {
91 + String pattern = SESSION_KEY_PREFIX + "*";
92 + Set<String> sessionKeys = redisTemplate.keys(pattern);
93 +
94 + if (sessionKeys != null) {
95 + int cleanedCount = 0;
96 + for (String sessionKey : sessionKeys) {
97 + // Redis会自动清理过期的key,这里可以添加额外的清理逻辑
98 + // 比如检查会话的最后活动时间等
99 + cleanedCount++;
100 + }
101 + log.debug("会话清理完成,检查了 {} 个会话", cleanedCount);
102 + }
103 + } catch (Exception e) {
104 + log.error("清理过期会话失败", e);
105 + }
106 + }
107 +}
...@@ -11,7 +11,6 @@ import org.springframework.stereotype.Service; ...@@ -11,7 +11,6 @@ import org.springframework.stereotype.Service;
11 11
12 import java.util.ArrayList; 12 import java.util.ArrayList;
13 import java.util.List; 13 import java.util.List;
14 -import java.util.stream.Collectors;
15 14
16 /** 15 /**
17 * 系统菜单Service实现类 16 * 系统菜单Service实现类
...@@ -77,7 +76,7 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl ...@@ -77,7 +76,7 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
77 wrapper.eq(SysMenu::getDelFlag, "0"); 76 wrapper.eq(SysMenu::getDelFlag, "0");
78 77
79 // 排序:按父菜单ID和排序号升序 78 // 排序:按父菜单ID和排序号升序
80 - wrapper.orderByAsc(SysMenu::getParentId, SysMenu::getSort); 79 + wrapper.orderByAsc(SysMenu::getParentId).orderByAsc(SysMenu::getSort);
81 80
82 return wrapper; 81 return wrapper;
83 } 82 }
......
...@@ -198,33 +198,6 @@ public class DictValueConverter { ...@@ -198,33 +198,6 @@ public class DictValueConverter {
198 } 198 }
199 } 199 }
200 200
201 - /**
202 - * 加载指定字典类型到Redis
203 - */
204 - private static void loadDictToRedis(String dictType) {
205 - if (staticDictItemService == null) {
206 - return;
207 - }
208 -
209 - try {
210 - // 根据字典类型获取字典项
211 - List<SysDictItem> dictItems = staticDictItemService.getDictItemsByType(dictType);
212 -
213 - Map<String, String> mapping = new HashMap<>();
214 - for (SysDictItem item : dictItems) {
215 - if (item.getDelFlag() == null || "0".equals(item.getDelFlag())) {
216 - mapping.put(item.getDictValue(), item.getDictLabel());
217 - }
218 - }
219 -
220 - // 存储到Redis
221 - String cacheKey = DICT_CACHE_PREFIX + dictType;
222 - staticRedisTemplate.opsForValue().set(cacheKey, mapping, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
223 -
224 - } catch (Exception e) {
225 - System.err.println("加载字典到Redis失败: " + e.getMessage());
226 - }
227 - }
228 201
229 /** 202 /**
230 * 根据字典类型ID获取字典类型编码 203 * 根据字典类型ID获取字典类型编码
......
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 + orderGrowthRate: string
15 + salesGrowthRate: string
16 +}
17 +
18 +export interface RecentActivity {
19 + id: number
20 + type: string
21 + title: string
22 + time: string
23 +}
24 +
25 +/**
26 + * 首页API
27 + */
28 +export const dashboardApi = {
29 + /**
30 + * 获取首页统计数据
31 + */
32 + getDashboardStats: (): Promise<DashboardStats> => {
33 + return request.get('/api/dashboard/stats')
34 + },
35 +
36 + /**
37 + * 获取最近活动
38 + */
39 + getRecentActivities: (): Promise<RecentActivity[]> => {
40 + return request.get('/api/dashboard/activities')
41 + },
42 +
43 + /**
44 + * 获取测试数据
45 + */
46 + getTestStats: (): Promise<DashboardStats> => {
47 + return request.get('/api/dashboard/test')
48 + }
49 +}
50 +
51 +export default dashboardApi
...@@ -32,6 +32,8 @@ export interface RebateSearchParams { ...@@ -32,6 +32,8 @@ export interface RebateSearchParams {
32 productCode?: string 32 productCode?: string
33 operateType?: number 33 operateType?: number
34 calcFlag?: number 34 calcFlag?: number
35 + auditStatus?: number
36 + dataSource?: string
35 rebateStartDate?: string 37 rebateStartDate?: string
36 rebateEndDate?: string 38 rebateEndDate?: string
37 } 39 }
......
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">{{ todayStats.orderGrowthRate || '+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">{{ todayStats.salesGrowthRate || '+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" v-if="!activitiesLoading">
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 v-if="recentActivities.length === 0" class="no-activities">
159 + 暂无最近活动
160 + </div>
161 + </div>
162 + <div v-else class="loading-activities">
163 + 正在加载活动数据...
164 + </div>
77 </div> 165 </div>
78 </div> 166 </div>
79 </template> 167 </template>
80 168
81 <script setup lang="ts"> 169 <script setup lang="ts">
82 -import { ref, onMounted } from 'vue' 170 +import { ref, onMounted, computed } from 'vue'
83 import { useRouter } from 'vue-router' 171 import { useRouter } from 'vue-router'
84 import { request } from '@/utils/request' 172 import { request } from '@/utils/request'
173 +import dashboardApi, { type DashboardStats, type RecentActivity } from '@/api/dashboard'
85 174
86 const router = useRouter() 175 const router = useRouter()
87 const currentTime = ref('') 176 const currentTime = ref('')
177 +const currentDate = ref('')
88 const userInfo = ref<any>(null) 178 const userInfo = ref<any>(null)
89 const loading = ref(false) 179 const loading = ref(false)
180 +const statsLoading = ref(false)
181 +
182 +// 今日统计数据
183 +const todayStats = ref<DashboardStats>({
184 + todayOrderCount: 0,
185 + pendingDeliveryCount: 0,
186 + pendingInvoiceCount: 0,
187 + todaySalesAmount: 0,
188 + onlineUserCount: 0,
189 + systemStatus: '正常运行',
190 + systemVersion: 'v1.0.0',
191 + orderGrowthRate: '+12.5%',
192 + salesGrowthRate: '+8.2%'
193 +})
194 +
195 +// 最近活动数据
196 +const recentActivities = ref<RecentActivity[]>([])
197 +const activitiesLoading = ref(false)
198 +
199 +// 获取用户姓名首字母
200 +const getUserInitials = () => {
201 + const name = userInfo.value?.username || userInfo.value?.realName || 'U'
202 + return name.charAt(0).toUpperCase()
203 +}
90 204
91 // 获取角色名称 205 // 获取角色名称
92 const getRoleName = (userInfo: any) => { 206 const getRoleName = (userInfo: any) => {
...@@ -119,6 +233,71 @@ const getRoleName = (userInfo: any) => { ...@@ -119,6 +233,71 @@ const getRoleName = (userInfo: any) => {
119 return '普通用户' 233 return '普通用户'
120 } 234 }
121 235
236 +// 格式化金额
237 +const formatAmount = (amount: number) => {
238 + return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
239 +}
240 +
241 +// 获取活动图标
242 +const getActivityIcon = (type: string) => {
243 + const iconMap: { [key: string]: string } = {
244 + 'order': '📋',
245 + 'delivery': '🚚',
246 + 'invoice': '🧾',
247 + 'rebate': '💰',
248 + 'system': '⚙️'
249 + }
250 + return iconMap[type] || '📄'
251 +}
252 +
253 +// 获取今日统计数据
254 +const fetchTodayStats = async () => {
255 + try {
256 + statsLoading.value = true
257 + console.log('开始获取首页统计数据...')
258 +
259 + const response = await dashboardApi.getDashboardStats()
260 + todayStats.value = response
261 + console.log('首页统计数据获取成功:', response)
262 + } catch (error: any) {
263 + console.error('获取统计数据失败:', error)
264 + console.error('错误详情:', error?.message)
265 + // 如果API调用失败,使用默认值
266 + todayStats.value = {
267 + todayOrderCount: 0,
268 + pendingDeliveryCount: 0,
269 + pendingInvoiceCount: 0,
270 + todaySalesAmount: 0,
271 + onlineUserCount: 0,
272 + systemStatus: '系统异常',
273 + systemVersion: 'v1.0.0',
274 + orderGrowthRate: '0%',
275 + salesGrowthRate: '0%'
276 + }
277 + } finally {
278 + statsLoading.value = false
279 + }
280 +}
281 +
282 +// 获取最近活动
283 +const fetchRecentActivities = async () => {
284 + try {
285 + activitiesLoading.value = true
286 + console.log('开始获取最近活动数据...')
287 +
288 + const response = await dashboardApi.getRecentActivities()
289 + recentActivities.value = response
290 + console.log('最近活动数据获取成功:', response)
291 + } catch (error: any) {
292 + console.error('获取最近活动失败:', error)
293 + console.error('错误详情:', error?.message)
294 + // 如果API调用失败,使用默认值
295 + recentActivities.value = []
296 + } finally {
297 + activitiesLoading.value = false
298 + }
299 +}
300 +
122 // 获取用户信息 301 // 获取用户信息
123 const fetchUserInfo = async () => { 302 const fetchUserInfo = async () => {
124 try { 303 try {
...@@ -138,9 +317,29 @@ const fetchUserInfo = async () => { ...@@ -138,9 +317,29 @@ const fetchUserInfo = async () => {
138 } 317 }
139 } 318 }
140 319
320 +// 更新时间
321 +const updateTime = () => {
322 + const now = new Date()
323 + currentTime.value = now.toLocaleString('zh-CN', {
324 + year: 'numeric',
325 + month: '2-digit',
326 + day: '2-digit',
327 + hour: '2-digit',
328 + minute: '2-digit',
329 + second: '2-digit'
330 + })
331 + currentDate.value = now.toLocaleDateString('zh-CN', {
332 + year: 'numeric',
333 + month: 'long',
334 + day: 'numeric',
335 + weekday: 'long'
336 + })
337 +}
338 +
141 onMounted(() => { 339 onMounted(() => {
142 - // 获取当前时间 340 + // 更新当前时间
143 - currentTime.value = new Date().toLocaleString() 341 + updateTime()
342 + setInterval(updateTime, 1000)
144 343
145 // 检查是否已登录 344 // 检查是否已登录
146 const token = localStorage.getItem('token') 345 const token = localStorage.getItem('token')
...@@ -149,8 +348,10 @@ onMounted(() => { ...@@ -149,8 +348,10 @@ onMounted(() => {
149 return 348 return
150 } 349 }
151 350
152 - // 获取用户信息 351 + // 获取用户信息、统计数据和最近活动
153 fetchUserInfo() 352 fetchUserInfo()
353 + fetchTodayStats()
354 + fetchRecentActivities()
154 }) 355 })
155 356
156 // 页面跳转函数 357 // 页面跳转函数
...@@ -170,151 +371,449 @@ const logout = () => { ...@@ -170,151 +371,449 @@ 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 + }
781 +
782 + .no-activities, .loading-activities {
783 + text-align: center;
784 + color: #666;
785 + padding: 20px;
786 + font-size: 14px;
787 + }
302 } 788 }
303 789
304 -.action-btn:hover { 790 +// 响应式设计
305 - background: #2980b9; 791 +@media (max-width: 1200px) {
792 + .main-content {
793 + grid-template-columns: 1fr;
794 + }
306 } 795 }
307 796
308 @media (max-width: 768px) { 797 @media (max-width: 768px) {
309 - .dashboard-stats { 798 + .dashboard-container {
310 - grid-template-columns: 1fr; 799 + padding: 16px;
311 } 800 }
312 - 801 +
313 - .dashboard-content { 802 + .welcome-section {
314 - grid-template-columns: 1fr; 803 + flex-direction: column;
804 + text-align: center;
805 + gap: 20px;
806 +
807 + .user-info {
808 + justify-content: center;
315 } 809 }
316 - 810 + }
317 - .quick-actions { 811 +
812 + .stats-grid {
813 + grid-template-columns: 1fr;
814 + }
815 +
816 + .actions-grid {
318 grid-template-columns: 1fr; 817 grid-template-columns: 1fr;
319 } 818 }
320 } 819 }
......
...@@ -2,51 +2,64 @@ ...@@ -2,51 +2,64 @@
2 <div class="rebate-page"> 2 <div class="rebate-page">
3 <!-- 搜索区域 --> 3 <!-- 搜索区域 -->
4 <div class="search-section"> 4 <div class="search-section">
5 - <div class="search-form"> 5 + <div class="search-row">
6 - <div class="search-row"> 6 + <div class="search-item">
7 - <div class="search-item"> 7 + <label>经销商名称:</label>
8 - <span class="search-label">经销商名称</span> 8 + <input
9 - <el-input 9 + v-model="searchForm.dealerName"
10 - v-model="searchForm.dealerName" 10 + class="search-input"
11 placeholder="请输入经销商名称" 11 placeholder="请输入经销商名称"
12 - clearable 12 + />
13 - size="small" 13 + </div>
14 - style="width: 160px" 14 + <div class="search-item">
15 - /> 15 + <label>返利编号:</label>
16 - </div> 16 + <input
17 - <div class="search-item"> 17 + v-model="searchForm.rebateNo"
18 - <span class="search-label">返利编号</span> 18 + class="search-input"
19 - <el-input 19 + placeholder="请输入返利编号"
20 - v-model="searchForm.rebateNo" 20 + />
21 - placeholder="请输入返利编号" 21 + </div>
22 - clearable 22 + <div class="search-item">
23 - size="small" 23 + <label>操作类型:</label>
24 - style="width: 160px" 24 + <select v-model="searchForm.operateType" class="search-select">
25 - /> 25 + <option value="">所有</option>
26 - </div> 26 + <option value="1">新增</option>
27 - <div class="search-item"> 27 + <option value="2">修改</option>
28 - <span class="search-label">日期</span> 28 + <option value="3">删除</option>
29 - <el-date-picker 29 + </select>
30 - v-model="dateRange" 30 + </div>
31 - type="daterange" 31 + <div class="search-item">
32 - range-separator="至" 32 + <label>计算状态:</label>
33 - start-placeholder="开始日期" 33 + <select v-model="searchForm.calcFlag" class="search-select">
34 - end-placeholder="结束日期" 34 + <option value="">所有</option>
35 - size="small" 35 + <option value="0">未计算</option>
36 - format="YYYY-MM-DD" 36 + <option value="1">已计算</option>
37 - value-format="YYYY-MM-DD" 37 + </select>
38 - @change="handleDateRangeChange" 38 + </div>
39 - style="width: 240px" 39 + </div>
40 - /> 40 + <div class="search-row">
41 - </div> 41 + <div class="search-item">
42 - <div class="search-actions"> 42 + <label>审核状态:</label>
43 - <el-button type="primary" size="small" @click="handleSearch" :loading="loading"> 43 + <select v-model="searchForm.auditStatus" class="search-select">
44 - 查询 44 + <option value="">所有</option>
45 - </el-button> 45 + <option value="0">待审核</option>
46 - <el-button size="small" @click="handleReset"> 46 + <option value="1">审核通过</option>
47 - 重置 47 + <option value="2">审核中</option>
48 - </el-button> 48 + <option value="3">审核拒绝</option>
49 - </div> 49 + </select>
50 + </div>
51 + <div class="search-item">
52 + <label>数据来源:</label>
53 + <select v-model="searchForm.dataSource" class="search-select">
54 + <option value="">所有</option>
55 + <option value="ERP系统">ERP系统</option>
56 + <option value="手工录入">手工录入</option>
57 + <option value="API导入">API导入</option>
58 + </select>
59 + </div>
60 + <div class="search-actions">
61 + <button @click="handleSearch" class="search-btn" :disabled="loading">🔍 搜索</button>
62 + <button @click="handleReset" class="reset-btn">🔄 重置</button>
50 </div> 63 </div>
51 </div> 64 </div>
52 </div> 65 </div>
...@@ -96,95 +109,103 @@ ...@@ -96,95 +109,103 @@
96 </div> 109 </div>
97 </div> 110 </div>
98 111
112 + <!-- 操作按钮区域 -->
113 + <div class="action-section">
114 + <div class="action-buttons">
115 + <button @click="handleExport" class="action-btn secondary" :disabled="exportLoading">
116 + 📋 导出
117 + </button>
118 + </div>
119 + </div>
120 +
99 <!-- 数据表格 --> 121 <!-- 数据表格 -->
100 <div class="table-section"> 122 <div class="table-section">
101 <div class="table-header"> 123 <div class="table-header">
102 - <div class="table-title"> 124 + <div class="table-controls">
103 - 返利记录 125 + <button @click="handleTableSearch" class="control-btn">🔍</button>
104 - </div> 126 + <button @click="handleTableRefresh" class="control-btn">🔄</button>
105 - <div class="table-actions"> 127 + <button @click="handleTableExport" class="control-btn">📋</button>
106 - <el-button type="primary" size="small" @click="handleExport" :loading="exportLoading"> 128 + <button @click="handleTableViewToggle" class="control-btn">⊞</button>
107 - <el-icon><Download /></el-icon>
108 - 导出
109 - </el-button>
110 - <div class="table-info">
111 - 数据状态: {{ loading ? '加载中...' : `共${rebateList.length}条记录` }}
112 - </div>
113 </div> 129 </div>
114 </div> 130 </div>
115 131
116 - <el-table 132 + <div class="table-container">
117 - v-loading="loading" 133 + <div v-if="loading" class="loading-overlay">
118 - :data="rebateList" 134 + <div class="loading-spinner">加载中...</div>
119 - style="width: 100%" 135 + </div>
120 - :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 'normal' }" 136 + <table class="data-table">
121 - :empty-text="rebateList.length === 0 ? '暂无数据' : ''" 137 + <thead>
122 - size="small" 138 + <tr>
123 - > 139 + <th>返利编号</th>
124 - <el-table-column prop="rebateNo" label="返利编号" width="140" align="center" /> 140 + <th>订单编号</th>
125 - <el-table-column prop="orderNo" label="订单编号" width="140" align="center" /> 141 + <th>经销商代码</th>
126 - <el-table-column prop="dealerCode" label="经销商代码" width="120" align="center" /> 142 + <th>经销商名称</th>
127 - <el-table-column prop="dealerName" label="经销商名称" min-width="150" /> 143 + <th>产品编码</th>
128 - <el-table-column prop="productCode" label="产品编码" width="120" align="center" /> 144 + <th>返利金额</th>
129 - <el-table-column prop="rebateAmount" label="返利金额" width="120" align="right"> 145 + <th>返利日期</th>
130 - <template #default="{ row }"> 146 + <th>操作类型</th>
131 - <span class="amount-text">{{ formatAmount(row.rebateAmount) }}</span> 147 + <th>计算状态</th>
132 - </template> 148 + <th>更新时间</th>
133 - </el-table-column> 149 + <th>操作</th>
134 - <el-table-column prop="rebateDate" label="返利日期" width="120" align="center" /> 150 + </tr>
135 - <el-table-column prop="operateTypeText" label="操作类型" width="100" align="center"> 151 + </thead>
136 - <template #default="{ row }"> 152 + <tbody>
137 - <el-tag :type="getOperateTypeTagType(row.operateType)" size="small"> 153 + <tr v-if="rebateList.length === 0">
138 - {{ row.operateTypeText || getOperateTypeText(row.operateType) }} 154 + <td colspan="11" class="empty-data">暂无数据</td>
139 - </el-tag> 155 + </tr>
140 - </template> 156 + <tr v-for="row in rebateList" :key="row.rebateId">
141 - </el-table-column> 157 + <td>{{ row.rebateNo }}</td>
142 - <el-table-column prop="calcFlagText" label="计算状态" width="100" align="center"> 158 + <td>{{ row.orderNo }}</td>
143 - <template #default="{ row }"> 159 + <td>{{ row.dealerCode }}</td>
144 - <el-tag :type="getCalcFlagTagType(row.calcFlag)" size="small"> 160 + <td>{{ row.dealerName }}</td>
145 - {{ row.calcFlagText || getCalcFlagText(row.calcFlag) }} 161 + <td>{{ row.productCode }}</td>
146 - </el-tag> 162 + <td class="amount-text">{{ formatAmount(row.rebateAmount) }}</td>
147 - </template> 163 + <td>{{ row.rebateDate }}</td>
148 - </el-table-column> 164 + <td>
149 - <el-table-column prop="updateTime" label="更新时间" width="150" align="center"> 165 + <span :class="getOperateTypeClass(row.operateType || 0)">
150 - <template #default="{ row }"> 166 + {{ row.operateTypeText || getOperateTypeText(row.operateType || 0) }}
151 - {{ formatDate(row.updateTime) }} 167 + </span>
152 - </template> 168 + </td>
153 - </el-table-column> 169 + <td>
154 - <el-table-column label="操作" width="80" align="center"> 170 + <span :class="getCalcFlagClass(row.calcFlag || 0)">
155 - <template #default="{ row }"> 171 + {{ row.calcFlagText || getCalcFlagText(row.calcFlag || 0) }}
156 - <el-button type="primary" size="small" link @click="handleViewDetail(row)"> 172 + </span>
157 - 详情 173 + </td>
158 - </el-button> 174 + <td>{{ formatDate(row.updateTime || '') }}</td>
159 - </template> 175 + <td>
160 - </el-table-column> 176 + <button @click="handleViewDetail(row)" class="action-link">详情</button>
161 - </el-table> 177 + </td>
178 + </tr>
179 + </tbody>
180 + </table>
181 + </div>
162 182
163 <!-- 分页 --> 183 <!-- 分页 -->
164 <div class="pagination-wrapper"> 184 <div class="pagination-wrapper">
165 <div class="pagination-info"> 185 <div class="pagination-info">
166 共 {{ pagination.total }} 条,{{ pagination.pageSize }}/页 186 共 {{ pagination.total }} 条,{{ pagination.pageSize }}/页
167 </div> 187 </div>
168 - <el-pagination 188 + <div class="pagination-controls">
169 - v-model:current-page="pagination.pageNum" 189 + <button @click="handlePrevPage" :disabled="pagination.pageNum <= 1" class="page-btn">‹</button>
170 - v-model:page-size="pagination.pageSize" 190 + <button
171 - :page-sizes="[10, 20, 50, 100]" 191 + v-for="page in getPageNumbers()"
172 - :total="pagination.total" 192 + :key="page"
173 - layout="sizes, prev, pager, next" 193 + @click="handlePageChange(page)"
174 - @size-change="handleSizeChange" 194 + :class="['page-btn', { active: page === pagination.pageNum }]"
175 - @current-change="handleCurrentChange" 195 + >
176 - small 196 + {{ page }}
177 - /> 197 + </button>
198 + <button @click="handleNextPage" :disabled="pagination.pageNum >= getTotalPages()" class="page-btn">›</button>
199 + </div>
178 <div class="pagination-jump"> 200 <div class="pagination-jump">
179 前往 201 前往
180 - <el-input 202 + <input
181 v-model="jumpPage" 203 v-model="jumpPage"
182 - size="small" 204 + class="jump-input"
183 - style="width: 50px; margin: 0 8px;"
184 @keyup.enter="handleJumpPage" 205 @keyup.enter="handleJumpPage"
185 /> 206 />
186 207
187 - </div> 208 + </div>
188 </div> 209 </div>
189 </div> 210 </div>
190 211
...@@ -435,6 +456,8 @@ const searchForm = reactive<RebateSearchParams>({ ...@@ -435,6 +456,8 @@ const searchForm = reactive<RebateSearchParams>({
435 productCode: '', 456 productCode: '',
436 operateType: undefined, 457 operateType: undefined,
437 calcFlag: undefined, 458 calcFlag: undefined,
459 + auditStatus: undefined,
460 + dataSource: '',
438 rebateStartDate: '', 461 rebateStartDate: '',
439 rebateEndDate: '' 462 rebateEndDate: ''
440 }) 463 })
...@@ -730,15 +753,17 @@ const handleReset = () => { ...@@ -730,15 +753,17 @@ const handleReset = () => {
730 Object.assign(searchForm, { 753 Object.assign(searchForm, {
731 pageNum: 1, 754 pageNum: 1,
732 pageSize: 10, 755 pageSize: 10,
733 - rebateNo: undefined, 756 + rebateNo: '',
734 - dealerName: undefined, 757 + dealerCode: '',
735 - productName: undefined, 758 + dealerName: '',
736 - rebateType: undefined, 759 + productCode: '',
737 - status: undefined, 760 + operateType: undefined,
738 - applyStartTime: undefined, 761 + calcFlag: undefined,
739 - applyEndTime: undefined 762 + auditStatus: undefined,
763 + dataSource: '',
764 + rebateStartDate: '',
765 + rebateEndDate: ''
740 }) 766 })
741 - applyDateRange.value = null
742 pagination.pageNum = 1 767 pagination.pageNum = 1
743 getRebateList() 768 getRebateList()
744 } 769 }
...@@ -748,6 +773,104 @@ const handleRefresh = () => { ...@@ -748,6 +773,104 @@ const handleRefresh = () => {
748 getRebateList() 773 getRebateList()
749 } 774 }
750 775
776 +// 表格控制按钮
777 +const handleTableSearch = () => {
778 + handleSearch()
779 +}
780 +
781 +const handleTableRefresh = () => {
782 + handleRefresh()
783 +}
784 +
785 +const handleTableExport = () => {
786 + handleExport()
787 +}
788 +
789 +const handleTableViewToggle = () => {
790 + // 切换视图模式
791 + console.log('切换视图模式')
792 +}
793 +
794 +// 分页控制
795 +const handlePrevPage = () => {
796 + if (pagination.pageNum > 1) {
797 + pagination.pageNum--
798 + getRebateList()
799 + }
800 +}
801 +
802 +const handleNextPage = () => {
803 + if (pagination.pageNum < getTotalPages()) {
804 + pagination.pageNum++
805 + getRebateList()
806 + }
807 +}
808 +
809 +const handlePageChange = (page: number | string) => {
810 + if (typeof page === 'number') {
811 + pagination.pageNum = page
812 + getRebateList()
813 + }
814 +}
815 +
816 +const getPageNumbers = () => {
817 + const totalPages = getTotalPages()
818 + const current = pagination.pageNum
819 + const pages = []
820 +
821 + if (totalPages <= 7) {
822 + for (let i = 1; i <= totalPages; i++) {
823 + pages.push(i)
824 + }
825 + } else {
826 + if (current <= 4) {
827 + for (let i = 1; i <= 5; i++) {
828 + pages.push(i)
829 + }
830 + pages.push('...')
831 + pages.push(totalPages)
832 + } else if (current >= totalPages - 3) {
833 + pages.push(1)
834 + pages.push('...')
835 + for (let i = totalPages - 4; i <= totalPages; i++) {
836 + pages.push(i)
837 + }
838 + } else {
839 + pages.push(1)
840 + pages.push('...')
841 + for (let i = current - 1; i <= current + 1; i++) {
842 + pages.push(i)
843 + }
844 + pages.push('...')
845 + pages.push(totalPages)
846 + }
847 + }
848 +
849 + return pages
850 +}
851 +
852 +const getTotalPages = () => {
853 + return Math.ceil(pagination.total / pagination.pageSize)
854 +}
855 +
856 +// 状态样式类
857 +const getOperateTypeClass = (type: number) => {
858 + const classes = {
859 + 1: 'status-tag success',
860 + 2: 'status-tag warning',
861 + 3: 'status-tag danger'
862 + }
863 + return classes[type as keyof typeof classes] || 'status-tag'
864 +}
865 +
866 +const getCalcFlagClass = (flag: number) => {
867 + const classes = {
868 + 0: 'status-tag warning',
869 + 1: 'status-tag success'
870 + }
871 + return classes[flag as keyof typeof classes] || 'status-tag'
872 +}
873 +
751 // 分页变化 874 // 分页变化
752 const handleSizeChange = (size: number) => { 875 const handleSizeChange = (size: number) => {
753 pagination.pageSize = size 876 pagination.pageSize = size
...@@ -784,11 +907,11 @@ const handleViewDetail = async (row: Rebate) => { ...@@ -784,11 +907,11 @@ const handleViewDetail = async (row: Rebate) => {
784 const response = await rebateApi.getRebateById(row.rebateId) 907 const response = await rebateApi.getRebateById(row.rebateId)
785 console.log('详情接口响应:', response) 908 console.log('详情接口响应:', response)
786 909
787 - // 根据request.ts拦截器的处理,response.data已经是实际数据 910 + // 根据request.ts拦截器的处理,response直接就是数据对象
788 - if (response && response.data) { 911 + if (response) {
789 - rebateDetail.value = response.data 912 + rebateDetail.value = response as unknown as Rebate
790 - detailDialogVisible.value = true 913 + detailDialogVisible.value = true
791 - console.log('详情数据:', response.data) 914 + console.log('详情数据:', response)
792 } else { 915 } else {
793 ElMessage.error('获取返利详情失败') 916 ElMessage.error('获取返利详情失败')
794 } 917 }
...@@ -1478,30 +1601,154 @@ onMounted(async () => { ...@@ -1478,30 +1601,154 @@ onMounted(async () => {
1478 border-radius: 4px; 1601 border-radius: 4px;
1479 box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); 1602 box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
1480 1603
1481 - .search-form { 1604 + .search-row {
1482 - .search-row { 1605 + display: flex;
1606 + align-items: center;
1607 + gap: 24px;
1608 + flex-wrap: wrap;
1609 + margin-bottom: 12px;
1610 +
1611 + &:last-child {
1612 + margin-bottom: 0;
1613 + }
1614 +
1615 + .search-item {
1483 display: flex; 1616 display: flex;
1484 align-items: center; 1617 align-items: center;
1485 - gap: 24px; 1618 + gap: 8px;
1486 - flex-wrap: wrap;
1487 1619
1488 - .search-item { 1620 + label {
1489 - display: flex; 1621 + font-size: 14px;
1490 - align-items: center; 1622 + color: #333;
1491 - gap: 8px; 1623 + white-space: nowrap;
1624 + min-width: 80px;
1625 + }
1492 1626
1493 - .search-label { 1627 + .search-input {
1494 - font-size: 14px; 1628 + padding: 8px 12px;
1495 - color: #333; 1629 + border: 1px solid #ddd;
1496 - white-space: nowrap; 1630 + border-radius: 4px;
1497 - min-width: 70px; 1631 + font-size: 14px;
1632 + width: 160px;
1633 + transition: border-color 0.3s;
1634 +
1635 + &:focus {
1636 + outline: none;
1637 + border-color: #409eff;
1498 } 1638 }
1499 } 1639 }
1500 1640
1501 - .search-actions { 1641 + .search-select {
1502 - margin-left: auto; 1642 + padding: 8px 12px;
1503 - display: flex; 1643 + border: 1px solid #ddd;
1504 - gap: 8px; 1644 + border-radius: 4px;
1645 + font-size: 14px;
1646 + width: 120px;
1647 + background: white;
1648 + cursor: pointer;
1649 + transition: border-color 0.3s;
1650 +
1651 + &:focus {
1652 + outline: none;
1653 + border-color: #409eff;
1654 + }
1655 + }
1656 + }
1657 +
1658 + .search-actions {
1659 + margin-left: auto;
1660 + display: flex;
1661 + gap: 8px;
1662 +
1663 + .search-btn, .reset-btn {
1664 + padding: 8px 16px;
1665 + border: 1px solid #ddd;
1666 + border-radius: 4px;
1667 + font-size: 14px;
1668 + cursor: pointer;
1669 + transition: all 0.3s;
1670 + background: white;
1671 +
1672 + &:hover {
1673 + background: #f5f7fa;
1674 + }
1675 +
1676 + &:disabled {
1677 + opacity: 0.6;
1678 + cursor: not-allowed;
1679 + }
1680 + }
1681 +
1682 + .search-btn {
1683 + background: #409eff;
1684 + color: white;
1685 + border-color: #409eff;
1686 +
1687 + &:hover {
1688 + background: #66b1ff;
1689 + }
1690 + }
1691 + }
1692 + }
1693 +}
1694 +
1695 +.action-section {
1696 + background: white;
1697 + padding: 12px 20px;
1698 + margin-bottom: 16px;
1699 + border-radius: 4px;
1700 + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
1701 +
1702 + .action-buttons {
1703 + display: flex;
1704 + gap: 12px;
1705 +
1706 + .action-btn {
1707 + padding: 8px 16px;
1708 + border: 1px solid #ddd;
1709 + border-radius: 4px;
1710 + font-size: 14px;
1711 + cursor: pointer;
1712 + transition: all 0.3s;
1713 + background: white;
1714 +
1715 + &:hover {
1716 + background: #f5f7fa;
1717 + }
1718 +
1719 + &:disabled {
1720 + opacity: 0.6;
1721 + cursor: not-allowed;
1722 + }
1723 +
1724 + &.primary {
1725 + background: #409eff;
1726 + color: white;
1727 + border-color: #409eff;
1728 +
1729 + &:hover {
1730 + background: #66b1ff;
1731 + }
1732 + }
1733 +
1734 + &.secondary {
1735 + background: #67c23a;
1736 + color: white;
1737 + border-color: #67c23a;
1738 +
1739 + &:hover {
1740 + background: #85ce61;
1741 + }
1742 + }
1743 +
1744 + &.danger {
1745 + background: #f56c6c;
1746 + color: white;
1747 + border-color: #f56c6c;
1748 +
1749 + &:hover {
1750 + background: #f78989;
1751 + }
1505 } 1752 }
1506 } 1753 }
1507 } 1754 }
...@@ -1580,61 +1827,113 @@ onMounted(async () => { ...@@ -1580,61 +1827,113 @@ onMounted(async () => {
1580 1827
1581 .table-section { 1828 .table-section {
1582 background: white; 1829 background: white;
1583 - border-radius: 8px; 1830 + border-radius: 4px;
1584 - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 1831 + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
1585 overflow: hidden; 1832 overflow: hidden;
1586 1833
1587 .table-header { 1834 .table-header {
1588 display: flex; 1835 display: flex;
1589 - justify-content: space-between; 1836 + justify-content: flex-end;
1590 align-items: center; 1837 align-items: center;
1591 - padding: 20px 20px 0 20px; 1838 + padding: 12px 20px;
1592 - margin-bottom: 16px; 1839 + border-bottom: 1px solid #f0f0f0;
1840 + background: #fafafa;
1593 1841
1594 - .table-title { 1842 + .table-controls {
1595 - font-size: 16px; 1843 + display: flex;
1596 - font-weight: 500; 1844 + gap: 8px;
1597 - color: #333; 1845 +
1846 + .control-btn {
1847 + padding: 6px 12px;
1848 + border: 1px solid #ddd;
1849 + border-radius: 4px;
1850 + background: white;
1851 + cursor: pointer;
1852 + font-size: 14px;
1853 + transition: all 0.3s;
1854 +
1855 + &:hover {
1856 + background: #f5f7fa;
1857 + border-color: #409eff;
1858 + }
1859 + }
1598 } 1860 }
1861 + }
1599 1862
1600 - .table-actions { 1863 + .table-container {
1864 + position: relative;
1865 +
1866 + .loading-overlay {
1867 + position: absolute;
1868 + top: 0;
1869 + left: 0;
1870 + right: 0;
1871 + bottom: 0;
1872 + background: rgba(255, 255, 255, 0.8);
1601 display: flex; 1873 display: flex;
1602 align-items: center; 1874 align-items: center;
1603 - gap: 12px; 1875 + justify-content: center;
1876 + z-index: 10;
1604 1877
1605 - .table-info { 1878 + .loading-spinner {
1606 - font-size: 12px; 1879 + font-size: 14px;
1607 - color: #999; 1880 + color: #666;
1608 } 1881 }
1609 } 1882 }
1610 - }
1611 1883
1612 - .el-table { 1884 + .data-table {
1613 - border: none; 1885 + width: 100%;
1886 + border-collapse: collapse;
1887 + font-size: 14px;
1614 1888
1615 - :deep(.el-table__header) { 1889 + thead {
1616 - th { 1890 + background: #fafafa;
1617 - background-color: #fafafa;
1618 - border: none;
1619 - font-weight: 500;
1620 - color: #333;
1621 - }
1622 - }
1623 1891
1624 - :deep(.el-table__body) { 1892 + th {
1625 - tr:hover > td { 1893 + padding: 12px 16px;
1626 - background-color: #f5f7fa; 1894 + text-align: left;
1895 + font-weight: 500;
1896 + color: #333;
1897 + border-bottom: 1px solid #e0e0e0;
1898 + white-space: nowrap;
1899 + }
1627 } 1900 }
1628 1901
1629 - td { 1902 + tbody {
1630 - border: none; 1903 + tr {
1631 - border-bottom: 1px solid #f0f0f0; 1904 + transition: background-color 0.3s;
1905 +
1906 + &:hover {
1907 + background: #f5f7fa;
1908 + }
1909 +
1910 + &:nth-child(even) {
1911 + background: #fafafa;
1912 + }
1913 +
1914 + &:nth-child(even):hover {
1915 + background: #f0f2f5;
1916 + }
1917 +
1918 + td {
1919 + padding: 12px 16px;
1920 + border-bottom: 1px solid #f0f0f0;
1921 + vertical-align: middle;
1922 +
1923 + &.empty-data {
1924 + text-align: center;
1925 + color: #999;
1926 + font-style: italic;
1927 + padding: 40px;
1928 + }
1929 + }
1930 + }
1632 } 1931 }
1633 } 1932 }
1634 } 1933 }
1635 1934
1636 .pagination-wrapper { 1935 .pagination-wrapper {
1637 - display: flex; 1936 + display: flex;
1638 justify-content: space-between; 1937 justify-content: space-between;
1639 align-items: center; 1938 align-items: center;
1640 padding: 12px 20px; 1939 padding: 12px 20px;
...@@ -1646,15 +1945,100 @@ onMounted(async () => { ...@@ -1646,15 +1945,100 @@ onMounted(async () => {
1646 color: #666; 1945 color: #666;
1647 } 1946 }
1648 1947
1948 + .pagination-controls {
1949 + display: flex;
1950 + gap: 4px;
1951 + align-items: center;
1952 +
1953 + .page-btn {
1954 + padding: 6px 12px;
1955 + border: 1px solid #ddd;
1956 + border-radius: 4px;
1957 + background: white;
1958 + cursor: pointer;
1959 + font-size: 14px;
1960 + transition: all 0.3s;
1961 + min-width: 32px;
1962 +
1963 + &:hover:not(:disabled) {
1964 + background: #f5f7fa;
1965 + border-color: #409eff;
1966 + }
1967 +
1968 + &:disabled {
1969 + opacity: 0.5;
1970 + cursor: not-allowed;
1971 + }
1972 +
1973 + &.active {
1974 + background: #409eff;
1975 + color: white;
1976 + border-color: #409eff;
1977 + }
1978 + }
1979 + }
1980 +
1649 .pagination-jump { 1981 .pagination-jump {
1650 display: flex; 1982 display: flex;
1651 align-items: center; 1983 align-items: center;
1984 + gap: 8px;
1652 font-size: 14px; 1985 font-size: 14px;
1653 color: #666; 1986 color: #666;
1987 +
1988 + .jump-input {
1989 + width: 50px;
1990 + padding: 4px 8px;
1991 + border: 1px solid #ddd;
1992 + border-radius: 4px;
1993 + text-align: center;
1994 + font-size: 14px;
1995 +
1996 + &:focus {
1997 + outline: none;
1998 + border-color: #409eff;
1999 + }
2000 + }
1654 } 2001 }
1655 } 2002 }
1656 } 2003 }
1657 2004
2005 +.status-tag {
2006 + padding: 2px 8px;
2007 + border-radius: 12px;
2008 + font-size: 12px;
2009 + font-weight: 500;
2010 +
2011 + &.success {
2012 + background: #f0f9ff;
2013 + color: #67c23a;
2014 + }
2015 +
2016 + &.warning {
2017 + background: #fdf6ec;
2018 + color: #e6a23c;
2019 + }
2020 +
2021 + &.danger {
2022 + background: #fef0f0;
2023 + color: #f56c6c;
2024 + }
2025 +}
2026 +
2027 +.action-link {
2028 + color: #409eff;
2029 + text-decoration: none;
2030 + cursor: pointer;
2031 + font-size: 14px;
2032 + background: none;
2033 + border: none;
2034 + padding: 0;
2035 +
2036 + &:hover {
2037 + color: #66b1ff;
2038 + text-decoration: underline;
2039 + }
2040 +}
2041 +
1658 .amount-text { 2042 .amount-text {
1659 color: #f56c6c; 2043 color: #f56c6c;
1660 font-weight: 500; 2044 font-weight: 500;
......