jiaxing.zhou

Merge remote-tracking branch 'origin/dev'

Showing 32 changed files with 2394 additions and 341 deletions
......@@ -103,7 +103,7 @@
**接口路径:** `GET /api/auth/userinfo`
**功能描述:** 获取当前登录用户的详细信息
**功能描述:** 获取当前登录用户的详细信息,包括用户名、真实姓名、角色列表和权限列表
**请求头:** `Authorization: Bearer {token}`
......@@ -114,11 +114,46 @@
"message": "获取用户信息成功",
"data": {
"username": "admin",
"authorities": ["ROLE_ADMIN"]
"realName": "系统管理员",
"roles": [
{
"roleId": 1,
"roleCode": "ADMIN",
"roleName": "系统管理员",
"status": 1,
"statusText": "正常",
"remark": "系统管理员角色",
"createBy": "admin",
"createTime": "2024-01-01T00:00:00",
"updateBy": "admin",
"updateTime": "2024-01-01T00:00:00"
}
],
"authorities": [
{
"authority": "ROLE_ADMIN"
}
]
}
}
```
**响应字段说明:**
- `username`: 用户名
- `realName`: 真实姓名
- `roles`: 用户角色列表
- `roleId`: 角色ID
- `roleCode`: 角色编码
- `roleName`: 角色名称
- `status`: 角色状态(0-停用/1-启用)
- `statusText`: 角色状态文本描述
- `remark`: 备注
- `createBy`: 创建者
- `createTime`: 创建时间
- `updateBy`: 更新者
- `updateTime`: 更新时间
- `authorities`: 用户权限列表(Spring Security权限对象)
## 2. 用户管理 (SysUserController)
### 2.1 获取用户列表
......@@ -2296,6 +2331,50 @@
}
```
### 8. 导出发票数据
**接口路径:** `POST /api/invoice/export`
**请求方法:** POST
**权限要求:** `invoice:export`
**请求参数:** 同发票查询接口参数
**响应:** 返回Excel文件流
---
## 八、数据导出接口
### 1. 订单数据导出
**接口路径:** `POST /order/export`
**请求方法:** POST
**权限要求:** `order:export`
**请求参数:** 同订单查询接口参数
**响应:** 返回Excel文件流,文件名格式:`订单数据_yyyyMMdd_HHmmss.xlsx`
### 2. 出库数据导出
**接口路径:** `POST /api/delivery/export`
**请求方法:** POST
**权限要求:** `delivery:export`
**请求参数:** 同出库查询接口参数
**响应:** 返回Excel文件流,文件名格式:`出库数据_yyyyMMdd_HHmmss.xlsx`
### 3. 发票数据导出
**接口路径:** `POST /api/invoice/export`
**请求方法:** POST
**权限要求:** `invoice:export`
**请求参数:** 同发票查询接口参数
**响应:** 返回Excel文件流,文件名格式:`发票数据_yyyyMMdd_HHmmss.xlsx`
---
**文档版本:** 1.0.0
......
package com.apple.erp.config;
import java.util.HashMap;
import java.util.Map;
/**
* 字典值配置类
* 集中管理所有字典值转换配置
*
* @author Apple ERP System
* @since 2025-01-01
*/
public class DictConfig {
/**
* 操作类型字典
*/
public static final Map<Integer, String> OPERATE_TYPE = new HashMap<Integer, String>() {{
put(1, "新增");
put(2, "修改");
put(3, "删除");
}};
/**
* 计算状态字典
*/
public static final Map<Integer, String> CALC_FLAG = new HashMap<Integer, String>() {{
put(0, "未计算");
put(1, "已计算");
}};
/**
* 审核状态字典
*/
public static final Map<Integer, String> AUDIT_STATUS = new HashMap<Integer, String>() {{
put(0, "待审核");
put(1, "审核通过");
put(2, "审核中");
put(3, "审核拒绝");
}};
/**
* 出库状态字典
*/
public static final Map<Integer, String> DELIVERY_STATUS = new HashMap<Integer, String>() {{
put(0, "未出库");
put(1, "已出库");
put(2, "部分出库");
}};
/**
* 发票状态字典
*/
public static final Map<Integer, String> INVOICE_STATUS = new HashMap<Integer, String>() {{
put(0, "未开票");
put(1, "已开票");
put(2, "部分开票");
}};
/**
* 返利计算状态字典
*/
public static final Map<Integer, String> REBATE_CALC_FLAG = new HashMap<Integer, String>() {{
put(0, "未计算");
put(1, "已计算");
}};
/**
* 审核状态字典
*/
public static final Map<Integer, String> VERIFY_STATUS = new HashMap<Integer, String>() {{
put(0, "待审核");
put(1, "审核通过");
put(2, "审核中");
put(3, "审核拒绝");
}};
/**
* 工单状态字典
*/
public static final Map<Integer, String> WORKORDER_STATUS = new HashMap<Integer, String>() {{
put(0, "待处理");
put(1, "处理中");
put(2, "已完成");
put(3, "已关闭");
}};
/**
* 严重程度字典
*/
public static final Map<Integer, String> SEVERITY_LEVEL = new HashMap<Integer, String>() {{
put(1, "低");
put(2, "中");
put(3, "高");
put(4, "紧急");
}};
}
package com.apple.erp.config;
import java.util.HashMap;
import java.util.Map;
/**
* 导出配置类
* 定义各模块的导出字段映射配置
*
* @author Apple ERP System
* @since 2025-01-01
*/
public class ExportConfig {
/**
* 订单导出配置
*/
public static final Map<String, String[]> ORDER_EXPORT_CONFIG = new HashMap<String, String[]>() {{
put("headers", new String[]{
"订单ID", "订单编号", "经销商编码", "经销商名称", "订单日期",
"订单金额", "返利金额", "出库状态", "开票状态", "返利计算状态",
"数据来源", "审核状态", "上传时间", "创建时间"
});
put("fields", new String[]{
"orderId", "orderNo", "dealerCode", "dealerName", "orderDate",
"totalAmount", "rebateAmount", "deliveryStatus", "invoiceStatus", "rebateCalcFlag",
"dataSource", "verifyStatus", "uploadTime", "createTime"
});
}};
/**
* 出库导出配置
*/
public static final Map<String, String[]> DELIVERY_EXPORT_CONFIG = new HashMap<String, String[]>() {{
put("headers", new String[]{
"出库单ID", "出库单编号", "经销商编码", "经销商名称", "出库日期",
"关联订单编号", "出库状态", "仓库编码", "数据来源", "创建时间"
});
put("fields", new String[]{
"deliveryId", "deliveryNo", "dealerCode", "dealerName", "deliveryDate",
"orderNo", "deliveryStatus", "warehouseCode", "dataSource", "createTime"
});
}};
/**
* 发票导出配置
*/
public static final Map<String, String[]> INVOICE_EXPORT_CONFIG = new HashMap<String, String[]>() {{
put("headers", new String[]{
"发票ID", "发票编号", "订单编号", "出库单编号", "经销商编码",
"经销商名称", "发票金额", "发票日期", "开票状态", "税率",
"数据来源", "创建时间"
});
put("fields", new String[]{
"invoiceId", "invoiceNo", "orderNo", "deliveryNo", "dealerCode",
"dealerName", "totalAmount", "invoiceDate", "invoiceStatus", "taxRate",
"dataSource", "createTime"
});
}};
/**
* 返利导出配置
*/
public static final Map<String, String[]> REBATE_EXPORT_CONFIG = new HashMap<String, String[]>() {{
put("headers", new String[]{
"返利ID", "返利编号", "订单编号", "经销商编码", "经销商名称",
"返利金额", "返利类型", "计算状态", "审核状态", "数据来源",
"创建时间", "更新时间"
});
put("fields", new String[]{
"rebateId", "rebateNo", "orderNo", "dealerCode", "dealerName",
"rebateAmount", "operateType", "calcFlag", "auditStatus", "dataSource",
"createTime", "updateTime"
});
}};
/**
* 异常工单导出配置
*/
public static final Map<String, String[]> EXCEPTION_WORKORDER_EXPORT_CONFIG = new HashMap<String, String[]>() {{
put("headers", new String[]{
"工单ID", "工单编号", "工单类型", "严重程度", "工单状态",
"问题描述", "处理人", "创建人", "创建时间", "更新时间"
});
put("fields", new String[]{
"workorderId", "workorderNo", "workorderType", "severityLevel", "workorderStatus",
"problemDescription", "assignee", "creator", "createTime", "updateTime"
});
}};
}
......@@ -4,6 +4,7 @@ import com.apple.erp.dto.request.LoginReq;
import com.apple.erp.dto.request.RefreshTokenReq;
import com.apple.erp.dto.response.ApiRes;
import com.apple.erp.dto.response.LoginRes;
import com.apple.erp.dto.response.RoleRes;
import com.apple.erp.dto.response.UserInfoRes;
import com.apple.erp.entity.SysUser;
import com.apple.erp.service.SysUserService;
......@@ -23,6 +24,7 @@ import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.List;
/**
* 认证控制器
......@@ -214,9 +216,34 @@ public class AuthController {
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
UserInfoRes userInfo = new UserInfoRes(username, authentication.getAuthorities());
ApiRes<UserInfoRes> response = ApiRes.success("获取用户信息成功", userInfo);
return ResponseEntity.ok(response);
try {
// 获取用户详细信息
SysUser user = sysUserService.findByUsername(username);
if (user != null) {
// 获取用户角色信息
List<RoleRes> roles = sysUserService.getUserRoles(user.getUserId());
UserInfoRes userInfo = new UserInfoRes(
user.getUsername(),
user.getRealName(),
roles,
authentication.getAuthorities()
);
ApiRes<UserInfoRes> response = ApiRes.success("获取用户信息成功", userInfo);
return ResponseEntity.ok(response);
} else {
// 如果找不到用户信息,返回基本信息
UserInfoRes userInfo = new UserInfoRes(username, authentication.getAuthorities());
ApiRes<UserInfoRes> response = ApiRes.success("获取用户信息成功", userInfo);
return ResponseEntity.ok(response);
}
} catch (Exception e) {
log.error("获取用户详细信息失败: " + e.getMessage(), e);
// 如果获取详细信息失败,返回基本信息
UserInfoRes userInfo = new UserInfoRes(username, authentication.getAuthorities());
ApiRes<UserInfoRes> response = ApiRes.success("获取用户信息成功", userInfo);
return ResponseEntity.ok(response);
}
} else {
ApiRes<UserInfoRes> response = ApiRes.error("未认证");
return ResponseEntity.status(401).body(response);
......
......@@ -6,16 +6,20 @@ import com.apple.erp.dto.DeliveryRes;
import com.apple.erp.dto.DeliveryUpdateReq;
import com.apple.erp.service.DeliveryMainService;
import com.apple.erp.dto.response.ApiRes;
import com.apple.erp.service.ExcelExportService;
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.*;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
......@@ -32,6 +36,9 @@ public class DeliveryMainController {
@Autowired
private DeliveryMainService deliveryMainService;
@Autowired
private ExcelExportService excelExportService;
@Operation(summary = "分页查询出库列表", description = "根据查询条件分页获取出库列表")
@GetMapping("/list")
......@@ -139,6 +146,40 @@ public class DeliveryMainController {
return ApiRes.error("修改出库状态失败: " + e.getMessage());
}
}
@Operation(summary = "导出出库数据", description = "根据查询条件导出出库数据到Excel")
@PostMapping("/export")
@PreAuthorize("hasAuthority('delivery:export')")
public ResponseEntity<byte[]> exportDeliveries(@Valid @RequestBody DeliveryQueryReq queryReq) {
try {
// 获取所有符合条件的数据(不分页)
DeliveryQueryReq exportQuery = new DeliveryQueryReq();
exportQuery.setDeliveryNo(queryReq.getDeliveryNo());
exportQuery.setDealerCode(queryReq.getDealerCode());
exportQuery.setDealerName(queryReq.getDealerName());
exportQuery.setDeliveryStatus(queryReq.getDeliveryStatus());
exportQuery.setWarehouseCode(queryReq.getWarehouseCode());
exportQuery.setDataSource(queryReq.getDataSource());
exportQuery.setDeliveryStartDate(queryReq.getDeliveryStartDate());
exportQuery.setDeliveryEndDate(queryReq.getDeliveryEndDate());
// 设置大分页获取所有数据
exportQuery.setPageNum(1);
exportQuery.setPageSize(10000);
Page<DeliveryRes> result = deliveryMainService.getDeliveryList(exportQuery);
List<DeliveryRes> deliveries = result.getRecords();
// 生成文件名
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String fileName = "出库数据_" + timestamp;
// 导出到Excel
return excelExportService.exportDeliveries(deliveries);
} catch (Exception e) {
throw new RuntimeException("导出出库数据失败: " + e.getMessage(), e);
}
}
}
......
......@@ -6,16 +6,20 @@ import com.apple.erp.dto.InvoiceRes;
import com.apple.erp.dto.InvoiceUpdateReq;
import com.apple.erp.service.InvoiceMainService;
import com.apple.erp.dto.response.ApiRes;
import com.apple.erp.service.ExcelExportService;
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.*;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
......@@ -32,6 +36,9 @@ public class InvoiceMainController {
@Autowired
private InvoiceMainService invoiceMainService;
@Autowired
private ExcelExportService excelExportService;
@Operation(summary = "分页查询发票列表", description = "根据查询条件分页获取发票列表")
@GetMapping("/list")
......@@ -139,6 +146,43 @@ public class InvoiceMainController {
return ApiRes.error("修改发票状态失败: " + e.getMessage());
}
}
@Operation(summary = "导出发票数据", description = "根据查询条件导出发票数据到Excel")
@PostMapping("/export")
@PreAuthorize("hasAuthority('invoice:export')")
public ResponseEntity<byte[]> exportInvoices(@Valid @RequestBody InvoiceQueryReq queryReq) {
try {
// 获取所有符合条件的数据(不分页)
InvoiceQueryReq exportQuery = new InvoiceQueryReq();
exportQuery.setInvoiceNo(queryReq.getInvoiceNo());
exportQuery.setOrderNo(queryReq.getOrderNo());
exportQuery.setDeliveryNo(queryReq.getDeliveryNo());
exportQuery.setDealerCode(queryReq.getDealerCode());
exportQuery.setDealerName(queryReq.getDealerName());
exportQuery.setInvoiceStatus(queryReq.getInvoiceStatus());
exportQuery.setDataSource(queryReq.getDataSource());
exportQuery.setInvoiceStartDate(queryReq.getInvoiceStartDate());
exportQuery.setInvoiceEndDate(queryReq.getInvoiceEndDate());
exportQuery.setMinAmount(queryReq.getMinAmount());
exportQuery.setMaxAmount(queryReq.getMaxAmount());
// 设置大分页获取所有数据
exportQuery.setPageNum(1);
exportQuery.setPageSize(10000);
Page<InvoiceRes> result = invoiceMainService.getInvoiceList(exportQuery);
List<InvoiceRes> invoices = result.getRecords();
// 生成文件名
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String fileName = "发票数据_" + timestamp;
// 导出到Excel
return excelExportService.exportInvoices(invoices);
} catch (Exception e) {
throw new RuntimeException("导出发票数据失败: " + e.getMessage(), e);
}
}
}
......
......@@ -6,16 +6,20 @@ import com.apple.erp.dto.OrderRes;
import com.apple.erp.dto.OrderUpdateReq;
import com.apple.erp.service.OrderMainService;
import com.apple.erp.dto.response.ApiRes;
import com.apple.erp.service.ExcelExportService;
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.*;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
......@@ -32,6 +36,9 @@ public class OrderMainController {
@Autowired
private OrderMainService orderMainService;
@Autowired
private ExcelExportService excelExportService;
@Operation(summary = "分页查询订单列表", description = "根据条件分页查询订单列表")
@GetMapping("/list")
......@@ -182,4 +189,42 @@ public class OrderMainController {
return ApiRes.error("修改返利计算状态失败: " + e.getMessage());
}
}
@Operation(summary = "导出订单数据", description = "根据查询条件导出订单数据到Excel")
@PostMapping("/export")
@PreAuthorize("hasAuthority('order:export')")
public ResponseEntity<byte[]> exportOrders(@Valid @RequestBody OrderQueryReq queryReq) {
try {
// 获取所有符合条件的数据(不分页)
OrderQueryReq exportQuery = new OrderQueryReq();
exportQuery.setOrderNo(queryReq.getOrderNo());
exportQuery.setDealerCode(queryReq.getDealerCode());
exportQuery.setDealerName(queryReq.getDealerName());
exportQuery.setDeliveryStatus(queryReq.getDeliveryStatus());
exportQuery.setInvoiceStatus(queryReq.getInvoiceStatus());
exportQuery.setRebateCalcFlag(queryReq.getRebateCalcFlag());
exportQuery.setDataSource(queryReq.getDataSource());
exportQuery.setVerifyStatus(queryReq.getVerifyStatus());
exportQuery.setOrderStartDate(queryReq.getOrderStartDate());
exportQuery.setOrderEndDate(queryReq.getOrderEndDate());
exportQuery.setMinAmount(queryReq.getMinAmount());
exportQuery.setMaxAmount(queryReq.getMaxAmount());
// 设置大分页获取所有数据
exportQuery.setPageNum(1);
exportQuery.setPageSize(10000);
Page<OrderRes> result = orderMainService.getOrderList(exportQuery);
List<OrderRes> orders = result.getRecords();
// 生成文件名
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String fileName = "订单数据_" + timestamp;
// 导出到Excel
return excelExportService.exportOrders(orders);
} catch (Exception e) {
throw new RuntimeException("导出订单数据失败: " + e.getMessage(), e);
}
}
}
......
......@@ -7,6 +7,7 @@ import com.apple.erp.dto.response.ApiRes;
import com.apple.erp.dto.response.RebateRes;
import com.apple.erp.entity.Rebate;
import com.apple.erp.service.RebateService;
import com.apple.erp.service.ExcelExportService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
......@@ -17,8 +18,12 @@ import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
/**
* 返利台账明细管理控制器
......@@ -36,6 +41,9 @@ public class RebateController {
@Autowired
private RebateService rebateService;
@Autowired
private ExcelExportService excelExportService;
@Operation(summary = "分页查询返利明细列表")
@PostMapping("/page")
......@@ -277,4 +285,40 @@ public class RebateController {
return ApiRes.error(e.getMessage());
}
}
@Operation(summary = "导出返利数据", description = "根据查询条件导出返利数据到Excel")
@PostMapping("/export")
@PreAuthorize("hasAuthority('rebate:export')")
public ResponseEntity<byte[]> exportRebates(@Valid @RequestBody RebateQueryReq queryReq) {
log.info("导出返利数据,参数:{}", queryReq);
try {
// 获取所有符合条件的数据(不分页)
RebateQueryReq exportQuery = new RebateQueryReq();
exportQuery.setRebateNo(queryReq.getRebateNo());
exportQuery.setOrderNo(queryReq.getOrderNo());
exportQuery.setDealerCode(queryReq.getDealerCode());
exportQuery.setDealerName(queryReq.getDealerName());
exportQuery.setOperateType(queryReq.getOperateType());
exportQuery.setCalcFlag(queryReq.getCalcFlag());
exportQuery.setAuditStatus(queryReq.getAuditStatus());
exportQuery.setDataSource(queryReq.getDataSource());
exportQuery.setStartDate(queryReq.getStartDate());
exportQuery.setEndDate(queryReq.getEndDate());
exportQuery.setMinAmount(queryReq.getMinAmount());
exportQuery.setMaxAmount(queryReq.getMaxAmount());
// 设置大分页获取所有数据
exportQuery.setPageNum(1);
exportQuery.setPageSize(10000);
IPage<RebateRes> result = rebateService.getRebatePage(exportQuery);
List<RebateRes> rebates = result.getRecords();
// 导出到Excel
return excelExportService.exportRebates(rebates);
} catch (Exception e) {
log.error("导出返利数据失败", e);
throw new RuntimeException("导出返利数据失败: " + e.getMessage(), e);
}
}
}
......
......@@ -193,6 +193,25 @@ public class SysDictItemController {
}
/**
* 刷新字典缓存
* 当字典项发生变化时,刷新Redis缓存以保持数据一致性
*
* @return 操作结果
*/
@Operation(summary = "刷新字典缓存", description = "刷新Redis字典缓存以保持数据一致性")
@PostMapping("/refreshCache")
@PreAuthorize("hasAuthority('sys:dict:edit')")
public ApiRes<Void> refreshCache() {
try {
// 调用字典值转换器的刷新方法
com.apple.erp.util.DictValueConverter.refreshCache();
return ApiRes.success("字典缓存刷新成功", null);
} catch (Exception e) {
return ApiRes.error("刷新字典缓存失败: " + e.getMessage());
}
}
/**
* 转换SysDictItem为DictItemRes
*
* @param dictItem 字典项实体
......
......@@ -58,6 +58,36 @@ public class RebateQueryReq {
private String rebateEndDate;
/**
* 审核状态
*/
private Integer auditStatus;
/**
* 数据来源
*/
private String dataSource;
/**
* 开始日期
*/
private String startDate;
/**
* 结束日期
*/
private String endDate;
/**
* 最小金额
*/
private java.math.BigDecimal minAmount;
/**
* 最大金额
*/
private java.math.BigDecimal maxAmount;
/**
* 页码
*/
private Integer pageNum = 1;
......
......@@ -5,6 +5,7 @@ import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
import java.util.List;
/**
* 用户信息响应对象
......@@ -20,6 +21,12 @@ public class UserInfoRes {
@Schema(description = "用户名", example = "admin")
private String username;
@Schema(description = "真实姓名", example = "管理员")
private String realName;
@Schema(description = "用户角色列表")
private List<RoleRes> roles;
@Schema(description = "用户权限列表")
private Collection<? extends GrantedAuthority> authorities;
......@@ -29,4 +36,11 @@ public class UserInfoRes {
this.username = username;
this.authorities = authorities;
}
public UserInfoRes(String username, String realName, List<RoleRes> roles, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.realName = realName;
this.roles = roles;
this.authorities = authorities;
}
}
......
package com.apple.erp.service;
import com.apple.erp.config.ExportConfig;
import com.apple.erp.util.GenericExcelExportUtil;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
/**
* Excel导出服务类
* 提供统一的导出接口,各模块可独立使用
*
* @author Apple ERP System
* @since 2025-01-01
*/
@Service
public class ExcelExportService {
private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
/**
* 导出订单数据
*/
public org.springframework.http.ResponseEntity<byte[]> exportOrders(List<?> data) {
Map<String, String[]> config = ExportConfig.ORDER_EXPORT_CONFIG;
String fileName = "订单数据_" + LocalDateTime.now().format(TIMESTAMP_FORMATTER);
return GenericExcelExportUtil.exportToExcel(data, config.get("headers"), config.get("fields"), fileName);
}
/**
* 导出出库数据
*/
public org.springframework.http.ResponseEntity<byte[]> exportDeliveries(List<?> data) {
Map<String, String[]> config = ExportConfig.DELIVERY_EXPORT_CONFIG;
String fileName = "出库数据_" + LocalDateTime.now().format(TIMESTAMP_FORMATTER);
return GenericExcelExportUtil.exportToExcel(data, config.get("headers"), config.get("fields"), fileName);
}
/**
* 导出发票数据
*/
public org.springframework.http.ResponseEntity<byte[]> exportInvoices(List<?> data) {
Map<String, String[]> config = ExportConfig.INVOICE_EXPORT_CONFIG;
String fileName = "发票数据_" + LocalDateTime.now().format(TIMESTAMP_FORMATTER);
return GenericExcelExportUtil.exportToExcel(data, config.get("headers"), config.get("fields"), fileName);
}
/**
* 导出返利数据
*/
public org.springframework.http.ResponseEntity<byte[]> exportRebates(List<?> data) {
Map<String, String[]> config = ExportConfig.REBATE_EXPORT_CONFIG;
String fileName = "返利数据_" + LocalDateTime.now().format(TIMESTAMP_FORMATTER);
return GenericExcelExportUtil.exportToExcel(data, config.get("headers"), config.get("fields"), fileName);
}
/**
* 导出异常工单数据
*/
public org.springframework.http.ResponseEntity<byte[]> exportExceptionWorkorders(List<?> data) {
Map<String, String[]> config = ExportConfig.EXCEPTION_WORKORDER_EXPORT_CONFIG;
String fileName = "异常工单数据_" + LocalDateTime.now().format(TIMESTAMP_FORMATTER);
return GenericExcelExportUtil.exportToExcel(data, config.get("headers"), config.get("fields"), fileName);
}
/**
* 通用导出方法
* 支持自定义表头和字段映射
*/
public org.springframework.http.ResponseEntity<byte[]> exportCustom(List<?> data, String[] headers, String[] fieldNames, String fileName) {
return GenericExcelExportUtil.exportToExcel(data, headers, fieldNames, fileName);
}
}
package com.apple.erp.util;
import com.apple.erp.entity.SysDictItem;
import com.apple.erp.entity.SysDictType;
import com.apple.erp.service.SysDictItemService;
import com.apple.erp.service.SysDictTypeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* 字典值转换器 - 基于Redis缓存的动态字典值转换
* 支持实时更新,无需重启应用,使用Redis分布式缓存
*
* @author Apple ERP System
* @since 2025-01-01
*/
@Component
public class DictValueConverter {
@Autowired
private SysDictItemService sysDictItemService;
@Autowired
private SysDictTypeService sysDictTypeService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static SysDictItemService staticDictItemService;
private static SysDictTypeService staticDictTypeService;
private static RedisTemplate<String, Object> staticRedisTemplate;
/**
* Redis缓存键前缀
*/
private static final String DICT_CACHE_PREFIX = "dict:cache:";
/**
* 缓存过期时间(小时)
*/
private static final long CACHE_EXPIRE_HOURS = 24;
@PostConstruct
public void init() {
staticDictItemService = sysDictItemService;
staticDictTypeService = sysDictTypeService;
staticRedisTemplate = redisTemplate;
System.out.println("DictValueConverter初始化开始...");
refreshCache();
System.out.println("DictValueConverter初始化完成");
}
/**
* 转换字典值 - 简化版本,直接使用硬编码映射
*/
public static String convert(Object value, String fieldName) {
if (value == null) {
return "";
}
// 将值转换为字符串进行匹配
String valueStr = value.toString();
// 直接使用硬编码映射进行转换
Map<String, String> mapping = getHardcodedMapping(fieldName);
if (mapping != null && !mapping.isEmpty()) {
String result = mapping.getOrDefault(valueStr, valueStr);
System.out.println("字典转换: " + fieldName + " = " + valueStr + " -> " + result);
return result;
}
return valueStr;
}
/**
* 获取硬编码的字典映射
*/
private static Map<String, String> getHardcodedMapping(String fieldName) {
Map<String, String> mapping = new HashMap<>();
switch (fieldName) {
case "deliveryStatus":
mapping.put("0", "未出库");
mapping.put("1", "已出库");
break;
case "invoiceStatus":
mapping.put("0", "未开票");
mapping.put("1", "已开票");
break;
case "rebateCalcFlag":
mapping.put("0", "未计算");
mapping.put("1", "已计算");
break;
case "verifyStatus":
mapping.put("0", "待验证");
mapping.put("1", "验证通过");
mapping.put("2", "验证失败");
break;
case "workorderStatus":
mapping.put("1", "待处理");
mapping.put("2", "处理中");
mapping.put("3", "已解决");
mapping.put("4", "已关闭");
break;
case "severityLevel":
mapping.put("1", "高");
mapping.put("2", "中");
mapping.put("3", "低");
break;
case "operateType":
mapping.put("1", "新增");
mapping.put("2", "修改");
mapping.put("3", "删除");
break;
case "calcFlag":
mapping.put("0", "未计算");
mapping.put("1", "已计算");
break;
case "auditStatus":
mapping.put("0", "待审核");
mapping.put("1", "审核通过");
mapping.put("2", "审核中");
mapping.put("3", "审核拒绝");
break;
}
return mapping;
}
/**
* 刷新字典缓存 - 清空Redis缓存并重新加载
*/
public static void refreshCache() {
if (staticRedisTemplate == null) {
System.out.println("Redis模板未初始化,跳过缓存刷新");
return;
}
try {
System.out.println("开始刷新字典缓存...");
// 清空所有字典缓存
String pattern = DICT_CACHE_PREFIX + "*";
staticRedisTemplate.delete(staticRedisTemplate.keys(pattern));
System.out.println("已清空现有字典缓存");
// 重新加载所有字典类型
loadAllDictsToRedis();
System.out.println("字典缓存刷新完成");
} catch (Exception e) {
System.err.println("刷新字典缓存失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 加载所有字典到Redis
*/
private static void loadAllDictsToRedis() {
if (staticDictItemService == null) {
return;
}
try {
// 从数据库加载所有字典项
List<SysDictItem> allDictItems = staticDictItemService.list();
// 按字典类型分组
Map<String, Map<String, String>> dictGroups = new HashMap<>();
for (SysDictItem item : allDictItems) {
if (item.getDelFlag() != null && !"0".equals(item.getDelFlag())) {
continue; // 跳过已删除的字典项
}
String dictType = getDictTypeByTypeId(item.getDictTypeId());
if (dictType != null) {
dictGroups.computeIfAbsent(dictType, k -> new HashMap<>())
.put(item.getDictValue(), item.getDictLabel());
}
}
// 将每个字典类型存储到Redis
for (Map.Entry<String, Map<String, String>> entry : dictGroups.entrySet()) {
String cacheKey = DICT_CACHE_PREFIX + entry.getKey();
staticRedisTemplate.opsForValue().set(cacheKey, entry.getValue(), CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
}
} catch (Exception e) {
// 如果数据库加载失败,使用默认配置作为降级方案
loadDefaultDictConfigToRedis();
}
}
/**
* 加载指定字典类型到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获取字典类型编码
* 从数据库查询字典类型表获取真实的字典类型编码
*/
private static String getDictTypeByTypeId(Long dictTypeId) {
if (staticDictTypeService == null || dictTypeId == null) {
return null;
}
try {
SysDictType dictType = staticDictTypeService.getById(dictTypeId);
if (dictType != null && dictType.getStatus() != null && dictType.getStatus() == 1) {
return dictType.getDictType();
}
} catch (Exception e) {
System.err.println("查询字典类型失败: " + e.getMessage());
}
return null;
}
/**
* 加载默认字典配置到Redis(降级方案)
*/
private static void loadDefaultDictConfigToRedis() {
if (staticRedisTemplate == null) {
return;
}
try {
// 操作类型
Map<String, String> operateType = new HashMap<>();
operateType.put("1", "新增");
operateType.put("2", "修改");
operateType.put("3", "删除");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "operateType", operateType, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 计算状态
Map<String, String> calcFlag = new HashMap<>();
calcFlag.put("0", "未计算");
calcFlag.put("1", "已计算");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "calcFlag", calcFlag, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 审核状态
Map<String, String> auditStatus = new HashMap<>();
auditStatus.put("0", "待审核");
auditStatus.put("1", "审核通过");
auditStatus.put("2", "审核中");
auditStatus.put("3", "审核拒绝");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "auditStatus", auditStatus, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 出库状态
Map<String, String> deliveryStatus = new HashMap<>();
deliveryStatus.put("0", "未出库");
deliveryStatus.put("1", "已出库");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "deliveryStatus", deliveryStatus, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 发票状态
Map<String, String> invoiceStatus = new HashMap<>();
invoiceStatus.put("0", "未开票");
invoiceStatus.put("1", "已开票");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "invoiceStatus", invoiceStatus, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 返利计算状态
Map<String, String> rebateCalcFlag = new HashMap<>();
rebateCalcFlag.put("0", "未计算");
rebateCalcFlag.put("1", "已计算");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "rebateCalcFlag", rebateCalcFlag, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 审核状态
Map<String, String> verifyStatus = new HashMap<>();
verifyStatus.put("0", "待验证");
verifyStatus.put("1", "验证通过");
verifyStatus.put("2", "验证失败");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "verifyStatus", verifyStatus, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 工单状态
Map<String, String> workorderStatus = new HashMap<>();
workorderStatus.put("1", "待处理");
workorderStatus.put("2", "处理中");
workorderStatus.put("3", "已解决");
workorderStatus.put("4", "已关闭");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "workorderStatus", workorderStatus, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
// 严重程度
Map<String, String> severityLevel = new HashMap<>();
severityLevel.put("1", "高");
severityLevel.put("2", "中");
severityLevel.put("3", "低");
staticRedisTemplate.opsForValue().set(DICT_CACHE_PREFIX + "severityLevel", severityLevel, CACHE_EXPIRE_HOURS, TimeUnit.HOURS);
} catch (Exception e) {
System.err.println("加载默认字典配置到Redis失败: " + e.getMessage());
}
}
}
package com.apple.erp.util;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* Excel导出工具类
*
* @author Apple ERP Team
* @version 1.0.0
* @since 2024-01-01
*/
public class ExcelExportUtil {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 导出数据到Excel
*
* @param data 数据列表
* @param headers 表头数组
* @param fileName 文件名
* @param <T> 数据类型
* @return ResponseEntity<byte[]>
*/
public static <T> ResponseEntity<byte[]> exportToExcel(List<T> data, String[] headers, String fileName) {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("数据导出");
// 创建表头样式
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle dataStyle = createDataStyle(workbook);
// 创建表头
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
// 填充数据
if (data != null && !data.isEmpty()) {
for (int i = 0; i < data.size(); i++) {
Row row = sheet.createRow(i + 1);
T item = data.get(i);
fillRowData(row, item, dataStyle);
}
}
// 自动调整列宽
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}
// 转换为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
byte[] bytes = outputStream.toByteArray();
// 设置响应头
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 对文件名进行URL编码以支持中文
String encodedFileName;
try {
encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", "UTF-8");
} catch (Exception e) {
encodedFileName = fileName + ".xlsx";
}
httpHeaders.set("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
httpHeaders.setContentLength(bytes.length);
return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
} catch (IOException e) {
throw new RuntimeException("Excel导出失败", e);
}
}
/**
* 创建表头样式
*/
private static CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
font.setFontHeightInPoints((short) 12);
style.setFont(font);
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
}
/**
* 创建数据样式
*/
private static CellStyle createDataStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.LEFT);
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
}
/**
* 填充行数据
*/
private static <T> void fillRowData(Row row, T item, CellStyle dataStyle) {
if (item == null) return;
Field[] fields = item.getClass().getDeclaredFields();
int cellIndex = 0;
for (Field field : fields) {
if (cellIndex >= row.getLastCellNum()) break;
try {
// 使用getter方法获取值,而不是直接访问字段
String fieldName = field.getName();
String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
Object value = null;
try {
java.lang.reflect.Method getter = item.getClass().getMethod(getterName);
value = getter.invoke(item);
} catch (Exception e) {
// 如果getter方法不存在,尝试直接访问字段
field.setAccessible(true);
value = field.get(item);
}
Cell cell = row.createCell(cellIndex);
cell.setCellStyle(dataStyle);
if (value != null) {
if (value instanceof String) {
cell.setCellValue((String) value);
} else if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else if (value instanceof LocalDateTime) {
cell.setCellValue(((LocalDateTime) value).format(DATE_TIME_FORMATTER));
} else {
cell.setCellValue(value.toString());
}
} else {
cell.setCellValue("");
}
cellIndex++;
} catch (Exception e) {
// 忽略无法访问的字段
System.out.println("无法访问字段: " + field.getName() + ", 错误: " + e.getMessage());
}
}
}
/**
* 导出订单数据到Excel
*
* @param data 订单数据列表
* @param fileName 文件名
* @return ResponseEntity<byte[]>
*/
public static ResponseEntity<byte[]> exportOrderToExcel(List<?> data, String fileName) {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("订单数据");
// 创建表头样式
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle dataStyle = createDataStyle(workbook);
// 创建表头
Row headerRow = sheet.createRow(0);
String[] headers = {
"订单ID", "订单编号", "经销商编码", "经销商名称", "订单日期",
"订单金额", "返利金额", "出库状态", "开票状态", "返利计算状态",
"数据来源", "审核状态", "上传时间", "创建时间"
};
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
// 填充数据
if (data != null && !data.isEmpty()) {
for (int i = 0; i < data.size(); i++) {
Row row = sheet.createRow(i + 1);
Object item = data.get(i);
// 使用反射获取字段值
fillOrderRowData(row, item, dataStyle);
}
}
// 自动调整列宽
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}
// 转换为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
byte[] bytes = outputStream.toByteArray();
// 设置响应头
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 对文件名进行URL编码以支持中文
String encodedFileName;
try {
encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", "UTF-8");
} catch (Exception e) {
encodedFileName = fileName + ".xlsx";
}
httpHeaders.set("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
httpHeaders.setContentLength(bytes.length);
return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
} catch (IOException e) {
throw new RuntimeException("Excel导出失败", e);
}
}
/**
* 填充订单行数据
*/
private static void fillOrderRowData(Row row, Object item, CellStyle dataStyle) {
try {
// 使用反射获取OrderRes的字段值
Class<?> clazz = item.getClass();
// 订单ID
setCellValue(row, 0, getFieldValue(clazz, item, "orderId"), dataStyle);
// 订单编号
setCellValue(row, 1, getFieldValue(clazz, item, "orderNo"), dataStyle);
// 经销商编码
setCellValue(row, 2, getFieldValue(clazz, item, "dealerCode"), dataStyle);
// 经销商名称
setCellValue(row, 3, getFieldValue(clazz, item, "dealerName"), dataStyle);
// 订单日期
setCellValue(row, 4, getFieldValue(clazz, item, "orderDate"), dataStyle);
// 订单金额
setCellValue(row, 5, getFieldValue(clazz, item, "totalAmount"), dataStyle);
// 返利金额
setCellValue(row, 6, getFieldValue(clazz, item, "rebateAmount"), dataStyle);
// 出库状态
setCellValue(row, 7, getFieldValue(clazz, item, "deliveryStatus"), dataStyle);
// 开票状态
setCellValue(row, 8, getFieldValue(clazz, item, "invoiceStatus"), dataStyle);
// 返利计算状态
setCellValue(row, 9, getFieldValue(clazz, item, "rebateCalcFlag"), dataStyle);
// 数据来源
setCellValue(row, 10, getFieldValue(clazz, item, "dataSource"), dataStyle);
// 审核状态
setCellValue(row, 11, getFieldValue(clazz, item, "verifyStatus"), dataStyle);
// 上传时间
setCellValue(row, 12, getFieldValue(clazz, item, "uploadTime"), dataStyle);
// 创建时间
setCellValue(row, 13, getFieldValue(clazz, item, "createTime"), dataStyle);
} catch (Exception e) {
System.out.println("填充订单数据失败: " + e.getMessage());
}
}
/**
* 获取字段值
*/
private static Object getFieldValue(Class<?> clazz, Object item, String fieldName) {
try {
String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
java.lang.reflect.Method getter = clazz.getMethod(getterName);
return getter.invoke(item);
} catch (Exception e) {
return null;
}
}
/**
* 设置单元格值
*/
private static void setCellValue(Row row, int cellIndex, Object value, CellStyle dataStyle) {
Cell cell = row.createCell(cellIndex);
cell.setCellStyle(dataStyle);
if (value != null) {
if (value instanceof String) {
cell.setCellValue((String) value);
} else if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else if (value instanceof LocalDateTime) {
cell.setCellValue(((LocalDateTime) value).format(DATE_TIME_FORMATTER));
} else {
cell.setCellValue(value.toString());
}
} else {
cell.setCellValue("");
}
}
/**
* 导出出库数据到Excel
*
* @param data 出库数据列表
* @param fileName 文件名
* @return ResponseEntity<byte[]>
*/
public static ResponseEntity<byte[]> exportDeliveryToExcel(List<?> data, String fileName) {
String[] headers = {
"出库单ID", "出库单编号", "经销商编码", "经销商名称", "出库日期",
"关联订单编号", "出库状态", "仓库编码", "数据来源", "创建时间"
};
return exportToExcel(data, headers, fileName);
}
/**
* 导出发票数据到Excel
*
* @param data 发票数据列表
* @param fileName 文件名
* @return ResponseEntity<byte[]>
*/
public static ResponseEntity<byte[]> exportInvoiceToExcel(List<?> data, String fileName) {
String[] headers = {
"发票ID", "发票编号", "订单编号", "出库单编号", "经销商编码",
"经销商名称", "发票金额", "发票日期", "开票状态", "税率",
"数据来源", "创建时间"
};
return exportToExcel(data, headers, fileName);
}
}
package com.apple.erp.util;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
/**
* 通用Excel导出工具类
* 支持任意实体类的Excel导出,通过注解配置字段映射
*
* @author Apple ERP System
* @since 2025-01-01
*/
public class GenericExcelExportUtil {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
/**
* 通用Excel导出方法
*
* @param data 数据列表
* @param headers 表头数组
* @param fieldNames 字段名数组(与表头对应)
* @param fileName 文件名(不含扩展名)
* @return ResponseEntity<byte[]>
*/
public static ResponseEntity<byte[]> exportToExcel(List<?> data, String[] headers, String[] fieldNames, String fileName) {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("数据导出");
// 创建样式
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle dataStyle = createDataStyle(workbook);
// 创建表头
Row headerRow = sheet.createRow(0);
for (int i = 0; i < headers.length; i++) {
Cell cell = headerRow.createCell(i);
cell.setCellValue(headers[i]);
cell.setCellStyle(headerStyle);
}
// 填充数据
if (data != null && !data.isEmpty()) {
for (int i = 0; i < data.size(); i++) {
Row row = sheet.createRow(i + 1);
Object item = data.get(i);
fillRowData(row, item, fieldNames, dataStyle);
}
}
// 自动调整列宽
for (int i = 0; i < headers.length; i++) {
sheet.autoSizeColumn(i);
}
// 转换为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
workbook.write(outputStream);
byte[] bytes = outputStream.toByteArray();
// 设置响应头
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);
// 对文件名进行URL编码以支持中文
String encodedFileName;
try {
encodedFileName = java.net.URLEncoder.encode(fileName + ".xlsx", "UTF-8");
} catch (Exception e) {
encodedFileName = fileName + ".xlsx";
}
httpHeaders.set("Content-Disposition", "attachment; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
httpHeaders.setContentLength(bytes.length);
return new ResponseEntity<>(bytes, httpHeaders, HttpStatus.OK);
} catch (IOException e) {
throw new RuntimeException("Excel导出失败", e);
}
}
/**
* 创建表头样式
*/
private static CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
Font font = workbook.createFont();
font.setBold(true);
font.setFontHeightInPoints((short) 12);
style.setFont(font);
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.CENTER);
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
}
/**
* 创建数据样式
*/
private static CellStyle createDataStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setBorderBottom(BorderStyle.THIN);
style.setBorderTop(BorderStyle.THIN);
style.setBorderRight(BorderStyle.THIN);
style.setBorderLeft(BorderStyle.THIN);
style.setVerticalAlignment(VerticalAlignment.CENTER);
return style;
}
/**
* 填充行数据
*/
private static void fillRowData(Row row, Object item, String[] fieldNames, CellStyle dataStyle) {
try {
Class<?> clazz = item.getClass();
for (int i = 0; i < fieldNames.length; i++) {
Cell cell = row.createCell(i);
cell.setCellStyle(dataStyle);
Object value = getFieldValue(clazz, item, fieldNames[i]);
// 对特定字段进行字典值转换
String convertedValue = convertDictValue(value, fieldNames[i]);
System.out.println("字段转换: " + fieldNames[i] + " = " + value + " -> " + convertedValue);
if (convertedValue != null && !convertedValue.equals(value != null ? value.toString() : "")) {
// 如果字典转换成功,使用转换后的值
cell.setCellValue(convertedValue);
} else {
// 如果字典转换失败或没有转换,使用原始值
setCellValue(cell, value);
}
}
} catch (Exception e) {
System.out.println("填充行数据失败: " + e.getMessage());
}
}
/**
* 获取字段值(支持getter方法和直接字段访问)
*/
private static Object getFieldValue(Class<?> clazz, Object item, String fieldName) {
try {
// 首先尝试getter方法
String getterName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
try {
return clazz.getMethod(getterName).invoke(item);
} catch (NoSuchMethodException e) {
// 如果getter方法不存在,尝试直接访问字段
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(item);
}
} catch (Exception e) {
return null;
}
}
/**
* 设置单元格值
*/
private static void setCellValue(Cell cell, Object value) {
if (value == null) {
cell.setCellValue("");
} else if (value instanceof String) {
cell.setCellValue((String) value);
} else if (value instanceof Number) {
cell.setCellValue(((Number) value).doubleValue());
} else if (value instanceof LocalDateTime) {
cell.setCellValue(((LocalDateTime) value).format(DATE_TIME_FORMATTER));
} else if (value instanceof java.time.LocalDate) {
cell.setCellValue(((java.time.LocalDate) value).format(DATE_FORMATTER));
} else if (value instanceof Boolean) {
cell.setCellValue((Boolean) value ? "是" : "否");
} else {
cell.setCellValue(value.toString());
}
}
/**
* 转换字典值 - 使用动态字典转换器
*/
private static String convertDictValue(Object value, String fieldName) {
String result = DictValueConverter.convert(value, fieldName);
// 调试日志:输出字典转换过程
if (value != null && !value.toString().equals(result)) {
System.out.println("字典转换: " + fieldName + " = " + value + " -> " + result);
}
return result;
}
}
......@@ -5,5 +5,5 @@
// Generated by unplugin-auto-import
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
}
......
......@@ -26,6 +26,7 @@ declare module 'vue' {
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElText: typeof import('element-plus/es')['ElText']
Header: typeof import('./src/components/layout/Header.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
......
......@@ -120,8 +120,11 @@ export const deliveryApi = {
// 修改出库状态
updateDeliveryStatus: (deliveryId: number, deliveryStatus: number) => {
return request.post(`/api/delivery/${deliveryId}/deliveryStatus`, null, {
params: { deliveryStatus }
})
return request.post(`/api/delivery/${deliveryId}/deliveryStatus`, { deliveryStatus })
},
// 导出出库数据
exportDeliveries: (params: DeliveryQueryReq) => {
return request.post('/api/delivery/export', params, { responseType: 'blob' })
}
}
......
import axios from 'axios'
import request from '@/utils/request'
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8083'
/**
* 字典管理API
*/
const api = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
api.interceptors.response.use(
(response) => {
return response.data
},
(error) => {
console.error('API请求错误:', error)
return Promise.reject(error)
}
)
export interface DictType {
dictTypeId: number
dictType: string
dictName: string
status: number
statusText: string
remark?: string
createBy?: string
createTime: string
updateBy?: string
updateTime: string
}
export interface DictItem {
dictItemId: number
dictTypeId: number
dictType: string
dictLabel: string
dictValue: string
sort: number
remark?: string
createBy?: string
createTime: string
updateBy?: string
updateTime: string
}
export interface DictTypeSearchParams {
dictType?: string
dictName?: string
status?: number
pageNum?: number
pageSize?: number
}
export interface DictItemSearchParams {
dictTypeId?: number
dictLabel?: string
dictValue?: string
pageNum?: number
pageSize?: number
}
export interface DictTypeAddReq {
dictType: string
dictName: string
status: number
remark?: string
}
export interface DictTypeUpdateReq extends DictTypeAddReq {
dictTypeId: number
}
export interface DictItemAddReq {
dictTypeId: number
dictLabel: string
dictValue: string
sort?: number
remark?: string
// 获取字典项
export const getDictItems = (dictType: string) => {
return request.get(`/api/dict/${dictType}`)
}
export interface DictItemUpdateReq extends DictItemAddReq {
dictItemId: number
// 获取所有字典项
export const getAllDictItems = () => {
return request.get('/api/dict/all')
}
export interface ApiResponse<T = any> {
code: number
message: string
data: T
}
export interface PageResponse<T> {
records: T[]
total: number
size: number
current: number
orders: any[]
optimizeCountSql: boolean
searchCount: boolean
maxLimit: any
countId: any
pages: number
}
export const dictApi = {
// 字典类型管理
// 获取字典类型列表
getDictTypes: (params: DictTypeSearchParams): Promise<ApiResponse<PageResponse<DictType>>> => {
return api.get('/api/system/dict/type/list', { params })
},
// 获取字典类型详情
getDictTypeById: (dictTypeId: number): Promise<ApiResponse<DictType>> => {
return api.get(`/api/system/dict/type/${dictTypeId}`)
},
// 新增字典类型
createDictType: (dictTypeData: DictTypeAddReq): Promise<ApiResponse<any>> => {
return api.post('/api/system/dict/type', dictTypeData)
},
// 修改字典类型
updateDictType: (dictTypeData: DictTypeUpdateReq): Promise<ApiResponse<any>> => {
return api.put('/api/system/dict/type', dictTypeData)
},
// 删除字典类型
deleteDictTypes: (dictTypeIds: number[]): Promise<ApiResponse<any>> => {
return api.delete(`/api/system/dict/type/${dictTypeIds.join(',')}`)
},
// 获取字典类型选择框列表
getDictTypeOptions: (): Promise<ApiResponse<DictType[]>> => {
return api.get('/api/system/dict/type/optionselect')
},
// 刷新字典缓存
refreshDictCache: (): Promise<ApiResponse<any>> => {
return api.delete('/api/system/dict/type/refreshCache')
},
// 字典项管理
// 获取字典项列表
getDictItems: (params: DictItemSearchParams): Promise<ApiResponse<PageResponse<DictItem>>> => {
return api.get('/api/system/dict/item/list', { params })
},
// 根据字典类型获取字典项列表
getDictItemsByType: (dictType: string): Promise<ApiResponse<DictItem[]>> => {
return api.get(`/api/system/dict/item/type/${dictType}`)
},
// 获取字典项详情
getDictItemById: (dictItemId: number): Promise<ApiResponse<DictItem>> => {
return api.get(`/api/system/dict/item/${dictItemId}`)
},
// 新增字典项
createDictItem: (dictItemData: DictItemAddReq): Promise<ApiResponse<any>> => {
return api.post('/api/system/dict/item', dictItemData)
},
// 修改字典项
updateDictItem: (dictItemData: DictItemUpdateReq): Promise<ApiResponse<any>> => {
return api.put('/api/system/dict/item', dictItemData)
},
// 删除字典项
deleteDictItems: (dictItemIds: number[]): Promise<ApiResponse<any>> => {
return api.delete(`/api/system/dict/item/${dictItemIds.join(',')}`)
},
// 刷新字典缓存
export const refreshDictCache = () => {
return request.post('/api/dict/refresh')
}
\ No newline at end of file
......
......@@ -121,8 +121,11 @@ export const invoiceApi = {
return request.post('/api/invoice/batchDelete', invoiceIds)
},
updateInvoiceStatus: (invoiceId: number, invoiceStatus: number) => {
return request.post(`/api/invoice/${invoiceId}/invoiceStatus`, null, {
params: { invoiceStatus }
})
return request.post(`/api/invoice/${invoiceId}/invoiceStatus`, { invoiceStatus })
},
// 导出发票数据
exportInvoices: (params: InvoiceQueryReq) => {
return request.post('/api/invoice/export', params, { responseType: 'blob' })
}
}
......
......@@ -150,5 +150,10 @@ export const orderApi = {
return request.post(`/order/${orderId}/rebateCalcFlag`, null, {
params: { rebateCalcFlag }
})
},
// 导出订单数据
exportOrders: (params: OrderQueryReq) => {
return request.post('/order/export', params, { responseType: 'blob' })
}
}
......
......@@ -209,6 +209,11 @@ export const getRebateTrendStats = (startDate?: string, endDate?: string) => {
})
}
// 导出返利数据
export const exportRebates = (params: RebateSearchParams) => {
return request.post('/api/rebate/export', params, { responseType: 'blob' })
}
export default {
getRebatePage,
getRebateById,
......@@ -227,5 +232,6 @@ export default {
exportRebate,
getRebateMonthlyStats,
getRebateStatusStats,
getRebateTrendStats
getRebateTrendStats,
exportRebates
}
......
......@@ -72,7 +72,8 @@
<div class="header-right">
<div class="user-info">
<span class="welcome-text">欢迎,{{ userInfo?.username || '用户' }}</span>
<span class="welcome-text" v-if="userInfoLoading">加载中...</span>
<span class="welcome-text" v-else>欢迎,{{ userInfo?.username || '未知' }}</span>
<div class="user-actions">
<button class="logout-btn" @click="handleLogout">退出登录</button>
</div>
......@@ -125,6 +126,7 @@
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { logoutApi } from '../api/auth'
import { request } from '../utils/request'
const router = useRouter()
const route = useRoute()
......@@ -134,6 +136,26 @@ const sidebarCollapsed = ref(false)
// 用户信息
const userInfo = ref<any>(null)
const userInfoLoading = ref(false)
// 获取用户信息
const fetchUserInfo = async () => {
try {
userInfoLoading.value = true
const response = await request.get('/api/auth/userinfo')
userInfo.value = response
console.log('顶部栏用户信息:', response)
} catch (error) {
console.error('获取用户信息失败:', error)
// 如果获取失败,尝试从本地存储获取
const storedUserInfo = localStorage.getItem('userInfo')
if (storedUserInfo) {
userInfo.value = JSON.parse(storedUserInfo)
}
} finally {
userInfoLoading.value = false
}
}
// 标签页容器引用
const tabContainer = ref<HTMLElement>()
......@@ -371,9 +393,17 @@ watch(() => route.path, (newPath) => {
// 组件挂载时获取用户信息
onMounted(() => {
const storedUserInfo = localStorage.getItem('userInfo')
if (storedUserInfo) {
userInfo.value = JSON.parse(storedUserInfo)
// 检查是否已登录
const token = localStorage.getItem('token')
if (token) {
// 调用API获取用户信息
fetchUserInfo()
} else {
// 如果没有token,尝试从本地存储获取
const storedUserInfo = localStorage.getItem('userInfo')
if (storedUserInfo) {
userInfo.value = JSON.parse(storedUserInfo)
}
}
})
</script>
......
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
......@@ -10,5 +12,6 @@ const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus)
// 挂载应用
app.mount('#app')
......
......@@ -46,6 +46,11 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
// 如果是blob响应(文件下载),直接返回
if (response.config.responseType === 'blob') {
return response.data
}
const { code, message, data } = response.data
// 请求成功
......@@ -121,8 +126,8 @@ export const request = {
return service.get(url, { params })
},
post<T = any>(url: string, data?: any): Promise<T> {
return service.post(url, data)
post<T = any>(url: string, data?: any, config?: any): Promise<T> {
return service.post(url, data, config)
},
put<T = any>(url: string, data?: any): Promise<T> {
......
......@@ -2,7 +2,8 @@
<div class="dashboard-container">
<div class="dashboard-header">
<h2>系统概览</h2>
<p>欢迎回来,{{ userInfo?.username || '用户' }}!</p>
<p v-if="loading">正在加载用户信息...</p>
<p v-else>欢迎回来,{{ userInfo?.username || '未知' }}!</p>
</div>
<div class="dashboard-stats">
......@@ -43,32 +44,34 @@
<div class="dashboard-card">
<h3>系统信息</h3>
<div class="info-grid">
<div class="info-item">
<div class="info-item">
<label>用户名:</label>
<span>{{ userInfo?.username || '未知' }}</span>
</div>
<div class="info-item">
<span v-if="loading">加载中...</span>
<span v-else>{{ userInfo?.username || '未知' }}</span>
</div>
<div class="info-item">
<label>角色:</label>
<span>{{ userInfo?.roles?.[0]?.roleName || '普通用户' }}</span>
</div>
<div class="info-item">
<span v-if="loading">加载中...</span>
<span v-else>{{ getRoleName(userInfo) || '普通用户' }}</span>
</div>
<div class="info-item">
<label>登录时间:</label>
<span>{{ currentTime }}</span>
</div>
<div class="info-item">
</div>
<div class="info-item">
<label>系统版本:</label>
<span>v1.0.0</span>
</div>
</div>
</div>
</div>
<div class="dashboard-card">
<h3>快速操作</h3>
<div class="quick-actions">
<button class="action-btn">👤 用户管理</button>
<button class="action-btn">🛡️ 角色管理</button>
<button class="action-btn">⚙️ 系统设置</button>
<button class="action-btn">📊 查看日志</button>
<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>
......@@ -78,28 +81,85 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { request } from '@/utils/request'
const router = useRouter()
const currentTime = ref('')
const userInfo = ref<any>(null)
const loading = ref(false)
// 获取角色名称
const getRoleName = (userInfo: any) => {
// 优先使用roles字段中的角色信息
if (userInfo?.roles && Array.isArray(userInfo.roles) && userInfo.roles.length > 0) {
return userInfo.roles[0].roleName || '普通用户'
}
// 如果没有roles字段,回退到authorities
const authorities = userInfo?.authorities
if (authorities && Array.isArray(authorities)) {
// 查找ROLE_开头的权限
const roleAuthority = authorities.find(auth =>
auth.authority && auth.authority.startsWith('ROLE_')
)
if (roleAuthority) {
// 移除ROLE_前缀并转换为中文
const roleName = roleAuthority.authority.replace('ROLE_', '')
const roleMap: { [key: string]: string } = {
'ADMIN': '管理员',
'USER': '普通用户',
'MANAGER': '经理',
'OPERATOR': '操作员'
}
return roleMap[roleName] || roleName
}
}
return '普通用户'
}
// 获取用户信息
const fetchUserInfo = async () => {
try {
loading.value = true
const response = await request.get('/api/auth/userinfo')
userInfo.value = response
console.log('用户信息:', response)
} catch (error) {
console.error('获取用户信息失败:', error)
// 如果获取失败,尝试从本地存储获取
const storedUserInfo = localStorage.getItem('userInfo')
if (storedUserInfo) {
userInfo.value = JSON.parse(storedUserInfo)
}
} finally {
loading.value = false
}
}
onMounted(() => {
// 获取当前时间
currentTime.value = new Date().toLocaleString()
// 获取用户信息
const storedUserInfo = localStorage.getItem('userInfo')
if (storedUserInfo) {
userInfo.value = JSON.parse(storedUserInfo)
}
// 检查是否已登录
const token = localStorage.getItem('token')
if (!token) {
router.push('/')
return
}
// 获取用户信息
fetchUserInfo()
})
// 页面跳转函数
const navigateTo = (path: string) => {
router.push(path).catch(err => {
console.error('页面跳转失败:', err)
})
}
const logout = () => {
// 清除本地存储
localStorage.removeItem('token')
......
......@@ -179,15 +179,11 @@
<div class="detail-grid">
<div class="detail-item">
<label>出库单ID:</label>
<span>{{ orderDetail.deliveryId }}</span>
<span>{{ deliveryDetail.deliveryId }}</span>
</div>
<div class="detail-item">
<label>出库单编号:</label>
<span>{{ orderDetail.deliveryNo }}</span>
</div>
<div class="detail-item">
<label>关联订单编号:</label>
<span>{{ orderDetail.orderNo }}</span>
<span>{{ deliveryDetail.deliveryNo }}</span>
</div>
<div class="detail-item">
<label>关联订单编号:</label>
......@@ -203,7 +199,7 @@
</div>
<div class="detail-item">
<label>出库日期:</label>
<span>{{ formatDate(orderDetail.deliveryDate) }}</span>
<span>{{ formatDate(deliveryDetail.deliveryDate) }}</span>
</div>
<div class="detail-item">
<label>出库状态:</label>
......@@ -213,28 +209,28 @@
</div>
<div class="detail-item">
<label>仓库编码:</label>
<span>{{ orderDetail.warehouseCode }}</span>
<span>{{ deliveryDetail.warehouseCode }}</span>
</div>
<div class="detail-item">
<label>数据来源:</label>
<span>{{ orderDetail.dataSource || '-' }}</span>
<span>{{ deliveryDetail.dataSource || '-' }}</span>
</div>
<div class="detail-item">
<label>上传时间:</label>
<span>{{ formatTime(orderDetail.uploadTime || '') }}</span>
<span>{{ formatTime(deliveryDetail.uploadTime || '') }}</span>
</div>
<div class="detail-item">
<label>创建时间:</label>
<span>{{ formatTime(orderDetail.createTime || '') }}</span>
<span>{{ formatTime(deliveryDetail.createTime || '') }}</span>
</div>
<div class="detail-item">
<label>更新时间:</label>
<span>{{ formatTime(orderDetail.updateTime || '') }}</span>
<span>{{ formatTime(deliveryDetail.updateTime || '') }}</span>
</div>
</div>
</div>
<div class="detail-section" v-if="orderDetail.deliveryItems && orderDetail.deliveryItems.length > 0">
<div class="detail-section" v-if="deliveryDetail?.deliveryItems && deliveryDetail.deliveryItems.length > 0">
<h4>出库明细</h4>
<table class="detail-table">
<thead>
......@@ -247,7 +243,7 @@
</tr>
</thead>
<tbody>
<tr v-for="item in orderDetail.deliveryItems" :key="item.deliveryItemId">
<tr v-for="item in deliveryDetail.deliveryItems" :key="item.itemId || item.deliveryId">
<td>{{ item.productCode }}</td>
<td>{{ item.productName }}</td>
<td>{{ item.deliveryQty }}</td>
......@@ -433,6 +429,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { deliveryApi, type DeliveryAddReq, type DeliveryInfo, type DeliveryQueryReq } from '../../api/delivery'
// 响应式数据
......@@ -518,7 +515,12 @@ const formatCurrency = (amount: number) => {
// 消息提示
const showMessage = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
console.log(`${type}: ${message}`)
alert(message)
ElMessage({
message,
type,
duration: 3000,
showClose: true
})
}
// 获取页码数组
......@@ -612,8 +614,78 @@ const handleDelete = async (delivery: DeliveryInfo) => {
}
const handleExport = () => {
showMessage('导出功能开发中...', 'warning')
const handleExport = async () => {
try {
// 显示确认弹窗
await ElMessageBox.confirm(
'确定要导出出库数据吗?导出将包含当前筛选条件下的所有数据。',
'确认导出',
{
confirmButtonText: '确定导出',
cancelButtonText: '取消',
type: 'warning',
center: true
}
)
// 用户确认后显示加载提示
const loadingMessage = ElMessage({
message: '正在导出数据,请稍候...',
type: 'warning',
duration: 0, // 不自动关闭
showClose: false
})
try {
// 准备导出参数
const exportParams = {
deliveryNo: searchParams.deliveryNo,
dealerCode: searchParams.dealerCode,
dealerName: searchParams.dealerName,
deliveryStatus: searchParams.deliveryStatus,
warehouseCode: searchParams.warehouseCode,
dataSource: searchParams.dataSource,
deliveryStartDate: searchParams.deliveryStartDate,
deliveryEndDate: searchParams.deliveryEndDate
}
// 调用导出接口
const response = await deliveryApi.exportDeliveries(exportParams)
// 创建下载链接
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 生成文件名
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, '').slice(0, 14)
link.download = `出库数据_${timestamp}.xlsx`
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
// 关闭加载提示,显示成功消息
loadingMessage.close()
ElMessage.success('导出成功!文件已开始下载')
} catch (exportError) {
// 关闭加载提示
loadingMessage.close()
throw exportError
}
} catch (error) {
if (error !== 'cancel') {
console.error('导出失败:', error)
ElMessage.error('导出失败,请重试')
}
}
}
// 新增出库相关函数
......@@ -680,9 +752,9 @@ const handleSubmitAdd = async () => {
deliveryItems: addFormData.deliveryItems.map(item => ({
productCode: item.productCode,
productName: item.productName,
productQty: item.deliveryQty,
unitPrice: item.deliveryPrice,
totalPrice: item.deliveryAmount
deliveryQty: item.deliveryQty,
deliveryPrice: item.deliveryPrice,
deliveryAmount: item.deliveryAmount
}))
}
......
......@@ -507,6 +507,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { invoiceApi, type InvoiceInfo, type InvoiceQueryReq } from '../../api/invoice'
// 响应式数据
......@@ -594,7 +595,12 @@ const formatCurrency = (amount: number) => {
// 消息提示
const showMessage = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
console.log(`${type}: ${message}`)
alert(message)
ElMessage({
message,
type,
duration: 3000,
showClose: true
})
}
// 获取页码数组
......@@ -825,8 +831,81 @@ const handleView = async (order: InvoiceInfo) => {
}
const handleExport = () => {
showMessage('导出功能开发中...', 'warning')
const handleExport = async () => {
try {
// 显示确认弹窗
await ElMessageBox.confirm(
'确定要导出发票数据吗?导出将包含当前筛选条件下的所有数据。',
'确认导出',
{
confirmButtonText: '确定导出',
cancelButtonText: '取消',
type: 'warning',
center: true
}
)
// 用户确认后显示加载提示
const loadingMessage = ElMessage({
message: '正在导出数据,请稍候...',
type: 'warning',
duration: 0, // 不自动关闭
showClose: false
})
try {
// 准备导出参数
const exportParams = {
invoiceNo: searchParams.invoiceNo,
orderNo: searchParams.orderNo,
deliveryNo: searchParams.deliveryNo,
dealerCode: searchParams.dealerCode,
dealerName: searchParams.dealerName,
invoiceStatus: searchParams.invoiceStatus,
dataSource: searchParams.dataSource,
invoiceStartDate: searchParams.invoiceStartDate,
invoiceEndDate: searchParams.invoiceEndDate,
minAmount: searchParams.minAmount,
maxAmount: searchParams.maxAmount
}
// 调用导出接口
const response = await invoiceApi.exportInvoices(exportParams)
// 创建下载链接
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 生成文件名
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, '').slice(0, 14)
link.download = `发票数据_${timestamp}.xlsx`
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
// 关闭加载提示,显示成功消息
loadingMessage.close()
ElMessage.success('导出成功!文件已开始下载')
} catch (exportError) {
// 关闭加载提示
loadingMessage.close()
throw exportError
}
} catch (error) {
if (error !== 'cancel') {
console.error('导出失败:', error)
ElMessage.error('导出失败,请重试')
}
}
}
const handlePrintInvoice = (invoice: InvoiceInfo) => {
......
......@@ -447,6 +447,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { orderApi, type OrderInfo, type OrderQueryReq, type OrderAddReq, type OrderUpdateReq, type OrderItemAddReq } from '../../api/order'
// 响应式数据
......@@ -586,9 +587,12 @@ const formatCurrency = (amount: number) => {
// 消息提示
const showMessage = (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
// 这里可以集成消息提示组件
console.log(`${type}: ${message}`)
alert(message)
ElMessage({
message,
type,
duration: 3000,
showClose: true
})
}
// 方法
......@@ -757,8 +761,82 @@ const handleBatchDelete = async () => {
}
const handleExport = () => {
showMessage('导出功能开发中...', 'warning')
const handleExport = async () => {
try {
// 显示确认弹窗
await ElMessageBox.confirm(
'确定要导出订单数据吗?导出将包含当前筛选条件下的所有数据。',
'确认导出',
{
confirmButtonText: '确定导出',
cancelButtonText: '取消',
type: 'warning',
center: true
}
)
// 用户确认后显示加载提示
const loadingMessage = ElMessage({
message: '正在导出数据,请稍候...',
type: 'warning',
duration: 0, // 不自动关闭
showClose: false
})
try {
// 准备导出参数
const exportParams = {
orderNo: searchParams.orderNo,
dealerCode: searchParams.dealerCode,
dealerName: searchParams.dealerName,
deliveryStatus: searchParams.deliveryStatus,
invoiceStatus: searchParams.invoiceStatus,
rebateCalcFlag: searchParams.rebateCalcFlag,
dataSource: searchParams.dataSource,
verifyStatus: searchParams.verifyStatus,
orderStartDate: searchParams.orderStartDate,
orderEndDate: searchParams.orderEndDate,
minAmount: searchParams.minAmount,
maxAmount: searchParams.maxAmount
}
// 调用导出接口
const response = await orderApi.exportOrders(exportParams)
// 创建下载链接
const blob = new Blob([response], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
// 生成文件名
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, '').slice(0, 14)
link.download = `订单数据_${timestamp}.xlsx`
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
// 关闭加载提示,显示成功消息
loadingMessage.close()
ElMessage.success('导出成功!文件已开始下载')
} catch (exportError) {
// 关闭加载提示
loadingMessage.close()
throw exportError
}
} catch (error) {
if (error !== 'cancel') {
console.error('导出失败:', error)
ElMessage.error('导出失败,请重试')
}
}
}
const handleSelectAll = () => {
......
......@@ -6,30 +6,30 @@
<div class="search-row">
<div class="search-item">
<span class="search-label">经销商名称</span>
<el-input
v-model="searchForm.dealerName"
placeholder="请输入经销商名称"
clearable
<el-input
v-model="searchForm.dealerName"
placeholder="请输入经销商名称"
clearable
size="small"
style="width: 160px"
/>
</div>
<div class="search-item">
<span class="search-label">返利编号</span>
<el-input
<el-input
v-model="searchForm.rebateNo"
placeholder="请输入返利编号"
clearable
clearable
size="small"
style="width: 160px"
/>
</div>
<div class="search-item">
<span class="search-label">日期</span>
<el-date-picker
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="small"
......@@ -42,10 +42,10 @@
<div class="search-actions">
<el-button type="primary" size="small" @click="handleSearch" :loading="loading">
查询
</el-button>
</el-button>
<el-button size="small" @click="handleReset">
重置
</el-button>
重置
</el-button>
</div>
</div>
</div>
......@@ -61,13 +61,13 @@
<div class="chart-legend">
<div class="legend-item">
<span class="legend-dot" style="background-color: #4CAF50;"></span>
<span>返利金额</span>
</div>
<span>返利合计</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background-color: #2196F3;"></span>
<span>已审核返利</span>
</div>
</div>
<span>已使用返利</span>
</div>
</div>
</div>
<div class="chart-content">
<div ref="trendChart" class="chart"></div>
......@@ -77,15 +77,15 @@
<!-- 右侧饼图 -->
<div class="chart-card pie-chart">
<div class="chart-header">
<h3 class="chart-title">计算统计</h3>
<h3 class="chart-title">返利统计</h3>
<div class="chart-legend">
<div class="legend-item">
<span class="legend-dot" style="background-color: #2196F3;"></span>
<span>已计算</span>
<span>剩余返利</span>
</div>
<div class="legend-item">
<span class="legend-dot" style="background-color: #4CAF50;"></span>
<span>未计算</span>
<span class="legend-dot" style="background-color: #9CCC65;"></span>
<span>已使用返利</span>
</div>
</div>
</div>
......@@ -102,14 +102,20 @@
<div class="table-title">
返利记录
</div>
<div class="table-info">
数据状态: {{ loading ? '加载中...' : `共${rebateList.length}条记录` }}
<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>
</div>
<el-table
<el-table
v-loading="loading"
:data="rebateList"
:data="rebateList"
style="width: 100%"
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 'normal' }"
:empty-text="rebateList.length === 0 ? '暂无数据' : ''"
......@@ -153,7 +159,7 @@
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<div class="pagination-info">
......@@ -178,7 +184,7 @@
@keyup.enter="handleJumpPage"
/>
</div>
</div>
</div>
</div>
......@@ -384,12 +390,13 @@ import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus, Check, Close, Money, Delete, Download } from '@element-plus/icons-vue'
import * as echarts from 'echarts'
import rebateApi, { type Rebate, type RebateSearchParams } from '@/api/rebate'
import rebateApi, { type Rebate, type RebateSearchParams, exportRebates } from '@/api/rebate'
import { formatDate } from '@/utils/index'
// 响应式数据
const loading = ref(false)
const submitLoading = ref(false)
const exportLoading = ref(false)
const auditLoading = ref(false)
const rebateList = ref<Rebate[]>([])
const selectedRebates = ref<Rebate[]>([])
......@@ -780,7 +787,7 @@ const handleViewDetail = async (row: Rebate) => {
// 根据request.ts拦截器的处理,response.data已经是实际数据
if (response && response.data) {
rebateDetail.value = response.data
detailDialogVisible.value = true
detailDialogVisible.value = true
console.log('详情数据:', response.data)
} else {
ElMessage.error('获取返利详情失败')
......@@ -884,7 +891,7 @@ const handleDelete = async (row: Rebate) => {
const { data } = await rebateApi.deleteRebate(row.rebateId!)
if (data.code === 200) {
ElMessage.success('删除成功')
getRebateList()
getRebateList()
} else {
ElMessage.error(data.message || '删除失败')
}
......@@ -942,7 +949,7 @@ const handleBatchDelete = async () => {
if (data.code === 200) {
ElMessage.success('批量删除成功')
getRebateList()
getRebateList()
} else {
ElMessage.error(data.message || '批量删除失败')
}
......@@ -1039,30 +1046,62 @@ const handleBatchRelease = async () => {
// 导出
const handleExport = async () => {
try {
loading.value = true
const params = { ...searchForm }
const response = await rebateApi.exportRebate(params)
// 显示确认弹窗
await ElMessageBox.confirm(
'确定要导出返利数据吗?导出将包含当前筛选条件下的所有数据。',
'确认导出',
{
confirmButtonText: '确定导出',
cancelButtonText: '取消',
type: 'warning',
center: true
}
)
// 创建下载链接
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
// 用户确认后显示加载提示
const loadingMessage = ElMessage({
message: '正在导出数据,请稍候...',
type: 'warning',
duration: 0, // 不自动关闭
showClose: false
})
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `返利数据_${new Date().toISOString().split('T')[0]}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
ElMessage.success('导出成功')
try {
exportLoading.value = true
const params = { ...searchForm }
const response = await exportRebates(params)
// 创建下载链接
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().split('T')[0]}.xlsx`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
// 关闭加载提示,显示成功消息
loadingMessage.close()
ElMessage.success('导出成功!文件已开始下载')
} catch (exportError) {
// 关闭加载提示
loadingMessage.close()
throw exportError
}
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败')
if (error !== 'cancel') {
console.error('导出失败:', error)
ElMessage.error('导出失败,请重试')
}
} finally {
loading.value = false
exportLoading.value = false
}
}
......@@ -1073,27 +1112,30 @@ const initTrendChart = async () => {
const chart = echarts.init(trendChart.value)
try {
// 调用后端API获取月度统计数据
const response = await rebateApi.getRebateMonthlyStats()
let monthlyData = []
// 调用后端API获取月度统计数据 - 查询2025年的数据
const response = await rebateApi.getRebateMonthlyStats(2025)
console.log('月度统计API响应:', response)
let monthlyData: any[] = []
if (response && response.data) {
monthlyData = response.data
// request.ts拦截器已经返回了data字段,所以response就是数据数组
if (response && Array.isArray(response)) {
monthlyData = response
}
console.log('处理后的月度数据:', monthlyData)
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
// 处理后端返回的月度数据
const totalTrendData = months.map((month, index) => {
const totalRebateData = months.map((month, index) => {
const monthIndex = index + 1
const monthData = monthlyData.find((item: any) => item.month === monthIndex)
return monthData ? monthData.totalAmount : 0
})
const calculatedTrendData = months.map((month, index) => {
const usedRebateData = months.map((month, index) => {
const monthIndex = index + 1
const monthData = monthlyData.find((item: any) => item.month === monthIndex)
return monthData ? monthData.calculatedAmount : 0
return monthData ? (monthData.calculatedAmount || 0) : 0
})
const option = {
......@@ -1143,10 +1185,10 @@ const initTrendChart = async () => {
},
series: [
{
name: '返利金额',
name: '返利合计',
type: 'line',
smooth: true,
data: totalTrendData,
data: totalRebateData,
itemStyle: { color: '#4CAF50' },
lineStyle: {
color: '#4CAF50',
......@@ -1157,10 +1199,10 @@ const initTrendChart = async () => {
showSymbol: true
},
{
name: '已审核返利',
name: '已使用返利',
type: 'line',
smooth: true,
data: calculatedTrendData,
data: usedRebateData,
itemStyle: { color: '#2196F3' },
lineStyle: {
color: '#2196F3',
......@@ -1185,8 +1227,8 @@ const initTrendChart = async () => {
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
const totalTrendData = months.map(() => Math.floor(totalAmount / 12))
const calculatedTrendData = months.map(() => Math.floor(calculatedAmount / 12))
const totalRebateDataFallback = months.map(() => Math.floor(totalAmount / 12))
const usedRebateDataFallback = months.map(() => Math.floor(calculatedAmount / 12))
const fallbackOption = {
tooltip: {
......@@ -1235,10 +1277,10 @@ const initTrendChart = async () => {
},
series: [
{
name: '返利金额',
name: '返利合计',
type: 'line',
smooth: true,
data: totalTrendData,
data: totalRebateDataFallback,
itemStyle: { color: '#4CAF50' },
lineStyle: {
color: '#4CAF50',
......@@ -1249,10 +1291,10 @@ const initTrendChart = async () => {
showSymbol: true
},
{
name: '已审核返利',
name: '已使用返利',
type: 'line',
smooth: true,
data: calculatedTrendData,
data: usedRebateDataFallback,
itemStyle: { color: '#2196F3' },
lineStyle: {
color: '#2196F3',
......@@ -1280,33 +1322,42 @@ const initPieChart = async () => {
try {
// 调用后端API获取状态统计数据
const response = await rebateApi.getRebateStatusStats()
let statusData = { calculatedCount: 0, unCalculatedCount: 0 }
console.log('状态统计API响应:', response)
let statusData: any = { calculatedAmount: 0, unCalculatedAmount: 0 }
if (response && response.data) {
statusData = response.data
// request.ts拦截器已经返回了data字段,所以response就是数据对象
if (response && typeof response === 'object') {
statusData = response
}
console.log('处理后的状态数据:', statusData)
// 计算剩余返利和已使用返利
const usedRebate = statusData.calculatedAmount || 0
const remainingRebate = statusData.unCalculatedAmount || 0
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
formatter: (params: any) => {
return `${params.name}: ¥${params.value.toLocaleString()} (${params.percent}%)`
}
},
series: [
{
name: '审核状态',
name: '返利统计',
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '50%'],
data: [
{
value: statusData.calculatedCount || 0,
name: '已计算',
value: remainingRebate,
name: '剩余返利',
itemStyle: { color: '#2196F3' }
},
{
value: statusData.unCalculatedCount || 0,
name: '未计算',
itemStyle: { color: '#4CAF50' }
value: usedRebate,
name: '已使用返利',
itemStyle: { color: '#9CCC65' }
}
],
label: {
......@@ -1339,30 +1390,36 @@ const initPieChart = async () => {
console.error('获取饼图数据失败:', error)
// 如果API调用失败,使用当前列表数据作为备用方案
const calculatedCount = rebateList.value.filter(item => item.calcFlag === 1).length
const unCalculatedCount = rebateList.value.filter(item => item.calcFlag === 0).length
const usedAmount = rebateList.value
.filter(item => item.calcFlag === 1)
.reduce((sum, item) => sum + (item.rebateAmount || 0), 0)
const remainingAmount = rebateList.value
.filter(item => item.calcFlag === 0)
.reduce((sum, item) => sum + (item.rebateAmount || 0), 0)
const fallbackOption = {
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
formatter: (params: any) => {
return `${params.name}: ¥${params.value.toLocaleString()} (${params.percent}%)`
}
},
series: [
{
name: '审核状态',
name: '返利统计',
type: 'pie',
radius: ['35%', '65%'],
center: ['50%', '50%'],
data: [
{
value: calculatedCount,
name: '已审核',
value: remainingAmount,
name: '剩余返利',
itemStyle: { color: '#2196F3' }
},
{
value: unCalculatedCount,
name: '未审核',
itemStyle: { color: '#4CAF50' }
value: usedAmount,
name: '已使用返利',
itemStyle: { color: '#9CCC65' }
}
],
label: {
......@@ -1475,7 +1532,7 @@ onMounted(async () => {
font-size: 16px;
font-weight: 500;
color: #333;
margin: 0;
margin: 0;
}
.chart-legend {
......@@ -1540,9 +1597,15 @@ onMounted(async () => {
color: #333;
}
.table-info {
font-size: 12px;
color: #999;
.table-actions {
display: flex;
align-items: center;
gap: 12px;
.table-info {
font-size: 12px;
color: #999;
}
}
}
......@@ -1571,7 +1634,7 @@ onMounted(async () => {
}
.pagination-wrapper {
display: flex;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
......
<template>
<div class="settings-container">
<!-- 页面标题 -->
<div class="page-header">
<h2>系统设置</h2>
<p>管理系统配置和参数</p>
</div>
<!-- 设置内容 -->
<div class="settings-content">
<div class="settings-card">
<h3>基本设置</h3>
<div class="setting-item">
<label>系统名称:</label>
<input v-model="settings.systemName" class="setting-input" placeholder="请输入系统名称" />
</div>
<div class="setting-item">
<label>系统版本:</label>
<input v-model="settings.systemVersion" class="setting-input" placeholder="请输入系统版本" />
</div>
<div class="setting-item">
<label>系统描述:</label>
<textarea v-model="settings.systemDescription" class="setting-textarea" placeholder="请输入系统描述"></textarea>
</div>
</div>
<div class="settings-card">
<h3>业务设置</h3>
<div class="setting-item">
<label>默认分页大小:</label>
<select v-model="settings.defaultPageSize" class="setting-select">
<option value="10">10条/页</option>
<option value="20">20条/页</option>
<option value="50">50条/页</option>
<option value="100">100条/页</option>
</select>
</div>
<div class="setting-item">
<label>数据保留天数:</label>
<input v-model="settings.dataRetentionDays" type="number" class="setting-input" placeholder="请输入数据保留天数" />
</div>
<div class="setting-item">
<label>自动备份:</label>
<label class="checkbox-label">
<input v-model="settings.autoBackup" type="checkbox" />
<span>启用自动备份</span>
</label>
</div>
</div>
<div class="settings-card">
<h3>安全设置</h3>
<div class="setting-item">
<label>会话超时时间(分钟):</label>
<input v-model="settings.sessionTimeout" type="number" class="setting-input" placeholder="请输入会话超时时间" />
</div>
<div class="setting-item">
<label>密码复杂度:</label>
<select v-model="settings.passwordComplexity" class="setting-select">
<option value="low">低</option>
<option value="medium">中</option>
<option value="high">高</option>
</select>
</div>
<div class="setting-item">
<label>登录失败锁定:</label>
<label class="checkbox-label">
<input v-model="settings.loginLock" type="checkbox" />
<span>启用登录失败锁定</span>
</label>
</div>
</div>
<div class="settings-card">
<h3>通知设置</h3>
<div class="setting-item">
<label>邮件通知:</label>
<label class="checkbox-label">
<input v-model="settings.emailNotification" type="checkbox" />
<span>启用邮件通知</span>
</label>
</div>
<div class="setting-item">
<label>短信通知:</label>
<label class="checkbox-label">
<input v-model="settings.smsNotification" type="checkbox" />
<span>启用短信通知</span>
</label>
</div>
<div class="setting-item">
<label>系统消息:</label>
<label class="checkbox-label">
<input v-model="settings.systemMessage" type="checkbox" />
<span>启用系统消息</span>
</label>
</div>
</div>
<!-- 操作按钮 -->
<div class="settings-actions">
<button @click="handleSave" class="action-btn primary">💾 保存设置</button>
<button @click="handleReset" class="action-btn secondary">🔄 重置</button>
<button @click="handleTest" class="action-btn info">🧪 测试连接</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
// 设置数据
const settings = reactive({
systemName: 'Apple ERP系统',
systemVersion: '1.0.0',
systemDescription: '企业资源规划管理系统',
defaultPageSize: 20,
dataRetentionDays: 365,
autoBackup: true,
sessionTimeout: 30,
passwordComplexity: 'medium',
loginLock: true,
emailNotification: true,
smsNotification: false,
systemMessage: true
})
// 原始设置(用于重置)
const originalSettings = ref({})
// 保存设置
const handleSave = () => {
// 这里可以调用后端API保存设置
console.log('保存设置:', settings)
showMessage('设置保存成功', 'success')
}
// 重置设置
const handleReset = () => {
Object.assign(settings, originalSettings.value)
showMessage('设置已重置', 'warning')
}
// 测试连接
const handleTest = () => {
showMessage('连接测试成功', 'success')
}
// 显示消息
const showMessage = (message: string, type: 'success' | 'warning' | 'error') => {
// 这里可以集成Element Plus的消息组件
console.log(`${type}: ${message}`)
}
// 组件挂载时加载设置
onMounted(() => {
// 这里可以调用后端API加载设置
originalSettings.value = { ...settings }
})
</script>
<style scoped>
.settings-container {
padding: 20px;
background-color: #f5f5f5;
min-height: 100vh;
}
.page-header {
margin-bottom: 30px;
text-align: center;
}
.page-header h2 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.page-header p {
color: #666;
font-size: 16px;
}
.settings-content {
max-width: 1200px;
margin: 0 auto;
}
.settings-card {
background: white;
border-radius: 8px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.settings-card h3 {
color: #333;
margin-bottom: 20px;
font-size: 18px;
border-bottom: 2px solid #e9ecef;
padding-bottom: 10px;
}
.setting-item {
display: flex;
align-items: center;
margin-bottom: 20px;
gap: 16px;
}
.setting-item label {
min-width: 150px;
color: #333;
font-weight: 500;
}
.setting-input,
.setting-select,
.setting-textarea {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s;
}
.setting-input:focus,
.setting-select:focus,
.setting-textarea:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.setting-textarea {
min-height: 80px;
resize: vertical;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
}
.settings-actions {
display: flex;
gap: 16px;
justify-content: center;
margin-top: 30px;
}
.action-btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
min-width: 120px;
}
.action-btn.primary {
background-color: #007bff;
color: white;
}
.action-btn.primary:hover {
background-color: #0056b3;
}
.action-btn.secondary {
background-color: #6c757d;
color: white;
}
.action-btn.secondary:hover {
background-color: #545b62;
}
.action-btn.info {
background-color: #17a2b8;
color: white;
}
.action-btn.info:hover {
background-color: #138496;
}
@media (max-width: 768px) {
.setting-item {
flex-direction: column;
align-items: flex-start;
}
.setting-item label {
min-width: auto;
margin-bottom: 8px;
}
.settings-actions {
flex-direction: column;
align-items: center;
}
}
</style>
......@@ -7,7 +7,7 @@
"auto-imports.d.ts",
"components.d.ts"
],
"exclude": ["src/**/__tests__/*"],
"exclude": ["src/**/__tests__/*", "src/views/test-menu.vue"],
"compilerOptions": {
"composite": true,
"baseUrl": ".",
......