feat: 完成履约管理模块全量功能迭代

本次迭代包含以下核心功能:
1. 新增履约时效总览可视化页面,支持多维度数据统计
2. 实现物料/客户/供应商的Excel批量导入导出功能
3. 新增订单批量结单功能,优化结单流程校验
4. 完善日志配置,新增文件日志落地
5. 修复分类查询逻辑,优化多租户数据隔离
6. 新增甲方履约结单管理页面与权限控制
7. 重构部分Mapper与Service接口,增强代码健壮性
This commit is contained in:
2026-06-18 11:10:36 +08:00
parent 7a8e4297e0
commit 7b71822a32
37 changed files with 1759 additions and 66 deletions

View File

@@ -1,14 +1,17 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.bid.BizClient;
import com.ruoyi.system.service.bid.IBizClientService;
@@ -60,6 +63,35 @@ public class BizClientController extends BaseController {
return toAjax(service.deleteBizClientByIds(clientIds));
}
// ========== Excel 导入导出 ==========
@PreAuthorize("@ss.hasPermi('bid:client:export')")
@Log(title = "甲方客户", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, BizClient query) {
List<BizClient> list = service.selectBizClientList(query);
ExcelUtil<BizClient> util = new ExcelUtil<BizClient>(BizClient.class);
util.exportExcel(response, list, "客户数据");
}
@PreAuthorize("@ss.hasPermi('bid:client:import')")
@Log(title = "甲方客户", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelUtil<BizClient> util = new ExcelUtil<BizClient>(BizClient.class);
List<BizClient> clientList = util.importExcel(file.getInputStream());
Long tenantId = getDeptId();
if (tenantId == null) { tenantId = 1L; }
String message = service.importClient(clientList, updateSupport, getUsername(), tenantId);
return success(message);
}
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<BizClient> util = new ExcelUtil<BizClient>(BizClient.class);
util.importTemplateExcel(response, "客户数据");
}
/**
* 查询客户的关联历史发货单
* 链路: client → client_quote → rfq → delivery_order

View File

@@ -1,6 +1,7 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@@ -83,13 +84,27 @@ public class BizDeliveryOrderController extends BaseController {
return toAjax(service.recall(id));
}
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit')")
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit') or @ss.hasPermi('bid:clientdelivery:closeDate:edit')")
@Log(title = "结单时间", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/closeDate")
public AjaxResult setCloseDate(@PathVariable Long id, @RequestParam String closeDate) {
return toAjax(service.setCloseDate(id, closeDate, getUsername()));
}
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit') or @ss.hasPermi('bid:clientdelivery:closeDate:edit')")
@Log(title = "结单时间", businessType = BusinessType.UPDATE)
@PutMapping("/batchCloseDate")
public AjaxResult batchSetCloseDate(@RequestBody Map<String, Object> params) {
@SuppressWarnings("unchecked")
List<Integer> rawIds = (List<Integer>) params.get("ids");
String closeDate = (String) params.get("closeDate");
if (rawIds == null || rawIds.isEmpty()) {
return AjaxResult.error("请选择要结单的订单");
}
List<Long> ids = rawIds.stream().map(Integer::longValue).collect(java.util.stream.Collectors.toList());
return toAjax(service.batchSetCloseDate(ids, closeDate, getUsername()));
}
// ════════════════════════════════════════
// 物料发货记录
// ════════════════════════════════════════════
@@ -107,18 +122,66 @@ public class BizDeliveryOrderController extends BaseController {
@PreAuthorize("@ss.hasPermi('bid:order:transit')")
@GetMapping("/transit/stats")
public AjaxResult transitStats() {
return success(service.selectTransitStats());
Long tenantId = getDeptId();
return success(service.selectTransitStats(tenantId));
}
@PreAuthorize("@ss.hasPermi('bid:order:history')")
@PreAuthorize("@ss.hasPermi('bid:order:history') or @ss.hasPermi('bid:clientdelivery:closeDate')")
@GetMapping("/history/stats")
public AjaxResult historyStats() {
return success(service.selectHistoryStats());
public AjaxResult historyStats(@RequestParam(required = false) String type) {
Long tenantId = getDeptId();
return success(service.selectHistoryStats(tenantId, type));
}
@PreAuthorize("@ss.hasPermi('bid:order:closeDate:edit')")
@GetMapping("/closeDate/stats")
public AjaxResult closeDateStats() {
return success(service.selectCloseDateStats());
Long tenantId = getDeptId();
return success(service.selectCloseDateStats(tenantId));
}
/** 履约时效可视化 - 时间线数据(供应商履约 + 甲方履约通用) */
@PreAuthorize("@ss.hasPermi('bid:order:timeline') or @ss.hasPermi('bid:clientdelivery:timeline') or @ss.hasPermi('bid:order:transit') or @ss.hasPermi('bid:order:history')")
@GetMapping("/timeline")
public AjaxResult timeline(@RequestParam(required = false) String type,
@RequestParam(required = false) String status,
@RequestParam(required = false) String dateFrom,
@RequestParam(required = false) String dateTo) {
Long tenantId = getDeptId();
List<Map<String, Object>> orders = service.selectTimelineData(tenantId, type, status, dateFrom, dateTo);
// 计算准时/延期统计数据
long onTime = orders.stream().filter(o -> {
Object cd = o.get("actualCloseDate");
Object dd = o.get("delayDate");
if (cd == null) return false;
// 有延期日期时以延期日期为准,否则以约定交货日为准
Object base = dd != null ? dd : o.get("deliveryDate");
if (base == null) return false;
try {
return java.sql.Date.valueOf(cd.toString()).compareTo(java.sql.Date.valueOf(base.toString())) <= 0;
} catch (Exception e) { return false; }
}).count();
long delayed = orders.stream().filter(o -> {
Object cd = o.get("actualCloseDate");
Object dd = o.get("delayDate");
if (cd == null) return false;
Object base = dd != null ? dd : o.get("deliveryDate");
if (base == null) return false;
try {
return java.sql.Date.valueOf(cd.toString()).compareTo(java.sql.Date.valueOf(base.toString())) > 0;
} catch (Exception e) { return false; }
}).count();
long pending = orders.stream().filter(o -> o.get("actualCloseDate") == null).count();
Map<String, Object> result = new java.util.HashMap<>();
result.put("orders", orders);
Map<String, Object> stats = new java.util.HashMap<>();
stats.put("onTime", onTime);
stats.put("delayed", delayed);
stats.put("pending", pending);
result.put("stats", stats);
return success(result);
}
}

View File

@@ -1,14 +1,17 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.bid.BizMaterial;
import com.ruoyi.system.service.bid.IBizMaterialService;
@@ -61,6 +64,35 @@ public class BizMaterialController extends BaseController {
return toAjax(service.deleteBizMaterialByIds(materialIds));
}
// ========== Excel 导入导出 ==========
@PreAuthorize("@ss.hasPermi('bid:material:export')")
@Log(title = "物料管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, BizMaterial query) {
List<BizMaterial> list = service.selectBizMaterialList(query);
ExcelUtil<BizMaterial> util = new ExcelUtil<BizMaterial>(BizMaterial.class);
util.exportExcel(response, list, "物料数据");
}
@PreAuthorize("@ss.hasPermi('bid:material:import')")
@Log(title = "物料管理", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelUtil<BizMaterial> util = new ExcelUtil<BizMaterial>(BizMaterial.class);
List<BizMaterial> materialList = util.importExcel(file.getInputStream());
Long tenantId = getDeptId();
if (tenantId == null) { tenantId = 1L; }
String message = service.importMaterial(materialList, updateSupport, getUsername(), tenantId);
return success(message);
}
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<BizMaterial> util = new ExcelUtil<BizMaterial>(BizMaterial.class);
util.importTemplateExcel(response, "物料数据");
}
// ========== 物料详情页接口 ==========
@PreAuthorize("@ss.hasPermi('bid:material:detail')")

View File

@@ -1,14 +1,17 @@
package com.ruoyi.web.controller.bid;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.system.domain.bid.BizSupplier;
import com.ruoyi.system.service.bid.IBizSupplierService;
@@ -54,4 +57,33 @@ public class BizSupplierController extends BaseController {
public AjaxResult remove(@PathVariable Long[] supplierIds) {
return toAjax(service.deleteBizSupplierByIds(supplierIds));
}
// ========== Excel 导入导出 ==========
@PreAuthorize("@ss.hasPermi('bid:supplier:export')")
@Log(title = "供应商管理", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, BizSupplier query) {
List<BizSupplier> list = service.selectBizSupplierList(query);
ExcelUtil<BizSupplier> util = new ExcelUtil<BizSupplier>(BizSupplier.class);
util.exportExcel(response, list, "供应商数据");
}
@PreAuthorize("@ss.hasPermi('bid:supplier:import')")
@Log(title = "供应商管理", businessType = BusinessType.IMPORT)
@PostMapping("/importData")
public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception {
ExcelUtil<BizSupplier> util = new ExcelUtil<BizSupplier>(BizSupplier.class);
List<BizSupplier> supplierList = util.importExcel(file.getInputStream());
Long tenantId = getDeptId();
if (tenantId == null) { tenantId = 1L; }
String message = service.importSupplier(supplierList, updateSupport, getUsername(), tenantId);
return success(message);
}
@PostMapping("/importTemplate")
public void importTemplate(HttpServletResponse response) {
ExcelUtil<BizSupplier> util = new ExcelUtil<BizSupplier>(BizSupplier.class);
util.importTemplateExcel(response, "供应商数据");
}
}

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />
<property name="log.path" value="logs" />
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
@@ -8,14 +9,38 @@
</encoder>
</appender>
<logger name="com.ruoyi" level="info" />
<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/ruoyi.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/ruoyi.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
</appender>
<appender name="error-file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<logger name="com.ruoyi" level="debug" />
<logger name="org.springframework" level="warn" />
<logger name="sys-user" level="info" />
<root level="info">
<appender-ref ref="console" />
<appender-ref ref="file" />
<appender-ref ref="error-file" />
</root>
<logger name="sys-user" level="info">
<appender-ref ref="console" />
</logger>
</configuration>

View File

@@ -1,19 +1,40 @@
package com.ruoyi.system.domain.bid;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.annotation.Excel;
public class BizClient extends BaseEntity {
private Long clientId;
private Long tenantId;
@Excel(name = "客户编号", type = Excel.Type.IMPORT)
private String clientNo;
@Excel(name = "客户名称", type = Excel.Type.IMPORT)
private String clientName;
@Excel(name = "联系人", type = Excel.Type.IMPORT)
private String contact;
@Excel(name = "电话", type = Excel.Type.IMPORT)
private String phone;
@Excel(name = "邮箱", type = Excel.Type.IMPORT)
private String email;
@Excel(name = "城市", type = Excel.Type.IMPORT)
private String city;
@Excel(name = "地址", type = Excel.Type.IMPORT)
private String address;
@Excel(name = "等级", type = Excel.Type.IMPORT, combo = {"A", "B", "C"}, defaultValue = "B")
private String grade;
@Excel(name = "来源", type = Excel.Type.IMPORT)
private String source;
@Excel(name = "状态", type = Excel.Type.IMPORT, readConverterExp = "0=正常,1=停用", defaultValue = "0")
private String status;
public Long getClientId() { return clientId; }

View File

@@ -1,25 +1,46 @@
package com.ruoyi.system.domain.bid;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.annotation.Excel;
public class BizMaterial extends BaseEntity {
private Long materialId;
private Long tenantId;
private Long categoryId;
@Excel(name = "物料编码", type = Excel.Type.IMPORT)
private String materialCode;
@Excel(name = "物料名称", type = Excel.Type.IMPORT)
private String materialName;
@Excel(name = "规格型号", type = Excel.Type.IMPORT)
private String spec;
@Excel(name = "单位", type = Excel.Type.IMPORT)
private String unit;
@Excel(name = "品牌", type = Excel.Type.IMPORT)
private String brand;
@Excel(name = "描述", type = Excel.Type.IMPORT)
private String description;
@Excel(name = "状态", type = Excel.Type.IMPORT, readConverterExp = "0=正常,1=停用", defaultValue = "0")
private String status;
// search helper
private String categoryName;
// 新增字段
@Excel(name = "性能参数", type = Excel.Type.IMPORT)
private String performanceParams;
@Excel(name = "材质", type = Excel.Type.IMPORT)
private String material;
@Excel(name = "用途", type = Excel.Type.IMPORT)
private String purpose;
private String imageUrl;
public Long getMaterialId() { return materialId; }

View File

@@ -1,16 +1,30 @@
package com.ruoyi.system.domain.bid;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.annotation.Excel;
public class BizSupplier extends BaseEntity {
private Long supplierId;
private Long tenantId;
@Excel(name = "供应商名称", type = Excel.Type.IMPORT)
private String supplierName;
@Excel(name = "联系人", type = Excel.Type.IMPORT)
private String contact;
@Excel(name = "电话", type = Excel.Type.IMPORT)
private String phone;
@Excel(name = "邮箱", type = Excel.Type.IMPORT)
private String email;
@Excel(name = "地址", type = Excel.Type.IMPORT)
private String address;
private Long userId;
@Excel(name = "状态", type = Excel.Type.IMPORT, readConverterExp = "0=正常,1=停用", defaultValue = "0")
private String status;
public Long getSupplierId() { return supplierId; }

View File

@@ -1,6 +1,7 @@
package com.ruoyi.system.mapper.bid;
import com.ruoyi.system.domain.bid.BizClient;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
@@ -12,4 +13,7 @@ public interface BizClientMapper {
int deleteBizClientById(Long id);
int deleteBizClientByIds(Long[] ids);
List<Map<String, Object>> selectClientDeliveryOrders(Long clientId);
// 按客户编号查询(导入判重用)
BizClient selectBizClientByNo(@Param("tenantId") Long tenantId, @Param("clientNo") String clientNo);
}

View File

@@ -7,10 +7,10 @@ import java.util.List;
import java.util.Map;
public interface BizDeliveryOrderMapper {
List<BizDeliveryOrder> selectBizDeliveryOrderList(BizDeliveryOrder query);
List<BizDeliveryOrder> selectBizDeliveryOrderList(@Param("query") BizDeliveryOrder query);
BizDeliveryOrder selectBizDeliveryOrderById(Long id);
int insertBizDeliveryOrder(BizDeliveryOrder record);
int updateBizDeliveryOrder(BizDeliveryOrder record);
int insertBizDeliveryOrder(@Param("record") BizDeliveryOrder record);
int updateBizDeliveryOrder(@Param("record") BizDeliveryOrder record);
int deleteBizDeliveryOrderById(Long id);
int deleteBizDeliveryOrderByIds(Long[] ids);
@@ -25,9 +25,15 @@ public interface BizDeliveryOrderMapper {
List<Map<String, Object>> selectMaterialRecords(@Param("materialId") Long materialId);
// 在途统计
Map<String, Object> selectTransitStats();
Map<String, Object> selectTransitStats(@Param("tenantId") Long tenantId);
// 履约时间线
List<Map<String, Object>> selectTimelineData(@Param("tenantId") Long tenantId,
@Param("type") String type,
@Param("status") String status,
@Param("dateFrom") String dateFrom,
@Param("dateTo") String dateTo);
// 历史统计
Map<String, Object> selectHistoryStats();
Map<String, Object> selectHistoryStats(@Param("tenantId") Long tenantId, @Param("type") String type);
// 结单统计
Map<String, Object> selectCloseDateStats();
Map<String, Object> selectCloseDateStats(@Param("tenantId") Long tenantId);
}

View File

@@ -32,4 +32,7 @@ public interface BizMaterialMapper {
// 根据物料名称精确匹配(同名称不同规格/品牌对比)
List<BizMaterial> selectMaterialsByExactName(@Param("materialName") String materialName, @Param("excludeId") Long excludeId);
// 按物料编码查询(导入判重用)
BizMaterial selectBizMaterialByCode(@Param("tenantId") Long tenantId, @Param("materialCode") String materialCode);
}

View File

@@ -12,4 +12,7 @@ public interface BizSupplierMapper {
int updateBizSupplier(BizSupplier record);
int deleteBizSupplierById(Long id);
int deleteBizSupplierByIds(Long[] ids);
// 按供应商名称查询(导入判重用)
BizSupplier selectBizSupplierByName(@Param("tenantId") Long tenantId, @Param("supplierName") String supplierName);
}

View File

@@ -12,4 +12,7 @@ public interface IBizClientService {
int deleteBizClientById(Long id);
int deleteBizClientByIds(Long[] ids);
List<Map<String, Object>> selectClientDeliveryOrders(Long clientId);
// Excel批量导入
String importClient(List<BizClient> clientList, Boolean updateSupport, String operName, Long tenantId);
}

View File

@@ -17,14 +17,17 @@ public interface IBizDeliveryOrderService {
int complete(Long id, String username);
int recall(Long id);
int setCloseDate(Long id, String closeDate, String username);
int batchSetCloseDate(List<Long> ids, String closeDate, String username);
// 物料发货记录
List<Map<String, Object>> selectMaterialRecords(Long materialId);
// 在途统计
Map<String, Object> selectTransitStats();
Map<String, Object> selectTransitStats(Long tenantId);
// 历史统计
Map<String, Object> selectHistoryStats();
Map<String, Object> selectHistoryStats(Long tenantId, String type);
// 结单统计
Map<String, Object> selectCloseDateStats();
Map<String, Object> selectCloseDateStats(Long tenantId);
// 履约时间线
List<Map<String, Object>> selectTimelineData(Long tenantId, String type, String status, String dateFrom, String dateTo);
}

View File

@@ -26,4 +26,7 @@ public interface IBizMaterialService {
// 根据物料名称精确匹配(同名称不同规格/品牌对比)
List<BizMaterial> selectMaterialsByExactName(String materialName, Long excludeId);
// Excel批量导入
String importMaterial(List<BizMaterial> materialList, Boolean updateSupport, String operName, Long tenantId);
}

View File

@@ -11,4 +11,7 @@ public interface IBizSupplierService {
int updateBizSupplier(BizSupplier record);
int deleteBizSupplierById(Long id);
int deleteBizSupplierByIds(Long[] ids);
// Excel批量导入
String importSupplier(List<BizSupplier> supplierList, Boolean updateSupport, String operName, Long tenantId);
}

View File

@@ -3,6 +3,8 @@ package com.ruoyi.system.service.bid.impl;
import com.ruoyi.system.domain.bid.BizClient;
import com.ruoyi.system.mapper.bid.BizClientMapper;
import com.ruoyi.system.service.bid.IBizClientService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -47,4 +49,64 @@ public class BizClientServiceImpl implements IBizClientService {
public List<Map<String, Object>> selectClientDeliveryOrders(Long clientId) {
return mapper.selectClientDeliveryOrders(clientId);
}
// ═══════════════════════════════════════════════
// Excel批量导入
// ═══════════════════════════════════════════════
@Override
public String importClient(List<BizClient> clientList, Boolean updateSupport, String operName, Long tenantId) {
if (StringUtils.isNull(clientList) || clientList.isEmpty()) {
throw new ServiceException("导入数据不能为空!");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (BizClient row : clientList) {
try {
if (StringUtils.isEmpty(row.getClientName())) {
throw new ServiceException("客户名称不能为空");
}
// 按客户编号判重(编号为空则跳过判重直接新增)
BizClient existing = null;
if (StringUtils.isNotEmpty(row.getClientNo())) {
existing = mapper.selectBizClientByNo(tenantId, row.getClientNo());
}
if (existing == null) {
row.setTenantId(tenantId);
row.setCreateBy(operName);
if (StringUtils.isEmpty(row.getStatus())) {
row.setStatus("0");
}
if (StringUtils.isEmpty(row.getGrade())) {
row.setGrade("B");
}
mapper.insertBizClient(row);
successNum++;
successMsg.append("<br/>" + successNum + "、客户 " + row.getClientName() + " 导入成功");
} else if (updateSupport) {
row.setClientId(existing.getClientId());
row.setTenantId(tenantId);
row.setUpdateBy(operName);
mapper.updateBizClient(row);
successNum++;
successMsg.append("<br/>" + successNum + "、客户 " + row.getClientName() + " 更新成功");
} else {
failureNum++;
failureMsg.append("<br/>" + failureNum + "、客户编号 " + row.getClientNo() + " 已存在");
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、客户 " + (row.getClientName() == null ? "" : row.getClientName()) + " 导入失败:" + e.getMessage();
failureMsg.append(msg);
}
}
if (failureNum > 0) {
failureMsg.insert(0, "导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
}

View File

@@ -14,6 +14,8 @@ import java.util.Date;
import java.util.List;
import java.util.Map;
import com.ruoyi.common.exception.ServiceException;
@Service
public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
@Autowired
@@ -126,11 +128,36 @@ public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
@Override
@Transactional
public int setCloseDate(Long id, String closeDate, String username) {
if (closeDate == null || closeDate.isEmpty()) {
return mapper.updateDeliveryStatus(id, null, null, null, "");
} else {
return mapper.updateDeliveryStatus(id, null, null, java.sql.Date.valueOf(closeDate), username);
BizDeliveryOrder d = mapper.selectBizDeliveryOrderById(id);
if (d == null) throw new RuntimeException("发货单不存在");
String status = d.getDeliveryStatus();
// 仅允许已签收(history)的订单设置结单日期
if (!"history".equals(status)) {
throw new RuntimeException("当前状态(" + status + ")不允许设置结单日期,请先完成收货签收");
}
if (closeDate == null || closeDate.isEmpty()) {
throw new RuntimeException("结单日期不能为空");
}
// 设置结单日期,同时将状态从 history(已签收) 置为 closed(已结单)
java.sql.Date parsedDate;
try {
parsedDate = java.sql.Date.valueOf(closeDate);
} catch (IllegalArgumentException e) {
throw new RuntimeException("日期格式错误,请使用 yyyy-MM-dd 格式");
}
return mapper.updateDeliveryStatus(id, "closed", null, parsedDate, username);
}
@Override
@Transactional
public int batchSetCloseDate(List<Long> ids, String closeDate, String username) {
if (ids == null || ids.isEmpty()) throw new ServiceException("请选择要结单的订单");
if (closeDate == null || closeDate.isEmpty()) throw new ServiceException("请设置结单日期");
int total = 0;
for (Long id : ids) {
total += setCloseDate(id, closeDate, username);
}
return total;
}
// ═══════════════════════════════════════════════
@@ -143,17 +170,22 @@ public class BizDeliveryOrderServiceImpl implements IBizDeliveryOrderService {
}
@Override
public Map<String, Object> selectTransitStats() {
return mapper.selectTransitStats();
public Map<String, Object> selectTransitStats(Long tenantId) {
return mapper.selectTransitStats(tenantId);
}
@Override
public Map<String, Object> selectHistoryStats() {
return mapper.selectHistoryStats();
public Map<String, Object> selectHistoryStats(Long tenantId, String type) {
return mapper.selectHistoryStats(tenantId, type);
}
@Override
public Map<String, Object> selectCloseDateStats() {
return mapper.selectCloseDateStats();
public Map<String, Object> selectCloseDateStats(Long tenantId) {
return mapper.selectCloseDateStats(tenantId);
}
@Override
public List<Map<String, Object>> selectTimelineData(Long tenantId, String type, String status, String dateFrom, String dateTo) {
return mapper.selectTimelineData(tenantId, type, status, dateFrom, dateTo);
}
}

View File

@@ -3,6 +3,8 @@ package com.ruoyi.system.service.bid.impl;
import com.ruoyi.system.domain.bid.BizMaterial;
import com.ruoyi.system.mapper.bid.BizMaterialMapper;
import com.ruoyi.system.service.bid.IBizMaterialService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
@@ -113,4 +115,62 @@ public class BizMaterialServiceImpl implements IBizMaterialService {
public List<BizMaterial> selectMaterialsByExactName(String materialName, Long excludeId) {
return mapper.selectMaterialsByExactName(materialName, excludeId);
}
// ═══════════════════════════════════════════════
// Excel批量导入
// ═══════════════════════════════════════════════
@Override
public String importMaterial(List<BizMaterial> materialList, Boolean updateSupport, String operName, Long tenantId) {
if (StringUtils.isNull(materialList) || materialList.isEmpty()) {
throw new ServiceException("导入数据不能为空!");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (BizMaterial row : materialList) {
try {
// 校验必填字段
if (StringUtils.isEmpty(row.getMaterialCode())) {
throw new ServiceException("物料编码不能为空");
}
if (StringUtils.isEmpty(row.getMaterialName())) {
throw new ServiceException("物料名称不能为空");
}
// 查询是否已存在
BizMaterial existing = mapper.selectBizMaterialByCode(tenantId, row.getMaterialCode());
if (existing == null) {
row.setTenantId(tenantId);
row.setCreateBy(operName);
if (StringUtils.isEmpty(row.getStatus())) {
row.setStatus("0");
}
mapper.insertBizMaterial(row);
successNum++;
successMsg.append("<br/>" + successNum + "、物料编码 " + row.getMaterialCode() + " 导入成功");
} else if (updateSupport) {
row.setMaterialId(existing.getMaterialId());
row.setTenantId(tenantId);
row.setUpdateBy(operName);
mapper.updateBizMaterial(row);
successNum++;
successMsg.append("<br/>" + successNum + "、物料编码 " + row.getMaterialCode() + " 更新成功");
} else {
failureNum++;
failureMsg.append("<br/>" + failureNum + "、物料编码 " + row.getMaterialCode() + " 已存在");
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、物料编码 " + (row.getMaterialCode() == null ? "" : row.getMaterialCode()) + " 导入失败:" + e.getMessage();
failureMsg.append(msg);
}
}
if (failureNum > 0) {
failureMsg.insert(0, "导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
}

View File

@@ -3,6 +3,8 @@ package com.ruoyi.system.service.bid.impl;
import com.ruoyi.system.domain.bid.BizSupplier;
import com.ruoyi.system.mapper.bid.BizSupplierMapper;
import com.ruoyi.system.service.bid.IBizSupplierService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -46,4 +48,58 @@ public class BizSupplierServiceImpl implements IBizSupplierService {
public int deleteBizSupplierByIds(Long[] ids) {
return mapper.deleteBizSupplierByIds(ids);
}
// ═══════════════════════════════════════════════
// Excel批量导入
// ═══════════════════════════════════════════════
@Override
public String importSupplier(List<BizSupplier> supplierList, Boolean updateSupport, String operName, Long tenantId) {
if (StringUtils.isNull(supplierList) || supplierList.isEmpty()) {
throw new ServiceException("导入数据不能为空!");
}
int successNum = 0;
int failureNum = 0;
StringBuilder successMsg = new StringBuilder();
StringBuilder failureMsg = new StringBuilder();
for (BizSupplier row : supplierList) {
try {
if (StringUtils.isEmpty(row.getSupplierName())) {
throw new ServiceException("供应商名称不能为空");
}
// 按供应商名称判重
BizSupplier existing = mapper.selectBizSupplierByName(tenantId, row.getSupplierName());
if (existing == null) {
row.setTenantId(tenantId);
row.setCreateBy(operName);
if (StringUtils.isEmpty(row.getStatus())) {
row.setStatus("0");
}
mapper.insertBizSupplier(row);
successNum++;
successMsg.append("<br/>" + successNum + "、供应商 " + row.getSupplierName() + " 导入成功");
} else if (updateSupport) {
row.setSupplierId(existing.getSupplierId());
row.setTenantId(tenantId);
row.setUpdateBy(operName);
mapper.updateBizSupplier(row);
successNum++;
successMsg.append("<br/>" + successNum + "、供应商 " + row.getSupplierName() + " 更新成功");
} else {
failureNum++;
failureMsg.append("<br/>" + failureNum + "、供应商 " + row.getSupplierName() + " 已存在");
}
} catch (Exception e) {
failureNum++;
String msg = "<br/>" + failureNum + "、供应商 " + (row.getSupplierName() == null ? "" : row.getSupplierName()) + " 导入失败:" + e.getMessage();
failureMsg.append(msg);
}
}
if (failureNum > 0) {
failureMsg.insert(0, "导入失败!共 " + failureNum + " 条数据格式不正确,错误如下:");
throw new ServiceException(failureMsg.toString());
} else {
successMsg.insert(0, "恭喜您,数据已全部导入成功!共 " + successNum + " 条,数据如下:");
}
return successMsg.toString();
}
}

View File

@@ -40,6 +40,11 @@
SELECT * FROM biz_client WHERE client_id=#{id}
</select>
<!-- 按客户编号查询(导入时判重用) -->
<select id="selectBizClientByNo" resultMap="BaseRM">
SELECT * FROM biz_client WHERE tenant_id=#{tenantId} AND client_no=#{clientNo} LIMIT 1
</select>
<insert id="insertBizClient" useGeneratedKeys="true" keyProperty="clientId">
INSERT INTO biz_client(tenant_id,client_no,client_name,contact,phone,email,city,address,grade,source,status,create_by,create_time)
VALUES(#{tenantId},#{clientNo},#{clientName},#{contact},#{phone},#{email},#{city},#{address},#{grade},#{source},#{status},#{createBy},NOW())

View File

@@ -37,13 +37,13 @@
LEFT JOIN biz_client_quote cq ON d.client_quote_id = cq.quote_id
LEFT JOIN biz_client cl ON cq.client_id = cl.client_id
<where>
<if test="tenantId != null"> AND d.tenant_id=#{tenantId}</if>
<if test="type != null and type != ''"> AND d.type=#{type}</if>
<if test="doNo != null and doNo != ''"> AND d.do_no LIKE CONCAT('%',#{doNo},'%')</if>
<if test="supplierId != null"> AND d.supplier_id=#{supplierId}</if>
<if test="deliveryStatus != null and deliveryStatus != ''"> AND d.delivery_status=#{deliveryStatus}</if>
<if test="supplierName != null and supplierName != ''"> AND s.supplier_name LIKE CONCAT('%',#{supplierName},'%')</if>
<if test="clientName != null and clientName != ''"> AND cl.client_name LIKE CONCAT('%',#{clientName},'%')</if>
<if test="query.tenantId != null"> AND d.tenant_id=#{query.tenantId}</if>
<if test="query.type != null and query.type != ''"> AND d.type=#{query.type}</if>
<if test="query.doNo != null and query.doNo != ''"> AND d.do_no LIKE CONCAT('%',#{query.doNo},'%')</if>
<if test="query.supplierId != null"> AND d.supplier_id=#{query.supplierId}</if>
<if test="query.deliveryStatus != null and query.deliveryStatus != ''"> AND d.delivery_status=#{query.deliveryStatus}</if>
<if test="query.supplierName != null and query.supplierName != ''"> AND s.supplier_name LIKE CONCAT('%',#{query.supplierName},'%')</if>
<if test="query.clientName != null and query.clientName != ''"> AND cl.client_name LIKE CONCAT('%',#{query.clientName},'%')</if>
</where>
ORDER BY d.create_time DESC
</select>
@@ -55,26 +55,26 @@
WHERE d.do_id=#{id}
</select>
<insert id="insertBizDeliveryOrder" useGeneratedKeys="true" keyProperty="doId">
<insert id="insertBizDeliveryOrder" useGeneratedKeys="true" keyProperty="record.doId">
INSERT INTO biz_delivery_order(tenant_id,do_no,type,rfq_id,quotation_id,client_quote_id,supplier_id,total_amount,currency,delivery_date,delay_date,actual_close_date,close_date_set_by,delivery_status,remark,create_by,create_time)
VALUES(#{tenantId},#{doNo},#{type},#{rfqId},#{quotationId},#{clientQuoteId},#{supplierId},#{totalAmount},#{currency},#{deliveryDate},#{delayDate},#{actualCloseDate},#{closeDateSetBy},#{deliveryStatus},#{remark},#{createBy},NOW())
VALUES(#{record.tenantId},#{record.doNo},#{record.type},#{record.rfqId},#{record.quotationId},#{record.clientQuoteId},#{record.supplierId},#{record.totalAmount},#{record.currency},#{record.deliveryDate},#{record.delayDate},#{record.actualCloseDate},#{record.closeDateSetBy},#{record.deliveryStatus},#{record.remark},#{record.createBy},NOW())
</insert>
<update id="updateBizDeliveryOrder">
UPDATE biz_delivery_order
<set>
<if test="doNo != null">do_no=#{doNo},</if>
<if test="supplierId != null">supplier_id=#{supplierId},</if>
<if test="totalAmount != null">total_amount=#{totalAmount},</if>
<if test="deliveryDate != null">delivery_date=#{deliveryDate},</if>
<if test="delayDate != null">delay_date=#{delayDate},</if>
<if test="actualCloseDate != null">actual_close_date=#{actualCloseDate},</if>
<if test="closeDateSetBy != null">close_date_set_by=#{closeDateSetBy},</if>
<if test="deliveryStatus != null">delivery_status=#{deliveryStatus},</if>
<if test="remark != null">remark=#{remark},</if>
update_by=#{updateBy}, update_time=NOW()
<if test="record.doNo != null">do_no=#{record.doNo},</if>
<if test="record.supplierId != null">supplier_id=#{record.supplierId},</if>
<if test="record.totalAmount != null">total_amount=#{record.totalAmount},</if>
<if test="record.deliveryDate != null">delivery_date=#{record.deliveryDate},</if>
<if test="record.delayDate != null">delay_date=#{record.delayDate},</if>
<if test="record.actualCloseDate != null">actual_close_date=#{record.actualCloseDate},</if>
<if test="record.closeDateSetBy != null">close_date_set_by=#{record.closeDateSetBy},</if>
<if test="record.deliveryStatus != null">delivery_status=#{record.deliveryStatus},</if>
<if test="record.remark != null">remark=#{record.remark},</if>
update_by=#{record.updateBy}, update_time=NOW()
</set>
WHERE do_id=#{doId}
WHERE do_id=#{record.doId}
</update>
<select id="selectTransitStats" resultType="java.util.Map">
@@ -84,16 +84,20 @@
SUM(CASE WHEN DATEDIFF(delivery_date, CURDATE()) &lt; 0 THEN 1 ELSE 0 END) AS overdue
FROM biz_delivery_order
WHERE delivery_status = 'transit'
<if test="tenantId != null"> AND tenant_id=#{tenantId}</if>
</select>
<select id="selectCloseDateStats" resultType="java.util.Map">
SELECT
COUNT(*) AS pendingClose,
(SELECT COUNT(*) FROM biz_delivery_order WHERE delivery_status='history'
<if test="tenantId != null"> AND tenant_id=#{tenantId}</if>
) AS pendingClose,
SUM(CASE WHEN actual_close_date = CURDATE() THEN 1 ELSE 0 END) AS todayClosed,
SUM(CASE WHEN YEARWEEK(actual_close_date, 1) = YEARWEEK(CURDATE(), 1) THEN 1 ELSE 0 END) AS weekClosed,
ROUND(AVG(DATEDIFF(actual_close_date, delivery_date)), 1) AS avgCycleDays
FROM biz_delivery_order
WHERE delivery_status = 'history'
WHERE delivery_status = 'closed'
<if test="tenantId != null"> AND tenant_id=#{tenantId}</if>
</select>
<select id="selectHistoryStats" resultType="java.util.Map">
@@ -104,6 +108,8 @@
ROUND(AVG(DATEDIFF(actual_close_date, delivery_date)), 1) AS avgDeliveryDays
FROM biz_delivery_order
WHERE delivery_status = 'history'
<if test="tenantId != null"> AND tenant_id=#{tenantId}</if>
<if test="type != null and type != ''"> AND type=#{type}</if>
</select>
<delete id="deleteBizDeliveryOrderById">DELETE FROM biz_delivery_order WHERE do_id=#{id}</delete>
@@ -148,4 +154,34 @@
WHERE di.material_id = #{materialId}
ORDER BY d.create_time DESC
</select>
<!-- ═══════════════════════════════════════════════
履约时效可视化 - 时间线数据
═══════════════════════════════════════════════ -->
<select id="selectTimelineData" resultType="java.util.Map">
SELECT
d.do_id AS doId,
d.do_no AS doNo,
d.type,
d.delivery_status AS deliveryStatus,
d.delivery_date AS deliveryDate,
d.delay_date AS delayDate,
d.actual_close_date AS actualCloseDate,
d.create_time AS createTime,
d.total_amount AS totalAmount,
COALESCE(s.supplier_name, cl.client_name) AS partyName,
(SELECT COUNT(*) FROM biz_delivery_order_item WHERE do_id = d.do_id) AS itemCount
FROM biz_delivery_order d
LEFT JOIN biz_supplier s ON d.supplier_id = s.supplier_id
LEFT JOIN biz_client_quote cq ON d.client_quote_id = cq.quote_id
LEFT JOIN biz_client cl ON cq.client_id = cl.client_id
<where>
<if test="tenantId != null"> AND d.tenant_id = #{tenantId}</if>
<if test="type != null and type != ''"> AND d.type = #{type}</if>
<if test="status != null and status != ''"> AND d.delivery_status = #{status}</if>
<if test="dateFrom != null and dateFrom != ''"> AND d.delivery_date &gt;= #{dateFrom}</if>
<if test="dateTo != null and dateTo != ''"> AND d.delivery_date &lt;= #{dateTo}</if>
</where>
ORDER BY d.create_time DESC
</select>
</mapper>

View File

@@ -6,6 +6,7 @@
<id property="materialId" column="material_id"/>
<result property="tenantId" column="tenant_id"/>
<result property="categoryId" column="category_id"/>
<result property="categoryName" column="category_name"/>
<result property="materialCode" column="material_code"/>
<result property="materialName" column="material_name"/>
<result property="spec" column="spec"/>
@@ -33,10 +34,10 @@
m.category_id = #{categoryId}
OR m.category_id IN (
SELECT category_id FROM biz_material_category
WHERE ancestors LIKE CONCAT(
(SELECT ancestors FROM biz_material_category WHERE category_id = #{categoryId}),
',%'
)
WHERE ancestors LIKE CONCAT('%,', #{categoryId}, ',%')
OR ancestors LIKE CONCAT(#{categoryId}, ',%')
OR ancestors LIKE CONCAT('%,', #{categoryId})
OR ancestors = #{categoryId}
)
)</if>
<if test="materialCode != null and materialCode != ''"> AND m.material_code LIKE CONCAT('%',#{materialCode},'%')</if>
@@ -55,6 +56,11 @@
WHERE m.material_id=#{id}
</select>
<!-- 按物料编码查询(导入时判重用) -->
<select id="selectBizMaterialByCode" resultMap="BaseRM">
SELECT * FROM biz_material WHERE tenant_id=#{tenantId} AND material_code=#{materialCode} LIMIT 1
</select>
<insert id="insertBizMaterial" useGeneratedKeys="true" keyProperty="materialId">
INSERT INTO biz_material(tenant_id,category_id,material_code,material_name,spec,unit,brand,
description,status,performance_params,material,purpose,image_url,create_by,create_time)

View File

@@ -30,6 +30,11 @@
<select id="selectBizSupplierById" resultMap="BaseRM">SELECT * FROM biz_supplier WHERE supplier_id=#{id}</select>
<!-- 按供应商名称查询(导入时判重用) -->
<select id="selectBizSupplierByName" resultMap="BaseRM">
SELECT * FROM biz_supplier WHERE tenant_id=#{tenantId} AND supplier_name=#{supplierName} LIMIT 1
</select>
<select id="selectBizSupplierByUserId" resultMap="BaseRM">
SELECT * FROM biz_supplier WHERE user_id = #{userId}
</select>

View File

@@ -9,3 +9,4 @@ export const shipDelivery = (id) => request({ url: baseUrl + '/' + id + '/ship',
export const completeDelivery = (id) => request({ url: baseUrl + '/' + id + '/complete', method: 'put' })
export const recallDelivery = (id) => request({ url: baseUrl + '/' + id + '/recall', method: 'put' })
export const setCloseDate = (id, closeDate) => request({ url: baseUrl + '/' + id + '/closeDate', method: 'put', params: { closeDate } })
export const batchSetCloseDate = (ids, closeDate) => request({ url: baseUrl + '/batchCloseDate', method: 'put', data: { ids, closeDate } })

View File

@@ -0,0 +1,5 @@
import request from '@/utils/request'
const baseUrl = '/bid/delivery'
/** 履约时间线数据 */
export const getTimeline = (params) => request({ url: baseUrl + '/timeline', method: 'get', params })

View File

@@ -232,7 +232,7 @@ export const dynamicRoutes = [
name: 'OrderObjection',
permissions: ['bid:objection:list'],
meta: { title: '订单异议', activeMenu: '/bid/order' }
}
},
]
},
@@ -268,6 +268,18 @@ export const dynamicRoutes = [
permissions: ['bid:clientdelivery:signed'],
children: [{ path: '', component: () => import('@/views/bid/clientDelivery/signed'), name: 'ClientDeliverySigned', meta: { title: '甲方签收', activeMenu: '/bid/clientDelivery' } }]
},
{
path: '/bid/clientDelivery/closeDate',
component: Layout,
permissions: ['bid:clientdelivery:closeDate'],
children: [{ path: '', component: () => import('@/views/bid/clientDelivery/closeDate'), name: 'ClientCloseDate', meta: { title: '甲方结单管理', activeMenu: '/bid/clientDelivery' } }]
},
{
path: '/bid/clientDelivery/timeline',
component: Layout,
permissions: ['bid:clientdelivery:timeline'],
children: [{ path: '', component: () => import('@/views/bid/clientDelivery/timeline'), name: 'ClientDeliveryTimeline', meta: { title: '甲方履约时效', activeMenu: '/bid/clientDelivery' } }]
},
{
path: '/bid/comparison/detail',
@@ -281,6 +293,20 @@ export const dynamicRoutes = [
meta: { title: '智慧比价分析', activeMenu: '/bid/comparison' }
}]
},
// ── 履约时效总览 ──
{
path: '/bid/timeline',
component: Layout,
permissions: ['bid:order:timeline'],
children: [{
path: '',
component: () => import('@/views/bid/order/timeline'),
name: 'OrderTimeline',
meta: { title: '履约时效总览', activeMenu: '/bid/timeline' }
}]
},
{
path: '/system/user-auth',
component: Layout,

View File

@@ -16,6 +16,8 @@
<el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch">搜索</el-button>
<div class="toolbar-right">
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd">新增客户</el-button>
<el-button type="info" size="small" icon="el-icon-upload2" @click="handleImport" v-hasPermi="['bid:client:import']">导入</el-button>
<el-button type="warning" size="small" icon="el-icon-download" @click="handleExport" v-hasPermi="['bid:client:export']">导出</el-button>
</div>
</div>
@@ -197,15 +199,27 @@
<el-button @click="detailOpen = false">关闭</el-button>
</div>
</el-dialog>
<!-- Excel 导入对话框 -->
<excel-import-dialog
ref="importRef"
title="客户导入"
action="/bid/client/importData"
template-action="/bid/client/importTemplate"
template-file-name="client_template"
update-support-label="是否更新已经存在的客户数据"
@success="getList" />
</div>
</template>
<script>
import { listClient, getClient, addClient, updateClient, delClient, getClientOrders } from "@/api/bid/client"
import { getDelivery } from "@/api/bid/delivery"
import ExcelImportDialog from "@/components/ExcelImportDialog"
export default {
name: "Client",
components: { ExcelImportDialog },
data() {
return {
activeTab: "list",
@@ -238,6 +252,9 @@ export default {
},
handleSearch() { this.queryParams.pageNum = 1; this.getList(); this.loadClientOptions() },
handleAdd() { this.editId = null; this.form = { grade: "B", status: "0", clientNo: "", clientName: "", contact: "", phone: "", email: "", city: "", address: "", remark: "" }; this.dialogTitle = "新增客户"; this.dialogOpen = true },
// ═══ Excel 导入导出 ═══
handleImport() { this.$refs.importRef.open(); },
handleExport() { this.download('/bid/client/export', { ...this.queryParams }, `client_${new Date().getTime()}.xlsx`); },
handleEdit(row) { this.editId = row.clientId; this.form = { ...row }; this.dialogTitle = "编辑客户"; this.dialogOpen = true },
cancelDialog() { this.dialogOpen = false; this.$refs.form && this.$refs.form.clearValidate() },
submitForm() {

View File

@@ -0,0 +1,234 @@
<template>
<div class="jd-cd-page">
<!-- 统计卡片 -->
<el-row :gutter="12" class="stat-row">
<el-col :span="6" v-for="c in statCards" :key="c.key">
<div class="stat-card">
<div class="stat-body"><div class="stat-num">{{ stats[c.key] != null ? stats[c.key] : '-' }}</div><div class="stat-lbl">{{ c.label }}</div></div>
<i :class="c.icon" class="stat-icon"></i>
</div>
</el-col>
</el-row>
<!-- 筛选栏 -->
<div class="jd-filter">
<div class="filter-left">
<el-input v-model="q.doNo" placeholder="搜索单号" clearable size="small" class="filter-input" @keyup.enter.native="handleSearch" />
<el-input v-model="q.clientName" placeholder="搜索甲方客户" clearable size="small" class="filter-input" @keyup.enter.native="handleSearch" />
<el-select v-model="q.deliveryStatus" placeholder="状态" clearable size="small" style="width:110px" @change="handleSearch">
<el-option label="已签收" value="history" />
<el-option label="已结单" value="closed" />
</el-select>
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
<el-button size="small" @click="resetSearch">重置</el-button>
</div>
<div class="filter-right">
<el-button size="small" icon="el-icon-refresh" @click="getList">刷新</el-button>
</div>
</div>
<div class="cd-body">
<!-- 左侧列表 -->
<div class="cd-left">
<div class="left-header">
<span class="left-title">{{ q.deliveryStatus === 'closed' ? '已结单订单' : '已签收订单' }}</span>
<el-tag :type="q.deliveryStatus === 'closed' ? 'info' : 'success'" size="small" effect="dark" style="margin-left:8px">
{{ q.deliveryStatus === 'closed' ? '已结单' : '已签收' }}
</el-tag>
</div>
<el-table ref="table" v-loading="loading" :data="list" border stripe size="small"
@selection-change="onSelectionChange" class="jd-table"
style="width:100%" :row-class-name="rowClass">
<el-table-column type="selection" width="38" align="center" />
<el-table-column label="单号" width="155">
<template slot-scope="s">
<span class="order-link">{{ s.row.doNo }}</span>
</template>
</el-table-column>
<el-table-column label="甲方客户" prop="clientName" min-width="130" show-overflow-tooltip />
<el-table-column label="金额" width="110" align="right">
<template slot-scope="s"><span class="amount">¥{{ s.row.totalAmount }}</span></template>
</el-table-column>
<el-table-column label="交货期" prop="deliveryDate" width="95" align="center" />
<el-table-column :label="q.deliveryStatus === 'closed' ? '结单日期' : '签收日期'" width="115" align="center">
<template slot-scope="s">
<el-date-picker v-if="q.deliveryStatus !== 'closed'" v-model="s.row._editDate" type="date" value-format="yyyy-MM-dd"
size="mini" style="width:105px" placeholder="选择日期" :clearable="true"
@change="onDateChange(s.row)" />
<span v-else class="closed-date">{{ s.row.actualCloseDate ? s.row.actualCloseDate.substring(0, 10) : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="周期" width="75" align="center">
<template slot-scope="s">
<span :class="cycleClass(s.row._cycleDays)">{{ s.row._cycleDays != null ? s.row._cycleDays + '天' : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="配送差异" width="85" align="center">
<template slot-scope="s">
<span :class="diffClass(s.row._diffDays)">{{ s.row._diffDays != null ? diffLabel(s.row._diffDays) : '-' }}</span>
</template>
</el-table-column>
</el-table>
<el-empty v-if="!loading && list.length === 0" :description="q.deliveryStatus === 'closed' ? '暂无已结单订单' : '暂无已签收订单'" style="padding:40px 0" />
<pagination v-show="total>0" :total="total" :page.sync="q.pageNum" :limit.sync="q.pageSize" @pagination="getList" />
</div>
<!-- 右侧操作区 (已结单状态下隐藏批量操作) -->
<div v-if="q.deliveryStatus !== 'closed'" class="cd-right">
<div class="right-panel">
<div class="right-title">批量操作</div>
<div class="right-section">
<div class="rs-header">已选择 <b>{{ selected.length }}</b> </div>
<div v-if="selected.length" class="rs-list">
<div v-for="r in selected" :key="r.doId" class="rs-item">{{ r.doNo }} {{ r.clientName }}</div>
</div>
<div v-else class="rs-empty">请在左侧勾选已签收订单</div>
</div>
<div class="right-section">
<div class="rs-header">批量设置签收日期</div>
<div class="rs-date-row">
<el-date-picker v-model="batchDate" type="date" value-format="yyyy-MM-dd" size="small" style="width:140px" placeholder="选择日期" />
<el-button size="small" @click="applyBatchDate" :disabled="!selected.length || !batchDate">应用到选中</el-button>
</div>
<div class="rs-quick">
<el-button size="mini" @click="batchDate = todayStr()">今天</el-button>
<el-button size="mini" @click="batchDate = yesterdayStr()">昨天</el-button>
</div>
</div>
<div class="right-section">
<div class="rs-header">批量确认结单</div>
<el-button type="primary" size="small" style="width:100%" @click="batchConfirm"
:disabled="!selected.length || !allHaveDate">确认结单 ({{ selected.length }})</el-button>
<div v-if="selected.length && !allHaveDate" class="rs-warn">有订单未设置签收日期</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { listDelivery, batchSetCloseDate } from "@/api/bid/delivery"
import request from '@/utils/request'
export default {
name: "ClientCloseDate",
data() {
return {
loading: false, list: [], total: 0, stats: {},
selected: [],
batchDate: null,
q: { pageNum: 1, pageSize: 50, type: "client", deliveryStatus: "history", doNo: "", clientName: "" },
statCards: [
{ key: "totalHistory", label: "待结单数", icon: "el-icon-document", color: "#e4393c" },
{ key: "monthCompleted", label: "本月已结单", icon: "el-icon-circle-check", color: "#67c23a" },
{ key: "totalAmount", label: "结单总金额", icon: "el-icon-money", color: "#e6a23c" },
{ key: "avgDeliveryDays", label: "平均配送周期(天)", icon: "el-icon-time", color: "#8e44ad" }
]
}
},
computed: {
allHaveDate() { return this.selected.every(r => r._editDate) }
},
created() { this.getList(); this.getStats() },
methods: {
getList() {
this.loading = true
listDelivery(this.q).then(r => {
this.list = (r.rows || []).map(d => ({
...d,
deliveryDate: d.deliveryDate ? d.deliveryDate.substring(0, 10) : '',
_editDate: d.actualCloseDate ? d.actualCloseDate.substring(0, 10) : '',
_cycleDays: null,
_diffDays: null
})).map(d => { this.calcRow(d); return d })
this.total = r.total || 0; this.loading = false
}).catch(() => { this.loading = false })
},
getStats() {
request({ url: '/bid/delivery/history/stats?type=client', method: 'get' }).then(r => { this.stats = r.data || {} }).catch(() => {})
},
handleSearch() { this.q.pageNum = 1; this.getList() },
resetSearch() { this.q.doNo = ""; this.q.clientName = ""; this.q.deliveryStatus = "history"; this.handleSearch() },
onSelectionChange(rows) { this.selected = rows },
rowClass({ row }) { return this.selected.includes(row) ? 'selected-row' : '' },
onDateChange(row) { this.calcRow(row) },
calcRow(row) {
if (!row.deliveryDate || !row._editDate) { row._cycleDays = null; row._diffDays = null; return }
const cd = new Date(row._editDate)
const baseDate = row.delayDate || row.deliveryDate
const dd = new Date(baseDate)
row._cycleDays = Math.round((cd - dd) / 86400000)
row._diffDays = row._cycleDays
},
cycleClass(d) { if (d === null) return ''; return d <= 0 ? 'diff-early' : 'diff-late' },
diffClass(d) { if (d === null) return ''; return d <= 0 ? 'diff-early' : 'diff-late' },
diffLabel(d) { if (d === 0) return '准时'; if (d < 0) return '提前' + Math.abs(d) + '天'; return '延期' + d + '天' },
todayStr() { const d = new Date(); return d.toISOString().slice(0, 10) },
yesterdayStr() { const d = new Date(); d.setDate(d.getDate() - 1); return d.toISOString().slice(0, 10) },
applyBatchDate() {
if (!this.batchDate || !this.selected.length) return
this.selected.forEach(r => { r._editDate = this.batchDate; this.calcRow(r) })
this.$modal.msgSuccess("已应用到 " + this.selected.length + " 条")
},
batchConfirm() {
if (!this.selected.length) return
if (!this.allHaveDate) { this.$modal.msgError("有订单未设置签收日期"); return }
this.$modal.confirm("确认批量结单 " + this.selected.length + " 条?").then(() => {
const ids = this.selected.map(r => r.doId)
return batchSetCloseDate(ids, this.selected[0]._editDate)
}).then(() => {
this.$modal.msgSuccess("批量结单成功"); this.getList(); this.getStats(); this.selected = []
}).catch(() => {})
}
}
}
</script>
<style scoped>
.jd-cd-page { padding: 12px; min-height: calc(100vh - 84px); }
.stat-row { margin-bottom: 12px !important; }
.stat-card {
background: #fff; border-radius: 6px; padding: 16px; display: flex; align-items: center; justify-content: space-between;
box-shadow: 0 1px 4px rgba(0,0,0,.06); transition: box-shadow .2s; cursor: default;
}
.stat-card:hover { box-shadow: 0 4px 16px rgba(0,0,0,.1); }
.stat-body { flex:1; }
.stat-num { font-size: 26px; font-weight: 700; color: #333333; line-height: 1.2; }
.stat-lbl { font-size: 12px; color: #909399; margin-top: 4px; }
.stat-icon { font-size: 32px; color: #ddd; }
.jd-filter { display: flex; align-items: center; background: #ffffff; padding: 10px 16px; border-radius: 2px; margin-bottom: 12px; flex-wrap: wrap; gap: 8px; }
.filter-left { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.filter-right { margin-left: auto; }
.filter-input { width: 130px; }
.cd-body { display: flex; gap: 12px; align-items: flex-start; }
.cd-left { flex: 1; background: #fff; border-radius: 2px; border: 1px solid #e5e5e5; padding: 14px; }
.left-header { display: flex; align-items: center; margin-bottom: 12px; }
.left-title { font-size: 14px; font-weight: 700; color: #333; }
.jd-table { border: none !important; }
.cd-right { width: 320px; flex-shrink: 0; background: #fff; border-radius: 2px; border: 1px solid #e5e5e5; padding: 16px; }
.right-title { font-size: 14px; font-weight: 700; color: #333; margin-bottom: 16px; padding-bottom: 8px; border-bottom: 2px solid #4A6FA5; }
.right-section { margin-bottom: 20px; }
.rs-header { font-size: 12px; color: #666; margin-bottom: 8px; }
.rs-list { max-height: 150px; overflow-y: auto; border: 1px solid #e5e5e5; border-radius: 2px; padding: 4px; }
.rs-item { padding: 4px 8px; font-size: 12px; color: #333; border-bottom: 1px solid #ffffff; }
.rs-item:last-child { border-bottom: none; }
.rs-empty { text-align: center; padding: 20px; color: #999; font-size: 12px; }
.rs-date-row { display: flex; gap: 6px; margin-bottom: 8px; }
.rs-quick { display: flex; gap: 6px; }
.rs-warn { font-size: 11px; color: #f56c6c; margin-top: 6px; }
.order-link { color: #4A6FA5; cursor: pointer; }
.order-link:hover { color: #4A6FA5; text-decoration: underline; }
::v-deep .selected-row td { background: #f5faff !important; }
.amount { color: #e4393c; font-weight: 700; }
.closed-date { color: #909399; font-size: 12px; }
.diff-early { color: #67c23a; font-weight: 600; }
.diff-late { color: #f56c6c; font-weight: 600; }
</style>

View File

@@ -0,0 +1,369 @@
<template>
<div class="timeline-page">
<!-- 统计卡片 -->
<el-row :gutter="12" class="stat-row">
<el-col :span="6" v-for="c in statCards" :key="c.key">
<div class="stat-card" :style="{borderLeftColor: c.color}">
<div class="stat-num" :style="{color: c.color}">{{ stats[c.key] != null ? stats[c.key] : '-' }}</div>
<div class="stat-lbl">{{ c.label }}</div>
</div>
</el-col>
</el-row>
<!-- 筛选栏 -->
<div class="filter-bar">
<el-select v-model="query.type" placeholder="履约类型" clearable size="small" style="width:120px" @change="loadData">
<el-option label="供应商履约" value="supplier" />
<el-option label="甲方履约" value="client" />
</el-select>
<el-select v-model="query.status" placeholder="订单状态" clearable size="small" style="width:120px" @change="loadData">
<el-option label="待发" value="pending" />
<el-option label="在途" value="transit" />
<el-option label="已签收" value="history" />
<el-option label="已结单" value="closed" />
</el-select>
<el-date-picker v-model="query.dateFrom" type="date" value-format="yyyy-MM-dd" placeholder="开始日期" size="small" style="width:140px" @change="loadData" />
<el-date-picker v-model="query.dateTo" type="date" value-format="yyyy-MM-dd" placeholder="结束日期" size="small" style="width:140px" @change="loadData" />
<el-button type="primary" size="small" icon="el-icon-search" @click="loadData">查询</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetSearch">重置</el-button>
<div class="filter-hint">
<span class="hint-dot" style="background:#67c23a"></span> 提前完成
<span class="hint-dot" style="background:#409eff"></span> 准时完成
<span class="hint-dot" style="background:#e6a23c"></span> 临期完成
<span class="hint-dot" style="background:#f56c6c"></span> 逾期完成
<span class="hint-dot" style="background:#909399"></span> 进行中
</div>
</div>
<!-- 甘特图 -->
<div class="chart-wrap">
<div v-if="loading" class="loading-box"><i class="el-icon-loading"></i> 加载中</div>
<div v-else-if="!orders.length" class="no-data">
{{ query.type === 'supplier' ? '暂无供应商履约订单数据' : query.type === 'client' ? '暂无甲方履约订单数据' : '暂无订单数据' }}
</div>
<div v-else ref="ganttChart" style="width:100%;height:600px"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getTimeline } from "@/api/bid/timeline"
export default {
name: "ClientDeliveryTimeline",
data() {
return {
loading: false,
orders: [],
stats: {},
// query.type 为空时查全部,可选 supplier/client
query: { type: "", status: "", dateFrom: "", dateTo: "" },
chart: null,
statCards: [
{ key: "onTime", label: "按期完成", color: "#67c23a" },
{ key: "delayed", label: "逾期完成", color: "#f56c6c" },
{ key: "pending", label: "待签收/待结单", color: "#e6a23c" },
{ key: "total", label: "订单总数", color: "#409eff" }
]
}
},
mounted() {
this.loadData()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
handleResize() {
if (this.chart) this.chart.resize()
},
loadData() {
this.loading = true
getTimeline(this.query).then(r => {
const data = r.data || {}
this.orders = data.orders || []
this.stats = data.stats || {}
this.stats.total = this.orders.length
this.loading = false
this.$nextTick(() => this.renderChart())
}).catch(() => { this.loading = false })
},
resetSearch() {
this.query = { type: "", status: "", dateFrom: "", dateTo: "" }
this.loadData()
},
// ═══ 计算订单履约状态 ═══
calcStatus(o) {
const base = o.delayDate || o.deliveryDate
if (o.actualCloseDate && base) {
const diff = this.daysBetween(base, o.actualCloseDate)
if (diff < 0) return 'early'
if (diff === 0) return 'onTime'
if (diff <= 3) return 'nearlyLate'
return 'overdue'
}
if (o.actualCloseDate && !base) return 'onTime'
if (base) {
const diff = this.daysBetween(base, this.todayStr())
if (diff < 0) return 'overdue'
if (diff <= 3) return 'nearlyLate'
}
return 'pending'
},
daysBetween(d1, d2) {
if (!d1 || !d2) return 0
return Math.round((new Date(d2) - new Date(d1)) / 86400000)
},
todayStr() {
const d = new Date()
return d.toISOString().slice(0, 10)
},
renderChart() {
if (!this.$refs.ganttChart || !this.orders.length) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(this.$refs.ganttChart, 'macarons')
const raw = this.orders
const barColors = {
early: '#67c23a',
onTime: '#409eff',
nearlyLate: '#e6a23c',
overdue: '#f56c6c',
pending: '#909399'
}
const statusLabels = {
early: '提前完成',
onTime: '准时完成',
nearlyLate: '临期完成',
overdue: '逾期完成',
pending: '进行中'
}
let minDate = Infinity, maxDate = -Infinity
raw.forEach(o => {
if (o.createTime) {
const t = new Date(o.createTime).getTime()
if (t < minDate) minDate = t
}
if (o.actualCloseDate) {
const t = new Date(o.actualCloseDate).getTime()
if (t > maxDate) maxDate = t
} else if (o.deliveryDate) {
const t = new Date(o.deliveryDate).getTime() + 86400000 * 7
if (t > maxDate) maxDate = t
}
})
if (!isFinite(minDate)) minDate = Date.now() - 86400000 * 30
if (maxDate < 0) maxDate = Date.now() + 86400000 * 7
minDate -= 86400000 * 2
maxDate += 86400000 * 3
const yNames = raw.map(o => o.doNo)
const ganttItems = raw.map((o, idx) => {
const startDate = o.createTime ? new Date(o.createTime).getTime() : minDate
const endDate = o.actualCloseDate
? new Date(o.actualCloseDate).getTime()
: Math.min(maxDate, Date.now())
const st = this.calcStatus(o)
return {
value: [startDate, endDate, idx],
itemStyle: { color: barColors[st] },
status: st,
order: o
}
})
const milestoneData = []
raw.forEach((o, idx) => {
if (o.deliveryDate) {
milestoneData.push({
coord: [new Date(o.deliveryDate).getTime(), idx],
symbol: 'diamond',
color: '#333',
label: '约定',
order: o
})
}
if (o.delayDate) {
milestoneData.push({
coord: [new Date(o.delayDate).getTime(), idx],
symbol: 'triangle',
color: '#e6a23c',
label: '延期',
order: o
})
}
if (o.actualCloseDate) {
milestoneData.push({
coord: [new Date(o.actualCloseDate).getTime(), idx],
symbol: 'circle',
color: '#f56c6c',
label: '签收',
order: o
})
}
})
const self = this
const option = {
tooltip: {
trigger: 'item',
formatter: function(params) {
const o = params.data && params.data.order ? params.data.order : raw[params.dataIndex]
if (!o) return ''
const st = self.calcStatus(o)
const base = o.delayDate || o.deliveryDate
let cycleInfo = ''
if (base && o.actualCloseDate) {
const diff = self.daysBetween(base, o.actualCloseDate)
cycleInfo = `<div>履约周期: ${diff}天 (${diff <= 0 ? '提前' + Math.abs(diff) + '天' : '延期' + diff + '天'})</div>`
}
return `
<div style="font-weight:700;font-size:14px;margin-bottom:4px">${o.doNo}</div>
<div>类型: ${o.type === 'client' ? '甲方履约' : '供应商履约'}</div>
<div>${o.type === 'client' ? '甲方客户' : '供应商'}: ${o.partyName || '-'}</div>
<div>金额: ¥${o.totalAmount || 0}</div>
<div>状态: ${o.deliveryStatus} (${statusLabels[st]})</div>
<hr style="margin:4px 0;border:none;border-top:1px solid #eee"/>
<div>创建: ${(o.createTime || '-').substring(0, 16)}</div>
<div>约定交货: ${o.deliveryDate || '-'}</div>
<div>延期至: ${o.delayDate || '-'}</div>
<div>签收/结单: ${o.actualCloseDate || '-'}</div>
${cycleInfo}
<div>物料数: ${o.itemCount || 0}</div>
`
}
},
legend: {
show: true,
top: 0,
right: 20,
data: [
{ name: '提前完成', itemStyle: { color: barColors.early } },
{ name: '准时完成', itemStyle: { color: barColors.onTime } },
{ name: '临期完成', itemStyle: { color: barColors.nearlyLate } },
{ name: '逾期完成', itemStyle: { color: barColors.overdue } },
{ name: '进行中', itemStyle: { color: barColors.pending } }
]
},
grid: {
left: 140,
right: 60,
top: 40,
bottom: 50
},
xAxis: {
type: 'time',
min: minDate,
max: maxDate,
axisLabel: {
formatter: '{MM}/{dd}',
fontSize: 11
},
splitLine: { show: true, lineStyle: { type: 'dashed', color: '#eee' } }
},
yAxis: {
type: 'category',
data: yNames,
axisLabel: {
fontSize: 11,
width: 130,
overflow: 'truncate'
},
axisTick: { show: false }
},
series: [
{
name: '履约周期',
type: 'custom',
renderItem: function(params, api) {
const start = api.coord([api.value(0), api.value(2)])
const end = api.coord([api.value(1), api.value(2)])
const height = api.size([0, 1])[1] * 0.5
const status = ganttItems[api.value(2)].status
return {
type: 'rect',
shape: {
x: start[0],
y: start[1] - height / 2,
width: Math.max(end[0] - start[0], 2),
height: height
},
style: {
fill: barColors[status],
opacity: 0.75,
stroke: '#fff',
lineWidth: 1
}
}
},
encode: { x: [0, 1], y: 2 },
data: ganttItems
},
{
name: '关键节点',
type: 'scatter',
symbolSize: 10,
data: milestoneData.map(m => ({
value: m.coord,
itemStyle: { color: m.color, borderColor: '#fff', borderWidth: 1.5 },
symbol: m.symbol,
order: m.order
})),
z: 10
}
]
}
this.chart.setOption(option)
// 点击跳转到对应订单详情页
this.chart.on('click', params => {
const o = params.data && params.data.order
if (!o) return
if (o.type === 'client') {
const path = o.deliveryStatus === 'pending' ? '/bid/clientDelivery/pending'
: o.deliveryStatus === 'transit' ? '/bid/clientDelivery/transit'
: '/bid/clientDelivery/signed'
this.$router.push({ path, query: { doNo: o.doNo } })
} else {
const path = o.deliveryStatus === 'pending' ? '/bid/order/pending'
: o.deliveryStatus === 'transit' ? '/bid/order/transit'
: '/bid/order/history'
this.$router.push({ path, query: { doNo: o.doNo } })
}
})
}
}
}
</script>
<style scoped>
.timeline-page { padding: 12px; min-height: calc(100vh - 84px); }
.stat-row { margin-bottom: 12px !important; }
.stat-card {
background: #fff; border-radius: 6px; padding: 16px 20px;
border-left: 4px solid #e4393c; box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.stat-num { font-size: 28px; font-weight: 700; line-height: 1.2; }
.stat-lbl { font-size: 12px; color: #909399; margin-top: 4px; }
.filter-bar {
background: #fff; padding: 10px 16px; border-radius: 2px; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.filter-hint { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 12px; color: #909399; }
.hint-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-left: 4px; }
.chart-wrap {
background: #fff; border-radius: 2px; border: 1px solid #e5e5e5; padding: 16px;
}
.loading-box, .no-data { text-align: center; padding: 60px; color: #909399; font-size: 14px; }
.loading-box i { font-size: 20px; margin-right: 6px; }
</style>

View File

@@ -60,6 +60,12 @@
<el-col :span="1.5">
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport" v-hasPermi="['bid:material:import']">导入</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['bid:material:export']">导出</el-button>
</el-col>
<el-col :span="1.5" v-if="currentCategoryName">
<el-tag size="medium" closable @close="clearCategoryFilter" type="warning">
当前分类: {{ currentCategoryName }}
@@ -303,6 +309,16 @@
<el-button type="primary" @click="submitCategoryForm">确定</el-button>
</div>
</el-dialog>
<!-- Excel 导入对话框 -->
<excel-import-dialog
ref="importRef"
title="物料导入"
action="/bid/material/importData"
template-action="/bid/material/importTemplate"
template-file-name="material_template"
update-support-label="是否更新已经存在的物料数据"
@success="getList" />
</div>
</template>
@@ -310,9 +326,11 @@
import { listMaterial, getMaterial, addMaterial, updateMaterial, delMaterial, listManufacturer } from "@/api/bid/material";
import { getCategoryList, addCategory, updateCategory, delCategory } from "@/api/bid/category";
import request from '@/utils/request'
import ExcelImportDialog from "@/components/ExcelImportDialog"
export default {
name: "Material",
components: { ExcelImportDialog },
data() {
return {
loading: false, multiple: true, total: 0, materialList: [],
@@ -409,6 +427,10 @@ export default {
handleAdd() {
this.reset();
this.perfParams = [];
// 自动带入当前选中的分类
if (this.queryParams.categoryId) {
this.form.categoryId = this.queryParams.categoryId;
}
this.open = true;
this.title = "新增物料";
},
@@ -428,6 +450,13 @@ export default {
const ids = row.materialId || (this.ids || []).join(",");
this.$modal.confirm("确认删除?").then(() => delMaterial(ids)).then(() => { this.getList(); this.$modal.msgSuccess("删除成功"); });
},
// ═══ Excel 导入导出 ═══
handleImport() {
this.$refs.importRef.open();
},
handleExport() {
this.download('/bid/material/export', { ...this.queryParams }, `material_${new Date().getTime()}.xlsx`);
},
handleStatusChange(row) { updateMaterial(row); },
// 性能参数
addPerfRow() {

View File

@@ -15,9 +15,8 @@
<div class="filter-left">
<el-input v-model="q.doNo" placeholder="搜索单号" clearable size="small" class="filter-input" @keyup.enter.native="handleSearch" />
<el-select v-model="q.deliveryStatus" placeholder="状态" clearable size="small" style="width:100px" @change="getList">
<el-option label="待发" value="pending" />
<el-option label="在途" value="transit" />
<el-option label="历史" value="history" />
<el-option label="已签收(待结单)" value="history" />
<el-option label="已结单" value="closed" />
</el-select>
<el-button type="primary" size="small" @click="handleSearch">搜索</el-button>
<el-button size="small" @click="resetSearch">重置</el-button>
@@ -31,7 +30,7 @@
<!-- 左侧列表 -->
<div class="cd-left">
<div class="left-header">
<span class="left-title">订单列表</span>
<span class="left-title">已签收待结单</span>
</div>
<el-table ref="table" v-loading="loading" :data="list" border stripe size="small"
@selection-change="onSelectionChange" class="jd-table"
@@ -101,7 +100,7 @@
</template>
<script>
import { listDelivery, setCloseDate } from "@/api/bid/delivery"
import { listDelivery, setCloseDate, batchSetCloseDate } from "@/api/bid/delivery"
import request from '@/utils/request'
export default {
@@ -111,7 +110,7 @@ export default {
loading: false, list: [], total: 0, stats: {},
selected: [],
batchDate: null,
q: { pageNum: 1, pageSize: 50, doNo: "", deliveryStatus: "" },
q: { pageNum: 1, pageSize: 50, doNo: "", deliveryStatus: "history" },
statCards: [
{ key: "pendingClose", label: "已收货未结单", icon: "el-icon-document", color: "#e4393c" },
{ key: "todayClosed", label: "今日结单", icon: "el-icon-circle-check", color: "#67c23a" },
@@ -150,7 +149,9 @@ export default {
calcRow(row) {
if (!row.deliveryDate || !row._editDate) { row._cycleDays = null; row._diffDays = null; return }
const cd = new Date(row._editDate)
const dd = new Date(row.deliveryDate)
// 履约周期以约定交货日为起点,存在延期日期时以延期日期替代计算差异
const baseDate = row.delayDate || row.deliveryDate
const dd = new Date(baseDate)
row._cycleDays = Math.round((cd - dd) / 86400000)
row._diffDays = row._cycleDays
},
@@ -169,8 +170,8 @@ export default {
if (!this.selected.length) return
if (!this.allHaveDate) { this.$modal.msgError("有订单未设置收货日期"); return }
this.$modal.confirm("确认批量结单 " + this.selected.length + " 条?").then(() => {
const promises = this.selected.map(r => setCloseDate(r.doId, r._editDate))
return Promise.all(promises)
const ids = this.selected.map(r => r.doId)
return batchSetCloseDate(ids, this.selected[0]._editDate)
}).then(() => {
this.$modal.msgSuccess("批量结单成功"); this.getList(); this.getStats(); this.selected = []
}).catch(() => {})

View File

@@ -0,0 +1,387 @@
<template>
<div class="timeline-page">
<!-- 统计卡片 -->
<el-row :gutter="12" class="stat-row">
<el-col :span="8" v-for="c in statCards" :key="c.key">
<div class="stat-card" :style="{borderLeftColor: c.color}">
<div class="stat-num" :style="{color: c.color}">{{ stats[c.key] != null ? stats[c.key] : '-' }}</div>
<div class="stat-lbl">{{ c.label }}</div>
</div>
</el-col>
</el-row>
<!-- 筛选栏 -->
<div class="filter-bar">
<el-select v-model="query.type" placeholder="全部类型" clearable size="small" style="width:130px" @change="loadData">
<el-option label="全部类型" value="" />
<el-option label="供应商履约" value="supplier" />
<el-option label="甲方履约" value="client" />
</el-select>
<el-select v-model="query.status" placeholder="全部状态" clearable size="small" style="width:120px" @change="loadData">
<el-option label="全部状态" value="" />
<el-option label="待发" value="pending" />
<el-option label="在途" value="transit" />
<el-option label="已签收" value="history" />
<el-option label="已结单" value="closed" />
</el-select>
<el-date-picker v-model="query.dateFrom" type="date" value-format="yyyy-MM-dd" placeholder="开始日期" size="small" style="width:140px" @change="loadData" />
<el-date-picker v-model="query.dateTo" type="date" value-format="yyyy-MM-dd" placeholder="结束日期" size="small" style="width:140px" @change="loadData" />
<el-button type="primary" size="small" icon="el-icon-search" @click="loadData">查询</el-button>
<el-button size="small" icon="el-icon-refresh" @click="resetSearch">重置</el-button>
<div class="filter-hint">
<span class="hint-dot" style="background:#67c23a"></span> 提前完成
<span class="hint-dot" style="background:#409eff"></span> 准时完成
<span class="hint-dot" style="background:#e6a23c"></span> 临期完成
<span class="hint-dot" style="background:#f56c6c"></span> 逾期完成
<span class="hint-dot" style="background:#909399"></span> 进行中
</div>
</div>
<!-- 甘特图 -->
<div class="chart-wrap">
<div v-if="loading" class="loading-box"><i class="el-icon-loading"></i> 加载中</div>
<div v-else-if="!orders.length" class="no-data">暂无订单数据</div>
<div v-else ref="ganttChart" style="width:100%;height:600px"></div>
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
import { getTimeline } from "@/api/bid/timeline"
export default {
name: "OrderTimeline",
data() {
return {
loading: false,
orders: [],
stats: {},
query: { type: "", status: "", dateFrom: "", dateTo: "" },
chart: null,
statCards: [
{ key: "onTime", label: "按期完成", color: "#67c23a" },
{ key: "delayed", label: "逾期完成", color: "#f56c6c" },
{ key: "pending", label: "进行中/待结单", color: "#e6a23c" }
]
}
},
mounted() {
this.loadData()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
if (this.chart) { this.chart.dispose(); this.chart = null }
},
methods: {
handleResize() {
if (this.chart) this.chart.resize()
},
loadData() {
this.loading = true
getTimeline(this.query).then(r => {
const data = r.data || {}
this.orders = data.orders || []
this.stats = data.stats || {}
this.loading = false
this.$nextTick(() => this.renderChart())
}).catch(() => { this.loading = false })
},
resetSearch() {
this.query = { type: "", status: "", dateFrom: "", dateTo: "" }
this.loadData()
},
// ═══ 计算订单履约状态 ═══
calcStatus(o) {
const base = o.delayDate || o.deliveryDate
if (o.actualCloseDate && base) {
const diff = this.daysBetween(base, o.actualCloseDate)
if (diff < 0) return 'early' // 提前完成
if (diff === 0) return 'onTime' // 准时完成
if (diff <= 3) return 'nearlyLate' // 临期完成延期1-3天
return 'overdue' // 逾期完成(延期>3天
}
if (o.actualCloseDate && !base) return 'onTime'
// 未结单:看是否已过交货期
if (base) {
const diff = this.daysBetween(base, this.todayStr())
if (diff < 0) return 'overdue' // 已逾期未结单
if (diff <= 3) return 'nearlyLate' // 临期
}
return 'pending'
},
daysBetween(d1, d2) {
if (!d1 || !d2) return 0
return Math.round((new Date(d2) - new Date(d1)) / 86400000)
},
todayStr() {
const d = new Date()
return d.toISOString().slice(0, 10)
},
renderChart() {
if (!this.$refs.ganttChart || !this.orders.length) return
if (this.chart) this.chart.dispose()
this.chart = echarts.init(this.$refs.ganttChart, 'macarons')
const raw = this.orders
const barColors = {
early: '#67c23a',
onTime: '#409eff',
nearlyLate: '#e6a23c',
overdue: '#f56c6c',
pending: '#909399'
}
const statusLabels = {
early: '提前完成',
onTime: '准时完成',
nearlyLate: '临期完成',
overdue: '逾期完成',
pending: '进行中'
}
// 计算全局时间范围
let minDate = Infinity, maxDate = -Infinity
raw.forEach(o => {
if (o.createTime) {
const t = new Date(o.createTime).getTime()
if (t < minDate) minDate = t
}
if (o.actualCloseDate) {
const t = new Date(o.actualCloseDate).getTime()
if (t > maxDate) maxDate = t
} else if (o.deliveryDate) {
// 未结单的以交货期+7天为终点展示
const t = new Date(o.deliveryDate).getTime() + 86400000 * 7
if (t > maxDate) maxDate = t
}
})
if (!isFinite(minDate)) minDate = Date.now() - 86400000 * 30
if (maxDate < 0) maxDate = Date.now() + 86400000 * 7
minDate -= 86400000 * 2
maxDate += 86400000 * 3
const yNames = raw.map(o => o.doNo)
// ═══ 使用 custom series 绘制甘特条 ═══
// 每个订单一个条:从创建时间到结单时间(或当前时间)
const ganttItems = raw.map((o, idx) => {
const startDate = o.createTime ? new Date(o.createTime).getTime() : minDate
const endDate = o.actualCloseDate
? new Date(o.actualCloseDate).getTime()
: Math.min(maxDate, Date.now())
const st = this.calcStatus(o)
return {
value: [startDate, endDate, idx],
itemStyle: { color: barColors[st] },
status: st,
order: o
}
})
// ═══ 关键节点标记数据(约定交货/延期/签收) ═══
const milestoneData = []
raw.forEach((o, idx) => {
if (o.deliveryDate) {
milestoneData.push({
coord: [new Date(o.deliveryDate).getTime(), idx],
symbol: 'diamond',
color: '#333',
label: '约定',
order: o
})
}
if (o.delayDate) {
milestoneData.push({
coord: [new Date(o.delayDate).getTime(), idx],
symbol: 'triangle',
color: '#e6a23c',
label: '延期',
order: o
})
}
if (o.actualCloseDate) {
milestoneData.push({
coord: [new Date(o.actualCloseDate).getTime(), idx],
symbol: 'circle',
color: '#f56c6c',
label: '签收',
order: o
})
}
})
const self = this
const option = {
tooltip: {
trigger: 'item',
formatter: function(params) {
const o = params.data && params.data.order ? params.data.order : raw[params.dataIndex]
if (!o) return ''
const st = self.calcStatus(o)
const base = o.delayDate || o.deliveryDate
let cycleInfo = ''
if (base && o.actualCloseDate) {
const diff = self.daysBetween(base, o.actualCloseDate)
cycleInfo = `<div>履约周期: ${diff}天 (${diff <= 0 ? '提前' + Math.abs(diff) + '天' : '延期' + diff + '天'})</div>`
}
return `
<div style="font-weight:700;font-size:14px;margin-bottom:4px">${o.doNo}</div>
<div>类型: ${o.type === 'client' ? '甲方履约' : '供应商履约'}</div>
<div>对方: ${o.partyName || '-'}</div>
<div>金额: ¥${o.totalAmount || 0}</div>
<div>状态: ${o.deliveryStatus} (${statusLabels[st]})</div>
<hr style="margin:4px 0;border:none;border-top:1px solid #eee"/>
<div>创建: ${(o.createTime || '-').substring(0, 16)}</div>
<div>约定交货: ${o.deliveryDate || '-'}</div>
<div>延期至: ${o.delayDate || '-'}</div>
<div>签收/结单: ${o.actualCloseDate || '-'}</div>
${cycleInfo}
<div>物料数: ${o.itemCount || 0}</div>
`
}
},
legend: {
show: true,
top: 0,
right: 20,
data: [
{ name: '提前完成', itemStyle: { color: barColors.early } },
{ name: '准时完成', itemStyle: { color: barColors.onTime } },
{ name: '临期完成', itemStyle: { color: barColors.nearlyLate } },
{ name: '逾期完成', itemStyle: { color: barColors.overdue } },
{ name: '进行中', itemStyle: { color: barColors.pending } }
]
},
grid: {
left: 140,
right: 60,
top: 40,
bottom: 50
},
xAxis: {
type: 'time',
min: minDate,
max: maxDate,
axisLabel: {
formatter: '{MM}/{dd}',
fontSize: 11
},
splitLine: { show: true, lineStyle: { type: 'dashed', color: '#eee' } }
},
yAxis: {
type: 'category',
data: yNames,
axisLabel: {
fontSize: 11,
width: 130,
overflow: 'truncate'
},
axisTick: { show: false }
},
series: [
// ═══ 甘特条custom series 绘制矩形) ═══
{
name: '履约周期',
type: 'custom',
renderItem: function(params, api) {
const start = api.coord([api.value(0), api.value(2)])
const end = api.coord([api.value(1), api.value(2)])
const height = api.size([0, 1])[1] * 0.5
const status = ganttItems[api.value(2)].status
return {
type: 'rect',
shape: {
x: start[0],
y: start[1] - height / 2,
width: Math.max(end[0] - start[0], 2),
height: height
},
style: {
fill: barColors[status],
opacity: 0.75,
stroke: '#fff',
lineWidth: 1
}
}
},
encode: { x: [0, 1], y: 2 },
data: ganttItems
},
// ═══ 关键节点标记 ═══
{
name: '关键节点',
type: 'scatter',
symbolSize: 10,
data: milestoneData.map(m => ({
value: m.coord,
itemStyle: { color: m.color, borderColor: '#fff', borderWidth: 1.5 },
symbol: m.symbol,
order: m.order
})),
z: 10
}
]
}
this.chart.setOption(option)
// 点击跳转到对应详情self 已在上方定义)
this.chart.on('click', function(params) {
// custom series: params.value = [startDate, endDate, idx]
// scatter series: params.data.order 或 params.value = [date, idx]
let o = null
if (params.data && params.data.order) {
o = params.data.order
} else if (params.value && params.value.length >= 3) {
const idx = Math.round(params.value[2])
o = self.orders[idx]
} else if (params.value && params.value.length >= 2) {
// scatter的坐标: [date, idx]
const idx = Math.round(params.value[1])
o = self.orders[idx]
}
if (!o) return
// 根据类型和状态跳转
if (o.type === 'client') {
const path = o.deliveryStatus === 'pending' ? '/bid/clientDelivery/pending'
: o.deliveryStatus === 'transit' ? '/bid/clientDelivery/transit'
: '/bid/clientDelivery/signed'
self.$router.push({ path, query: { doNo: o.doNo } })
} else {
const path = o.deliveryStatus === 'pending' ? '/bid/order/pending'
: o.deliveryStatus === 'transit' ? '/bid/order/transit'
: '/bid/order/history'
self.$router.push({ path, query: { doNo: o.doNo } })
}
})
}
}
}
</script>
<style scoped>
.timeline-page { padding: 12px; min-height: calc(100vh - 84px); }
.stat-row { margin-bottom: 12px !important; }
.stat-card {
background: #fff; border-radius: 6px; padding: 16px 20px;
border-left: 4px solid #e4393c; box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
.stat-num { font-size: 28px; font-weight: 700; line-height: 1.2; }
.stat-lbl { font-size: 12px; color: #909399; margin-top: 4px; }
.filter-bar {
background: #fff; padding: 10px 16px; border-radius: 2px; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.filter-hint { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 12px; color: #909399; }
.hint-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-left: 4px; }
.chart-wrap {
background: #fff; border-radius: 2px; border: 1px solid #e5e5e5; padding: 16px;
}
.loading-box, .no-data { text-align: center; padding: 60px; color: #909399; font-size: 14px; }
.loading-box i { font-size: 20px; margin-right: 6px; }
</style>

View File

@@ -16,6 +16,8 @@
/>
<el-button type="primary" size="small" icon="el-icon-search" @click="handleSearch" />
<el-button type="primary" size="small" icon="el-icon-plus" @click="handleAdd" />
<el-button type="info" size="small" icon="el-icon-upload2" @click="handleImport" v-hasPermi="['bid:supplier:import']" />
<el-button type="warning" size="small" icon="el-icon-download" @click="handleExport" v-hasPermi="['bid:supplier:export']" />
</div>
</div>
@@ -297,6 +299,16 @@
<el-button type="primary" @click="submitAdd">确定</el-button>
</div>
</el-dialog>
<!-- Excel 导入对话框 -->
<excel-import-dialog
ref="importRef"
title="供应商导入"
action="/bid/supplier/importData"
template-action="/bid/supplier/importTemplate"
template-file-name="supplier_template"
update-support-label="是否更新已经存在的供应商数据"
@success="getList" />
</div>
</template>
@@ -304,9 +316,11 @@
import { listSupplier, getSupplier, addSupplier, updateSupplier, delSupplier } from "@/api/bid/supplier";
import { listObjection } from "@/api/bid/objection";
import { getSupplierQuoteItems } from "@/api/bid/quotation";
import ExcelImportDialog from "@/components/ExcelImportDialog"
export default {
name: "SupplierManage",
components: { ExcelImportDialog },
data() {
return {
// ---- 左侧列表 ----
@@ -389,6 +403,10 @@ export default {
this.getList();
},
// ═══ Excel 导入导出 ═══
handleImport() { this.$refs.importRef.open(); },
handleExport() { this.download('/bid/supplier/export', { ...this.queryParams }, `supplier_${new Date().getTime()}.xlsx`); },
handleSizeChange(size) {
this.queryParams.pageSize = size;
this.queryParams.pageNum = 1;

View File

@@ -0,0 +1,17 @@
-- ═══════════════════════════════════════════════════════════
-- 甲方履约 - 结单时间管理 菜单与权限
-- ═══════════════════════════════════════════════════════════
SET NAMES utf8mb4;
-- 1. 创建菜单
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2044, '甲方结单管理', 2040, 4, 'closeDate', 'bid/clientDelivery/closeDate', 1, 0, 'C', '0', '0', 'bid:clientdelivery:closeDate', 'date', 'admin', NOW());
-- 2. 给 admin 角色授权
INSERT IGNORE INTO sys_role_menu(role_id, menu_id) VALUES(1, 2044);
-- 3. 按钮权限
INSERT IGNORE INTO sys_menu(menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
SELECT '结单确认', menu_id, 1, '#', NULL, 1, 0, 'F', '0', '0', 'bid:clientdelivery:closeDate:edit', '#', 'admin', NOW()
FROM sys_menu WHERE perms = 'bid:clientdelivery:closeDate' AND menu_id = 2044;

42
sql/fix_menu_import.sql Normal file
View File

@@ -0,0 +1,42 @@
-- ═══════════════════════════════════════════════════════════
-- Excel 批量导入功能 - 权限菜单初始化
-- 物料/客户/供应商 三模块的导入导出按钮权限
-- ═══════════════════════════════════════════════════════════
SET NAMES utf8mb4;
-- 清理可能存在的旧数据
DELETE FROM sys_role_menu WHERE menu_id IN (2140, 2141, 2142, 2143, 2144, 2145);
DELETE FROM sys_menu WHERE menu_id IN (2140, 2141, 2142, 2143, 2144, 2145);
-- 1. 物料导入按钮 (parent=2001 物料管理)
INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2140, UNHEX('E789A9E69699E5AFBCE585A5'), 2001, 7, '', NULL, 1, 0, 'F', '0', '0', 'bid:material:import', '#', 'admin', NOW());
-- 2. 物料导出按钮
INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2141, UNHEX('E789A9E69699E5AFBCE587BA'), 2001, 8, '', NULL, 1, 0, 'F', '0', '0', 'bid:material:export', '#', 'admin', NOW());
-- 3. 客户导入按钮 (parent=2028 甲方客户管理)
INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2142, UNHEX('E5AEA2E688B7E5AFBCE585A5'), 2028, 10, '', NULL, 1, 0, 'F', '0', '0', 'bid:client:import', '#', 'admin', NOW());
-- 4. 客户导出按钮
INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2143, UNHEX('E5AEA2E688B7E5AFBCE587BA'), 2028, 11, '', NULL, 1, 0, 'F', '0', '0', 'bid:client:export', '#', 'admin', NOW());
-- 5. 供应商导入按钮 (parent=2002 供应商管理)
INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2144, UNHEX('E4BE9BE5BA94E59586E5AFBCE585A5'), 2002, 7, '', NULL, 1, 0, 'F', '0', '0', 'bid:supplier:import', '#', 'admin', NOW());
-- 6. 供应商导出按钮
INSERT INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2145, UNHEX('E4BE9BE5BA94E59586E5AFBCE587BA'), 2002, 8, '', NULL, 1, 0, 'F', '0', '0', 'bid:supplier:export', '#', 'admin', NOW());
-- 7. 给 admin 角色授权
INSERT INTO sys_role_menu(role_id, menu_id) VALUES(1, 2140);
INSERT INTO sys_role_menu(role_id, menu_id) VALUES(1, 2141);
INSERT INTO sys_role_menu(role_id, menu_id) VALUES(1, 2142);
INSERT INTO sys_role_menu(role_id, menu_id) VALUES(1, 2143);
INSERT INTO sys_role_menu(role_id, menu_id) VALUES(1, 2144);
INSERT INTO sys_role_menu(role_id, menu_id) VALUES(1, 2145);

17
sql/fix_menu_timeline.sql Normal file
View File

@@ -0,0 +1,17 @@
-- ═══════════════════════════════════════════════════════════
-- 履约时效总览 菜单(同时覆盖供应商履约和甲方履约的甘特图)
-- 挂在订单履约(2120)下,与供应商履约、甲方履约同级
-- ═══════════════════════════════════════════════════════════
SET NAMES utf8mb4;
-- 1. 删除旧的子菜单2122挂在2121下2123挂在2040下
DELETE FROM sys_role_menu WHERE menu_id IN (2122, 2123);
DELETE FROM sys_menu WHERE menu_id IN (2122, 2123);
-- 2. 新增统一菜单,挂在订单履约(2120)下排序3
INSERT IGNORE INTO sys_menu(menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time)
VALUES(2124, '履约时效总览', 2120, 3, 'timeline', 'bid/order/timeline', 1, 0, 'C', '0', '0', 'bid:order:timeline', 'chart', 'admin', NOW());
-- 3. 给 admin 角色授权
INSERT IGNORE INTO sys_role_menu(role_id, menu_id) VALUES(1, 2124);