Showing
21 changed files
with
1963 additions
and
297 deletions
| ... | @@ -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 | * 认证控制器 |
| ... | @@ -52,6 +55,9 @@ public class AuthController { | ... | @@ -52,6 +55,9 @@ public class AuthController { |
| 52 | @Autowired | 55 | @Autowired |
| 53 | private SysLogService sysLogService; | 56 | private SysLogService sysLogService; |
| 54 | 57 | ||
| 58 | + @Autowired | ||
| 59 | + private SessionService sessionService; | ||
| 60 | + | ||
| 55 | /** | 61 | /** |
| 56 | * 用户登录 | 62 | * 用户登录 |
| 57 | */ | 63 | */ |
| ... | @@ -96,6 +102,21 @@ public class AuthController { | ... | @@ -96,6 +102,21 @@ public class AuthController { |
| 96 | String token = jwtUtils.generateToken(username); | 102 | String token = jwtUtils.generateToken(username); |
| 97 | String refreshToken = jwtUtils.generateRefreshToken(username); | 103 | String refreshToken = jwtUtils.generateRefreshToken(username); |
| 98 | 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 | + } | ||
| 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); |
| 101 | return ResponseEntity.ok(response); | 122 | return ResponseEntity.ok(response); |
| ... | @@ -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获取字典类型编码 | ... | ... |
frontend/src/api/dashboard.ts
0 → 100644
| 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 | + <p class="welcome-subtitle" v-if="loading">正在加载用户信息...</p> | ||
| 8 | + <p class="welcome-subtitle" v-else>您好,{{ userInfo?.username || userInfo?.realName || '用户' }}!今天是 {{ currentDate }}</p> | ||
| 9 | + </div> | ||
| 10 | + <div class="user-info"> | ||
| 11 | + <div class="user-avatar"> | ||
| 12 | + <span>{{ getUserInitials() }}</span> | ||
| 13 | + </div> | ||
| 14 | + <div class="user-details"> | ||
| 15 | + <div class="user-name">{{ userInfo?.username || userInfo?.realName || '未知用户' }}</div> | ||
| 16 | + <div class="user-role">{{ getRoleName(userInfo) || '普通用户' }}</div> | ||
| 17 | + </div> | ||
| 18 | + </div> | ||
| 7 | </div> | 19 | </div> |
| 8 | 20 | ||
| 9 | - <div class="dashboard-stats"> | 21 | + <!-- 核心业务统计 --> |
| 10 | - <div class="stat-card"> | 22 | + <div class="stats-section"> |
| 11 | - <div class="stat-icon">👤</div> | 23 | + <h2 class="section-title">核心业务概览</h2> |
| 24 | + <div class="stats-grid"> | ||
| 25 | + <div class="stat-card primary"> | ||
| 26 | + <div class="stat-icon">📦</div> | ||
| 12 | <div class="stat-content"> | 27 | <div class="stat-content"> |
| 13 | - <h3>用户总数</h3> | 28 | + <div class="stat-label">今日订单</div> |
| 14 | - <p class="stat-number">1,234</p> | 29 | + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.todayOrderCount || 0) }}</div> |
| 30 | + <div class="stat-change positive">{{ todayStats.orderGrowthRate || '+12.5%' }}</div> | ||
| 15 | </div> | 31 | </div> |
| 16 | </div> | 32 | </div> |
| 17 | 33 | ||
| 18 | - <div class="stat-card"> | 34 | + <div class="stat-card success"> |
| 19 | - <div class="stat-icon">📈</div> | 35 | + <div class="stat-icon">🚚</div> |
| 20 | <div class="stat-content"> | 36 | <div class="stat-content"> |
| 21 | - <h3>今日访问</h3> | 37 | + <div class="stat-label">待出库</div> |
| 22 | - <p class="stat-number">567</p> | 38 | + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingDeliveryCount || 0) }}</div> |
| 39 | + <div class="stat-change">需要处理</div> | ||
| 23 | </div> | 40 | </div> |
| 24 | </div> | 41 | </div> |
| 25 | 42 | ||
| 26 | - <div class="stat-card"> | 43 | + <div class="stat-card warning"> |
| 27 | - <div class="stat-icon">✅</div> | 44 | + <div class="stat-icon">🧾</div> |
| 28 | <div class="stat-content"> | 45 | <div class="stat-content"> |
| 29 | - <h3>系统状态</h3> | 46 | + <div class="stat-label">待开票</div> |
| 30 | - <p class="stat-number">正常</p> | 47 | + <div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingInvoiceCount || 0) }}</div> |
| 48 | + <div class="stat-change">待处理</div> | ||
| 31 | </div> | 49 | </div> |
| 32 | </div> | 50 | </div> |
| 33 | 51 | ||
| 34 | - <div class="stat-card"> | 52 | + <div class="stat-card info"> |
| 35 | - <div class="stat-icon">⏱️</div> | 53 | + <div class="stat-icon">💰</div> |
| 36 | <div class="stat-content"> | 54 | <div class="stat-content"> |
| 37 | - <h3>运行时间</h3> | 55 | + <div class="stat-label">今日销售额</div> |
| 38 | - <p class="stat-number">{{ currentTime }}</p> | 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> |
| 140 | + </div> | ||
| 64 | </div> | 141 | </div> |
| 65 | </div> | 142 | </div> |
| 66 | </div> | 143 | </div> |
| 67 | 144 | ||
| 68 | - <div class="dashboard-card"> | 145 | + <!-- 最近活动 --> |
| 69 | - <h3>快速操作</h3> | 146 | + <div class="recent-activities"> |
| 70 | - <div class="quick-actions"> | 147 | + <h3 class="card-title">最近活动</h3> |
| 71 | - <button class="action-btn" @click="navigateTo('/main/users')">👤 用户管理</button> | 148 | + <div class="activity-list" v-if="!activitiesLoading"> |
| 72 | - <button class="action-btn" @click="navigateTo('/main/sys/role')">🛡️ 角色管理</button> | 149 | + <div class="activity-item" v-for="activity in recentActivities" :key="activity.id"> |
| 73 | - <button class="action-btn" @click="navigateTo('/main/settings')">⚙️ 系统设置</button> | 150 | + <div class="activity-icon" :class="activity.type"> |
| 74 | - <button class="action-btn" @click="navigateTo('/main/sys/log')">📊 查看日志</button> | 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> | ||
| 157 | + </div> | ||
| 158 | + <div v-if="recentActivities.length === 0" class="no-activities"> | ||
| 159 | + 暂无最近活动 | ||
| 160 | + </div> | ||
| 75 | </div> | 161 | </div> |
| 162 | + <div v-else class="loading-activities"> | ||
| 163 | + 正在加载活动数据... | ||
| 76 | </div> | 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 { |
| 181 | -} | 383 | + display: flex; |
| 182 | - | 384 | + justify-content: space-between; |
| 183 | -.dashboard-header h2 { | 385 | + align-items: center; |
| 184 | - font-size: 20px; | 386 | + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| 185 | - color: #333; | 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; | ||
| 186 | margin: 0 0 8px 0; | 397 | margin: 0 0 8px 0; |
| 398 | + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||
| 187 | } | 399 | } |
| 188 | 400 | ||
| 189 | -.dashboard-header p { | 401 | + .welcome-subtitle { |
| 190 | - color: #666; | 402 | + font-size: 16px; |
| 191 | margin: 0; | 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 | + } | ||
| 192 | } | 439 | } |
| 193 | 440 | ||
| 194 | -.dashboard-stats { | 441 | +// 统计区域 |
| 442 | +.stats-section { | ||
| 443 | + margin-bottom: 32px; | ||
| 444 | + | ||
| 445 | + .section-title { | ||
| 446 | + font-size: 20px; | ||
| 447 | + font-weight: 600; | ||
| 448 | + color: #333; | ||
| 449 | + margin: 0 0 20px 0; | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + .stats-grid { | ||
| 195 | display: grid; | 453 | display: grid; |
| 196 | - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | 454 | + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); |
| 197 | gap: 20px; | 455 | gap: 20px; |
| 198 | - margin-bottom: 30px; | 456 | + } |
| 199 | -} | ||
| 200 | 457 | ||
| 201 | .stat-card { | 458 | .stat-card { |
| 202 | background: white; | 459 | background: white; |
| 203 | - padding: 20px; | 460 | + padding: 24px; |
| 204 | - border-radius: 8px; | 461 | + border-radius: 12px; |
| 205 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | 462 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
| 206 | display: flex; | 463 | display: flex; |
| 207 | align-items: center; | 464 | align-items: center; |
| 208 | - gap: 15px; | 465 | + gap: 20px; |
| 209 | - transition: transform 0.3s ease; | 466 | + transition: all 0.3s ease; |
| 210 | -} | 467 | + border-left: 4px solid; |
| 211 | 468 | ||
| 212 | -.stat-card:hover { | 469 | + &:hover { |
| 213 | - transform: translateY(-2px); | 470 | + transform: translateY(-4px); |
| 214 | -} | 471 | + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12); |
| 472 | + } | ||
| 215 | 473 | ||
| 216 | -.stat-icon { | 474 | + &.primary { |
| 217 | - font-size: 20px; | 475 | + border-left-color: #3498db; |
| 218 | - width: 32px; | 476 | + } |
| 219 | - height: 32px; | 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; | ||
| 220 | display: flex; | 494 | display: flex; |
| 221 | align-items: center; | 495 | align-items: center; |
| 222 | justify-content: center; | 496 | justify-content: center; |
| 223 | - background: #f8f9fa; | 497 | + border-radius: 12px; |
| 224 | - border-radius: 6px; | 498 | + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); |
| 225 | -} | 499 | + } |
| 226 | 500 | ||
| 227 | -.stat-content h3 { | 501 | + .stat-content { |
| 228 | - margin: 0 0 5px 0; | 502 | + flex: 1; |
| 229 | - font-size: 12px; | 503 | + |
| 504 | + .stat-label { | ||
| 505 | + font-size: 14px; | ||
| 230 | color: #666; | 506 | color: #666; |
| 507 | + margin-bottom: 8px; | ||
| 231 | font-weight: 500; | 508 | font-weight: 500; |
| 232 | -} | 509 | + } |
| 233 | 510 | ||
| 234 | -.stat-number { | 511 | + .stat-value { |
| 235 | - margin: 0; | 512 | + font-size: 28px; |
| 236 | - font-size: 18px; | 513 | + font-weight: 700; |
| 237 | - font-weight: bold; | ||
| 238 | color: #333; | 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 | + } | ||
| 239 | } | 532 | } |
| 240 | 533 | ||
| 241 | -.dashboard-content { | 534 | +// 主要内容区域 |
| 535 | +.main-content { | ||
| 242 | display: grid; | 536 | display: grid; |
| 243 | - grid-template-columns: 1fr 1fr; | 537 | + grid-template-columns: 2fr 1fr; |
| 244 | - gap: 20px; | 538 | + gap: 24px; |
| 539 | + margin-bottom: 32px; | ||
| 245 | } | 540 | } |
| 246 | 541 | ||
| 247 | -.dashboard-card { | 542 | +// 快速操作卡片 |
| 543 | +.quick-actions-card { | ||
| 248 | background: white; | 544 | background: white; |
| 249 | padding: 24px; | 545 | padding: 24px; |
| 250 | - border-radius: 8px; | 546 | + border-radius: 12px; |
| 251 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | 547 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
| 252 | -} | ||
| 253 | 548 | ||
| 254 | -.dashboard-card h3 { | 549 | + .card-title { |
| 255 | - margin: 0 0 16px 0; | 550 | + font-size: 18px; |
| 551 | + font-weight: 600; | ||
| 256 | color: #333; | 552 | color: #333; |
| 553 | + margin: 0 0 20px 0; | ||
| 554 | + } | ||
| 555 | + | ||
| 556 | + .actions-grid { | ||
| 557 | + display: grid; | ||
| 558 | + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | ||
| 559 | + gap: 16px; | ||
| 560 | + } | ||
| 561 | + | ||
| 562 | + .action-btn { | ||
| 563 | + background: white; | ||
| 564 | + border: 2px solid #e9ecef; | ||
| 565 | + border-radius: 12px; | ||
| 566 | + padding: 20px; | ||
| 567 | + cursor: pointer; | ||
| 568 | + transition: all 0.3s ease; | ||
| 569 | + display: flex; | ||
| 570 | + align-items: center; | ||
| 571 | + gap: 16px; | ||
| 572 | + text-align: left; | ||
| 573 | + | ||
| 574 | + &:hover { | ||
| 575 | + transform: translateY(-2px); | ||
| 576 | + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); | ||
| 577 | + } | ||
| 578 | + | ||
| 579 | + &.primary { | ||
| 580 | + border-color: #3498db; | ||
| 581 | + background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); | ||
| 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 { | ||
| 257 | font-size: 16px; | 642 | font-size: 16px; |
| 258 | font-weight: 600; | 643 | font-weight: 600; |
| 259 | -} | 644 | + margin-bottom: 4px; |
| 645 | + } | ||
| 260 | 646 | ||
| 261 | -.info-grid { | 647 | + .action-desc { |
| 262 | - display: grid; | 648 | + font-size: 12px; |
| 263 | - gap: 12px; | 649 | + opacity: 0.8; |
| 650 | + } | ||
| 651 | + } | ||
| 652 | + } | ||
| 264 | } | 653 | } |
| 265 | 654 | ||
| 655 | +// 系统信息卡片 | ||
| 656 | +.system-info-card { | ||
| 657 | + background: white; | ||
| 658 | + padding: 24px; | ||
| 659 | + border-radius: 12px; | ||
| 660 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); | ||
| 661 | + | ||
| 662 | + .card-title { | ||
| 663 | + font-size: 18px; | ||
| 664 | + font-weight: 600; | ||
| 665 | + color: #333; | ||
| 666 | + margin: 0 0 20px 0; | ||
| 667 | + } | ||
| 668 | + | ||
| 669 | + .info-list { | ||
| 266 | .info-item { | 670 | .info-item { |
| 267 | display: flex; | 671 | display: flex; |
| 268 | justify-content: space-between; | 672 | justify-content: space-between; |
| 269 | align-items: center; | 673 | align-items: center; |
| 270 | - padding: 8px 0; | 674 | + padding: 12px 0; |
| 271 | border-bottom: 1px solid #f0f0f0; | 675 | border-bottom: 1px solid #f0f0f0; |
| 272 | -} | ||
| 273 | 676 | ||
| 274 | -.info-item:last-child { | 677 | + &:last-child { |
| 275 | border-bottom: none; | 678 | border-bottom: none; |
| 276 | } | 679 | } |
| 277 | 680 | ||
| 278 | -.info-item label { | 681 | + .info-label { |
| 279 | font-weight: 500; | 682 | font-weight: 500; |
| 280 | color: #666; | 683 | color: #666; |
| 281 | -} | 684 | + font-size: 14px; |
| 685 | + } | ||
| 282 | 686 | ||
| 283 | -.info-item span { | 687 | + .info-value { |
| 284 | color: #333; | 688 | color: #333; |
| 285 | -} | 689 | + font-weight: 500; |
| 286 | 690 | ||
| 287 | -.quick-actions { | 691 | + &.status-normal { |
| 288 | - display: grid; | 692 | + color: #2ecc71; |
| 289 | - grid-template-columns: 1fr 1fr; | 693 | + } |
| 290 | - gap: 12px; | 694 | + |
| 695 | + &.status-error { | ||
| 696 | + color: #e74c3c; | ||
| 697 | + } | ||
| 698 | + } | ||
| 699 | + } | ||
| 700 | + } | ||
| 291 | } | 701 | } |
| 292 | 702 | ||
| 293 | -.action-btn { | 703 | +// 最近活动 |
| 294 | - background: #3498db; | 704 | +.recent-activities { |
| 295 | - color: white; | 705 | + background: white; |
| 296 | - border: none; | 706 | + padding: 24px; |
| 297 | - padding: 8px 12px; | 707 | + border-radius: 12px; |
| 298 | - border-radius: 4px; | 708 | + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); |
| 299 | - cursor: pointer; | 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; | ||
| 733 | + display: flex; | ||
| 734 | + align-items: center; | ||
| 735 | + justify-content: center; | ||
| 736 | + font-size: 16px; | ||
| 737 | + | ||
| 738 | + &.order { | ||
| 739 | + background: #e3f2fd; | ||
| 740 | + color: #1976d2; | ||
| 741 | + } | ||
| 742 | + | ||
| 743 | + &.delivery { | ||
| 744 | + background: #e8f5e8; | ||
| 745 | + color: #2e7d32; | ||
| 746 | + } | ||
| 747 | + | ||
| 748 | + &.invoice { | ||
| 749 | + background: #fff3e0; | ||
| 750 | + color: #f57c00; | ||
| 751 | + } | ||
| 752 | + | ||
| 753 | + &.rebate { | ||
| 754 | + background: #f3e5f5; | ||
| 755 | + color: #7b1fa2; | ||
| 756 | + } | ||
| 757 | + | ||
| 758 | + &.system { | ||
| 759 | + background: #f1f8e9; | ||
| 760 | + color: #558b2f; | ||
| 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 { | ||
| 300 | font-size: 12px; | 775 | font-size: 12px; |
| 301 | - transition: background-color 0.3s; | 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; |
| 800 | + } | ||
| 801 | + | ||
| 802 | + .welcome-section { | ||
| 803 | + flex-direction: column; | ||
| 804 | + text-align: center; | ||
| 805 | + gap: 20px; | ||
| 806 | + | ||
| 807 | + .user-info { | ||
| 808 | + justify-content: center; | ||
| 809 | + } | ||
| 311 | } | 810 | } |
| 312 | 811 | ||
| 313 | - .dashboard-content { | 812 | + .stats-grid { |
| 314 | grid-template-columns: 1fr; | 813 | grid-template-columns: 1fr; |
| 315 | } | 814 | } |
| 316 | 815 | ||
| 317 | - .quick-actions { | 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"> | ||
| 6 | <div class="search-row"> | 5 | <div class="search-row"> |
| 7 | <div class="search-item"> | 6 | <div class="search-item"> |
| 8 | - <span class="search-label">经销商名称</span> | 7 | + <label>经销商名称:</label> |
| 9 | - <el-input | 8 | + <input |
| 10 | v-model="searchForm.dealerName" | 9 | v-model="searchForm.dealerName" |
| 10 | + class="search-input" | ||
| 11 | placeholder="请输入经销商名称" | 11 | placeholder="请输入经销商名称" |
| 12 | - clearable | ||
| 13 | - size="small" | ||
| 14 | - style="width: 160px" | ||
| 15 | /> | 12 | /> |
| 16 | </div> | 13 | </div> |
| 17 | <div class="search-item"> | 14 | <div class="search-item"> |
| 18 | - <span class="search-label">返利编号</span> | 15 | + <label>返利编号:</label> |
| 19 | - <el-input | 16 | + <input |
| 20 | v-model="searchForm.rebateNo" | 17 | v-model="searchForm.rebateNo" |
| 18 | + class="search-input" | ||
| 21 | placeholder="请输入返利编号" | 19 | placeholder="请输入返利编号" |
| 22 | - clearable | ||
| 23 | - size="small" | ||
| 24 | - style="width: 160px" | ||
| 25 | /> | 20 | /> |
| 26 | </div> | 21 | </div> |
| 27 | <div class="search-item"> | 22 | <div class="search-item"> |
| 28 | - <span class="search-label">日期</span> | 23 | + <label>操作类型:</label> |
| 29 | - <el-date-picker | 24 | + <select v-model="searchForm.operateType" class="search-select"> |
| 30 | - v-model="dateRange" | 25 | + <option value="">所有</option> |
| 31 | - type="daterange" | 26 | + <option value="1">新增</option> |
| 32 | - range-separator="至" | 27 | + <option value="2">修改</option> |
| 33 | - start-placeholder="开始日期" | 28 | + <option value="3">删除</option> |
| 34 | - end-placeholder="结束日期" | 29 | + </select> |
| 35 | - size="small" | ||
| 36 | - format="YYYY-MM-DD" | ||
| 37 | - value-format="YYYY-MM-DD" | ||
| 38 | - @change="handleDateRangeChange" | ||
| 39 | - style="width: 240px" | ||
| 40 | - /> | ||
| 41 | </div> | 30 | </div> |
| 42 | - <div class="search-actions"> | 31 | + <div class="search-item"> |
| 43 | - <el-button type="primary" size="small" @click="handleSearch" :loading="loading"> | 32 | + <label>计算状态:</label> |
| 44 | - 查询 | 33 | + <select v-model="searchForm.calcFlag" class="search-select"> |
| 45 | - </el-button> | 34 | + <option value="">所有</option> |
| 46 | - <el-button size="small" @click="handleReset"> | 35 | + <option value="0">未计算</option> |
| 47 | - 重置 | 36 | + <option value="1">已计算</option> |
| 48 | - </el-button> | 37 | + </select> |
| 38 | + </div> | ||
| 39 | + </div> | ||
| 40 | + <div class="search-row"> | ||
| 41 | + <div class="search-item"> | ||
| 42 | + <label>审核状态:</label> | ||
| 43 | + <select v-model="searchForm.auditStatus" class="search-select"> | ||
| 44 | + <option value="">所有</option> | ||
| 45 | + <option value="0">待审核</option> | ||
| 46 | + <option value="1">审核通过</option> | ||
| 47 | + <option value="2">审核中</option> | ||
| 48 | + <option value="3">审核拒绝</option> | ||
| 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> | ||
| 49 | </div> | 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,91 +109,99 @@ | ... | @@ -96,91 +109,99 @@ |
| 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> |
| 126 | + <button @click="handleTableRefresh" class="control-btn">🔄</button> | ||
| 127 | + <button @click="handleTableExport" class="control-btn">📋</button> | ||
| 128 | + <button @click="handleTableViewToggle" class="control-btn">⊞</button> | ||
| 104 | </div> | 129 | </div> |
| 105 | - <div class="table-actions"> | ||
| 106 | - <el-button type="primary" size="small" @click="handleExport" :loading="exportLoading"> | ||
| 107 | - <el-icon><Download /></el-icon> | ||
| 108 | - 导出 | ||
| 109 | - </el-button> | ||
| 110 | - <div class="table-info"> | ||
| 111 | - 数据状态: {{ loading ? '加载中...' : `共${rebateList.length}条记录` }} | ||
| 112 | </div> | 130 | </div> |
| 131 | + | ||
| 132 | + <div class="table-container"> | ||
| 133 | + <div v-if="loading" class="loading-overlay"> | ||
| 134 | + <div class="loading-spinner">加载中...</div> | ||
| 113 | </div> | 135 | </div> |
| 136 | + <table class="data-table"> | ||
| 137 | + <thead> | ||
| 138 | + <tr> | ||
| 139 | + <th>返利编号</th> | ||
| 140 | + <th>订单编号</th> | ||
| 141 | + <th>经销商代码</th> | ||
| 142 | + <th>经销商名称</th> | ||
| 143 | + <th>产品编码</th> | ||
| 144 | + <th>返利金额</th> | ||
| 145 | + <th>返利日期</th> | ||
| 146 | + <th>操作类型</th> | ||
| 147 | + <th>计算状态</th> | ||
| 148 | + <th>更新时间</th> | ||
| 149 | + <th>操作</th> | ||
| 150 | + </tr> | ||
| 151 | + </thead> | ||
| 152 | + <tbody> | ||
| 153 | + <tr v-if="rebateList.length === 0"> | ||
| 154 | + <td colspan="11" class="empty-data">暂无数据</td> | ||
| 155 | + </tr> | ||
| 156 | + <tr v-for="row in rebateList" :key="row.rebateId"> | ||
| 157 | + <td>{{ row.rebateNo }}</td> | ||
| 158 | + <td>{{ row.orderNo }}</td> | ||
| 159 | + <td>{{ row.dealerCode }}</td> | ||
| 160 | + <td>{{ row.dealerName }}</td> | ||
| 161 | + <td>{{ row.productCode }}</td> | ||
| 162 | + <td class="amount-text">{{ formatAmount(row.rebateAmount) }}</td> | ||
| 163 | + <td>{{ row.rebateDate }}</td> | ||
| 164 | + <td> | ||
| 165 | + <span :class="getOperateTypeClass(row.operateType || 0)"> | ||
| 166 | + {{ row.operateTypeText || getOperateTypeText(row.operateType || 0) }} | ||
| 167 | + </span> | ||
| 168 | + </td> | ||
| 169 | + <td> | ||
| 170 | + <span :class="getCalcFlagClass(row.calcFlag || 0)"> | ||
| 171 | + {{ row.calcFlagText || getCalcFlagText(row.calcFlag || 0) }} | ||
| 172 | + </span> | ||
| 173 | + </td> | ||
| 174 | + <td>{{ formatDate(row.updateTime || '') }}</td> | ||
| 175 | + <td> | ||
| 176 | + <button @click="handleViewDetail(row)" class="action-link">详情</button> | ||
| 177 | + </td> | ||
| 178 | + </tr> | ||
| 179 | + </tbody> | ||
| 180 | + </table> | ||
| 114 | </div> | 181 | </div> |
| 115 | 182 | ||
| 116 | - <el-table | ||
| 117 | - v-loading="loading" | ||
| 118 | - :data="rebateList" | ||
| 119 | - style="width: 100%" | ||
| 120 | - :header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 'normal' }" | ||
| 121 | - :empty-text="rebateList.length === 0 ? '暂无数据' : ''" | ||
| 122 | - size="small" | ||
| 123 | - > | ||
| 124 | - <el-table-column prop="rebateNo" label="返利编号" width="140" align="center" /> | ||
| 125 | - <el-table-column prop="orderNo" label="订单编号" width="140" align="center" /> | ||
| 126 | - <el-table-column prop="dealerCode" label="经销商代码" width="120" align="center" /> | ||
| 127 | - <el-table-column prop="dealerName" label="经销商名称" min-width="150" /> | ||
| 128 | - <el-table-column prop="productCode" label="产品编码" width="120" align="center" /> | ||
| 129 | - <el-table-column prop="rebateAmount" label="返利金额" width="120" align="right"> | ||
| 130 | - <template #default="{ row }"> | ||
| 131 | - <span class="amount-text">{{ formatAmount(row.rebateAmount) }}</span> | ||
| 132 | - </template> | ||
| 133 | - </el-table-column> | ||
| 134 | - <el-table-column prop="rebateDate" label="返利日期" width="120" align="center" /> | ||
| 135 | - <el-table-column prop="operateTypeText" label="操作类型" width="100" align="center"> | ||
| 136 | - <template #default="{ row }"> | ||
| 137 | - <el-tag :type="getOperateTypeTagType(row.operateType)" size="small"> | ||
| 138 | - {{ row.operateTypeText || getOperateTypeText(row.operateType) }} | ||
| 139 | - </el-tag> | ||
| 140 | - </template> | ||
| 141 | - </el-table-column> | ||
| 142 | - <el-table-column prop="calcFlagText" label="计算状态" width="100" align="center"> | ||
| 143 | - <template #default="{ row }"> | ||
| 144 | - <el-tag :type="getCalcFlagTagType(row.calcFlag)" size="small"> | ||
| 145 | - {{ row.calcFlagText || getCalcFlagText(row.calcFlag) }} | ||
| 146 | - </el-tag> | ||
| 147 | - </template> | ||
| 148 | - </el-table-column> | ||
| 149 | - <el-table-column prop="updateTime" label="更新时间" width="150" align="center"> | ||
| 150 | - <template #default="{ row }"> | ||
| 151 | - {{ formatDate(row.updateTime) }} | ||
| 152 | - </template> | ||
| 153 | - </el-table-column> | ||
| 154 | - <el-table-column label="操作" width="80" align="center"> | ||
| 155 | - <template #default="{ row }"> | ||
| 156 | - <el-button type="primary" size="small" link @click="handleViewDetail(row)"> | ||
| 157 | - 详情 | ||
| 158 | - </el-button> | ||
| 159 | - </template> | ||
| 160 | - </el-table-column> | ||
| 161 | - </el-table> | ||
| 162 | - | ||
| 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 | 页 |
| ... | @@ -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,23 +1601,57 @@ onMounted(async () => { | ... | @@ -1478,23 +1601,57 @@ 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 { | ||
| 1482 | .search-row { | 1604 | .search-row { |
| 1483 | display: flex; | 1605 | display: flex; |
| 1484 | align-items: center; | 1606 | align-items: center; |
| 1485 | gap: 24px; | 1607 | gap: 24px; |
| 1486 | flex-wrap: wrap; | 1608 | flex-wrap: wrap; |
| 1609 | + margin-bottom: 12px; | ||
| 1610 | + | ||
| 1611 | + &:last-child { | ||
| 1612 | + margin-bottom: 0; | ||
| 1613 | + } | ||
| 1487 | 1614 | ||
| 1488 | .search-item { | 1615 | .search-item { |
| 1489 | display: flex; | 1616 | display: flex; |
| 1490 | align-items: center; | 1617 | align-items: center; |
| 1491 | gap: 8px; | 1618 | gap: 8px; |
| 1492 | 1619 | ||
| 1493 | - .search-label { | 1620 | + label { |
| 1494 | font-size: 14px; | 1621 | font-size: 14px; |
| 1495 | color: #333; | 1622 | color: #333; |
| 1496 | white-space: nowrap; | 1623 | white-space: nowrap; |
| 1497 | - min-width: 70px; | 1624 | + min-width: 80px; |
| 1625 | + } | ||
| 1626 | + | ||
| 1627 | + .search-input { | ||
| 1628 | + padding: 8px 12px; | ||
| 1629 | + border: 1px solid #ddd; | ||
| 1630 | + border-radius: 4px; | ||
| 1631 | + font-size: 14px; | ||
| 1632 | + width: 160px; | ||
| 1633 | + transition: border-color 0.3s; | ||
| 1634 | + | ||
| 1635 | + &:focus { | ||
| 1636 | + outline: none; | ||
| 1637 | + border-color: #409eff; | ||
| 1638 | + } | ||
| 1639 | + } | ||
| 1640 | + | ||
| 1641 | + .search-select { | ||
| 1642 | + padding: 8px 12px; | ||
| 1643 | + border: 1px solid #ddd; | ||
| 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 | + } | ||
| 1498 | } | 1655 | } |
| 1499 | } | 1656 | } |
| 1500 | 1657 | ||
| ... | @@ -1502,6 +1659,96 @@ onMounted(async () => { | ... | @@ -1502,6 +1659,96 @@ onMounted(async () => { |
| 1502 | margin-left: auto; | 1659 | margin-left: auto; |
| 1503 | display: flex; | 1660 | display: flex; |
| 1504 | gap: 8px; | 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,55 +1827,107 @@ onMounted(async () => { | ... | @@ -1580,55 +1827,107 @@ 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 | + } | ||
| 1862 | + | ||
| 1863 | + .table-container { | ||
| 1864 | + position: relative; | ||
| 1599 | 1865 | ||
| 1600 | - .table-actions { | 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 | - } | ||
| 1609 | } | 1881 | } |
| 1610 | } | 1882 | } |
| 1611 | 1883 | ||
| 1612 | - .el-table { | 1884 | + .data-table { |
| 1613 | - border: none; | 1885 | + width: 100%; |
| 1886 | + border-collapse: collapse; | ||
| 1887 | + font-size: 14px; | ||
| 1888 | + | ||
| 1889 | + thead { | ||
| 1890 | + background: #fafafa; | ||
| 1614 | 1891 | ||
| 1615 | - :deep(.el-table__header) { | ||
| 1616 | th { | 1892 | th { |
| 1617 | - background-color: #fafafa; | 1893 | + padding: 12px 16px; |
| 1618 | - border: none; | 1894 | + text-align: left; |
| 1619 | font-weight: 500; | 1895 | font-weight: 500; |
| 1620 | color: #333; | 1896 | color: #333; |
| 1897 | + border-bottom: 1px solid #e0e0e0; | ||
| 1898 | + white-space: nowrap; | ||
| 1621 | } | 1899 | } |
| 1622 | } | 1900 | } |
| 1623 | 1901 | ||
| 1624 | - :deep(.el-table__body) { | 1902 | + tbody { |
| 1625 | - tr:hover > td { | 1903 | + tr { |
| 1626 | - background-color: #f5f7fa; | 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; | ||
| 1627 | } | 1916 | } |
| 1628 | 1917 | ||
| 1629 | td { | 1918 | td { |
| 1630 | - border: none; | 1919 | + padding: 12px 16px; |
| 1631 | border-bottom: 1px solid #f0f0f0; | 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 | } |
| ... | @@ -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; | ... | ... |
-
Please register or login to post a comment