feat: 完成履约管理模块全量功能迭代
本次迭代包含以下核心功能: 1. 新增履约时效总览可视化页面,支持多维度数据统计 2. 实现物料/客户/供应商的Excel批量导入导出功能 3. 新增订单批量结单功能,优化结单流程校验 4. 完善日志配置,新增文件日志落地 5. 修复分类查询逻辑,优化多租户数据隔离 6. 新增甲方履约结单管理页面与权限控制 7. 重构部分Mapper与Service接口,增强代码健壮性
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')")
|
||||
|
||||
@@ -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, "供应商数据");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()) < 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 >= #{dateFrom}</if>
|
||||
<if test="dateTo != null and dateTo != ''"> AND d.delivery_date <= #{dateTo}</if>
|
||||
</where>
|
||||
ORDER BY d.create_time DESC
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 } })
|
||||
|
||||
5
ruoyi-ui/src/api/bid/timeline.js
Normal file
5
ruoyi-ui/src/api/bid/timeline.js
Normal 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 })
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
234
ruoyi-ui/src/views/bid/clientDelivery/closeDate.vue
Normal file
234
ruoyi-ui/src/views/bid/clientDelivery/closeDate.vue
Normal 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>
|
||||
369
ruoyi-ui/src/views/bid/clientDelivery/timeline.vue
Normal file
369
ruoyi-ui/src/views/bid/clientDelivery/timeline.vue
Normal 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>
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(() => {})
|
||||
|
||||
387
ruoyi-ui/src/views/bid/order/timeline.vue
Normal file
387
ruoyi-ui/src/views/bid/order/timeline.vue
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
17
sql/fix_menu_client_close_date.sql
Normal file
17
sql/fix_menu_client_close_date.sql
Normal 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
42
sql/fix_menu_import.sql
Normal 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
17
sql/fix_menu_timeline.sql
Normal 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);
|
||||
Reference in New Issue
Block a user