zhouhui.jiang
Showing 30 changed files with 2763 additions and 386 deletions
......@@ -27,7 +27,55 @@
- `message`: 响应消息
- `data`: 响应数据,具体内容根据接口而定
## 1. 认证管理 (AuthController)
## 1. 首页仪表板 (DashboardController)
### 1.1 获取首页统计数据
**接口路径:** `GET /api/dashboard/stats`
**功能描述:** 获取首页仪表板的统计数据,包括今日订单、待出库、待开票、销售额等信息
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
```
**响应示例:**
```json
{
"code": 200,
"message": "获取统计数据成功",
"data": {
"todayOrderCount": 23,
"pendingDeliveryCount": 8,
"pendingInvoiceCount": 5,
"todaySalesAmount": 125680.50,
"onlineUserCount": 156,
"systemStatus": "正常运行",
"systemVersion": "v1.0.0"
}
}
```
**错误响应示例:**
```json
{
"code": 500,
"message": "获取统计数据失败: 数据库连接异常",
"data": null
}
```
**响应字段说明:**
- `todayOrderCount`: 今日订单数量
- `pendingDeliveryCount`: 待出库数量
- `pendingInvoiceCount`: 待开票数量
- `todaySalesAmount`: 今日销售额
- `onlineUserCount`: 在线用户数
- `systemStatus`: 系统状态
- `systemVersion`: 系统版本
## 2. 认证管理 (AuthController)
### 1.1 用户登录
......@@ -2491,8 +2539,418 @@
**响应:** 返回Excel文件流,文件名格式:`发票数据_yyyyMMdd_HHmmss.xlsx`
### 4. 异常工单数据导出
**接口路径:** `POST /api/exception-workorder/export`
**请求方法:** POST
**权限要求:** `exception:workorder:export`
**请求参数:** 同异常工单查询接口参数
**响应:** 返回Excel文件流,文件名格式:`异常工单数据_yyyyMMdd_HHmmss.xlsx`
## 12. 异常工单管理 (ExceptionWorkorderController)
### 12.1 分页查询异常工单列表
**接口路径:** `GET /api/exception-workorder/list`
**功能描述:** 根据条件分页查询异常工单列表
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
```
**请求参数:**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| workorderNo | String | 否 | 工单编号(模糊查询) |
| orderNo | String | 否 | 关联订单号(模糊查询) |
| dealerCode | String | 否 | 经销商编码 |
| dealerName | String | 否 | 经销商名称(模糊查询) |
| workorderStatus | Integer | 否 | 工单状态(1-待处理/2-处理中/3-已解决/4-已关闭) |
| exceptionType | Integer | 否 | 异常类型(1-逻辑验证异常/2-源头验证异常/3-交叉验证异常) |
| severityLevel | Integer | 否 | 严重程度(1-高/2-中/3-低) |
| handlerUser | String | 否 | 处理人(模糊查询) |
| startTime | String | 否 | 创建开始时间(yyyy-MM-dd HH:mm:ss) |
| endTime | String | 否 | 创建结束时间(yyyy-MM-dd HH:mm:ss) |
| pageNum | Integer | 是 | 页码,默认1 |
| pageSize | Integer | 是 | 每页大小,默认10 |
**权限要求:** `exception:workorder:list`
**响应示例:**
```json
{
"code": 200,
"message": "操作成功",
"data": {
"records": [
{
"workorderId": 1,
"workorderNo": "EW202501290001",
"orderNo": "ORD202501290001",
"dealerCode": "D001",
"dealerName": "北京经销商",
"exceptionType": 1,
"exceptionTypeName": "逻辑验证异常",
"severityLevel": 1,
"severityLevelName": "高",
"workorderStatus": 1,
"workorderStatusName": "待处理",
"createTime": "2025-01-29 10:00:00",
"expectCompleteTime": "2025-01-30 18:00:00",
"handlerUser": "张三",
"exceptionDesc": "订单金额与产品单价不匹配",
"handleSuggest": "请核实订单明细",
"dataSource": "系统自动生成",
"createBy": "system",
"updateBy": null,
"updateTime": null,
"workorderLogs": []
}
],
"total": 1,
"size": 10,
"current": 1,
"pages": 1
}
}
```
### 12.2 获取异常工单详情
**接口路径:** `GET /api/exception-workorder/{workorderId}`
**功能描述:** 根据工单ID获取异常工单详细信息,包含处理日志
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
```
**路径参数:**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| workorderId | Long | 是 | 工单ID |
**权限要求:** `exception:workorder:detail`
**响应示例:**
```json
{
"code": 200,
"message": "操作成功",
"data": {
"workorderId": 1,
"workorderNo": "EW202501290001",
"orderNo": "ORD202501290001",
"dealerCode": "D001",
"dealerName": "北京经销商",
"exceptionType": 1,
"exceptionTypeName": "逻辑验证异常",
"severityLevel": 1,
"severityLevelName": "高",
"workorderStatus": 2,
"workorderStatusName": "处理中",
"createTime": "2025-01-29 10:00:00",
"expectCompleteTime": "2025-01-30 18:00:00",
"handlerUser": "张三",
"exceptionDesc": "订单金额与产品单价不匹配",
"handleSuggest": "请核实订单明细",
"dataSource": "系统自动生成",
"createBy": "system",
"updateBy": "张三",
"updateTime": "2025-01-29 11:00:00",
"workorderLogs": [
{
"logId": 1,
"workorderId": 1,
"workorderNo": "EW202501290001",
"handleUser": "张三",
"handleTime": "2025-01-29 11:00:00",
"beforeStatus": 1,
"beforeStatusName": "待处理",
"afterStatus": 2,
"afterStatusName": "处理中",
"handleOpinion": "开始处理此工单",
"attachUrl": null,
"createBy": "张三",
"createTime": "2025-01-29 11:00:00"
}
]
}
}
```
### 12.3 新增异常工单
**接口路径:** `POST /api/exception-workorder`
**功能描述:** 创建新的异常工单
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
```
**请求体:**
```json
{
"workorderNo": "EW202501290002",
"orderNo": "ORD202501290002",
"dealerCode": "D002",
"dealerName": "上海经销商",
"exceptionType": 2,
"severityLevel": 2,
"workorderStatus": 1,
"expectCompleteTime": "2025-01-30 18:00:00",
"handlerUser": "李四",
"exceptionDesc": "经销商信息不完整",
"handleSuggest": "请补充经销商详细信息",
"dataSource": "手动创建"
}
```
**权限要求:** `exception:workorder:add`
**响应示例:**
```json
{
"code": 200,
"message": "新增异常工单成功",
"data": null
}
```
### 12.4 更新异常工单
**接口路径:** `PUT /api/exception-workorder`
**功能描述:** 更新异常工单信息
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
```
**请求体:**
```json
{
"workorderId": 1,
"workorderNo": "EW202501290001",
"orderNo": "ORD202501290001",
"dealerCode": "D001",
"dealerName": "北京经销商",
"exceptionType": 1,
"severityLevel": 1,
"workorderStatus": 2,
"expectCompleteTime": "2025-01-30 18:00:00",
"handlerUser": "张三",
"exceptionDesc": "订单金额与产品单价不匹配",
"handleSuggest": "请核实订单明细并联系客户确认",
"dataSource": "系统自动生成"
}
```
**权限要求:** `exception:workorder:edit`
**响应示例:**
```json
{
"code": 200,
"message": "更新异常工单成功",
"data": null
}
```
### 12.5 更新工单状态
**接口路径:** `POST /api/exception-workorder/status`
**功能描述:** 更新异常工单状态并记录处理日志
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
```
**请求体:**
```json
{
"workorderId": 1,
"workorderStatus": 3,
"handlerUser": "张三",
"handleOpinion": "问题已解决,客户确认无误",
"attachUrl": "http://example.com/attachment.pdf"
}
```
**权限要求:** `exception:workorder:edit`
**响应示例:**
```json
{
"code": 200,
"message": "更新工单状态成功",
"data": null
}
```
### 12.6 删除异常工单
**接口路径:** `DELETE /api/exception-workorder/{workorderId}`
**功能描述:** 根据工单ID逻辑删除异常工单
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
```
**路径参数:**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| workorderId | Long | 是 | 工单ID |
**权限要求:** `exception:workorder:delete`
**响应示例:**
```json
{
"code": 200,
"message": "删除异常工单成功",
"data": null
}
```
### 12.7 批量删除异常工单
**接口路径:** `DELETE /api/exception-workorder/batch`
**功能描述:** 根据工单ID列表批量逻辑删除异常工单
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
```
**请求体:**
```json
[1, 2, 3]
```
**权限要求:** `exception:workorder:delete`
**响应示例:**
```json
{
"code": 200,
"message": "批量删除异常工单成功",
"data": null
}
```
### 12.8 批量更新工单状态
**接口路径:** `POST /api/exception-workorder/batch-status`
**功能描述:** 批量更新异常工单状态
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
```
**请求参数:**
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| workorderIds | String | 是 | 工单ID列表,逗号分隔 |
| workorderStatus | Integer | 是 | 工单状态 |
| handlerUser | String | 是 | 处理人 |
**权限要求:** `exception:workorder:edit`
**响应示例:**
```json
{
"code": 200,
"message": "批量更新工单状态成功",
"data": null
}
```
### 12.9 获取异常工单统计信息
**接口路径:** `GET /api/exception-workorder/stats`
**功能描述:** 获取异常工单的统计信息
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
```
**权限要求:** `exception:workorder:list`
**响应示例:**
```json
{
"code": 200,
"message": "操作成功",
"data": [
{
"workorderNo": "total",
"workorderId": 10,
"exceptionType": 3,
"severityLevel": 2,
"workorderStatus": 4,
"handlerUser": 1,
"exceptionDesc": 5,
"handleSuggest": 3,
"dataSource": 2
}
]
}
```
### 12.10 导出异常工单数据
**接口路径:** `POST /api/exception-workorder/export`
**功能描述:** 根据查询条件导出异常工单数据到Excel
**请求头:**
```
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
```
**请求体:** 同异常工单查询接口参数(可选,为空时导出所有数据)
**权限要求:** `exception:workorder:export`
**响应:** 返回Excel文件流,文件名格式:`异常工单数据_yyyyMMdd_HHmmss.xlsx`
**导出字段:**
- 工单ID
- 工单编号
- 工单类型(异常类型名称)
- 严重程度(严重程度名称)
- 工单状态(工单状态名称)
- 问题描述
- 处理人
- 创建人
- 创建时间
- 更新时间
---
**文档版本:** 1.0.0
**最后更新:** 2025-01-27
**最后更新:** 2025-01-29
**维护人员:** Apple ERP Team
......
package com.apple.erp.config;
import com.apple.erp.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -34,9 +32,6 @@ import java.util.Arrays;
public class SecurityConfig {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Autowired
......@@ -72,6 +67,7 @@ public class SecurityConfig {
.antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/test/public").permitAll() // 允许测试接口
.antMatchers("/api/dashboard/**").authenticated() // 首页统计需要认证
.antMatchers("/actuator/**").permitAll()
.antMatchers("/druid/**").permitAll()
.antMatchers("/swagger-ui/**").permitAll()
......
......@@ -9,6 +9,7 @@ import com.apple.erp.dto.response.UserInfoRes;
import com.apple.erp.entity.SysUser;
import com.apple.erp.service.SysUserService;
import com.apple.erp.service.SysLogService;
import com.apple.erp.service.SessionService;
import com.apple.erp.utils.JwtUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
......@@ -24,7 +25,9 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 认证控制器
......@@ -51,6 +54,9 @@ public class AuthController {
@Autowired
private SysLogService sysLogService;
@Autowired
private SessionService sessionService;
/**
* 用户登录
......@@ -95,6 +101,21 @@ public class AuthController {
// 生成JWT令牌
String token = jwtUtils.generateToken(username);
String refreshToken = jwtUtils.generateRefreshToken(username);
// 将用户会话信息存储到Redis
try {
Map<String, Object> sessionData = new HashMap<>();
sessionData.put("username", username);
sessionData.put("loginTime", System.currentTimeMillis());
sessionData.put("clientIp", getClientIp(request));
sessionData.put("token", token);
sessionService.storeUserSession(username, sessionData);
log.info("用户会话已存储: {}", username);
} catch (Exception e) {
log.error("存储用户会话失败", e);
// Redis失败不影响登录流程
}
LoginRes loginResponse = new LoginRes(token, refreshToken, username);
ApiRes<LoginRes> response = ApiRes.success("登录成功", loginResponse);
......@@ -192,6 +213,16 @@ public class AuthController {
}
}
// 清除Redis中的用户会话
if (username != null) {
try {
sessionService.removeUserSession(username);
log.info("用户会话已清除: {}", username);
} catch (Exception e) {
log.error("清除用户会话失败", e);
}
}
// 清除Spring Security上下文
SecurityContextHolder.clearContext();
......
package com.apple.erp.controller;
import com.apple.erp.dto.response.ApiRes;
import com.apple.erp.dto.response.DashboardStatsRes;
import com.apple.erp.service.DashboardService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* 首页仪表板控制器
* 提供首页统计数据接口
*/
@Slf4j
@RestController
@RequestMapping("/api/dashboard")
public class DashboardController {
@Autowired
private DashboardService dashboardService;
/**
* 获取首页统计数据
* @return 统计数据
*/
@GetMapping("/stats")
public ApiRes<DashboardStatsRes> getDashboardStats() {
log.info("获取首页统计数据请求");
try {
DashboardStatsRes result = dashboardService.getDashboardStats();
log.info("首页统计数据获取成功: {}", result);
return ApiRes.success("获取统计数据成功", result);
} catch (Exception e) {
log.error("获取首页统计数据失败", e);
return ApiRes.error(500, "获取统计数据失败: " + e.getMessage());
}
}
/**
* 测试接口
* @return 测试数据
*/
@GetMapping("/test")
public ApiRes<DashboardStatsRes> getTestStats() {
log.info("测试接口调用");
DashboardStatsRes stats = new DashboardStatsRes();
stats.setTodayOrderCount(10);
stats.setPendingDeliveryCount(5);
stats.setPendingInvoiceCount(3);
stats.setTodaySalesAmount(10000.0);
stats.setOnlineUserCount(100);
stats.setSystemStatus("测试正常");
stats.setSystemVersion("v1.0.0");
return ApiRes.success("测试成功", stats);
}
/**
* 获取最近活动
* @return 最近活动列表
*/
@GetMapping("/activities")
public ApiRes<List<Map<String, Object>>> getRecentActivities() {
log.info("获取最近活动请求");
try {
List<Map<String, Object>> activities = dashboardService.getRecentActivities();
return ApiRes.success("获取最近活动成功", activities);
} catch (Exception e) {
log.error("获取最近活动失败", e);
return ApiRes.error(500, "获取最近活动失败: " + e.getMessage());
}
}
}
......@@ -6,12 +6,14 @@ import com.apple.erp.dto.ExceptionWorkorderRes;
import com.apple.erp.dto.ExceptionWorkorderStatusUpdateReq;
import com.apple.erp.dto.ExceptionWorkorderUpdateReq;
import com.apple.erp.service.ExceptionWorkorderService;
import com.apple.erp.service.ExcelExportService;
import com.apple.erp.dto.response.ApiRes;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
......@@ -33,6 +35,9 @@ public class ExceptionWorkorderController {
@Autowired
private ExceptionWorkorderService exceptionWorkorderService;
@Autowired
private ExcelExportService excelExportService;
@Operation(summary = "分页查询异常工单列表", description = "根据条件分页查询异常工单列表")
@GetMapping("/list")
......@@ -168,4 +173,22 @@ public class ExceptionWorkorderController {
List<ExceptionWorkorderRes> stats = exceptionWorkorderService.getExceptionWorkorderStats();
return ApiRes.success(stats);
}
@Operation(summary = "导出异常工单数据", description = "根据查询条件导出异常工单数据到Excel")
@PostMapping("/export")
@PreAuthorize("hasAuthority('exception:workorder:export')")
public ResponseEntity<byte[]> exportExceptionWorkorders(@RequestBody(required = false) ExceptionWorkorderQueryReq queryReq) {
try {
// 如果请求体为空,创建默认查询条件
if (queryReq == null) {
queryReq = new ExceptionWorkorderQueryReq();
}
// 获取异常工单数据
List<ExceptionWorkorderRes> workorders = exceptionWorkorderService.getExceptionWorkorderListForExport(queryReq);
// 导出Excel
return excelExportService.exportExceptionWorkorders(workorders);
} catch (Exception e) {
throw new RuntimeException("导出异常工单数据失败: " + e.getMessage());
}
}
}
......
package com.apple.erp.dto.response;
import lombok.Data;
/**
* 首页统计数据响应DTO
*/
@Data
public class DashboardStatsRes {
/**
* 今日订单数量
*/
private Integer todayOrderCount;
/**
* 待出库数量
*/
private Integer pendingDeliveryCount;
/**
* 待开票数量
*/
private Integer pendingInvoiceCount;
/**
* 今日销售额
*/
private Double todaySalesAmount;
/**
* 在线用户数
*/
private Integer onlineUserCount;
/**
* 系统状态
*/
private String systemStatus;
/**
* 系统版本
*/
private String systemVersion;
/**
* 今日订单增长率
*/
private String orderGrowthRate;
/**
* 今日销售额增长率
*/
private String salesGrowthRate;
}
......@@ -54,4 +54,12 @@ public interface ExceptionWorkorderMapper extends BaseMapper<ExceptionWorkorder>
int batchUpdateWorkorderStatus(@Param("workorderIds") List<Long> workorderIds,
@Param("workorderStatus") Integer workorderStatus,
@Param("handlerUser") String handlerUser);
/**
* 获取异常工单列表用于导出
*
* @param queryReq 查询条件
* @return 异常工单列表(不分页)
*/
List<ExceptionWorkorderRes> selectExceptionWorkorderListForExport(@Param("query") ExceptionWorkorderQueryReq queryReq);
}
......
package com.apple.erp.service;
import com.apple.erp.dto.response.DashboardStatsRes;
import java.util.List;
import java.util.Map;
/**
* 首页仪表板服务接口
*/
public interface DashboardService {
/**
* 获取首页统计数据
* @return 统计数据
*/
DashboardStatsRes getDashboardStats();
/**
* 获取最近活动
* @return 最近活动列表
*/
List<Map<String, Object>> getRecentActivities();
}
......@@ -67,6 +67,20 @@ public interface DeliveryMainService extends IService<DeliveryMain> {
* @return 是否成功
*/
boolean updateDeliveryStatus(Long deliveryId, Integer deliveryStatus);
/**
* 获取待出库数量
* @return 待出库数量
*/
Integer getPendingDeliveryCount();
/**
* 获取最近的出库记录
*
* @param limit 限制数量
* @return 最近出库列表
*/
List<DeliveryRes> getRecentDeliveries(Integer limit);
}
......
......@@ -91,4 +91,12 @@ public interface ExceptionWorkorderService extends IService<ExceptionWorkorder>
* @return 统计信息
*/
List<ExceptionWorkorderRes> getExceptionWorkorderStats();
/**
* 获取异常工单列表用于导出
*
* @param queryReq 查询条件
* @return 异常工单列表(不分页)
*/
List<ExceptionWorkorderRes> getExceptionWorkorderListForExport(ExceptionWorkorderQueryReq queryReq);
}
......
......@@ -67,6 +67,20 @@ public interface InvoiceMainService extends IService<InvoiceMain> {
* @return 是否成功
*/
boolean updateInvoiceStatus(Long invoiceId, Integer invoiceStatus);
/**
* 获取待开票数量
* @return 待开票数量
*/
Integer getPendingInvoiceCount();
/**
* 获取最近的发票记录
*
* @param limit 限制数量
* @return 最近发票列表
*/
List<InvoiceRes> getRecentInvoices(Integer limit);
}
......
......@@ -92,5 +92,29 @@ public interface OrderMainService extends IService<OrderMain> {
* @return 是否成功
*/
boolean updateRebateCalcFlag(Long orderId, Integer rebateCalcFlag);
/**
* 获取今日订单数量
*
* @param date 日期
* @return 订单数量
*/
Integer getTodayOrderCount(String date);
/**
* 获取今日销售额
*
* @param date 日期
* @return 销售额
*/
Double getTodaySalesAmount(String date);
/**
* 获取最近的订单记录
*
* @param limit 限制数量
* @return 最近订单列表
*/
List<OrderRes> getRecentOrders(Integer limit);
}
......
......@@ -139,4 +139,12 @@ public interface RebateService extends IService<Rebate> {
* @return 趋势统计数据
*/
List<Map<String, Object>> getRebateTrendStats(String startDate, String endDate);
/**
* 获取最近的返利记录
*
* @param limit 限制数量
* @return 最近返利列表
*/
List<RebateRes> getRecentRebates(Integer limit);
}
......
package com.apple.erp.service;
import java.util.Map;
/**
* 会话管理服务接口
*
* @author Apple ERP Team
* @version 1.0.0
* @since 2024-01-01
*/
public interface SessionService {
/**
* 存储用户会话信息
* @param username 用户名
* @param sessionData 会话数据
*/
void storeUserSession(String username, Map<String, Object> sessionData);
/**
* 获取用户会话信息
* @param username 用户名
* @return 会话数据
*/
Map<String, Object> getUserSession(String username);
/**
* 删除用户会话
* @param username 用户名
*/
void removeUserSession(String username);
/**
* 获取在线用户数
* @return 在线用户数
*/
Integer getOnlineUserCount();
/**
* 清理过期会话
*/
void cleanupExpiredSessions();
}
package com.apple.erp.service.impl;
import com.apple.erp.dto.response.DashboardStatsRes;
import com.apple.erp.dto.OrderRes;
import com.apple.erp.dto.DeliveryRes;
import com.apple.erp.dto.InvoiceRes;
import com.apple.erp.dto.response.RebateRes;
import com.apple.erp.service.DashboardService;
import com.apple.erp.service.OrderMainService;
import com.apple.erp.service.DeliveryMainService;
import com.apple.erp.service.InvoiceMainService;
import com.apple.erp.service.RebateService;
import com.apple.erp.service.SessionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 首页仪表板服务实现
*/
@Slf4j
@Service
public class DashboardServiceImpl implements DashboardService {
@Autowired
private OrderMainService orderMainService;
@Autowired
private DeliveryMainService deliveryMainService;
@Autowired
private InvoiceMainService invoiceMainService;
@Autowired
private RebateService rebateService;
@Autowired
private SessionService sessionService;
@Override
public DashboardStatsRes getDashboardStats() {
log.info("开始获取首页统计数据");
DashboardStatsRes stats = new DashboardStatsRes();
try {
// 获取今日订单数量
String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
log.info("查询今日订单数量,日期: {}", today);
stats.setTodayOrderCount(orderMainService.getTodayOrderCount(today));
// 获取待出库数量(状态为已确认但未出库的订单)
log.info("查询待出库数量");
stats.setPendingDeliveryCount(deliveryMainService.getPendingDeliveryCount());
// 获取待开票数量(已出库但未开票的订单)
log.info("查询待开票数量");
stats.setPendingInvoiceCount(invoiceMainService.getPendingInvoiceCount());
// 获取今日销售额
log.info("查询今日销售额,日期: {}", today);
stats.setTodaySalesAmount(orderMainService.getTodaySalesAmount(today));
// 获取在线用户数(从Redis或Session中获取)
stats.setOnlineUserCount(getOnlineUserCount());
// 系统状态
stats.setSystemStatus(getSystemStatus());
// 系统版本
stats.setSystemVersion("v1.0.0");
// 计算增长率数据
stats.setOrderGrowthRate(calculateOrderGrowthRate(today));
stats.setSalesGrowthRate(calculateSalesGrowthRate(today));
log.info("首页统计数据获取完成: {}", stats);
} catch (Exception e) {
log.error("获取首页统计数据失败", e);
// 如果获取数据失败,返回默认值
stats.setTodayOrderCount(0);
stats.setPendingDeliveryCount(0);
stats.setPendingInvoiceCount(0);
stats.setTodaySalesAmount(0.0);
stats.setOnlineUserCount(0);
stats.setSystemStatus("系统异常");
stats.setSystemVersion("v1.0.0");
stats.setOrderGrowthRate("0%");
stats.setSalesGrowthRate("0%");
}
return stats;
}
@Override
public List<Map<String, Object>> getRecentActivities() {
log.info("开始获取最近活动数据");
List<Map<String, Object>> activities = new ArrayList<>();
try {
// 从数据库获取最近的活动数据
activities.addAll(getRecentOrders());
activities.addAll(getRecentDeliveries());
activities.addAll(getRecentInvoices());
activities.addAll(getRecentRebates());
// 按时间排序,取最新的5条
activities.sort((a, b) -> {
String timeA = (String) a.get("time");
String timeB = (String) b.get("time");
return timeB.compareTo(timeA);
});
if (activities.size() > 5) {
activities = activities.subList(0, 5);
}
log.info("最近活动数据获取完成,共{}条记录", activities.size());
} catch (Exception e) {
log.error("获取最近活动数据失败", e);
}
return activities;
}
/**
* 获取最近的订单活动
*/
private List<Map<String, Object>> getRecentOrders() {
List<Map<String, Object>> activities = new ArrayList<>();
try {
List<OrderRes> recentOrders = orderMainService.getRecentOrders(2);
for (OrderRes order : recentOrders) {
Map<String, Object> activity = new HashMap<>();
activity.put("id", order.getOrderId());
activity.put("type", "order");
activity.put("title", "新订单 " + order.getOrderNo() + " 已创建");
activity.put("time", formatTimeAgo(order.getCreateTime()));
activities.add(activity);
}
} catch (Exception e) {
log.error("获取最近订单活动失败", e);
}
return activities;
}
/**
* 获取最近的出库活动
*/
private List<Map<String, Object>> getRecentDeliveries() {
List<Map<String, Object>> activities = new ArrayList<>();
try {
List<DeliveryRes> recentDeliveries = deliveryMainService.getRecentDeliveries(2);
for (DeliveryRes delivery : recentDeliveries) {
Map<String, Object> activity = new HashMap<>();
activity.put("id", delivery.getDeliveryId());
activity.put("type", "delivery");
activity.put("title", "出库单 " + delivery.getDeliveryNo() + " 已创建");
activity.put("time", formatTimeAgo(delivery.getCreateTime()));
activities.add(activity);
}
} catch (Exception e) {
log.error("获取最近出库活动失败", e);
}
return activities;
}
/**
* 获取最近的发票活动
*/
private List<Map<String, Object>> getRecentInvoices() {
List<Map<String, Object>> activities = new ArrayList<>();
try {
List<InvoiceRes> recentInvoices = invoiceMainService.getRecentInvoices(2);
for (InvoiceRes invoice : recentInvoices) {
Map<String, Object> activity = new HashMap<>();
activity.put("id", invoice.getInvoiceId());
activity.put("type", "invoice");
activity.put("title", "发票 " + invoice.getInvoiceNo() + " 已开具");
activity.put("time", formatTimeAgo(invoice.getCreateTime()));
activities.add(activity);
}
} catch (Exception e) {
log.error("获取最近发票活动失败", e);
}
return activities;
}
/**
* 获取最近的返利活动
*/
private List<Map<String, Object>> getRecentRebates() {
List<Map<String, Object>> activities = new ArrayList<>();
try {
List<RebateRes> recentRebates = rebateService.getRecentRebates(2);
for (RebateRes rebate : recentRebates) {
Map<String, Object> activity = new HashMap<>();
activity.put("id", rebate.getRebateId());
activity.put("type", "rebate");
activity.put("title", "返利 " + rebate.getRebateNo() + " 已计算");
activity.put("time", formatTimeAgo(rebate.getCreateTime()));
activities.add(activity);
}
} catch (Exception e) {
log.error("获取最近返利活动失败", e);
}
return activities;
}
/**
* 获取在线用户数
*/
private Integer getOnlineUserCount() {
try {
// 使用SessionService获取在线用户数
Integer onlineCount = sessionService.getOnlineUserCount();
log.info("获取在线用户数: {}", onlineCount);
return onlineCount;
} catch (Exception e) {
log.error("获取在线用户数失败", e);
// 如果Redis不可用,尝试从数据库获取活跃用户数
return getActiveUserCountFromDatabase();
}
}
/**
* 从数据库获取活跃用户数(Redis不可用时的备用方案)
*/
private Integer getActiveUserCountFromDatabase() {
try {
// 查询最近30分钟内有登录记录的用户数
// 这里需要根据实际的用户表结构调整
log.info("从数据库获取活跃用户数");
// TODO: 实现数据库查询逻辑
return 0;
} catch (Exception e) {
log.error("从数据库获取活跃用户数失败", e);
return 0;
}
}
/**
* 获取系统状态
*/
private String getSystemStatus() {
try {
// 检查数据库连接状态
// 检查关键服务状态
// 这里暂时返回正常运行,后续可以添加健康检查
return "正常运行";
} catch (Exception e) {
log.error("获取系统状态失败", e);
return "系统异常";
}
}
/**
* 计算订单增长率
*/
private String calculateOrderGrowthRate(String today) {
try {
// 获取昨日订单数量
String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
Integer yesterdayCount = orderMainService.getTodayOrderCount(yesterday);
Integer todayCount = orderMainService.getTodayOrderCount(today);
if (yesterdayCount == null || yesterdayCount == 0) {
return todayCount > 0 ? "+100%" : "0%";
}
double growthRate = ((double) (todayCount - yesterdayCount) / yesterdayCount) * 100;
return String.format("%+.1f%%", growthRate);
} catch (Exception e) {
log.error("计算订单增长率失败", e);
return "0%";
}
}
/**
* 计算销售额增长率
*/
private String calculateSalesGrowthRate(String today) {
try {
// 获取昨日销售额
String yesterday = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
Double yesterdayAmount = orderMainService.getTodaySalesAmount(yesterday);
Double todayAmount = orderMainService.getTodaySalesAmount(today);
if (yesterdayAmount == null || yesterdayAmount == 0) {
return todayAmount > 0 ? "+100%" : "0%";
}
double growthRate = ((todayAmount - yesterdayAmount) / yesterdayAmount) * 100;
return String.format("%+.1f%%", growthRate);
} catch (Exception e) {
log.error("计算销售额增长率失败", e);
return "0%";
}
}
/**
* 格式化时间为相对时间
*/
private String formatTimeAgo(java.time.LocalDateTime dateTime) {
if (dateTime == null) {
return "未知时间";
}
java.time.LocalDateTime now = java.time.LocalDateTime.now();
long minutes = java.time.Duration.between(dateTime, now).toMinutes();
if (minutes < 1) {
return "刚刚";
} else if (minutes < 60) {
return minutes + "分钟前";
} else if (minutes < 1440) { // 24小时
long hours = minutes / 60;
return hours + "小时前";
} else {
long days = minutes / 1440;
return days + "天前";
}
}
}
......@@ -22,6 +22,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
......@@ -254,6 +255,34 @@ public class DeliveryMainServiceImpl extends ServiceImpl<DeliveryMainMapper, Del
return null;
}
}
@Override
public Integer getPendingDeliveryCount() {
LambdaQueryWrapper<DeliveryMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeliveryMain::getDelFlag, "0")
.eq(DeliveryMain::getDeliveryStatus, 0); // 0表示待出库
return Math.toIntExact(count(wrapper));
}
@Override
public List<DeliveryRes> getRecentDeliveries(Integer limit) {
try {
LambdaQueryWrapper<DeliveryMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(DeliveryMain::getDelFlag, "0")
.orderByDesc(DeliveryMain::getCreateTime)
.last("LIMIT " + (limit != null ? limit : 5));
List<DeliveryMain> deliveries = list(wrapper);
return deliveries.stream().map(delivery -> {
DeliveryRes deliveryRes = new DeliveryRes();
BeanUtils.copyProperties(delivery, deliveryRes);
return deliveryRes;
}).collect(Collectors.toList());
} catch (Exception e) {
log.error("获取最近出库记录失败", e);
return new ArrayList<>();
}
}
}
......
......@@ -18,11 +18,9 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 异常工单Service实现类
......@@ -200,4 +198,10 @@ public class ExceptionWorkorderServiceImpl extends ServiceImpl<ExceptionWorkorde
public List<ExceptionWorkorderRes> getExceptionWorkorderStats() {
return exceptionWorkorderMapper.selectExceptionWorkorderStats();
}
@Override
public List<ExceptionWorkorderRes> getExceptionWorkorderListForExport(ExceptionWorkorderQueryReq queryReq) {
// 不分页查询所有数据用于导出
return exceptionWorkorderMapper.selectExceptionWorkorderListForExport(queryReq);
}
}
......
......@@ -21,6 +21,7 @@ import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
......@@ -229,4 +230,31 @@ public class InvoiceMainServiceImpl extends ServiceImpl<InvoiceMainMapper, Invoi
}
}
@Override
public Integer getPendingInvoiceCount() {
LambdaQueryWrapper<InvoiceMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(InvoiceMain::getDelFlag, "0")
.eq(InvoiceMain::getInvoiceStatus, 0); // 0表示待开票
return Math.toIntExact(count(wrapper));
}
@Override
public List<InvoiceRes> getRecentInvoices(Integer limit) {
try {
LambdaQueryWrapper<InvoiceMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(InvoiceMain::getDelFlag, "0")
.orderByDesc(InvoiceMain::getCreateTime)
.last("LIMIT " + (limit != null ? limit : 5));
List<InvoiceMain> invoices = list(wrapper);
return invoices.stream().map(invoice -> {
InvoiceRes invoiceRes = new InvoiceRes();
BeanUtils.copyProperties(invoice, invoiceRes);
return invoiceRes;
}).collect(Collectors.toList());
} catch (Exception e) {
log.error("获取最近发票记录失败", e);
return new ArrayList<>();
}
}
}
......
......@@ -16,6 +16,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
......@@ -288,5 +289,59 @@ public class OrderMainServiceImpl extends ServiceImpl<OrderMainMapper, OrderMain
order.setUpdateTime(LocalDateTime.now());
return orderMainMapper.updateById(order) > 0;
}
@Override
public Integer getTodayOrderCount(String date) {
try {
LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderMain::getDelFlag, "0")
.like(OrderMain::getCreateTime, date);
long count = count(wrapper);
System.out.println("今日订单数量查询结果: " + count + ", 日期: " + date);
return Math.toIntExact(count);
} catch (Exception e) {
System.err.println("查询今日订单数量失败: " + e.getMessage());
return 0;
}
}
@Override
public Double getTodaySalesAmount(String date) {
try {
LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderMain::getDelFlag, "0")
.like(OrderMain::getCreateTime, date);
List<OrderMain> orders = list(wrapper);
double totalAmount = orders.stream()
.mapToDouble(order -> order.getTotalAmount() != null ? order.getTotalAmount().doubleValue() : 0.0)
.sum();
System.out.println("今日销售额查询结果: " + totalAmount + ", 日期: " + date);
return totalAmount;
} catch (Exception e) {
System.err.println("查询今日销售额失败: " + e.getMessage());
return 0.0;
}
}
@Override
public List<OrderRes> getRecentOrders(Integer limit) {
try {
LambdaQueryWrapper<OrderMain> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(OrderMain::getDelFlag, "0")
.orderByDesc(OrderMain::getCreateTime)
.last("LIMIT " + (limit != null ? limit : 5));
List<OrderMain> orders = list(wrapper);
return orders.stream().map(order -> {
OrderRes orderRes = new OrderRes();
BeanUtils.copyProperties(order, orderRes);
return orderRes;
}).collect(Collectors.toList());
} catch (Exception e) {
log.error("获取最近订单失败", e);
return new ArrayList<>();
}
}
}
......
......@@ -275,4 +275,24 @@ public class RebateServiceImpl extends ServiceImpl<RebateMapper, Rebate> impleme
log.info("获取返利趋势统计数据,开始日期:{},结束日期:{}", startDate, endDate);
return rebateMapper.getRebateTrendStats(startDate, endDate);
}
@Override
public List<RebateRes> getRecentRebates(Integer limit) {
try {
LambdaQueryWrapper<Rebate> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Rebate::getDelFlag, "0")
.orderByDesc(Rebate::getCreateTime)
.last("LIMIT " + (limit != null ? limit : 5));
List<Rebate> rebates = list(wrapper);
return rebates.stream().map(rebate -> {
RebateRes rebateRes = new RebateRes();
BeanUtils.copyProperties(rebate, rebateRes);
return rebateRes;
}).collect(Collectors.toList());
} catch (Exception e) {
log.error("获取最近返利记录失败", e);
return new ArrayList<>();
}
}
}
......
package com.apple.erp.service.impl;
import com.apple.erp.service.SessionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* 会话管理服务实现
*
* @author Apple ERP Team
* @version 1.0.0
* @since 2024-01-01
*/
@Slf4j
@Service
public class SessionServiceImpl implements SessionService {
private static final String SESSION_KEY_PREFIX = "user:session:";
private static final long SESSION_TIMEOUT = 1800; // 30分钟
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void storeUserSession(String username, Map<String, Object> sessionData) {
try {
String sessionKey = SESSION_KEY_PREFIX + username;
redisTemplate.opsForValue().set(sessionKey, sessionData, SESSION_TIMEOUT);
log.debug("用户会话已存储: {}", username);
} catch (Exception e) {
log.error("存储用户会话失败: {}", username, e);
}
}
@Override
public Map<String, Object> getUserSession(String username) {
try {
String sessionKey = SESSION_KEY_PREFIX + username;
Object sessionData = redisTemplate.opsForValue().get(sessionKey);
if (sessionData instanceof Map) {
return (Map<String, Object>) sessionData;
}
} catch (Exception e) {
log.error("获取用户会话失败: {}", username, e);
}
return new HashMap<>();
}
@Override
public void removeUserSession(String username) {
try {
String sessionKey = SESSION_KEY_PREFIX + username;
redisTemplate.delete(sessionKey);
log.debug("用户会话已删除: {}", username);
} catch (Exception e) {
log.error("删除用户会话失败: {}", username, e);
}
}
@Override
public Integer getOnlineUserCount() {
try {
String pattern = SESSION_KEY_PREFIX + "*";
Set<String> sessionKeys = redisTemplate.keys(pattern);
if (sessionKeys != null) {
int onlineCount = sessionKeys.size();
log.debug("当前在线用户数: {}", onlineCount);
return onlineCount;
} else {
log.warn("Redis中未找到用户会话数据");
return 0;
}
} catch (Exception e) {
log.error("获取在线用户数失败", e);
return 0;
}
}
@Override
@Scheduled(fixedRate = 300000) // 每5分钟执行一次
public void cleanupExpiredSessions() {
try {
String pattern = SESSION_KEY_PREFIX + "*";
Set<String> sessionKeys = redisTemplate.keys(pattern);
if (sessionKeys != null) {
int cleanedCount = 0;
for (String sessionKey : sessionKeys) {
// Redis会自动清理过期的key,这里可以添加额外的清理逻辑
// 比如检查会话的最后活动时间等
cleanedCount++;
}
log.debug("会话清理完成,检查了 {} 个会话", cleanedCount);
}
} catch (Exception e) {
log.error("清理过期会话失败", e);
}
}
}
......@@ -11,7 +11,6 @@ import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 系统菜单Service实现类
......@@ -77,7 +76,7 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
wrapper.eq(SysMenu::getDelFlag, "0");
// 排序:按父菜单ID和排序号升序
wrapper.orderByAsc(SysMenu::getParentId, SysMenu::getSort);
wrapper.orderByAsc(SysMenu::getParentId).orderByAsc(SysMenu::getSort);
return wrapper;
}
......
......@@ -198,33 +198,6 @@ public class DictValueConverter {
}
}
/**
* 加载指定字典类型到Redis
*/
private static void loadDictToRedis(String dictType) {
if (staticDictItemService == null) {
return;
}
try {
// 根据字典类型获取字典项
List<SysDictItem> dictItems = staticDictItemService.getDictItemsByType(dictType);
Map<String, String> mapping = new HashMap<>();
for (SysDictItem item : dictItems) {
if (item.getDelFlag() == null || "0".equals(item.getDelFlag())) {
mapping.put(item.getDictValue(), item.getDictLabel());
}
}
// 存储到Redis
String cacheKey = DICT_CACHE_PREFIX + dictType;
staticRedisTemplate.opsForValue().set(cacheKey, mapping, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
} catch (Exception e) {
System.err.println("加载字典到Redis失败: " + e.getMessage());
}
}
/**
* 根据字典类型ID获取字典类型编码
......
......@@ -150,4 +150,76 @@
AND del_flag = '0'
</update>
<!-- 获取异常工单列表用于导出 -->
<select id="selectExceptionWorkorderListForExport" resultType="com.apple.erp.dto.ExceptionWorkorderRes">
SELECT
ew.workorder_id,
ew.workorder_no,
ew.order_no,
ew.dealer_code,
ew.dealer_name,
ew.exception_type,
CASE ew.exception_type
WHEN 1 THEN '逻辑验证异常'
WHEN 2 THEN '源头验证异常'
WHEN 3 THEN '交叉验证异常'
ELSE '未知类型'
END AS exception_type_name,
ew.severity_level,
CASE ew.severity_level
WHEN 1 THEN '高'
WHEN 2 THEN '中'
WHEN 3 THEN '低'
ELSE '未知'
END AS severity_level_name,
ew.workorder_status,
CASE ew.workorder_status
WHEN 1 THEN '待处理'
WHEN 2 THEN '处理中'
WHEN 3 THEN '已解决'
WHEN 4 THEN '已关闭'
ELSE '未知状态'
END AS workorder_status_name,
ew.exception_desc,
ew.handler_user,
ew.create_by,
ew.create_time,
ew.update_time
FROM t_exception_workorder ew
<where>
ew.del_flag = '0'
<if test="query.workorderNo != null and query.workorderNo != ''">
AND ew.workorder_no LIKE CONCAT('%', #{query.workorderNo}, '%')
</if>
<if test="query.orderNo != null and query.orderNo != ''">
AND ew.order_no LIKE CONCAT('%', #{query.orderNo}, '%')
</if>
<if test="query.dealerCode != null and query.dealerCode != ''">
AND ew.dealer_code = #{query.dealerCode}
</if>
<if test="query.dealerName != null and query.dealerName != ''">
AND ew.dealer_name LIKE CONCAT('%', #{query.dealerName}, '%')
</if>
<if test="query.workorderStatus != null">
AND ew.workorder_status = #{query.workorderStatus}
</if>
<if test="query.exceptionType != null">
AND ew.exception_type = #{query.exceptionType}
</if>
<if test="query.severityLevel != null">
AND ew.severity_level = #{query.severityLevel}
</if>
<if test="query.handlerUser != null and query.handlerUser != ''">
AND ew.handler_user LIKE CONCAT('%', #{query.handlerUser}, '%')
</if>
<if test="query.startTime != null">
AND ew.create_time >= #{query.startTime}
</if>
<if test="query.endTime != null">
AND ew.create_time &lt;= #{query.endTime}
</if>
</where>
ORDER BY ew.create_time DESC
</select>
</mapper>
......
import { request } from '@/utils/request'
/**
* 首页统计数据响应接口
*/
export interface DashboardStats {
todayOrderCount: number
pendingDeliveryCount: number
pendingInvoiceCount: number
todaySalesAmount: number
onlineUserCount: number
systemStatus: string
systemVersion: string
orderGrowthRate: string
salesGrowthRate: string
}
export interface RecentActivity {
id: number
type: string
title: string
time: string
}
/**
* 首页API
*/
export const dashboardApi = {
/**
* 获取首页统计数据
*/
getDashboardStats: (): Promise<DashboardStats> => {
return request.get('/api/dashboard/stats')
},
/**
* 获取最近活动
*/
getRecentActivities: (): Promise<RecentActivity[]> => {
return request.get('/api/dashboard/activities')
},
/**
* 获取测试数据
*/
getTestStats: (): Promise<DashboardStats> => {
return request.get('/api/dashboard/test')
}
}
export default dashboardApi
......@@ -146,6 +146,13 @@ export const exceptionWorkorderApi = {
// 获取异常工单统计信息
getExceptionWorkorderStats: () => {
return request.get('/api/exception-workorder/stats')
},
// 导出异常工单数据
exportExceptionWorkorders: (params: ExceptionWorkorderQueryReq) => {
return request.post('/api/exception-workorder/export', params, {
responseType: 'blob'
})
}
}
......
......@@ -32,6 +32,8 @@ export interface RebateSearchParams {
productCode?: string
operateType?: number
calcFlag?: number
auditStatus?: number
dataSource?: string
rebateStartDate?: string
rebateEndDate?: string
}
......
<template>
<div class="dashboard-container">
<div class="dashboard-header">
<h2>系统概览</h2>
<p v-if="loading">正在加载用户信息...</p>
<p v-else>欢迎回来,{{ userInfo?.username || '未知' }}!</p>
</div>
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-icon">👤</div>
<div class="stat-content">
<h3>用户总数</h3>
<p class="stat-number">1,234</p>
</div>
<!-- 欢迎区域 -->
<div class="welcome-section">
<div class="welcome-content">
<h1 class="welcome-title">欢迎使用 Apple经销商ERP系统</h1>
<p class="welcome-subtitle" v-if="loading">正在加载用户信息...</p>
<p class="welcome-subtitle" v-else>您好,{{ userInfo?.username || userInfo?.realName || '用户' }}!今天是 {{ currentDate }}</p>
</div>
<div class="stat-card">
<div class="stat-icon">📈</div>
<div class="stat-content">
<h3>今日访问</h3>
<p class="stat-number">567</p>
<div class="user-info">
<div class="user-avatar">
<span>{{ getUserInitials() }}</span>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">✅</div>
<div class="stat-content">
<h3>系统状态</h3>
<p class="stat-number">正常</p>
<div class="user-details">
<div class="user-name">{{ userInfo?.username || userInfo?.realName || '未知用户' }}</div>
<div class="user-role">{{ getRoleName(userInfo) || '普通用户' }}</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">⏱️</div>
<div class="stat-content">
<h3>运行时间</h3>
<p class="stat-number">{{ currentTime }}</p>
</div>
<!-- 核心业务统计 -->
<div class="stats-section">
<h2 class="section-title">核心业务概览</h2>
<div class="stats-grid">
<div class="stat-card primary">
<div class="stat-icon">📦</div>
<div class="stat-content">
<div class="stat-label">今日订单</div>
<div class="stat-value">{{ statsLoading ? '...' : (todayStats.todayOrderCount || 0) }}</div>
<div class="stat-change positive">{{ todayStats.orderGrowthRate || '+12.5%' }}</div>
</div>
</div>
<div class="stat-card success">
<div class="stat-icon">🚚</div>
<div class="stat-content">
<div class="stat-label">待出库</div>
<div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingDeliveryCount || 0) }}</div>
<div class="stat-change">需要处理</div>
</div>
</div>
<div class="stat-card warning">
<div class="stat-icon">🧾</div>
<div class="stat-content">
<div class="stat-label">待开票</div>
<div class="stat-value">{{ statsLoading ? '...' : (todayStats.pendingInvoiceCount || 0) }}</div>
<div class="stat-change">待处理</div>
</div>
</div>
<div class="stat-card info">
<div class="stat-icon">💰</div>
<div class="stat-content">
<div class="stat-label">今日销售额</div>
<div class="stat-value">¥{{ statsLoading ? '...' : formatAmount(todayStats.todaySalesAmount || 0) }}</div>
<div class="stat-change positive">{{ todayStats.salesGrowthRate || '+8.2%' }}</div>
</div>
</div>
</div>
</div>
<div class="dashboard-content">
<div class="dashboard-card">
<h3>系统信息</h3>
<div class="info-grid">
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 快速操作 -->
<div class="quick-actions-card">
<h3 class="card-title">快速操作</h3>
<div class="actions-grid">
<button class="action-btn primary" @click="navigateTo('/main/order')">
<div class="action-icon">📋</div>
<div class="action-text">
<div class="action-title">订单管理</div>
<div class="action-desc">查看和管理订单</div>
</div>
</button>
<button class="action-btn success" @click="navigateTo('/main/delivery')">
<div class="action-icon">🚚</div>
<div class="action-text">
<div class="action-title">出库管理</div>
<div class="action-desc">处理出库业务</div>
</div>
</button>
<button class="action-btn warning" @click="navigateTo('/main/invoice')">
<div class="action-icon">🧾</div>
<div class="action-text">
<div class="action-title">发票管理</div>
<div class="action-desc">开具和管理发票</div>
</div>
</button>
<button class="action-btn info" @click="navigateTo('/main/rebate')">
<div class="action-icon">💰</div>
<div class="action-text">
<div class="action-title">返利管理</div>
<div class="action-desc">返利计算和发放</div>
</div>
</button>
<button class="action-btn secondary" @click="navigateTo('/main/product')">
<div class="action-icon">📱</div>
<div class="action-text">
<div class="action-title">产品管理</div>
<div class="action-desc">管理产品信息</div>
</div>
</button>
<button class="action-btn secondary" @click="navigateTo('/main/dealer')">
<div class="action-icon">🏢</div>
<div class="action-text">
<div class="action-title">经销商管理</div>
<div class="action-desc">管理经销商信息</div>
</div>
</button>
</div>
</div>
<!-- 系统信息 -->
<div class="system-info-card">
<h3 class="card-title">系统信息</h3>
<div class="info-list">
<div class="info-item">
<label>用户名:</label>
<span v-if="loading">加载中...</span>
<span v-else>{{ userInfo?.username || '未知' }}</span>
<div class="info-label">系统版本</div>
<div class="info-value">{{ todayStats.systemVersion }}</div>
</div>
<div class="info-item">
<label>角色:</label>
<span v-if="loading">加载中...</span>
<span v-else>{{ getRoleName(userInfo) || '普通用户' }}</span>
<div class="info-label">当前时间</div>
<div class="info-value">{{ currentTime }}</div>
</div>
<div class="info-item">
<label>登录时间:</label>
<span>{{ currentTime }}</span>
<div class="info-label">系统状态</div>
<div class="info-value" :class="{ 'status-normal': todayStats.systemStatus === '正常运行', 'status-error': todayStats.systemStatus !== '正常运行' }">
{{ todayStats.systemStatus }}
</div>
</div>
<div class="info-item">
<label>系统版本:</label>
<span>v1.0.0</span>
<div class="info-label">在线用户</div>
<div class="info-value">{{ todayStats.onlineUserCount }}</div>
</div>
</div>
</div>
<div class="dashboard-card">
<h3>快速操作</h3>
<div class="quick-actions">
<button class="action-btn" @click="navigateTo('/main/users')">👤 用户管理</button>
<button class="action-btn" @click="navigateTo('/main/sys/role')">🛡️ 角色管理</button>
<button class="action-btn" @click="navigateTo('/main/settings')">⚙️ 系统设置</button>
<button class="action-btn" @click="navigateTo('/main/sys/log')">📊 查看日志</button>
</div>
</div>
<!-- 最近活动 -->
<div class="recent-activities">
<h3 class="card-title">最近活动</h3>
<div class="activity-list" v-if="!activitiesLoading">
<div class="activity-item" v-for="activity in recentActivities" :key="activity.id">
<div class="activity-icon" :class="activity.type">
{{ getActivityIcon(activity.type) }}
</div>
<div class="activity-content">
<div class="activity-title">{{ activity.title }}</div>
<div class="activity-time">{{ activity.time }}</div>
</div>
</div>
</div>
<div v-if="recentActivities.length === 0" class="no-activities">
暂无最近活动
</div>
</div>
<div v-else class="loading-activities">
正在加载活动数据...
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/utils/request'
import dashboardApi, { type DashboardStats, type RecentActivity } from '@/api/dashboard'
const router = useRouter()
const currentTime = ref('')
const currentDate = ref('')
const userInfo = ref<any>(null)
const loading = ref(false)
const statsLoading = ref(false)
// 今日统计数据
const todayStats = ref<DashboardStats>({
todayOrderCount: 0,
pendingDeliveryCount: 0,
pendingInvoiceCount: 0,
todaySalesAmount: 0,
onlineUserCount: 0,
systemStatus: '正常运行',
systemVersion: 'v1.0.0',
orderGrowthRate: '+12.5%',
salesGrowthRate: '+8.2%'
})
// 最近活动数据
const recentActivities = ref<RecentActivity[]>([])
const activitiesLoading = ref(false)
// 获取用户姓名首字母
const getUserInitials = () => {
const name = userInfo.value?.username || userInfo.value?.realName || 'U'
return name.charAt(0).toUpperCase()
}
// 获取角色名称
const getRoleName = (userInfo: any) => {
......@@ -119,6 +233,71 @@ const getRoleName = (userInfo: any) => {
return '普通用户'
}
// 格式化金额
const formatAmount = (amount: number) => {
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2 })
}
// 获取活动图标
const getActivityIcon = (type: string) => {
const iconMap: { [key: string]: string } = {
'order': '📋',
'delivery': '🚚',
'invoice': '🧾',
'rebate': '💰',
'system': '⚙️'
}
return iconMap[type] || '📄'
}
// 获取今日统计数据
const fetchTodayStats = async () => {
try {
statsLoading.value = true
console.log('开始获取首页统计数据...')
const response = await dashboardApi.getDashboardStats()
todayStats.value = response
console.log('首页统计数据获取成功:', response)
} catch (error: any) {
console.error('获取统计数据失败:', error)
console.error('错误详情:', error?.message)
// 如果API调用失败,使用默认值
todayStats.value = {
todayOrderCount: 0,
pendingDeliveryCount: 0,
pendingInvoiceCount: 0,
todaySalesAmount: 0,
onlineUserCount: 0,
systemStatus: '系统异常',
systemVersion: 'v1.0.0',
orderGrowthRate: '0%',
salesGrowthRate: '0%'
}
} finally {
statsLoading.value = false
}
}
// 获取最近活动
const fetchRecentActivities = async () => {
try {
activitiesLoading.value = true
console.log('开始获取最近活动数据...')
const response = await dashboardApi.getRecentActivities()
recentActivities.value = response
console.log('最近活动数据获取成功:', response)
} catch (error: any) {
console.error('获取最近活动失败:', error)
console.error('错误详情:', error?.message)
// 如果API调用失败,使用默认值
recentActivities.value = []
} finally {
activitiesLoading.value = false
}
}
// 获取用户信息
const fetchUserInfo = async () => {
try {
......@@ -138,9 +317,29 @@ const fetchUserInfo = async () => {
}
}
// 更新时间
const updateTime = () => {
const now = new Date()
currentTime.value = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
currentDate.value = now.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})
}
onMounted(() => {
// 获取当前时间
currentTime.value = new Date().toLocaleString()
// 更新当前时间
updateTime()
setInterval(updateTime, 1000)
// 检查是否已登录
const token = localStorage.getItem('token')
......@@ -149,8 +348,10 @@ onMounted(() => {
return
}
// 获取用户信息
// 获取用户信息、统计数据和最近活动
fetchUserInfo()
fetchTodayStats()
fetchRecentActivities()
})
// 页面跳转函数
......@@ -170,151 +371,449 @@ const logout = () => {
}
</script>
<style scoped>
<style scoped lang="scss">
.dashboard-container {
padding: 20px;
padding: 24px;
min-height: 100vh;
background: #f5f7fa;
}
.dashboard-header {
margin-bottom: 30px;
// 欢迎区域
.welcome-section {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 32px;
border-radius: 12px;
margin-bottom: 32px;
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
.welcome-content {
.welcome-title {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px 0;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.welcome-subtitle {
font-size: 16px;
margin: 0;
opacity: 0.9;
}
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
.user-avatar {
width: 60px;
height: 60px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
backdrop-filter: blur(10px);
}
.user-details {
.user-name {
font-size: 18px;
font-weight: 600;
margin-bottom: 4px;
}
.user-role {
font-size: 14px;
opacity: 0.8;
}
}
}
}
.dashboard-header h2 {
font-size: 20px;
color: #333;
margin: 0 0 8px 0;
}
.dashboard-header p {
color: #666;
margin: 0;
// 统计区域
.stats-section {
margin-bottom: 32px;
.section-title {
font-size: 20px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.stat-card {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
gap: 20px;
transition: all 0.3s ease;
border-left: 4px solid;
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
}
&.primary {
border-left-color: #3498db;
}
&.success {
border-left-color: #2ecc71;
}
&.warning {
border-left-color: #f39c12;
}
&.info {
border-left-color: #9b59b6;
}
.stat-icon {
font-size: 32px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.stat-content {
flex: 1;
.stat-label {
font-size: 14px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #333;
margin-bottom: 4px;
}
.stat-change {
font-size: 12px;
font-weight: 500;
&.positive {
color: #2ecc71;
}
&:not(.positive) {
color: #666;
}
}
}
}
}
.dashboard-stats {
// 主要内容区域
.main-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
grid-template-columns: 2fr 1fr;
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
// 快速操作卡片
.quick-actions-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
align-items: center;
gap: 15px;
transition: transform 0.3s ease;
}
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.stat-card:hover {
transform: translateY(-2px);
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.stat-icon {
font-size: 20px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 6px;
}
.actions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.stat-content h3 {
margin: 0 0 5px 0;
font-size: 12px;
color: #666;
font-weight: 500;
}
.action-btn {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 16px;
text-align: left;
.stat-number {
margin: 0;
font-size: 18px;
font-weight: bold;
color: #333;
}
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.dashboard-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
&.primary {
border-color: #3498db;
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #2980b9 0%, #1f618d 100%);
}
}
&.success {
border-color: #2ecc71;
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #27ae60 0%, #229954 100%);
}
}
&.warning {
border-color: #f39c12;
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #e67e22 0%, #d35400 100%);
}
}
&.info {
border-color: #9b59b6;
background: linear-gradient(135deg, #9b59b6 0%, #8e44ad 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #8e44ad 0%, #7d3c98 100%);
}
}
&.secondary {
border-color: #95a5a6;
background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);
color: white;
&:hover {
background: linear-gradient(135deg, #7f8c8d 0%, #6c7b7d 100%);
}
}
.action-icon {
font-size: 24px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.2);
border-radius: 8px;
}
.action-text {
.action-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.action-desc {
font-size: 12px;
opacity: 0.8;
}
}
}
}
.dashboard-card {
// 系统信息卡片
.system-info-card {
background: white;
padding: 24px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.dashboard-card h3 {
margin: 0 0 16px 0;
color: #333;
font-size: 16px;
font-weight: 600;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.info-grid {
display: grid;
gap: 12px;
.info-list {
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.info-label {
font-weight: 500;
color: #666;
font-size: 14px;
}
.info-value {
color: #333;
font-weight: 500;
&.status-normal {
color: #2ecc71;
}
&.status-error {
color: #e74c3c;
}
}
}
}
}
.info-item {
// 最近活动
.recent-activities {
background: white;
padding: 24px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
.card-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0 0 20px 0;
}
.activity-list {
.activity-item {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.activity-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
justify-content: center;
font-size: 16px;
&.order {
background: #e3f2fd;
color: #1976d2;
}
.info-item label {
font-weight: 500;
color: #666;
}
.info-item span {
color: #333;
}
&.delivery {
background: #e8f5e8;
color: #2e7d32;
}
.quick-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
&.invoice {
background: #fff3e0;
color: #f57c00;
}
.action-btn {
background: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.3s;
&.rebate {
background: #f3e5f5;
color: #7b1fa2;
}
&.system {
background: #f1f8e9;
color: #558b2f;
}
}
.activity-content {
flex: 1;
.activity-title {
font-size: 14px;
color: #333;
margin-bottom: 4px;
font-weight: 500;
}
.activity-time {
font-size: 12px;
color: #666;
}
}
}
}
.no-activities, .loading-activities {
text-align: center;
color: #666;
padding: 20px;
font-size: 14px;
}
}
.action-btn:hover {
background: #2980b9;
// 响应式设计
@media (max-width: 1200px) {
.main-content {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.dashboard-stats {
grid-template-columns: 1fr;
.dashboard-container {
padding: 16px;
}
.dashboard-content {
grid-template-columns: 1fr;
.welcome-section {
flex-direction: column;
text-align: center;
gap: 20px;
.user-info {
justify-content: center;
}
.quick-actions {
}
.stats-grid {
grid-template-columns: 1fr;
}
.actions-grid {
grid-template-columns: 1fr;
}
}
......
......@@ -734,8 +734,38 @@ const handleBatchProcess = () => {
}
// 导出
const handleExport = () => {
ElMessage.info('导出功能开发中...')
const handleExport = async () => {
try {
await ElMessageBox.confirm('确定要导出异常工单数据吗?', '确认导出', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
ElMessage.info('正在导出数据,请稍候...')
const response = await exceptionWorkorderApi.exportExceptionWorkorders(searchParams.value)
// 创建下载链接
const blob = new Blob([response as unknown as BlobPart], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `异常工单数据_${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
} catch (error: any) {
if (error !== 'cancel') {
console.error('导出失败:', error)
ElMessage.error('导出失败: ' + (error?.message || '未知错误'))
}
}
}
// 提交新增
......
......@@ -2,51 +2,64 @@
<div class="rebate-page">
<!-- 搜索区域 -->
<div class="search-section">
<div class="search-form">
<div class="search-row">
<div class="search-item">
<span class="search-label">经销商名称</span>
<el-input
v-model="searchForm.dealerName"
<div class="search-row">
<div class="search-item">
<label>经销商名称:</label>
<input
v-model="searchForm.dealerName"
class="search-input"
placeholder="请输入经销商名称"
clearable
size="small"
style="width: 160px"
/>
</div>
<div class="search-item">
<span class="search-label">返利编号</span>
<el-input
v-model="searchForm.rebateNo"
placeholder="请输入返利编号"
clearable
size="small"
style="width: 160px"
/>
</div>
<div class="search-item">
<span class="search-label">日期</span>
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
@change="handleDateRangeChange"
style="width: 240px"
/>
</div>
<div class="search-actions">
<el-button type="primary" size="small" @click="handleSearch" :loading="loading">
查询
</el-button>
<el-button size="small" @click="handleReset">
重置
</el-button>
</div>
/>
</div>
<div class="search-item">
<label>返利编号:</label>
<input
v-model="searchForm.rebateNo"
class="search-input"
placeholder="请输入返利编号"
/>
</div>
<div class="search-item">
<label>操作类型:</label>
<select v-model="searchForm.operateType" class="search-select">
<option value="">所有</option>
<option value="1">新增</option>
<option value="2">修改</option>
<option value="3">删除</option>
</select>
</div>
<div class="search-item">
<label>计算状态:</label>
<select v-model="searchForm.calcFlag" class="search-select">
<option value="">所有</option>
<option value="0">未计算</option>
<option value="1">已计算</option>
</select>
</div>
</div>
<div class="search-row">
<div class="search-item">
<label>审核状态:</label>
<select v-model="searchForm.auditStatus" class="search-select">
<option value="">所有</option>
<option value="0">待审核</option>
<option value="1">审核通过</option>
<option value="2">审核中</option>
<option value="3">审核拒绝</option>
</select>
</div>
<div class="search-item">
<label>数据来源:</label>
<select v-model="searchForm.dataSource" class="search-select">
<option value="">所有</option>
<option value="ERP系统">ERP系统</option>
<option value="手工录入">手工录入</option>
<option value="API导入">API导入</option>
</select>
</div>
<div class="search-actions">
<button @click="handleSearch" class="search-btn" :disabled="loading">🔍 搜索</button>
<button @click="handleReset" class="reset-btn">🔄 重置</button>
</div>
</div>
</div>
......@@ -96,95 +109,103 @@
</div>
</div>
<!-- 操作按钮区域 -->
<div class="action-section">
<div class="action-buttons">
<button @click="handleExport" class="action-btn secondary" :disabled="exportLoading">
📋 导出
</button>
</div>
</div>
<!-- 数据表格 -->
<div class="table-section">
<div class="table-header">
<div class="table-title">
返利记录
</div>
<div class="table-actions">
<el-button type="primary" size="small" @click="handleExport" :loading="exportLoading">
<el-icon><Download /></el-icon>
导出
</el-button>
<div class="table-info">
数据状态: {{ loading ? '加载中...' : `共${rebateList.length}条记录` }}
</div>
<div class="table-controls">
<button @click="handleTableSearch" class="control-btn">🔍</button>
<button @click="handleTableRefresh" class="control-btn">🔄</button>
<button @click="handleTableExport" class="control-btn">📋</button>
<button @click="handleTableViewToggle" class="control-btn">⊞</button>
</div>
</div>
<el-table
v-loading="loading"
:data="rebateList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 'normal' }"
:empty-text="rebateList.length === 0 ? '暂无数据' : ''"
size="small"
>
<el-table-column prop="rebateNo" label="返利编号" width="140" align="center" />
<el-table-column prop="orderNo" label="订单编号" width="140" align="center" />
<el-table-column prop="dealerCode" label="经销商代码" width="120" align="center" />
<el-table-column prop="dealerName" label="经销商名称" min-width="150" />
<el-table-column prop="productCode" label="产品编码" width="120" align="center" />
<el-table-column prop="rebateAmount" label="返利金额" width="120" align="right">
<template #default="{ row }">
<span class="amount-text">{{ formatAmount(row.rebateAmount) }}</span>
</template>
</el-table-column>
<el-table-column prop="rebateDate" label="返利日期" width="120" align="center" />
<el-table-column prop="operateTypeText" label="操作类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getOperateTypeTagType(row.operateType)" size="small">
{{ row.operateTypeText || getOperateTypeText(row.operateType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="calcFlagText" label="计算状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getCalcFlagTagType(row.calcFlag)" size="small">
{{ row.calcFlagText || getCalcFlagText(row.calcFlag) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" width="150" align="center">
<template #default="{ row }">
{{ formatDate(row.updateTime) }}
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ row }">
<el-button type="primary" size="small" link @click="handleViewDetail(row)">
详情
</el-button>
</template>
</el-table-column>
</el-table>
<div class="table-container">
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">加载中...</div>
</div>
<table class="data-table">
<thead>
<tr>
<th>返利编号</th>
<th>订单编号</th>
<th>经销商代码</th>
<th>经销商名称</th>
<th>产品编码</th>
<th>返利金额</th>
<th>返利日期</th>
<th>操作类型</th>
<th>计算状态</th>
<th>更新时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="rebateList.length === 0">
<td colspan="11" class="empty-data">暂无数据</td>
</tr>
<tr v-for="row in rebateList" :key="row.rebateId">
<td>{{ row.rebateNo }}</td>
<td>{{ row.orderNo }}</td>
<td>{{ row.dealerCode }}</td>
<td>{{ row.dealerName }}</td>
<td>{{ row.productCode }}</td>
<td class="amount-text">{{ formatAmount(row.rebateAmount) }}</td>
<td>{{ row.rebateDate }}</td>
<td>
<span :class="getOperateTypeClass(row.operateType || 0)">
{{ row.operateTypeText || getOperateTypeText(row.operateType || 0) }}
</span>
</td>
<td>
<span :class="getCalcFlagClass(row.calcFlag || 0)">
{{ row.calcFlagText || getCalcFlagText(row.calcFlag || 0) }}
</span>
</td>
<td>{{ formatDate(row.updateTime || '') }}</td>
<td>
<button @click="handleViewDetail(row)" class="action-link">详情</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<div class="pagination-info">
共 {{ pagination.total }} 条,{{ pagination.pageSize }}/页
</div>
<el-pagination
v-model:current-page="pagination.pageNum"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
small
/>
<div class="pagination-controls">
<button @click="handlePrevPage" :disabled="pagination.pageNum <= 1" class="page-btn">‹</button>
<button
v-for="page in getPageNumbers()"
:key="page"
@click="handlePageChange(page)"
:class="['page-btn', { active: page === pagination.pageNum }]"
>
{{ page }}
</button>
<button @click="handleNextPage" :disabled="pagination.pageNum >= getTotalPages()" class="page-btn">›</button>
</div>
<div class="pagination-jump">
前往
<el-input
<input
v-model="jumpPage"
size="small"
style="width: 50px; margin: 0 8px;"
class="jump-input"
@keyup.enter="handleJumpPage"
/>
</div>
</div>
</div>
</div>
......@@ -435,6 +456,8 @@ const searchForm = reactive<RebateSearchParams>({
productCode: '',
operateType: undefined,
calcFlag: undefined,
auditStatus: undefined,
dataSource: '',
rebateStartDate: '',
rebateEndDate: ''
})
......@@ -730,15 +753,17 @@ const handleReset = () => {
Object.assign(searchForm, {
pageNum: 1,
pageSize: 10,
rebateNo: undefined,
dealerName: undefined,
productName: undefined,
rebateType: undefined,
status: undefined,
applyStartTime: undefined,
applyEndTime: undefined
rebateNo: '',
dealerCode: '',
dealerName: '',
productCode: '',
operateType: undefined,
calcFlag: undefined,
auditStatus: undefined,
dataSource: '',
rebateStartDate: '',
rebateEndDate: ''
})
applyDateRange.value = null
pagination.pageNum = 1
getRebateList()
}
......@@ -748,6 +773,104 @@ const handleRefresh = () => {
getRebateList()
}
// 表格控制按钮
const handleTableSearch = () => {
handleSearch()
}
const handleTableRefresh = () => {
handleRefresh()
}
const handleTableExport = () => {
handleExport()
}
const handleTableViewToggle = () => {
// 切换视图模式
console.log('切换视图模式')
}
// 分页控制
const handlePrevPage = () => {
if (pagination.pageNum > 1) {
pagination.pageNum--
getRebateList()
}
}
const handleNextPage = () => {
if (pagination.pageNum < getTotalPages()) {
pagination.pageNum++
getRebateList()
}
}
const handlePageChange = (page: number | string) => {
if (typeof page === 'number') {
pagination.pageNum = page
getRebateList()
}
}
const getPageNumbers = () => {
const totalPages = getTotalPages()
const current = pagination.pageNum
const pages = []
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) {
pages.push(i)
}
} else {
if (current <= 4) {
for (let i = 1; i <= 5; i++) {
pages.push(i)
}
pages.push('...')
pages.push(totalPages)
} else if (current >= totalPages - 3) {
pages.push(1)
pages.push('...')
for (let i = totalPages - 4; i <= totalPages; i++) {
pages.push(i)
}
} else {
pages.push(1)
pages.push('...')
for (let i = current - 1; i <= current + 1; i++) {
pages.push(i)
}
pages.push('...')
pages.push(totalPages)
}
}
return pages
}
const getTotalPages = () => {
return Math.ceil(pagination.total / pagination.pageSize)
}
// 状态样式类
const getOperateTypeClass = (type: number) => {
const classes = {
1: 'status-tag success',
2: 'status-tag warning',
3: 'status-tag danger'
}
return classes[type as keyof typeof classes] || 'status-tag'
}
const getCalcFlagClass = (flag: number) => {
const classes = {
0: 'status-tag warning',
1: 'status-tag success'
}
return classes[flag as keyof typeof classes] || 'status-tag'
}
// 分页变化
const handleSizeChange = (size: number) => {
pagination.pageSize = size
......@@ -784,11 +907,11 @@ const handleViewDetail = async (row: Rebate) => {
const response = await rebateApi.getRebateById(row.rebateId)
console.log('详情接口响应:', response)
// 根据request.ts拦截器的处理,response.data已经是实际数据
if (response && response.data) {
rebateDetail.value = response.data
detailDialogVisible.value = true
console.log('详情数据:', response.data)
// 根据request.ts拦截器的处理,response直接就是数据对象
if (response) {
rebateDetail.value = response as unknown as Rebate
detailDialogVisible.value = true
console.log('详情数据:', response)
} else {
ElMessage.error('获取返利详情失败')
}
......@@ -1478,30 +1601,154 @@ onMounted(async () => {
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
.search-form {
.search-row {
.search-row {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
.search-item {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
gap: 8px;
.search-item {
display: flex;
align-items: center;
gap: 8px;
label {
font-size: 14px;
color: #333;
white-space: nowrap;
min-width: 80px;
}
.search-label {
font-size: 14px;
color: #333;
white-space: nowrap;
min-width: 70px;
.search-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 160px;
transition: border-color 0.3s;
&:focus {
outline: none;
border-color: #409eff;
}
}
.search-actions {
margin-left: auto;
display: flex;
gap: 8px;
.search-select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
width: 120px;
background: white;
cursor: pointer;
transition: border-color 0.3s;
&:focus {
outline: none;
border-color: #409eff;
}
}
}
.search-actions {
margin-left: auto;
display: flex;
gap: 8px;
.search-btn, .reset-btn {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
background: white;
&:hover {
background: #f5f7fa;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
.search-btn {
background: #409eff;
color: white;
border-color: #409eff;
&:hover {
background: #66b1ff;
}
}
}
}
}
.action-section {
background: white;
padding: 12px 20px;
margin-bottom: 16px;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
.action-buttons {
display: flex;
gap: 12px;
.action-btn {
padding: 8px 16px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
background: white;
&:hover {
background: #f5f7fa;
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
&.primary {
background: #409eff;
color: white;
border-color: #409eff;
&:hover {
background: #66b1ff;
}
}
&.secondary {
background: #67c23a;
color: white;
border-color: #67c23a;
&:hover {
background: #85ce61;
}
}
&.danger {
background: #f56c6c;
color: white;
border-color: #f56c6c;
&:hover {
background: #f78989;
}
}
}
}
......@@ -1580,61 +1827,113 @@ onMounted(async () => {
.table-section {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
.table-header {
display: flex;
justify-content: space-between;
justify-content: flex-end;
align-items: center;
padding: 20px 20px 0 20px;
margin-bottom: 16px;
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
.table-title {
font-size: 16px;
font-weight: 500;
color: #333;
.table-controls {
display: flex;
gap: 8px;
.control-btn {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
&:hover {
background: #f5f7fa;
border-color: #409eff;
}
}
}
}
.table-actions {
.table-container {
position: relative;
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
gap: 12px;
justify-content: center;
z-index: 10;
.table-info {
font-size: 12px;
color: #999;
.loading-spinner {
font-size: 14px;
color: #666;
}
}
}
.el-table {
border: none;
.data-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
:deep(.el-table__header) {
th {
background-color: #fafafa;
border: none;
font-weight: 500;
color: #333;
}
}
thead {
background: #fafafa;
:deep(.el-table__body) {
tr:hover > td {
background-color: #f5f7fa;
th {
padding: 12px 16px;
text-align: left;
font-weight: 500;
color: #333;
border-bottom: 1px solid #e0e0e0;
white-space: nowrap;
}
}
td {
border: none;
border-bottom: 1px solid #f0f0f0;
tbody {
tr {
transition: background-color 0.3s;
&:hover {
background: #f5f7fa;
}
&:nth-child(even) {
background: #fafafa;
}
&:nth-child(even):hover {
background: #f0f2f5;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
&.empty-data {
text-align: center;
color: #999;
font-style: italic;
padding: 40px;
}
}
}
}
}
}
.pagination-wrapper {
display: flex;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
......@@ -1646,15 +1945,100 @@ onMounted(async () => {
color: #666;
}
.pagination-controls {
display: flex;
gap: 4px;
align-items: center;
.page-btn {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
min-width: 32px;
&:hover:not(:disabled) {
background: #f5f7fa;
border-color: #409eff;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&.active {
background: #409eff;
color: white;
border-color: #409eff;
}
}
}
.pagination-jump {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #666;
.jump-input {
width: 50px;
padding: 4px 8px;
border: 1px solid #ddd;
border-radius: 4px;
text-align: center;
font-size: 14px;
&:focus {
outline: none;
border-color: #409eff;
}
}
}
}
}
.status-tag {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
&.success {
background: #f0f9ff;
color: #67c23a;
}
&.warning {
background: #fdf6ec;
color: #e6a23c;
}
&.danger {
background: #fef0f0;
color: #f56c6c;
}
}
.action-link {
color: #409eff;
text-decoration: none;
cursor: pointer;
font-size: 14px;
background: none;
border: none;
padding: 0;
&:hover {
color: #66b1ff;
text-decoration: underline;
}
}
.amount-text {
color: #f56c6c;
font-weight: 500;
......