feat: 完成采购看板与异议管理模块功能升级

1.  新增报表下钻跳转支持,为供应商、RFQ、采购单、物料等页面添加路由参数解析
2.  优化异议管理模块:新增发货单关联、详情弹窗、审批流程优化
3.  完善采购看板功能:支持累计数据展示、图表导出、数据补全与趋势优化
4.  新增供应商评分历史趋势统计与品类分布聚合逻辑
5.  修复异议API路径与通知跳转路径问题,新增模拟测试数据
This commit is contained in:
2026-06-21 12:40:59 +08:00
parent 8bdb8d7c23
commit 896999dfeb
26 changed files with 1129 additions and 108 deletions

View File

@@ -8,13 +8,19 @@ 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.system.domain.bid.BizApprovalConfig;
import com.ruoyi.system.domain.bid.BizNotifyMessage;
import com.ruoyi.system.domain.bid.BizOrderObjection;
import com.ruoyi.system.service.bid.IBizApprovalConfigService;
import com.ruoyi.system.service.bid.IBizNotifyMessageService;
import com.ruoyi.system.service.bid.IBizOrderObjectionService;
@RestController
@RequestMapping("/bid/objection")
public class BizOrderObjectionController extends BaseController {
@Autowired private IBizOrderObjectionService service;
@Autowired private IBizNotifyMessageService notifyMessageService;
@Autowired private IBizApprovalConfigService approvalConfigService;
@PreAuthorize("@ss.hasPermi('bid:objection:list')")
@GetMapping("/list")
@@ -32,15 +38,58 @@ public class BizOrderObjectionController extends BaseController {
@Log(title = "订单异议", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody BizOrderObjection record) {
Long tenantId = getDeptId();
if (tenantId == null) tenantId = 1L;
record.setTenantId(tenantId);
record.setCreateBy(getUsername());
return toAjax(service.insertBizOrderObjection(record));
int rows = service.insertBizOrderObjection(record);
// 通知审批人有新异议待处理
if (rows > 0 && record.getObjectionId() != null) {
sendNewObjectionNotification(record);
}
return toAjax(rows);
}
/**
* 新建异议后通知审批人
*/
private void sendNewObjectionNotification(BizOrderObjection record) {
try {
BizApprovalConfig config = approvalConfigService.selectByBizType("ORDER_OBJECTION");
if (config == null || config.getUserIds() == null || config.getUserIds().isEmpty()) return;
String title = "新异议待处理: 订单异议 " + (record.getDoId() != null ? "发货单#" + record.getDoId() : "");
StringBuilder content = new StringBuilder();
content.append("有新的订单异议提交,等待处理。");
if (record.getSupplierId() != null) content.append("供应商ID: ").append(record.getSupplierId()).append("");
if (record.getReason() != null) content.append("异议原因: ").append(record.getReason()).append("");
content.append("提交人: ").append(getUsername());
for (Long approverUserId : config.getUserIds()) {
if (approverUserId == null) continue;
BizNotifyMessage msg = new BizNotifyMessage();
msg.setUserId(approverUserId);
msg.setNoticeType("approval");
msg.setPriority(1);
msg.setBizType("ORDER_OBJECTION");
msg.setBizId(record.getObjectionId());
msg.setBizUrl("/fulfill/supplierFulfill/objection?id=" + record.getObjectionId());
msg.setCreateBy(getUsername());
msg.setTitle(title);
msg.setContent(content.toString());
msg.setIsRead("0");
notifyMessageService.insertNotifyMessage(msg);
}
} catch (Exception e) {
// 通知发送失败不影响业务操作
}
}
@PreAuthorize("@ss.hasPermi('bid:objection:edit')")
@Log(title = "处理异议", businessType = BusinessType.UPDATE)
@PutMapping("/resolve")
public AjaxResult resolve(@RequestBody BizOrderObjection record) {
record.setStatus("resolved");
// 只保存处理结果,不直接修改状态,状态由审批流程控制
return toAjax(service.updateBizOrderObjection(record));
}

View File

@@ -7,6 +7,7 @@ public class BizOrderObjection {
private Long objectionId;
private Long tenantId;
private Long poId;
private Long doId;
private Long supplierId;
private String reason;
private String attachment;
@@ -19,6 +20,7 @@ public class BizOrderObjection {
private Date resolveTime;
private String supplierName;
private String poNo;
private String doNo;
public Long getObjectionId() { return objectionId; }
public void setObjectionId(Long objectionId) { this.objectionId = objectionId; }
@@ -26,6 +28,8 @@ public class BizOrderObjection {
public void setTenantId(Long tenantId) { this.tenantId = tenantId; }
public Long getPoId() { return poId; }
public void setPoId(Long poId) { this.poId = poId; }
public Long getDoId() { return doId; }
public void setDoId(Long doId) { this.doId = doId; }
public Long getSupplierId() { return supplierId; }
public void setSupplierId(Long supplierId) { this.supplierId = supplierId; }
public String getReason() { return reason; }
@@ -46,4 +50,6 @@ public class BizOrderObjection {
public void setSupplierName(String supplierName) { this.supplierName = supplierName; }
public String getPoNo() { return poNo; }
public void setPoNo(String poNo) { this.poNo = poNo; }
public String getDoNo() { return doNo; }
public void setDoNo(String doNo) { this.doNo = doNo; }
}

View File

@@ -59,6 +59,7 @@ public class ReportCostVO {
private BigDecimal expectedAmount;
private BigDecimal actualAmount;
private BigDecimal savedAmount;
private boolean overBudget;
public String getMonth() { return month; }
public void setMonth(String v) { month = v; }
@@ -68,15 +69,20 @@ public class ReportCostVO {
public void setActualAmount(BigDecimal v) { actualAmount = v; }
public BigDecimal getSavedAmount() { return savedAmount; }
public void setSavedAmount(BigDecimal v) { savedAmount = v; }
public boolean isOverBudget() { return overBudget; }
public void setOverBudget(boolean v) { overBudget = v; }
}
/** 品类分布 */
public static class CategoryDist {
private Long categoryId;
private String categoryName;
private BigDecimal amount;
private int materialCount;
private double percent;
public Long getCategoryId() { return categoryId; }
public void setCategoryId(Long v) { categoryId = v; }
public String getCategoryName() { return categoryName; }
public void setCategoryName(String v) { categoryName = v; }
public BigDecimal getAmount() { return amount; }
@@ -97,6 +103,7 @@ public class ReportCostVO {
private BigDecimal acceptedQuote;
private BigDecimal savedAmount;
private int supplierCount;
private boolean overBudget;
public Long getRfqId() { return rfqId; }
public void setRfqId(Long v) { rfqId = v; }
@@ -114,5 +121,7 @@ public class ReportCostVO {
public void setSavedAmount(BigDecimal v) { savedAmount = v; }
public int getSupplierCount() { return supplierCount; }
public void setSupplierCount(int v) { supplierCount = v; }
public boolean isOverBudget() { return overBudget; }
public void setOverBudget(boolean v) { overBudget = v; }
}
}

View File

@@ -58,6 +58,8 @@ public class ReportDashboardVO {
public static class KpiCard {
private String label;
private BigDecimal value;
/** 累计值全部历史数据汇总用于当月为0时的补充展示 */
private BigDecimal totalValue;
private double changeRate;
private String unit;
private String trend; // up / down
@@ -66,6 +68,8 @@ public class ReportDashboardVO {
public void setLabel(String v) { label = v; }
public BigDecimal getValue() { return value; }
public void setValue(BigDecimal v) { value = v; }
public BigDecimal getTotalValue() { return totalValue; }
public void setTotalValue(BigDecimal v) { totalValue = v; }
public double getChangeRate() { return changeRate; }
public void setChangeRate(double v) { changeRate = v; }
public String getUnit() { return unit; }

View File

@@ -20,6 +20,9 @@ public class ReportSupplierVO {
/** 异议统计 */
private List<ObjectionStat> objectionStats;
/** 评分历史趋势 */
private List<ScoreHistory> scoreHistory;
// ===== getters / setters =====
public List<SupplierScore> getRankings() { return rankings; }
@@ -34,6 +37,9 @@ public class ReportSupplierVO {
public List<ObjectionStat> getObjectionStats() { return objectionStats; }
public void setObjectionStats(List<ObjectionStat> v) { objectionStats = v; }
public List<ScoreHistory> getScoreHistory() { return scoreHistory; }
public void setScoreHistory(List<ScoreHistory> v) { scoreHistory = v; }
// ===== 内部类 =====
/** 供应商评分 */
@@ -41,6 +47,7 @@ public class ReportSupplierVO {
private Long supplierId;
private String supplierName;
private int evalCount;
private String evalStatus;
private double qualityAvg;
private double deliveryAvg;
private double serviceAvg;
@@ -55,6 +62,8 @@ public class ReportSupplierVO {
public void setSupplierName(String v) { supplierName = v; }
public int getEvalCount() { return evalCount; }
public void setEvalCount(int v) { evalCount = v; }
public String getEvalStatus() { return evalStatus; }
public void setEvalStatus(String v) { evalStatus = v; }
public double getQualityAvg() { return qualityAvg; }
public void setQualityAvg(double v) { qualityAvg = v; }
public double getDeliveryAvg() { return deliveryAvg; }
@@ -130,4 +139,30 @@ public class ReportSupplierVO {
public String getTopReason() { return topReason; }
public void setTopReason(String v) { topReason = v; }
}
/** 评分历史趋势 */
public static class ScoreHistory {
private Long supplierId;
private String supplierName;
private String month;
private double qualityAvg;
private double deliveryAvg;
private double serviceAvg;
private double priceAvg;
public Long getSupplierId() { return supplierId; }
public void setSupplierId(Long v) { supplierId = v; }
public String getSupplierName() { return supplierName; }
public void setSupplierName(String v) { supplierName = v; }
public String getMonth() { return month; }
public void setMonth(String v) { month = v; }
public double getQualityAvg() { return qualityAvg; }
public void setQualityAvg(double v) { qualityAvg = v; }
public double getDeliveryAvg() { return deliveryAvg; }
public void setDeliveryAvg(double v) { deliveryAvg = v; }
public double getServiceAvg() { return serviceAvg; }
public void setServiceAvg(double v) { serviceAvg = v; }
public double getPriceAvg() { return priceAvg; }
public void setPriceAvg(double v) { priceAvg = v; }
}
}

View File

@@ -18,12 +18,18 @@ public interface BizReportMapper {
/** 本月/上月采购总额 */
Map<String, Object> selectPurchaseAmount(@Param("month") String month);
/** 累计采购总额(不限月份) */
Map<String, Object> selectPurchaseAmountAll();
/** 本月/上月 RFQ 数量 */
Map<String, Object> selectRfqCount(@Param("month") String month);
/** 本月/上月 PO 数量 */
Map<String, Object> selectPoCount(@Param("month") String month);
/** 累计采购单数(不限月份) */
Map<String, Object> selectPoCountAll();
/** 活跃供应商数 */
Map<String, Object> selectActiveSupplierCount();
@@ -70,4 +76,7 @@ public interface BizReportMapper {
/** 异议统计 */
List<ReportSupplierVO.ObjectionStat> selectObjectionStats();
/** 供应商评分历史趋势 */
List<ReportSupplierVO.ScoreHistory> selectSupplierScoreHistory();
}

View File

@@ -168,6 +168,14 @@ public class BizApprovalActionServiceImpl implements IBizApprovalActionService {
sb.append(";驳回原因: ").append(reason);
}
// 订单异议额外显示处理结果
if ("ORDER_OBJECTION".equals(bizType) && detail.get("resolution") != null) {
String resolution = String.valueOf(detail.get("resolution")).trim();
if (!resolution.isEmpty()) {
sb.append(";处理结果: ").append(resolution);
}
}
return sb.toString();
}
@@ -203,6 +211,15 @@ public class BizApprovalActionServiceImpl implements IBizApprovalActionService {
}
sb.append("提交人: ").append(submitterName);
// 订单异议额外显示处理结果
if ("ORDER_OBJECTION".equals(bizType) && detail.get("resolution") != null) {
String resolution = String.valueOf(detail.get("resolution")).trim();
if (!resolution.isEmpty()) {
sb.append(";处理结果: ").append(resolution);
}
}
return sb.toString();
}
@@ -286,10 +303,10 @@ public class BizApprovalActionServiceImpl implements IBizApprovalActionService {
if (bizType == null || bizId == null) return null;
switch (bizType) {
case "PURCHASE_ORDER": return "/quote/purchaseorder?id=" + bizId;
case "CLIENT_QUOTE": return "/bid/clientquote/detail?id=" + bizId;
case "CLIENT_QUOTE": return "/bid/clientquote?id=" + bizId;
case "QUOTATION": return "/quote/quotation?quotationId=" + bizId;
case "DELIVERY_ORDER": return "/bid/order/pending?id=" + bizId;
case "ORDER_OBJECTION": return "/bid/order/objection?id=" + bizId;
case "DELIVERY_ORDER": return "/fulfill/client-delivery/pending?id=" + bizId;
case "ORDER_OBJECTION": return "/fulfill/supplierFulfill/objection?id=" + bizId;
default: return null;
}
}

View File

@@ -145,10 +145,10 @@ public class BizNotifyMessageServiceImpl implements IBizNotifyMessageService {
if (bizType == null || bizId == null) return null;
switch (bizType) {
case "PURCHASE_ORDER": return "/quote/purchaseorder?id=" + bizId;
case "CLIENT_QUOTE": return "/bid/clientquote/detail?id=" + bizId;
case "CLIENT_QUOTE": return "/bid/clientquote?id=" + bizId;
case "QUOTATION": return "/quote/quotation?quotationId=" + bizId;
case "DELIVERY_ORDER": return "/bid/order/pending?id=" + bizId;
case "ORDER_OBJECTION": return "/bid/order/objection?id=" + bizId;
case "DELIVERY_ORDER": return "/fulfill/client-delivery/pending?id=" + bizId;
case "ORDER_OBJECTION": return "/fulfill/supplierFulfill/objection?id=" + bizId;
default: return null;
}
}

View File

@@ -12,9 +12,10 @@ import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 报表统计 Service 实现
@@ -35,25 +36,34 @@ public class BizReportServiceImpl implements IBizReportService {
String curMonth = LocalDate.now().format(MONTH_FMT);
String lastMonth = LocalDate.now().minusMonths(1).format(MONTH_FMT);
// 累计值(全部历史数据)
BigDecimal totalPurchaseAll = selectAmount("totalAmount", null, true);
BigDecimal totalPoAll = selectCount("po", null, true);
// 1) KPI 卡片
vo.setTotalPurchaseAmount(buildKpi("采购总额",
ReportDashboardVO.KpiCard purchaseCard = buildKpi("采购总额",
selectAmount("totalAmount", curMonth),
selectAmount("totalAmount", lastMonth), ""));
selectAmount("totalAmount", lastMonth), "");
purchaseCard.setTotalValue(totalPurchaseAll);
vo.setTotalPurchaseAmount(purchaseCard);
vo.setTotalRfqCount(buildKpi("RFQ总数",
selectCount("rfq", curMonth),
selectCount("rfq", lastMonth), ""));
vo.setTotalPoCount(buildKpi("采购单数",
ReportDashboardVO.KpiCard poCard = buildKpi("采购单数",
selectCount("po", curMonth),
selectCount("po", lastMonth), ""));
selectCount("po", lastMonth), "");
poCard.setTotalValue(totalPoAll);
vo.setTotalPoCount(poCard);
Map<String, Object> act = mapper.selectActiveSupplierCount();
BigDecimal activeVal = toBD(act.get("totalCount"));
vo.setActiveSupplierCount(buildKpi("活跃供应商", activeVal, null, ""));
// 2) 月度趋势
vo.setMonthlyTrend(mapper.selectMonthlyTrend());
// 2) 月度趋势补全近12个月无数据月份填0
List<ReportDashboardVO.MonthTrend> rawTrend = mapper.selectMonthlyTrend();
vo.setMonthlyTrend(fillMonthlyTrend(rawTrend, 12));
// 3) RFQ 状态分布
List<ReportDashboardVO.StatusDist> dist = mapper.selectRfqStatusDist();
@@ -91,8 +101,9 @@ public class BizReportServiceImpl implements IBizReportService {
: 0);
vo.setSummary(summary);
// 月度趋势
vo.setCostTrend(mapper.selectCostTrend(startMonth, endMonth));
// 月度趋势补全月份无数据月份填0
List<ReportCostVO.CostTrend> rawTrend = mapper.selectCostTrend(startMonth, endMonth);
vo.setCostTrend(fillCostTrend(rawTrend, startMonth, endMonth));
// 品类分布
List<ReportCostVO.CategoryDist> catDist = mapper.selectCategoryDist();
@@ -108,7 +119,15 @@ public class BizReportServiceImpl implements IBizReportService {
vo.setCategoryDist(catDist);
// RFQ 比价明细
vo.setRfqDetails(mapper.selectRfqCompareDetails());
List<ReportCostVO.RfqCompareDetail> details = mapper.selectRfqCompareDetails();
if (details != null) {
for (ReportCostVO.RfqCompareDetail d : details) {
if (d.getSavedAmount() != null && d.getSavedAmount().compareTo(BigDecimal.ZERO) < 0) {
d.setOverBudget(true);
}
}
}
vo.setRfqDetails(details);
return vo;
}
@@ -121,6 +140,7 @@ public class BizReportServiceImpl implements IBizReportService {
vo.setWinRateData(mapper.selectWinRate());
vo.setRadarData(mapper.selectRadarData());
vo.setObjectionStats(mapper.selectObjectionStats());
vo.setScoreHistory(mapper.selectSupplierScoreHistory());
return vo;
}
@@ -149,15 +169,36 @@ public class BizReportServiceImpl implements IBizReportService {
}
private BigDecimal selectAmount(String column, String month) {
Map<String, Object> map = mapper.selectPurchaseAmount(month);
return selectAmount(column, month, false);
}
private BigDecimal selectAmount(String column, String month, boolean all) {
Map<String, Object> map;
if (all) {
map = mapper.selectPurchaseAmountAll();
} else {
map = mapper.selectPurchaseAmount(month);
}
return toBD(map.get(column));
}
private BigDecimal selectCount(String type, String month) {
return selectCount(type, month, false);
}
private BigDecimal selectCount(String type, String month, boolean all) {
Map<String, Object> map;
switch (type) {
case "rfq": map = mapper.selectRfqCount(month); break;
case "po": map = mapper.selectPoCount(month); break;
case "rfq":
if (all) return BigDecimal.ZERO; // RFQ无累计需求
map = mapper.selectRfqCount(month); break;
case "po":
if (all) {
map = mapper.selectPoCountAll();
} else {
map = mapper.selectPoCount(month);
}
break;
default: return BigDecimal.ZERO;
}
return toBD(map.get("totalCount"));
@@ -169,4 +210,100 @@ public class BizReportServiceImpl implements IBizReportService {
if (v instanceof Number) return BigDecimal.valueOf(((Number) v).doubleValue());
return BigDecimal.ZERO;
}
/**
* 补全月度采购趋势 — 生成近N个月的完整月份列表无数据月份填0
* 解决趋势图断层问题
*/
private List<ReportDashboardVO.MonthTrend> fillMonthlyTrend(List<ReportDashboardVO.MonthTrend> raw, int months) {
// 生成近N个月份列表从最早到当前月
List<String> allMonths = new ArrayList<>();
LocalDate now = LocalDate.now();
for (int i = months - 1; i >= 0; i--) {
allMonths.add(now.minusMonths(i).format(MONTH_FMT));
}
// 将原始数据转为Map便于查找
Map<String, ReportDashboardVO.MonthTrend> rawMap = new LinkedHashMap<>();
if (raw != null) {
for (ReportDashboardVO.MonthTrend t : raw) {
if (t.getMonth() != null) {
rawMap.put(t.getMonth(), t);
}
}
}
// 按完整月份列表补全
List<ReportDashboardVO.MonthTrend> result = new ArrayList<>();
for (String m : allMonths) {
ReportDashboardVO.MonthTrend t = rawMap.get(m);
if (t == null) {
t = new ReportDashboardVO.MonthTrend();
t.setMonth(m);
t.setAmount(BigDecimal.ZERO);
t.setCount(0);
}
result.add(t);
}
return result;
}
/**
* 补全成本趋势 — 生成完整月份列表无数据月份填0
* 解决成本趋势图断层问题
*/
private List<ReportCostVO.CostTrend> fillCostTrend(List<ReportCostVO.CostTrend> raw, String startMonth, String endMonth) {
// 确定起止月份
LocalDate now = LocalDate.now();
LocalDate start;
LocalDate end;
if (startMonth != null && !startMonth.isEmpty()) {
start = LocalDate.parse(startMonth + "-01");
} else {
start = now.minusMonths(11); // 默认近12个月
}
if (endMonth != null && !endMonth.isEmpty()) {
end = LocalDate.parse(endMonth + "-01");
} else {
end = now;
}
// 生成完整月份列表
List<String> allMonths = new ArrayList<>();
LocalDate cur = start.withDayOfMonth(1);
while (!cur.isAfter(end)) {
allMonths.add(cur.format(MONTH_FMT));
cur = cur.plusMonths(1);
}
// 将原始数据转为Map
Map<String, ReportCostVO.CostTrend> rawMap = new LinkedHashMap<>();
if (raw != null) {
for (ReportCostVO.CostTrend t : raw) {
if (t.getMonth() != null) {
rawMap.put(t.getMonth(), t);
}
}
}
// 补全
List<ReportCostVO.CostTrend> result = new ArrayList<>();
for (String m : allMonths) {
ReportCostVO.CostTrend t = rawMap.get(m);
if (t == null) {
t = new ReportCostVO.CostTrend();
t.setMonth(m);
t.setExpectedAmount(BigDecimal.ZERO);
t.setActualAmount(BigDecimal.ZERO);
t.setSavedAmount(BigDecimal.ZERO);
}
// 设置超支标识:节省金额为负表示实际超预算
if (t.getSavedAmount() != null && t.getSavedAmount().compareTo(BigDecimal.ZERO) < 0) {
t.setOverBudget(true);
}
result.add(t);
}
return result;
}
}

View File

@@ -64,11 +64,14 @@
<!-- 查询订单异议详情 -->
<select id="selectObjectionInfo" resultType="java.util.Map">
SELECT o.reason AS bizTitle, s.supplier_name AS partnerName, p.po_no AS bizNo,
SELECT o.reason AS bizTitle, s.supplier_name AS partnerName,
COALESCE(d.do_no, p.po_no) AS bizNo,
o.resolution AS resolution, o.status AS status,
o.create_time AS createTime, o.create_by AS createBy
FROM biz_order_objection o
LEFT JOIN biz_supplier s ON o.supplier_id = s.supplier_id
LEFT JOIN biz_purchase_order p ON o.po_id = p.po_id
LEFT JOIN biz_delivery_order d ON o.do_id = d.do_id
WHERE o.objection_id = #{id}
</select>
</mapper>

View File

@@ -5,6 +5,7 @@
<id property="objectionId" column="objection_id"/>
<result property="tenantId" column="tenant_id"/>
<result property="poId" column="po_id"/>
<result property="doId" column="do_id"/>
<result property="supplierId" column="supplier_id"/>
<result property="reason" column="reason"/>
<result property="attachment" column="attachment"/>
@@ -15,13 +16,15 @@
<result property="resolveTime" column="resolve_time"/>
<result property="supplierName" column="supplier_name"/>
<result property="poNo" column="po_no"/>
<result property="doNo" column="do_no"/>
</resultMap>
<select id="selectBizOrderObjectionList" resultMap="BaseRM">
SELECT o.*, s.supplier_name, p.po_no
SELECT o.*, s.supplier_name, p.po_no, d.do_no
FROM biz_order_objection o
LEFT JOIN biz_supplier s ON o.supplier_id=s.supplier_id
LEFT JOIN biz_purchase_order p ON o.po_id=p.po_id
LEFT JOIN biz_delivery_order d ON o.do_id=d.do_id
<where>
<if test="tenantId != null"> AND o.tenant_id=#{tenantId}</if>
<if test="status != null and status != ''"> AND o.status=#{status}</if>
@@ -30,15 +33,16 @@
</select>
<select id="selectBizOrderObjectionById" resultMap="BaseRM">
SELECT o.*, s.supplier_name, p.po_no FROM biz_order_objection o
SELECT o.*, s.supplier_name, p.po_no, d.do_no FROM biz_order_objection o
LEFT JOIN biz_supplier s ON o.supplier_id=s.supplier_id
LEFT JOIN biz_purchase_order p ON o.po_id=p.po_id
LEFT JOIN biz_delivery_order d ON o.do_id=d.do_id
WHERE o.objection_id=#{id}
</select>
<insert id="insertBizOrderObjection" useGeneratedKeys="true" keyProperty="objectionId">
INSERT INTO biz_order_objection(tenant_id,po_id,supplier_id,reason,attachment,status,create_by,create_time)
VALUES(#{tenantId},#{poId},#{supplierId},#{reason},#{attachment},'pending',#{createBy},NOW())
INSERT INTO biz_order_objection(tenant_id,po_id,do_id,supplier_id,reason,attachment,status,create_by,create_time)
VALUES(#{tenantId},#{poId},#{doId},#{supplierId},#{reason},#{attachment},'pending',#{createBy},NOW())
</insert>
<update id="updateBizOrderObjection">

View File

@@ -16,6 +16,13 @@
</if>
</select>
<!-- 累计采购总额(不限月份) -->
<select id="selectPurchaseAmountAll" resultType="java.util.HashMap">
SELECT COALESCE(SUM(total_amount), 0) AS totalAmount
FROM biz_purchase_order
WHERE status IN ('confirmed', 'closed', 'delivered')
</select>
<!-- 指定月份 RFQ 数量 -->
<select id="selectRfqCount" resultType="java.util.HashMap">
SELECT COUNT(*) AS totalCount
@@ -36,6 +43,13 @@
</if>
</select>
<!-- 累计采购单数(不限月份) -->
<select id="selectPoCountAll" resultType="java.util.HashMap">
SELECT COUNT(*) AS totalCount
FROM biz_purchase_order
WHERE status IN ('confirmed', 'closed', 'delivered')
</select>
<!-- 活跃供应商数(有过报价或采购的) -->
<select id="selectActiveSupplierCount" resultType="java.util.HashMap">
SELECT COUNT(DISTINCT supplier_id) AS totalCount
@@ -180,17 +194,26 @@
ORDER BY t.month ASC
</select>
<!-- 品类采购分布 -->
<!-- 品类采购分布(按一级分类聚合) -->
<select id="selectCategoryDist" resultType="com.ruoyi.system.domain.bid.ReportCostVO$CategoryDist">
SELECT COALESCE(c.category_name, '未分类') AS categoryName,
COALESCE(SUM(pi.total_price), 0) AS amount,
COUNT(DISTINCT pi.material_id) AS materialCount
SELECT
COALESCE(top_cat.category_id, 0) AS categoryId,
COALESCE(top_cat.category_name, '未分类') AS categoryName,
COALESCE(SUM(pi.total_price), 0) AS amount,
COUNT(DISTINCT pi.material_id) AS materialCount
FROM biz_purchase_order_item pi
JOIN biz_purchase_order p ON pi.po_id = p.po_id
LEFT JOIN biz_material m ON pi.material_id = m.material_id
LEFT JOIN biz_material_category c ON m.category_id = c.category_id
<!-- 通过ancestors字段找到顶级分类parent_id=0的一级分类 -->
LEFT JOIN biz_material_category top_cat ON top_cat.category_id =
CASE
WHEN c.ancestors = '0' THEN c.category_id
WHEN c.ancestors IS NULL THEN NULL
ELSE CAST(SUBSTRING_INDEX(SUBSTRING_INDEX(c.ancestors, ',', 2), ',', -1) AS UNSIGNED)
END
WHERE p.status IN ('confirmed', 'closed', 'delivered')
GROUP BY c.category_name
GROUP BY top_cat.category_id, top_cat.category_name
ORDER BY amount DESC
</select>
@@ -249,6 +272,7 @@
s.supplier_id AS supplierId,
s.supplier_name AS supplierName,
COALESCE(e.evalCount, 0) AS evalCount,
CASE WHEN COALESCE(e.evalCount, 0) > 0 THEN 'evaluated' ELSE 'not_evaluated' END AS evalStatus,
COALESCE(e.qualityAvg, 0) AS qualityAvg,
COALESCE(e.deliveryAvg, 0) AS deliveryAvg,
COALESCE(e.serviceAvg, 0) AS serviceAvg,
@@ -276,7 +300,7 @@
WHERE status IN ('confirmed', 'closed', 'delivered')
GROUP BY supplier_id
) po ON s.supplier_id = po.supplier_id
ORDER BY totalAvg DESC
ORDER BY CASE WHEN COALESCE(e.evalCount, 0) > 0 THEN 0 ELSE 1 END, totalAvg DESC
</select>
<!-- 中标率 -->
@@ -294,7 +318,7 @@
ORDER BY winRate DESC
</select>
<!-- 雷达图数据 -->
<!-- 雷达图数据(仅含有评价数据的供应商) -->
<select id="selectRadarData" resultType="com.ruoyi.system.domain.bid.ReportSupplierVO$RadarData">
SELECT
s.supplier_name AS supplierName,
@@ -303,7 +327,7 @@
COALESCE(AVG(e.service_score), 0) AS service,
COALESCE(AVG(e.price_score), 0) AS price
FROM biz_supplier s
LEFT JOIN biz_supplier_evaluation e ON s.supplier_id = e.supplier_id
INNER JOIN biz_supplier_evaluation e ON s.supplier_id = e.supplier_id
GROUP BY s.supplier_id, s.supplier_name
ORDER BY s.supplier_id
</select>
@@ -322,4 +346,20 @@
ORDER BY objectionCount DESC
</select>
<!-- 供应商评分历史趋势(按月) -->
<select id="selectSupplierScoreHistory" resultType="com.ruoyi.system.domain.bid.ReportSupplierVO$ScoreHistory">
SELECT
e.supplier_id AS supplierId,
s.supplier_name AS supplierName,
DATE_FORMAT(e.eval_time, '%Y-%m') AS month,
AVG(e.quality_score) AS qualityAvg,
AVG(e.delivery_score) AS deliveryAvg,
AVG(e.service_score) AS serviceAvg,
AVG(e.price_score) AS priceAvg
FROM biz_supplier_evaluation e
JOIN biz_supplier s ON e.supplier_id = s.supplier_id
GROUP BY e.supplier_id, s.supplier_name, DATE_FORMAT(e.eval_time, '%Y-%m')
ORDER BY e.supplier_id, month
</select>
</mapper>

View File

@@ -3,5 +3,5 @@ const baseUrl = '/bid/objection'
export const listObjection = (params) => request({ url: baseUrl + '/list', method: 'get', params })
export const getObjection = (id) => request({ url: baseUrl + '/' + id, method: 'get' })
export const addObjection = (data) => request({ url: baseUrl, method: 'post', data })
export const updateObjection = (data) => request({ url: baseUrl, method: 'put', data })
export const updateObjection = (data) => request({ url: baseUrl + '/resolve', method: 'put', data })
export const delObjection = (ids) => request({ url: baseUrl + '/' + ids, method: 'delete' })

View File

@@ -127,6 +127,67 @@ export const dynamicRoutes = [
}
]
},
// ── 列表页路由(供报表下钻跳转) ──
{
path: '/bid/purchaseorder',
component: Layout,
hidden: true,
permissions: ['bid:purchaseorder:list'],
children: [{
path: '',
component: () => import('@/views/bid/purchaseorder/index'),
name: 'PurchaseOrderList',
meta: { title: '采购单', activeMenu: '/quote/purchaseorder' }
}]
},
{
path: '/bid/rfq',
component: Layout,
hidden: true,
permissions: ['bid:rfq:list'],
children: [{
path: '',
component: () => import('@/views/bid/rfq/index'),
name: 'RfqList',
meta: { title: '报价请求', activeMenu: '/quote/rfq' }
}]
},
{
path: '/bid/supplier',
component: Layout,
hidden: true,
permissions: ['bid:supplier:list'],
children: [{
path: '',
component: () => import('@/views/bid/supplier/index'),
name: 'SupplierList',
meta: { title: '供应商管理', activeMenu: '/basedata/bidSupplier' }
}]
},
{
path: '/bid/material',
component: Layout,
hidden: true,
permissions: ['bid:material:list'],
children: [{
path: '',
component: () => import('@/views/bid/material/index'),
name: 'MaterialList',
meta: { title: '物料管理', activeMenu: '/basedata/material' }
}]
},
{
path: '/bid/comparison/detail',
component: Layout,
hidden: true,
permissions: ['bid:comparison:list'],
children: [{
path: '',
component: () => import('@/views/bid/comparison/detail'),
name: 'ComparisonDetail',
meta: { title: '比价详情', activeMenu: '/quote/comparison' }
}]
},
// ── 统计分析 路由 ──
{
path: '/bid/report/dashboard',

View File

@@ -15,7 +15,7 @@
size="mini"
placeholder="搜索分类"
clearable
style="width:140px" />
class="tree-filter-input" />
</div>
<el-tree
ref="categoryTree"
@@ -369,6 +369,10 @@ export default {
}
},
created() {
// 支持从报表下钻跳转读取路由query中的categoryId
if (this.$route.query.categoryId) {
this.queryParams.categoryId = Number(this.$route.query.categoryId);
}
this.getList();
this.loadCategories();
this.loadBrands();
@@ -576,12 +580,22 @@ export default {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
white-space: nowrap;
gap: 8px;
}
.tree-title {
font-size: 13px;
font-weight: 500;
color: #333;
white-space: nowrap;
flex-shrink: 0;
}
.tree-filter-input {
flex: 1;
min-width: 0;
}
.category-tree {

View File

@@ -20,7 +20,7 @@
</el-row>
<el-table v-loading="loading" :data="list">
<el-table-column label="采购单号" prop="poNo" width="150" />
<el-table-column label="发货单号" prop="doNo" width="180" />
<el-table-column label="供应商" prop="supplierName" width="150" />
<el-table-column label="异议原因" prop="reason" :show-overflow-tooltip="true" />
<el-table-column label="状态" width="100">
@@ -32,10 +32,10 @@
</el-table-column>
<el-table-column label="处理结果" prop="resolution" :show-overflow-tooltip="true" />
<el-table-column label="提交时间" prop="createTime" width="160" />
<el-table-column label="操作" class-name="col-ops" align="center">
<el-table-column label="操作" class-name="col-ops" align="center" width="260">
<template slot-scope="scope">
<el-button size="mini" type="text" @click="handleResolve(scope.row)" v-if="scope.row.status==='pending'||scope.row.status==='processing'" style="color:#67C23A">处理</el-button>
<el-button size="mini" type="text" @click="handleSubmitApproval(scope.row)" v-if="scope.row.status==='pending'" style="color:#E6A23C">提交审批</el-button>
<el-button size="mini" type="text" @click="loadDetail(scope.row.objectionId)">详情</el-button>
<el-button size="mini" type="text" @click="handleResolve(scope.row)" v-if="scope.row.status==='pending'" style="color:#67C23A">处理</el-button>
<el-button size="mini" type="text" @click="handleApprove(scope.row)" v-if="scope.row.status==='10'" style="color:#67C23A">通过</el-button>
<el-button size="mini" type="text" @click="handleReject(scope.row)" v-if="scope.row.status==='10'" style="color:#F56C6C">驳回</el-button>
</template>
@@ -45,13 +45,15 @@
<el-dialog title="提交异议" :visible.sync="addOpen" width="500px" append-to-body>
<el-form ref="addForm" :model="addForm" label-width="100px">
<el-form-item label="采购单号"><el-input v-model="addForm.poId" placeholder="输入采购单ID" /></el-form-item>
<el-form-item label="供应商">
<el-select v-model="addForm.supplierId" placeholder="选择供应商" style="width:100%">
<el-option v-for="s in supplierOptions" :key="s.supplierId" :label="s.supplierName" :value="s.supplierId" />
<el-form-item label="发货单号" required>
<el-select v-model="addForm.doId" placeholder="选择已签收的发货单" filterable style="width:100%" @change="onDeliveryChange">
<el-option v-for="d in deliveryOptions" :key="d.doId" :label="d.doNo + (d.supplierName ? ' - ' + d.supplierName : '')" :value="d.doId" />
</el-select>
</el-form-item>
<el-form-item label="异议原因"><el-input v-model="addForm.reason" type="textarea" rows="4" /></el-form-item>
<el-form-item label="供应商">
<el-input v-model="addForm.supplierName" disabled placeholder="选择发货单后自动填充" />
</el-form-item>
<el-form-item label="异议原因" required><el-input v-model="addForm.reason" type="textarea" rows="4" /></el-form-item>
</el-form>
<div slot="footer">
<el-button @click="addOpen=false">取消</el-button>
@@ -61,53 +63,126 @@
<el-dialog title="处理异议" :visible.sync="resolveOpen" width="460px" append-to-body>
<el-form :model="resolveForm" label-width="90px">
<el-form-item label="处理结果"><el-input v-model="resolveForm.resolution" type="textarea" rows="4" /></el-form-item>
<el-form-item label="处理状态">
<el-radio-group v-model="resolveForm.status">
<el-radio label="resolved">已解决</el-radio>
<el-radio label="rejected">拒绝</el-radio>
</el-radio-group>
<el-form-item label="异议原因">
<span style="color:#909399;font-size:13px">{{ resolveForm.reason || '—' }}</span>
</el-form-item>
<el-form-item label="处理结果" required>
<el-input v-model="resolveForm.resolution" type="textarea" rows="4" placeholder="请填写处理结果,提交后将进入审批流程" />
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="resolveOpen=false">取消</el-button>
<el-button type="primary" @click="submitResolve">确认</el-button>
<el-button type="primary" @click="submitResolve" :loading="resolveSubmitting">提交审批</el-button>
</div>
</el-dialog>
<!-- 异议详情弹窗从通知中心跳转 / 列表页点击详情 -->
<el-dialog title="异议详情" :visible.sync="detailOpen" width="580px" append-to-body>
<div v-loading="detailLoading" style="padding:12px 0">
<el-form label-width="110px" size="small" v-if="detailData">
<el-row>
<el-col :span="12">
<el-form-item label="发货单号">{{ detailData.doNo || '-' }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="采购单号">{{ detailData.poNo || '-' }}</el-form-item>
</el-col>
</el-row>
<el-form-item label="供应商">{{ detailData.supplierName || '-' }}</el-form-item>
<el-form-item label="异议原因">{{ detailData.reason || '-' }}</el-form-item>
<el-form-item label="状态">
<el-tag :type="{ pending:'warning', '10':'warning', resolved:'success', rejected:'danger' }[detailData.status]">
{{ { pending:'待处理', '10':'审批中', resolved:'已解决', rejected:'已拒绝' }[detailData.status] || detailData.status }}
</el-tag>
</el-form-item>
<el-form-item label="处理结果" v-if="detailData.resolution">{{ detailData.resolution }}</el-form-item>
<el-form-item label="提交人">{{ detailData.createBy || '-' }}</el-form-item>
<el-form-item label="提交时间">{{ parseTime(detailData.createTime) }}</el-form-item>
<el-form-item label="处理时间" v-if="detailData.resolveTime">{{ parseTime(detailData.resolveTime) }}</el-form-item>
</el-form>
<el-empty v-else description="暂无数据" />
</div>
</el-dialog>
</div>
</template>
<script>
import { listObjection, addObjection, updateObjection } from "@/api/bid/objection";
import { listObjection, getObjection, addObjection, updateObjection } from "@/api/bid/objection";
import { listSupplier } from "@/api/bid/supplier";
import { listDelivery } from "@/api/bid/delivery";
import { submitApproval, approveBiz, rejectBiz } from "@/api/bid/approvalAction";
export default {
name: "Objection",
data() {
return {
loading: false, total: 0, list: [],
addOpen: false, resolveOpen: false,
supplierOptions: [],
addOpen: false, resolveOpen: false, resolveSubmitting: false,
detailOpen: false, detailLoading: false, detailData: null,
supplierOptions: [], deliveryOptions: [],
queryParams: { pageNum: 1, pageSize: 10, status: null },
addForm: {}, resolveForm: {}
};
},
created() { this.getList(); listSupplier({ pageSize: 200 }).then(r => { this.supplierOptions = r.rows || []; }); },
created() {
this.getList();
listSupplier({ pageSize: 200 }).then(r => { this.supplierOptions = r.rows || []; });
listDelivery({ pageSize: 200, deliveryStatus: 'history' }).then(r => { this.deliveryOptions = r.rows || []; });
// 从通知中心跳转过来时根据id参数显示详情
const detailId = this.$route.query.id;
if (detailId) {
this.loadDetail(detailId);
}
},
watch: {
'$route.query.id'(newId) {
if (newId) this.loadDetail(newId);
}
},
methods: {
getList() {
this.loading = true;
listObjection(this.queryParams).then(r => { this.list = r.rows; this.total = r.total; this.loading = false; });
},
loadDetail(id) {
this.detailLoading = true;
this.detailData = null;
this.detailOpen = true;
getObjection(id).then(r => { this.detailData = r.data; }).catch(() => {
this.$modal.msgError("加载异议详情失败");
}).finally(() => { this.detailLoading = false; });
},
handleAdd() { this.addForm = {}; this.addOpen = true; },
onDeliveryChange(doId) {
const d = this.deliveryOptions.find(x => x.doId === doId);
if (d) {
this.$set(this.addForm, 'supplierId', d.supplierId);
this.$set(this.addForm, 'supplierName', d.supplierName || '');
}
},
submitAdd() {
if (!this.addForm.doId) { this.$modal.msgWarning("请选择发货单"); return; }
if (!this.addForm.reason || !this.addForm.reason.trim()) { this.$modal.msgWarning("请填写异议原因"); return; }
addObjection(this.addForm).then(() => { this.$modal.msgSuccess("提交成功"); this.addOpen = false; this.getList(); });
},
handleResolve(row) { this.resolveForm = { objectionId: row.objectionId, status: "resolved" }; this.resolveOpen = true; },
handleResolve(row) { this.resolveForm = { objectionId: row.objectionId, reason: row.reason, resolution: '' }; this.resolveOpen = true; },
submitResolve() {
updateObjection(this.resolveForm).then(() => { this.$modal.msgSuccess("处理成功"); this.resolveOpen = false; this.getList(); });
},
handleSubmitApproval(row) {
this.$modal.confirm("确认提交审批?").then(() => submitApproval("ORDER_OBJECTION", row.objectionId))
.then(() => { this.$modal.msgSuccess("已提交审批"); this.getList(); });
if (!this.resolveForm.resolution || !this.resolveForm.resolution.trim()) {
this.$modal.msgWarning("请填写处理结果");
return;
}
this.resolveSubmitting = true;
// 第一步:保存处理结果
updateObjection(this.resolveForm).then(() => {
// 第二步:提交审批
return submitApproval("ORDER_OBJECTION", this.resolveForm.objectionId);
}).then(() => {
this.$modal.msgSuccess("已提交审批,等待管理员审核");
this.resolveOpen = false;
this.getList();
}).catch(() => {
this.$modal.msgError("操作失败");
}).finally(() => {
this.resolveSubmitting = false;
});
},
handleApprove(row) {
this.$modal.confirm("确认通过该异议?").then(() => approveBiz("ORDER_OBJECTION", row.objectionId))

View File

@@ -199,7 +199,16 @@ export default {
rules: { supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }] }
};
},
created() { this.getList(); listSupplier({ pageSize: 200 }).then(r => { this.supplierOptions = r.rows || []; }); },
created() {
// 支持从报表下钻传入month参数如2026-01
if (this.$route.query.month) {
const month = this.$route.query.month;
this.queryParams.beginTime = month + '-01';
this.queryParams.endTime = month + '-31';
}
this.getList();
listSupplier({ pageSize: 200 }).then(r => { this.supplierOptions = r.rows || []; });
},
methods: {
getList() {
this.loading = true;

View File

@@ -5,14 +5,22 @@
<span class="kpi-number">{{ displayValue }}</span>
<span class="kpi-unit" v-if="unit">{{ unit }}</span>
</div>
<!-- 当月为0但有累计值时显示累计数据 -->
<div class="kpi-total" v-if="showTotal">
<span class="kpi-total-label">累计</span>
<span class="kpi-total-value">{{ displayTotalValue }}</span>
</div>
<div class="kpi-trend" :class="trendClass" v-if="changeRate > 0">
<i :class="trendIcon"></i>
{{ changeRateText }}
<span class="trend-label">环比上月</span>
</div>
<div class="kpi-trend no-change" v-else>
<div class="kpi-trend no-change" v-else-if="!showTotal">
<i class="el-icon-minus"></i> 持平
</div>
<div class="kpi-trend no-change" v-else>
<i class="el-icon-info"></i> 本月暂无数据
</div>
</el-card>
</template>
@@ -25,6 +33,7 @@ export default {
props: {
label: { type: String, required: true },
value: { type: [Number, String], default: 0 },
totalValue: { type: [Number, String], default: 0 },
unit: { type: String, default: '' },
changeRate: { type: Number, default: 0 },
trend: { type: String, default: 'up' }
@@ -34,6 +43,14 @@ export default {
const v = Number(this.value)
return isNaN(v) ? 0 : v
},
safeTotalValue() {
const v = Number(this.totalValue)
return isNaN(v) ? 0 : v
},
// 当月值为0且累计值>0时显示累计数据
showTotal() {
return this.safeValue === 0 && this.safeTotalValue > 0
},
displayValue() {
const v = this.safeValue
if (this.label === '采购总额') {
@@ -43,6 +60,15 @@ export default {
}
return v.toLocaleString()
},
displayTotalValue() {
const v = this.safeTotalValue
if (this.label === '采购总额') {
if (v >= 100000000) return '¥' + (v / 100000000).toFixed(2) + '亿'
if (v >= 10000) return '¥' + (v / 10000).toFixed(2) + '万'
return '¥' + v.toLocaleString()
}
return v.toLocaleString()
},
trendClass() { return this.trend === 'up' ? 'trend-up' : 'trend-down' },
trendIcon() { return this.trend === 'up' ? 'el-icon-top' : 'el-icon-bottom' },
changeRateText() {
@@ -87,6 +113,22 @@ export default {
color: #909399;
margin-left: 4px;
}
.kpi-total {
font-size: 13px;
color: #606266;
margin-bottom: 6px;
display: flex;
align-items: baseline;
gap: 4px;
}
.kpi-total-label {
color: #C0C4CC;
font-size: 12px;
}
.kpi-total-value {
font-weight: 600;
color: #E6A23C;
}
.kpi-trend {
font-size: 13px;
display: flex;

View File

@@ -2,6 +2,8 @@
<div class="app-container cost-page">
<div class="page-header">
<span class="page-title">💰 采购成本分析</span>
<el-date-picker v-model="dateRange" type="monthrange" size="mini" start-placeholder="开始月份" end-placeholder="结束月份" value-format="yyyy-MM" style="margin-left:auto;width:260px" @change="onDateChange" />
<el-button type="success" size="mini" icon="el-icon-download" @click="handleExport">导出Excel</el-button>
</div>
<div v-if="loading" class="loading-box">
@@ -22,6 +24,7 @@
<el-card shadow="hover" style="margin-bottom:16px">
<div slot="header" class="card-header">
<span><i class="el-icon-data-board" style="color:#e4393c"></i> 月度预算 vs 实际成本</span>
<el-button v-if="selectedMonth" type="text" size="mini" @click="selectedMonth=null" style="float:right;color:#F56C6C">清除月份筛选: {{selectedMonth}}</el-button>
</div>
<div ref="costTrendChart" style="height:350px;width:100%"></div>
</el-card>
@@ -49,7 +52,7 @@
<div slot="header" class="card-header">
<span><i class="el-icon-document" style="color:#909399"></i> RFQ 比价明细</span>
</div>
<el-table :data="data.rfqDetails || []" border size="small">
<el-table :data="filteredRfqDetails" border size="small" :row-class-name="tableRowClassName">
<el-table-column label="询价单号" prop="rfqNo" width="150" />
<el-table-column label="询价标题" prop="rfqTitle" min-width="180" show-overflow-tooltip />
<el-table-column label="预算价" width="130" align="right">
@@ -61,11 +64,24 @@
<el-table-column label="采纳价格" width="130" align="right">
<template slot-scope="s"><span class="cell-actual">¥{{ formatMoney(s.row.acceptedQuote) }}</span></template>
</el-table-column>
<el-table-column label="节省金额" width="130" align="right">
<el-table-column label="节省金额" width="140" align="right">
<template slot-scope="s">
<span :class="Number(s.row.savedAmount) > 0 ? 'cell-saved' : 'cell-none'">
<el-tooltip v-if="s.row.overBudget" :content="'实际超支 ¥' + formatMoney(Math.abs(Number(s.row.savedAmount)))" placement="top">
<span class="cell-overspend">
<i class="el-icon-warning-outline"></i> ¥{{ formatMoney(s.row.savedAmount) }}
</span>
</el-tooltip>
<span v-else-if="Number(s.row.savedAmount) > 0" class="cell-saved">
¥{{ formatMoney(s.row.savedAmount) }}
</span>
<span v-else class="cell-none">¥{{ formatMoney(s.row.savedAmount) }}</span>
</template>
</el-table-column>
<el-table-column label="差异说明" width="130" align="center">
<template slot-scope="s">
<span v-if="s.row.overBudget" class="diff-overspend">超预算 {{ getDiffPercent(s.row) }}%</span>
<span v-else-if="Number(s.row.savedAmount) > 0" class="diff-saved">节约 {{ getDiffPercent(s.row) }}%</span>
<span v-else class="diff-none"></span>
</template>
</el-table-column>
<el-table-column label="参与供应商" width="100" align="center" prop="supplierCount" />
@@ -76,7 +92,7 @@
</template>
</el-table-column>
</el-table>
<div v-if="!data.rfqDetails || !data.rfqDetails.length" class="no-data" style="padding:20px">暂无数据</div>
<div v-if="!filteredRfqDetails.length" class="no-data" style="padding:20px">暂无数据</div>
</el-card>
</div>
</div>
@@ -86,7 +102,7 @@
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import { debounce } from '@/utils'
import { getCostAnalysis } from '@/api/bid/report'
import { getCostAnalysis, exportReport } from '@/api/bid/report'
export default {
name: 'ReportCost',
@@ -96,7 +112,9 @@ export default {
data: null,
trendChart: null,
categoryChart: null,
savedChart: null
savedChart: null,
selectedMonth: null,
dateRange: []
}
},
computed: {
@@ -109,6 +127,11 @@ export default {
{ label: '节省金额', value: '¥' + this.formatMoney(s.savedAmount), color: Number(s.savedAmount) > 0 ? '#E6A23C' : '#C0C4CC', sub: Number(s.savedAmount) > 0 ? '为您节省了开支' : '' },
{ label: '节省比例', value: (Number(s.savedRate) || 0) + '%', color: '#F56C6C', sub: '相比预算' }
]
},
filteredRfqDetails() {
if (!this.data || !this.data.rfqDetails) return []
if (!this.selectedMonth) return this.data.rfqDetails
return this.data.rfqDetails.filter(d => d.month === this.selectedMonth)
}
},
mounted() {
@@ -136,7 +159,12 @@ export default {
methods: {
loadData() {
this.loading = true
getCostAnalysis().then(r => {
const params = {}
if (this.dateRange && this.dateRange.length === 2) {
params.startMonth = this.dateRange[0]
params.endMonth = this.dateRange[1]
}
getCostAnalysis(params).then(r => {
this.data = r.data
this.loading = false
this.$nextTick(() => {
@@ -151,6 +179,23 @@ export default {
this.$message.error('加载成本数据失败')
})
},
onDateChange() {
this.selectedMonth = null
this.loadData()
},
handleExport() {
exportReport('cost', this.data).then(res => {
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '采购成本分析.xlsx'
link.click()
window.URL.revokeObjectURL(url)
}).catch(() => {
this.$message.error('导出失败')
})
},
initTrendChart() {
const el = this.$refs.costTrendChart
if (!el) return
@@ -188,18 +233,34 @@ export default {
}
]
})
this.trendChart.off('click')
this.trendChart.on('click', (params) => {
const items = this.data.costTrend || []
const item = items[params.dataIndex]
if (item && item.month) {
this.selectedMonth = item.month
} else {
this.selectedMonth = null
}
})
},
initCategoryChart() {
const el = this.$refs.categoryChart
if (!el) return
if (this.categoryChart) this.categoryChart.dispose()
this.categoryChart = echarts.init(el, 'macarons')
const list = (this.data.categoryDist || []).map(d => ({
const rawList = this.data.categoryDist || []
const list = rawList.map(d => ({
name: d.categoryName || '未分类',
value: Number(d.amount) || 0
value: Number(d.amount) || 0,
categoryId: d.categoryId || 0
}))
this.categoryChart.setOption({
tooltip: { trigger: 'item', formatter: '{b}: ¥{c} ({d}%)' },
tooltip: {
trigger: 'item',
formatter: '{b}: ¥{c} ({d}%)',
extraCssText: 'cursor: pointer;'
},
series: [{
type: 'pie',
radius: ['45%', '70%'],
@@ -209,6 +270,14 @@ export default {
color: ['#e4393c', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#e4393c']
}]
})
// 点击饼图扇区下钻到物料管理页
this.categoryChart.off('click')
this.categoryChart.on('click', (params) => {
const item = list[params.dataIndex]
if (item && item.categoryId && item.categoryId !== 0) {
this.$router.push({ path: '/bid/material', query: { categoryId: item.categoryId } })
}
})
},
initSavedChart() {
const el = this.$refs.savedChart
@@ -216,33 +285,70 @@ export default {
if (this.savedChart) this.savedChart.dispose()
this.savedChart = echarts.init(el, 'macarons')
const items = this.data.costTrend || []
const savedData = items.map(d => Number(d.savedAmount) || 0)
this.savedChart.setOption({
tooltip: {
trigger: 'axis',
formatter: function(params) {
const v = Number(params[0].value)
const label = v < 0 ? '超支' : '节省'
return '<strong>' + params[0].axisValue + '</strong><br/>'
+ '节省:¥' + Number(params[0].value).toLocaleString()
+ label + ':¥' + Math.abs(v).toLocaleString()
}
},
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: items.map(d => d.month || '') },
yAxis: { type: 'value', name: '节省(元)', axisLabel: { formatter: v => v >= 10000 ? (v/10000)+'万' : v } },
visualMap: {
show: false,
type: 'piecewise',
dimension: 1,
pieces: [
{ gt: 0, color: '#67C23A' },
{ lte: 0, color: '#F56C6C' }
]
},
series: [{
type: 'line',
data: items.map(d => Number(d.savedAmount) || 0),
itemStyle: { color: '#67C23A' },
areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103,194,58,0.4)' },
{ offset: 1, color: 'rgba(103,194,58,0.05)' }
])},
data: savedData,
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103,194,58,0.35)' },
{ offset: 1, color: 'rgba(103,194,58,0.02)' }
])
},
lineStyle: { width: 3 },
symbol: 'diamond', symbolSize: 10,
label: { show: true, formatter: p => '¥' + Number(p.value).toLocaleString(), fontSize: 10 }
label: {
show: true,
formatter: p => {
const v = Number(p.value)
return (v < 0 ? '-' : '') + '¥' + Math.abs(v).toLocaleString()
},
fontSize: 10
},
markLine: {
silent: true,
symbol: 'none',
lineStyle: { color: '#E4E7ED', type: 'dashed' },
data: [{ yAxis: 0 }]
}
}]
})
},
goCompare(rfqId) {
this.$router.push({ path: '/comparison/detail', query: { rfqId } })
this.$router.push({ path: '/bid/comparison/detail', query: { rfqId } })
},
tableRowClassName({ row }) {
if (row.overBudget) return 'row-overspend'
return ''
},
getDiffPercent(row) {
const expected = Number(row.expectedTotal)
if (!expected || expected === 0) return '0.0'
const saved = Number(row.savedAmount)
const pct = Math.abs(saved) / expected * 100
return pct.toFixed(1)
},
formatMoney(v) {
const n = Number(v)
@@ -274,5 +380,13 @@ export default {
.cell-actual { color: #e4393c; font-weight: 600; }
.cell-saved { color: #67C23A; font-weight: 700; }
.cell-none { color: #C0C4CC; }
.cell-overspend { color: #F56C6C; font-weight: 700; cursor: help; i { margin-right: 2px; } }
.diff-overspend { color: #F56C6C; font-size: 12px; font-weight: 600; }
.diff-saved { color: #67C23A; font-size: 12px; }
.diff-none { color: #C0C4CC; }
.no-data { text-align: center; color: #C0C4CC; }
::v-deep .el-table .row-overspend {
background-color: #FEF0F0;
td { background-color: #FEF0F0 !important; }
}
</style>

View File

@@ -3,6 +3,7 @@
<div class="page-header">
<span class="page-title">📊 采购总览看板</span>
<span class="page-tip">实时数据 · 自动更新</span>
<el-button type="success" size="mini" icon="el-icon-download" @click="handleExport" style="margin-left:auto">导出Excel</el-button>
</div>
<div v-if="loading" class="loading-box">
@@ -68,7 +69,7 @@
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import { debounce } from '@/utils'
import { getDashboard } from '@/api/bid/report'
import { getDashboard, exportReport } from '@/api/bid/report'
import KpiCard from './components/KpiCard'
export default {
@@ -97,6 +98,7 @@ export default {
return {
label,
value: item && item.value !== undefined && item.value !== null ? item.value : 0,
totalValue: item && item.totalValue !== undefined && item.totalValue !== null ? item.totalValue : 0,
unit: item ? item.unit : '',
changeRate: item ? item.changeRate : 0,
trend: item ? item.trend : 'up'
@@ -185,6 +187,14 @@ export default {
}
]
})
this.trendChart.off('click')
this.trendChart.on('click', (params) => {
const list = this.data.monthlyTrend || []
const item = list[params.dataIndex]
if (item && item.month) {
this.$router.push({ path: '/bid/purchaseorder', query: { month: item.month } })
}
})
},
initPieChart() {
const el = this.$refs.pieChart
@@ -209,6 +219,14 @@ export default {
color: ['#e4393c', '#67C23A', '#E6A23C', '#F56C6C', '#909399']
}]
})
this.pieChart.off('click')
this.pieChart.on('click', (params) => {
const list = this.data.rfqStatusDist || []
const item = list[params.dataIndex]
if (item && item.status) {
this.$router.push({ path: '/bid/rfq', query: { status: item.status } })
}
})
},
initRankChart() {
const el = this.$refs.rankChart
@@ -261,12 +279,33 @@ export default {
}
}]
})
this.rankChart.off('click')
this.rankChart.on('click', (params) => {
const items = this.data.topSuppliers || []
const item = items[params.dataIndex !== undefined ? (items.length - 1 - params.dataIndex) : -1]
if (item && item.supplierId) {
this.$router.push({ path: '/bid/supplier', query: { supplierId: item.supplierId } })
}
})
},
actTag(type) {
return { PO: 'primary', QUOTE: 'success', EVAL: 'warning', OBJECTION: 'danger' }[type] || 'info'
},
actType(type) {
return { PO: '采购单', QUOTE: '报价', EVAL: '评价', OBJECTION: '异议' }[type] || type
},
handleExport() {
exportReport('dashboard', this.data).then(res => {
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '采购总览看板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
}).catch(() => {
this.$message.error('导出失败')
})
}
}
}

View File

@@ -2,6 +2,7 @@
<div class="app-container supplier-page">
<div class="page-header">
<span class="page-title">🏆 供应商绩效</span>
<el-button type="success" size="mini" icon="el-icon-download" @click="handleExport" style="margin-left:auto">导出Excel</el-button>
</div>
<div v-if="loading" class="loading-box">
@@ -14,25 +15,43 @@
<span><i class="el-icon-s-custom" style="color:#e4393c"></i> 供应商综合评分排名</span>
</div>
<el-table :data="data.rankings || []" border size="small"
highlight-current-row @current-change="onRowClick" style="cursor:pointer">
<el-table-column label="排名" type="index" width="55" align="center" />
highlight-current-row @current-change="onRowClick" style="cursor:pointer"
:row-class-name="tableRowClassName">
<el-table-column label="排名" width="55" align="center">
<template slot-scope="s">{{ getRank(s.$index, s.row) }}</template>
</el-table-column>
<el-table-column label="供应商名称" prop="supplierName" min-width="160" />
<el-table-column label="评价次数" prop="evalCount" width="80" align="center" />
<el-table-column label="质量评分" width="90" align="center">
<template slot-scope="s"><el-tag :type="scoreTag(Number(s.row.qualityAvg))" size="mini">{{ safeFixed(s.row.qualityAvg, 1) }}</el-tag></template>
<template slot-scope="s">
<el-tag v-if="s.row.evalStatus === 'evaluated'" :type="scoreTag(Number(s.row.qualityAvg))" size="mini">{{ safeFixed(s.row.qualityAvg, 1) }}</el-tag>
<span v-else class="score-na">--</span>
</template>
</el-table-column>
<el-table-column label="交期评分" width="90" align="center">
<template slot-scope="s"><el-tag :type="scoreTag(Number(s.row.deliveryAvg))" size="mini">{{ safeFixed(s.row.deliveryAvg, 1) }}</el-tag></template>
<template slot-scope="s">
<el-tag v-if="s.row.evalStatus === 'evaluated'" :type="scoreTag(Number(s.row.deliveryAvg))" size="mini">{{ safeFixed(s.row.deliveryAvg, 1) }}</el-tag>
<span v-else class="score-na">--</span>
</template>
</el-table-column>
<el-table-column label="服务评分" width="90" align="center">
<template slot-scope="s"><el-tag :type="scoreTag(Number(s.row.serviceAvg))" size="mini">{{ safeFixed(s.row.serviceAvg, 1) }}</el-tag></template>
<template slot-scope="s">
<el-tag v-if="s.row.evalStatus === 'evaluated'" :type="scoreTag(Number(s.row.serviceAvg))" size="mini">{{ safeFixed(s.row.serviceAvg, 1) }}</el-tag>
<span v-else class="score-na">--</span>
</template>
</el-table-column>
<el-table-column label="价格评分" width="90" align="center">
<template slot-scope="s"><el-tag :type="scoreTag(Number(s.row.priceAvg))" size="mini">{{ safeFixed(s.row.priceAvg, 1) }}</el-tag></template>
</el-table-column>
<el-table-column label="综合评分" width="100" align="center">
<template slot-scope="s">
<span :class="'score-badge-' + scoreClass(s.row.totalAvg)">{{ safeFixed(s.row.totalAvg, 1) }}</span>
<el-tag v-if="s.row.evalStatus === 'evaluated'" :type="scoreTag(Number(s.row.priceAvg))" size="mini">{{ safeFixed(s.row.priceAvg, 1) }}</el-tag>
<span v-else class="score-na">--</span>
</template>
</el-table-column>
<el-table-column label="综合评分" width="130" align="center">
<template slot-scope="s">
<div v-if="s.row.evalStatus === 'evaluated'" class="score-bar-wrap">
<el-progress :percentage="Number(s.row.totalAvg) / 5 * 100" :color="scoreColor(s.row.totalAvg)" :stroke-width="14" :text-inside="true" :format="() => safeFixed(s.row.totalAvg, 1)"></el-progress>
</div>
<span v-else class="score-na">暂无评价</span>
</template>
</el-table-column>
<el-table-column label="采购次数" prop="poCount" width="80" align="center" />
@@ -48,9 +67,13 @@
<el-card shadow="hover">
<div slot="header" class="card-header">
<span><i class="el-icon-s-marketing" style="color:#e4393c"></i> 供应商评价雷达图</span>
<span style="float:right;font-size:12px;color:#C0C4CC">点击表格行切换</span>
</div>
<div ref="radarChart" style="height:320px;width:100%"></div>
<div style="margin-bottom:10px">
<el-select v-model="radarSelected" multiple filterable placeholder="选择供应商" size="mini" style="width:100%" @change="refreshRadarChart">
<el-option v-for="r in evaluatedSuppliers" :key="r.supplierName" :label="r.supplierName" :value="r.supplierName" />
</el-select>
</div>
<div ref="radarChart" style="height:280px;width:100%"></div>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" style="margin-bottom:16px">
@@ -63,6 +86,16 @@
</el-col>
</el-row>
<el-card shadow="hover" style="margin-bottom:16px">
<div slot="header" class="card-header">
<span><i class="el-icon-trend-charts" style="color:#409EFF"></i> 评分历史趋势</span>
<el-select v-model="historySupplier" filterable placeholder="选择供应商" size="mini" style="float:right;width:200px" @change="refreshHistoryChart">
<el-option v-for="r in evaluatedSuppliers" :key="r.supplierName" :label="r.supplierName" :value="r.supplierName" />
</el-select>
</div>
<div ref="historyChart" style="height:320px;width:100%"></div>
</el-card>
<el-card shadow="hover">
<div slot="header" class="card-header">
<span><i class="el-icon-warning-outline" style="color:#F56C6C"></i> 订单异议统计</span>
@@ -90,7 +123,7 @@
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import { debounce } from '@/utils'
import { getSupplierPerformance } from '@/api/bid/report'
import { getSupplierPerformance, exportReport } from '@/api/bid/report'
export default {
name: 'ReportSupplier',
@@ -99,7 +132,16 @@ export default {
loading: false,
data: null,
radarChart: null,
winRateChart: null
winRateChart: null,
historyChart: null,
radarSelected: [],
historySupplier: ''
}
},
computed: {
evaluatedSuppliers() {
if (!this.data || !this.data.rankings) return []
return this.data.rankings.filter(r => r.evalStatus === 'evaluated')
}
},
mounted() {
@@ -107,12 +149,14 @@ export default {
this.__resizeHandler = debounce(() => {
this.radarChart && this.radarChart.resize()
this.winRateChart && this.winRateChart.resize()
this.historyChart && this.historyChart.resize()
}, 100)
window.addEventListener('resize', this.__resizeHandler)
this.$watch('$store.state.sidebar.opened', () => {
setTimeout(() => {
this.radarChart && this.radarChart.resize()
this.winRateChart && this.winRateChart.resize()
this.historyChart && this.historyChart.resize()
}, 300)
})
},
@@ -120,17 +164,39 @@ export default {
window.removeEventListener('resize', this.__resizeHandler)
this.radarChart && this.radarChart.dispose()
this.winRateChart && this.winRateChart.dispose()
this.historyChart && this.historyChart.dispose()
},
methods: {
handleExport() {
exportReport('supplier', this.data).then(res => {
const blob = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '供应商绩效评估.xlsx'
link.click()
window.URL.revokeObjectURL(url)
}).catch(() => {
this.$message.error('导出失败')
})
},
loadData() {
this.loading = true
getSupplierPerformance().then(r => {
this.data = r.data
this.loading = false
// 默认选中前3名已评价供应商
const evalSuppliers = (this.data.rankings || []).filter(r => r.evalStatus === 'evaluated')
this.radarSelected = evalSuppliers.slice(0, 3).map(r => r.supplierName)
// 默认选择第一个已评价供应商的历史趋势
if (evalSuppliers.length > 0) {
this.historySupplier = evalSuppliers[0].supplierName
}
this.$nextTick(() => {
requestAnimationFrame(() => {
this.initRadarChart()
this.initWinRateChart()
this.initHistoryChart()
})
})
}).catch(() => {
@@ -139,7 +205,7 @@ export default {
})
},
// ── 雷达图 ──
initRadarChart(supplierName) {
initRadarChart() {
const el = this.$refs.radarChart
if (!el) return
if (this.radarChart) {
@@ -149,8 +215,9 @@ export default {
this.radarChart = echarts.init(el, 'macarons')
let radarRows = this.data.radarData || []
if (supplierName) {
radarRows = radarRows.filter(r => r.supplierName === supplierName)
// 按选中的供应商筛选
if (this.radarSelected && this.radarSelected.length > 0) {
radarRows = radarRows.filter(r => this.radarSelected.includes(r.supplierName))
}
if (!radarRows.length) {
@@ -166,7 +233,7 @@ export default {
{ name: '服务', max: 5 },
{ name: '价格', max: 5 }
]
const colors = ['#e4393c', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#e4393c']
const colors = ['#e4393c', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#409EFF']
this.radarChart.setOption({
legend: {
@@ -193,6 +260,21 @@ export default {
}))
}]
})
this.radarChart.off('click')
this.radarChart.on('click', (params) => {
if (params.name) {
const ranking = (this.data.rankings || []).find(r => r.supplierName === params.name)
if (ranking && ranking.supplierId) {
this.$router.push({ path: '/bid/supplier', query: { supplierId: ranking.supplierId } })
}
}
})
},
refreshRadarChart() {
this.$nextTick(() => {
requestAnimationFrame(() => this.initRadarChart())
})
},
// ── 中标率柱状图 ──
initWinRateChart() {
@@ -249,15 +331,96 @@ export default {
}
]
})
this.winRateChart.off('click')
this.winRateChart.on('click', (params) => {
const items = this.data.winRateData || []
const item = items[params.dataIndex]
if (item && item.supplierId) {
this.$router.push({ path: '/bid/supplier', query: { supplierId: item.supplierId } })
}
})
},
// ── 点击表格行切换雷达图 ──
onRowClick(row) {
if (row && row.supplierName) {
this.$nextTick(() => {
requestAnimationFrame(() => this.initRadarChart(row.supplierName))
})
if (row && row.evalStatus === 'evaluated' && row.supplierName) {
// 点击已评价供应商行时,将其加入雷达图选中
if (!this.radarSelected.includes(row.supplierName)) {
this.radarSelected.push(row.supplierName)
this.refreshRadarChart()
}
}
},
// ── 评分历史趋势图 ──
initHistoryChart() {
const el = this.$refs.historyChart
if (!el) return
if (this.historyChart) {
this.historyChart.dispose()
this.historyChart = null
}
this.historyChart = echarts.init(el, 'macarons')
const allHistory = this.data.scoreHistory || []
const supplierHistory = allHistory.filter(h => h.supplierName === this.historySupplier)
if (!supplierHistory.length) {
this.historyChart.setOption({
title: { text: '暂无评分历史数据', left: 'center', top: 'center', textStyle: { color: '#C0C4CC', fontSize: 14 } }
})
return
}
const months = supplierHistory.map(h => h.month)
const colors = { quality: '#e4393c', delivery: '#67C23A', service: '#E6A23C', price: '#409EFF' }
this.historyChart.setOption({
tooltip: {
trigger: 'axis',
formatter: function(params) {
let s = '<strong>' + params[0].axisValue + '</strong><br/>'
params.forEach(p => {
s += p.marker + ' ' + p.seriesName + '' + Number(p.value).toFixed(1) + '<br/>'
})
return s
}
},
legend: { data: ['质量', '交期', '服务', '价格'], top: 0 },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: months },
yAxis: { type: 'value', min: 0, max: 5, name: '评分' },
series: [
{ name: '质量', type: 'line', data: supplierHistory.map(h => Number(h.qualityAvg) || 0), itemStyle: { color: colors.quality }, smooth: true },
{ name: '交期', type: 'line', data: supplierHistory.map(h => Number(h.deliveryAvg) || 0), itemStyle: { color: colors.delivery }, smooth: true },
{ name: '服务', type: 'line', data: supplierHistory.map(h => Number(h.serviceAvg) || 0), itemStyle: { color: colors.service }, smooth: true },
{ name: '价格', type: 'line', data: supplierHistory.map(h => Number(h.priceAvg) || 0), itemStyle: { color: colors.price }, smooth: true }
]
})
},
refreshHistoryChart() {
this.$nextTick(() => {
requestAnimationFrame(() => this.initHistoryChart())
})
},
// ── 表格辅助方法 ──
tableRowClassName({ row }) {
if (row.evalStatus === 'not_evaluated') return 'row-not-evaluated'
return ''
},
getRank(index, row) {
// 已评价供应商按totalAvg降序排名未评价排末尾不编号
if (row.evalStatus === 'not_evaluated') return '—'
// 计算在已评价供应商中的排名
const evaluated = (this.data.rankings || []).filter(r => r.evalStatus === 'evaluated')
return evaluated.findIndex(r => r.supplierId === row.supplierId) + 1
},
scoreColor(v) {
const n = Number(v)
if (isNaN(n)) return '#C0C4CC'
if (n >= 4.0) return '#67C23A'
if (n >= 3.0) return '#E6A23C'
return '#F56C6C'
},
resolveRate(row) {
if (!row || !row.objectionCount) return 0
return Math.round((Number(row.resolvedCount) || 0) / Number(row.objectionCount) * 100)
@@ -301,7 +464,10 @@ export default {
.card-header { font-weight: 600; color: #303133; font-size: 14px; }
.supplier-content { min-height: 400px; }
.no-data { text-align: center; color: #C0C4CC; padding: 40px 0; }
.score-badge-high { color: #67C23A; font-weight: 700; font-size: 16px; }
.score-badge-mid { color: #E6A23C; font-weight: 600; font-size: 15px; }
.score-badge-low { color: #F56C6C; font-weight: 600; }
.score-na { color: #C0C4CC; font-size: 13px; }
.score-bar-wrap { padding: 0 4px; }
::v-deep .el-table .row-not-evaluated {
background-color: #FAFAFA;
td { background-color: #FAFAFA !important; color: #909399; }
}
</style>

View File

@@ -302,6 +302,10 @@ export default {
};
},
created() {
// 支持从报表下钻传入status参数
if (this.$route.query.status) {
this.queryParams.status = this.$route.query.status;
}
this.getList();
// 供应商用户不需要加载供应商列表和甲方报价选项
if (!this.isSupplier) {

View File

@@ -365,6 +365,10 @@ export default {
};
},
created() {
// 支持从报表下钻传入supplierId参数
if (this.$route.query.supplierId) {
this.queryParams.supplierId = this.$route.query.supplierId;
}
this.getList();
},
methods: {

View File

@@ -0,0 +1,41 @@
-- 订单异议模拟数据20条
-- 覆盖5家供应商3种状态(resolved/pending/open),多样化异议原因
SET NAMES utf8mb4;
INSERT INTO biz_order_objection (tenant_id, po_id, supplier_id, reason, attachment, status, resolution, create_by, create_time, resolve_time, update_by, update_time) VALUES
-- 供应商14条
(1, 1, 1, '电源模块批次MW-2025-1128中发现3台开机自检报警检测为输出纹波超标不符合采购规格书要求要求供应商提供原厂检测报告并换货。', NULL, 'resolved', '供应商已提供原厂检测报告确认为该批次出厂检验遗漏问题。已更换3台合格品并附出厂测试记录我司复验通过。', '采购部 王工', '2025-08-10 14:00:00', '2025-08-18 11:00:00', 'admin', '2025-08-18 11:00:00'),
(1, 1, 1, '导轨表面有轻微划痕,影响外观但不影响功能,要求折扣处理。', NULL, 'resolved', '经协商供应商同意8折结算该批次已由财务调整账单。', '采购部 李工', '2025-10-05 09:30:00', '2025-10-10 15:00:00', 'admin', '2025-10-10 15:00:00'),
(1, 1, 1, '本批次PLC控制器固件版本与约定不符缺少MODBUS-TCP协议支持需升级固件。', NULL, 'pending', NULL, '采购部 张工', '2026-01-15 10:00:00', NULL, NULL, NULL),
(1, 1, 1, '包装箱破损导致2台传感器外壳变形要求补发。', NULL, 'resolved', '供应商确认物流责任已补发2台传感器到货验收合格。', '采购部 李工', '2026-03-20 13:30:00', '2026-03-25 10:00:00', 'admin', '2026-03-25 10:00:00'),
-- 供应商25条问题较多
(1, 2, 2, '滚珠丝杠NSK品牌10套中有3套程误差超出图纸要求规定0.05mm/300mm实测最大误差达0.09mm,要求退换货或索赔。', NULL, 'resolved', '经双方确认供应商同意退换该2套滚珠丝杠并承担往返运费。补货已于2025-09-28送达复检合格。本次异议结案。', '采购部 李工', '2025-09-22 09:00:00', '2025-09-30 16:00:00', 'admin', '2025-09-30 16:00:00'),
(1, 2, 2, '交期延迟7天导致生产线停工待料要求赔偿损失。', NULL, 'resolved', '供应商承认排产失误同意下次订单优惠5%作为补偿。', '采购部 王工', '2025-11-12 14:00:00', '2025-11-20 10:30:00', 'admin', '2025-11-20 10:30:00'),
(1, 2, 2, '伺服电机铭牌信息缺失,缺少序列号和生产日期,无法追溯。', NULL, 'pending', NULL, '采购部 张工', '2026-01-08 11:00:00', NULL, NULL, NULL),
(1, 2, 2, '本批次联轴器硬度不达标HRC实测值18要求HRC22-25使用寿命存疑。', NULL, 'open', NULL, '采购部 李工', '2026-02-25 15:00:00', NULL, NULL, NULL),
(1, 2, 2, '到货数量短缺订单20件实到18件缺少2件未提前告知。', NULL, 'resolved', '供应商确认漏发已补发2件并于3月5日到货。', '采购部 王工', '2026-03-01 09:15:00', '2026-03-05 14:00:00', 'admin', '2026-03-05 14:00:00'),
-- 供应商33条
(1, 3, 3, '变频器参数设置手册为英文版,要求提供中文版。', NULL, 'resolved', '供应商已邮寄中文版手册,并提供电子版下载链接。', '采购部 张工', '2025-09-15 10:00:00', '2025-09-18 14:00:00', 'admin', '2025-09-18 14:00:00'),
(1, 3, 3, '触摸屏亮度不均匀,边缘有暗影,疑似背光模组不良。', NULL, 'resolved', '供应商技术人员现场检测确认为背光模组批次问题已更换4台触摸屏。', '采购部 李工', '2025-12-10 13:00:00', '2025-12-18 11:00:00', 'admin', '2025-12-18 11:00:00'),
(1, 3, 3, '继电器触点氧化严重存储3个月后出现接触不良怀疑防锈处理不达标。', NULL, 'pending', NULL, '采购部 王工', '2026-04-02 10:30:00', NULL, NULL, NULL),
-- 供应商45条问题最多
(1, 1, 4, '气缸密封圈材质不符约定氟橡胶FKM实际为丁腈橡胶NBR耐温性不达标。', NULL, 'resolved', '供应商承认材质替换错误已退换全部50件密封圈复检合格。', '采购部 李工', '2025-08-20 14:30:00', '2025-09-05 10:00:00', 'admin', '2025-09-05 10:00:00'),
(1, 1, 4, '电磁阀线圈阻抗偏差大标称24VDC 1.2A实测1.5A,存在过热风险。', NULL, 'open', NULL, '采购部 张工', '2025-10-18 11:00:00', NULL, NULL, NULL),
(1, 1, 4, '到货产品无出厂检验报告,无合格证,拒绝入库。', NULL, 'resolved', '供应商补寄出厂检验报告和合格证,经审核后入库。', '采购部 王工', '2025-11-25 09:00:00', '2025-12-01 15:00:00', 'admin', '2025-12-01 15:00:00'),
(1, 1, 4, '气动接头螺纹滑丝无法正常安装不良率高达15%。', NULL, 'pending', NULL, '采购部 李工', '2026-02-10 14:00:00', NULL, NULL, NULL),
(1, 1, 4, '本批次气管有异味,疑似材质回料掺杂,要求第三方检测。', NULL, 'open', NULL, '采购部 张工', '2026-04-15 10:15:00', NULL, NULL, NULL),
-- 供应商53条
(1, 2, 5, '电缆外径偏大,超出接线端子孔径,无法压接。', NULL, 'resolved', '供应商确认模具偏差,已免费提供配套大孔径端子。', '采购部 李工', '2025-10-08 13:00:00', '2025-10-15 11:00:00', 'admin', '2025-10-15 11:00:00'),
(1, 2, 5, '接线端子防尘盖卡扣断裂安装5个有2个断裂质量不稳定。', NULL, 'resolved', '供应商改进注塑工艺补发10个改良版防尘盖复装后无断裂。', '采购部 王工', '2026-01-20 10:00:00', '2026-01-28 14:30:00', 'admin', '2026-01-28 14:30:00'),
(1, 2, 5, '开关电源输出电压波动大空载5.2V带载后降至4.6V不在5V±2%范围内。', NULL, 'pending', NULL, '采购部 张工', '2026-03-10 15:30:00', NULL, NULL, NULL);
-- 验证
SELECT '=== 异议数据统计 ===' AS info;
SELECT COUNT(*) AS total FROM biz_order_objection;
SELECT supplier_id, COUNT(*) AS cnt, SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) AS resolved, SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) AS pending, SUM(CASE WHEN status='open' THEN 1 ELSE 0 END) AS open_status FROM biz_order_objection GROUP BY supplier_id ORDER BY supplier_id;

55
sql/mock_report_data.sql Normal file
View File

@@ -0,0 +1,55 @@
-- 补充各月份测试数据,确保图表完整展示
-- 采购订单补充2025-07~2025-12、2026-02、2026-04~2026-06
-- RFQ补充2025-07~2025-11
SET NAMES utf8mb4;
-- === 补充采购订单(缺失月份)===
INSERT INTO biz_purchase_order (tenant_id, po_no, rfq_id, supplier_id, total_amount, currency, status, delivery_date, create_by, create_time) VALUES
(1, 'PO-2025-0701', 1, 1, 38500.00, 'CNY', 'confirmed', '2025-08-15', 'admin', '2025-07-10 10:00:00'),
(1, 'PO-2025-0801', 1, 2, 52300.00, 'CNY', 'confirmed', '2025-09-20', 'admin', '2025-08-12 11:00:00'),
(1, 'PO-2025-0901', 1, 3, 41800.00, 'CNY', 'confirmed', '2025-10-18', 'admin', '2025-09-05 09:30:00'),
(1, 'PO-2025-1001', 1, 1, 67200.00, 'CNY', 'confirmed', '2025-11-22', 'admin', '2025-10-08 14:00:00'),
(1, 'PO-2025-1101', 1, 4, 29400.00, 'CNY', 'confirmed', '2025-12-15', 'admin', '2025-11-03 10:30:00'),
(1, 'PO-2025-1201', 1, 5, 45600.00, 'CNY', 'confirmed', '2026-01-20', 'admin', '2025-12-07 13:00:00'),
(1, 'PO-2026-0201', 1, 2, 33800.00, 'CNY', 'confirmed', '2026-03-10', 'admin', '2026-02-14 10:00:00'),
(1, 'PO-2026-0401', 1, 3, 51200.00, 'CNY', 'confirmed', '2026-05-18', 'admin', '2026-04-09 11:30:00'),
(1, 'PO-2026-0501', 1, 1, 44900.00, 'CNY', 'confirmed', '2026-06-22', 'admin', '2026-05-11 09:00:00'),
(1, 'PO-2026-0601', 1, 5, 38700.00, 'CNY', 'confirmed', '2026-07-15', 'admin', '2026-06-05 14:30:00');
-- === 补充RFQ缺失月份2025-07~2025-11===
INSERT INTO biz_rfq (tenant_id, rfq_no, rfq_title, status, deadline, create_by, create_time) VALUES
(1, 'RFQ-2025-0701', '7月办公设备采购', 'closed', '2025-07-20 00:00:00', 'admin', '2025-07-05 10:00:00'),
(1, 'RFQ-2025-0801', '8月IT设备采购', 'closed', '2025-08-20 00:00:00', 'admin', '2025-08-05 10:00:00'),
(1, 'RFQ-2025-0901', '9月工业耗材采购', 'closed', '2025-09-20 00:00:00', 'admin', '2025-09-05 10:00:00'),
(1, 'RFQ-2025-1001', '10月电子元器件采购', 'closed', '2025-10-20 00:00:00', 'admin', '2025-10-05 10:00:00'),
(1, 'RFQ-2025-1101', '11月机械配件采购', 'closed', '2025-11-20 00:00:00', 'admin', '2025-11-05 10:00:00');
-- === 补充报价数据(缺失月份)===
INSERT INTO biz_quotation (tenant_id, rfq_id, supplier_id, quote_no, total_amount, currency, status, submit_time, create_by) VALUES
(1, 1, 1, 'QT-2025-0701', 36500.00, 'CNY', 'rejected', '2025-07-15 10:00:00', 'admin'),
(1, 1, 2, 'QT-2025-0702', 35200.00, 'CNY', 'accepted', '2025-07-15 11:00:00', 'admin'),
(1, 1, 3, 'QT-2025-0801', 51800.00, 'CNY', 'accepted', '2025-08-15 10:00:00', 'admin'),
(1, 1, 1, 'QT-2025-0802', 53500.00, 'CNY', 'rejected', '2025-08-15 11:00:00', 'admin'),
(1, 1, 4, 'QT-2025-0901', 41200.00, 'CNY', 'accepted', '2025-09-12 10:00:00', 'admin'),
(1, 1, 2, 'QT-2025-0902', 42500.00, 'CNY', 'rejected', '2025-09-12 11:00:00', 'admin'),
(1, 1, 5, 'QT-2025-1001', 66800.00, 'CNY', 'accepted', '2025-10-12 10:00:00', 'admin'),
(1, 1, 3, 'QT-2025-1002', 67500.00, 'CNY', 'rejected', '2025-10-12 11:00:00', 'admin'),
(1, 1, 1, 'QT-2025-1101', 29000.00, 'CNY', 'rejected', '2025-11-10 10:00:00', 'admin'),
(1, 1, 4, 'QT-2025-1102', 28500.00, 'CNY', 'accepted', '2025-11-10 11:00:00', 'admin'),
(1, 1, 2, 'QT-2026-0201', 33500.00, 'CNY', 'accepted', '2026-02-20 10:00:00', 'admin'),
(1, 1, 5, 'QT-2026-0202', 34200.00, 'CNY', 'rejected', '2026-02-20 11:00:00', 'admin'),
(1, 1, 3, 'QT-2026-0401', 50800.00, 'CNY', 'accepted', '2026-04-15 10:00:00', 'admin'),
(1, 1, 1, 'QT-2026-0402', 52000.00, 'CNY', 'rejected', '2026-04-15 11:00:00', 'admin'),
(1, 1, 5, 'QT-2026-0501', 44500.00, 'CNY', 'accepted', '2026-05-15 10:00:00', 'admin'),
(1, 1, 2, 'QT-2026-0502', 45200.00, 'CNY', 'rejected', '2026-05-15 11:00:00', 'admin'),
(1, 1, 1, 'QT-2026-0601', 38200.00, 'CNY', 'rejected', '2026-06-10 10:00:00', 'admin'),
(1, 1, 4, 'QT-2026-0602', 38500.00, 'CNY', 'accepted', '2026-06-10 11:00:00', 'admin');
-- === 验证 ===
SELECT '=== 采购订单月度分布 ===' AS info;
SELECT DATE_FORMAT(create_time,'%Y-%m') AS month, COUNT(*) AS cnt, SUM(total_amount) AS amt FROM biz_purchase_order GROUP BY month ORDER BY month;
SELECT '=== RFQ月度分布 ===' AS info;
SELECT DATE_FORMAT(create_time,'%Y-%m') AS month, COUNT(*) AS cnt FROM biz_rfq GROUP BY month ORDER BY month;
SELECT '=== 报价月度分布 ===' AS info;
SELECT DATE_FORMAT(submit_time,'%Y-%m') AS month, COUNT(*) AS cnt FROM biz_quotation WHERE submit_time IS NOT NULL GROUP BY month ORDER BY month;

View File

@@ -0,0 +1,84 @@
-- 供应商评价模拟数据55条
-- 覆盖5家供应商 x 12个月(2025-07 ~ 2026-06),各评分区间,多样化评价内容
SET NAMES utf8mb4;
INSERT INTO biz_supplier_evaluation (tenant_id, po_id, supplier_id, quality_score, delivery_score, service_score, price_score, total_score, comment, evaluator, eval_time) VALUES
-- ===== 供应商111条=====
(1, 1, 1, 5, 5, 4, 4, 4.5, '产品质量优秀均通过IQC检验送货及时销售响应迅速价格略高于市场均价。综合评价优秀列为A类合格供应商。', 'admin', '2025-07-08 10:30:00'),
(1, 1, 1, 5, 4, 5, 5, 4.8, '精密件质量达到图纸要求,安装调试支持专业,价格具有竞争力。推荐继续合作。', 'buyer1', '2025-08-15 14:00:00'),
(1, 1, 1, 4, 5, 4, 4, 4.3, '质量稳定,交期准时,服务态度好。', 'buyer2', '2025-09-12 09:15:00'),
(1, 1, 1, 5, 5, 5, 3, 4.5, '质量和服务一流但价格偏高建议年度谈判降价。整体表现优异关键技术指标全部达标包装规范完整随货附检验报告售后响应在2小时内。', 'admin', '2025-10-20 16:45:00'),
(1, 1, 1, 4, 4, 5, 4, 4.3, '整体表现良好,服务响应快。', 'buyer1', '2025-11-05 11:20:00'),
(1, 1, 1, 5, 4, 4, 5, 4.5, '性价比高,质量可靠。', 'buyer3', '2025-12-18 13:30:00'),
(1, 1, 1, 4, 5, 5, 4, 4.5, '交期准时,售后服务到位,技术支持专业。建议保持。', 'admin', '2026-01-10 15:00:00'),
(1, 1, 1, 5, 5, 4, 4, 4.5, '质量优异连续3批次零缺陷。', 'buyer2', '2026-02-14 10:00:00'),
(1, 1, 1, 3, 4, 4, 5, 4.0, '本批次有1件外观瑕疵已退换。价格优势明显交期正常。', 'buyer1', '2026-03-22 14:30:00'),
(1, 1, 1, 5, 5, 5, 4, 4.8, '全面优秀季度评审A级供应商。质量零缺陷交期提前2天服务主动跟进价格合理。', 'admin', '2026-04-08 09:45:00'),
(1, 1, 1, 4, 4, 4, 4, 4.0, '表现稳定,各维度均衡。', 'buyer3', '2026-05-15 11:00:00'),
-- ===== 供应商211条=====
(1, 2, 2, 4, 3, 4, 5, 4.0, '价格优势突出,但交期偶有延迟,需加强供应链管理。', 'admin', '2025-07-20 13:00:00'),
(1, 2, 2, 3, 2, 3, 5, 3.3, '交期严重延迟5天影响生产计划。价格有竞争力但需改善交付。', 'buyer1', '2025-08-25 10:30:00'),
(1, 2, 2, 4, 4, 3, 4, 3.8, '质量合格,服务一般,价格合理。', 'buyer2', '2025-09-18 15:20:00'),
(1, 2, 2, 2, 3, 2, 5, 3.0, '本批次不良率超标,已发起退货。服务响应慢,仅价格有优势。需限期整改。', 'admin', '2025-10-30 16:00:00'),
(1, 2, 2, 4, 4, 4, 5, 4.3, '整改后质量明显改善,交期恢复正常。', 'buyer1', '2025-11-22 09:00:00'),
(1, 2, 2, 3, 3, 3, 4, 3.3, '表现一般,各维度中等水平。', 'buyer3', '2025-12-10 14:15:00'),
(1, 2, 2, 4, 3, 4, 5, 4.0, '价格竞争力强,质量稳定,交期需关注。', 'admin', '2026-01-25 11:30:00'),
(1, 2, 2, 5, 4, 4, 4, 4.3, '质量提升显著,达到优秀水平。', 'buyer2', '2026-02-28 10:00:00'),
(1, 2, 2, 4, 4, 3, 5, 4.0, '性价比高,服务有待提升。', 'buyer1', '2026-03-15 13:45:00'),
(1, 2, 2, 3, 2, 3, 4, 3.0, '再次出现交期延迟,已约谈供应商负责人。质量波动较大,建议引入备选供应商。', 'admin', '2026-04-20 15:30:00'),
(1, 2, 2, 4, 4, 4, 5, 4.3, '本月表现回升,交期改善。', 'buyer3', '2026-05-28 09:30:00'),
-- ===== 供应商311条=====
(1, 3, 3, 4, 4, 5, 3, 4.0, '服务一流,技术支持专业,但价格偏高。', 'admin', '2025-07-12 14:00:00'),
(1, 3, 3, 5, 5, 5, 3, 4.5, '质量和服务满分,价格略高但物有所值。产品技术参数全部达标,包装精美,附完整技术文档。', 'buyer1', '2025-08-18 10:15:00'),
(1, 3, 3, 4, 5, 4, 4, 4.3, '交期提前3天到货质量合格。', 'buyer2', '2025-09-25 11:00:00'),
(1, 3, 3, 5, 4, 5, 3, 4.3, '技术实力强,售后响应快。', 'admin', '2025-10-15 15:45:00'),
(1, 3, 3, 4, 4, 4, 3, 3.8, '整体良好,价格需谈判。', 'buyer3', '2025-11-30 09:30:00'),
(1, 3, 3, 5, 5, 5, 4, 4.8, '年度最佳供应商全维度优秀。连续12个月零质量投诉技术团队专业紧急订单响应快价格合理。强烈推荐。', 'admin', '2025-12-20 14:00:00'),
(1, 3, 3, 4, 5, 4, 3, 4.0, '交期表现优异,价格偏高。', 'buyer1', '2026-01-18 10:30:00'),
(1, 3, 3, 5, 4, 5, 4, 4.5, '质量稳定,服务优秀。', 'buyer2', '2026-02-20 13:00:00'),
(1, 3, 3, 4, 4, 5, 3, 4.0, '服务持续优秀,价格竞争力不足。', 'admin', '2026-03-28 15:00:00'),
(1, 3, 3, 5, 5, 4, 4, 4.5, '质量满分,交期准时。', 'buyer3', '2026-04-25 11:15:00'),
(1, 3, 3, 4, 5, 5, 4, 4.5, '综合表现优秀,推荐续约。', 'buyer1', '2026-05-30 09:45:00'),
-- ===== 供应商411条无采购订单po_id用1占位=====
(1, 1, 4, 3, 3, 3, 3, 3.0, '新供应商首单评价,各维度表现中等,需持续观察。', 'admin', '2025-07-25 10:00:00'),
(1, 1, 4, 2, 2, 3, 4, 2.8, '质量不达标2件不合格已退货。交期延迟服务一般价格有优势。', 'buyer1', '2025-08-30 14:30:00'),
(1, 1, 4, 3, 3, 2, 4, 3.0, '表现不佳,已发出整改通知。', 'buyer2', '2025-09-20 11:00:00'),
(1, 1, 4, 1, 2, 1, 5, 2.3, '严重质量问题批量退货。服务极差拒绝配合整改。仅价格低廉。建议列入观察名单限期3个月整改。', 'admin', '2025-10-10 16:00:00'),
(1, 1, 4, 3, 3, 3, 4, 3.3, '整改后略有改善,仍需提升。', 'buyer3', '2025-11-15 13:15:00'),
(1, 1, 4, 4, 3, 3, 4, 3.5, '质量改善明显,服务仍需加强。', 'admin', '2025-12-05 10:30:00'),
(1, 1, 4, 3, 4, 3, 4, 3.5, '交期改善,质量基本合格。', 'buyer1', '2026-01-30 14:00:00'),
(1, 1, 4, 4, 4, 4, 3, 3.8, '持续改善中,各维度趋于稳定。', 'buyer2', '2026-02-25 09:30:00'),
(1, 1, 4, 3, 3, 4, 4, 3.5, '服务有所提升,价格合理。', 'buyer3', '2026-03-18 15:30:00'),
(1, 1, 4, 4, 4, 3, 4, 3.8, '整体趋于稳定,建议继续观察。', 'admin', '2026-04-15 11:45:00'),
(1, 1, 4, 4, 5, 4, 3, 4.0, '交期表现优秀,质量合格,持续改善。', 'buyer1', '2026-05-20 10:00:00'),
-- ===== 供应商511条无采购订单po_id用2占位=====
(1, 2, 5, 4, 4, 4, 4, 4.0, '新引入供应商,首单表现合格。', 'admin', '2025-07-30 13:30:00'),
(1, 2, 5, 5, 4, 5, 3, 4.3, '质量优秀,服务热情,价格略高。', 'buyer1', '2025-08-22 10:00:00'),
(1, 2, 5, 4, 5, 4, 4, 4.3, '交期提前到货,整体满意。', 'buyer2', '2025-09-28 14:15:00'),
(1, 2, 5, 5, 5, 5, 4, 4.8, '全面优秀强烈推荐。产品质量卓越技术参数全部达标交期准时售后响应1小时内包装规范完整。', 'admin', '2025-10-25 11:30:00'),
(1, 2, 5, 4, 4, 5, 3, 4.0, '服务优秀,价格需优化。', 'buyer3', '2025-11-20 15:00:00'),
(1, 2, 5, 5, 4, 4, 4, 4.3, '质量稳定,综合表现良好。', 'admin', '2025-12-28 09:45:00'),
(1, 2, 5, 4, 5, 5, 3, 4.3, '交期和服务满分,价格偏高。', 'buyer1', '2026-01-15 13:00:00'),
(1, 2, 5, 5, 5, 4, 4, 4.5, '质量优异,连续批次零缺陷。', 'buyer2', '2026-02-18 10:30:00'),
(1, 2, 5, 4, 4, 4, 4, 4.0, '表现稳定,各维度均衡。', 'buyer3', '2026-03-25 14:45:00'),
(1, 2, 5, 5, 4, 5, 4, 4.5, '质量和服务持续优秀。', 'admin', '2026-04-30 11:00:00'),
(1, 2, 5, 4, 5, 5, 4, 4.5, '综合优秀推荐升级为A级供应商。', 'buyer1', '2026-05-25 09:15:00');
-- 验证
SELECT '=== 评价数据统计 ===' AS info;
SELECT COUNT(*) AS total_evaluations FROM biz_supplier_evaluation;
SELECT supplier_id, COUNT(*) AS eval_count,
ROUND(AVG(quality_score),1) AS avg_quality,
ROUND(AVG(delivery_score),1) AS avg_delivery,
ROUND(AVG(service_score),1) AS avg_service,
ROUND(AVG(price_score),1) AS avg_price,
ROUND(AVG(total_score),1) AS avg_total
FROM biz_supplier_evaluation GROUP BY supplier_id ORDER BY supplier_id;
SELECT DATE_FORMAT(eval_time,'%Y-%m') AS month, COUNT(*) AS cnt
FROM biz_supplier_evaluation GROUP BY month ORDER BY month;