41d885b2b3f731650328813da002be9b050d5805..446434c70bdfb85dfe9268791fe819f54141cab5
8 天以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
446434 对比 | 目录
8 天以前 spring
fix: 库存管理与入库报表页面建议都做下分页
e505a8 对比 | 目录
8 天以前 zss
Merge remote-tracking branch 'origin/dev_New' into dev_New
d51e42 对比 | 目录
8 天以前 zss
库存报表中生产报工-报废来源未能展示
f65d1a 对比 | 目录
8 天以前 spring
fix: 完成用户评价、打卡签到页面
69e864 对比 | 目录
8 天以前 spring
fix: 售后质量追溯
f1e300 对比 | 目录
8 天以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
20259b 对比 | 目录
8 天以前 spring
fix: 完成车辆管理、车辆油耗管理、运输任务管理页面编辑
c34443 对比 | 目录
8 天以前 gongchunyi
fix: 生产核算分析修改对接接口
00ec5a 对比 | 目录
8 天以前 gongchunyi
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
9cf2a2 对比 | 目录
8 天以前 gongchunyi
feat: 首页工序数据生产统计明细、质量统计接口对接
2661e2 对比 | 目录
8 天以前 spring
fix: 售后管理,添加附件、维修记录前端页面
238542 对比 | 目录
8 天以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
e1a081 对比 | 目录
8 天以前 spring
fix: 解决大屏冲突
b651c3 对比 | 目录
8 天以前 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
208882 对比 | 目录
8 天以前 gaoluyang
进销存升级 1.采购台账中有多个产品时(处于待审核状态),删除其中一个产品后保存,提示有多个,后面重新查看此台账,产品成功删除。
b77bf8 对比 | 目录
8 天以前 zhang_nuo
首页工序数据生产统计明细、质量统计
dff9f0 对比 | 目录
8 天以前 spring
fix: 质量数据分析
fae43c 对比 | 目录
8 天以前 spring
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
b12b55 对比 | 目录
8 天以前 spring
fix: 大屏样式修改
cbe874 对比 | 目录
9 天以前 huminmin
修改入库、出库来源选项
814958 对比 | 目录
9 天以前 huminmin
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
38ff45 对比 | 目录
9 天以前 huminmin
删除生产订单
18e0b5 对比 | 目录
9 天以前 gongchunyi
fix: 未完成付款判断错误
c0b986 对比 | 目录
9 天以前 huminmin
新增生产订单
176ca0 对比 | 目录
2026-02-03 gongchunyi
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
21b1ac 对比 | 目录
2026-02-03 gongchunyi
fix: 备注字段名与后端不不一致
b086dc 对比 | 目录
2026-02-03 huminmin
原材料检验增加采购订单号;过程检验和出厂检验增加生产工单号
31529c 对比 | 目录
2026-02-03 gongchunyi
fix: 销售报价中规格型号、单位、单价改为必填项校验
789c3a 对比 | 目录
2026-02-03 gongchunyi
feat: 添加备注列
119731 对比 | 目录
2026-02-03 gongchunyi
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
d1a90b 对比 | 目录
2026-02-03 gongchunyi
fix: 输入框输入位数长度、滑动条
649cf9 对比 | 目录
2026-02-03 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
4657a2 对比 | 目录
2026-02-03 gaoluyang
进销存升级 1.先查看规章制度管理条数为3条,后查看用印管理页面记录,由原来6条变为3条 2.用印管理分页bug
b3f818 对比 | 目录
2026-02-03 gongchunyi
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
a42d42 对比 | 目录
2026-02-03 gongchunyi
fix: 工单执行效率分析接口错误
dc7ead 对比 | 目录
2026-02-03 gaoluyang
Merge remote-tracking branch 'origin/dev_New' into dev_New
07a33c 对比 | 目录
2026-02-03 gaoluyang
进销存升级 1.协同办公—知识库,知识库点击第二页还是展示第一页
c7836a 对比 | 目录
2026-02-03 zss
Merge remote-tracking branch 'origin/dev_New' into dev_New
0cac9f 对比 | 目录
2026-02-03 zss
调整隐患上报的整改与验收权限
d9746b 对比 | 目录
2026-02-03 gaoluyang
进销存升级 1.生产报工带出来的出厂检数据编辑时,规格型号回显还是有问题 2.用印管理分页数字展示有误
f54d7f 对比 | 目录
2026-02-03 gaoluyang
进销存升级 1.首页应付应收不要筛选类型
c2a4ff 对比 | 目录
2026-02-03 huminmin
根据是否发货,展示销售台账,生产订单列表表格颜色
a34001 对比 | 目录
2026-02-02 gongchunyi
Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-ma...
5a5f5d 对比 | 目录
2026-02-02 gongchunyi
feat: 进销质量类分析接口对接
e1535c 对比 | 目录
2026-02-02 huminmin
工序,工艺路线增加是否质检
241197 对比 | 目录
2026-02-02 huminmin
工序,工艺路线增加是否质检
825155 对比 | 目录
2026-02-02 张诺
Merge remote-tracking branch 'origin/dev_New' into dev_New
11e1de 对比 | 目录
2026-02-02 张诺
BI大屏质量分析模块
90efce 对比 | 目录
2026-02-02 gaoluyang
进销存升级 1.生产报工带出来的原材料、过程、出厂检数据编辑时,规格型号回显还是有问题
714ab1 对比 | 目录
已添加19个文件
已修改47个文件
11159 ■■■■ 文件已修改
src/api/basicData/enum.js 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/customerService/index.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/reportAnalysis/qualityReport.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/viewIndex.js 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/FileListDialog.vue 543 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Echarts/echarts.vue 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 54 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue 122 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 589 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/afterSalesHandling/index.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/attendanceManagement/index.vue 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 600 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/document/attachmentManager.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 1691 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/Record.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 59 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/transportTaskManagement/index.vue 692 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleFuelManagement/index.vue 556 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleManagement/index.vue 581 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/index.vue 469 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/filesDia.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/New.vue 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/afterSalesTraceability/index.vue 595 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 44 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-top.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-center.vue 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-top.vue 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/left-bottom.vue 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue 448 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue 189 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/right-top.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/index.vue 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/reportManagement/index.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/accidentReportingRecord/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/dangerInvestigation/index.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/emergencyPlanReview/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/hazardSourceLedger/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/hazardousMaterialsControl/index.vue 68 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeQualifications/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safetyTrainingAssessment/index.vue 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPayment/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 40 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/enum.js
@@ -7,17 +7,35 @@
    })
}
export function findAllQualifiedStockRecordTypeOptions() {
// åˆæ ¼å…¥åº“来源类型
export function findAllQualifiedStockInRecordTypeOptions() {
    return request({
        url: '/basic/enum/StockQualifiedRecordTypeEnum',
        url: '/basic/enum/StockInQualifiedRecordTypeEnum',
        method: 'get'
    })
}
export function findAllUnqualifiedStockRecordTypeOptions() {
// ä¸åˆæ ¼å…¥åº“来源类型
export function findAllUnQualifiedStockInRecordTypeOptions() {
    return request({
        url: '/basic/enum/StockUnQualifiedRecordTypeEnum',
        url: '/basic/enum/StockInUnQualifiedRecordTypeEnum',
        method: 'get'
    })
}
// åˆæ ¼å‡ºåº“来源类型
export function findAllQualifiedStockOutRecordTypeOptions() {
    return request({
        url: '/basic/enum/StockOutQualifiedRecordTypeEnum',
        method: 'get'
    })
}
// ä¸åˆæ ¼å‡ºåº“来源类型
export function findAllUnQualifiedStockOutRecordTypeOptions() {
    return request({
        url: '/basic/enum/StockOutUnQualifiedRecordTypeEnum',
        method: 'get'
    })
}
src/api/customerService/index.js
@@ -42,6 +42,40 @@
  })
}
// å”®åŽå¤„理-附件列表
export function afterSalesServiceFileListPage(query) {
  return request({
    url: '/afterSalesService/file/listPage',
    method: 'get',
    params: query,
  })
}
// å”®åŽå¤„理-附件新增
export function afterSalesServiceFileAdd(data) {
  return request({
    url: '/afterSalesService/file/add',
    method: 'post',
    data,
  })
}
// å”®åŽå¤„理-附件删除
export function afterSalesServiceFileDel(ids) {
  return request({
    url: '/afterSalesService/file/del',
    method: 'delete',
    data: ids,
  })
}
// å”®åŽå¤„理-维修记录列表
export function afterSalesServiceRepairListPage(query) {
  return request({
    url: '/afterSalesService/repair/listPage',
    method: 'get',
    params: query,
  })
}
// ä¸´æœŸå”®åŽç®¡ç†-分页查询
export function expiryAfterSalesListPage(query) {
  return request({
src/api/productionManagement/productionOrder.js
@@ -36,6 +36,22 @@
  });
}
// ç”Ÿäº§è®¢å•-新增
export function addProductOrder(data) {
  return request({
    url: "/productOrder/addProductOrder",
    method: "post",
    data: data,
  });
}
export function delProductOrder(ids) {
  return request({
    url: `/productOrder/${ids}`,
    method: "delete",
  });
}
// ç”Ÿäº§è®¢å•-查询产品结构列表
export function listProcessBom(query) {
  return request({
src/api/reportAnalysis/qualityReport.js
@@ -43,10 +43,10 @@
}
// èŽ·å–çƒ­ç‚¹æ£€æµ‹æŒ‡æ ‡ç»Ÿè®¡
export function getTopParameters(inspectType) {
export function getTopParameters(modelType) {
  return request({
    url: '/qualityReport/getTopParameters',
    method: 'get',
    params: { inspectType }
    params: { modelType }
  })
}
src/api/viewIndex.js
@@ -1,6 +1,91 @@
// é¦–页接口
import request from "@/utils/request";
//  å·¥åºæ•°æ®ç”Ÿäº§ç»Ÿè®¡æ˜Žç»†
export const processDataProductionStatistics = (params) => {
  return request({
    url: "/home/processDataProductionStatistics",
    method: "get",
    params,
  });
};
//  è´¨é‡ç»Ÿè®¡
export const qualityInspectionStatistics = (params) => {
  return request({
    url: "/home/qualityInspectionStatistics",
    method: "get",
    params,
  });
};
//  åŽŸææ–™æ£€æµ‹
export const rawMaterialDetection = (query) => {
  return request({
    url: "/home/rawMaterialDetection",
    method: "get",
    params: query,
  });
};
//  è¿‡ç¨‹æ£€æµ‹
export const processDetection = (query) => {
  return request({
    url: "/home/processDetection",
    method: "get",
    params: query,
  });
};
//  æˆå“å‡ºåŽ‚æ£€æµ‹
export const factoryDetection = (query) => {
  return request({
    url: "/home/factoryDetection",
    method: "get",
    params: query,
  });
};
//  æ£€éªŒæ•°é‡
export const qualityInspectionCount = () => {
  return request({
    url: "/home/qualityInspectionCount",
    method: "get",
  });
};
//  ä¸åˆæ ¼é¢„è­¦
export const nonComplianceWarning = () => {
  return request({
    url: "/home/nonComplianceWarning",
    method: "get",
  });
};
//  å®Œæˆæ£€éªŒæ•°
export const completedInspectionCount = () => {
  return request({
    url: "/home/completedInspectionCount",
    method: "get",
  });
};
//  ä¸åˆæ ¼äº§å“æŽ’名
export const unqualifiedProductRanking = () => {
  return request({
    url: "/home/unqualifiedProductRanking",
    method: "get",
  });
};
//  ä¸åˆæ ¼æ£€å“å¤„理分析
export const unqualifiedProductProcessingAnalysis = () => {
  return request({
    url: "/home/unqualifiedProductProcessingAnalysis",
    method: "get",
  });
};
// é”€å”®-采购-库存数据
export const getBusiness = () => {
  return request({
@@ -15,11 +100,30 @@
    method: "get",
  });
};
// è´¨æ£€åˆ†æž
export const qualityStatistics = () => {
// è´¨æ£€åˆ†æžï¼ˆå¯ä¼  dateType: 1周 2月 3季度)
export const qualityStatistics = (params) => {
  return request({
    url: "/home/qualityStatistics",
    method: "get",
    params,
  });
};
// å·¥å•执行效率分析(dateType: 1周 2月 3季度)
export const workOrderEfficiencyAnalysis = (params) => {
  return request({
    url: "/home/workOrderEfficiencyAnalysis",
    method: "get",
    params,
  });
};
// ç”Ÿäº§æ ¸ç®—分析
export const productionAccountingAnalysis = (query) => {
  return request({
    url: "/home/productionAccountingAnalysis",
    method: "get",
    params: query,
  });
};
// åº”收应付统计
@@ -51,6 +155,14 @@
export const getProgressStatistics = () => {
  return request({
    url: "/home/progressStatistics",
    method: "get",
  });
};
// è®¢å•数量统计(生产订单数、已完成订单数、待生产订单数)
export const orderCount = () => {
  return request({
    url: "/home/orderCount",
    method: "get",
  });
};
@@ -123,6 +235,15 @@
  });
};
// å·¥åºäº§å‡ºåˆ†æžï¼ˆdateType: 1周 2月 3季度)
export const processOutputAnalysis = (params) => {
  return request({
    url: "/home/processOutputAnalysis",
    method: "get",
    params,
  });
};
// åŽŸææ–™é‡‡è´­é‡‘é¢å æ¯”
export const rawMaterialPurchaseAmountRatio = () => {
  return request({
@@ -157,6 +278,15 @@
  });
};
// æŠ•入产出分析
export const inputOutputAnalysis = (params) => {
  return request({
    url: "/home/inputOutputAnalysis",
    method: "get",
    params,
  });
};
// äº§å“å‘¨è½¬å¤©æ•°
export const productTurnoverDays = () => {
  return request({
src/components/Dialog/FileListDialog.vue
@@ -1,309 +1,328 @@
<template>
  <el-dialog
    v-model="dialogVisible"
    :title="title"
    :width="width"
    :before-close="handleClose"
  >
    <div class="file-list-toolbar" v-if="showToolbar">
  <el-dialog v-model="dialogVisible"
             :title="title"
             :width="width"
             :before-close="handleClose">
    <div class="file-list-toolbar"
         v-if="showToolbar">
      <template v-if="useBuiltInUpload">
        <el-upload
          v-model:file-list="uploadFileList"
          class="upload-demo"
          :action="uploadAction"
          :headers="uploadHeaders"
          :show-file-list="false"
          :on-success="handleDefaultUploadSuccess"
          :on-error="handleDefaultUploadError"
        >
          <el-button
            v-if="showUploadButton"
            type="primary"
            size="small"
          >
        <el-upload v-model:file-list="uploadFileList"
                   class="upload-demo"
                   :action="uploadAction"
                   :headers="uploadHeaders"
                   :show-file-list="false"
                   :on-success="handleDefaultUploadSuccess"
                   :on-error="handleDefaultUploadError">
          <el-button v-if="showUploadButton"
                     type="primary"
                     size="small">
            ä¸Šä¼ é™„ä»¶
          </el-button>
        </el-upload>
      </template>
      <template v-else>
        <el-button
          v-if="showUploadButton"
          type="primary"
          size="small"
          @click="handleUpload"
        >
        <el-button v-if="showUploadButton"
                   type="primary"
                   size="small"
                   @click="handleUpload">
          æ–°å¢žé™„ä»¶
        </el-button>
      </template>
    </div>
    <el-table :data="tableData" border :height="tableHeight">
      <el-table-column
        :label="nameColumnLabel"
        :prop="nameColumnProp"
        :min-width="nameColumnMinWidth"
        show-overflow-tooltip
      />
      <el-table-column
        v-if="showActions"
        fixed="right"
        label="操作"
        :width="actionColumnWidth"
        align="center"
      >
    <el-table :data="tableData"
              border
              :height="tableHeight">
      <el-table-column :label="nameColumnLabel"
                       :prop="nameColumnProp"
                       :min-width="nameColumnMinWidth"
                       show-overflow-tooltip />
      <el-table-column v-if="showActions"
                       fixed="right"
                       label="操作"
                       :width="actionColumnWidth"
                       align="center">
        <template #default="scope">
          <el-button
            v-if="showDownload"
            link
            type="primary"
            size="small"
            @click="handleDownload(scope.row)"
          >
          <el-button v-if="showDownload"
                     link
                     type="primary"
                     size="small"
                     @click="handleDownload(scope.row)">
            ä¸‹è½½
          </el-button>
          <el-button
            v-if="showPreview"
            link
            type="primary"
            size="small"
            @click="handlePreview(scope.row)"
          >
          <el-button v-if="showPreview"
                     link
                     type="primary"
                     size="small"
                     @click="handlePreview(scope.row)">
            é¢„览
          </el-button>
          <el-button
            v-if="showDeleteButton"
            link
            type="danger"
            size="small"
            @click="handleDelete(scope.row, scope.$index)"
          >
          <el-button v-if="showDeleteButton"
                     link
                     type="danger"
                     size="small"
                     @click="handleDelete(scope.row, scope.$index)">
            åˆ é™¤
          </el-button>
          <slot name="actions" :row="scope.row"></slot>
          <slot name="actions"
                :row="scope.row"></slot>
        </template>
      </el-table-column>
      <slot name="columns"></slot>
    </el-table>
    <pagination v-if="isShowPagination"
                style="margin-bottom: 20px;"
                :total="page.total"
                :page="page.current"
                :limit="page.size"
                @pagination="paginationSearch"
                @change="handleChange" />
  </el-dialog>
  <filePreview v-if="showPreview" ref="filePreviewRef" />
  <filePreview v-if="showPreview"
               ref="filePreviewRef" />
</template>
<script setup>
import { ref, computed, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import filePreview from '@/components/filePreview/index.vue'
import { getToken } from '@/utils/auth'
  import { ref, computed, getCurrentInstance } from "vue";
  import pagination from "@/components/Pagination/index.vue";
  import { ElMessage } from "element-plus";
  import filePreview from "@/components/filePreview/index.vue";
  import { getToken } from "@/utils/auth";
const props = defineProps({
  modelValue: {
    type: Boolean,
    default: false
  },
  title: {
    type: String,
    default: '附件'
  },
  width: {
    type: String,
    default: '40%'
  },
  tableHeight: {
    type: String,
    default: '40vh'
  },
  nameColumnLabel: {
    type: String,
    default: '附件名称'
  },
  nameColumnProp: {
    type: String,
    default: 'name'
  },
  nameColumnMinWidth: {
    type: [String, Number],
    default: 400
  },
  actionColumnWidth: {
    type: [String, Number],
    default: 160
  },
  showActions: {
    type: Boolean,
    default: true
  },
  showDownload: {
    type: Boolean,
    default: true
  },
  showPreview: {
    type: Boolean,
    default: true
  },
  showUploadButton: {
    type: Boolean,
    default: false
  },
  showDeleteButton: {
    type: Boolean,
    default: false
  },
  urlField: {
    type: String,
    default: 'url'
  },
  downloadMethod: {
    type: Function,
    default: null
  },
  previewMethod: {
    type: Function,
    default: null
  },
  uploadMethod: {
    type: Function,
    default: null
  },
  deleteMethod: {
    type: Function,
    default: null
  },
  rulesRegulationsManagementId: {
    type: [String, Number],
    default: ''
  },
  uploadUrl: {
    type: String,
    default: `${import.meta.env.VITE_APP_BASE_API}/file/upload`
  }
})
  const props = defineProps({
    modelValue: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "附件",
    },
    width: {
      type: String,
      default: "40%",
    },
    tableHeight: {
      type: String,
      default: "40vh",
    },
    nameColumnLabel: {
      type: String,
      default: "附件名称",
    },
    nameColumnProp: {
      type: String,
      default: "name",
    },
    nameColumnMinWidth: {
      type: [String, Number],
      default: 400,
    },
    actionColumnWidth: {
      type: [String, Number],
      default: 160,
    },
    showActions: {
      type: Boolean,
      default: true,
    },
    showDownload: {
      type: Boolean,
      default: true,
    },
    showPreview: {
      type: Boolean,
      default: true,
    },
    showUploadButton: {
      type: Boolean,
      default: false,
    },
    showDeleteButton: {
      type: Boolean,
      default: false,
    },
    urlField: {
      type: String,
      default: "url",
    },
    downloadMethod: {
      type: Function,
      default: null,
    },
    previewMethod: {
      type: Function,
      default: null,
    },
    uploadMethod: {
      type: Function,
      default: null,
    },
    deleteMethod: {
      type: Function,
      default: null,
    },
    rulesRegulationsManagementId: {
      type: [String, Number],
      default: "",
    },
    uploadUrl: {
      type: String,
      default: `${import.meta.env.VITE_APP_BASE_API}/file/upload`,
    },
    isShowPagination: {
      type: Boolean,
      default: false,
    },
    page: {
      type: Object,
      default: () => ({
        current: 1,
        size: 10,
        total: 0,
      }),
    },
  });
const emit = defineEmits(['update:modelValue', 'close', 'download', 'preview', 'upload', 'delete'])
  const emit = defineEmits([
    "update:modelValue",
    "close",
    "download",
    "preview",
    "upload",
    "delete",
  ]);
const { proxy } = getCurrentInstance()
const filePreviewRef = ref(null)
const uploadFileList = ref([])
  const { proxy } = getCurrentInstance();
  const filePreviewRef = ref(null);
  const uploadFileList = ref([]);
const dialogVisible = computed({
  get: () => props.modelValue,
  set: (val) => emit('update:modelValue', val)
})
  const dialogVisible = computed({
    get: () => props.modelValue,
    set: val => emit("update:modelValue", val),
  });
const tableData = ref([])
const showToolbar = computed(() => props.showUploadButton)
const useBuiltInUpload = computed(() => !props.uploadMethod)
const uploadAction = computed(() => props.uploadUrl)
const uploadHeaders = computed(() => ({
  Authorization: `Bearer ${getToken()}`
}))
  const tableData = ref([]);
  const showToolbar = computed(() => props.showUploadButton);
  const useBuiltInUpload = computed(() => !props.uploadMethod);
  const uploadAction = computed(() => props.uploadUrl);
  const uploadHeaders = computed(() => ({
    Authorization: `Bearer ${getToken()}`,
  }));
const handleClose = () => {
  emit('close')
  dialogVisible.value = false
}
  const handleClose = () => {
    emit("close");
    dialogVisible.value = false;
  };
const handleDownload = (row) => {
  if (props.downloadMethod) {
    props.downloadMethod(row)
  } else {
    // é»˜è®¤ä¸‹è½½æ–¹æ³•
    proxy.$download.name(row[props.urlField])
  }
  emit('download', row)
}
const handlePreview = (row) => {
  if (props.previewMethod) {
    props.previewMethod(row)
  } else {
    // é»˜è®¤é¢„览方法
    if (filePreviewRef.value) {
      filePreviewRef.value.open(row[props.urlField])
  const handleDownload = row => {
    if (props.downloadMethod) {
      props.downloadMethod(row);
    } else {
      // é»˜è®¤ä¸‹è½½æ–¹æ³•
      proxy.$download.name(row[props.urlField]);
    }
  }
  emit('preview', row)
}
    emit("download", row);
  };
const open = (list) => {
  dialogVisible.value = true
  tableData.value = list || []
}
const handleUpload = async () => {
  if (props.uploadMethod) {
    // å¦‚果提供了自定义上传方法,由父组件负责更新列表(通过 setList)
    // è¿™é‡Œä¸å†è‡ªåŠ¨æ·»åŠ ï¼Œé¿å…ä¸Žçˆ¶ç»„ä»¶çš„ setList é‡å¤
    await props.uploadMethod()
  }
  emit('upload')
}
const handleDelete = async (row, index) => {
  if (props.deleteMethod) {
    const result = await props.deleteMethod(row, index)
    if (result === false) {
      return
  const handlePreview = row => {
    if (props.previewMethod) {
      props.previewMethod(row);
    } else {
      // é»˜è®¤é¢„览方法
      if (filePreviewRef.value) {
        filePreviewRef.value.open(row[props.urlField]);
      }
    }
    // å¦‚果提供了 deleteMethod,由父组件负责刷新列表,不在这里删除
  } else {
    // å¦‚果没有提供 deleteMethod,才在组件内部删除
    removeAttachment(index)
  }
  emit('delete', row)
}
    emit("preview", row);
  };
  const paginationSearch = page => {
    props.page.current = page.page;
    props.page.size = page.limit;
    emit("pagination", page.page, page.limit);
  };
const addAttachment = (item) => {
  tableData.value = [...tableData.value, item]
}
  const open = list => {
    dialogVisible.value = true;
    tableData.value = list || [];
  };
const handleDefaultUploadSuccess = async (res, file) => {
  if (res?.code !== 200) {
    ElMessage.error(res?.msg || '文件上传失败')
    return
  }
  if (!props.rulesRegulationsManagementId) {
    ElMessage.error('缺少规章制度ID,无法保存附件')
    return
  }
  const fileName = res?.data?.originalName || file?.name
  const fileUrl = res?.data?.tempPath || res?.data?.url
  const payload = {
    fileName,
    fileUrl,
    rulesRegulationsManagementId: props.rulesRegulationsManagementId,
    raw: res?.data || {}
  }
  emit('upload', payload)
}
  const handleUpload = async () => {
    if (props.uploadMethod) {
      // å¦‚果提供了自定义上传方法,由父组件负责更新列表(通过 setList)
      // è¿™é‡Œä¸å†è‡ªåŠ¨æ·»åŠ ï¼Œé¿å…ä¸Žçˆ¶ç»„ä»¶çš„ setList é‡å¤
      await props.uploadMethod();
    }
    emit("upload");
  };
const handleDefaultUploadError = () => {
  ElMessage.error('文件上传失败')
}
  const handleDelete = async (row, index) => {
    if (props.deleteMethod) {
      const result = await props.deleteMethod(row, index);
      if (result === false) {
        return;
      }
      // å¦‚果提供了 deleteMethod,由父组件负责刷新列表,不在这里删除
    } else {
      // å¦‚果没有提供 deleteMethod,才在组件内部删除
      removeAttachment(index);
    }
    emit("delete", row);
  };
const removeAttachment = (index) => {
  if (index > -1 && index < tableData.value.length) {
    const newList = [...tableData.value]
    newList.splice(index, 1)
    tableData.value = newList
  }
}
  const addAttachment = item => {
    tableData.value = [...tableData.value, item];
  };
const setList = (list) => {
  tableData.value = list || []
}
  const handleDefaultUploadSuccess = async (res, file) => {
    if (res?.code !== 200) {
      ElMessage.error(res?.msg || "文件上传失败");
      return;
    }
    if (!props.rulesRegulationsManagementId) {
      ElMessage.error("缺少规章制度ID,无法保存附件");
      return;
    }
    const fileName = res?.data?.originalName || file?.name;
    const fileUrl = res?.data?.tempPath || res?.data?.url;
    const payload = {
      fileName,
      fileUrl,
      rulesRegulationsManagementId: props.rulesRegulationsManagementId,
      raw: res?.data || {},
    };
    emit("upload", payload);
  };
defineExpose({
  open,
  addAttachment,
  removeAttachment,
  setList,
  handleUpload,
  handleDelete
})
  const handleDefaultUploadError = () => {
    ElMessage.error("文件上传失败");
  };
  const removeAttachment = index => {
    if (index > -1 && index < tableData.value.length) {
      const newList = [...tableData.value];
      newList.splice(index, 1);
      tableData.value = newList;
    }
  };
  const setList = list => {
    tableData.value = list || [];
  };
  defineExpose({
    open,
    addAttachment,
    removeAttachment,
    setList,
    handleUpload,
    handleDelete,
  });
</script>
<style scoped>
.file-list-toolbar {
  margin-bottom: 8px;
  text-align: right;
}
  .file-list-toolbar {
    margin-bottom: 8px;
    text-align: right;
  }
</style>
src/components/Echarts/echarts.vue
@@ -9,7 +9,7 @@
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
const emit = defineEmits(['finished'])
const emit = defineEmits(['finished', 'click'])
// Props
const props = defineProps({
@@ -26,7 +26,7 @@
  },
  dataset: {
    type: Object,
    default: () => {}
    default: () => { }
  },
  xAxis: {
    type: Array,
@@ -82,10 +82,10 @@
    type: Object,
    default: () => ({})
  },
    option: {
        type: Object,
        default: () => ({})
    },
  option: {
    type: Object,
    default: () => ({})
  },
})
import { watch } from 'vue'
@@ -128,6 +128,9 @@
  chartInstance = echarts.init(chartRef.value)
  finishedHandler = () => emit('finished')
  chartInstance.on('finished', finishedHandler)
  chartInstance.on('click', (params) => {
    emit('click', params)
  })
  renderChart()
  // setOption åŽè¡¥ä¸€æ¬¡ resize,确保首屏尺寸正确
  nextTick(() => {
@@ -137,8 +140,8 @@
// Methods
function generateChart(option) {
  const copiedOption = option
  const copiedOption = option
  if (copiedOption.series && copiedOption.series.length > 0) {
    copiedOption.series.forEach((s, index) => {
      if (s.type === 'line' && props.lineColors.length) {
@@ -152,7 +155,7 @@
      }
    })
  }
  chartInstance.setOption(copiedOption)
}
@@ -170,7 +173,7 @@
    tooltip: props.tooltip,
    visualMap: Object.keys(props.visualMap).length ? props.visualMap : undefined,
  }
  chartInstance.clear()
  generateChart(option)
}
@@ -202,19 +205,19 @@
// Watch all reactive props that affect the chart
watch(
    () => [props.xAxis, props.yAxis, props.series, props.legend, props.tooltip, props.visualMap],
    () => {
      // å¦‚果首屏还没初始化成功,等待容器 ready åŽå†æ¸²æŸ“
      if (!chartInstance) {
        initChartWhenReady()
        return
      }
      renderChart()
      // æ•°æ®å˜åŒ–后补一次 resize,避免布局变化导致的偏移
      nextTick(() => {
        if (chartInstance) chartInstance.resize()
      })
    },
    { deep: true, immediate: true }
  () => [props.xAxis, props.yAxis, props.series, props.legend, props.tooltip, props.visualMap],
  () => {
    // å¦‚果首屏还没初始化成功,等待容器 ready åŽå†æ¸²æŸ“
    if (!chartInstance) {
      initChartWhenReady()
      return
    }
    renderChart()
    // æ•°æ®å˜åŒ–后补一次 resize,避免布局变化导致的偏移
    nextTick(() => {
      if (chartInstance) chartInstance.resize()
    })
  },
  { deep: true, immediate: true }
)
</script>
src/views/basicData/product/index.vue
@@ -30,11 +30,8 @@
          :props="{ children: 'children', label: 'label' }"
          highlight-current
          node-key="id"
          style="
            height: calc(100vh - 190px);
            overflow-y: scroll;
            scrollbar-width: none;
          "
          class="product-tree-scroll"
          style="height: calc(100vh - 190px); overflow-y: auto"
        >
          <template #default="{ node, data }">
            <div class="custom-tree-node">
@@ -43,7 +40,7 @@
                  <component :is="data.children && data.children.length > 0
                  ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
                </el-icon>
                {{ data.label }}
                <span class="tree-node-label">{{ data.label }}</span>
              </span>
              <div>
                <el-button
@@ -111,6 +108,8 @@
              <el-input
                v-model="form.productName"
                placeholder="请输入产品名称"
                maxlength="20"
                show-word-limit
                clearable
                @keydown.enter.prevent
              />
@@ -239,7 +238,10 @@
    productName: "",
  },
  rules: {
    productName: [{ required: true, message: "请输入", trigger: "blur" }],
    productName: [
      { required: true, message: "请输入", trigger: "blur" },
      { max: 20, message: "产品名称不能超过20个字符", trigger: "blur" },
    ],
  },
  modelForm: {
    model: "",
@@ -467,18 +469,21 @@
  display: flex;
}
.left {
  width: 380px;
  width: 450px;
  min-width: 450px;
  padding: 16px;
  background: #ffffff;
}
.right {
  width: calc(100% - 380px);
  flex: 1;
  min-width: 0;
  padding: 16px;
  margin-left: 20px;
  background: #ffffff;
}
.custom-tree-node {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
@@ -486,13 +491,42 @@
  padding-right: 8px;
}
.tree-node-content {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center; /* åž‚直居中 */
  align-items: center;
  height: 100%;
  overflow: hidden;
}
.tree-node-content .orange-icon {
  flex-shrink: 0;
}
.tree-node-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.orange-icon {
  color: orange;
  font-size: 18px;
  margin-right: 8px; /* å›¾æ ‡ä¸Žæ–‡å­—之间加点间距 */
}
.product-tree-scroll {
  scrollbar-width: thin;
  scrollbar-color: #c0c4cc #f5f7fa;
}
.product-tree-scroll::-webkit-scrollbar {
  width: 8px;
}
.product-tree-scroll::-webkit-scrollbar-track {
  background: #f5f7fa;
  border-radius: 4px;
}
.product-tree-scroll::-webkit-scrollbar-thumb {
  background: #c0c4cc;
  border-radius: 4px;
}
.product-tree-scroll::-webkit-scrollbar-thumb:hover {
  background: #909399;
}
</style>
src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -425,8 +425,15 @@
  listKnowledgeBase({...page.value, ...searchForm.value})
  .then(res => {
    tableLoading.value = false;
    tableData.value = res.data.records
    page.value.total = res.data.total;
    // å¦‚果当前页数超过总页数,重置到第1页并重新查询
    const maxPage = Math.ceil(res.data.total / page.value.size) || 1;
    if (page.value.current > maxPage && maxPage > 0) {
      page.value.current = 1;
      // é‡æ–°æŸ¥è¯¢ç¬¬1页数据
      return getList();
    }
    tableData.value = res.data.records;
  }).catch(err => {
    tableLoading.value = false;
  })
@@ -434,9 +441,14 @@
// åˆ†é¡µå¤„理
const pagination = (obj) => {
  const oldSize = page.value.size;
  page.value.current = obj.page;
  page.value.size = obj.limit;
  handleQuery();
  // å¦‚æžœ size æ”¹å˜äº†ï¼Œé‡ç½®åˆ°ç¬¬1页,避免当前页超出范围
  if (oldSize !== obj.limit) {
    page.value.current = 1;
  }
  getList();
};
// é€‰æ‹©å˜åŒ–处理
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue
@@ -42,66 +42,15 @@
            </el-button>
          </el-col>
        </el-row>
        <el-table :data="regulations"
                  border
                  v-loading="tableLoading"
                  style="width: 100%">
          <el-table-column prop="regulationNum"
                           label="制度编号"
                           width="120" />
          <el-table-column prop="title"
                           label="制度标题"
                           min-width="150" />
          <el-table-column prop="category"
                           label="分类"
                           width="120">
            <template #default="scope">
              <el-tag>{{ getCategoryText(scope.row.category) }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="version"
                           label="版本"
                           width="120" />
          <el-table-column prop="createUserName"
                           label="发布人"
                           width="120" />
          <el-table-column prop="createTime"
                           label="发布时间"
                           width="180" />
          <el-table-column prop="status"
                           label="状态"
                           width="100">
            <template #default="scope">
              <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
                {{ scope.row.status === 'active' ? '生效中' : '已废止' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="readCount"
                           label="已读人数"
                           width="100" />
          <el-table-column label="操作"
                           width="320"
                           fixed="right">
            <template #default="scope">
              <el-button link
                         @click="viewRegulation(scope.row)">查看</el-button>
              <el-button link
                         type="primary"
                         @click="handleEdit(scope.row)">编辑</el-button>
              <el-button link
                         type="danger"
                         @click="repealEdit(scope.row)">废弃</el-button>
              <el-button link
                         type="success"
                         @click="viewVersionHistory(scope.row)">版本历史</el-button>
              <!-- <el-button link type="warning" @click="viewReadStatus(scope.row)">阅读状态</el-button> -->
              <el-button link
                         type="primary"
                         @click="openFileDialog(scope.row)">附件</el-button>
            </template>
          </el-table-column>
        </el-table>
        <PIMTable
          rowKey="id"
          :column="regulationTableColumn"
          :tableData="regulations"
          :tableLoading="tableLoading"
          :page="page"
          :isShowPagination="true"
          @pagination="paginationChange"
        />
      </div>
    </el-card>
    <!-- ç”¨å°ç”³è¯·å¯¹è¯æ¡†ï¼ˆå·²ç§»é™¤ï¼‰ -->
@@ -271,7 +220,7 @@
                    :delete-method="handleAttachmentDelete"
                    :rules-regulations-management-id="currentFileRuleId"
                    :name-column-label="'附件名称'"
                    @upload="handleAttachmentUpload" />
                    @upload="handleAttachmentUpload"/>
  </div>
</template>
@@ -293,6 +242,7 @@
    delRuleFile,
    addRuleFile,
  } from "@/api/collaborativeApproval/rulesRegulationsManagementFile.js";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  // å“åº”式数据
  const operationType = ref("add");
@@ -310,7 +260,7 @@
  const currentFileRuleId = ref(null);
  const filePage = reactive({
    current: 1,
    size: 10,
    size: 1000,
    total: 0,
  });
  // è§„章制度相关
@@ -360,6 +310,45 @@
  });
  const regulations = ref([]);
  // è¡¨æ ¼åˆ—配置
  const regulationTableColumn = ref([
    { label: "制度编号", prop: "regulationNum"},
    { label: "制度标题", prop: "title" },
    {
      label: "分类",
      prop: "category",
      dataType: "tag",
      formatData: (v) => getCategoryText(v),
      formatType: () => "info",
    },
    { label: "版本", prop: "version", width: 120 },
    { label: "发布人", prop: "createUserName", width: 120 },
    { label: "发布时间", prop: "createTime", width: 180 },
    {
      label: "状态",
      prop: "status",
      width: 100,
      dataType: "tag",
      formatData: (v) => (v === "active" ? "生效中" : "已废止"),
      formatType: (v) => (v === "active" ? "success" : "info"),
    },
    { label: "已读人数", prop: "readCount", width: 100 },
    {
      dataType: "action",
      label: "操作",
      width: 320,
      fixed: "right",
      align: "center",
      operation: [
        { name: "查看", clickFun: (row) => viewRegulation(row) },
        { name: "编辑", clickFun: (row) => handleEdit(row) },
        { name: "废弃", clickFun: (row) => repealEdit(row) },
        { name: "版本历史", clickFun: (row) => viewVersionHistory(row) },
        { name: "附件", clickFun: (row) => openFileDialog(row) },
      ],
    },
  ]);
  const versionHistory = ref([]);
@@ -642,14 +631,19 @@
        regulations.value = res.data.records;
        // è¿‡æ»¤æŽ‰å·²åºŸå¼ƒçš„制度
        // regulations.value = res.data.records.filter(item => item.status !== 'repealed')
        page.value.total = res.data.total;
        page.total = res.data.total;
        tableLoading.value = false;
      })
      .catch(err => {
        tableLoading.value = false;
      });
  };
  // åˆ†é¡µå˜åŒ–处理
  const paginationChange = (obj) => {
    page.current = obj.page;
    page.size = obj.limit;
    getRegulationList();
  };
  onMounted(() => {
    // åˆå§‹åŒ–
    getRegulationList();
src/views/collaborativeApproval/sealManagement/index.vue
@@ -36,48 +36,15 @@
              </el-col>
            </el-row>
            <el-table :data="sealApplications" border v-loading="tableLoading" style="width: 100%">
              <el-table-column prop="applicationNum" label="申请编号" width="120" />
              <el-table-column prop="title" label="申请标题" min-width="200" />
              <el-table-column prop="createUserName" label="申请人" width="120" />
              <el-table-column prop="department" label="所属部门" width="150" />
              <el-table-column prop="sealType" label="用印类型" width="120">
                <template #default="scope">
                  {{ getSealTypeText(scope.row.sealType) }}
                </template>
              </el-table-column>
              <el-table-column prop="createTime" label="申请时间" width="180" />
              <el-table-column prop="status" label="状态" width="100">
                <template #default="scope">
                  <el-tag :type="getStatusType(scope.row.status)">
                    {{ getStatusText(scope.row.status) }}
                  </el-tag>
                </template>
              </el-table-column>
              <el-table-column label="操作" width="200" fixed="right">
                <template #default="scope">
                  <el-button link @click="viewSealDetail(scope.row)">查看</el-button>
                  <el-button
                    v-if="scope.row.status === 'pending'"
                                        link
                    type="primary"
                    @click="approveSeal(scope.row)"
                  >
                    å®¡æ‰¹
                  </el-button>
                  <el-button
                    v-if="scope.row.status === 'pending'"
                                        link
                    type="danger"
                    @click="rejectSeal(scope.row)"
                  >
                    æ‹’绝
                  </el-button>
                </template>
              </el-table-column>
            </el-table>
                    <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
                                            :page="page.current" :limit="page.size" @pagination="paginationChange" />
            <PIMTable
              rowKey="id"
              :column="sealTableColumn"
              :tableData="sealApplications"
              :tableLoading="tableLoading"
              :page="page"
              :isShowPagination="true"
              @pagination="paginationChange"
            />
        </div> 
    </el-card>
@@ -128,55 +95,6 @@
      </el-form>
    </FormDialog>
    <!-- è§„章制度发布对话框 -->
    <!-- <el-dialog v-model="showRegulationDialog" :title="operationType === 'add' ? '发布制度' : '编辑制度'" width="800px">
      <el-form :model="regulationForm" :rules="regulationRules" ref="regulationFormRef" label-width="100px">
        <el-form-item label="制度编号" prop="regulationNum">
          <el-input v-model="regulationForm.regulationNum" placeholder="请输入制度编号" />
        </el-form-item>
        <el-form-item label="制度标题" prop="title">
          <el-input v-model="regulationForm.title" placeholder="请输入制度标题" />
        </el-form-item>
        <el-form-item label="制度分类" prop="category">
          <el-select v-model="regulationForm.category" placeholder="请选择制度分类" style="width: 100%">
            <el-option label="人事制度" value="hr" />
            <el-option label="财务制度" value="finance" />
            <el-option label="安全制度" value="safety" />
            <el-option label="技术制度" value="tech" />
          </el-select>
        </el-form-item>
        <el-form-item label="制度内容" prop="content">
          <el-input v-model="regulationForm.content" type="textarea" :rows="10" placeholder="请输入制度详细内容" />
        </el-form-item>
        <el-form-item label="制度版本" prop="version">
          <el-input v-model="regulationForm.version" placeholder="请输入制度版本" />
        </el-form-item>
        <el-form-item label="生效时间" prop="effectiveTime">
          <el-date-picker v-model="regulationForm.effectiveTime" type="datetime" format="YYYY-MM-DD HH:mm:ss"
             value-format="YYYY-MM-DD HH:mm:ss" placeholder="选择生效时间" style="width: 100%" />
        </el-form-item>
        <el-form-item label="适用范围" prop="scope">
          <el-checkbox-group v-model="regulationForm.scope">
            <el-checkbox label="all">全体员工</el-checkbox>
            <el-checkbox label="manager">管理层</el-checkbox>
            <el-checkbox label="hr">人事部门</el-checkbox>
            <el-checkbox label="finance">财务部门</el-checkbox>
            <el-checkbox label="tech">技术部门</el-checkbox>
          </el-checkbox-group>
        </el-form-item>
        <el-form-item label="是否需要确认" prop="requireConfirm">
          <el-switch v-model="regulationForm.requireConfirm" />
          <span class="ml-10">开启后员工需要阅读确认</span>
        </el-form-item>
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="showRegulationDialog = false">取消</el-button>
          <el-button type="primary" @click="submitRegulation">发布制度</el-button>
        </span>
      </template>
    </el-dialog> -->
    <!-- ç”¨å°è¯¦æƒ…对话框 -->
    <FormDialog
      v-model="showSealDetailDialog"
@@ -204,81 +122,6 @@
      </div>
    </FormDialog>
    <!-- è§„章制度详情对话框 -->
    <FormDialog
      v-model="showRegulationDetailDialog"
      title="规章制度详情"
      :width="'800px'"
      @close="closeRegulationDetailDialog"
      @confirm="handleRegulationDetailConfirm"
      @cancel="closeRegulationDetailDialog"
    >
      <div v-if="currentRegulationDetail">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="制度编号">{{ currentRegulationDetail.id }}</el-descriptions-item>
          <el-descriptions-item label="制度标题">{{ currentRegulationDetail.title }}</el-descriptions-item>
          <el-descriptions-item label="分类">{{ getCategoryText(currentRegulationDetail.category) }}</el-descriptions-item>
          <el-descriptions-item label="版本">{{ currentRegulationDetail.version }}</el-descriptions-item>
          <el-descriptions-item label="发布人">{{ currentRegulationDetail.createUserName }}</el-descriptions-item>
          <el-descriptions-item label="发布时间">{{ currentRegulationDetail.createTime }}</el-descriptions-item>
        </el-descriptions>
        <div class="mt-20">
          <h4>制度内容</h4>
          <div class="regulation-content">{{ currentRegulationDetail.content }}</div>
        </div>
        <!-- å¦‚æžœtableData>0 æ˜¾ç¤º -->
        <div style="margin: 10px 0;" v-if="tableData && tableData.length > 0" >
          <el-button type="success" @click="resetForm(currentRegulationDetail)">确认查看</el-button>
        </div>
      </div>
    </FormDialog>
    <!-- ç‰ˆæœ¬åŽ†å²å¯¹è¯æ¡† -->
    <FormDialog
      v-model="showVersionHistoryDialog"
      title="版本历史"
      :width="'800px'"
      @close="closeVersionHistoryDialog"
      @confirm="closeVersionHistoryDialog"
      @cancel="closeVersionHistoryDialog"
    >
      <el-table :data="versionHistory" style="width: 100%;margin-bottom: 10px">
        <el-table-column prop="version" label="版本号" width="100" />
        <el-table-column prop="updateTime" label="更新时间" width="180" />
        <el-table-column prop="createUserName" label="更新人" width="120" />
        <el-table-column prop="changeLog" label="变更说明">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
              {{ scope.row.status === 'active' ? '生效中' : '已废止' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </FormDialog>
    <!-- é˜…读状态对话框 -->
    <FormDialog
      v-model="showReadStatusDialog"
      title="阅读状态"
      :width="'800px'"
      @close="closeReadStatusDialog"
      @confirm="closeReadStatusDialog"
      @cancel="closeReadStatusDialog"
    >
      <el-table :data="readStatusList" style="width: 100%;margin-bottom: 10px">
        <el-table-column prop="employee" label="员工姓名" width="120" />
        <el-table-column prop="department" label="所属部门" width="150" />
        <el-table-column prop="createTime" label="阅读时间" width="180" />
        <el-table-column prop="confirmTime" label="确认时间" width="180" />
        <el-table-column prop="status" label="状态" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'confirmed' ? 'success' : 'warning'">
              {{ scope.row.status === 'confirmed' ? '已确认' : '未确认' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
    </FormDialog>
  </div>
</template>
@@ -286,20 +129,13 @@
import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { listSealApplication, addSealApplication, updateSealApplication,listRuleManagement,addRuleManagement,updateRuleManagement,delRuleManagement,getReadingStatusByRuleId,getReadingStatusList,addReadingStatus,updateReadingStatus  } from '@/api/collaborativeApproval/sealManagement.js'
import { el } from 'element-plus/es/locales.mjs'
import { getUserProfile, userListNoPageByTenantId } from '@/api/system/user.js'
import { listSealApplication, addSealApplication, updateSealApplication } from '@/api/collaborativeApproval/sealManagement.js'
import { userListNoPageByTenantId } from '@/api/system/user.js'
import useUserStore from '@/store/modules/user'
import { userLoginFacotryList } from "@/api/system/user.js"
import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"
import FormDialog from '@/components/Dialog/FormDialog.vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
// å“åº”式数据
const currentUser = ref(null)
const activeTab = ref('seal')
const operationType = ref('add')
const tableData = ref([])
// ç”¨å°ç”³è¯·ç›¸å…³
const userStore = useUserStore()
const route = useRoute()
@@ -335,64 +171,11 @@
// åˆ†é¡µå‚æ•°
const page = reactive({
  current: 1,
  size: 100,
  size: 10,
  total: 0
})
// è§„章制度相关
const showRegulationDialog = ref(false)
const showRegulationDetailDialog = ref(false)
const showVersionHistoryDialog = ref(false)
const showReadStatusDialog = ref(false)
const currentRegulationDetail = ref(null)
const regulationFormRef = ref()
const regulationForm = reactive({
  id: '',
  regulationNum: '',
  title: '',
  category: '',
  content: '',
  version: '',
  status: 'active',
  readCount: 0,
  effectiveTime: '',
  scope: [],
  requireConfirm: false
})
const readStatus = ref({
  id: '',
  ruleId: '',
  employee: '',
  department: '',
  createTime: '',
  confirmTime: '',
  status: 'unconfirmed'
})
const regulationRules = {
  title: [{ required: true, message: '请输入制度标题', trigger: 'blur' }],
  category: [{ required: true, message: '请选择制度分类', trigger: 'change' }],
  content: [{ required: true, message: '请输入制度内容', trigger: 'blur' }],
  effectiveTime: [{ required: true, message: '请选择生效时间', trigger: 'change' }],
  scope: [{ required: true, message: '请选择适用范围', trigger: 'change' }]
}
const regulationSearchForm = reactive({
  title: '',
  category: ''
})
// å‡æ•°æ®
const sealApplications = ref([])
const regulations = ref([])
const versionHistory = ref([])
const readStatusList = ref([])
  // { employee: '陈志强', department: '销售部', readTime: '2025-01-11 10:30:00', confirmTime: '2025-01-11 10:35:00', status: 'confirmed' },
  // { employee: '刘雅婷', department: '技术部', readTime: '2025-01-11 14:20:00', confirmTime: '', status: 'unconfirmed' },
  // { employee: '王建国', department: '财务部', readTime: '2025-01-12 09:15:00', confirmTime: '2025-01-12 09:20:00', status: 'confirmed' }
// ç”¨å°ç”³è¯·çŠ¶æ€
const getStatusType = (status) => {
@@ -403,7 +186,7 @@
  }
  return statusMap[status] || 'info'
}
// åˆ¶åº¦çŠ¶æ€
// ç”¨å°ç”³è¯·çŠ¶æ€æ–‡æœ¬
const getStatusText = (status) => {
  const statusMap = {
    pending: '待审批',
@@ -418,20 +201,56 @@
    official: '公章',
    contract: '合同专用章',
    finance: '财务专用章',
    legal: '法人章',
    tegal: '技术专用章'
  }
  return sealTypeMap[sealType] || '未知'
}
// åˆ¶åº¦åˆ†ç±»
const getCategoryText = (category) => {
  const categoryMap = {
    hr: '人事制度',
    finance: '财务制度',
    safety: '安全制度',
    tech: '技术制度'
// ç”¨å°ç”³è¯·è¡¨æ ¼åˆ—配置(需在 getStatusText/getSealTypeText ç­‰ä¹‹åŽå®šä¹‰ï¼‰
const sealTableColumn = ref([
  { label: '申请编号', prop: 'applicationNum',},
  { label: '申请标题', prop: 'title', showOverflowTooltip: true },
  { label: '申请人', prop: 'createUserName', },
  { label: '所属部门', prop: 'department', width: 150 },
  {
    label: '用印类型',
    prop: 'sealType',
    dataType: 'tag',
    formatData: (v) => getSealTypeText(v),
    formatType: () => 'info'
  },
  { label: '申请时间', prop: 'createTime', width: 180 },
  {
    label: '状态',
    prop: 'status',
    width: 100,
    dataType: 'tag',
    formatData: (v) => getStatusText(v),
    formatType: (v) => getStatusType(v)
  },
  {
    dataType: 'action',
    label: '操作',
    width: 200,
    fixed: 'right',
    align: 'center',
    operation: [
      { name: '查看', clickFun: (row) => viewSealDetail(row) },
      {
        name: '审批',
        clickFun: (row) => approveSeal(row),
        showHide: (row) => row.status === 'pending'
      },
      {
        name: '拒绝',
        clickFun: (row) => rejectSeal(row),
        showHide: (row) => row.status === 'pending'
      }
    ]
  }
  return categoryMap[category] || '未知'
}
])
// æœç´¢å°ç« ç”³è¯·
const searchSealApplications = () => {
  page.current=1
@@ -445,17 +264,6 @@
  sealSearchForm.status = ''
  sealSearchForm.applicationNum = ''
  searchSealApplications()
}
// æœç´¢åˆ¶åº¦
const searchRegulations = () => {
  page.current=1
  getRegulationList()
}
// é‡ç½®åˆ¶åº¦æœç´¢
const resetRegulationSearch = () => {
  regulationSearchForm.title = ''
  regulationSearchForm.category = ''
  searchRegulations()
}
// æäº¤ç”¨å°ç”³è¯·
const submitSealApplication = async () => {
@@ -505,106 +313,6 @@
const closeSealDetailDialog = () => {
  showSealDetailDialog.value = false
}
// å…³é—­è§„章制度详情对话框
const closeRegulationDetailDialog = () => {
  showRegulationDetailDialog.value = false
}
// å¤„理规章制度详情确认
const handleRegulationDetailConfirm = () => {
  // å¦‚æžœtableData>0,执行确认查看操作
  if (currentRegulationDetail.value && tableData.value && tableData.value.length > 0) {
    resetForm(currentRegulationDetail.value)
  }
  closeRegulationDetailDialog()
}
// å…³é—­ç‰ˆæœ¬åŽ†å²å¯¹è¯æ¡†
const closeVersionHistoryDialog = () => {
  showVersionHistoryDialog.value = false
}
// å…³é—­é˜…读状态对话框
const closeReadStatusDialog = () => {
  showReadStatusDialog.value = false
}
// æ–°å¢ž
const handleAdd = () => {
  operationType.value = 'add'
  resetRegulationForm()
  showRegulationDialog.value = true
}
// ç¼–辑
const handleEdit = (row) => {
  operationType.value = 'edit'
  Object.assign(regulationForm, row)
  showRegulationDialog.value = true
}
// åºŸå¼ƒ
const repealEdit = (row) => {
  operationType.value = 'edit'
  Object.assign(regulationForm, row)
  regulationForm.status = 'repealed'
  ElMessageBox.confirm('确认废弃该制度?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    updateRuleManagement(regulationForm).then(res => {
      if(res.code == 200){
        ElMessage.success('制度废弃成功')
        // showRegulationDialog.value = false
        getRegulationList()
        resetRegulationForm()
      }
    })
  }).catch(() => {
    ElMessage({
      type: 'info',
      message: '已取消废弃'
    })
  })
}
// å‘布制度
const submitRegulation = async () => {
  try {
    await regulationFormRef.value.validate()
    if(operationType.value == 'add'){
      addRuleManagement(regulationForm).then(res => {
        if(res.code == 200){
          ElMessage.success('制度发布成功')
          showRegulationDialog.value = false
          getRegulationList()
          resetRegulationForm()
        }
      })
    }else{
      updateRuleManagement(regulationForm).then(res => {
        if(res.code == 200){
          ElMessage.success('制度编辑成功')
          showRegulationDialog.value = false
          resetRegulationForm()
          getRegulationList()
      }})}
  }catch(err){
    ElMessage.error(err.msg)
  }
}
//重置制度表单
const resetRegulationForm = () => {
  Object.assign(regulationForm, {
    id: '',
    regulationNum: '',
    title: '',
    category: '',
    content: '',
    version: '',
    status: 'active',
    readCount: 0,
    effectiveTime: '',
    scope: [],
    requireConfirm: false
})
}
// æŸ¥çœ‹ç”¨å°ç”³è¯·è¯¦æƒ…
const viewSealDetail = (row) => {
@@ -613,7 +321,6 @@
}
// å®¡æ‰¹ç”¨å°ç”³è¯·
const approveSeal = (row) => {
  console.log(row)
  ElMessageBox.confirm('确认通过该用印申请?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
@@ -623,6 +330,7 @@
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('审批通过')
        getSealApplicationList()
      }
    })
  })
@@ -638,122 +346,10 @@
    row.status = 'rejected'
    updateSealApplication(row).then(res => {
      if(res.code == 200){
        ElMessage.success('审批拒绝')
        ElMessage.success('已拒绝申请')
        getSealApplicationList()
      }
    })
    ElMessage.success('已拒绝申请')
  })
}
// èŽ·å–åœ¨èŒå‘˜å·¥åˆ—è¡¨
const getList = () => {
  tableLoading.value = true;
      //获取当前登录用户信息
  getUserProfile().then(res => {
    if(res.code == 200){
      console.log(res.data.userName)
      currentUser.value = res.data.userName
    }
  })
  staffOnJobListPage({staffState: 1, ...page}).then(res => {
    tableLoading.value = false;
    // tableData.value = res.data.records
    // //筛选出和currentUser同名的人员
    tableData.value = res.data.records.filter(item => item.staffName === currentUser.value)
    page.total = res.data.total;
    if(tableData.value.length == 0){
    ElMessage.error('当前用户未加入任何部门')
    }
  }).catch(err => {
    tableLoading.value = false;
  })
};
// æŸ¥çœ‹åˆ¶åº¦ç‰ˆæœ¬åŽ†å²
const viewVersionHistory = (row) => {
  showVersionHistoryDialog.value = true
  const params = {
    category: row.category
  }
  listRuleManagement(page,params).then(res => {
    if(res.code == 200){
      versionHistory.value = res.data.records
    }
  })
}
// æŸ¥çœ‹åˆ¶åº¦è¯¦æƒ…
const viewRegulation = (row) => {
  getList()
  currentRegulationDetail.value = row
  showRegulationDetailDialog.value = true
  getReadingStatusByRuleId(row.id).then(res => {
    if(res.code == 200){
      readStatusList.value = res.data
      if(readStatusList.value.length==0 && tableData.value.length>0){
          const params = {
          ruleId: row.id,
          employee: tableData.value[0].staffName,
          department: tableData.value[0].postJob,
          status: 'unconfirmed'
        }
        addReadingStatus(params).then(res => {
          if(res.code == 200){
            ElMessage.success('制度阅读成功')
          }
        })
      }
    }
  })
}
// æŸ¥çœ‹åˆ¶åº¦é˜…读状态
const viewReadStatus = (row) => {
  showReadStatusDialog.value = true
  //查看阅读状态列表
  getReadingStatusByRuleId(row.id).then(res => {
    if(res.code == 200){
      readStatusList.value = res.data
    }
  })
}
//确认查看
const resetForm = (row) => {
  console.log("row",row)
  row.readCount = row.readCount + 1
  updateRuleManagement(row).then(res => {
    if(res.code == 200){
      ElMessage.success('查看数量修改成功')
      //修改阅读状态
      //根据制度id和当前登录的员工得到阅读状态
      // let item = readStatusList.value.filter(item => item.employee == tableData.value[0].staffName )
      // if(item.length>0){
      //   item[0].status = 'confirmed',
      //   item[0].confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
      // }
      // ç­›é€‰å½“前员工对应该制度的阅读状态记录
      let statusItem = readStatusList.value.find(item => item.employee === tableData.value[0].staffName && item.ruleId === row.id);
      if (statusItem) {
        // å¦‚果找到记录,更新状态和确认时间
        statusItem.status = 'confirmed';
        // æ ¼å¼åŒ–时间为"YYYY-MM-DD HH:mm:ss"格式
        const now = new Date();
        statusItem.confirmTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
        // statusItem.confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
        updateReadingStatus(statusItem).then(res => {
          if(res.code == 200){
            ElMessage.success('制度阅读状态修改成功')
          }
        })
      }
    }
  })
}
@@ -766,46 +362,15 @@
// èŽ·å–å°ç« ç”³è¯·åˆ—è¡¨æ•°æ®
const getSealApplicationList = async () => {
  tableLoading.value = true
  listSealApplication(page,sealSearchForm)
  listSealApplication(page, sealSearchForm)
  .then(res => {
    //获取当前登录的部门信息
// èŽ·å–å½“å‰ç™»å½•çš„éƒ¨é—¨ä¿¡æ¯å¹¶è¿‡æ»¤æ•°æ®
    const currentFactoryName = userStore.currentFactoryName
    if (currentFactoryName) {
      // æ ¹æ®currentFactoryName过滤出department相同的数据
      sealApplications.value = res.data.records.filter(item => item.department === currentFactoryName)
      // æ›´æ–°è¿‡æ»¤åŽçš„æ€»æ•°
      page.total = sealApplications.value.length
    } else {
      // å¦‚果没有currentFactoryName,则显示所有数据
      sealApplications.value = res.data.records
      page.total = res.data.total
    }
    // sealApplications.value = res.data.records
    // page.value.total = res.data.total;
    tableLoading.value = false;
    sealApplications.value = res.data.records
    page.total = res.data.total
    tableLoading.value = false
  }).catch(err => {
    tableLoading.value = false;
    tableLoading.value = false
  })
}
// èŽ·å–è§„ç« åˆ¶åº¦åˆ—è¡¨æ•°æ®
const getRegulationList = async () => {
  tableLoading.value = true
  listRuleManagement(page,regulationSearchForm)
  .then(res => {
    regulations.value = res.data.records
    // è¿‡æ»¤æŽ‰å·²åºŸå¼ƒçš„制度
    // regulations.value = res.data.records.filter(item => item.status !== 'repealed')
    page.total = res.data.total;
    tableLoading.value = false;
  }).catch(err => {
    tableLoading.value = false;
  })
}
// åˆ†é¡µå˜åŒ–处理
const paginationChange = (obj) => {
  page.current = obj.page;
@@ -831,7 +396,6 @@
  } else {
    getSealApplicationList()
  }
  getRegulationList()
})
</script>
@@ -854,26 +418,7 @@
  margin-bottom: 20px;
}
.mt-20 {
  margin-top: 20px;
}
.ml-10 {
  margin-left: 10px;
}
.regulation-content {
  background-color: #f5f5f5;
  padding: 15px;
  border-radius: 4px;
  line-height: 1.6;
  white-space: pre-wrap;
  height: 200px;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
</style>
src/views/customerService/afterSalesHandling/index.vue
@@ -46,15 +46,64 @@
            ></PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
        <FileListDialog
            ref="fileListRef"
            v-model="fileListDialogVisible"
            title="售后附件"
            :show-upload-button="true"
            :show-delete-button="true"
            :upload-method="handleFileUpload"
            :delete-method="handleFileDelete"
        />
        <el-dialog
            v-model="repairDialogVisible"
            title="维修记录"
            width="700px"
            destroy-on-close
            @close="repairRecordList = []"
        >
            <el-table
                :data="repairRecordList"
                border
                v-loading="repairRecordLoading"
                max-height="400"
            >
                <el-table-column type="index" label="序号" width="55" align="center" />
                <el-table-column label="维修日期" prop="maintenanceTime" min-width="120" show-overflow-tooltip>
                    <template #default="{ row }">
                        {{ row.maintenanceTime || row.repairTime || '-' }}
                    </template>
                </el-table-column>
                <el-table-column label="维修人" prop="maintenanceName" min-width="100" show-overflow-tooltip>
                    <template #default="{ row }">
                        {{ row.maintenanceName || row.repairName || '-' }}
                    </template>
                </el-table-column>
                <el-table-column label="维修结果" prop="maintenanceResult" min-width="180" show-overflow-tooltip />
            </el-table>
            <template #footer>
                <el-button @click="repairDialogVisible = false">关闭</el-button>
            </template>
        </el-dialog>
    </div>
</template>
<script setup>
import {Search} from "@element-plus/icons-vue";
import {onMounted, ref, getCurrentInstance, nextTick} from "vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import FormDia from "@/views/customerService/afterSalesHandling/components/formDia.vue";
import {ElMessageBox} from "element-plus";
import {afterSalesServiceDelete, afterSalesServiceListPage} from "@/api/customerService/index.js";
import FileListDialog from "@/components/Dialog/FileListDialog.vue";
import { ElMessageBox } from "element-plus";
import request from "@/utils/request";
import { getToken } from "@/utils/auth";
import {
    afterSalesServiceDelete,
    afterSalesServiceListPage,
    afterSalesServiceFileListPage,
    afterSalesServiceFileAdd,
    afterSalesServiceFileDel,
    afterSalesServiceRepairListPage,
} from "@/api/customerService/index.js";
import useUserStore from "@/store/modules/user.js";
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
@@ -134,7 +183,7 @@
        label: "操作",
        align: "center",
        fixed: 'right',
        width: 120,
        width: 240,
        operation: [
            {
                name: "处理",
@@ -151,6 +200,22 @@
                type: "text",
                clickFun: (row) => {
                    openForm("view", row);
                },
            },
            // TODO ä¸ºå†™æŠ¥å‘Šæ·»åŠ çš„
            {
                name: "附件",
                type: "text",
                clickFun: (row) => {
                    openFilesFormDia(row);
                },
            },
            // TODO ä¸ºå†™æŠ¥å‘Šæ·»åŠ çš„
            {
                name: "维修记录",
                type: "text",
                clickFun: (row) => {
                    openRepairDialog(row);
                },
            },
        ],
@@ -170,6 +235,166 @@
    selectedRows.value = selection;
};
const formDia = ref()
const fileListRef = ref(null)
const fileListDialogVisible = ref(false)
const currentFileRow = ref(null)
const repairDialogVisible = ref(false)
const repairRecordList = ref([])
const repairRecordLoading = ref(false)
// æ‰“开维修记录弹框
const openRepairDialog = async (row) => {
    repairDialogVisible.value = true
    repairRecordLoading.value = true
    repairRecordList.value = []
    try {
        const res = await afterSalesServiceRepairListPage({
            afterSalesServiceId: row.id,
            current: 1,
            size: 100,
        })
        if (res.code === 200 && res.data?.records) {
            repairRecordList.value = res.data.records
        }
    } catch (error) {
        proxy.$modal.msgError("获取维修记录失败")
    } finally {
        repairRecordLoading.value = false
    }
}
// æ‰“开附件弹框-----  TODO:接口是没有对接的,需要新增接口,为写报告添加的
const openFilesFormDia = async (row) => {
    currentFileRow.value = row
    try {
        const res = await afterSalesServiceFileListPage({
            afterSalesServiceId: row.id,
            current: 1,
            size: 100,
        })
        if (res.code === 200 && fileListRef.value) {
            const fileList = (res.data?.records || []).map((item) => ({
                name: item.name || item.fileName,
                url: item.url || item.fileUrl,
                id: item.id,
                ...item,
            }))
            fileListRef.value.open(fileList)
            fileListDialogVisible.value = true
        } else {
            fileListRef.value?.open([])
            fileListDialogVisible.value = true
        }
    } catch (error) {
        proxy.$modal.msgError("获取附件列表失败")
        fileListRef.value?.open([])
        fileListDialogVisible.value = true
    }
}
// ä¸Šä¼ é™„ä»¶
const handleFileUpload = async () => {
    if (!currentFileRow.value) {
        proxy.$modal.msgWarning("请先选择数据")
        return
    }
    return new Promise((resolve) => {
        const input = document.createElement("input")
        input.type = "file"
        input.style.display = "none"
        input.onchange = async (e) => {
            const file = e.target.files[0]
            if (!file) {
                resolve(null)
                return
            }
            try {
                const formData = new FormData()
                formData.append("file", file)
                const uploadRes = await request({
                    url: "/file/upload",
                    method: "post",
                    data: formData,
                    headers: {
                        "Content-Type": "multipart/form-data",
                        Authorization: `Bearer ${getToken()}`,
                    },
                })
                if (uploadRes.code === 200) {
                    const fileData = {
                        afterSalesServiceId: currentFileRow.value.id,
                        name: uploadRes.data?.originalName || file.name,
                        url: uploadRes.data?.tempPath || uploadRes.data?.url,
                    }
                    const saveRes = await afterSalesServiceFileAdd(fileData)
                    if (saveRes.code === 200) {
                        proxy.$modal.msgSuccess("文件上传成功")
                        const listRes = await afterSalesServiceFileListPage({
                            afterSalesServiceId: currentFileRow.value.id,
                            current: 1,
                            size: 100,
                        })
                        if (listRes.code === 200 && fileListRef.value) {
                            const fileList = (listRes.data?.records || []).map((item) => ({
                                name: item.name || item.fileName,
                                url: item.url || item.fileUrl,
                                id: item.id,
                                ...item,
                            }))
                            fileListRef.value.setList(fileList)
                        }
                        resolve({ name: fileData.name, url: fileData.url, id: saveRes.data?.id })
                    } else {
                        proxy.$modal.msgError(saveRes.msg || "文件保存失败")
                        resolve(null)
                    }
                } else {
                    proxy.$modal.msgError(uploadRes.msg || "文件上传失败")
                    resolve(null)
                }
            } catch (err) {
                proxy.$modal.msgError("文件上传失败")
                resolve(null)
            } finally {
                document.body.removeChild(input)
            }
        }
        document.body.appendChild(input)
        input.click()
    })
}
// åˆ é™¤é™„ä»¶
const handleFileDelete = async (row) => {
    try {
        const res = await afterSalesServiceFileDel([row.id])
        if (res.code === 200) {
            proxy.$modal.msgSuccess("删除成功")
            if (currentFileRow.value && fileListRef.value) {
                const listRes = await afterSalesServiceFileListPage({
                    afterSalesServiceId: currentFileRow.value.id,
                    current: 1,
                    size: 100,
                })
                if (listRes.code === 200) {
                    const fileList = (listRes.data?.records || []).map((item) => ({
                        name: item.name || item.fileName,
                        url: item.url || item.fileUrl,
                        id: item.id,
                        ...item,
                    }))
                    fileListRef.value.setList(fileList)
                }
            }
        } else {
            proxy.$modal.msgError(res.msg || "删除失败")
            return false
        }
    } catch (error) {
        proxy.$modal.msgError("删除失败")
        return false
    }
}
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
src/views/equipmentManagement/attendanceManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,403 @@
<template>
  <div class="app-container">
    <!-- æœåŠ¡è¯„ä»·æ¦‚è§ˆï¼šæ¨¡æ‹Ÿå‘˜å·¥ä¸šç»©è¯„åˆ† -->
    <el-row :gutter="16" class="mb16">
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">本月平均评分</div>
          <div class="kpi-value">
            {{ overallAvgScore.toFixed(1) }}
            <span class="kpi-unit">分</span>
          </div>
          <el-rate v-model="overallAvgScore" disabled show-score score-template="{value} / 5" />
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">已评价维修工单</div>
          <div class="kpi-value">
            {{ ratedCount }}
            <span class="kpi-unit">单</span>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">待评价维修工单</div>
          <div class="kpi-value kpi-warning">
            {{ pendingCount }}
            <span class="kpi-unit">单</span>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æŸ¥è¯¢æ¡ä»¶ï¼šç®¡ç†å‘˜æŒ‰å·¥ç¨‹å¸ˆ / å®¢æˆ· / æ—¶é—´è¿½æº¯è¯„ä»· -->
    <div class="search_form">
      <div>
        <span class="search_title">维修工程师:</span>
        <el-input
          v-model="searchForm.engineerName"
          placeholder="请输入工程师姓名"
          style="width: 180px"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">客户名称:</span>
        <el-input
          v-model="searchForm.customerName"
          placeholder="请输入客户名称"
          style="width: 180px"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">完成时间:</span>
        <el-date-picker
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
        />
        <span class="search_title ml10">评价状态:</span>
        <el-select
          v-model="searchForm.status"
          placeholder="请选择"
          style="width: 140px"
          clearable
        >
          <el-option label="待评价" value="pending" />
          <el-option label="已评价" value="rated" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button icon="Download" @click="handleExport">
          å¯¼å‡ºè¯„价统计
        </el-button>
      </div>
    </div>
    <!-- ç»´ä¿®è¯„价列表:模拟“维修完成后触发评价”场景 -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 24em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="orderNo" label="维修工单号" width="160" show-overflow-tooltip />
        <el-table-column prop="deviceName" label="设备名称" width="160" show-overflow-tooltip />
        <el-table-column prop="customerName" label="客户名称" width="180" show-overflow-tooltip />
        <el-table-column prop="engineerName" label="维修工程师" width="120" />
        <el-table-column prop="completeTime" label="维修完成时间" width="180" />
        <el-table-column prop="score" label="星级评分" width="140" align="center">
          <template #default="scope">
            <el-rate v-if="scope.row.score" v-model="scope.row.score" disabled />
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="评价状态" width="100" align="center">
          <template #default="scope">
            <el-tag
              :type="scope.row.status === 'rated' ? 'success' : 'warning'"
              size="small"
            >
              {{ scope.row.status === 'rated' ? '已评价' : '待评价' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="feedback" label="客户反馈" show-overflow-tooltip />
        <el-table-column label="操作" width="160" align="center" fixed="right">
          <template #default="scope">
            <el-button
              v-if="scope.row.status === 'pending'"
              type="primary"
              link
              size="small"
              @click="openEvaluate(scope.row)"
            >
              åŽ»è¯„ä»·
            </el-button>
            <el-button
              v-else
              type="primary"
              link
              size="small"
              @click="openEvaluate(scope.row)"
            >
              æŸ¥çœ‹ / ä¿®æ”¹è¯„ä»·
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- è¯„价弹框:模拟“维修完成后弹出客户端评价” -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="520px"
      destroy-on-close
    >
      <div class="dialog-order-info" v-if="currentOrder">
        <div>维修工单:{{ currentOrder.orderNo }}</div>
        <div>设备名称:{{ currentOrder.deviceName }}</div>
        <div>维修工程师:{{ currentOrder.engineerName }}</div>
      </div>
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="星级评分:" prop="score">
          <el-rate v-model="form.score" :max="5" />
        </el-form-item>
        <el-form-item label="文字反馈:" prop="feedback">
          <el-input
            v-model="form.feedback"
            type="textarea"
            :rows="4"
            placeholder="请填写对本次维修服务的评价,如响应速度、专业程度、沟通体验等"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">提 äº¤ è¯„ ä»·</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { ElMessage } from "element-plus";
// æ¨¡æ‹Ÿç»´ä¿®å·¥å• + å®¢æˆ·è¯„价数据
const rawOrders = ref([
  {
    id: 1,
    orderNo: "WX-2024-1201-001",
    deviceName: "空压机 A1 å·",
    customerName: "华南电子科技有限公司",
    engineerName: "王师傅",
    completeTime: "2024-12-01 10:30:00",
    completeDate: "2024-12-01",
    status: "rated",
    score: 5,
    feedback: "维修非常专业,响应速度快,现场解释也很清晰,满意。",
  },
  {
    id: 2,
    orderNo: "WX-2024-1201-002",
    deviceName: "注塑机 B3 å·",
    customerName: "华东精密制造有限公司",
    engineerName: "李师傅",
    completeTime: "2024-12-01 15:20:00",
    completeDate: "2024-12-01",
    status: "rated",
    score: 4,
    feedback: "整体还不错,就是到场时间稍微长了一点,希望后面能再快一些。",
  },
  {
    id: 3,
    orderNo: "WX-2024-1202-003",
    deviceName: "焊接机器人 C2 å·",
    customerName: "西南新能源科技股份",
    engineerName: "张师傅",
    completeTime: "2024-12-02 11:05:00",
    completeDate: "2024-12-02",
    status: "pending",
    score: null,
    feedback: "",
  },
  {
    id: 4,
    orderNo: "WX-2024-1203-005",
    deviceName: "测试台 D1 å·",
    customerName: "北方汽车零部件有限公司",
    engineerName: "王师傅",
    completeTime: "2024-12-03 09:50:00",
    completeDate: "2024-12-03",
    status: "pending",
    score: null,
    feedback: "",
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  engineerName: "",
  customerName: "",
  dateRange: [],
  status: "",
});
// åˆ—表数据
const tableData = ref([...rawOrders.value]);
// ç»Ÿè®¡ï¼šæ•´ä½“评分、已评价 / å¾…评价数量
const ratedOrders = computed(() =>
  rawOrders.value.filter((o) => o.status === "rated" && o.score)
);
const overallAvgScore = computed(() => {
  if (!ratedOrders.value.length) return 0;
  const sum = ratedOrders.value.reduce((acc, cur) => acc + (cur.score || 0), 0);
  return sum / ratedOrders.value.length;
});
const ratedCount = computed(() => ratedOrders.value.length);
const pendingCount = computed(
  () => rawOrders.value.filter((o) => o.status === "pending").length
);
// æŸ¥è¯¢ / é‡ç½®
const recomputeTable = () => {
  const list = rawOrders.value.filter((item) => {
    if (
      searchForm.engineerName &&
      !item.engineerName.includes(searchForm.engineerName.trim())
    ) {
      return false;
    }
    if (
      searchForm.customerName &&
      !item.customerName.includes(searchForm.customerName.trim())
    ) {
      return false;
    }
    if (searchForm.status && item.status !== searchForm.status) {
      return false;
    }
    if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
      const [start, end] = searchForm.dateRange;
      if (item.completeDate < start || item.completeDate > end) {
        return false;
      }
    }
    return true;
  });
  tableData.value = list;
};
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.engineerName = "";
  searchForm.customerName = "";
  searchForm.dateRange = [];
  searchForm.status = "";
  recomputeTable();
};
// å¯¼å‡ºï¼ˆæ¼”示)
const handleExport = () => {
  ElMessage.success("当前为演示页面,评价导出功能未对接实际接口");
};
// è¯„价弹框
const dialogVisible = ref(false);
const dialogTitle = ref("维修服务评价");
const currentOrder = ref(null);
const formRef = ref(null);
const form = reactive({
  score: 0,
  feedback: "",
});
const rules = {
  score: [{ required: true, message: "请选择星级评分", trigger: "change" }],
  feedback: [{ required: true, message: "请填写文字反馈", trigger: "blur" }],
};
// æ‰“开评价:模拟“维修完成确认后弹出评价弹框”
const openEvaluate = (row) => {
  currentOrder.value = row;
  dialogTitle.value =
    row.status === "pending" ? "维修服务评价" : "查看 / ä¿®æ”¹è¯„ä»·";
  form.score = row.score || 0;
  form.feedback = row.feedback || "";
  dialogVisible.value = true;
};
// æäº¤è¯„价:同步到本地“员工业绩统计”
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid || !currentOrder.value) return;
    const target = rawOrders.value.find((o) => o.id === currentOrder.value.id);
    if (target) {
      target.score = form.score;
      target.feedback = form.feedback;
      target.status = "rated";
    }
    ElMessage.success("评价提交成功,已同步至员工业绩统计");
    dialogVisible.value = false;
    recomputeTable();
  });
};
// åˆå§‹åŒ–列表
recomputeTable();
</script>
<style scoped lang="scss">
.mb16 {
  margin-bottom: 16px;
}
.kpi-title {
  font-size: 13px;
  color: #909399;
}
.kpi-value {
  margin-top: 6px;
  font-size: 24px;
  font-weight: 600;
  color: #303133;
}
.kpi-unit {
  font-size: 12px;
  margin-left: 4px;
  color: #909399;
}
.kpi-warning {
  color: #e6a23c;
}
.dialog-order-info {
  margin-bottom: 12px;
  font-size: 13px;
  color: #606266;
  line-height: 1.8;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -1,31 +1,37 @@
<template>
  <div class="app-container">
    <el-form :inline="true" :model="queryParams" class="search-form">
    <el-form :inline="true"
             :model="queryParams"
             class="search-form">
      <el-form-item label="巡检任务名称">
        <el-input
            v-model="queryParams.taskName"
            placeholder="请输入巡检任务名称"
            clearable
            style="width: 200px "
        />
        <el-input v-model="queryParams.taskName"
                  placeholder="请输入巡检任务名称"
                  clearable
                  style="width: 200px " />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="handleQuery">查询</el-button>
        <el-button type="primary"
                   @click="handleQuery">查询</el-button>
        <el-button @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <el-card>
      <div style="display: flex;flex-direction: row;justify-content: space-between;margin-bottom: 10px;">
        <el-radio-group v-model="activeRadio" @change="radioChange">
        <el-radio-group v-model="activeRadio"
                        @change="radioChange">
          <el-radio-button v-for="tab in radios"
                           :key="tab.name"
                           :label="tab.label"
                           :value="tab.name"/>
                           :value="tab.name" />
        </el-radio-group>
        <!-- æ“ä½œæŒ‰é’®åŒº -->
        <el-space v-if="activeRadio !== 'task'">
          <el-button type="primary" :icon="Plus" @click="handleAdd(undefined)">新建</el-button>
          <el-button type="danger" :icon="Delete" @click="handleDelete">删除</el-button>
          <el-button type="primary"
                     :icon="Plus"
                     @click="handleAdd(undefined)">新建</el-button>
          <el-button type="danger"
                     :icon="Delete"
                     @click="handleDelete">删除</el-button>
          <el-button @click="handleOut">导出</el-button>
        </el-space>
        <el-space v-else>
@@ -34,320 +40,354 @@
      </div>
      <div>
        <PIMTable :table-loading="tableLoading"
                :table-data="tableData"
                :column="tableColumns"
                @selection-change="handleSelectionChange"
                @pagination="handlePagination"
                :is-selection="true"
                :border="true"
                :page="{
                  :table-data="tableData"
                  :column="tableColumns"
                  @selection-change="handleSelectionChange"
                  @pagination="handlePagination"
                  :is-selection="true"
                  :border="true"
                  :page="{
                  current: pageNum,
                  size: pageSize,
                  total: total,
                  layout: 'total, sizes, prev, pager, next, jumper'
                }"
                :table-style="{ width: '100%', height: 'calc(100vh - 23em)' }"
        >
                  :table-style="{ width: '100%', height: 'calc(100vh - 23em)' }">
          <template #inspector="{ row }">
            <div class="person-tags">
              <!-- è°ƒè¯•信息,上线时删除 -->
              <!-- {{ console.log('inspector data:', row.inspector) }} -->
              <template v-if="row.inspector && row.inspector.length > 0">
                <el-tag
                  v-for="(person, index) in row.inspector"
                  :key="index"
                  size="small"
                  type="primary"
                  class="person-tag"
                >
                <el-tag v-for="(person, index) in row.inspector"
                        :key="index"
                        size="small"
                        type="primary"
                        class="person-tag">
                  {{ person }}
                </el-tag>
              </template>
              <span v-else class="no-data">--</span>
              <span v-else
                    class="no-data">--</span>
            </div>
          </template>
        </PIMTable>
      </div>
    </el-card>
    <form-dia ref="formDia" @closeDia="handleQuery"></form-dia>
    <form-dia ref="formDia"
              @closeDia="handleQuery"></form-dia>
    <view-files ref="viewFiles"></view-files>
  </div>
</template>
<script setup>
import { Delete, Plus } from "@element-plus/icons-vue";
import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue";
import { ElMessageBox } from "element-plus";
  import { Delete, Plus } from "@element-plus/icons-vue";
  import { onMounted, ref, reactive, getCurrentInstance, nextTick } from "vue";
  import { ElMessageBox } from "element-plus";
// ç»„件引入
import PIMTable from "@/components/PIMTable/PIMTable.vue";
import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
  // ç»„件引入
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  import FormDia from "@/views/equipmentManagement/inspectionManagement/components/formDia.vue";
  import ViewFiles from "@/views/equipmentManagement/inspectionManagement/components/viewFiles.vue";
// æŽ¥å£å¼•å…¥
import {
  delTimingTask,
  inspectionTaskList,
  timingTaskList
} from "@/api/inspectionManagement/index.js";
  // æŽ¥å£å¼•å…¥
  import {
    delTimingTask,
    inspectionTaskList,
    timingTaskList,
  } from "@/api/inspectionManagement/index.js";
// å…¨å±€å˜é‡
const { proxy } = getCurrentInstance();
const formDia = ref();
const viewFiles = ref();
  // å…¨å±€å˜é‡
  const { proxy } = getCurrentInstance();
  const formDia = ref();
  const viewFiles = ref();
// æŸ¥è¯¢å‚æ•°
const queryParams = reactive({
  taskName: "",
});
  // æŸ¥è¯¢å‚æ•°
  const queryParams = reactive({
    taskName: "",
  });
// å•选框配置
const activeRadio = ref("taskManage");
const radios = reactive([
  { name: "taskManage", label: "定时任务管理" },
  { name: "task", label: "定时任务记录" },
]);
  // å•选框配置
  const activeRadio = ref("taskManage");
  const radios = reactive([
    { name: "taskManage", label: "定时任务管理" },
    { name: "task", label: "定时任务记录" },
  ]);
// è¡¨æ ¼æ•°æ®
const selectedRows = ref([]);
const tableData = ref([]);
const operationsArr = ref([]);
const tableColumns = ref([]);
const tableLoading = ref(false);
const total = ref(0);
const pageNum = ref(1);
const pageSize = ref(10);
  // è¡¨æ ¼æ•°æ®
  const selectedRows = ref([]);
  const tableData = ref([]);
  const operationsArr = ref([]);
  const tableColumns = ref([]);
  const tableLoading = ref(false);
  const total = ref(0);
  const pageNum = ref(1);
  const pageSize = ref(10);
// åˆ—配置
const columns = ref([
  { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
  { prop: "remarks", label: "备注", minWidth: 150 },
  { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
  {
    prop: "frequencyType",
    label: "频次",
    minWidth: 150,
    formatter: (_, __, val) => ({
      DAILY: "每日",
      WEEKLY: "每周",
      MONTHLY: "每月",
      QUARTERLY: "季度"
    }[val] || "")
  },
  {
    prop: "frequencyDetail",
    label: "开始日期与时间",
    minWidth: 150,
    formatter: (row, column, cellValue) => {
      // å…ˆåˆ¤æ–­æ˜¯å¦æ˜¯å­—符串
      if (typeof cellValue !== 'string') return '';
      let val = cellValue;
      const replacements = {
        MON: '周一',
        TUE: '周二',
        WED: '周三',
        THU: '周四',
        FRI: '周五',
        SAT: '周六',
        SUN: '周日'
      };
      // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
      return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => replacements[match]);
    }
  },
  { prop: "registrant", label: "登记人", minWidth: 100 },
  { prop: "createTime", label: "登记日期", minWidth: 100 },
]);
  // åˆ—配置
  const columns = ref([
    { prop: "taskName", label: "巡检任务名称", minWidth: 160 },
    { prop: "remarks", label: "备注", minWidth: 150 },
    { prop: "inspector", label: "执行巡检人", minWidth: 150, slot: "inspector" },
    {
      prop: "frequencyType",
      label: "频次",
      minWidth: 150,
      // formatter: (_, __, val) => ({
      //   DAILY: "每日",
      //   WEEKLY: "每周",
      //   MONTHLY: "每月",
      //   QUARTERLY: "季度"
      // }[val] || "")
      formatData: params => {
        return params === "DAILY"
          ? "每日"
          : params === "WEEKLY"
          ? "每周"
          : params === "MONTHLY"
          ? "每月"
          : params === "QUARTERLY"
          ? "季度"
          : "";
      },
    },
    {
      prop: "frequencyDetail",
      label: "开始日期与时间",
      minWidth: 150,
      formatter: (row, column, cellValue) => {
        // å…ˆåˆ¤æ–­æ˜¯å¦æ˜¯å­—符串
        if (typeof cellValue !== "string") return "";
        let val = cellValue;
        const replacements = {
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
        };
        // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
    },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "createTime", label: "登记日期", minWidth: 100 },
  ]);
// æ“ä½œåˆ—配置
const getOperationColumn = (operations) => {
  if (!operations || operations.length === 0) return null;
  const operationConfig = {
    label: "操作",
    width: 130,
    fixed: "right",
    dataType: "action",
    operation: operations.map(op => {
      switch (op) {
        case 'edit':
          return {
            name: "编辑",
            clickFun: handleAdd,
            color: "#409EFF"
          };
        case 'viewFile':
          return {
            name: "查看附件",
            clickFun: viewFile,
            color: "#67C23A"
          };
        default:
          return null;
      }
    }).filter(Boolean)
  // æ“ä½œåˆ—配置
  const getOperationColumn = operations => {
    if (!operations || operations.length === 0) return null;
    const operationConfig = {
      label: "操作",
      width: 130,
      fixed: "right",
      dataType: "action",
      operation: operations
        .map(op => {
          switch (op) {
            case "edit":
              return {
                name: "编辑",
                clickFun: handleAdd,
                color: "#409EFF",
              };
            case "viewFile":
              return {
                name: "查看附件",
                clickFun: viewFile,
                color: "#67C23A",
              };
            default:
              return null;
          }
        })
        .filter(Boolean),
    };
    return operationConfig;
  };
  return operationConfig;
};
onMounted(() => {
  radioChange('taskManage');
});
// å•选变化
const radioChange = (value) => {
  if (value === "taskManage") {
    const operationColumn = getOperationColumn(['edit']);
    tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
    operationsArr.value = ['edit'];
  } else if (value === "task") {
    const operationColumn = getOperationColumn(['viewFile']);
    tableColumns.value = [...columns.value, ...(operationColumn ? [operationColumn] : [])];
    operationsArr.value = ['viewFile'];
  }
  pageNum.value = 1;
  pageSize.value = 10;
  getList();
};
// æŸ¥è¯¢æ“ä½œ
const handleQuery = () => {
  pageNum.value = 1;
  pageSize.value = 10;
  getList();
};
// åˆ†é¡µå¤„理
const handlePagination = (val) => {
    pageNum.value = val.page;
    pageSize.value = val.limit;
    getList();
};
// èŽ·å–åˆ—è¡¨æ•°æ®
const getList = () => {
  tableLoading.value = true;
  const params = { ...queryParams, size: pageSize.value, current: pageNum.value };
  let apiCall;
  if (activeRadio.value === "task") {
    apiCall = inspectionTaskList(params);
  } else {
    apiCall = timingTaskList(params);
  }
  apiCall.then(res => {
    const rawData = res.data.records || [];
    // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
    tableData.value = rawData.map(item => {
      const processedItem = { ...item };
      // å¤„理 inspector å­—段
      if (processedItem.inspector) {
        if (typeof processedItem.inspector === 'string') {
          // å­—符串按逗号分割
          processedItem.inspector = processedItem.inspector.split(',').map(s => s.trim()).filter(s => s);
        } else if (!Array.isArray(processedItem.inspector)) {
          // éžæ•°ç»„转为数组
          processedItem.inspector = [processedItem.inspector];
        }
      } else {
        // ç©ºå€¼è®¾ä¸ºç©ºæ•°ç»„
        processedItem.inspector = [];
      }
      return processedItem;
    });
    total.value = res.data.total || 0;
  }).finally(() => {
    tableLoading.value = false;
  onMounted(() => {
    radioChange("taskManage");
  });
};
// é‡ç½®æŸ¥è¯¢
const resetQuery = () => {
  for (const key in queryParams) {
    if (!["pageNum", "pageSize"].includes(key)) {
      queryParams[key] = "";
  // å•选变化
  const radioChange = value => {
    if (value === "taskManage") {
      const operationColumn = getOperationColumn(["edit"]);
      tableColumns.value = [
        ...columns.value,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["edit"];
    } else if (value === "task") {
      const operationColumn = getOperationColumn(["viewFile"]);
      tableColumns.value = [
        ...columns.value,
        ...(operationColumn ? [operationColumn] : []),
      ];
      operationsArr.value = ["viewFile"];
    }
  }
  handleQuery();
};
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
// æ–°å¢ž / ç¼–辑
const handleAdd = (row) => {
  const type = row ? 'edit' : 'add';
  nextTick(() => {
    formDia.value?.openDialog(type, row);
  });
};
  // æŸ¥è¯¢æ“ä½œ
  const handleQuery = () => {
    pageNum.value = 1;
    pageSize.value = 10;
    getList();
  };
  // åˆ†é¡µå¤„理
  const handlePagination = val => {
    pageNum.value = val.page;
    pageSize.value = val.limit;
    getList();
  };
  // èŽ·å–åˆ—è¡¨æ•°æ®
  const getList = () => {
    tableLoading.value = true;
// æŸ¥çœ‹é™„ä»¶
const viewFile = (row) => {
  nextTick(() => {
    viewFiles.value?.openDialog(row);
  });
};
    const params = {
      ...queryParams,
      size: pageSize.value,
      current: pageNum.value,
    };
// åˆ é™¤æ“ä½œ
const handleDelete = () => {
  if (!selectedRows.value.length) {
    proxy.$modal.msgWarning("请选择要删除的数据");
    return;
  }
  const deleteIds = selectedRows.value.map(item => item.id);
  proxy.$modal.confirm('是否确认删除所选数据项?').then(() => {
    return delTimingTask(deleteIds);
  }).then(() => {
    proxy.$modal.msgSuccess("删除成功");
    handleQuery();
  }).catch(() => {});
};
    let apiCall;
    if (activeRadio.value === "task") {
      apiCall = inspectionTaskList(params);
    } else {
      apiCall = timingTaskList(params);
    }
// å¤šé€‰å˜æ›´
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
    apiCall
      .then(res => {
        const rawData = res.data.records || [];
        // å¤„理 inspector å­—段,将字符串转换为数组(适用于所有情况)
        tableData.value = rawData.map(item => {
          const processedItem = { ...item };
// å¯¼å‡º
const handleOut = () => {
  ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      // æ ¹æ®å½“前选中的标签页调用不同的导出接口
      if (activeRadio.value === "taskManage") {
        // å®šæ—¶ä»»åŠ¡ç®¡ç†
        proxy.download("/timingTask/export", {}, "定时任务管理.xlsx");
      } else if (activeRadio.value === "task") {
        // å®šæ—¶ä»»åŠ¡è®°å½•
        proxy.download("/inspectionTask/export", {}, "定时任务记录.xlsx");
          // å¤„理 inspector å­—段
          if (processedItem.inspector) {
            if (typeof processedItem.inspector === "string") {
              // å­—符串按逗号分割
              processedItem.inspector = processedItem.inspector
                .split(",")
                .map(s => s.trim())
                .filter(s => s);
            } else if (!Array.isArray(processedItem.inspector)) {
              // éžæ•°ç»„转为数组
              processedItem.inspector = [processedItem.inspector];
            }
          } else {
            // ç©ºå€¼è®¾ä¸ºç©ºæ•°ç»„
            processedItem.inspector = [];
          }
          return processedItem;
        });
        total.value = res.data.total || 0;
      })
      .finally(() => {
        tableLoading.value = false;
      });
  };
  // é‡ç½®æŸ¥è¯¢
  const resetQuery = () => {
    for (const key in queryParams) {
      if (!["pageNum", "pageSize"].includes(key)) {
        queryParams[key] = "";
      }
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
    }
    handleQuery();
  };
  // æ–°å¢ž / ç¼–辑
  const handleAdd = row => {
    const type = row ? "edit" : "add";
    nextTick(() => {
      formDia.value?.openDialog(type, row);
    });
};
  };
  // æŸ¥çœ‹é™„ä»¶
  const viewFile = row => {
    nextTick(() => {
      viewFiles.value?.openDialog(row);
    });
  };
  // åˆ é™¤æ“ä½œ
  const handleDelete = () => {
    if (!selectedRows.value.length) {
      proxy.$modal.msgWarning("请选择要删除的数据");
      return;
    }
    const deleteIds = selectedRows.value.map(item => item.id);
    proxy.$modal
      .confirm("是否确认删除所选数据项?")
      .then(() => {
        return delTimingTask(deleteIds);
      })
      .then(() => {
        proxy.$modal.msgSuccess("删除成功");
        handleQuery();
      })
      .catch(() => {});
  };
  // å¤šé€‰å˜æ›´
  const handleSelectionChange = selection => {
    selectedRows.value = selection;
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    })
      .then(() => {
        // æ ¹æ®å½“前选中的标签页调用不同的导出接口
        if (activeRadio.value === "taskManage") {
          // å®šæ—¶ä»»åŠ¡ç®¡ç†
          proxy.download("/timingTask/export", {}, "定时任务管理.xlsx");
        } else if (activeRadio.value === "task") {
          // å®šæ—¶ä»»åŠ¡è®°å½•
          proxy.download("/inspectionTask/export", {}, "定时任务记录.xlsx");
        }
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
      });
  };
</script>
<style scoped>
.person-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}
  .person-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
  }
.person-tag {
  margin-right: 4px;
  margin-bottom: 2px;
}
  .person-tag {
    margin-right: 4px;
    margin-bottom: 2px;
  }
.no-data {
  color: #909399;
  font-size: 14px;
}
  .no-data {
    color: #909399;
    font-size: 14px;
  }
</style>
src/views/fileManagement/document/attachmentManager.vue
@@ -13,7 +13,6 @@
          :on-remove="handleRemove"
          :file-list="fileList"
          multiple
          :limit="10"
          :show-file-list="false"
          :data="{documentId: currentDocumentId}"
          accept=".doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt,.xml,.jpg,.jpeg,.png,.gif,.bmp,.rar,.zip,.7z"
@@ -30,7 +29,7 @@
      <!-- é™„件列表 -->
      <div class="attachment-list">
        <el-table :data="fileList" border height="400px" v-loading="loading">
        <el-table :data="fileList" border max-height="400px" v-loading="loading">
          <el-table-column label="序号" type="index" width="60" align="center" />
          <el-table-column label="附件名称" prop="name" min-width="200" show-overflow-tooltip />
          <el-table-column label="文件大小" prop="size" width="100" align="center">
src/views/index.vue
@@ -1,345 +1,436 @@
<template>
    <div class="dashboard">
        <!-- é¡¶éƒ¨æ¨ªå‘两栏 -->
        <div class="dashboard-top">
            <!-- å·¦ï¼šä¼ä¸šä¿¡æ¯+三大数据卡片(上下排列) -->
            <div class="top-left">
                <div class="company-info">
                    <div class="section-title">登陆信息</div>
                    <div style="display: flex;align-items: center;gap: 20px">
                        <img :src="userStore.avatar" class="avatar" alt=""/>
                        <div class="company-card">
                            <div class="company-name">{{userStore.name}}</div>
                            <div class="company-meta">{{userStore.roleName}}</div>
                        </div>
                        <div style="display: flex;align-items: center;gap: 8px">
                            <el-icon color="#5053B5" size="22"><Clock /></el-icon>
                            <span>登陆日期:{{userStore.currentLoginTime}}</span>
                        </div>
                    </div>
                </div>
                <div class="data-cards">
                    <div class="data-card sales">
                        <div class="data-title">销售数据</div>
                        <div class="data-num">
                            <div>
                                <div class="data-desc">本月销售额/元</div>
                                <div class="data-value">{{businessInfo.monthSaleMoney}}</div>
                            </div>
                            <div>
                                <div class="data-desc">未开票金额/元</div>
                                <div class="data-value">{{businessInfo.monthSaleHaveMoney}}</div>
                            </div>
                        </div>
                    </div>
                    <div class="data-card purchase">
                        <div class="data-title">采购数据</div>
                        <div class="data-num">
                            <div>
                                <div class="data-desc">本月采购额/元</div>
                                <div class="data-value">{{businessInfo.monthPurchaseMoney}}</div>
                            </div>
                            <div>
                                <div class="data-desc">待付款金额/元</div>
                                <div class="data-value">{{businessInfo.monthPurchaseHaveMoney}}</div>
                            </div>
                        </div>
                    </div>
                    <div class="data-card inventory">
                        <div class="data-title">库存数据</div>
                        <div class="data-num">
                            <div>
                                <div class="data-desc">当前库存总量/ä»¶</div>
                                <div class="data-value">{{businessInfo.inventoryNum}}</div>
                            </div>
                            <div>
                                <div class="data-desc">今日入库/ä»¶</div>
                                <div class="data-value">{{businessInfo.todayInventoryNum}}</div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            <!-- å³ï¼šå¾…办事项 -->
            <div class="todo-panel">
                <div class="section-title">待办事项</div>
                <ul class="todo-list" v-if="todoList.length > 0">
                    <li v-for="item in todoList" :key="item.id">
                        <div style="display: flex;flex-direction: column;justify-content: space-between;width: 100%;gap: 20px">
                            <div style="display: flex;justify-content: space-between;align-items: center;">
                                <div class="todo-title">待办编号:{{item.approveId}}</div>
                                <div class="todo-division">部门:{{item.approveDeptName}}</div>
                                <div class="todo-time">{{item.approveTime}}</div>
                            </div>
                            <div class="todo-division">待办事由:{{item.approveReason}}</div>
                        </div>
                    </li>
                </ul>
                <div v-else style="text-align: center">
                    æš‚无数据
                </div>
            </div>
        </div>
        <!-- ä¸­éƒ¨æ¨ªå‘两栏 -->
        <div class="dashboard-row">
            <div class="main-panel">
                <div class="section-title">客户合同金额分析</div>
                <div class="contract-summary">
                    <div class="contract-info">
                        <img src="../assets/images/khtitle.png" alt="" style="width: 42px"/>
                        <div class="contract-card">
                            <div class="contract-name">总合同金额(元)</div>
                            <div class="contract-meta">
                                <div class="main-amount">{{sum}}</div>
                                <div>周同比: <span class="up">{{yny}}% </span> æ—¥çŽ¯æ¯”: <span class="up">{{chain}}% </span></div>
                            </div>
                        </div>
                    </div>
                </div>
                <div style="display: flex;align-items: center;gap: 20px;justify-content: space-evenly;height: 180px;margin-top: 20px">
                    <div>
                        <Echarts ref="chart" :legend="pieLegend" :chartStyle="chartStylePie"
                                         :series="materialPieSeries"
                                         :tooltip="pieTooltip"></Echarts>
                    </div>
                    <ul class="contract-list">
                        <li v-for="item in materialPieSeries[0].data" :key="item.name">
                            <div style="display: flex;align-items: center;justify-content: space-between;width: 100%">
                                <div class="line" :style="{color: item.itemStyle.color}">●{{item.name}}</div>
                                <div style="width: 70px">{{item.rate}}%</div>
                                <div>ï¿¥{{item.value}}</div>
                            </div>
                        </li>
                    </ul>
                </div>
            </div>
            <div class="main-panel">
                <div style="display: flex;justify-content: space-between;">
                    <div class="section-title">应收应付统计</div>
                    <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable">
                        <el-radio-button label="按周" :value="1" />
                        <el-radio-button label="按月" :value="2" />
                        <el-radio-button label="按季度" :value="3" />
                    </el-radio-group>
                </div>
                <Echarts ref="chart"
                                 :color="barColors2"
                                 :chartStyle="chartStyle"
                                 :grid="grid"
                                 :series="barSeries"
                                 :tooltip="tooltip"
                                 :xAxis="xAxis"
                                 :yAxis="yAxis"
                                 style="height: 260px"></Echarts>
            </div>
        </div>
        <!-- åº•部横向两栏 -->
        <div class="dashboard-row">
<!--            <div class="main-panel">-->
<!--                <div class="section-title">质量统计</div>-->
<!--                <div class="quality-cards">-->
<!--                    <div class="quality-card one">原材料已检测数 <span>{{qualityStatisticsObject.supplierNum}}ä»¶</span></div>-->
<!--                    <div class="quality-card two">过程检验数量 <span>{{qualityStatisticsObject.processNum}}ä»¶</span></div>-->
<!--                    <div class="quality-card three">出厂已检数量 <span>{{qualityStatisticsObject.factoryNum}}ä»¶</span></div>-->
<!--                </div>-->
<!--                <Echarts ref="chart"-->
<!--                                 :chartStyle="chartStyle"-->
<!--                                 :grid="grid"-->
<!--                                 :legend="barLegend"-->
<!--                                 :series="barSeries1"-->
<!--                                 :tooltip="tooltip"-->
<!--                                 :xAxis="xAxis1"-->
<!--                                 :yAxis="yAxis1"-->
<!--                                 style="height: 260px"></Echarts>-->
<!--            </div>-->
            <div class="main-panel">
                <div class="section-title">回款与开票分析</div>
                <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
                                 :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" style="height: 270px;"></Echarts>
            </div>
        </div>
    </div>
  <div class="dashboard">
    <!-- é¡¶éƒ¨æ¨ªå‘两栏 -->
    <div class="dashboard-top">
      <!-- å·¦ï¼šä¼ä¸šä¿¡æ¯+三大数据卡片(上下排列) -->
      <div class="top-left">
        <div class="company-info">
          <!-- é¡¶éƒ¨é—®å€™æ¡ -->
          <div class="welcome-banner">
            <div class="welcome-title">
              <span class="welcome-user">{{ userStore.roleName || '系统管理员' }}</span>
              <span> æ‚¨å¥½ï¼ç¥æ‚¨å¼€å¿ƒæ¯ä¸€å¤©</span>
            </div>
            <div class="welcome-time">登录于: {{ userStore.currentLoginTime }}</div>
          </div>
          <!-- ç”¨æˆ·ä¿¡æ¯å¡ç‰‡ -->
          <div class="user-card">
            <img :src="userStore.avatar" class="avatar" alt="" />
            <div class="user-card-main">
              <div class="user-name">{{ userStore.name }}</div>
              <div class="user-role">{{ userStore.roleName }}</div>
              <div class="user-meta">
                <span>{{ userStore.phoneNumber || '123456789' }}</span>
                <span class="sep">|</span>
                <span>{{ userStore.deptName || '组织架构' }}</span>
                <span class="sep">|</span>
                <span>{{ userStore.postName || '岗位名' }}</span>
              </div>
            </div>
          </div>
        </div>
      </div>
      <div class="data-cards">
        <div class="data-card sales">
          <div class="data-title">销售数据</div>
          <div class="data-num">
            <div>
              <div class="data-desc">本月销售额/元</div>
              <div class="data-value">{{ businessInfo.monthSaleMoney }}</div>
            </div>
            <div>
              <div class="data-desc">未开票金额/元</div>
              <div class="data-value">{{ businessInfo.monthSaleHaveMoney }}</div>
            </div>
          </div>
        </div>
        <div class="data-card purchase">
          <div class="data-title">采购数据</div>
          <div class="data-num">
            <div>
              <div class="data-desc">本月采购额/元</div>
              <div class="data-value">{{ businessInfo.monthPurchaseMoney }}</div>
            </div>
            <div>
              <div class="data-desc">待付款金额/元</div>
              <div class="data-value">{{ businessInfo.monthPurchaseHaveMoney }}</div>
            </div>
          </div>
        </div>
        <div class="data-card inventory">
          <div class="data-title">库存数据</div>
          <div class="data-num">
            <div>
              <div class="data-desc">当前库存总量/ä»¶</div>
              <div class="data-value">{{ businessInfo.inventoryNum }}</div>
            </div>
            <div>
              <div class="data-desc">今日入库/ä»¶</div>
              <div class="data-value">{{ businessInfo.todayInventoryNum }}</div>
            </div>
          </div>
        </div>
      </div>
      <!-- å³ï¼šå¾…办事项 -->
      <div class="todo-panel">
        <div class="section-title">待办事项</div>
        <ul class="todo-list" v-if="todoList.length > 0">
          <li v-for="item in todoList" :key="item.id">
            <div style="display: flex;flex-direction: column;justify-content: space-between;width: 100%;gap: 20px">
              <div style="display: flex;justify-content: space-between;align-items: center;">
                <div class="todo-title">待办编号:{{ item.approveId }}</div>
                <div class="todo-division">部门:{{ item.approveDeptName }}</div>
                <div class="todo-time">{{ item.approveTime }}</div>
              </div>
              <div class="todo-division">待办事由:{{ item.approveReason }}</div>
            </div>
          </li>
        </ul>
        <div v-else style="text-align: center">
          æš‚无数据
        </div>
      </div>
    </div>
    <div class="dashboard-row">
      <div class="main-panel process-panel">
        <div class="process-panel__header">
          <div class="section-title">工序数据生产统计明细</div>
          <div style="display: flex; gap: 10px; align-items: center;">
            <el-button type="primary" size="small" plain icon="Filter" @click="openProcessDialog">选择工序</el-button>
            <el-button type="info" size="small" plain icon="Refresh" @click="resetProcessFilter">重置</el-button>
            <el-radio-group v-model="processRange" size="small" @change="refreshProcessStats">
              <el-radio-button :value="1">日</el-radio-button>
              <el-radio-button :value="2">周</el-radio-button>
              <el-radio-button :value="3">月</el-radio-button>
            </el-radio-group>
          </div>
        </div>
        <div class="process-panel__body">
          <div class="process-panel__chart">
            <Echarts :chartStyle="{ width: '100%', height: '100%' }" :grid="processGrid" :series="processSeries"
              :tooltip="processTooltip" :xAxis="processXAxis" :yAxis="processYAxis" style="height: 100%"
              @click="handleChartClick" />
          </div>
          <div class="process-panel__aside">
            <div class="process-legend">
              <div class="process-legend__item">
                <span class="dot dot-blue"></span><span>投入量</span>
              </div>
              <div class="process-legend__item">
                <span class="dot dot-yellow"></span><span>报废量</span>
              </div>
              <div class="process-legend__item">
                <span class="dot dot-teal"></span><span>产出量</span>
              </div>
            </div>
            <div class="process-card process-card--name">{{ processAside.processName }}</div>
            <div class="process-card">
              <div class="process-card__label">累计总投入</div>
              <div class="process-card__value">{{ formatAmount(processAside.totalInput) }}<span class="unit">元</span>
              </div>
            </div>
            <div class="process-card">
              <div class="process-card__label">累计总报废</div>
              <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }}<span class="unit">元</span>
              </div>
            </div>
            <div class="process-card">
              <div class="process-card__label">累计总产出</div>
              <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }}<span class="unit">元</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- å·¥åºé€‰æ‹©å¼¹çª— -->
    <el-dialog v-model="processDialogVisible" title="选择工序" width="500px" append-to-body>
      <div class="process-selection-wrapper">
        <el-checkbox-group v-model="tempProcessIds">
          <div class="process-grid">
            <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border>
              {{ item.name }}
            </el-checkbox>
          </div>
        </el-checkbox-group>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="processDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleProcessDialogConfirm">确认</el-button>
        </span>
      </template>
    </el-dialog>
    <!-- ä¸­éƒ¨æ¨ªå‘两栏 -->
    <div class="dashboard-row">
      <div class="main-panel">
        <div class="section-title">客户合同金额分析</div>
        <div class="contract-summary">
          <div class="contract-info">
            <img src="../assets/images/khtitle.png" alt="" style="width: 42px" />
            <div class="contract-card">
              <div class="contract-name">总合同金额(元)</div>
              <div class="contract-meta">
                <div class="main-amount">{{ sum }}</div>
                <div>周同比: <span class="up">{{ yny }}% </span> æ—¥çŽ¯æ¯”: <span class="up">{{ chain }}% </span></div>
              </div>
            </div>
          </div>
        </div>
        <div
          style="display: flex;align-items: center;gap: 20px;justify-content: space-evenly;height: 180px;margin-top: 20px">
          <div>
            <Echarts ref="chart" :legend="pieLegend" :chartStyle="chartStylePie" :series="materialPieSeries"
              :tooltip="pieTooltip"></Echarts>
          </div>
          <ul class="contract-list">
            <li v-for="item in materialPieSeries[0].data" :key="item.name">
              <div style="display: flex;align-items: center;justify-content: space-between;width: 100%">
                <div class="line" :style="{ color: item.itemStyle.color }">●{{ item.name }}</div>
                <div style="width: 70px">{{ item.rate }}%</div>
                <div>ï¿¥{{ item.value }}</div>
              </div>
            </li>
          </ul>
        </div>
      </div>
      <div class="main-panel">
        <div style="display: flex;justify-content: space-between;">
          <div class="section-title">应收应付统计</div>
          <!--                    <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable">-->
          <!--                        <el-radio-button label="按周" :value="1" />-->
          <!--                        <el-radio-button label="按月" :value="2" />-->
          <!--                        <el-radio-button label="按季度" :value="3" />-->
          <!--                    </el-radio-group>-->
        </div>
        <Echarts ref="chart" :color="barColors2" :chartStyle="chartStyle" :grid="grid" :series="barSeries"
          :tooltip="tooltip" :xAxis="xAxis" :yAxis="yAxis" style="height: 260px"></Echarts>
      </div>
    </div>
    <!-- åº•部横向两栏 -->
    <div class="dashboard-row">
      <div class="main-panel">
        <div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;">
          <div class="section-title" style="margin-bottom: 0;">质量统计</div>
          <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo">
            <el-radio-button :value="1">周</el-radio-button>
            <el-radio-button :value="2">月</el-radio-button>
            <el-radio-button :value="3">季度</el-radio-button>
          </el-radio-group>
        </div>
        <div class="quality-cards">
          <div class="quality-card one">原材料已检测数 <span>{{ qualityStatisticsObject.supplierNum }}ä»¶</span></div>
          <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}ä»¶</span></div>
          <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}ä»¶</span></div>
        </div>
        <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="barLegend" :series="barSeries1"
          :tooltip="tooltip" :xAxis="xAxis1" :yAxis="yAxis1" style="height: 260px"></Echarts>
      </div>
      <div class="main-panel">
        <div class="section-title">回款与开票分析</div>
        <Echarts ref="invoiceChart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
          :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" style="height: 270px;" />
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed, reactive } from 'vue'
import Echarts from "@/components/Echarts/echarts.vue";
import * as echarts from 'echarts';
import useUserStore from "@/store/modules/user.js";
import {
    analysisCustomerContractAmounts, getAmountHalfYear,
    getBusiness,
    homeTodos,
    qualityStatistics,
    statisticsReceivablePayable
  analysisCustomerContractAmounts, getAmountHalfYear,
  getBusiness,
  homeTodos,
  processDataProductionStatistics,
  statisticsReceivablePayable,
  qualityInspectionStatistics
} from "@/api/viewIndex.js";
import { list } from '@/api/productionManagement/productionProcess';
const userStore = useUserStore()
const processOptions = ref([])
const selectedProcessIds = ref([])
const tempProcessIds = ref([])
const processDialogVisible = ref(false)
const activeProcessIndex = ref(0)
const businessInfo = ref({
    inventoryNum: 0,
    monthPurchaseHaveMoney: 0,
    monthPurchaseMoney: 0,
    monthSaleHaveMoney: 0,
    monthSaleMoney: 0,
    todayInventoryNum: 0,
  inventoryNum: 0,
  monthPurchaseHaveMoney: 0,
  monthPurchaseMoney: 0,
  monthSaleHaveMoney: 0,
  monthSaleMoney: 0,
  todayInventoryNum: 0,
})
const qualityStatisticsObject = ref({
    supplierNum: 0,
    processNum: 0,
    factoryNum: 0,
  supplierNum: 0,
  processNum: 0,
  factoryNum: 0,
})
const sum = ref(0)
const yny = ref(0)
const chain = ref(0)
const pieLegend = reactive({
    show: false,
  show: false,
})
const barSeries = ref([
    {
        type: 'bar',
        data: [],
        label: {
            show: true,
        }
    },
  {
    type: 'bar',
    data: [],
    label: {
      show: true,
    }
  },
])
const barSeries1 = ref([
    {
        name: '原材料不合格数',
        type: 'bar',
        barGap: 0,
        emphasis: {
            focus: 'series'
        },
        data: []
    },
    {
        name: '过程不合格数',
        type: 'bar',
        emphasis: {
            focus: 'series'
        },
        data: []
    },
    {
        name: '出厂不合格数',
        type: 'bar',
        emphasis: {
            focus: 'series'
        },
        data: []
    },
  {
    name: '原材料不合格数',
    type: 'bar',
    barGap: 0,
    emphasis: {
      focus: 'series'
    },
    data: []
  },
  {
    name: '过程不合格数',
    type: 'bar',
    emphasis: {
      focus: 'series'
    },
    data: []
  },
  {
    name: '出厂不合格数',
    type: 'bar',
    emphasis: {
      focus: 'series'
    },
    data: []
  },
])
const chartStyle = {
    width: '100%',
    height: '100%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
  width: '100%',
  height: '100%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
}
const chartStylePie = {
    width: '140%',
    height: '140%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
  width: '140%',
  height: '140%' // è®¾ç½®å›¾è¡¨å®¹å™¨çš„高度
}
const grid = {
    left: '3%',
        right: '4%',
        bottom: '3%',
        containLabel: true
  left: '3%',
  right: '4%',
  bottom: '3%',
  containLabel: true
}
const barLegend = {
    show: true,
    data: ['原材料不合格数', '过程不合格数', '出厂不合格数']
  show: true,
  data: ['原材料不合格数', '过程不合格数', '出厂不合格数']
}
const barLegend1 = {
    show: true,
    data: ['预付账款', '应付账款', '预收账款', '应收账款']
  show: true,
  data: ['预付账款', '应付账款', '预收账款', '应收账款']
}
const lineLegend = {
    show: true,
    data: ['开票', '回款']
  show: true,
  data: ['开票', '回款']
}
const tooltip = {
    trigger: 'axis',
    axisPointer: {
        type: 'shadow'
    }
  trigger: 'axis',
  axisPointer: {
    type: 'shadow'
  }
}
const xAxis = [{
    type: 'value',
  type: 'value',
}]
const xAxis1 = ref([{
    type: 'category',
    axisTick: { show: false },
    data: []
  type: 'category',
  axisTick: { show: false },
  data: []
}])
const yAxis = [{
    type: 'category',
    data: [ '应付账款', '应收账款',]
  type: 'category',
  data: ['应付账款', '应收账款',]
}]
const yAxis1 = [{
    type: 'value'
  type: 'value'
}]
const pieTooltip = reactive({
    trigger: 'item',
    formatter: function (params) {
        // åŠ¨æ€ç”Ÿæˆæç¤ºä¿¡æ¯ï¼ŒåŸºäºŽæ•°æ®é¡¹çš„ name å±žæ€§
        const description = params.name === '本月回款金额' ? '本月回款金额' : '应收款金额';
        return `${description} ${formatNumber(params.value)}元 ${params.percent}%`;
    },
    position: 'right'
  trigger: 'item',
  formatter: function (params) {
    // åŠ¨æ€ç”Ÿæˆæç¤ºä¿¡æ¯ï¼ŒåŸºäºŽæ•°æ®é¡¹çš„ name å±žæ€§
    const description = params.name === '本月回款金额' ? '本月回款金额' : '应收款金额';
    return `${description} ${formatNumber(params.value)}元 ${params.percent}%`;
  },
  position: 'right'
})
const materialPieSeries = ref([
    {
        type: 'pie',
        radius: ['66%', '90%'],
        avoidLabelOverlap: false,
        itemStyle: {
            borderColor: '#fff',
            borderWidth: 2
        },
        label: {
            show: false
        },
        data: []
    }
  {
    type: 'pie',
    radius: ['66%', '90%'],
    avoidLabelOverlap: false,
    itemStyle: {
      borderColor: '#fff',
      borderWidth: 2
    },
    label: {
      show: false
    },
    data: []
  }
])
const lineSeries = ref([
    {
        type: 'line',
        data: [],
        label: {
            show: true
        },
  {
    type: 'line',
    data: [],
    label: {
      show: true
    },
    showSymbol: true, // æ˜¾ç¤ºåœ†ç‚¹
    },
  },
])
const tooltipLine = {
    trigger: 'axis',
  trigger: 'axis',
}
const yAxis2 = ref([
    {
        type: 'value',
    }
  {
    type: 'value',
  }
])
const xAxis2 = ref([
    {
        type: 'category',
        data: [],
        axisLabel: {
            interval: 0,
            formatter: function(value) {
                return value.replace(/~/g, '\n');
            },
        }
    }
  {
    type: 'category',
    data: [],
    axisLabel: {
      interval: 0,
      formatter: function (value) {
        return value.replace(/~/g, '\n');
      },
    }
  }
])
// å¾…办事项
const todoList = ref([])
const radio1 = ref(1)
const qualityRange = ref(1)
// å›¾è¡¨å¼•用
const barChart = ref(null)
@@ -352,453 +443,821 @@
}
onMounted(() => {
    getBusinessData()
    analysisCustomer()
    todoInfoS()
    statisticsReceivable()
    qualityStatisticsInfo()
    getAmountHalfYearNum()
  getBusinessData()
  analysisCustomer()
  todoInfoS()
  statisticsReceivable()
  qualityStatisticsInfo()
  getAmountHalfYearNum()
  getProcessList()
})
// æ•°æ®ç»Ÿè®¡
const getBusinessData = () => {
    getBusiness().then((res) => {
        businessInfo.value = {...res.data}
    })
  getBusiness().then((res) => {
    businessInfo.value = { ...res.data }
  })
}
// åˆåŒé‡‘额
const analysisCustomer = () => {
    analysisCustomerContractAmounts().then((res) => {
        sum.value = res.data.sum
        yny.value = res.data.yny
        chain.value = res.data.chain
  analysisCustomerContractAmounts().then((res) => {
    sum.value = res.data.sum
    yny.value = res.data.yny
    chain.value = res.data.chain
    // ä¸ºæ¯ä¸ªæ•°æ®é¡¹åˆ†é…éšæœºé¢œè‰²
    materialPieSeries.value[0].data = res.data.item.map(item => ({
      ...item,
      itemStyle: { color: getRandomColor() }
    }))
    })
  })
}
// å¾…办事项
const todoInfoS = () => {
    homeTodos().then((res) => {
        todoList.value = res.data
    })
  homeTodos().then((res) => {
    todoList.value = res.data
  })
}
// èŽ·å–å·¥åºåˆ—è¡¨
const getProcessList = () => {
  list().then(res => {
    processOptions.value = res.data
  })
}
const openProcessDialog = () => {
  tempProcessIds.value = [...selectedProcessIds.value]
  processDialogVisible.value = true
}
const handleProcessDialogConfirm = () => {
  selectedProcessIds.value = [...tempProcessIds.value]
  processDialogVisible.value = false
  refreshProcessStats()
}
const resetProcessFilter = () => {
  selectedProcessIds.value = []
  tempProcessIds.value = []
  refreshProcessStats()
}
const handleChartClick = (params) => {
  if (params && params.dataIndex !== undefined) {
    activeProcessIndex.value = params.dataIndex
  }
}
// åº”付应收统计
const statisticsReceivable = (type) => {
    statisticsReceivablePayable({type: radio1.value}).then((res) => {
        barSeries.value[0].data = [
            // { value: res.data.prepayMoney, itemStyle: { color: barColors2[0] } },
            { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } },
            // { value: res.data.advanceMoney, itemStyle: { color: barColors2[2] } },
            { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } }
        ]
    })
const statisticsReceivable = () => {
  statisticsReceivablePayable({ type: radio1.value }).then((res) => {
    barSeries.value[0].data = [
      // { value: res.data.prepayMoney, itemStyle: { color: barColors2[0] } },
      { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } },
      // { value: res.data.advanceMoney, itemStyle: { color: barColors2[2] } },
      { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } }
    ]
  })
}
// è´¨æ£€ç»Ÿè®¡
const qualityStatisticsInfo = () => {
    qualityStatistics().then((res) => {
        res.data.item.forEach(item => {
            xAxis1.value[0].data.push(item.date)
            barSeries1.value[0].data.push(item.supplierNum)
            barSeries1.value[1].data.push(item.processNum)
            barSeries1.value[2].data.push(item.factoryNum)
        })
        qualityStatisticsObject.value.supplierNum = res.data.supplierNum
        qualityStatisticsObject.value.processNum = res.data.processNum
        qualityStatisticsObject.value.factoryNum = res.data.factoryNum
    })
  qualityInspectionStatistics({ type: qualityRange.value }).then((res) => {
    xAxis1.value[0].data = []
    barSeries1.value[0].data = []
    barSeries1.value[1].data = []
    barSeries1.value[2].data = []
    res.data.item.forEach(item => {
      xAxis1.value[0].data.push(item.date)
      barSeries1.value[0].data.push(item.supplierNum)
      barSeries1.value[1].data.push(item.processNum)
      barSeries1.value[2].data.push(item.factoryNum)
    })
    qualityStatisticsObject.value.supplierNum = res.data.supplierNum
    qualityStatisticsObject.value.processNum = res.data.processNum
    qualityStatisticsObject.value.factoryNum = res.data.factoryNum
  })
}
const getAmountHalfYearNum = async () => {
    const res = await getAmountHalfYear()
    console.log(res)
    const monthName = []
    const receiptAmount = []
    const invoiceAmount = []
    res.data.forEach(item => {
        monthName.push(item.month)
        receiptAmount.push(item.receiptAmount)
        invoiceAmount.push(item.invoiceAmount)
    })
    // æ­£ç¡®å“åº”式赋值:创建新的 xAxis å’Œ series å¯¹è±¡
    xAxis2.value[0].data = monthName
    xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~'));
    lineSeries.value = [
        {
            name: '开票',
            type: 'line',
            data: receiptAmount,
            stack: 'Total',
            areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    {
                        offset: 0,
                        color: 'rgba(131, 207, 255, 1)'
                    },
                    {
                        offset: 1,
                        color: 'rgba(186, 228, 255, 1)'
                    }
                ])
            },
            itemStyle: {
                color: '#2D99FF',
                borderColor: '#2D99FF'
            },
            emphasis: {
                focus: 'series'
            },
            lineStyle: {
                width: 0
            },
            showSymbol: true,
        },
        {
            name: '回款',
            type: 'line',
            data: invoiceAmount,
            stack: 'Total',
            lineStyle: {
                width: 0
            },
            itemStyle: {
                color: '#83CFFF',
                borderColor: '#83CFFF'
            },
            showSymbol: true,
            areaStyle: {
                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
                    {
                        offset: 0,
                        color: 'rgba(54, 153, 255, 1)'
                    },
                    {
                        offset: 1,
                        color: 'rgba(89, 169, 254, 1)'
                    }
                ])
            },
            emphasis: {
                focus: 'series'
            },
        }
    ]
  const res = await getAmountHalfYear()
  console.log(res)
  const monthName = []
  const receiptAmount = []
  const invoiceAmount = []
  res.data.forEach(item => {
    monthName.push(item.month)
    receiptAmount.push(item.receiptAmount)
    invoiceAmount.push(item.invoiceAmount)
  })
  // æ­£ç¡®å“åº”式赋值:创建新的 xAxis å’Œ series å¯¹è±¡
  xAxis2.value[0].data = monthName
  xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~'));
  lineSeries.value = [
    {
      name: '开票',
      type: 'line',
      data: receiptAmount,
      stack: 'Total',
      areaStyle: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          {
            offset: 0,
            color: 'rgba(131, 207, 255, 1)'
          },
          {
            offset: 1,
            color: 'rgba(186, 228, 255, 1)'
          }
        ])
      },
      itemStyle: {
        color: '#2D99FF',
        borderColor: '#2D99FF'
      },
      emphasis: {
        focus: 'series'
      },
      lineStyle: {
        width: 0
      },
      showSymbol: true,
    },
    {
      name: '回款',
      type: 'line',
      data: invoiceAmount,
      stack: 'Total',
      lineStyle: {
        width: 0
      },
      itemStyle: {
        color: '#83CFFF',
        borderColor: '#83CFFF'
      },
      showSymbol: true,
      areaStyle: {
        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
          {
            offset: 0,
            color: 'rgba(54, 153, 255, 1)'
          },
          {
            offset: 1,
            color: 'rgba(89, 169, 254, 1)'
          }
        ])
      },
      emphasis: {
        focus: 'series'
      },
    }
  ]
}
// å·¥åºæ•°æ®ç”Ÿäº§ç»Ÿè®¡æ˜Žç»†ï¼ˆå‡æ•°æ® + å›¾è¡¨ï¼‰
const processRange = ref(1)
const processChartData = ref([])
const processXAxis = ref([
  {
    nameTextStyle: { color: 'rgba(0,0,0,0.35)', fontSize: 12 },
    axisLabel: { color: 'rgba(0,0,0,0.35)' },
    splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)', type: 'dashed' } },
  },
])
const processYAxis = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLine: { show: false },
    axisLabel: { color: 'rgba(0,0,0,0.45)' },
    data: [],
  },
])
const processGrid = reactive({ left: 0, right: 100, top: 30, bottom: 20, containLabel: true })
const processTooltip = reactive({
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  formatter: (params) => {
    const name = params?.[0]?.name ?? ''
    const list = Array.isArray(params) ? params : []
    const lines = list
      .map((p) => {
        const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>`
        return `${colorBox}${p.seriesName} <b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>`
      })
      .join('<br/>')
    return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>`
  },
})
const processSeries = computed(() => {
  const input = processChartData.value.map((i) => i.input)
  const scrap = processChartData.value.map((i) => i.scrap)
  const output = processChartData.value.map((i) => i.output)
  return [
    {
      name: '投入量',
      type: 'bar',
      stack: 'total',
      barWidth: 22,
      itemStyle: { color: '#1E5BFF', borderRadius: [6, 0, 0, 6] },
      data: input,
    },
    {
      name: '报废量',
      type: 'bar',
      stack: 'total',
      barWidth: 22,
      itemStyle: { color: '#F7B500' },
      data: scrap,
    },
    {
      name: '产出量',
      type: 'bar',
      stack: 'total',
      barWidth: 22,
      itemStyle: { color: '#19C6C6', borderRadius: [0, 6, 6, 0] },
      data: output,
    },
  ]
})
const processAside = computed(() => {
  const list = processChartData.value
  const item = list[activeProcessIndex.value] || {}
  return {
    processName: item.name || '暂无数据',
    totalInput: item.input || 0,
    totalScrap: item.scrap || 0,
    totalOutput: item.output || 0,
  }
})
const formatAmount = (n) => {
  const num = Number(n || 0)
  return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const refreshProcessStats = () => {
  processDataProductionStatistics({
    type: processRange.value,
    processIds: selectedProcessIds.value.length > 0 ? selectedProcessIds.value.join(',') : null
  }).then(res => {
    processChartData.value = res.data.map(item => ({
      name: item.processName,
      input: item.totalInput,
      scrap: item.totalScrap,
      output: item.totalOutput
    }))
    processYAxis.value[0].data = processChartData.value.map((i) => i.name)
    activeProcessIndex.value = 0
  })
}
onMounted(() => {
  getBusinessData()
  analysisCustomer()
  todoInfoS()
  statisticsReceivable()
  qualityStatisticsInfo()
  getAmountHalfYearNum()
  refreshProcessStats()
})
</script>
<style scoped>
.dashboard {
    background: #f5f7fa;
    min-height: 100vh;
    padding: 20px;
    box-sizing: border-box;
}
.dashboard-top {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
}
.company-info {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 20px;
    min-width: 0;
    background-color: #EFF2FB; /* ä½¿ç”¨æŒ‡å®šçš„背景颜色 */
    background-image: url("../assets/images/denglu.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    border-radius: 12px;
    height: 138px;
}
.avatar {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    object-fit: contain;
    background: #fff;
    border: 1px solid #eee;
}
.company-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    position: relative;
    padding-right: 15px;
  background: #f5f7fa;
  min-height: 100vh;
  padding: 20px;
  box-sizing: border-box;
}
.company-card::after {
    content: '';
    position: absolute;
    right: 0;
    top: 0;
    bottom: 0;
    width: 1px;
    background-color: #C9C5C5;
    border-radius: 2px;
.dashboard-top {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
  align-items: flex-start;
  justify-content: space-evenly;
}
.company-name {
    font-weight: 400;
    font-size: 16px;
    color: #161A9A;
.company-info {
  padding: 0;
  overflow: hidden;
  border-radius: 12px;
  background: #fff;
  height: 100%;
}
.company-meta {
    font-weight: 400;
    font-size: 12px;
    color: #818185;
.welcome-banner {
  padding: 10px 10px;
  background: linear-gradient(135deg, rgba(229, 240, 255, 0.9), rgba(214, 232, 255, 0.7), rgba(207, 236, 255, 0.9));
}
.welcome-title {
  font-size: 18px;
  font-weight: 700;
  color: #222;
  line-height: 1.3;
}
.welcome-user {
  margin-right: 6px;
}
.welcome-time {
  margin-top: 10px;
  font-size: 16px;
  color: rgba(0, 0, 0, 0.55);
}
.user-card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 18px 22px;
}
.user-card-main {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}
.user-name {
  font-size: 16px;
  font-weight: bold;
  color: #111;
  letter-spacing: 1px;
}
.user-role {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 20px;
  padding: 5px 10px;
  background: rgba(245, 246, 248, 1);
  color: #333;
  width: fit-content;
  font-weight: 600;
}
.user-meta {
  font-size: 12px;
  color: rgba(0, 0, 0, 0.55);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-meta .sep {
  margin: 0 10px;
  color: rgba(0, 0, 0, 0.25);
}
.avatar {
  width: 90px;
  height: 90px;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
}
.data-cards {
    display: flex;
    gap: 16px;
    justify-content: flex-start;
    background: #ffffff;
    border-radius: 12px;
    padding: 20px;
  width: 50%;
  display: flex;
  gap: 16px;
  justify-content: flex-start;
  background: #ffffff;
  border-radius: 12px;
  padding: 20px;
}
.data-title {
    font-weight: 700;
    font-size: 26px;
    color: #FFFFFF;
  font-weight: 700;
  font-size: 26px;
  color: #FFFFFF;
}
.data-num {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 20px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-top: 20px;
}
.data-card {
    background: #fff;
    border-radius: 12px;
    padding: 14px 10px 10px 10px;
    min-width: 160px;
    box-shadow: 0 2px 8px #eee;
    display: flex;
    flex-direction: column;
    width: 32%;
    height: 140px;
  background: #fff;
  border-radius: 12px;
  padding: 14px 10px 10px 10px;
  min-width: 160px;
  box-shadow: 0 2px 8px #eee;
  display: flex;
  flex-direction: column;
  width: 32%;
  height: 140px;
}
.data-card.sales {
    background-image: url("../assets/images/xioashoushuju.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  background-image: url("../assets/images/xioashoushuju.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.data-card.purchase {
    background-image: url("../assets/images/caigou.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  background-image: url("../assets/images/caigou.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.data-card.inventory {
    background-image: url("../assets/images/kucun.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  background-image: url("../assets/images/kucun.png");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.data-desc {
    font-weight: 500;
    font-size: 13px;
    color: #FFFFFF;
  font-weight: 500;
  font-size: 13px;
  color: #FFFFFF;
}
.data-value {
    font-size: 18px;
    font-weight: 500;
    margin: 10px 0;
    color: #FFFFFF;
  font-size: 18px;
  font-weight: 500;
  margin: 10px 0;
  color: #FFFFFF;
}
.top-left {
    display: flex;
    flex-direction: column;
    gap: 20px;
    width: 50%;
  display: flex;
  flex-direction: column;
  gap: 20px;
  height: 180px;
  width: 20%;
}
.todo-panel {
    background: #fff;
    border-radius: 12px;
    padding: 20px;
    width: 50%;
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  height: 180px;
  width: 30%;
}
.todo-list {
    list-style: none;
    padding: 0;
    margin: 0;
    font-size: 15px;
    overflow-y: auto;
    height: 260px;
  height: 100px;
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: 15px;
  overflow-y: auto;
}
.todo-list li {
    border-radius: 8px;
    margin-bottom: 12px;
    padding: 8px 20px;
    height: 74px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: rgba(225,227,250,0.62);
  border-radius: 8px;
  margin-bottom: 12px;
  padding: 8px 20px;
  height: 74px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background: rgba(225, 227, 250, 0.62);
}
.todo-title {
    font-weight: 400;
    font-size: 12px;
    color: #000000;
    position: relative;
  font-weight: 400;
  font-size: 12px;
  color: #000000;
  position: relative;
}
.todo-title::before {
    content: ''; /* å¿…需,表示这里有一个内容 */
    position: absolute;
    left: -10px; /* å®šä½åˆ°å·¦ä¾§ */
    top: 50%; /* åž‚直居中 */
    transform: translateY(-50%); /* å¾®è°ƒåž‚直居中 */
    width: 6px; /* åœ†çš„直径 */
    height: 6px; /* åœ†çš„直径 */
    background: #498CEB;
    border-radius: 50%; /* è®©å…¶å˜æˆåœ†å½¢ */
  content: '';
  /* å¿…需,表示这里有一个内容 */
  position: absolute;
  left: -10px;
  /* å®šä½åˆ°å·¦ä¾§ */
  top: 50%;
  /* åž‚直居中 */
  transform: translateY(-50%);
  /* å¾®è°ƒåž‚直居中 */
  width: 6px;
  /* åœ†çš„直径 */
  height: 6px;
  /* åœ†çš„直径 */
  background: #498CEB;
  border-radius: 50%;
  /* è®©å…¶å˜æˆåœ†å½¢ */
}
.todo-division {
    font-weight: 400;
    font-size: 12px;
    color: #000000;
  font-weight: 400;
  font-size: 12px;
  color: #000000;
}
.todo-time {
    font-weight: 400;
    font-size: 12px;
    color: #000000;
  font-weight: 400;
  font-size: 12px;
  color: #000000;
}
.todo-meta {
    color: #888;
    font-size: 13px;
  color: #888;
  font-size: 13px;
}
.dashboard-row {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}
.main-panel {
    background: #fff;
    border-radius: 12px;
    padding: 20px;
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
}
.section-title {
    position: relative;
    font-size: 18px;
    color: #333;
    padding-left: 10px;
    margin-bottom: 10px;
    font-weight: 700;
  position: relative;
  font-size: 18px;
  color: #333;
  padding-left: 10px;
  margin-bottom: 10px;
  font-weight: 700;
}
.section-title::before {
    position: absolute;
    left: 0;
    top: 4px;
    content: '';
    width: 4px;
    height: 18px;
    background-color: #002FA7;
    border-radius: 2px;
  position: absolute;
  left: 0;
  top: 4px;
  content: '';
  width: 4px;
  height: 18px;
  background-color: #002FA7;
  border-radius: 2px;
}
.contract-info {
    display: flex;
    align-items: center;
    gap: 20px;
    height: 90px;
    background: rgba(245,245,245,0.59);
    width: 100%;
    border-radius: 10px;
    padding: 10px 30px;
  display: flex;
  align-items: center;
  gap: 20px;
  height: 90px;
  background: rgba(245, 245, 245, 0.59);
  width: 100%;
  border-radius: 10px;
  padding: 10px 30px;
}
.contract-summary {
    display: flex;
    align-items: center;
    gap: 30px;
  display: flex;
  align-items: center;
  gap: 30px;
}
.contract-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.contract-name {
    font-weight: 400;
    font-size: 14px;
    color: #050505;
  font-weight: 400;
  font-size: 14px;
  color: #050505;
}
.contract-meta {
    display: flex;
    align-items: center;
    width: 100%;
    gap: 80px;
  display: flex;
  align-items: center;
  width: 100%;
  gap: 80px;
}
.main-amount {
    font-size: 24px;
    color: rgba(51,50,50,0.85);
  font-size: 24px;
  color: rgba(51, 50, 50, 0.85);
}
.up { color: #e57373; }
.up {
  color: #e57373;
}
.contract-list {
    margin-top: 16px;
    font-size: 14px;
    color: #666;
    list-style: none;
    padding: 0;
    height: 190px;
    overflow-y: auto;
    width: 460px;
  margin-top: 16px;
  font-size: 14px;
  color: #666;
  list-style: none;
  padding: 0;
  height: 190px;
  overflow-y: auto;
  width: 460px;
}
.line {
    position: relative;
    width: 230px;
  position: relative;
  width: 230px;
}
.line::after {
    content: '';
    position: absolute;
    right: 2px;
    top: 0;
    bottom: 0;
    width: 1px;
    background-color: #C9C5C5;
    border-radius: 2px;
  content: '';
  position: absolute;
  right: 2px;
  top: 0;
  bottom: 0;
  width: 1px;
  background-color: #C9C5C5;
  border-radius: 2px;
}
.contract-list li {
    margin-top: 10px;
  margin-top: 10px;
}
.quality-cards {
    display: flex;
    gap: 12px;
    margin-bottom: 12px;
  display: flex;
  gap: 12px;
  margin-bottom: 12px;
}
.quality-card {
    border-radius: 8px;
    padding: 15px 10px 10px 50px;
    font-weight: 400;
    font-size: 12px;
    color: rgba(0,0,0,0.67);
    width: 236px;
    height: 49px;
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
  border-radius: 8px;
  padding: 15px 10px 10px 50px;
  font-weight: 400;
  font-size: 12px;
  color: rgba(0, 0, 0, 0.67);
  width: 236px;
  height: 49px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}
.quality-card.one {
    background-image: url("../assets/images/yuancailiao.png");
  background-image: url("../assets/images/yuancailiao.png");
}
.quality-card.two {
    background-image: url("../assets/images/guocheng.png");
  background-image: url("../assets/images/guocheng.png");
}
.quality-card.three {
    background-image: url("../assets/images/chuchang.png");
  background-image: url("../assets/images/chuchang.png");
}
.quality-card span {
    color: #4fc3f7;
    font-weight: bold;
    margin-left: 6px;
  color: #4fc3f7;
  font-weight: bold;
  margin-left: 6px;
}
.chart {
    width: 100%;
    height: 220px;
    margin-top: 10px;
  width: 100%;
  height: 220px;
  margin-top: 10px;
}
.process-panel {
  padding-bottom: 10px;
}
.process-panel__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.process-panel__body {
  display: flex;
  gap: 24px;
  align-items: stretch;
  margin-top: 10px;
}
.process-panel__chart {
  flex: 1;
  min-width: 0;
  padding: 6px 0;
}
.process-panel__aside {
  width: 260px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.process-legend {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-start;
  padding: 8px 6px;
}
.process-legend__item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: rgba(0, 0, 0, 0.55);
}
.dot {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
}
.dot-blue {
  background: #1E5BFF;
}
.dot-yellow {
  background: #F7B500;
}
.dot-teal {
  background: #19C6C6;
}
.process-card {
  background: rgba(245, 247, 250, 0.9);
  border-radius: 10px;
  padding: 16px 16px;
}
.process-card--name {
  background: rgba(235, 242, 255, 1);
  color: #1E5BFF;
  font-weight: 800;
  font-size: 14px;
}
.process-card__label {
  font-size: 13px;
  color: rgba(0, 0, 0, 0.55);
  margin-bottom: 10px;
}
.process-card__value {
  font-size: 24px;
  font-weight: 800;
  color: rgba(0, 0, 0, 0.8);
}
.process-card__value .unit {
  font-size: 12px;
  font-weight: 600;
  color: rgba(0, 0, 0, 0.45);
  margin-left: 6px;
}
@media (max-width: 1200px) {
  .process-panel__body {
    flex-direction: column;
  }
  .process-panel__aside {
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
  }
  .process-card {
    flex: 1;
    min-width: 220px;
  }
}
.process-selection-wrapper {
  max-height: 400px;
  overflow-y: auto;
  padding: 10px;
}
.process-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 12px;
}
:deep(.el-checkbox.is-bordered) {
  margin-left: 0 !important;
  width: 100%;
}
</style>
src/views/inventoryManagement/dispatchLog/Record.vue
@@ -112,9 +112,7 @@
    delStockOut,
} from "@/api/inventoryManagement/stockOut.js";
import {
  findAllQualifiedStockRecordTypeOptions,
  findAllStockRecordTypeOptions,
  findAllUnqualifiedStockRecordTypeOptions
  findAllQualifiedStockOutRecordTypeOptions, findAllUnQualifiedStockOutRecordTypeOptions,
} from "@/api/basicData/enum.js";
const userStore = useUserStore();
@@ -186,13 +184,13 @@
// èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
const fetchStockRecordTypeOptions = () => {
  if (props.type === '0') {
    findAllQualifiedStockRecordTypeOptions()
    findAllQualifiedStockOutRecordTypeOptions()
        .then(res => {
          stockRecordTypeOptions.value = res.data;
        })
    return
  }
  findAllUnqualifiedStockRecordTypeOptions()
  findAllUnQualifiedStockOutRecordTypeOptions()
      .then(res => {
        stockRecordTypeOptions.value = res.data;
      })
src/views/inventoryManagement/receiptManagement/Record.vue
@@ -109,8 +109,7 @@
  batchDeleteStockInRecords,
} from "@/api/inventoryManagement/stockInRecord.js";
import {
  findAllQualifiedStockRecordTypeOptions,
  findAllUnqualifiedStockRecordTypeOptions
  findAllQualifiedStockInRecordTypeOptions, findAllUnQualifiedStockInRecordTypeOptions,
} from "@/api/basicData/enum.js";
const {proxy} = getCurrentInstance();
@@ -130,7 +129,7 @@
const stockRecordTypeOptions = ref([]);
const page = reactive({
  current: 1,
  size: 100,
  size: 10,
});
const total = ref(0);
@@ -168,6 +167,7 @@
  getStockInRecordListPage(params)
      .then(res => {
        tableData.value = res.data.records;
        total.value = res.data.total || 0;
      }).finally(() => {
    tableLoading.value = false;
  })
@@ -176,13 +176,13 @@
// èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
const fetchStockRecordTypeOptions = () => {
  if (props.type === '0') {
    findAllQualifiedStockRecordTypeOptions()
    findAllQualifiedStockInRecordTypeOptions()
        .then(res => {
          stockRecordTypeOptions.value = res.data;
        })
    return
  }
  findAllUnqualifiedStockRecordTypeOptions()
  findAllUnQualifiedStockInRecordTypeOptions()
      .then(res => {
        stockRecordTypeOptions.value = res.data;
      })
src/views/inventoryManagement/stockReport/index.vue
@@ -48,7 +48,7 @@
          style="width: 240px;"
        />
        
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
        <el-button type="primary" @click="onSearch" style="margin-left: 10px">
          æŸ¥è¯¢
        </el-button>
        <el-button @click="handleReset">重置</el-button>
@@ -230,24 +230,30 @@
             show-overflow-tooltip
           />
        </el-table>
        <pagination
          :total="total"
          layout="total, sizes, prev, pager, next, jumper"
          :page="page.current"
          :limit="page.size"
          @pagination="paginationChange"
        />
      </el-card>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import {
  getStockMonthlyReport,
  getStockInOutReport,
} from '@/api/inventoryManagement/stockReport'
import pagination from '@/components/PIMTable/Pagination.vue'
import {
  getStockInventoryInAndOutReportList,
  getStockInventoryReportList
} from "@/api/inventoryManagement/stockInventory.js";
import {findAllQualifiedStockRecordTypeOptions} from "@/api/basicData/enum.js";
import {
  findAllQualifiedStockInRecordTypeOptions,findAllUnQualifiedStockInRecordTypeOptions,
} from "@/api/basicData/enum.js";
const { proxy } = getCurrentInstance()
@@ -269,6 +275,13 @@
  tableData: []
})
const page = reactive({
  current: 1,
  size: 10,
})
const total = ref(0)
const stockRecordTypeOptions = ref([])
const getRecordType = (recordType) => {
@@ -277,9 +290,13 @@
// èŽ·å–æ¥æºç±»åž‹é€‰é¡¹
const fetchStockRecordTypeOptions = () => {
  findAllQualifiedStockRecordTypeOptions()
  findAllQualifiedStockInRecordTypeOptions()
      .then(res => {
        stockRecordTypeOptions.value = res.data;
        findAllUnQualifiedStockInRecordTypeOptions()
          .then(res => {
          stockRecordTypeOptions.value = [...stockRecordTypeOptions.value,...res.data];
      })
      })
}
@@ -295,6 +312,7 @@
// æŠ¥è¡¨ç±»åž‹æ”¹å˜
const handleReportTypeChange = () => {
  page.current = 1
  reportData.value = {
    summary: null,
    chartData: null,
@@ -310,7 +328,12 @@
  
  tableLoading.value = true
  try {
    const params = getQueryParams()
    const baseParams = getQueryParams()
    const params = {
      ...baseParams,
      current: page.current,
      size: page.size,
    }
    let response
    if (searchForm.reportType === 'inout') {
@@ -319,7 +342,8 @@
      response = await getStockInventoryReportList(params)
    }
    if (response.code === 200) {
      reportData.value.tableData = response.data.records
      reportData.value.tableData = response.data.records || []
      total.value = response.data.total || 0
      // reportData.value.summary = response.data.summary
      // reportData.value.chartData = response.data.chartData
      // nextTick(() => {
@@ -332,6 +356,19 @@
  } finally {
    tableLoading.value = false
  }
}
// æŸ¥è¯¢æŒ‰é’®ï¼šé‡ç½®åˆ°ç¬¬ä¸€é¡µå¹¶æŸ¥è¯¢
const onSearch = () => {
  page.current = 1
  handleQuery()
}
// åˆ†é¡µå˜åŒ–
const paginationChange = (obj) => {
  page.current = obj.page
  page.size = obj.limit
  handleQuery()
}
// // ç”Ÿæˆå‡æ•°æ®
// const generateMockData = () => {
@@ -552,6 +589,8 @@
  ]
  fetchStockRecordTypeOptions()
  // åˆå§‹åŒ–加载一次数据
  handleQuery()
})
</script>
src/views/inventoryManagement/transportTaskManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,692 @@
<template>
  <div class="app-container">
    <!-- ç»Ÿè®¡æ¦‚览 -->
    <el-row :gutter="16" style="margin-bottom: 16px">
      <el-col :span="6">
        <el-card shadow="never">
          <div>总任务数</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ totalTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>进行中任务</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ runningTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>已完成任务</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ finishedTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>完成率</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ completionRate }}%
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">任务编号:</span>
        <el-input
          v-model="searchForm.taskNo"
          style="width: 200px"
          placeholder="请输入任务编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">车辆编号:</span>
        <el-input
          v-model="searchForm.vehicleCode"
          style="width: 200px"
          placeholder="请输入车辆编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">任务日期:</span>
        <el-date-picker
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
          @change="handleQuery"
        />
        <span class="search_title ml10">状态:</span>
        <el-select
          v-model="searchForm.status"
          style="width: 140px"
          placeholder="请选择任务状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">
          æ–°å»ºè¿è¾“任务
        </el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 22em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="tableRowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="taskNo"
          label="任务编号"
          width="150"
          show-overflow-tooltip
        />
        <el-table-column
          prop="outboundOrderNo"
          label="出库订单号"
          width="180"
          show-overflow-tooltip
        />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="130"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="driverName"
          label="司机"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          prop="loadAddress"
          label="装货地点"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="deliveryAddress"
          label="送货地点"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="loadTime"
          label="装货时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="deliveryTime"
          label="送货时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="signTime"
          label="签收时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column label="状态" width="110" align="center">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="进度" width="150" align="center">
          <template #default="scope">
            <el-progress
              :percentage="scope.row.progress"
              :status="scope.row.status === '已完成' ? 'success' : undefined"
              :stroke-width="12"
              :show-text="false"
            />
            <div style="font-size: 12px; margin-top: 4px">
              {{ scope.row.progress }}%
            </div>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="160" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="780px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="任务编号:" prop="taskNo">
              <el-input
                v-model="form.taskNo"
                placeholder="请输入任务编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="出库订单号:" prop="outboundOrderNo">
              <el-input
                v-model="form.outboundOrderNo"
                placeholder="请输入关联出库订单号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input
                v-model="form.vehicleCode"
                placeholder="请输入车辆编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
              <el-input
                v-model="form.plateNumber"
                placeholder="请输入车牌号码"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="司机:" prop="driverName">
              <el-input
                v-model="form.driverName"
                placeholder="请输入司机姓名"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="司机电话:" prop="driverPhone">
              <el-input
                v-model="form.driverPhone"
                placeholder="请输入司机联系电话"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="装货地点:" prop="loadAddress">
              <el-input
                v-model="form.loadAddress"
                placeholder="请输入装货地点"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="送货地点:" prop="deliveryAddress">
              <el-input
                v-model="form.deliveryAddress"
                placeholder="请输入送货地点"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="装货时间:" prop="loadTime">
              <el-date-picker
                v-model="form.loadTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择装货时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="送货时间:" prop="deliveryTime">
              <el-date-picker
                v-model="form.deliveryTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择送货时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="签收时间:" prop="signTime">
              <el-date-picker
                v-model="form.signTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择签收时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="状态:" prop="status">
              <el-select v-model="form.status" placeholder="请选择任务状态">
                <el-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="计划日期:" prop="planDate">
              <el-date-picker
                v-model="form.planDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择计划日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹Ÿè¿è¾“任务数据
const rawTasks = ref([
  {
    id: 1,
    taskNo: "T2024-1201-001",
    outboundOrderNo: "OUT-2024-1201-1001",
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    driverName: "张师傅",
    driverPhone: "13800000001",
    loadAddress: "深圳仓库A区",
    deliveryAddress: "广州客户一部",
    planDate: "2024-12-01",
    loadTime: "2024-12-01 09:00:00",
    deliveryTime: "2024-12-01 14:30:00",
    signTime: "2024-12-01 15:00:00",
    status: "已完成",
  },
  {
    id: 2,
    taskNo: "T2024-1201-002",
    outboundOrderNo: "OUT-2024-1201-1002",
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    driverName: "李师傅",
    driverPhone: "13800000002",
    loadAddress: "深圳仓库B区",
    deliveryAddress: "东莞客户二部",
    planDate: "2024-12-01",
    loadTime: "2024-12-01 10:00:00",
    deliveryTime: "2024-12-01 13:00:00",
    signTime: "",
    status: "运输中",
  },
  {
    id: 3,
    taskNo: "T2024-1202-001",
    outboundOrderNo: "OUT-2024-1202-1003",
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    driverName: "张师傅",
    driverPhone: "13800000001",
    loadAddress: "深圳仓库A区",
    deliveryAddress: "佛山客户三部",
    planDate: "2024-12-02",
    loadTime: "2024-12-02 08:30:00",
    deliveryTime: "",
    signTime: "",
    status: "待发车",
  },
  {
    id: 4,
    taskNo: "T2024-1203-001",
    outboundOrderNo: "OUT-2024-1203-1004",
    vehicleCode: "CL-202403",
    plateNumber: "粤C11223",
    driverName: "王师傅",
    driverPhone: "13800000003",
    loadAddress: "深圳仓库C区",
    deliveryAddress: "惠州客户四部",
    planDate: "2024-12-03",
    loadTime: "",
    deliveryTime: "",
    signTime: "",
    status: "未开始",
  },
]);
// çŠ¶æ€æžšä¸¾
const statusOptions = [
  { label: "未开始", value: "未开始" },
  { label: "待发车", value: "待发车" },
  { label: "运输中", value: "运输中" },
  { label: "待签收", value: "待签收" },
  { label: "已完成", value: "已完成" },
];
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  taskNo: "",
  vehicleCode: "",
  dateRange: [],
  status: "",
});
// è¡¨æ ¼æ•°æ®ï¼ˆå¸¦è¿›åº¦ç­‰è®¡ç®—字段)
const tableData = ref([]);
// ç»Ÿè®¡
const totalTasks = computed(() => rawTasks.value.length);
const finishedTasks = computed(
  () => rawTasks.value.filter((t) => t.status === "已完成").length
);
const runningTasks = computed(
  () =>
    rawTasks.value.filter((t) =>
      ["待发车", "运输中", "待签收"].includes(t.status)
    ).length
);
const completionRate = computed(() => {
  if (!totalTasks.value) return 0;
  return Math.round((finishedTasks.value / totalTasks.value) * 100);
});
// è®¡ç®—单条任务进度
const computeProgress = (task) => {
  if (task.status === "已完成" || task.signTime) return 100;
  if (task.status === "待签收" || task.deliveryTime) return 80;
  if (task.status === "运输中") return 60;
  if (task.status === "待发车" || task.loadTime) return 30;
  if (task.status === "未开始") return 0;
  return 0;
};
// çŠ¶æ€ tag æ ·å¼
const statusTagType = (status) => {
  if (status === "已完成") return "success";
  if (status === "运输中") return "warning";
  if (status === "待签收" || status === "待发车") return "info";
  return "default";
};
// é‡ç®—表格数据
const recomputeTable = () => {
  const filtered = rawTasks.value
    .filter((t) => {
      if (searchForm.taskNo && !t.taskNo.includes(searchForm.taskNo.trim())) {
        return false;
      }
      if (
        searchForm.vehicleCode &&
        !t.vehicleCode.includes(searchForm.vehicleCode.trim())
      ) {
        return false;
      }
      if (searchForm.status && t.status !== searchForm.status) {
        return false;
      }
      if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
        const [start, end] = searchForm.dateRange;
        if (!t.planDate || t.planDate < start || t.planDate > end) {
          return false;
        }
      }
      return true;
    })
    .map((t) => ({
      ...t,
      progress: computeProgress(t),
    }));
  tableData.value = filtered;
};
// æŸ¥è¯¢
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.taskNo = "";
  searchForm.vehicleCode = "";
  searchForm.dateRange = [];
  searchForm.status = "";
  recomputeTable();
};
// è¡Œæ ·å¼
const tableRowClassName = ({ row }) => {
  if (row.status === "已完成") {
    return "row-finished";
  }
  if (row.status === "运输中") {
    return "row-running";
  }
  return "";
};
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新建运输任务");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  taskNo: "",
  outboundOrderNo: "",
  vehicleCode: "",
  plateNumber: "",
  driverName: "",
  driverPhone: "",
  loadAddress: "",
  deliveryAddress: "",
  planDate: "",
  loadTime: "",
  deliveryTime: "",
  signTime: "",
  status: "未开始",
});
const rules = {
  taskNo: [{ required: true, message: "请输入任务编号", trigger: "blur" }],
  outboundOrderNo: [
    { required: true, message: "请输入出库订单号", trigger: "blur" },
  ],
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  driverName: [{ required: true, message: "请输入司机姓名", trigger: "blur" }],
  loadAddress: [{ required: true, message: "请输入装货地点", trigger: "blur" }],
  deliveryAddress: [
    { required: true, message: "请输入送货地点", trigger: "blur" },
  ],
  planDate: [{ required: true, message: "请选择计划日期", trigger: "change" }],
};
// æ–°å»º
const openAdd = () => {
  dialogTitle.value = "新建运输任务";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    taskNo: "",
    outboundOrderNo: "",
    vehicleCode: "",
    plateNumber: "",
    driverName: "",
    driverPhone: "",
    loadAddress: "",
    deliveryAddress: "",
    planDate: "",
    loadTime: "",
    deliveryTime: "",
    signTime: "",
    status: "未开始",
  });
  dialogVisible.value = true;
};
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑运输任务";
  isEdit.value = true;
  Object.assign(form, row);
  dialogVisible.value = true;
};
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (isEdit.value) {
      const index = rawTasks.value.findIndex((t) => t.id === form.id);
      if (index !== -1) {
        rawTasks.value[index] = { ...form };
      }
      ElMessage.success("运输任务已更新");
    } else {
      const newId = rawTasks.value.length
        ? Math.max(...rawTasks.value.map((t) => t.id)) + 1
        : 1;
      rawTasks.value.push({ ...form, id: newId });
      ElMessage.success("运输任务已新增");
    }
    dialogVisible.value = false;
    recomputeTable();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该运输任务?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      rawTasks.value = rawTasks.value.filter((t) => t.id !== row.id);
      recomputeTable();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
onMounted(() => {
  recomputeTable();
});
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
::v-deep(.row-finished) {
  background-color: #f6ffed;
}
::v-deep(.row-running) {
  background-color: #fffbe6;
}
</style>
src/views/inventoryManagement/vehicleFuelManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,556 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">车辆编号:</span>
        <el-input
          v-model="searchForm.vehicleCode"
          style="width: 200px"
          placeholder="请输入车辆编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">加油时间:</span>
        <el-date-picker
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
          @change="handleQuery"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">
          æ–°å¢žåŠ æ²¹è®°å½•
        </el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="tableRowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="130"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="fuelDate"
          label="加油日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="gunNo"
          label="油枪号"
          width="90"
          align="center"
        />
        <el-table-column
          prop="amount"
          label="金额(元)"
          width="100"
          align="right"
        >
          <template #default="scope">
            <span>{{ scope.row.amount?.toFixed(2) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          prop="liters"
          label="升数(L)"
          width="90"
          align="right"
        >
          <template #default="scope">
            <span>{{ scope.row.liters?.toFixed(2) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          prop="startMileage"
          label="起始里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="endMileage"
          label="结束里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="distance"
          label="行驶里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="fuelConsumption"
          label="油耗(L/100km)"
          width="130"
          align="center"
        >
          <template #default="scope">
            <span
              :style="scope.row.isAbnormal ? 'color:#F56C6C;font-weight:600;' : ''"
            >
              {{ scope.row.fuelConsumption != null ? scope.row.fuelConsumption.toFixed(2) : '-' }}
            </span>
          </template>
        </el-table-column>
        <el-table-column
          prop="avgConsumption"
          label="车辆平均油耗"
          width="130"
          align="center"
        >
          <template #default="scope">
            <span>
              {{ scope.row.avgConsumption != null ? scope.row.avgConsumption.toFixed(2) : '-' }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="异常预警" width="100" align="center">
          <template #default="scope">
            <el-tag v-if="scope.row.isAbnormal" type="danger" size="small">
              å¼‚常
            </el-tag>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="140" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="640px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input
                v-model="form.vehicleCode"
                placeholder="请输入车辆编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
              <el-input
                v-model="form.plateNumber"
                placeholder="请输入车牌号码"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="加油日期:" prop="fuelDate">
              <el-date-picker
                v-model="form.fuelDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择加油日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="油枪号:" prop="gunNo">
              <el-input
                v-model="form.gunNo"
                placeholder="请输入油枪号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="金额(元):" prop="amount">
              <el-input-number
                v-model="form.amount"
                :min="0"
                :step="0.01"
                :precision="2"
                placeholder="请输入金额"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="升数(L):" prop="liters">
              <el-input-number
                v-model="form.liters"
                :min="0"
                :step="0.01"
                :precision="2"
                placeholder="请输入升数"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="起始里程(km):" prop="startMileage">
              <el-input-number
                v-model="form.startMileage"
                :min="0"
                :step="1"
                placeholder="请输入起始里程"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束里程(km):" prop="endMileage">
              <el-input-number
                v-model="form.endMileage"
                :min="0"
                :step="1"
                placeholder="请输入结束里程"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹ŸåŠ æ²¹è®°å½•æ•°æ®
const rawRecords = ref([
  {
    id: 1,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2024-12-01",
    gunNo: "01",
    amount: 500,
    liters: 70,
    startMileage: 12000,
    endMileage: 12600,
  },
  {
    id: 2,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2024-12-15",
    gunNo: "02",
    amount: 520,
    liters: 72,
    startMileage: 12600,
    endMileage: 13250,
  },
  {
    id: 3,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    fuelDate: "2024-12-05",
    gunNo: "03",
    amount: 430,
    liters: 60,
    startMileage: 8000,
    endMileage: 8520,
  },
  {
    id: 4,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    fuelDate: "2024-12-20",
    gunNo: "01",
    amount: 450,
    liters: 63,
    startMileage: 8520,
    endMileage: 9000,
  },
  {
    id: 5,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2025-01-05",
    gunNo: "01",
    amount: 700,
    liters: 90,
    startMileage: 13250,
    endMileage: 13600, // æ˜Žæ˜¾å¼‚常油耗
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  vehicleCode: "",
  dateRange: [],
});
// è¡¨æ ¼æ•°æ®ï¼ˆåŒ…含计算字段)
const tableData = ref([]);
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新增加油记录");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  vehicleCode: "",
  plateNumber: "",
  fuelDate: "",
  gunNo: "",
  amount: null,
  liters: null,
  startMileage: null,
  endMileage: null,
});
const rules = {
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  fuelDate: [{ required: true, message: "请选择加油日期", trigger: "change" }],
  gunNo: [{ required: true, message: "请输入油枪号", trigger: "blur" }],
  amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
  liters: [{ required: true, message: "请输入升数", trigger: "blur" }],
  startMileage: [{ required: true, message: "请输入起始里程", trigger: "blur" }],
  endMileage: [{ required: true, message: "请输入结束里程", trigger: "blur" }],
};
// é‡æ–°è®¡ç®—油耗、平均油耗和异常预警
const recomputeTable = () => {
  const records = rawRecords.value;
  // 1. å…ˆæŒ‰è½¦è¾†ç»Ÿè®¡å¹³å‡æ²¹è€—
  const stats = {};
  records.forEach((r) => {
    const distance = r.endMileage - r.startMileage;
    if (distance <= 0 || !r.liters) return;
    const cons = (r.liters / distance) * 100; // L/100km
    if (!stats[r.vehicleCode]) {
      stats[r.vehicleCode] = { totalCons: 0, count: 0 };
    }
    stats[r.vehicleCode].totalCons += cons;
    stats[r.vehicleCode].count += 1;
  });
  const avgMap = {};
  Object.keys(stats).forEach((key) => {
    avgMap[key] = stats[key].totalCons / stats[key].count;
  });
  // 2. æŒ‰ç­›é€‰æ¡ä»¶è¿‡æ»¤å¹¶è¡¥å……计算字段
  const filtered = records
    .filter((r) => {
      if (
        searchForm.vehicleCode &&
        !r.vehicleCode.includes(searchForm.vehicleCode.trim())
      ) {
        return false;
      }
      if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
        const [start, end] = searchForm.dateRange;
        if (r.fuelDate < start || r.fuelDate > end) {
          return false;
        }
      }
      return true;
    })
    .map((r) => {
      const distance = r.endMileage - r.startMileage;
      const fuelConsumption =
        distance > 0 && r.liters
          ? (r.liters / distance) * 100
          : null;
      const avgConsumption =
        avgMap[r.vehicleCode] != null ? avgMap[r.vehicleCode] : null;
      const isAbnormal =
        avgConsumption != null &&
        fuelConsumption != null &&
        fuelConsumption > avgConsumption * 1.2;
      return {
        ...r,
        distance,
        fuelConsumption,
        avgConsumption,
        isAbnormal,
      };
    });
  tableData.value = filtered;
};
// æŸ¥è¯¢
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.vehicleCode = "";
  searchForm.dateRange = [];
  recomputeTable();
};
// è¡Œæ ·å¼ï¼ˆå¼‚常高亮)
const tableRowClassName = ({ row }) => {
  if (row.isAbnormal) {
    return "row-abnormal";
  }
  return "";
};
// æ–°å¢ž
const openAdd = () => {
  dialogTitle.value = "新增加油记录";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    vehicleCode: "",
    plateNumber: "",
    fuelDate: "",
    gunNo: "",
    amount: null,
    liters: null,
    startMileage: null,
    endMileage: null,
  });
  dialogVisible.value = true;
};
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑加油记录";
  isEdit.value = true;
  Object.assign(form, row);
  dialogVisible.value = true;
};
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (form.endMileage <= form.startMileage) {
      ElMessage.warning("结束里程必须大于起始里程");
      return;
    }
    if (isEdit.value) {
      const index = rawRecords.value.findIndex((r) => r.id === form.id);
      if (index !== -1) {
        rawRecords.value[index] = { ...form };
      }
      ElMessage.success("加油记录已更新");
    } else {
      const newId = rawRecords.value.length
        ? Math.max(...rawRecords.value.map((r) => r.id)) + 1
        : 1;
      rawRecords.value.push({ ...form, id: newId });
      ElMessage.success("加油记录已新增");
    }
    dialogVisible.value = false;
    recomputeTable();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该加油记录?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      rawRecords.value = rawRecords.value.filter((r) => r.id !== row.id);
      recomputeTable();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
onMounted(() => {
  recomputeTable();
});
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
::v-deep(.row-abnormal) {
  background-color: #fff5f5;
}
</style>
src/views/inventoryManagement/vehicleManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,581 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">车牌号码:</span>
        <el-input
          v-model="searchForm.plateNumber"
          style="width: 180px"
          placeholder="请输入车牌号码"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">车辆类型:</span>
        <el-select
          v-model="searchForm.vehicleType"
          style="width: 160px"
          placeholder="请选择车辆类型"
          clearable
        >
          <el-option
            v-for="item in vehicleTypeOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">所属部门:</span>
        <el-select
          v-model="searchForm.department"
          style="width: 160px"
          placeholder="请选择所属部门"
          clearable
        >
          <el-option
            v-for="item in departmentOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">状态:</span>
        <el-select
          v-model="searchForm.status"
          style="width: 140px"
          placeholder="请选择状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">归档状态:</span>
        <el-select
          v-model="searchForm.archived"
          style="width: 140px"
          placeholder="请选择归档状态"
          clearable
        >
          <el-option label="未归档" value="false" />
          <el-option label="已归档" value="true" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">新增车辆</el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="140"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="vehicleType"
          label="车辆类型"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="department"
          label="所属部门"
          width="140"
          show-overflow-tooltip
        />
        <el-table-column
          prop="purchaseDate"
          label="购置日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseNumber"
          label="行驶证编号"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseIssueDate"
          label="发证日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseExpireDate"
          label="到期日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column label="状态" width="100" align="center">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="归档状态" width="100" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.archived ? 'info' : 'success'">
              {{ scope.row.archived ? '已归档' : '未归档' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="220" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="warning"
              link
              size="small"
              :disabled="scope.row.archived"
              @click="archiveRow(scope.row)"
            >
              å½’æ¡£
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="600px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input v-model="form.vehicleCode" placeholder="请输入车辆编号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
              <el-input v-model="form.plateNumber" placeholder="请输入车牌号码" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆类型:" prop="vehicleType">
              <el-select
                v-model="form.vehicleType"
                placeholder="请选择车辆类型"
                clearable
              >
                <el-option
                  v-for="item in vehicleTypeOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="所属部门:" prop="department">
              <el-select
                v-model="form.department"
                placeholder="请选择所属部门"
                clearable
              >
                <el-option
                  v-for="item in departmentOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="购置日期:" prop="purchaseDate">
              <el-date-picker
                v-model="form.purchaseDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择购置日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="状态:" prop="status">
              <el-select v-model="form.status" placeholder="请选择状态">
                <el-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="行驶证编号:" prop="licenseNumber">
              <el-input
                v-model="form.licenseNumber"
                placeholder="请输入行驶证编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="发证日期:" prop="licenseIssueDate">
              <el-date-picker
                v-model="form.licenseIssueDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择发证日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="到期日期:" prop="licenseExpireDate">
              <el-date-picker
                v-model="form.licenseExpireDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择到期日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹Ÿè½¦è¾†åŸºç¡€æ•°æ®
const allVehicles = ref([
  {
    id: 1,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    vehicleType: "厢式货车",
    department: "物流一部",
    purchaseDate: "2022-03-15",
    licenseNumber: "4401-2022-0001",
    licenseIssueDate: "2022-03-10",
    licenseExpireDate: "2026-03-10",
    status: "在用",
    archived: false,
  },
  {
    id: 2,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    vehicleType: "冷藏车",
    department: "物流二部",
    purchaseDate: "2021-08-01",
    licenseNumber: "4401-2021-0123",
    licenseIssueDate: "2021-07-28",
    licenseExpireDate: "2025-07-28",
    status: "ç»´ä¿®",
    archived: false,
  },
  {
    id: 3,
    vehicleCode: "CL-202403",
    plateNumber: "粤C11223",
    vehicleType: "牵引车",
    department: "项目运输部",
    purchaseDate: "2020-05-20",
    licenseNumber: "4401-2020-0456",
    licenseIssueDate: "2020-05-18",
    licenseExpireDate: "2024-05-18",
    status: "闲置",
    archived: false,
  },
  {
    id: 4,
    vehicleCode: "CL-202404",
    plateNumber: "粤D33445",
    vehicleType: "厢式货车",
    department: "资产管理部",
    purchaseDate: "2019-11-11",
    licenseNumber: "4401-2019-0789",
    licenseIssueDate: "2019-11-08",
    licenseExpireDate: "2023-11-08",
    status: "在用",
    archived: true,
  },
]);
// ä¸‹æ‹‰æžšä¸¾
const vehicleTypeOptions = [
  { label: "厢式货车", value: "厢式货车" },
  { label: "冷藏车", value: "冷藏车" },
  { label: "牵引车", value: "牵引车" },
  { label: "其他", value: "其他" },
];
const departmentOptions = [
  { label: "物流一部", value: "物流一部" },
  { label: "物流二部", value: "物流二部" },
  { label: "项目运输部", value: "项目运输部" },
  { label: "资产管理部", value: "资产管理部" },
];
const statusOptions = [
  { label: "在用", value: "在用" },
  { label: "闲置", value: "闲置" },
  { label: "ç»´ä¿®", value: "ç»´ä¿®" },
];
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  plateNumber: "",
  vehicleType: "",
  department: "",
  status: "",
  archived: "",
});
// è¡¨æ ¼æ•°æ®
const tableData = ref([...allVehicles.value]);
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新增车辆");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  vehicleCode: "",
  plateNumber: "",
  vehicleType: "",
  department: "",
  purchaseDate: "",
  licenseNumber: "",
  licenseIssueDate: "",
  licenseExpireDate: "",
  status: "在用",
  archived: false,
});
const rules = {
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  vehicleType: [{ required: true, message: "请选择车辆类型", trigger: "change" }],
  department: [{ required: true, message: "请选择所属部门", trigger: "change" }],
  purchaseDate: [{ required: true, message: "请选择购置日期", trigger: "change" }],
  status: [{ required: true, message: "请选择状态", trigger: "change" }],
  licenseNumber: [{ required: true, message: "请输入行驶证编号", trigger: "blur" }],
  licenseIssueDate: [{ required: true, message: "请选择发证日期", trigger: "change" }],
};
// æŸ¥è¯¢
const handleQuery = () => {
  tableData.value = allVehicles.value.filter((item) => {
    if (
      searchForm.plateNumber &&
      !item.plateNumber.includes(searchForm.plateNumber.trim())
    ) {
      return false;
    }
    if (searchForm.vehicleType && item.vehicleType !== searchForm.vehicleType) {
      return false;
    }
    if (searchForm.department && item.department !== searchForm.department) {
      return false;
    }
    if (searchForm.status && item.status !== searchForm.status) {
      return false;
    }
    if (searchForm.archived !== "") {
      const targetArchived = searchForm.archived === "true";
      if (item.archived !== targetArchived) return false;
    }
    return true;
  });
};
const resetSearch = () => {
  searchForm.plateNumber = "";
  searchForm.vehicleType = "";
  searchForm.department = "";
  searchForm.status = "";
  searchForm.archived = "";
  handleQuery();
};
// æ–°å¢ž
const openAdd = () => {
  dialogTitle.value = "新增车辆";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    vehicleCode: "",
    plateNumber: "",
    vehicleType: "",
    department: "",
    purchaseDate: "",
    licenseInfo: "",
    status: "在用",
    archived: false,
  });
  dialogVisible.value = true;
};
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑车辆";
  isEdit.value = true;
  Object.assign(form, row);
  dialogVisible.value = true;
};
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (isEdit.value) {
      const index = allVehicles.value.findIndex((v) => v.id === form.id);
      if (index !== -1) {
        allVehicles.value[index] = { ...form };
      }
      ElMessage.success("车辆信息已更新");
    } else {
      const newId = allVehicles.value.length
        ? Math.max(...allVehicles.value.map((v) => v.id)) + 1
        : 1;
      allVehicles.value.push({ ...form, id: newId });
      ElMessage.success("车辆信息已新增");
    }
    dialogVisible.value = false;
    handleQuery();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// å½’æ¡£
const archiveRow = (row) => {
  if (row.archived) return;
  ElMessageBox.confirm(
    "是否确认将该车辆归档?归档后仅保留查询,不再参与运输任务分配。",
    "归档提示",
    {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      row.archived = true;
      if (row.status === "在用") {
        row.status = "闲置";
      }
      ElMessage.success("车辆已归档");
      handleQuery();
    })
    .catch(() => {});
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该车辆基础信息?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      allVehicles.value = allVehicles.value.filter((v) => v.id !== row.id);
      handleQuery();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
// çŠ¶æ€æ ·å¼
const statusTagType = (status) => {
  if (status === "在用") return "success";
  if (status === "闲置") return "info";
  if (status === "ç»´ä¿®") return "warning";
  return "default";
};
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
</style>
src/views/personnelManagement/attendanceCheckin/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,469 @@
<template>
  <div class="app-container">
    <!-- å‘˜å·¥æ‰“卡区 -->
    <el-card shadow="never" class="mb16">
      <div class="attendance-header">
        <div>
          <div class="title">打卡签到</div>
          <div class="sub-title">支持一键打卡,自动记录上下班时间</div>
        </div>
        <div class="attendance-actions">
          <div class="time-block">
            <div class="label">当前时间</div>
            <div class="value">{{ nowTime }}</div>
          </div>
          <el-button type="primary" size="large" @click="handleCheckInOut">
            {{ checkInOutText }}
          </el-button>
        </div>
      </div>
      <el-descriptions border :column="4" class="mt10">
        <el-descriptions-item label="员工姓名">
          {{ currentUser.name }}
        </el-descriptions-item>
        <el-descriptions-item label="工号">
          {{ currentUser.no }}
        </el-descriptions-item>
        <el-descriptions-item label="所属部门">
          {{ currentUser.dept }}
        </el-descriptions-item>
        <el-descriptions-item label="今日状态">
          <el-tag :type="todayStatusTag" size="small">
            {{ todayStatusText }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="上班时间">
          {{ todayRecord?.checkInTime || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="下班时间">
          {{ todayRecord?.checkOutTime || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="工时(小时)">
          {{ todayRecord?.workHours ?? '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="异常标记">
          <span v-if="todayRecord?.status === 'normal'">-</span>
          <el-tag v-else type="danger" size="small">
            {{ todayRecord?.statusText }}
          </el-tag>
        </el-descriptions-item>
      </el-descriptions>
    </el-card>
    <!-- æŸ¥è¯¢æ¡ä»¶ï¼ˆç®¡ç†å‘˜è€ƒå‹¤æ—¥æŠ¥ï¼‰ -->
    <div class="search_form">
      <div>
        <span class="search_title">部门:</span>
        <el-select
          v-model="searchForm.dept"
          placeholder="请选择部门"
          style="width: 180px"
          clearable
        >
          <el-option
            v-for="item in deptOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">日期:</span>
        <el-date-picker
          v-model="searchForm.date"
          type="date"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          placeholder="请选择日期"
          clearable
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button icon="Download" @click="handleExport">
          å¯¼å‡ºè€ƒå‹¤æ—¥æŠ¥
        </el-button>
      </div>
    </div>
    <!-- è€ƒå‹¤æ—¥æŠ¥è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 24em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="rowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="date"
          label="日期"
          width="120"
        />
        <el-table-column
          prop="dept"
          label="部门"
          width="140"
        />
        <el-table-column
          prop="name"
          label="姓名"
          width="120"
        />
        <el-table-column
          prop="no"
          label="工号"
          width="120"
        />
        <el-table-column
          prop="checkInTime"
          label="上班时间"
          width="140"
        />
        <el-table-column
          prop="checkOutTime"
          label="下班时间"
          width="140"
        />
        <el-table-column
          prop="workHours"
          label="工时(小时)"
          width="110"
          align="center"
        />
        <el-table-column
          prop="statusText"
          label="考勤状态"
          width="120"
          align="center"
        >
          <template #default="scope">
            <el-tag
              v-if="scope.row.status === 'normal'"
              type="success"
              size="small"
            >
              æ­£å¸¸
            </el-tag>
            <el-tag
              v-else
              type="danger"
              size="small"
            >
              {{ scope.row.statusText }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          prop="remark"
          label="备注"
          show-overflow-tooltip
        />
      </el-table>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
import { ElMessage } from "element-plus";
// æ¨¡æ‹Ÿå½“前登录员工
const currentUser = reactive({
  id: 1,
  name: "张三",
  no: "E10001",
  dept: "生产一部",
});
// éƒ¨é—¨é€‰é¡¹
const deptOptions = [
  { label: "生产一部", value: "生产一部" },
  { label: "生产二部", value: "生产二部" },
  { label: "设备维护部", value: "设备维护部" },
  { label: "质检部", value: "质检部" },
];
// æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
const rawAttendance = ref([
  {
    id: 1,
    date: "2024-12-01",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:58",
    checkOutTime: "18:10",
    workHours: 9.2,
    status: "normal",
    statusText: "正常",
    remark: "",
  },
  {
    id: 2,
    date: "2024-12-01",
    userId: 2,
    name: "李四",
    no: "E10002",
    dept: "生产一部",
    checkInTime: "09:15",
    checkOutTime: "18:05",
    workHours: 8.8,
    status: "late",
    statusText: "迟到",
    remark: "因交通拥堵迟到",
  },
  {
    id: 3,
    date: "2024-12-01",
    userId: 3,
    name: "王五",
    no: "E20001",
    dept: "设备维护部",
    checkInTime: "08:50",
    checkOutTime: "17:20",
    workHours: 8.5,
    status: "early",
    statusText: "早退",
    remark: "外出处理紧急故障",
  },
  {
    id: 4,
    date: "2024-12-02",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:45",
    checkOutTime: "18:30",
    workHours: 9.7,
    status: "normal",
    statusText: "正常",
    remark: "加班0.5小时",
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  dept: "",
  date: "",
});
// è¡¨æ ¼æ•°æ®
const tableData = ref([]);
// å½“前时间展示
const nowTime = ref("");
let timer = null;
const updateNowTime = () => {
  const now = new Date();
  const Y = now.getFullYear();
  const M = String(now.getMonth() + 1).padStart(2, "0");
  const D = String(now.getDate()).padStart(2, "0");
  const h = String(now.getHours()).padStart(2, "0");
  const m = String(now.getMinutes()).padStart(2, "0");
  const s = String(now.getSeconds()).padStart(2, "0");
  nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
};
// ä»Šæ—¥æ—¥æœŸ
const todayStr = computed(() => nowTime.value.slice(0, 10));
// å½“日当前员工考勤记录
const todayRecord = computed(() =>
  rawAttendance.value.find(
    (item) =>
      item.userId === currentUser.id && item.date === todayStr.value
  )
);
// æ‰“卡按钮文本
const checkInOutText = computed(() => {
  if (!todayRecord.value || !todayRecord.value.checkInTime) {
    return "上班打卡";
  }
  if (!todayRecord.value.checkOutTime) {
    return "下班打卡";
  }
  return "今日已打卡完成";
});
// ä»Šæ—¥çŠ¶æ€å±•ç¤º
const todayStatusTag = computed(() => {
  if (!todayRecord.value) return "info";
  if (todayRecord.value.status === "normal") return "success";
  return "danger";
});
const todayStatusText = computed(() => {
  if (!todayRecord.value) return "未打卡";
  return todayRecord.value.statusText || "正常";
});
// è¡Œæ ·å¼ï¼šå¼‚常高亮
const rowClassName = ({ row }) => {
  if (row.status === "late" || row.status === "early") {
    return "row-abnormal";
  }
  return "";
};
// æŸ¥è¯¢
const recomputeTable = () => {
  const list = rawAttendance.value.filter((item) => {
    if (searchForm.dept && item.dept !== searchForm.dept) {
      return false;
    }
    if (searchForm.date && item.date !== searchForm.date) {
      return false;
    }
    return true;
  });
  tableData.value = list;
};
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.dept = "";
  searchForm.date = "";
  recomputeTable();
};
// å¯¼å‡ºï¼ˆæ¼”示)
const handleExport = () => {
  ElMessage.success("当前为演示页面,导出功能未对接实际接口");
};
// æ‰“卡逻辑(仅前端模拟)
const handleCheckInOut = () => {
  const [dateStr, timeStr] = nowTime.value.split(" ");
  if (!dateStr || !timeStr) return;
  // ä¸Šç­æ‰“卡
  if (!todayRecord.value) {
    const newId = rawAttendance.value.length
      ? Math.max(...rawAttendance.value.map((i) => i.id)) + 1
      : 1;
    const status =
      timeStr > "09:00:00" ? "late" : "normal";
    const statusText = status === "late" ? "迟到" : "正常";
    rawAttendance.value.push({
      id: newId,
      date: dateStr,
      userId: currentUser.id,
      name: currentUser.name,
      no: currentUser.no,
      dept: currentUser.dept,
      checkInTime: timeStr.slice(0, 5),
      checkOutTime: "",
      workHours: null,
      status,
      statusText,
      remark: "",
    });
    ElMessage.success("上班打卡成功");
  } else if (!todayRecord.value.checkOutTime) {
    // ä¸‹ç­æ‰“卡
    todayRecord.value.checkOutTime = timeStr.slice(0, 5);
    // ç®€å•按 9:00-18:00 è®¡ç®—工时
    const start = todayRecord.value.checkInTime || "09:00";
    const [sh, sm] = start.split(":").map((v) => parseInt(v, 10));
    const [eh, em] = todayRecord.value.checkOutTime
      .split(":")
      .map((v) => parseInt(v, 10));
    const diff = (eh * 60 + em - (sh * 60 + sm)) / 60;
    todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1));
    // æ—©é€€åˆ¤æ–­ï¼š18:00 å‰ç¦»å¼€è§†ä¸ºæ—©é€€ï¼ˆåªç¤ºæ„ï¼‰
    if (timeStr < "18:00:00") {
      todayRecord.value.status = "early";
      todayRecord.value.statusText = "早退";
    } else if (todayRecord.value.status === "normal") {
      todayRecord.value.statusText = "正常";
    }
    ElMessage.success("下班打卡成功");
  } else {
    ElMessage.info("今日已完成上下班打卡");
  }
  recomputeTable();
};
onMounted(() => {
  updateNowTime();
  timer = setInterval(updateNowTime, 1000);
  // é»˜è®¤å±•示当天数据
  const today = new Date();
  const Y = today.getFullYear();
  const M = String(today.getMonth() + 1).padStart(2, "0");
  const D = String(today.getDate()).padStart(2, "0");
  searchForm.date = `${Y}-${M}-${D}`;
  recomputeTable();
});
onBeforeUnmount(() => {
  if (timer) {
    clearInterval(timer);
  }
});
</script>
<style scoped lang="scss">
.mb16 {
  margin-bottom: 16px;
}
.attendance-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.attendance-header .title {
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 4px;
}
.attendance-header .sub-title {
  font-size: 13px;
  color: #909399;
}
.attendance-actions {
  display: flex;
  align-items: center;
  gap: 16px;
}
.time-block {
  text-align: right;
}
.time-block .label {
  font-size: 12px;
  color: #909399;
}
.time-block .value {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
::v-deep(.row-abnormal) {
  background-color: #fff5f5;
}
</style>
src/views/personnelManagement/contractManagement/filesDia.vue
@@ -28,6 +28,7 @@
          :tableData="tableData"
          :tableLoading="tableLoading"
          :isSelection="true"
          :page="page"
          @selection-change="handleSelectionChange"
          height="500"
          @pagination="paginationSearch"
@@ -118,7 +119,7 @@
const getList = () => {
  fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
    tableData.value = res.data.records;
        total.value = res.data.total;
    page.total = res.data.total;
  })
}
// è¡¨æ ¼é€‰æ‹©æ•°æ®
src/views/procurementManagement/procurementLedger/index.vue
@@ -157,6 +157,10 @@
                         prop="entryDate"
                         width="100"
                         show-overflow-tooltip />
        <el-table-column label="备注"
                         prop="remarks"
                         width="200"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
                         label="操作"
                         width="120"
@@ -450,8 +454,8 @@
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注·:"
                          prop="remark">
              <el-input v-model="form.remark"
                          prop="remarks">
              <el-input v-model="form.remarks"
                        placeholder="请输入"
                        clearable
                        type="textarea"
@@ -462,7 +466,7 @@
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:"
                          prop="remark">
                          prop="purchaseLedgerFiles">
              <el-upload v-model:file-list="fileList"
                         :action="upload.url"
                         multiple
@@ -1644,7 +1648,7 @@
          delProduct(ids).then(res => {
            proxy.$modal.msgSuccess("删除成功");
            closeProductDia();
            getSalesLedgerWithProducts({ id: currentId.value, type: 2 }).then(
            getPurchaseById({ id: currentId.value, type: 2 }).then(
              res => {
                productData.value = res.productData;
              }
src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -82,10 +82,15 @@
      <el-table-column label="产品名称" prop="productName" min-width="160" />
      <el-table-column label="规格名称" prop="model" min-width="140" />
      <el-table-column label="单位" prop="unit" width="100" />
      <el-table-column label="是否质检" prop="isQuality" width="100">
        <template #default="scope">
          {{scope.row.isQuality ? "是" : "否"}}
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" fixed="right" width="150">
        <template #default="scope">
          <el-button type="primary" link size="small" @click="handleEdit(scope.row)">编辑</el-button>
          <el-button type="danger" link size="small" @click="handleDelete(scope.row)">删除</el-button>
          <el-button type="primary" link size="small" @click="handleEdit(scope.row)" :disabled="scope.row.isComplete">编辑</el-button>
          <el-button type="danger" link size="small" @click="handleDelete(scope.row)" :disabled="scope.row.isComplete">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
@@ -130,14 +135,15 @@
                {{ item.model }}
                <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> -->
              </div>
              <el-tag type="primary" class="product-tag" v-if="item.isQuality">质检</el-tag>
            </div>
            <div v-else class="product-info empty">暂无产品信息</div>
          </div>
          
          <!-- æ“ä½œæŒ‰é’® -->
          <div class="card-footer">
            <el-button type="primary" link size="small" @click="handleEdit(item)">编辑</el-button>
            <el-button type="danger" link size="small" @click="handleDelete(item)">删除</el-button>
            <el-button type="primary" link size="small" @click="handleEdit(item)" :disabled="item.isComplete">编辑</el-button>
            <el-button type="danger" link size="small" @click="handleDelete(item)" :disabled="item.isComplete">删除</el-button>
          </div>
        </div>
      </div>
@@ -188,6 +194,10 @@
              clearable 
              :disabled="true" 
          />
        </el-form-item>
        <el-form-item label="是否质检" prop="isQuality">
          <el-switch v-model="form.isQuality" :active-value="true" inactive-value="false"/>
        </el-form-item>
      </el-form>
@@ -262,6 +272,7 @@
  productName: "",
  model: "",
  unit: "",
  isQuality: false,
});
const rules = {
@@ -340,6 +351,7 @@
    productName: row.productName || "",
    model: row.model || "",
    unit: row.unit || "",
    isQuality: row.isQuality,
  };
  dialogVisible.value = true;
};
@@ -402,12 +414,14 @@
              productRouteId: routeId.value,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
              isQuality: form.value.isQuality,
              dragSort,
            })
          : addOrUpdateProcessRouteItem({
              routeId: routeId.value,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
              isQuality: form.value.isQuality,
              dragSort,
            });
@@ -432,12 +446,14 @@
              id: form.value.id,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
              isQuality: form.value.isQuality,
            })
          : addOrUpdateProcessRouteItem({
              routeId: routeId.value,
              processId: form.value.processId,
              productModelId: form.value.productModelId,
              id: form.value.id,
              isQuality: form.value.isQuality,
            });
        updatePromise
@@ -733,6 +749,10 @@
  color: #409eff;
}
.product-tag {
  margin: 10px 0;
}
.card-footer {
  display: flex;
  justify-content: space-around;
src/views/productionManagement/productionOrder/New.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
<template>
  <div>
    <el-dialog
        v-model="isShow"
        title="新增生产订单"
        width="800"
        @close="closeModal"
    >
      <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
        <el-form-item
            label="产品名称"
            prop="productModelId"
            :rules="[
                {
                required: true,
                message: '请选择产品',
                trigger: 'change',
              }
            ]"
        >
          <el-button type="primary" @click="showProductSelectDialog = true">
            {{ formState.productName ? formState.productName : '选择产品' }}
          </el-button>
        </el-form-item>
        <el-form-item
            label="规格"
            prop="productModelName"
        >
          <el-input v-model="formState.productModelName"  disabled />
        </el-form-item>
        <el-form-item
            label="单位"
            prop="unit"
        >
          <el-input v-model="formState.unit"  disabled />
        </el-form-item>
        <el-form-item label="工艺路线">
          <el-select v-model="formState.routeId"
                     placeholder="请选择工艺路线"
                     style="width: 100%;"
                     :loading="bindRouteLoading">
            <el-option v-for="item in routeOptions"
                       :key="item.id"
                       :label="`${item.processRouteCode || ''}`"
                       :value="item.id" />
          </el-select>
        </el-form-item>
        <el-form-item
            label="需求数量"
            prop="quantity"
        >
          <el-input-number v-model="formState.quantity" :step="1" :min="1" style="width: 100%" />
        </el-form-item>
      </el-form>
      <!-- äº§å“é€‰æ‹©å¼¹çª— -->
      <ProductSelectDialog
          v-model="showProductSelectDialog"
          @confirm="handleProductSelect"
          single
      />
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="handleSubmit">确认</el-button>
          <el-button @click="closeModal">取消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import {ref, computed, getCurrentInstance} from "vue";
import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
import {addProductOrder, listProcessRoute} from "@/api/productionManagement/productionOrder.js";
const props = defineProps({
  visible: {
    type: Boolean,
    required: true,
  },
  type: {
    type: String,
    required: true,
    default: 'qualified',
  },
});
const emit = defineEmits(['update:visible', 'completed']);
// å“åº”式数据(替代选项式的 data)
const formState = ref({
  productId: undefined,
  productModelId: undefined,
  routeId: undefined,
  productName: "",
  productModelName: "",
  unit: "",
  quantity: 0,
});
const isShow = computed({
  get() {
    return props.visible;
  },
  set(val) {
    emit('update:visible', val);
  },
});
const showProductSelectDialog = ref(false);
let { proxy } = getCurrentInstance()
const closeModal = () => {
  // é‡ç½®è¡¨å•数据
  formState.value = {
    productId: undefined,
    productModelId: undefined,
    routeId: undefined,
    productName: "",
    productModelName: "",
    quantity: '',
  };
  isShow.value = false;
};
// äº§å“é€‰æ‹©å¤„理
const handleProductSelect = async (products) => {
  if (products && products.length > 0) {
    const product = products[0];
    formState.value.productId = product.productId;
    formState.value.productName = product.productName;
    formState.value.productModelName = product.model;
    formState.value.productModelId = product.id;
    formState.value.unit = product.unit;
    showProductSelectDialog.value = false;
    fetchRouteOptions( product.id);
    // è§¦å‘表单验证更新
    proxy.$refs["formRef"]?.validateField('productModelId');
  }
};
const routeOptions = ref([]);
const bindRouteLoading = ref(false);
const fetchRouteOptions = (productModelId) => {
  formState.value.routeId = undefined;
  routeOptions.value = []
  bindRouteLoading.value = true;
  listProcessRoute({ productModelId: productModelId }).then(res => {
    routeOptions.value = res.data || [];
  }).finally(() => {
    bindRouteLoading.value = false;
  })
}
const handleSubmit = () => {
  proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      // éªŒè¯æ˜¯å¦é€‰æ‹©äº†äº§å“å’Œè§„æ ¼
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择产品");
        return;
      }
      if (!formState.value.productModelId) {
        proxy.$modal.msgError("请选择规格");
        return;
      }
      addProductOrder(formState.value).then(res => {
        // å…³é—­æ¨¡æ€æ¡†
        isShow.value = false;
        // å‘ŠçŸ¥çˆ¶ç»„件已完成
        emit('completed');
        proxy.$modal.msgSuccess("提交成功");
      })
    }
  })
};
defineExpose({
  closeModal,
  handleSubmit,
  isShow,
});
</script>
src/views/productionManagement/productionOrder/index.vue
@@ -41,6 +41,8 @@
        </el-form-item>
      </el-form>
      <div>
        <el-button type="primary" @click="isShowNewModal = true">新增</el-button>
        <el-button type="danger" @click="handleDelete">删除</el-button>
        <el-button @click="handleOut">导出</el-button>
      </div>
    </div>
@@ -51,6 +53,8 @@
                :page="page"
                :tableLoading="tableLoading"
                :row-class-name="tableRowClassName"
                :isSelection="true"
                @selection-change="handleSelectionChange"
                @pagination="pagination">
        <template #completionStatus="{ row }">
          <el-progress
@@ -86,6 +90,10 @@
        </span>
      </template>
    </el-dialog>
    <new-product-order v-if="isShowNewModal"
                         v-model:visible="isShowNewModal"
                         @completed="handleQuery" />
  </div>
</template>
@@ -98,12 +106,17 @@
    productOrderListPage,
    listProcessRoute,
    bindingRoute,
    listProcessBom,
    listProcessBom, delProductOrder,
  } from "@/api/productionManagement/productionOrder.js";
  import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
  import {fileDel} from "@/api/financialManagement/revenueManagement.js";
  import PIMTable from "@/components/PIMTable/PIMTable.vue";
  const NewProductOrder = defineAsyncComponent(() => import("@/views/productionManagement/productionOrder/New.vue"));
  const { proxy } = getCurrentInstance();
  const router = useRouter();
  const isShowNewModal = ref(false);
  const tableColumn = ref([
    {
@@ -208,6 +221,7 @@
    size: 100,
    total: 0,
  });
  const selectedRows = ref([]);
  const data = reactive({
    searchForm: {
@@ -239,8 +253,10 @@
  // æ·»åŠ è¡¨è¡Œç±»åæ–¹æ³•
  const tableRowClassName = ({ row }) => {
    const diff = row.deliveryDaysDiff;
    if (!row.deliveryDate) return '';
    if (row.isFh) return '';
    const diff = row.deliveryDaysDiff;
    if (diff === 15) {
      return 'yellow';
    } else if (diff === 10) {
@@ -385,6 +401,33 @@
    });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
  const handleSelectionChange = (selection) => {
    selectedRows.value = selection;
  };
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      ids = selectedRows.value.map((item) => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
      return;
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }).then(() => {
      delProductOrder(ids).then((res) => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
    }).catch(() => {
      proxy.$modal.msg("已取消");
    });
  };
  // å¯¼å‡º
  const handleOut = () => {
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
src/views/productionManagement/productionProcess/Edit.vue
@@ -28,6 +28,9 @@
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
        </el-form-item>
        <el-form-item label="是否质检" prop="isQuality">
          <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formState.remark" type="textarea" />
        </el-form-item>
@@ -67,6 +70,7 @@
  no: props.record.no,
  remark: props.record.remark,
  salaryQuota: props.record.salaryQuota,
  isQuality: props.record.isQuality,
});
const isShow = computed({
@@ -87,6 +91,7 @@
      no: newRecord.no || '',
      remark: newRecord.remark || '',
      salaryQuota: newRecord.salaryQuota || '',
      isQuality: props.record.isQuality,
    };
  }
}, { immediate: true, deep: true });
@@ -100,6 +105,7 @@
      no: props.record.no || '',
      remark: props.record.remark || '',
      salaryQuota: props.record.salaryQuota || '',
      isQuality: props.record.isQuality,
    };
  }
});
src/views/productionManagement/productionProcess/New.vue
@@ -28,6 +28,9 @@
        <el-form-item label="工资定额" prop="salaryQuota">
          <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
        </el-form-item>
        <el-form-item label="是否质检" prop="isQuality">
          <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="formState.remark" type="textarea" />
        </el-form-item>
@@ -60,6 +63,7 @@
  name: '',
  remark: '',
  salaryQuota:  '',
  isQuality: false,
});
const isShow = computed({
src/views/productionManagement/productionProcess/index.vue
@@ -98,12 +98,18 @@
      label: "工序名称",
      prop: "name",
    },
    {
      label: "工资定额",
      prop: "salaryQuota",
    },
    {
      label: "是否质检",
      prop: "isQuality",
      formatData: (params) => {
        return params ? "是" : "否";
      },
    },
    {
      label: "备注",
      prop: "remark",
    },
src/views/qualityManagement/afterSalesTraceability/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,595 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-form
      :model="queryParams"
      ref="queryForm"
      :inline="true"
      v-show="showSearch"
      label-width="90px"
    >
      <el-form-item label="产品型号" prop="productModel">
        <el-input
          v-model="queryParams.productModel"
          placeholder="请输入产品型号"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="客户名称" prop="customerName">
        <el-input
          v-model="queryParams.customerName"
          placeholder="请输入客户名称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="反馈时间" prop="feedbackRange">
        <el-date-picker
          v-model="queryParams.feedbackRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
        />
      </el-form-item>
      <el-form-item label="处理状态" prop="status">
        <el-select
          v-model="queryParams.status"
          placeholder="请选择处理状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">
          æœç´¢
        </el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ“ä½œåŒº -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="3">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
        >
          æ–°å¢žå”®åŽè´¨é‡è®°å½•
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
        >
          ä¿®æ”¹
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
        >
          åˆ é™¤
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="warning"
          plain
          icon="Download"
          @click="handleExport"
        >
          å¯¼å‡º
        </el-button>
      </el-col>
      <right-toolbar
        v-model:showSearch="showSearch"
        @queryTable="getList"
      />
    </el-row>
    <!-- æ•°æ®è¡¨ -->
    <el-table
      v-loading="loading"
      :data="afterSalesList"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="序号" type="index" width="55" align="center" />
      <el-table-column label="销售合同号" prop="contractNo" width="160" />
      <el-table-column label="产品编号" prop="productCode" width="140" />
      <el-table-column label="产品型号" prop="productModel" width="140" />
      <el-table-column label="客户名称" prop="customerName" width="160" />
      <el-table-column label="联系方式" prop="contact" width="140" />
      <el-table-column label="反馈时间" prop="feedbackTime" width="160">
        <template #default="scope">
          <span>{{ scope.row.feedbackTime }}</span>
        </template>
      </el-table-column>
      <el-table-column label="问题描述" prop="problemDesc" show-overflow-tooltip />
      <el-table-column label="维修情况" prop="repairInfo" show-overflow-tooltip />
      <el-table-column label="处理结果" prop="result" show-overflow-tooltip />
      <el-table-column label="处理状态" prop="status" width="120" align="center">
        <template #default="scope">
          <dict-tag
            :options="statusOptions"
            :value="scope.row.status"
          />
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160" fixed="right">
        <template #default="scope">
          <el-button size="small" type="text" icon="Edit" @click="handleUpdate(scope.row)">
            ä¿®æ”¹
          </el-button>
          <el-button size="small" type="text" icon="Delete" @click="handleDelete(scope.row)">
            åˆ é™¤
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
    <!-- æ–°å¢ž/修改弹窗 -->
    <el-dialog
      :title="title"
      v-model="open"
      width="900px"
      append-to-body
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
      >
        <el-row>
          <el-col :span="12">
            <el-form-item label="销售合同号" prop="contractNo">
              <el-input
                v-model="form.contractNo"
                placeholder="请选择关联销售合同号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品编号" prop="productCode">
              <el-input
                v-model="form.productCode"
                placeholder="请选择关联产品编号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="产品型号" prop="productModel">
              <el-input
                v-model="form.productModel"
                placeholder="请输入产品型号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户名称" prop="customerName">
              <el-input
                v-model="form.customerName"
                placeholder="请输入客户名称"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="联系方式" prop="contact">
              <el-input
                v-model="form.contact"
                placeholder="请输入客户联系方式"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="反馈时间" prop="feedbackTime">
              <el-date-picker
                v-model="form.feedbackTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择反馈时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="客户反馈问题" prop="problemDesc">
              <el-input
                v-model="form.problemDesc"
                type="textarea"
                :rows="3"
                placeholder="请详细记录客户反馈问题、现象描述等信息"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="维修情况" prop="repairInfo">
              <el-input
                v-model="form.repairInfo"
                type="textarea"
                :rows="2"
                placeholder="记录维修过程、使用备件、返修次数等"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="处理结果" prop="result">
              <el-input
                v-model="form.result"
                type="textarea"
                :rows="2"
                placeholder="记录最终处理结果,如更换、退货、升级等"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="处理状态" prop="status">
              <el-select
                v-model="form.status"
                placeholder="请选择处理状态"
              >
                <el-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="备注" prop="remark">
              <el-input
                v-model="form.remark"
                type="textarea"
                :rows="2"
                placeholder="可记录后续跟踪意见、复盘结论等"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup name="AfterSalesTraceability">
import { ref, reactive, onMounted } from "vue";
import { ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
// çŠ¶æ€å­—å…¸
const statusOptions = ref([
  { label: "待处理", value: "0" },
  { label: "处理中", value: "1" },
  { label: "已完成", value: "2" },
  { label: "已关闭", value: "3" },
]);
// æ¨¡æ‹Ÿå”®åŽè´¨é‡æ•°æ®
const afterSalesList = ref([
  {
    id: 1,
    contractNo: "SC-2024-001",
    productCode: "P-10001",
    productModel: "XG-500A",
    customerName: "华南电子科技有限公司",
    contact: "å¼ å·¥ / 13800000001",
    feedbackTime: "2024-12-01 10:23:00",
    problemDesc: "使用三个月后出现间歇性掉电,影响产线稳定运行。",
    repairInfo: "安排工程师上门检修,更换电源模块并加固接线端子。",
    result: "更换电源总成,恢复正常使用,建议客户增加UPS保护。",
    status: "2",
    remark: "列入重点跟踪客户,后续观察一个季度。",
  },
  {
    id: 2,
    contractNo: "SC-2024-015",
    productCode: "P-10045",
    productModel: "XG-500B",
    customerName: "华东精密制造有限公司",
    contact: "李工 / 13800000002",
    feedbackTime: "2024-12-05 15:40:00",
    problemDesc: "部分批次出现外壳刮花,客户投诉外观质量不达标。",
    repairInfo: "与生产现场核查,确认来料搬运及包装环节存在磕碰风险。",
    result: "对问题批次重新返工,补发良品,并优化包装防护方案。",
    status: "1",
    remark: "需跟踪后续批次投诉率变化。",
  },
  {
    id: 3,
    contractNo: "SC-2024-032",
    productCode: "P-10110",
    productModel: "XG-600C",
    customerName: "西南新能源科技股份",
    contact: "王工 / 13800000003",
    feedbackTime: "2024-11-28 09:15:00",
    problemDesc: "现场调试时发现接口不兼容,需要适配客户旧版系统。",
    repairInfo: "远程技术支持+现场工程师联合排查,提供过渡适配方案。",
    result: "通过更换接插件并升级固件版本解决。",
    status: "0",
    remark: "建议下次合同前置沟通接口规格。",
  },
]);
const open = ref(false);
const loading = ref(false);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(3);
const title = ref("新增售后质量记录");
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    productModel: null,
    customerName: null,
    feedbackRange: [],
    status: null,
  },
  rules: {
    contractNo: [
      { required: true, message: "销售合同号不能为空", trigger: "blur" },
    ],
    productCode: [
      { required: true, message: "产品编号不能为空", trigger: "blur" },
    ],
    productModel: [
      { required: true, message: "产品型号不能为空", trigger: "blur" },
    ],
    customerName: [
      { required: true, message: "客户名称不能为空", trigger: "blur" },
    ],
    contact: [
      { required: true, message: "联系方式不能为空", trigger: "blur" },
    ],
    feedbackTime: [
      { required: true, message: "反馈时间不能为空", trigger: "change" },
    ],
    problemDesc: [
      { required: true, message: "客户反馈问题不能为空", trigger: "blur" },
    ],
    status: [
      { required: true, message: "处理状态不能为空", trigger: "change" },
    ],
  },
});
const { queryParams, form, rules } = toRefs(data);
// æŸ¥è¯¢åˆ—表(仅前端筛选,不调接口)
function getList() {
  loading.value = true;
  const list = afterSalesList.value.filter((item) => {
    if (
      queryParams.value.productModel &&
      !item.productModel
        ?.toLowerCase()
        .includes(queryParams.value.productModel.toLowerCase())
    ) {
      return false;
    }
    if (
      queryParams.value.customerName &&
      !item.customerName
        ?.toLowerCase()
        .includes(queryParams.value.customerName.toLowerCase())
    ) {
      return false;
    }
    if (queryParams.value.status && item.status !== queryParams.value.status) {
      return false;
    }
    if (
      Array.isArray(queryParams.value.feedbackRange) &&
      queryParams.value.feedbackRange.length === 2
    ) {
      const [start, end] = queryParams.value.feedbackRange;
      const dateStr = item.feedbackTime?.slice(0, 10);
      if (dateStr < start || dateStr > end) {
        return false;
      }
    }
    return true;
  });
  total.value = list.length;
  // æ­¤å¤„未做分页,仅模拟全量展示
  afterSalesList.value = list;
  loading.value = false;
}
// å–消
function cancel() {
  open.value = false;
  reset();
}
// è¡¨å•重置
function reset() {
  form.value = {
    id: null,
    contractNo: null,
    productCode: null,
    productModel: null,
    customerName: null,
    contact: null,
    feedbackTime: null,
    problemDesc: null,
    repairInfo: null,
    result: null,
    status: "0",
    remark: null,
  };
  proxy.resetForm("formRef");
}
// æœç´¢
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
}
// é‡ç½®
function resetQuery() {
  proxy.resetForm("queryForm");
  queryParams.value.feedbackRange = [];
  handleQuery();
}
// å¤šé€‰
function handleSelectionChange(selection) {
  ids.value = selection.map((item) => item.id);
  single.value = selection.length !== 1;
  multiple.value = !selection.length;
}
// æ–°å¢ž
function handleAdd() {
  reset();
  open.value = true;
  title.value = "新增售后质量记录";
}
// ä¿®æ”¹
function handleUpdate(row) {
  reset();
  const current = row || afterSalesList.value.find((item) => item.id === ids.value[0]);
  if (current) {
    form.value = { ...current };
    open.value = true;
    title.value = "修改售后质量记录";
  }
}
// æäº¤
function submitForm() {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      if (form.value.id != null) {
        // ä¿®æ”¹ï¼šæ›¿æ¢æœ¬åœ°æ¨¡æ‹Ÿæ•°æ®
        const index = afterSalesList.value.findIndex(
          (item) => item.id === form.value.id
        );
        if (index !== -1) {
          afterSalesList.value.splice(index, 1, { ...form.value });
        }
        proxy.$modal.msgSuccess("修改成功");
      } else {
        // æ–°å¢žï¼šæ’入本地模拟数据
        const newId =
          afterSalesList.value.length > 0
            ? Math.max(...afterSalesList.value.map((i) => i.id)) + 1
            : 1;
        afterSalesList.value.push({ ...form.value, id: newId });
        proxy.$modal.msgSuccess("新增成功");
      }
      open.value = false;
      getList();
    }
  });
}
// åˆ é™¤
function handleDelete(row) {
  const deleteIds = row?.id ? [row.id] : ids.value;
  if (!deleteIds || deleteIds.length === 0) {
    proxy.$modal.msgWarning("请先选择要删除的记录");
    return;
  }
  ElMessageBox.confirm(
    '是否确认删除选中的售后质量记录?',
    "警告",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      // å‰ç«¯åˆ é™¤æœ¬åœ°æ¨¡æ‹Ÿæ•°æ®
      afterSalesList.value = afterSalesList.value.filter(
        (item) => !deleteIds.includes(item.id)
      );
      proxy.$modal.msgSuccess("删除成功");
      getList();
    })
    .catch(() => {});
}
// å¯¼å‡º
function handleExport() {
  proxy.$modal.msgSuccess("导出功能为演示功能,当前未对接实际导出接口");
}
onMounted(() => {
  getList();
});
</script>
<style scoped>
.mb8 {
  margin-bottom: 8px;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -201,64 +201,91 @@
const openDialog = async (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  getOptions().then((res) => {
    supplierList.value = res.data;
  });
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    form.value = {}
  // å…ˆæ¸…空表单验证状态,避免闪烁
  await nextTick();
  proxy.$refs.formRef?.clearValidate();
  // å¹¶è¡ŒåŠ è½½åŸºç¡€æ•°æ®
  const [userListsRes] = await Promise.all([
    userListNoPage(),
    getProductOptions(),
    getOptions().then((res) => {
      supplierList.value = res.data;
    })
  ]);
  userList.value = userListsRes.data;
  form.value = {}
  testStandardOptions.value = [];
  tableData.value = [];
  getProductOptions();
  if (operationType.value === 'edit') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
    // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
    form.value = {...row, testStandardId: ''}
        currentProductId.value = row.productId || 0
        // ç¼–辑模式下,先加载指标选项,然后加载参数列表
        if (currentProductId.value) {
            // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
            let params = {
                productId: currentProductId.value,
                inspectType: 2
            }
            qualityInspectDetailByProductId(params).then(res => {
                testStandardOptions.value = res.data || [];
                // ä½¿ç”¨ nextTick å’Œ setTimeout ç¡®ä¿é€‰é¡¹å·²ç»æ¸²æŸ“到 DOM
                nextTick(() => {
                    setTimeout(() => {
                        // å¦‚果编辑数据中有 testStandardId,则设置并加载对应的参数
                        if (savedTestStandardId) {
                            // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
                            const matchedOption = testStandardOptions.value.find(item =>
                                item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
                            );
                            if (matchedOption) {
                                // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id(保持类型一致)
                                form.value.testStandardId = matchedOption.id;
                                // ç¼–辑场景保留已有检验值,直接拉取原参数数据
                                getQualityInspectParamList(row.id);
                            } else {
                                // å¦‚果找不到匹配项,尝试直接使用原值
                                console.warn('未找到匹配的指标选项,testStandardId:', savedTestStandardId, '可用选项:', testStandardOptions.value);
                                form.value.testStandardId = savedTestStandardId;
                                getQualityInspectParamList(row.id);
                            }
                        } else {
                            // å¦åˆ™ä½¿ç”¨æ—§çš„逻辑
                            getQualityInspectParamList(row.id);
                        }
                    }, 100);
                });
            });
        } else {
            getQualityInspectParamList(row.id);
        }
    currentProductId.value = row.productId || 0
    // æ¸…空验证状态,避免数据加载过程中的校验闪烁
    nextTick(() => {
      proxy.$refs.formRef?.clearValidate();
    });
    // ç¼–辑模式下,并行加载规格型号和指标选项
    if (currentProductId.value) {
      // è®¾ç½®äº§å“åç§°
      form.value.productName = findNodeById(productOptions.value, currentProductId.value);
      // å¹¶è¡ŒåŠ è½½è§„æ ¼åž‹å·å’ŒæŒ‡æ ‡é€‰é¡¹
      const params = {
        productId: currentProductId.value,
        inspectType: 2
      };
      Promise.all([
        modelList({ id: currentProductId.value }),
        qualityInspectDetailByProductId(params)
      ]).then(([modelRes, testStandardRes]) => {
        // è®¾ç½®è§„格型号选项
        modelOptions.value = modelRes || [];
        // å¦‚果表单中已有 productModelId,设置对应的 model å’Œ unit
        if (form.value.productModelId && modelOptions.value.length > 0) {
          const selectedModel = modelOptions.value.find(item => item.id == form.value.productModelId);
          if (selectedModel) {
            form.value.model = selectedModel.model || '';
            form.value.unit = selectedModel.unit || '';
          }
        }
        // è®¾ç½®æŒ‡æ ‡é€‰é¡¹
        testStandardOptions.value = testStandardRes.data || [];
        // è®¾ç½® testStandardId å¹¶åŠ è½½å‚æ•°åˆ—è¡¨
        nextTick(() => {
          if (savedTestStandardId) {
            // ç¡®ä¿ç±»åž‹åŒ¹é…ï¼ˆitem.id å¯èƒ½æ˜¯æ•°å­—或字符串)
            const matchedOption = testStandardOptions.value.find(item =>
              item.id == savedTestStandardId || String(item.id) === String(savedTestStandardId)
            );
            if (matchedOption) {
              // ç¡®ä¿ä½¿ç”¨åŒ¹é…é¡¹çš„ id(保持类型一致)
              form.value.testStandardId = matchedOption.id;
            } else {
              // å¦‚果找不到匹配项,尝试直接使用原值
              console.warn('未找到匹配的指标选项,testStandardId:', savedTestStandardId, '可用选项:', testStandardOptions.value);
              form.value.testStandardId = savedTestStandardId;
            }
          }
          // ç¼–辑场景保留已有检验值,直接拉取原参数数据
          getQualityInspectParamList(row.id);
        });
      });
    } else {
      getQualityInspectParamList(row.id);
    }
  }
}
const getProductOptions = () => {
  productTreeList().then((res) => {
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
  });
};
src/views/qualityManagement/finalInspection/index.vue
@@ -96,6 +96,11 @@
    width: 120
  },
  {
    label: "生产工单号",
    prop: "workOrderNo",
    width: 120
  },
  {
    label: "检验员",
    prop: "checkName",
  },
src/views/qualityManagement/processInspection/components/formDia.vue
@@ -207,22 +207,50 @@
// æ‰“开弹框
const openDialog = async (type, row) => {
    operationType.value = type;
    dialogFormVisible.value = true;
    getOptions().then((res) => {
        supplierList.value = res.data;
    });
    let userLists = await userListNoPage();
    userList.value = userLists.data;
    form.value = {}
    // å…ˆé‡ç½®è¡¨å•数据(保持字段完整,避免弹窗首次渲染时触发必填红框“闪一下”)
    form.value = {
        checkTime: "",
        process: "",
        checkName: "",
        productName: "",
        productId: "",
        productModelId: "",
        model: "",
        testStandardId: "",
        unit: "",
        quantity: "",
        checkCompany: "",
        checkResult: "",
    }
    testStandardOptions.value = [];
    tableData.value = [];
    getProductOptions();
    // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
    await getProductOptions();
    if (operationType.value === 'edit') {
        // å…ˆä¿å­˜ testStandardId,避免被清空
        const savedTestStandardId = row.testStandardId;
        // å…ˆè®¾ç½®è¡¨å•数据,但暂时清空 testStandardId,等选项加载完成后再设置
        form.value = {...row, testStandardId: ''}
        currentProductId.value = row.productId || 0
        // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
        if (currentProductId.value) {
            try {
                const res = await modelList({ id: currentProductId.value });
                modelOptions.value = res || [];
                // åŒæ­¥å›žå¡« model / unit(有些接口返回的 row é‡Œå¯èƒ½æ²¡å¸¦å…¨ï¼‰
                if (form.value.productModelId) {
                    handleChangeModel(form.value.productModelId);
                }
            } catch (e) {
                console.error("加载规格型号失败", e);
                modelOptions.value = [];
            }
        }
        // ç¼–辑模式下,先加载指标选项,然后加载参数列表
        if (currentProductId.value) {
            // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
@@ -264,10 +292,16 @@
            getQualityInspectParamList(row.id);
        }
    }
    // æœ€åŽå†æ‰“开弹窗,并清理校验态,避免必填提示闪烁
    dialogFormVisible.value = true;
    nextTick(() => {
        proxy.$refs?.formRef?.clearValidate?.();
    });
}
const getProductOptions = () => {
  productTreeList().then((res) => {
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
        return productOptions.value;
  });
};
const getModels = (value) => {
src/views/qualityManagement/processInspection/index.vue
@@ -96,6 +96,11 @@
    width: 120
  },
  {
    label: "生产工单号",
    prop: "workOrderNo",
    width: 120
  },
  {
    label: "工序",
    prop: "process",
    width: 230
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -218,21 +218,49 @@
const modelOptions = ref([]);
// æ‰“开弹框
const openDialog = (type, row) => {
const openDialog = async (type, row) => {
  operationType.value = type;
  dialogFormVisible.value = true;
  getOptions().then((res) => {
    supplierList.value = res.data;
  });
    form.value = {}
  // å…ˆé‡ç½®è¡¨å•数据(保持字段完整,避免弹窗首次渲染时触发必填红框“闪一下”)
    form.value = {
    checkTime: "",
    supplier: "",
    checkName: "",
    productName: "",
    productId: "",
    productModelId: "",
    model: "",
    testStandardId: "",
    unit: "",
    quantity: "",
    checkCompany: "",
    checkResult: "",
  }
  testStandardOptions.value = [];
  tableData.value = [];
  getProductOptions();
  // å…ˆç¡®ä¿äº§å“æ ‘已加载,否则编辑时产品/规格型号无法反显
  await getProductOptions();
  if (operationType.value === 'edit') {
    // å…ˆä¿å­˜ testStandardId,避免被清空
    const savedTestStandardId = row.testStandardId;
    form.value = {...row}
    currentProductId.value = row.productId || 0
    // å…³é”®ï¼šç¼–辑时加载规格型号下拉选项,才能反显 productModelId
    if (currentProductId.value) {
      try {
        const res = await modelList({ id: currentProductId.value });
        modelOptions.value = res || [];
        // åŒæ­¥å›žå¡« model / unit(有些接口返回的 row é‡Œå¯èƒ½æ²¡å¸¦å…¨ï¼‰
        if (form.value.productModelId) {
          handleChangeModel(form.value.productModelId);
        }
      } catch (e) {
        console.error("加载规格型号失败", e);
        modelOptions.value = [];
      }
    }
    // ç¼–辑模式下,先加载指标选项,然后加载参数列表
    if (currentProductId.value) {
      // å…ˆåŠ è½½æŒ‡æ ‡é€‰é¡¹
@@ -273,10 +301,16 @@
      getQualityInspectParamList(row.id);
    }
  }
  // æœ€åŽå†æ‰“开弹窗,并清理校验态,避免必填提示闪烁
  dialogFormVisible.value = true;
  nextTick(() => {
    proxy.$refs?.formRef?.clearValidate?.();
  });
}
const getProductOptions = () => {
  productTreeList().then((res) => {
  return productTreeList().then((res) => {
    productOptions.value = convertIdToValue(res);
    return productOptions.value;
  });
};
const getModels = (value) => {
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -98,6 +98,11 @@
    width: 120
  },
  {
    label: "采购订单号",
    prop: "purchaseContractNo",
    width: 120
  },
  {
    label: "供应商",
    prop: "supplier",
    width: 230
src/views/reportAnalysis/productionAnalysis/components/center-center.vue
@@ -30,7 +30,7 @@
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import { productInOutAnalysis } from '@/api/viewIndex.js'
import { inputOutputAnalysis } from '@/api/viewIndex.js'
const chartStyle = { width: '100%', height: '100%' }
const grid = {
@@ -132,13 +132,13 @@
}
const fetchData = () => {
  productInOutAnalysis({ type: 1 })
  inputOutputAnalysis()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const list = res.data
        xAxis1.value[0].data = list.map((d) => d.date)
        lineSeries.value[0].data = list.map((d) => Number(d.outCount) || 0)
        lineSeries.value[1].data = list.map((d) => Number(d.inCount) || 0)
        lineSeries.value[0].data = list.map((d) => Number(d.outputSum) || 0)
        lineSeries.value[1].data = list.map((d) => Number(d.inputSum) || 0)
      }
    })
    .catch((err) => {
src/views/reportAnalysis/productionAnalysis/components/center-top.vue
@@ -25,7 +25,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js'
import { orderCount } from '@/api/viewIndex.js'
const statItems = ref([])
@@ -37,7 +37,7 @@
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
const fetchData = () => {
  salesPurchaseStorageProductCount()
  orderCount()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        statItems.value = res.data.map((item) => ({
@@ -48,7 +48,7 @@
      }
    })
    .catch((err) => {
      console.error('获取销售/采购/储存产品数失败:', err)
      console.error('获取订单数量统计失败:', err)
    })
}
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -24,7 +24,7 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import { processOutputAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from '@/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue'
@@ -73,7 +73,7 @@
    formatter: function (name) {
      const item = pieObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}`
      return `{title|${name}}{value|${item.value}}{unit|ä»¶}{percent|${item.rate}}{unit|%}`
    },
    textStyle: {
      rich: {
@@ -106,12 +106,12 @@
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}元 ({d}%)',
  formatter: '{a} <br/>{b} : {c}ä»¶ ({d}%)',
}
const pieSeries = computed(() => [
  {
    name: '产品销售金额分析',
    name: '工序产出分析',
    type: 'pie',
    radius: '60%',
    center: ['25%', '50%'],
@@ -150,7 +150,7 @@
})
const fetchData = () => {
  productSalesAnalysis()
  processOutputAnalysis({ dateType: dateType.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
@@ -162,7 +162,7 @@
      }
    })
    .catch((err) => {
      console.error('获取产品销售金额分析失败:', err)
      console.error('获取工序产出分析失败:', err)
    })
}
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
@@ -5,161 +5,100 @@
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="barLegend"
        :series="chartSeries"
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 260px"
      />
      <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="barLegend" :series="chartSeries"
        :tooltip="tooltip" :xAxis="xAxis1" :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }" style="height: 320px" />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { qualityStatistics } from '@/api/viewIndex.js'
import { productionAccountingAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import Echarts from '@/components/Echarts/echarts.vue'
const dateType = ref(1) // 1=周 2=月 3=季度
const dateType = ref(1)
const chartStyle = {
  width: '100%',
  height: '140%',
}
const grid = { left: '10%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const chartStyle = { width: '100%', height: '100%' }
const grid = { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true }
const barLegend = {
  show: true,
  textStyle: { color: '#B8C8E0' },
  data: ['产量', '工资', '合格率'],
  data: ['完成数量', '工资金额', '合格率'],
  top: '0%'
}
// æŸ±çŠ¶å›¾ï¼šäº§é‡ã€å·¥èµ„ï¼›æŠ˜çº¿å›¾ï¼šåˆæ ¼çŽ‡ï¼ˆç»¿è‰²ï¼‰
// åˆå§‹åŒ– series ç»“æž„
const chartSeries = ref([
  {
    name: '产量',
    name: '完成数量',
    type: 'bar',
    barWidth: 20,
    barGap: '40%',
    yAxisIndex: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(0, 164, 237, 0)' },
          { offset: 0, color: 'rgba(78, 228, 255, 1)' },
        ],
      },
    },
    data: [],
    barWidth: 15,
    itemStyle: { color: '#4EE4FF' },
    data: []
  },
  {
    name: '工资',
    name: '工资金额',
    type: 'bar',
    barGap: '40%',
    barWidth: 20,
    yAxisIndex: 1,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
          { offset: 0, color: 'rgba(144, 97, 248, 1)' },
        ],
      },
    },
    data: [],
    barWidth: 15,
    itemStyle: { color: '#00A4ED' },
    data: []
  },
  {
    name: '合格率',
    type: 'line',
    yAxisIndex: 2,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
    itemStyle: { color: 'rgba(90, 216, 166, 1)' },
    data: [],
    emphasis: { focus: 'series' },
  },
    yAxisIndex: 1,
    smooth: true,
    itemStyle: { color: '#FFD339' },
    data: []
  }
])
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] }
])
const yAxis1 = [
  { type: 'value', name: '数量/金额', axisLabel: { color: '#B8C8E0' }, splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } } },
  { type: 'value', name: '合格率(%)', max: 100, axisLabel: { formatter: '{value}%', color: '#B8C8E0' }, splitLine: { show: false } }
]
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  axisPointer: { type: 'shadow' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      let unit = 'ä»¶'
      if (item.seriesName === '合格率') unit = '%'
      else if (item.seriesName === '工资') unit = '元'
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
    let res = params[0].axisValueLabel + '<br/>'
    params.forEach(item => {
      const unit = item.seriesName === '合格率' ? '%' : (item.seriesName === '工资金额' ? ' å…ƒ' : ' ä¸ª')
      res += `${item.marker} ${item.seriesName}: ${item.value}${unit}<br/>`
    })
    return result
  },
    return res
  }
}
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: '产量(ä»¶)', position: 'left', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  { type: 'value', name: '工资(元)', position: 'left', offset: 50, axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  {
    type: 'value',
    name: '合格率(%)',
    position: 'right',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
    nameTextStyle: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
  },
]
const handleDateTypeChange = () => {
  fetchData()
}
const fetchData = () => {
  qualityStatistics()
  productionAccountingAnalysis({ type: dateType.value })
    .then((res) => {
      if (!res?.data?.item || !Array.isArray(res.data.item)) return
      const items = res.data.item
      xAxis1.value[0].data = items.map((d) => d.date)
      // äº§é‡ï¼šå‡ºåŽ‚æ•°
      chartSeries.value[0].data = items.map((d) => Number(d.factoryNum) || 0)
      // å·¥èµ„:暂无单独接口,用 0 å ä½ï¼ŒåŽç»­å¯æŽ¥å·¥èµ„接口
      chartSeries.value[1].data = items.map(() => 0)
      // åˆæ ¼çŽ‡ï¼šå‡ºåŽ‚æ•°/过程数*100(无单独接口时用此占位)
      chartSeries.value[2].data = items.map((d) => {
        const processNum = Number(d.processNum) || 0
        const factoryNum = Number(d.factoryNum) || 0
        if (processNum <= 0) return 0
        return Math.min(100, Math.round((factoryNum / processNum) * 100))
      })
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
        xAxis1.value[0].data = items.map(item => item.dateStr)
        chartSeries.value[0].data = items.map(item => Number(item.numberOfCompleted) || 0)
        chartSeries.value[1].data = items.map(item => Number(item.amount) || 0)
        chartSeries.value[2].data = items.map(item => Number(item.passRate) || 0)
        console.log('更新后的数据:', chartSeries.value)
      }
    })
    .catch((err) => {
      console.error('获取产量、工资与合格率数据失败:', err)
      console.error('数据加载失败', err)
    })
}
@@ -169,24 +108,17 @@
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.filters-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
  box-sizing: border-box;
}
</style>
.filters-row {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 10px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/right-top.vue
@@ -2,6 +2,9 @@
  <div>
    <PanelHeader title="工单执行效率分析" />
    <div class="main-panel panel-item-customers">
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
@@ -20,13 +23,16 @@
<script setup>
import { ref, onMounted } from 'vue'
import { qualityStatistics } from '@/api/viewIndex.js'
import { workOrderEfficiencyAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
const dateType = ref(1) // 1=周 2=月 3=季度
const chartStyle = {
  width: '100%',
  height: '160%',
  height: '140%',
}
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
@@ -37,7 +43,6 @@
  data: ['开工', '完成', '良品率'],
}
// æŸ±çŠ¶å›¾ï¼šå¼€å·¥ã€å®Œæˆï¼›æŠ˜çº¿å›¾ï¼šè‰¯å“çŽ‡ï¼ˆé¢œè‰² rgba(90, 216, 166, 1))
const chartSeries = ref([
  {
    name: '开工',
@@ -111,6 +116,7 @@
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: 'ä»¶', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  {
@@ -124,26 +130,25 @@
  },
]
const handleDateTypeChange = () => {
  fetchData()
}
const fetchData = () => {
  qualityStatistics()
  workOrderEfficiencyAnalysis({ dateType: dateType.value })
    .then((res) => {
      if (!res?.data?.item || !Array.isArray(res.data.item)) return
      const items = res.data.item
      if (res.code !== 200 || !Array.isArray(res.data)) return
      const items = res.data
      xAxis1.value[0].data = items.map((d) => d.date)
      // å¼€å·¥ï¼šè¿‡ç¨‹æ£€éªŒæ•°
      chartSeries.value[0].data = items.map((d) => Number(d.processNum) || 0)
      // å®Œæˆï¼šå‡ºåŽ‚æ•°
      chartSeries.value[1].data = items.map((d) => Number(d.factoryNum) || 0)
      // è‰¯å“çŽ‡ï¼šå‡ºåŽ‚æ•°/过程数*100(无单独接口时用此占位)
      chartSeries.value[2].data = items.map((d) => {
        const processNum = Number(d.processNum) || 0
        const factoryNum = Number(d.factoryNum) || 0
        if (processNum <= 0) return 0
        return Math.min(100, Math.round((factoryNum / processNum) * 100))
      })
      // å¼€å·¥
      chartSeries.value[0].data = items.map((d) => Number(d.startQuantity) || 0)
      // å®Œæˆ
      chartSeries.value[1].data = items.map((d) => Number(d.finishQuantity) || 0)
      // è‰¯å“çއ
      chartSeries.value[2].data = items.map((d) => Math.min(100, parseFloat(d.yieldRate) || 0))
    })
    .catch((err) => {
      console.error('获取开工与良品率数据失败:', err)
      console.error('获取工单执行效率分析失败:', err)
    })
}
@@ -159,6 +164,14 @@
  gap: 20px;
}
.filters-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,306 @@
<template>
  <div class="carousel-cards">
    <button
      v-if="canScrollLeft"
      class="nav-button nav-button-left"
      @click="scrollLeftFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="左箭头" />
    </button>
    <div
      class="cards-container"
      :style="{ '--visible-count': visibleCount }"
      ref="cardsContainerRef"
    >
      <div
        v-for="(item, index) in items"
        :key="index"
        class="card-item"
      >
        <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div>
        <div class="card-title">
          <div class="card-label">{{ item.label }}</div>
          <div class="card-value">
            <span class="value-number">{{ item.value }}</span>
            <span class="value-unit">{{ item.unit }}</span>
          </div>
          <div v-if="item.rate ?? item.ratio ?? item.percent" class="card-rate">
            <span class="rate-value">{{ item.rate ?? item.ratio ?? item.percent }}%</span>
          </div>
        </div>
      </div>
    </div>
    <button
      v-if="canScrollRight"
      class="nav-button nav-button-right"
      @click="scrollRightFn"
    >
      <img src="@/assets/BI/jiantou.png" alt="右箭头" />
    </button>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
const props = defineProps({
  items: {
    type: Array,
    default: () => [],
    validator: (value) => {
      return value.every(item =>
        item && typeof item.label !== 'undefined' &&
        typeof item.value !== 'undefined' &&
        typeof item.unit !== 'undefined'
      )
    }
  },
  visibleCount: {
    type: Number,
    default: 3
  }
})
const cardsContainerRef = ref(null)
const currentScrollLeft = ref(0)
const maxScrollLeft = ref(0)
// æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘左滚动
const canScrollLeft = computed(() => {
  return currentScrollLeft.value > 0
})
// æ£€æŸ¥æ˜¯å¦å¯ä»¥å‘右滚动
const canScrollRight = computed(() => {
  return currentScrollLeft.value < maxScrollLeft.value
})
// æ›´æ–°æ»šåŠ¨çŠ¶æ€
const updateScrollState = () => {
  const container = cardsContainerRef.value
  if (!container) return
  currentScrollLeft.value = container.scrollLeft
  maxScrollLeft.value = container.scrollWidth - container.clientWidth
}
// å‘左滚动
const scrollLeftFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: -scrollDistance,
    behavior: 'smooth'
  })
  // å»¶è¿Ÿæ›´æ–°çŠ¶æ€ï¼Œç­‰å¾…æ»šåŠ¨åŠ¨ç”»å®Œæˆ
  setTimeout(() => {
    updateScrollState()
  }, 300)
}
// å‘右滚动
const scrollRightFn = () => {
  const container = cardsContainerRef.value
  if (!container) return
  const scrollItems = Array.from(container.querySelectorAll('.card-item'))
  if (scrollItems.length === 0) return
  const itemWidth = scrollItems[0]?.offsetWidth || 0
  const gap = 12
  const scrollDistance = itemWidth + gap
  container.scrollBy({
    left: scrollDistance,
    behavior: 'smooth'
  })
  // å»¶è¿Ÿæ›´æ–°çŠ¶æ€ï¼Œç­‰å¾…æ»šåŠ¨åŠ¨ç”»å®Œæˆ
  setTimeout(() => {
    updateScrollState()
  }, 300)
}
// ç›‘听 items å˜åŒ–,更新滚动状态
watch(() => props.items, () => {
  nextTick(() => {
    updateScrollState()
  })
}, { deep: true })
onMounted(() => {
  nextTick(() => {
    updateScrollState()
    // ç›‘听滚动事件
    const container = cardsContainerRef.value
    if (container) {
      container.addEventListener('scroll', updateScrollState)
    }
  })
})
onBeforeUnmount(() => {
  // æ¸…理滚动事件监听器
  const container = cardsContainerRef.value
  if (container) {
    container.removeEventListener('scroll', updateScrollState)
  }
})
</script>
<style scoped>
.carousel-cards {
  width: 100%;
  overflow: hidden;
  position: relative;
  display: flex;
  align-items: center;
}
.cards-container {
  display: flex;
  gap: 12px;
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none; /* Firefox */
  -ms-overflow-style: none; /* IE and Edge */
  padding-bottom: 4px;
  scroll-behavior: smooth;
}
.cards-container::-webkit-scrollbar {
  display: none; /* Chrome, Safari, Opera */
}
.nav-button {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 32px;
  height: 32px;
  background: rgba(26, 88, 176, 0.6);
  border: 1px solid rgba(26, 88, 176, 0.8);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 10;
  transition: all 0.3s ease;
  padding: 0;
}
.nav-button:hover {
  background: rgba(26, 88, 176, 0.8);
  transform: translateY(-50%) scale(1.1);
}
.nav-button-left {
  left: -16px;
}
.nav-button-left img {
  width: 16px;
  height: 16px;
  transform: rotate(180deg);
}
.nav-button-right {
  right: -16px;
}
.nav-button-right img {
  width: 16px;
  height: 16px;
}
.card-item {
  flex: 0 0 calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
  display: flex;
  align-items: center;
  background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
  border-radius: 8px 8px 8px 8px;
  padding: 12px 16px;
  transition: all 0.3s ease;
}
.card-item:hover {
  transform: translateY(-2px);
}
.card-icon {
  width: 80px;
  height: 60px;
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  flex-shrink: 0;
  margin-right: 12px;
}
.card-title {
  display: flex;
  align-items: flex-start;
  flex-direction: column;
  flex: 1;
}
.card-label {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  margin-bottom: 4px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  width: 100%;
}
.card-value {
  display: flex;
  align-items: baseline;
  gap: 4px;
}
.card-rate {
  margin-top: 4px;
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 400;
  font-size: 12px;
  color: rgba(255, 255, 255, 0.85);
}
.rate-label {
  opacity: 0.85;
}
.rate-value {
  font-weight: 500;
}
.value-number {
  font-weight: 400;
  font-size: 14px;
  color: #FFFFFF;
  line-height: 1;
}
.value-unit {
  font-size: 14px;
  color: #FFFFFF;
  font-weight: 400;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,94 @@
<template>
  <el-radio-group
    v-model="currentValue"
    class="date-type-switch"
    @change="handleChange"
  >
    <el-radio-button :label="1">周</el-radio-button>
    <el-radio-button :label="2">月</el-radio-button>
    <el-radio-button :label="3">季度</el-radio-button>
  </el-radio-group>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"周"
  },
})
const emit = defineEmits(['update:modelValue', 'change'])
const currentValue = ref(props.modelValue)
// ç›‘听外部值变化
watch(
  () => props.modelValue,
  (newVal) => {
    currentValue.value = newVal
  }
)
// å¤„理值变化
const handleChange = (value) => {
  emit('update:modelValue', value)
  emit('change', value)
}
</script>
<style scoped>
.date-type-switch {
  display: inline-flex;
}
/* æœªé€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
}
/* ç¬¬ä¸€ä¸ªæŒ‰é’®å·¦ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
}
/* æœ€åŽä¸€ä¸ªæŒ‰é’®å³ä¾§åœ†è§’ */
.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
/* æŒ‰é’®ä¹‹é—´çš„分隔线 */
.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
}
/* é€‰ä¸­çŠ¶æ€çš„æ ·å¼ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  box-shadow: none;
}
/* æ‚¬åœæ•ˆæžœ */
.date-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
}
/* é€‰ä¸­çŠ¶æ€æ‚¬åœ */
.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  color: #ffffff;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
<template>
  <div class="panel-header">
    <span class="panel-title">{{ title }}</span>
  </div>
</template>
<script setup>
defineProps({
  title: {
    type: String,
    required: true,
    default: ''
  }
})
</script>
<style scoped>
.panel-header {
  background-image: url("@/assets/BI/kehuhetongback@2x.png");
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
}
.panel-title {
  width: 100%;
  font-weight: 500;
  font-size: 16px;
  color: #D9ECFF;
  padding-left: 46px;
  line-height: 36px;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
<template>
  <el-radio-group
    v-model="currentValue"
    class="product-type-switch"
    @change="handleChange"
  >
    <el-radio-button :label="1">原材料</el-radio-button>
    <el-radio-button :label="3">半成品</el-radio-button>
    <el-radio-button :label="2">成品</el-radio-button>
  </el-radio-group>
</template>
<script setup>
import { ref, watch } from 'vue'
const props = defineProps({
  modelValue: {
    type: Number,
    default: 1, // é»˜è®¤é€‰ä¸­"原材料"
  },
})
const emit = defineEmits(['update:modelValue', 'change'])
const currentValue = ref(props.modelValue)
watch(
  () => props.modelValue,
  (newVal) => {
    currentValue.value = newVal
  }
)
const handleChange = (value) => {
  emit('update:modelValue', value)
  emit('change', value)
}
</script>
<style scoped>
.product-type-switch {
  display: inline-flex;
}
.product-type-switch :deep(.el-radio-button__inner) {
  background-color: rgba(26, 88, 176, 0.3);
  color: rgba(184, 200, 224, 0.8);
  border-color: rgba(255, 255, 255, 0.2);
  border-radius: 0;
  padding: 6px 20px;
  font-size: 14px;
  transition: all 0.3s;
}
.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
  border-top-left-radius: 4px;
  border-bottom-left-radius: 4px;
}
.product-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
  border-top-right-radius: 4px;
  border-bottom-right-radius: 4px;
}
.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
  border-right: 1px solid rgba(255, 255, 255, 0.2);
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
  background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
  color: #ffffff;
  border-color: rgba(51, 120, 255, 0.8);
  box-shadow: none;
}
.product-type-switch :deep(.el-radio-button__inner:hover) {
  color: rgba(184, 200, 224, 1);
  border-color: rgba(255, 255, 255, 0.3);
}
.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
  background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
  color: #ffffff;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,254 @@
<template>
  <div>
    <div class="chart-header">
      <PanelHeader title="完成检验数" />
      <div class="warn-range" @click="handleRangeClick">近7天</div>
    </div>
    <div class="main-panel panel-item-customers">
      <Echarts
          ref="chart"
          :chartStyle="chartStyle"
          :grid="grid"
          :legend="barLegend"
          :series="chartSeries"
          :tooltip="tooltip"
          :xAxis="xAxis1"
          :yAxis="yAxis1"
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
          style="height: 260px"
      />
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { completedInspectionCount } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
const chartStyle = {
  width: '100%',
  height: '135%',
}
const grid = { left: '8%', right: '8%', bottom: '8%', top: '15%', containLabel: true }
const barLegend = {
  show: true,
  top: '5%',
  left: 'center',
  textStyle: { color: '#B8C8E0', fontSize: 14 },
  itemGap: 30,
  data: ['合格', '不合格', '合格率'],
}
// æŸ±çŠ¶å›¾ï¼šåˆæ ¼ï¼ˆé»„è‰²ï¼‰ã€ä¸åˆæ ¼ï¼ˆç´«è‰²ï¼‰ï¼›æŠ˜çº¿å›¾ï¼šåˆæ ¼çŽ‡ï¼ˆè“è‰²ï¼‰
const chartSeries = ref([
  {
    name: '合格',
    type: 'bar',
    barWidth: 20,
    barGap: '20%',
    yAxisIndex: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 0, color: 'rgba(255, 215, 0, 1)' }, // é‡‘黄色顶部
          { offset: 1, color: 'rgba(255, 215, 0, 0.5)' }, // åŠé€æ˜Žåº•部
        ],
      },
    },
    data: [],
  },
  {
    name: '不合格',
    type: 'bar',
    barGap: '20%',
    barWidth: 20,
    yAxisIndex: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 0, color: 'rgba(144, 97, 248, 1)' }, // ç´«è‰²é¡¶éƒ¨
          { offset: 1, color: 'rgba(144, 97, 248, 0.6)' }, // åŠé€æ˜Žåº•部
        ],
      },
    },
    data: [],
  },
  {
    name: '合格率',
    type: 'line',
    yAxisIndex: 1,
    smooth: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: {
      color: 'rgba(78, 228, 255, 1)', // é’色
      width: 2,
    },
    itemStyle: {
      color: 'rgba(78, 228, 255, 1)',
      borderWidth: 2,
      borderColor: '#fff',
    },
    emphasis: {
      focus: 'series',
      itemStyle: {
        shadowBlur: 10,
        shadowColor: 'rgba(78, 228, 255, 0.8)',
      },
    },
    data: [],
  },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  backgroundColor: 'rgba(0, 0, 0, 0.8)',
  borderColor: 'rgba(78, 228, 255, 0.5)',
  borderWidth: 1,
  textStyle: { color: '#B8C8E0' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      let unit = ''
      if (item.seriesName === '合格率') {
        unit = '%'
      } else {
        unit = 'ä»¶'
      }
      result += `<div style="margin: 4px 0;">${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
    })
    return result
  },
}
const xAxis1 = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    axisLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
    data: [],
  },
])
const yAxis1 = [
  {
    type: 'value',
    name: '单位: ä»¶',
    nameLocation: 'start',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 10] },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    axisLine: { show: false },
    splitLine: {
      show: true,
      lineStyle: { color: 'rgba(184, 200, 224, 0.2)', type: 'dashed' },
    },
  },
  {
    type: 'value',
    name: '单位: %',
    nameLocation: 'end',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 10] },
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', fontSize: 12, formatter: '{value}' },
    axisLine: { show: false },
    splitLine: {
      show: true,
      lineStyle: { color: 'rgba(184, 200, 224, 0.2)', type: 'dashed' },
    },
  },
]
const fetchData = () => {
  completedInspectionCount()
    .then((res) => {
      if (res?.code === 200 && Array.isArray(res?.data)) {
        const items = res.data
        // æ›´æ–°X轴日期数据
        xAxis1.value[0].data = items.map((d) => d.dateStr || '')
        // æ›´æ–°åˆæ ¼æ•°ï¼ˆé»„色柱状图)
        chartSeries.value[0].data = items.map((d) => Number(d.qualifiedCount) || 0)
        // æ›´æ–°ä¸åˆæ ¼æ•°ï¼ˆç´«è‰²æŸ±çŠ¶å›¾ï¼‰
        chartSeries.value[1].data = items.map((d) => Number(d.unqualifiedCount) || 0)
        // æ›´æ–°åˆæ ¼çŽ‡ï¼ˆè“è‰²æŠ˜çº¿å›¾ï¼‰
        chartSeries.value[2].data = items.map((d) => Number(d.passRate) || 0)
      }
    })
    .catch((err) => {
      console.error('获取完成检验数数据失败:', err)
    })
}
const handleRangeClick = () => {
  // å…ˆæŒ‰æˆªå›¾åšé™æ€"近7天",后续有真实筛选需求再接入
  fetchData()
}
onMounted(() => {
  fetchData()
})
</script>
<style scoped>
.chart-header {
  position: relative;
  display: flex;
  align-items: center;
}
.warn-range {
  position: absolute;
  right: 0;
  top: 0;
  height: 32px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  color: #ffffff;
  font-weight: 600;
  font-size: 14px;
  background: linear-gradient(180deg, rgba(51, 120, 255, 1) 0%, rgba(0, 164, 237, 1) 100%);
  border: 1px solid rgba(78, 228, 255, 0.25);
  cursor: pointer;
  z-index: 10;
}
.warn-range:hover {
  background: linear-gradient(180deg, rgba(51, 140, 255, 1) 0%, rgba(0, 184, 237, 1) 100%);
}
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
  position: relative;
  background: radial-gradient(circle at 50% 50%, rgba(78, 228, 255, 0.05) 0%, rgba(0, 0, 0, 0) 70%);
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/center-center.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,355 @@
<template>
  <div>
    <div class="warn-panel">
      <div class="warn-header">
        <div class="warn-header-left">
          <div class="warn-badge"></div>
          <span class="warn-title">不合格预警</span>
        </div>
        <div class="warn-range" @click="handleRangeClick">近7天</div>
      </div>
      <div class="warn-body">
        <div class="warn-list" role="list">
          <div v-for="item in warnings" :key="item.id" class="warn-item" role="listitem" @click="openWarning(item)">
            <div class="warn-tag" :class="tagClass(item.type)">{{ item.parentProductTitle }}-{{ item.productTitle }}
            </div>
            <div class="warn-text" :title="item.title">{{ item.title }}</div>
            <div class="warn-action" @click.stop="openWarning(item)">查看</div>
            <div class="warn-date">{{ item.date }}</div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { computed, getCurrentInstance, ref, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import { nonComplianceWarning } from '@/api/viewIndex.js'
const { proxy } = getCurrentInstance() || {}
const warnings = ref([])
// å æ¯”数据
const ratios = ref({
  rawMaterialRatio: 0,
  semiFinishedProductRatio: 0,
  finishedProductRatio: 0,
})
const TAG_COLORS = {
  raw: '#7C4DFF',
  final: '#F5A000',
  semi: '#FF66CC',
}
const tagClass = (type) => {
  if (type === 'raw') return 'tag-raw'
  if (type === 'final') return 'tag-final'
  return 'tag-semi'
}
// æ ¹æ®productTitle映射类型
const mapProductTitleToType = (productTitle) => {
  if (productTitle === '原材料') return 'raw'
  if (productTitle === '半成品') return 'semi'
  if (productTitle === '成品') return 'final'
  return 'raw' // é»˜è®¤å€¼
}
const pieChartStyle = { width: '100%', height: '100%' }
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const pieTooltip = {
  trigger: 'item',
  formatter: (p) => `${p.name}:${p.value}%`,
}
const pieData = computed(() => {
  return [
    { name: '原材料', value: ratios.value.rawMaterialRatio, itemStyle: { color: TAG_COLORS.raw } },
    { name: '半成品', value: ratios.value.semiFinishedProductRatio, itemStyle: { color: TAG_COLORS.semi } },
    { name: '成品', value: ratios.value.finishedProductRatio, itemStyle: { color: TAG_COLORS.final } },
  ]
})
const pieSeries = computed(() => {
  return [
    {
      type: 'pie',
      radius: ['0%', '68%'],
      center: ['50%', '50%'],
      startAngle: 90,
      clockwise: true,
      avoidLabelOverlap: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: {
        borderColor: '#071a3a',
        borderWidth: 4,
        shadowBlur: 14,
        shadowColor: 'rgba(0, 0, 0, 0.35)',
      },
      data: pieData.value,
    },
    {
      // å†…圈暗环,增强层次
      type: 'pie',
      radius: ['70%', '74%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: { color: 'rgba(78, 228, 255, 0.12)' },
      data: [1],
    },
  ]
})
const fetchWarnings = async () => {
  try {
    const res = await nonComplianceWarning()
    if (res?.code === 200 && res?.data) {
      const data = res.data
      // æ›´æ–°å æ¯”数据
      ratios.value = {
        rawMaterialRatio: data.rawMaterialRatio ?? 0,
        semiFinishedProductRatio: data.semiFinishedProductRatio ?? 0,
        finishedProductRatio: data.finishedProductRatio ?? 0,
      }
      // æ›´æ–°è­¦å‘Šåˆ—表
      const children = data.children || []
      warnings.value = children.map((item, idx) => {
        const type = mapProductTitleToType(item.parentProductTitle)
        const date = item.date ? item.date.replace(/-/g, '.') : ''
        return {
          id: item.id ?? `warning-${idx}`,
          type,
          parentProductTitle: item.parentProductTitle || '原材料',
          productTitle: item.productTitle || '原材料',
          title: item.description || '不合格预警',
          date,
        }
      })
    }
  } catch (e) {
    // æŽ¥å£å¤±è´¥åˆ™ä¿æŒç©ºæ•°æ®
    console.error('获取不合格预警失败:', e)
  }
}
const openWarning = (item) => {
  const title = `【${item.parentProductTitle}-${item.productTitle}】${item.title}`
  if (proxy?.$modal?.alert) {
    proxy.$modal.alert(title)
    return
  }
  // å…œåº•:没有全局 modal æ—¶ç”¨ console
  console.log('warning:', { ...item })
}
const handleRangeClick = () => {
  // å…ˆæŒ‰æˆªå›¾åšé™æ€â€œè¿‘7天”,后续有真实筛选需求再接入
}
onMounted(() => {
  fetchWarnings()
})
</script>
<style scoped>
.warn-panel {
  border: 1px solid #1a58b0;
  padding: 0 18px 18px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  height: 100%;
  box-sizing: border-box;
}
.warn-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid;
  border-image: linear-gradient(270deg,
      rgba(0, 126, 255, 0) 0%,
      rgba(0, 126, 255, 0.4549) 35%,
      #007eff 78%,
      #007eff 100%) 1;
  padding: 10px 0 6px;
}
.warn-header-left {
  display: flex;
  align-items: center;
  gap: 10px;
}
.warn-badge {
  width: 18px;
  height: 18px;
  background: linear-gradient(180deg, #2aa8ff 0%, #4ee4ff 100%);
  clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
  box-shadow: 0 0 12px rgba(78, 228, 255, 0.25);
}
.warn-title {
  font-weight: 600;
  font-size: 18px;
  background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  line-height: 24px;
}
.warn-range {
  height: 32px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 4px;
  color: #ffffff;
  font-weight: 600;
  background: linear-gradient(180deg, rgba(51, 120, 255, 1) 0%, rgba(0, 164, 237, 1) 100%);
  border: 1px solid rgba(78, 228, 255, 0.25);
  cursor: pointer;
}
.warn-body {
  display: grid;
  gap: 18px;
  align-items: stretch;
  min-height: 260px;
}
.warn-list {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding-top: 6px;
}
.warn-item {
  display: grid;
  grid-template-columns: 130px 1fr auto 110px;
  align-items: center;
  gap: 12px;
  color: #b8c8e0;
  font-size: 14px;
  line-height: 1.2;
  padding: 8px 0;
  border-radius: 4px;
  transition: background-color 0.2s, color 0.2s;
}
.warn-item:hover {
  color: #ff4d4f;
  background-color: rgba(255, 77, 79, 0.06);
}
.warn-item:hover .warn-text {
  color: #ff4d4f;
}
.warn-tag {
  height: 28px;
  padding: 0 10px;
  border-radius: 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 700;
  color: #ffffff;
}
.tag-raw {
  background: #7c4dff;
}
.tag-final {
  background: #f5a000;
}
.tag-semi {
  background: #ff66cc;
}
.warn-text {
  color: #e8f1ff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.warn-action {
  color: #ff4d4f;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
.warn-date {
  color: rgba(184, 200, 224, 0.75);
  white-space: nowrap;
}
.warn-chart {
  display: flex;
  align-items: center;
  justify-content: center;
}
.chart-frame {
  width: 100%;
  height: 260px;
  border: 2px dashed rgba(184, 200, 224, 0.35);
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 50%, rgba(78, 228, 255, 0.08) 0%, rgba(0, 0, 0, 0) 65%);
}
/* å¤–圈刻度环 */
.chart-frame::before {
  content: '';
  position: absolute;
  width: 220px;
  height: 220px;
  border-radius: 50%;
  background: repeating-conic-gradient(from 0deg, rgba(78, 228, 255, 0.75) 0 1deg, rgba(78, 228, 255, 0) 1deg 9deg);
  -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%);
  mask: radial-gradient(circle, transparent 62%, #000 63%);
  opacity: 0.5;
  pointer-events: none;
}
/* åå­—辅助线 */
.chart-frame::after {
  content: '';
  position: absolute;
  width: 240px;
  height: 240px;
  background:
    linear-gradient(to right, rgba(78, 228, 255, 0) 0%, rgba(78, 228, 255, 0.55) 50%, rgba(78, 228, 255, 0) 100%),
    linear-gradient(to bottom, rgba(78, 228, 255, 0) 0%, rgba(78, 228, 255, 0.55) 50%, rgba(78, 228, 255, 0) 100%);
  background-size: 100% 1px, 1px 100%;
  background-position: center, center;
  background-repeat: no-repeat;
  opacity: 0.35;
  pointer-events: none;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/center-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,147 @@
<template>
  <div>
    <!-- é¡¶éƒ¨ç»Ÿè®¡å¡ç‰‡ -->
    <div class="stats-cards">
      <div v-for="item in statItems" :key="item.name" class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">{{ item.name }}</span>
          <span class="card-value">{{ item.value }}</span>
          <div class="card-compare" :class="compareClass(Number(item.rate))">
            <span>同比</span>
            <span class="compare-value">{{ formatPercent(item.rate) }}</span>
            <span class="compare-icon">{{ Number(item.rate) >= 0 ? '↑' : '↓' }}</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { qualityInspectionCount } from '@/api/viewIndex.js'
const statItems = ref([])
const formatPercent = (val) => {
  const num = Number(val) || 0
  return `${num.toFixed(2)}%`
}
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
const fetchData = () => {
  qualityInspectionCount()
    .then((res) => {
      if (res.code === 200 && res.data) {
        const data = res.data
        statItems.value = [
          {
            name: '总检验数',
            value: data.totalCount ?? 0,
            rate: data.totalCountGrowthRate ?? 0,
          },
          {
            name: '今日待完成数',
            value: data.todayPendingCount ?? 0,
            rate: data.todayPendingCountGrowthRate ?? 0,
          },
          {
            name: '今日已完成数',
            value: data.todayCompletedCount ?? 0,
            rate: data.todayCompletedCountGrowthRate ?? 0,
          },
        ]
      }
    })
    .catch((err) => {
      console.error('获取质量检验统计失败:', err)
    })
}
onMounted(() => {
  fetchData()
})
</script>
<style scoped>
.stats-cards {
  display: flex;
  gap: 30px;
}
.stat-card {
  flex: 1;
  display: flex;
  align-items: center;
  background-image: url('@/assets/BI/border@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  height: 142px;
}
.card-icon {
  width: 100px;
  height: 100px;
  margin: 20px 20px 0 10px;
}
.card-content {
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.card-value {
  font-weight: 500;
  font-size: 40px;
  background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.card-label {
  font-weight: 400;
  font-size: 16px;
  color: rgba(208, 231, 255, 0.7);
}
.card-compare {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
}
.card-compare>span:first-child {
  font-size: 13px;
  opacity: 0.8;
}
.compare-value {
  font-weight: 600;
}
.compare-icon {
  font-size: 14px;
  position: relative;
  top: -1px;
  /* è½»å¾®ä¸Šç§»ï¼Œè®©ç®­å¤´ä¸Žæ–‡å­—垂直居中对齐 */
}
.compare-up .compare-value,
.compare-up .compare-icon {
  color: #00c853;
}
.compare-down .compare-value,
.compare-down .compare-icon {
  color: #ff5252;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/left-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,170 @@
<template>
  <div>
    <PanelHeader title="在制品统计分析" />
    <div class="main-panel panel-item-customers">
      <CarouselCards :items="cardItems" :visible-count="3" />
      <div class="chart-wrapper">
        <Echarts
          ref="chart"
          :chartStyle="chartStyle"
          :grid="grid"
          :legend="workInProcessBarLegend"
          :series="workInProcessBarSeries"
          :tooltip="tooltip"
          :xAxis="workInProcessXAxis"
          :yAxis="workInProcessYAxis"
          :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
          style="height: 100%"
        />
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import CarouselCards from './CarouselCards.vue'
import { getWorkInProcessTurnover } from '@/api/viewIndex.js'
// åœ¨åˆ¶å“å‘¨è½¬ç»Ÿè®¡å¯¹è±¡
const workInProcessStatistics = ref({
  totalQuantity: 0,
  avgTurnoverDays: 0,
  turnoverEfficiency: 0,
})
// è½®æ’­å¡ç‰‡æ•°æ®ï¼ˆç”± workInProcessStatistics åŒæ­¥ï¼‰
const cardItems = ref([])
const chartStyle = {
  width: '100%',
  height: '100%',
}
const grid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  containLabel: true,
}
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  formatter: function (params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`
    })
    return result
  },
}
// åœ¨åˆ¶å“å·¥åºæŸ±çŠ¶å›¾é…ç½®
const workInProcessXAxis = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0' },
    data: [],
  },
])
const workInProcessYAxis = [
  {
    type: 'value',
    axisLabel: { color: '#B8C8E0' },
    name: '',
  },
]
const workInProcessBarLegend = {
  show: false,
  textStyle: { color: '#B8C8E0' },
  data: [],
}
const workInProcessBarSeries = ref([
  {
    name: '在制品数量',
    type: 'bar',
    barWidth: 25,
    barGap: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 0,
        x2: 0,
        y2: 1,
        colorStops: [
        { offset: 1, color: 'rgba(0,164,237,0)' },
        { offset: 0, color: '#4EE4FF' },
        ],
      },
    },
    label: {
      show: true,
      position: 'top',
      color: '#B8C8E0',
    },
    data: [],
  },
])
const workInProcessTurnoverInfo = () => {
  getWorkInProcessTurnover()
    .then((res) => {
      if (!res || !res.data) return
      const stats = {
        totalQuantity: res.data.totalOrderCount || 0,
        avgTurnoverDays: res.data.averageTurnoverDays || 0,
        turnoverEfficiency: res.data.turnoverEfficiency || 0,
      }
      workInProcessStatistics.value = stats
      cardItems.value = [
        { label: '总在制数量', value: stats.totalQuantity, unit: 'ä»¶' },
        { label: '平均周转天数', value: stats.avgTurnoverDays, unit: '天' },
        { label: '周转效率', value: stats.turnoverEfficiency, unit: '%' },
      ]
      if (res.data.processDetails && Array.isArray(res.data.processDetails)) {
        workInProcessXAxis.value[0].data = res.data.processDetails
      } else {
        workInProcessXAxis.value[0].data = []
      }
      if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) {
        workInProcessBarSeries.value[0].data = res.data.processQuantityDetails
      } else {
        workInProcessBarSeries.value[0].data = []
      }
    })
    .catch((err) => {
      console.error('获取在制品周转统计失败:', err)
    })
}
onMounted(() => {
  workInProcessTurnoverInfo()
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
}
.chart-wrapper {
  height: 70%;
  flex: 1;
  min-height: 200px;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,448 @@
<template>
  <div>
    <PanelHeader title="质量指标合格分析" />
    <div class="main-panel panel-item-customers">
      <div v-for="section in sections" :key="section.key" class="inspect-block">
        <div class="filters-row">
          <div class="filters-row-left">
            <span></span>
            <p>{{ section.title }}</p>
          </div>
          <DateTypeSwitch v-model="section.dateType" @change="(v) => handleDateTypeChange(section.key, v)" />
        </div>
        <div class="inspect-body">
          <div class="ring">
            <Echarts :chartStyle="ringChartStyle" :series="buildRingSeries(section)" :tooltip="ringTooltip"
              :legend="{ show: false }" :options="ringOptions" />
          </div>
          <div class="stats">
            <div class="stat-row">
              <div class="stat-left">
                <span class="dot dot-qualified"></span>
                <span class="stat-label">合格数</span>
              </div>
              <div class="stat-right">
                <span class="stat-value">{{ section.qualifiedCount }}</span>
                <span class="stat-percent">{{ formatPercent(section.qualifiedRate) }}</span>
              </div>
            </div>
            <div class="stat-row">
              <div class="stat-left">
                <span class="dot dot-unqualified"></span>
                <span class="stat-label">不合格数</span>
              </div>
              <div class="stat-right">
                <span class="stat-value">{{ section.unqualifiedCount }}</span>
                <span class="stat-percent">{{ formatPercent(section.unqualifiedRate) }}</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { reactive, onMounted } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
import { rawMaterialDetection, processDetection, factoryDetection } from '@/api/viewIndex.js'
const QUALIFIED_COLOR = '#4EE4FF'
const UNQUALIFIED_COLOR = '#3378FF'
const TRACK_COLOR = 'rgba(78, 228, 255, 0.12)'
const apiMap = {
  raw: rawMaterialDetection,
  process: processDetection,
  final: factoryDetection,
}
const fetchSectionData = async (section) => {
  const api = apiMap[section.key]
  if (!api) return
  try {
    const res = await api({
      type: section.dateType,
    })
    if (res?.code === 200 && res?.data) {
      const data = res.data
      section.qualifiedCount = Number(data.qualifiedCount || 0)
      section.unqualifiedCount = Number(data.unqualifiedCount || 0)
      section.qualifiedRate = Number(data.qualifiedRate || 0)
      section.unqualifiedRate = Number(data.unqualifiedRate || 0)
    }
  } catch (err) {
    console.error(`${section.key} æŽ¥å£è¯·æ±‚失败`, err)
  }
}
const sections = reactive([
  {
    key: 'raw',
    title: '原材料检测',
    dateType: 1,
    qualifiedCount: 0,
    unqualifiedCount: 0,
    qualifiedRate: 0,
    unqualifiedRate: 0,
  },
  {
    key: 'process',
    title: '过程检测',
    dateType: 1,
    qualifiedCount: 0,
    unqualifiedCount: 0,
    qualifiedRate: 0,
    unqualifiedRate: 0,
  },
  {
    key: 'final',
    title: '成品出厂检测',
    dateType: 1,
    qualifiedCount: 0,
    unqualifiedCount: 0,
    qualifiedRate: 0,
    unqualifiedRate: 0,
  },
])
const ringChartStyle = {
  width: '110px',
  height: '110px',
}
const ringOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' },
}
const ringTooltip = {
  show: false,
}
const calcRates = (qualifiedCount, unqualifiedCount) => {
  const total = Number(qualifiedCount || 0) + Number(unqualifiedCount || 0)
  if (total <= 0) return { qualifiedRate: 0, unqualifiedRate: 0 }
  const qualifiedRate = Math.round((Number(qualifiedCount || 0) / total) * 100)
  const unqualifiedRate = Math.max(0, 100 - qualifiedRate)
  return { qualifiedRate, unqualifiedRate }
}
const formatPercent = (v) => `${Number(v || 0)}%`
const buildRingSeries = (section) => {
  const qualified = Number(section.qualifiedCount || 0)
  const unqualified = Number(section.unqualifiedCount || 0)
  const total = qualified + unqualified
  return [
    {
      type: 'pie',
      radius: ['68%', '82%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: { color: TRACK_COLOR },
      data: [1],
    },
    {
      name: section.title,
      type: 'pie',
      radius: ['68%', '82%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      labelLine: { show: false },
      startAngle: 90,
      clockwise: true,
      minAngle: total > 0 ? 8 : 0,
      itemStyle: {
        borderColor: 'rgba(10, 28, 58, 0.95)',
        borderWidth: 2,
      },
      data: [
        {
          value: qualified,
          name: '合格数',
          itemStyle: {
            color: QUALIFIED_COLOR,
            shadowBlur: 16,
            shadowColor: 'rgba(78, 228, 255, 0.45)',
          },
        },
        {
          value: unqualified,
          name: '不合格数',
          itemStyle: {
            color: UNQUALIFIED_COLOR,
            shadowBlur: 10,
            shadowColor: 'rgba(51, 120, 255, 0.35)',
          },
        },
      ],
    },
    {
      type: 'pie',
      radius: ['52%', '56%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: { color: 'rgba(0, 127, 255, 0.22)' },
      data: [1],
    },
  ]
}
const handleDateTypeChange = (key, dateType) => {
  const section = sections.find((s) => s.key === key)
  if (!section) return
  section.dateType = dateType
  // åˆ‡æ¢æ—¥æœŸç±»åž‹æ—¶é‡æ–°èŽ·å–æ•°æ®
  fetchSectionData(section)
}
// ç»„件挂载时获取所有section的数据
onMounted(() => {
  sections.forEach((section) => {
    fetchSectionData(section)
  })
})
</script>
<style scoped>
.main-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  gap: 0;
}
.filters-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
  .filters-row-left {
    width: 50%;
    color: white;
    /* ç”¨flex替代float,让子元素对齐更稳定 */
    display: flex;
    align-items: center;
    span {
      /* æ ¸å¿ƒï¼šçˆ¶çº§ç›¸å¯¹å®šä½ï¼Œä½œä¸ºä¼ªå…ƒç´ åŸºå‡† */
      position: relative;
      display: inline-block;
      /* ç»™ä¼ªå…ƒç´ å’Œæ–‡å­—留空间 */
      padding-left: 22px;
      /* æ–‡å­—垂直居中 */
      line-height: 23px;
      margin-right: 8px;
      &::after {
        content: '';
        display: inline-block;
        width: 16px;
        height: 16px;
        clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
        background: #217AFF;
        position: absolute;
        top: 50%;
        left: 0;
        transform: translateY(-50%);
        /* ç¡®ä¿è±å½¢åœ¨æ¸å˜å—上方 */
        z-index: 1;
      }
      &::before {
        content: '';
        display: inline-block;
        width: 18px;
        height: 7px;
        border-radius: 8px;
        background: linear-gradient(360deg, rgba(33, 133, 255, 0.4) 0%, rgba(33, 221, 255, 0) 100%);
        position: absolute;
        top: 50%;
        left: -1px;
        /* ç²¾å‡†è´´åœ¨è±å½¢æ­£ä¸‹æ–¹ */
        transform: translateY(calc(0% + 8px));
        z-index: 0;
      }
    }
    p {
      width: 100px;
      height: 23px;
      /* æ¸å˜èµ·å§‹è‰²å’Œè±å½¢ç»Ÿä¸€ï¼Œæ›´åè°ƒ */
      background: linear-gradient(90deg, #217AFF 0%, rgba(33, 221, 255, 0) 100%);
      /* ç²¾å‡†åž‚直居中 */
      line-height: 26px;
      text-align: center;
      color: white;
      /* ç”¨é«˜åº¦çš„一半做圆角,确保左边是完美半圆 */
      border-radius: 12px 0 0 12px;
      /* å¯é€‰ï¼šåŠ ä¸€ç‚¹å·¦å†…è¾¹è·ï¼Œè®©æ–‡å­—ä¸è´´è¾¹ */
      padding-left: 4px;
    }
  }
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 14px 18px;
  width: 100%;
  height: 960px;
  box-sizing: border-box;
}
.inspect-block {
  flex: 1 1 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 8px 0;
  gap: 6px;
  position: relative;
}
.inspect-block:not(:last-child)::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: linear-gradient(90deg, rgba(33, 122, 255, 0) 0%, rgba(33, 122, 255, 0.55) 50%, rgba(33, 122, 255, 0) 100%);
  pointer-events: none;
}
.inspect-body {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  justify-content: space-around;
  align-items: center;
  gap: 18px;
}
.ring {
  width: 120px;
  height: 120px;
  flex: 0 0 120px;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* å¤–圈刻度(点状环) */
.ring::before {
  content: '';
  position: absolute;
  inset: -8px;
  border-radius: 50%;
  background: repeating-conic-gradient(from 0deg,
      rgba(78, 228, 255, 0.75) 0 1deg,
      rgba(78, 228, 255, 0) 1deg 9deg);
  -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%);
  mask: radial-gradient(circle, transparent 62%, #000 63%);
  opacity: 0.35;
  pointer-events: none;
}
/* æŸ”和发光背景 */
.ring::after {
  content: '';
  position: absolute;
  inset: -20px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(78, 228, 255, 0.18) 0%, rgba(78, 228, 255, 0.06) 40%, rgba(0, 0, 0, 0) 70%);
  filter: blur(0.2px);
  pointer-events: none;
}
.stats {
  width: 240px;
  flex: 0 0 240px;
  display: grid;
  grid-template-rows: 1fr 1fr;
  gap: 10px;
}
.stat-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  height: 100%;
  padding: 10px 14px;
  border-radius: 4px;
  border: 1px solid rgba(78, 228, 255, 0.22);
  background: linear-gradient(90deg, rgba(33, 122, 255, 0.28) 0%, rgba(10, 28, 58, 0.35) 55%, rgba(10, 28, 58, 0.2) 100%);
  box-shadow: inset 0 0 18px rgba(16, 45, 95, 0.25);
}
.stat-left {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: #b8c8e0;
  font-size: 12px;
}
.dot {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
  box-shadow: 0 0 10px rgba(78, 228, 255, 0.25);
}
.dot-qualified {
  background: rgba(184, 200, 224, 0.85);
}
.dot-unqualified {
  background: #4ee4ff;
}
.stat-right {
  display: inline-flex;
  align-items: baseline;
  gap: 14px;
}
.stat-value {
  color: #ffffff;
  font-size: 14px;
  font-weight: 600;
  min-width: 40px;
  text-align: right;
  text-shadow: 0 0 10px rgba(78, 228, 255, 0.15);
}
.stat-percent {
  color: rgba(184, 200, 224, 0.95);
  font-size: 12px;
  min-width: 40px;
  text-align: right;
}
/* è®©åˆ‡æ¢æŒ‰é’®æ›´è´´è¿‘截图(更紧凑) */
:deep(.date-type-switch .el-radio-button__inner) {
  padding: 4px 16px;
  font-size: 12px;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,189 @@
<template>
  <div>
    <PanelHeader title="不合格检品处理分析" />
    <div class="panel-item-customers">
      <div class="pie-chart-wrapper" ref="pieWrapperRef">
        <div class="pie-background" ref="pieBackgroundRef"></div>
        <Echarts ref="chart" :chartStyle="chartStyle" :legend="landLegend" :series="computedSeries"
          :tooltip="landTooltip" :color="landColors" :options="pieOptions" style="height: 100%" class="land-chart" />
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from './PanelHeader.vue'
import { unqualifiedProductProcessingAnalysis } from '@/api/viewIndex.js'
import { useChartBackground } from '@/hooks/useChartBackground.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const chart = ref(null)
//  æ•°æ®åˆ—表
const dataList = ref([])
//  é¢œè‰²åˆ—表
const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
//  label å¯Œæ–‡æœ¬æ ·å¼
const dotRich = landColors.reduce((acc, color, idx) => {
  acc[`dot${idx}`] = {
    width: 8,
    height: 8,
    borderRadius: 8,
    backgroundColor: color,
    align: 'center',
  }
  return acc
}, {})
//  å›¾ä¾‹é…ç½®
const landLegend = ref({
  show: false,
  icon: 'circle',
  data: [],
  right: '8%',
  top: '40%',
  orient: 'vertical',
  textStyle: {
    color: '#fff',
    rich: {
      unit: { color: '#fff', fontSize: 12, padding: [0, 10, 0, 0] },
      text: { width: 60, color: '#fff', fontSize: 12 },
    }
  }
})
//  æç¤ºæ¡†é…ç½®
const landTooltip = {
  trigger: 'item',
  alwaysShowContent: false,
  position: function (pt) {
    return [pt[0], 130]
  },
  formatter: function (params) {
    // ç¡®ä¿ params.data å­˜åœ¨
    if (!params.data) return ''
    const { name, value, rate } = params.data
    return `${name}<br/>数量:${value}个<br/>占比:${rate}%`
  },
}
//  ä½¿ç”¨è®¡ç®—属性处理 Series
const computedSeries = computed(() => {
  return [
    {
      name: '不合格检品处理分析',
      type: 'pie',
      radius: ['35%', '55%'],
      center: ['50%', '50%'],
      label: {
        show: true,
        position: 'outside',
        color: '#fff',
        rich: {
          ...dotRich,
          parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20 },
          child: { fontSize: 12, color: '#fff', lineHeight: 18 },
        },
        formatter: function (params) {
          if (!params.data) return ''
          const dotKey = `dot${params.dataIndex % landColors.length}`
          return `{${dotKey}|} {parent|${params.data.name} (${params.data.value}个)}`
        },
      },
      labelLine: {
        show: true,
        length: 20,
        lineStyle: { color: '#B8C8E0' },
      },
      data: dataList.value,
    },
    {
      // å†…圈装饰
      type: 'pie',
      radius: ['35%', '40%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      itemStyle: { color: 'rgba(0, 127, 255, 0.25)' },
      data: [1],
    },
  ]
})
const chartStyle = { width: '100%', height: '126%' }
const pieOptions = { backgroundColor: 'transparent' }
//  èƒŒæ™¯å¤„理钩子
const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  offsetX: '-51.5%',
  offsetY: '-39%',
  watchData: dataList
})
const loadData = async () => {
  try {
    const res = await unqualifiedProductProcessingAnalysis()
    if (res && res.code === 200) {
      dataList.value = (res.data || []).map((it) => ({
        name: it.name,
        value: Number(it.value || 0),
        rate: it.rate,
      }))
      landLegend.value.data = dataList.value.map((d) => d.name)
      // æ•°æ®æ›´æ–°åŽå¾®è°ƒèƒŒæ™¯
      setTimeout(() => {
        adjustBackgroundPosition()
      }, 100)
    }
  } catch (e) {
    console.error('获取数据失败:', e)
  }
}
onMounted(() => {
  loadData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
<style scoped>
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 420px;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
}
.pie-background {
  position: absolute;
  width: 360px;
  height: 360px;
  background-image: url('@/assets/BI/玫瑰图边框.png');
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
  left: 50%;
  top: 50%;
  transform: translate(-51.5%, -39%);
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/right-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,132 @@
<template>
  <div>
    <PanelHeader title="不合格产品排名" />
    <div class="main-panel panel-item-customers">
      <div class="main-panel-container">
        <div style="color: white" class="main-panel-box" v-for="(item, index) in panelList" :key="index">
          <!-- <div style="flex: 1" class="main-panel-box-left">{{ item.rank }}</div> -->
          <div style="flex: 1" class="main-panel-box-left">{{ item.productName }}</div>
          <div style="flex: 3" class="main-panel-box-right">
            <!-- <div class="main-panel-box-right-title">{{ item.productName }}</div> -->
            <div class="main-panel-box-right-text">
              <span>总数量:{{ item.total }}</span>
              <span>已完成:{{ item.finished }}</span>
              <span>合格率:{{ item.qualifiedRate }}%</span>
            </div>
            <div class="main-panel-box-right-progress">
              <el-progress :percentage="item.percentage" :format="format" />
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { unqualifiedProductRanking } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
const panelList = ref([])
const format = (percentage) => {
  return `${percentage}%`
}
const fetchData = () => {
  unqualifiedProductRanking()
    .then((res) => {
      if (res?.code === 200 && Array.isArray(res?.data)) {
        const data = res.data
        panelList.value = data.map((item, index) => {
          const total = Number(item.totalCount) || 0
          const finished = Number(item.completedCount) || 0
          const passRate = Number(item.passRate) || 0
          return {
            rank: `Top${index + 1}`,
            productName: item.productName || `产品${index + 1}`,
            total: total.toFixed(2),
            finished: finished.toFixed(2),
            qualifiedRate: passRate.toFixed(2),
            percentage: Math.min(100, Math.max(0, passRate)), // ç¡®ä¿ç™¾åˆ†æ¯”在0-100之间
          }
        })
      }
    })
    .catch((err) => {
      console.error('获取工单执行效率分析数据失败:', err)
    })
}
onMounted(() => {
  fetchData()
})
</script>
<style scoped>
.main-panel-box {
  display: flex;
  flex-direction: row;
  align-items: center;
  height: 40px;
  .main-panel-box-left {
    background: red;
    border-radius: 20px;
    text-align: center;
    line-height: 32px;
    margin: 0 20px;
  }
  .main-panel-box-right {
    display: flex;
    flex-direction: column;
    flex: 1;
    .main-panel-box-right-title {
      font-size: 14px;
      font-weight: 600;
      color: #ffffff;
      margin-bottom: 6px;
    }
    .main-panel-box-right-text {
      font-size: 12px;
      display: flex;
      justify-content: space-between;
      padding-right: 60px;
      margin-bottom: 4px;
    }
    .main-panel-box-right-progress {
      :deep(.el-progress__text) {
        color: white !important;
      }
    }
  }
}
.main-panel-container {
  display: flex;
  flex-direction: column;
  gap: 12px;
  height: 100%;
  overflow-y: auto;
}
.main-panel {
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
  overflow: hidden;
}
</style>
src/views/reportAnalysis/qualityAnalysis/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,289 @@
<template>
  <div class="scale-container">
    <div class="data-dashboard" :style="{ transform: `scale(${scaleRatio})` }">
    <!-- å…¨å±æŒ‰é’® - ç§»åŠ¨åˆ°å·¦ä¸Šè§’ -->
    <button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '退出全屏' : '全屏显示'">
      <svg v-if="!isFullscreen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
      </svg>
      <svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
        <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
      </svg>
    </button>
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <div class="dashboard-header">
      <div class="factory-name">质量数据分析</div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
    <div class="dashboard-content">
      <!-- å·¦ä¾§åŒºåŸŸ -->
      <div class="left-panel">
        <LeftTop />
      </div>
      <!-- ä¸­é—´åŒºåŸŸ -->
      <div class="center-panel">
        <CenterTop />
        <CenterCenter/>
        <CenterBottom />
      </div>
      <!-- å³ä¾§åŒºåŸŸ -->
      <div class="right-panel">
        <RightTop />
        <RightBottom />
      </div>
    </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
import RightTop from './components/right-top.vue'
import RightBottom from './components/right-bottom.vue'
import useUserStore from '@/store/modules/user'
import LeftTop from './components/left-top.vue'
import CenterTop from './components/center-top.vue'
import CenterBottom from './components/center-bottom.vue'
// å…¨å±ç›¸å…³çŠ¶æ€
const isFullscreen = ref(false);
// ç¼©æ”¾æ¯”例
const scaleRatio = ref(1)
// è®¾è®¡å°ºå¯¸ï¼ˆåŸºå‡†å°ºå¯¸ï¼‰- æ ¹æ®å®žé™…设计稿调整
const designWidth = 1920
const designHeight = 1080
// ç”¨æˆ·store
const userStore = useUserStore()
// è®¡ç®—缩放比例
const calculateScale = () => {
  const container = document.querySelector('.scale-container')
  if (!container) return
  // èŽ·å–å®¹å™¨çš„å®žé™…å°ºå¯¸
  const rect = container.getBoundingClientRect?.()
  const containerWidth = container.clientWidth || rect?.width || window.innerWidth
  const containerHeight = container.clientHeight || rect?.height || window.innerHeight
  // è®¡ç®—宽高缩放比例,取较小值以保证内容完整显示(等比缩放)
  const scaleX = containerWidth / designWidth
  const scaleY = containerHeight / designHeight
  scaleRatio.value = Math.min(scaleX, scaleY)
}
// çª—口大小变化处理
const handleResize = () => {
  // å»¶è¿Ÿæ‰§è¡Œï¼Œç¡®ä¿DOM更新完成
  setTimeout(() => {
    calculateScale()
  }, 100)
}
// å…¨å±åŠŸèƒ½å®žçŽ° - é’ˆå¯¹scale-container元素
const toggleFullscreen = () => {
  const element = document.querySelector('.scale-container')
  if (!element) return
  if (!isFullscreen.value) {
    if (element.requestFullscreen) {
      element.requestFullscreen()
    } else if (element.webkitRequestFullscreen) {
      element.webkitRequestFullscreen()
    } else if (element.msRequestFullscreen) {
      element.msRequestFullscreen()
    }
  } else {
    if (document.exitFullscreen) {
      document.exitFullscreen()
    } else if (document.webkitExitFullscreen) {
      document.webkitExitFullscreen()
    } else if (document.msExitFullscreen) {
      document.msExitFullscreen()
    }
  }
}
// ç›‘听全屏变化事件
const handleFullscreenChange = () => {
  const fullscreenElement = document.fullscreenElement ||
                           document.webkitFullscreenElement ||
                           document.msFullscreenElement
  isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
  // å…¨å±çŠ¶æ€å˜åŒ–æ—¶ï¼Œå»¶è¿Ÿé‡æ–°è®¡ç®—ç¼©æ”¾æ¯”ä¾‹ï¼ˆç¡®ä¿DOM更新完成)
  setTimeout(() => {
    calculateScale()
  }, 200)
}
// ç”Ÿå‘½å‘¨æœŸé’©å­
onMounted(() => {
  // ä½¿ç”¨nextTick确保DOM完全渲染后再初始化
  nextTick(() => {
    // è®¡ç®—初始缩放比例
    calculateScale()
  })
  window.addEventListener('resize', handleResize)
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
})
onBeforeUnmount(() => {
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
  // ç§»é™¤æˆ‘们添加的autofit动态调整监听器
  if (window._autofitUpdateHandler) {
    window.removeEventListener('resize', window._autofitUpdateHandler)
    delete window._autofitUpdateHandler
  }
  // å…³é—­autofit
  autofit.off()
})
</script>
<style scoped>
/* å¤–部缩放容器 - å æ®æ•´ä¸ªè§†å£ */
.scale-container {
position: relative;
width: 100%;
/* é¡µé¢åœ¨å¸¸è§„布局下(有顶栏)默认减去 84px,避免内容被裁切 */
height: calc(100vh - 84px);
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
overflow: hidden;
}
/* å†…部内容区域 - å›ºå®šè®¾è®¡å°ºå¯¸ */
.data-dashboard {
position: relative;
width: 1920px;
height: 1080px;
background-image: url("@/assets/BI/backImage@2x.png");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform-origin: center center;
}
/* å…¨å±çŠ¶æ€çš„æ ·å¼ - ä½œç”¨äºŽscale-container */
.scale-container:fullscreen {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
/* Webkit浏览器前缀 */
.scale-container:-webkit-full-screen {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
/* MS浏览器前缀 */
.scale-container:-ms-fullscreen {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background-color: #000;
z-index: 9999;
}
.dashboard-header {
position: relative;
z-index: 1;
height: 86px;
background-image: url("@/assets/BI/biaoti.png");
background-size: cover;
background-repeat: no-repeat;
display: flex;
align-items: center;
justify-content: center;
}
.factory-name {
font-weight: 600;
font-size: 52px;
color: #FFFFFF;
top: 16px;
position: absolute;
}
.fullscreen-btn {
position: absolute;
top: 10px;
left: 20px;
width: 40px;
height: 40px;
background: rgba(0, 20, 60, 0.8);
border: 1px solid rgba(0, 212, 255, 0.3);
border-radius: 6px;
color: #00d4ff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
z-index: 10000;
}
.fullscreen-btn:hover {
background: rgba(0, 30, 90, 0.9);
border-color: rgba(0, 212, 255, 0.5);
}
.dashboard-content {
position: relative;
z-index: 1;
display: flex;
gap: 30px;
padding: 0 30px;
height: calc(100% - 86px);
overflow: hidden;
}
/* ç¡®ä¿å„面板能够正确显示 */
.left-panel, .center-panel, .right-panel {
overflow: hidden;
}
.left-panel,
.right-panel {
flex: 1;
display: flex;
flex-direction: column;
gap: 24px;
width: 520px;
}
.center-panel {
flex: 1.5;
display: flex;
flex-direction: column;
gap: 20px;
}
</style>
src/views/reportAnalysis/reportManagement/index.vue
@@ -302,17 +302,17 @@
};
const getYearlyStatValue = (type, field) => {
  const stat = yearlyPassRateData.value.find(item => item.inspectType === type);
  const stat = yearlyPassRateData.value.find(item => item.modelType === type);
  return stat ? stat[field] : 0;
};
const getInspectStatValue = (type, field) => {
  const stat = inspectStatisticsData.value.find(item => item.inspectType === type);
  const stat = inspectStatisticsData.value.find(item => item.modelType === type);
  return stat ? stat[field] : 0;
};
const getPassRateStatValue = (type, field) => {
  const stat = passRateStatisticsData.value.find(item => item.inspectType === type);
  const stat = passRateStatisticsData.value.find(item => item.modelType === type);
  if (stat) {
    if (field === 'completionRate' || field === 'passRate') {
      return stat[field] ? Number(stat[field]).toFixed(0) + '%' : '0%';
@@ -387,7 +387,7 @@
const fetchTopParametersData = async () => {
  try {
    const typeMap = { raw: 0, semi: 1, final: 2 };
    const typeMap = { raw: 1, semi: 2, final: 3 };
    const res = await getTopParameters(typeMap[activeTab.value]);
    if (res.code === 200) {
      topParametersData.value = res.data;
src/views/safeProduction/accidentReportingRecord/index.vue
@@ -203,9 +203,9 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
@@ -513,7 +513,7 @@
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
        page.value.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
@@ -524,7 +524,7 @@
  const pagination = obj => {
    page.value.current = obj.page;
    page.value.size = obj.limit;
    handleQuery();
    getList();
  };
  const currentUserId = ref("");
  const currentUserName = ref("");
src/views/safeProduction/dangerInvestigation/index.vue
@@ -111,13 +111,13 @@
        </el-table-column>
        <el-table-column fixed="right"
                         label="操作"
                         min-width="250"
                         min-width="150"
                         align="center">
          <template #default="scope">
            <el-button link
            <!-- <el-button link
                       type="primary"
                       size="small"
                       @click="openForm('edit', scope.row)">编辑</el-button>
                       @click="openForm('edit', scope.row)">编辑</el-button> -->
            <el-button link
                       type="primary"
                       size="small"
@@ -125,11 +125,12 @@
            <el-button link
                       type="primary"
                       size="small"
                       :disabled="scope.row.isRectify || scope.row.rectifyActualTime"
                       @click="openForm('edit2', scope.row)">整改</el-button>
            <el-button link
                       type="primary"
                       size="small"
                       :disabled="!scope.row.rectifyActualTime"
                       :disabled="!scope.row.rectifyActualTime || scope.row.verifyTime"
                       @click="openForm('edit3', scope.row)">验收</el-button>
          </template>
        </el-table-column>
@@ -419,8 +420,11 @@
                    v-model="fileListDialogVisible"
                    :show-upload-button="true"
                    :show-delete-button="true"
                    :is-show-pagination="true"
                    :page="filePagination"
                    :upload-method="handleUpload"
                    :delete-method="handleFileDelete"
                    @pagination="paginationSearch"
                    title="附件列表" />
  </div>
</template>
@@ -655,6 +659,14 @@
        tableLoading.value = false;
        tableData.value = res.data.records;
        total.value = res.data.total;
        tableData.value.forEach(item => {
          // console.log(item.rectifyUserId, currentUserId.value, "=======");
          if (Number(item.rectifyUserId) != Number(currentUserId.value)) {
            item.isRectify = true;
          } else {
            item.isRectify = false;
          }
        });
        return res;
      })
      .catch(() => {
@@ -889,6 +901,9 @@
        proxy.$modal.msg("已取消");
      });
  };
  const isPeople = rectifyUserId => {
    return Number(rectifyUserId) == Number(currentUserId.value);
  };
  /**
   * åˆ¤æ–­æ˜¯å¦å¯ä»¥å‘è´§
@@ -913,6 +928,11 @@
    const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
    return statusStr === "待发货" || statusStr === "审核拒绝";
  };
  const filePagination = ref({
    current: 1,
    size: 10,
    total: 0,
  });
  /**
   * ä¸‹è½½æ–‡ä»¶
@@ -924,9 +944,15 @@
  const currentFileRow = ref(null);
  const downLoadFile = row => {
    currentFileRow.value = row;
    fileListPage({ safeHiddenId: row.id }).then(res => {
    fileListPage({
      safeHiddenId: row.id,
      current: filePagination.value.current,
      size: filePagination.value.size,
    }).then(res => {
      if (fileListRef.value) {
        fileListRef.value.open(res.data.records);
        fileListRef.value.open(res.data.records || []);
        console.log("res.data", res.data);
        filePagination.value.total = res.data.total || 0;
      }
    });
  };
@@ -958,11 +984,11 @@
  };
  onMounted(() => {
    getCurrentFactoryName();
    getList();
    userListNoPage().then(res => {
      userList.value = res.data;
    });
    getCurrentFactoryName();
  });
  // ä¸Šä¼ é™„ä»¶
  const handleUpload = async () => {
@@ -1012,6 +1038,8 @@
              // é‡æ–°åŠ è½½æ–‡ä»¶åˆ—è¡¨
              const listRes = await fileListPage({
                safeHiddenId: currentFileRow.value.id,
                current: filePagination.value.current,
                size: filePagination.value.size,
              });
              if (listRes.code === 200 && fileListRef.value) {
                const fileList = (listRes.data?.records || []).map(item => ({
@@ -1021,6 +1049,7 @@
                  ...item,
                }));
                fileListRef.value.setList(fileList);
                filePagination.value.total = listRes.data?.total || 0;
              }
              // è¿”回新文件信息
              resolve({
@@ -1048,6 +1077,26 @@
      input.click();
    });
  };
  // åˆ†é¡µæŸ¥è¯¢æ–‡ä»¶åˆ—表
  const paginationSearch = async (page, size) => {
    filePagination.value.current = page;
    filePagination.value.size = size;
    const listRes = await fileListPage({
      safeHiddenId: currentFileRow.value.id,
      current: filePagination.value.current,
      size: filePagination.value.size,
    });
    if (listRes.code === 200) {
      const fileList = (listRes.data?.records || []).map(item => ({
        name: item.name,
        url: item.url,
        id: item.id,
        ...item,
      }));
      fileListRef.value.setList(fileList);
      filePagination.value.total = listRes.data?.total || 0;
    }
  };
  // åˆ é™¤é™„ä»¶
  const handleFileDelete = async row => {
    try {
@@ -1058,6 +1107,8 @@
        if (currentFileRow.value && fileListRef.value) {
          const listRes = await fileListPage({
            safeHiddenId: currentFileRow.value.id,
            current: filePagination.value.current,
            size: filePagination.value.size,
          });
          if (listRes.code === 200) {
            const fileList = (listRes.data?.records || []).map(item => ({
@@ -1067,6 +1118,7 @@
              ...item,
            }));
            fileListRef.value.setList(fileList);
            filePagination.value.total = listRes.data?.total || 0;
          }
        }
        return true; // è¿”回 true è¡¨ç¤ºåˆ é™¤æˆåŠŸï¼Œç»„ä»¶ä¼šæ›´æ–°åˆ—è¡¨
src/views/safeProduction/emergencyPlanReview/index.vue
@@ -165,9 +165,9 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
@@ -445,7 +445,7 @@
      .then(res => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
        page.value.total = res.data.total;
      })
      .catch(err => {
        tableLoading.value = false;
@@ -456,7 +456,7 @@
  const pagination = obj => {
    page.value.current = obj.page;
    page.value.size = obj.limit;
    handleQuery();
    getList();
  };
  // é€‰æ‹©å˜åŒ–处理
src/views/safeProduction/hazardSourceLedger/index.vue
@@ -159,9 +159,9 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
@@ -457,7 +457,7 @@
  const pagination = obj => {
    page.value.current = obj.page;
    page.value.size = obj.limit;
    handleQuery();
    getList();
  };
  // é€‰æ‹©å˜åŒ–处理
src/views/safeProduction/hazardousMaterialsControl/index.vue
@@ -244,9 +244,9 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
@@ -258,9 +258,14 @@
      <div>
        <el-table :data="safeHazardList"
                  border
                  ref="safeHazardTableRef"
                  v-loading="safeHazardLoading"
                  style="width: 100%"
                  @row-click="handleSafeHazardSelect">
                  :selection="selectedSafeHazardIds"
                  @selection-change="handleSafeHazardSelectionChange"
                  style="width: 100%">
          <el-table-column type="selection"
                           width="55"
                           :selectable="isSelectable" />
          <el-table-column prop="code"
                           label="危险源编码"
                           width="180"
@@ -302,6 +307,8 @@
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary"
                     @click="handleSafeHazardSelect">确定</el-button>
          <el-button @click="safeHazardSelectVisible = false">取消</el-button>
        </span>
      </template>
@@ -541,11 +548,17 @@
    }
  };
  const handleApplyQtyChange = () => {
    if (Number(form.value.applyQty) < 0) {
      ElMessage.error("领用数量不能小于0");
      form.value.applyQty = 0;
      return;
    }
    if (form.value.applyQty > valueItem.value.stockQty) {
      ElMessage.error("领用数量不能大于库存数量");
      form.value.applyQty = "";
    }
  };
  const selectedSafeHazardIds = ref([]);
  // å¼€å§‹è‡ªåŠ¨åˆ·æ–°
  const startAutoRefresh = () => {
@@ -580,7 +593,7 @@
  const fetchSafeHazardList = () => {
    safeHazardLoading.value = true;
    return safeHazardListPage({
      page: safeHazardPage.value.current,
      current: safeHazardPage.value.current,
      size: safeHazardPage.value.size,
    })
      .then(res => {
@@ -592,12 +605,36 @@
      });
  };
  const handleSafeHazardSelect = item => {
  const isSelectable = row => {
    // åªæœ‰åº“存数量大于0的行才能被选择
    return Number(row.stockQty) > 0;
  };
  const handleSafeHazardSelectionChange = selection => {
    // åªä¿ç•™æœ€åŽä¸€ä¸ªé€‰ä¸­çš„项
    if (selection.length > 1) {
      const lastSelected = selection[selection.length - 1];
      selectedSafeHazardIds.value = [lastSelected];
      proxy.$refs.safeHazardTableRef.clearSelection();
      proxy.$refs.safeHazardTableRef.toggleRowSelection(lastSelected, true);
    } else if (selection.length === 1) {
      selectedSafeHazardIds.value = [selection[0]];
    } else {
      selectedSafeHazardIds.value = [];
    }
  };
  const handleSafeHazardSelect = () => {
    if (!selectedSafeHazardIds.value.length) {
      ElMessage.error("请选择一个危险源");
      return;
    }
    valueItem.value = {
      ...item,
      ...selectedSafeHazardIds.value[0],
    };
    valueItem.value.type = getTypeLabel(valueItem.value.type);
    form.value.safeHazardId = item.id;
    form.value.safeHazardId = selectedSafeHazardIds.value[0].id;
    safeHazardSelectVisible.value = false;
  };
@@ -611,12 +648,20 @@
  const pagination1 = obj => {
    page.value.current = obj.page;
    page.value.size = obj.limit;
    handleQuery();
    getList();
  };
  // é€‰æ‹©å˜åŒ–处理
  const handleSelectionChange = selection => {
    selectedIds.value = selection.map(item => item.id);
    // ä¸»è¡¨æ ¼ä¹Ÿåªä¿ç•™æœ€åŽä¸€ä¸ªé€‰ä¸­çš„项
    if (selection.length > 1) {
      const lastSelected = selection[selection.length - 1];
      selectedIds.value = [lastSelected.id];
    } else if (selection.length === 1) {
      selectedIds.value = [selection[0].id];
    } else {
      selectedIds.value = [];
    }
  };
  // æ‰“开表单
@@ -741,7 +786,7 @@
          .catch(err => {
            ElMessage.error(err.msg);
          });
      } else {
      } else if (dialogType.value === "edit") {
        await formRef1.value.validate();
        safeHazardRecordUpdate({ ...form.value })
          .then(res => {
@@ -754,6 +799,9 @@
          .catch(err => {
            ElMessage.error(err.msg);
          });
      } else if (dialogType.value === "view") {
        // æŸ¥çœ‹æ¨¡å¼ä¸‹ä¸æäº¤è¡¨å•
        dialogVisible.value = false;
      }
    } catch (error) {
      console.error("表单验证失败:", error);
src/views/safeProduction/safeQualifications/index.vue
@@ -207,8 +207,11 @@
                    v-model="fileListDialogVisible"
                    :show-upload-button="true"
                    :show-delete-button="true"
                    :is-show-pagination="true"
                    :page="filePagination"
                    :upload-method="handleUpload"
                    :delete-method="handleFileDelete"
                    @pagination="paginationSearch"
                    title="附件列表" />
  </div>
</template>
@@ -514,12 +517,22 @@
  const fileListRef = ref(null);
  const fileListDialogVisible = ref(false);
  const currentFileRow = ref(null);
  const filePagination = ref({
    current: 1,
    size: 10,
    total: 0,
  });
  const downLoadFile = row => {
    currentFileRow.value = row;
    fileListPage({ safeCertificationId: row.id }).then(res => {
    fileListPage({
      safeCertificationId: row.id,
      current: filePagination.value.current,
      size: filePagination.value.size,
    }).then(res => {
      if (fileListRef.value) {
        fileListRef.value.open(res.data.records);
      }
      filePagination.value.total = res.data.total || 0;
    });
  };
  const currentFactoryName = ref("");
@@ -603,6 +616,8 @@
              // é‡æ–°åŠ è½½æ–‡ä»¶åˆ—è¡¨
              const listRes = await fileListPage({
                safeCertificationId: currentFileRow.value.id,
                current: filePagination.value.current,
                size: filePagination.value.size,
              });
              if (listRes.code === 200 && fileListRef.value) {
                const fileList = (listRes.data?.records || []).map(item => ({
@@ -612,6 +627,7 @@
                  ...item,
                }));
                fileListRef.value.setList(fileList);
                filePagination.value.total = listRes.data?.total || 0;
              }
              // è¿”回新文件信息
              resolve({
@@ -639,6 +655,26 @@
      input.click();
    });
  };
  const paginationSearch = async (page, size) => {
    filePagination.value.current = page;
    filePagination.value.size = size;
    const listRes = await fileListPage({
      safeCertificationId: currentFileRow.value.id,
      current: filePagination.value.current,
      size: filePagination.value.size,
    });
    if (listRes.code === 200) {
      const fileList = (listRes.data?.records || []).map(item => ({
        name: item.name,
        url: item.url,
        id: item.id,
        ...item,
      }));
      fileListRef.value.setList(fileList);
      filePagination.value.total = listRes.data?.total || 0;
    }
  };
  // åˆ é™¤é™„ä»¶
  const handleFileDelete = async row => {
    try {
@@ -649,6 +685,8 @@
        if (currentFileRow.value && fileListRef.value) {
          const listRes = await fileListPage({
            safeCertificationId: currentFileRow.value.id,
            current: filePagination.value.current,
            size: filePagination.value.size,
          });
          if (listRes.code === 200) {
            const fileList = (listRes.data?.records || []).map(item => ({
@@ -658,6 +696,7 @@
              ...item,
            }));
            fileListRef.value.setList(fileList);
            filePagination.value.total = listRes.data?.total || 0;
          }
        }
        return true; // è¿”回 true è¡¨ç¤ºåˆ é™¤æˆåŠŸï¼Œç»„ä»¶ä¼šæ›´æ–°åˆ—è¡¨
src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -181,9 +181,9 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary"
                     @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
@@ -363,8 +363,11 @@
                    v-model="fileListDialogVisible"
                    :show-upload-button="true"
                    :show-delete-button="true"
                    :is-show-pagination="true"
                    :page="filePagination"
                    :upload-method="handleUpload"
                    :delete-method="handleFileDelete"
                    @pagination="paginationSearch"
                    title="附件列表" />
  </div>
</template>
@@ -626,7 +629,7 @@
        {
          name: "结果明细",
          type: "text",
          // disabled: row => row.state !== 2,
          disabled: row => row.state == 0,
          clickFun: row => {
            viewResultDetail(row);
          },
@@ -790,9 +793,14 @@
  const currentFileRow = ref(null);
  const downLoadFile = row => {
    currentFileRow.value = row;
    safeTrainingFileListPage({ safeTrainingId: row.id }).then(res => {
    safeTrainingFileListPage({
      safeTrainingId: row.id,
      current: filePagination.value.current,
      size: filePagination.value.size,
    }).then(res => {
      if (fileListRef.value) {
        fileListRef.value.open(res.data.records);
        filePagination.value.total = res.data?.total || 0;
      }
    });
  };
@@ -844,6 +852,8 @@
              // é‡æ–°åŠ è½½æ–‡ä»¶åˆ—è¡¨
              const listRes = await safeTrainingFileListPage({
                safeTrainingId: currentFileRow.value.id,
                current: filePagination.value.current,
                size: filePagination.value.size,
              });
              if (listRes.code === 200 && fileListRef.value) {
                const fileList = (listRes.data?.records || []).map(item => ({
@@ -853,6 +863,7 @@
                  ...item,
                }));
                fileListRef.value.setList(fileList);
                filePagination.value.total = listRes.data?.total || 0;
              }
              // è¿”回新文件信息
              resolve({
@@ -880,6 +891,31 @@
      input.click();
    });
  };
  const filePagination = ref({
    current: 1,
    size: 10,
    total: 0,
  });
  const paginationSearch = async (page, size) => {
    filePagination.value.current = page;
    filePagination.value.size = size;
    const listRes = await safeTrainingFileListPage({
      safeTrainingId: currentFileRow.value.id,
      current: filePagination.value.current,
      size: filePagination.value.size,
    });
    if (listRes.code === 200) {
      const fileList = (listRes.data?.records || []).map(item => ({
        name: item.name,
        url: item.url,
        id: item.id,
        ...item,
      }));
      fileListRef.value.setList(fileList);
      filePagination.value.total = listRes.data?.total || 0;
    }
  };
  // åˆ é™¤é™„ä»¶
  const handleFileDelete = async row => {
    try {
@@ -890,6 +926,8 @@
        if (currentFileRow.value && fileListRef.value) {
          const listRes = await safeTrainingFileListPage({
            safeTrainingId: currentFileRow.value.id,
            current: filePagination.value.current,
            size: filePagination.value.size,
          });
          if (listRes.code === 200) {
            const fileList = (listRes.data?.records || []).map(item => ({
@@ -899,6 +937,7 @@
              ...item,
            }));
            fileListRef.value.setList(fileList);
            filePagination.value.total = listRes.data?.total || 0;
          }
        }
        return true; // è¿”回 true è¡¨ç¤ºåˆ é™¤æˆåŠŸï¼Œç»„ä»¶ä¼šæ›´æ–°åˆ—è¡¨
@@ -971,7 +1010,7 @@
  const pagination = obj => {
    page.value.current = obj.page;
    page.value.size = obj.limit;
    handleQuery();
    getList();
  };
  // é€‰æ‹©å˜åŒ–处理
src/views/salesManagement/receiptPayment/index.vue
@@ -335,7 +335,7 @@
const getStatusTagType = (statusName = '') => {
  const normalized = statusName.trim();
  if (!normalized) return 'info';
  return normalized === '未完成回款' ? 'danger' : 'success';
  return normalized === '未完成付款' ? 'danger' : 'success';
};
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
src/views/salesManagement/salesLedger/index.vue
@@ -118,6 +118,7 @@
        <el-table-column label="录入日期" prop="entryDate" width="120" show-overflow-tooltip />
        <el-table-column label="签订日期" prop="executionDate" width="120" show-overflow-tooltip />
        <el-table-column label="交付日期" prop="deliveryDate" width="120" show-overflow-tooltip />
        <el-table-column label="备注" prop="remarks" width="200" show-overflow-tooltip />
        <el-table-column fixed="right" label="操作" min-width="100" align="center">
          <template #default="scope">
            <el-button link type="primary" size="small" @click="openForm('edit', scope.row)">编辑</el-button>
@@ -221,7 +222,8 @@
                </el-row>
                <el-table :data="productData" border @selection-change="productSelected" show-summary
                                    :summary-method="summarizeMainTable">
                    <el-table-column align="center" type="selection" width="55" v-if="operationType !== 'view'" />
                    <el-table-column align="center" type="selection" width="55" v-if="operationType !== 'view'"
                        :selectable="(row) => !isProductShipped(row)" />
                    <el-table-column align="center" label="序号" type="index" width="60" />
                    <el-table-column label="产品大类" prop="productCategory" />
                    <el-table-column label="规格型号" prop="specificationModel" />
@@ -233,20 +235,22 @@
                    <el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
                    <el-table-column fixed="right" label="操作" min-width="60" align="center" v-if="operationType !== 'view'">
                        <template #default="scope">
                            <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row,scope.$index)">编辑</el-button>
                            <el-button link type="primary" size="small"
                                :disabled="isProductShipped(scope.row)"
                                @click="openProductForm('edit', scope.row,scope.$index)">编辑</el-button>
                        </template>
                    </el-table-column>
                </el-table>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="备注·:" prop="remark">
                            <el-input v-model="form.remark" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" />
                        <el-form-item label="备注:" prop="remarks">
                            <el-input v-model="form.remarks" placeholder="请输入" clearable type="textarea" :rows="2" :disabled="operationType === 'view'" />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="24">
                        <el-form-item label="附件材料:" prop="remark">
                        <el-form-item label="附件材料:" prop="salesLedgerFiles">
                            <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload
                                                 :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"
                                                 :on-success="handleUploadSuccess" :on-remove="handleRemove">
@@ -1001,8 +1005,10 @@
// æ·»åŠ è¡¨è¡Œç±»åæ–¹æ³•
const tableRowClassName = ({ row }) => {
  const diff = row.deliveryDaysDiff;
  if (!row.deliveryDate) return '';
  if (row.isFh) return '';
  const diff = row.deliveryDaysDiff;
  if (diff === 15) {
    return 'yellow';
  } else if (diff === 10) {
@@ -1222,6 +1228,12 @@
const productIndex = ref(0);
// æ‰“开产品弹框
const openProductForm = async (type, row, index) => {
    // ç¼–辑时检查产品是否已发货或审核通过
    if (type === "edit" && isProductShipped(row)) {
        proxy.$modal.msgWarning("已发货或审核通过的产品不能编辑");
        return;
    }
    productOperationType.value = type;
    productForm.value = {};
    proxy.resetForm("productFormRef");
@@ -1288,6 +1300,14 @@
        proxy.$modal.msgWarning("请选择数据");
        return;
    }
    // æ£€æŸ¥æ˜¯å¦æœ‰å·²å‘货或审核通过的产品
    const shippedProducts = productSelectedRows.value.filter(row => isProductShipped(row));
    if (shippedProducts.length > 0) {
        proxy.$modal.msgWarning("已发货或审核通过的产品不能删除");
        return;
    }
    if (operationType.value === "add") {
        productSelectedRows.value.forEach((selectedRow) => {
            const index = productData.value.findIndex(
@@ -1362,6 +1382,14 @@
            proxy.$modal.msg("已取消");
        });
};
/** åˆ¤æ–­å•个产品是否已发货(根据shippingStatus判断,已发货或审核通过不可编辑和删除) */
const isProductShipped = (product) => {
    if (!product) return false;
    const status = String(product.shippingStatus || "").trim();
    // å¦‚果发货状态是"已发货"或"审核通过",则不可编辑和删除
    return status === "已发货" || status === "审核通过";
};
/** åˆ¤æ–­é”€å”®è®¢å•下是否存在已发货/发货完成的产品(不可删除) */
const hasShippedProducts = (products) => {
    if (!products || !products.length) return false;
src/views/salesManagement/salesQuotation/index.vue
@@ -231,43 +231,52 @@
            <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0">
            <el-table-column prop="product" label="产品名称" width="200">
              <template #default="scope">
                                <el-tree-select
                                    v-model="scope.row.productId"
                                    placeholder="请选择"
                                    clearable
                                    check-strictly
                                    @change="getModels($event, scope.row)"
                                    :data="productOptions"
                                    :render-after-expand="false"
                                    style="width: 100%"
                                />
                <el-form-item :prop="`products.${scope.$index}.productId`" class="product-table-form-item">
                  <el-tree-select
                    v-model="scope.row.productId"
                    placeholder="请选择"
                    clearable
                    check-strictly
                    @change="getModels($event, scope.row)"
                    :data="productOptions"
                    :render-after-expand="false"
                    style="width: 100%"
                  />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="specification" label="规格型号" width="150">
            <el-table-column prop="specification" label="规格型号" width="200">
              <template #default="scope">
                                <el-select
                                    v-model="scope.row.specificationId"
                                    placeholder="请选择"
                                    clearable
                                    @change="getProductModel($event, scope.row)"
                                >
                                    <el-option
                                        v-for="item in scope.row.modelOptions || []"
                                        :key="item.id"
                                        :label="item.model"
                                        :value="item.id"
                                    />
                                </el-select>
                <el-form-item :prop="`products.${scope.$index}.specificationId`" class="product-table-form-item">
                  <el-select
                    v-model="scope.row.specificationId"
                    placeholder="请选择"
                    clearable
                    @change="getProductModel($event, scope.row)"
                    style="width: 100%"
                  >
                    <el-option
                      v-for="item in scope.row.modelOptions || []"
                      :key="item.id"
                      :label="item.model"
                      :value="item.id"
                    />
                  </el-select>
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="unit" label="单位">
              <template #default="scope">
                <el-input v-model="scope.row.unit" placeholder="单位" />
                <el-form-item :prop="`products.${scope.$index}.unit`" class="product-table-form-item">
                  <el-input v-model="scope.row.unit" placeholder="单位" clearable/>
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价">
              <template #default="scope">
                <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" />
                <el-form-item :prop="`products.${scope.$index}.unitPrice`" class="product-table-form-item">
                  <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" />
                </el-form-item>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80" align="center">
@@ -393,13 +402,30 @@
  totalAmount: 0
})
const rules = {
const baseRules = {
  customer: [{ required: true, message: '请选择客户', trigger: 'change' }],
  salesperson: [{ required: true, message: '请选择业务员', trigger: 'change' }],
  quotationDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }],
  validDate: [{ required: true, message: '请选择有效期', trigger: 'change' }],
  paymentMethod: [{ required: true, message: '请输入付款方式', trigger: 'blur' }]
}
const productRowRules = {
  productId: [{ required: true, message: '请选择产品名称', trigger: 'change' }],
  specificationId: [{ required: true, message: '请选择规格型号', trigger: 'change' }],
  unit: [{ required: true, message: '请填写单位', trigger: 'blur' }],
  unitPrice: [{ required: true, message: '请填写单价', trigger: 'change' }]
}
const rules = computed(() => {
  const r = { ...baseRules }
  ;(form.products || []).forEach((_, i) => {
    r[`products.${i}.productId`] = productRowRules.productId
    r[`products.${i}.specificationId`] = productRowRules.specificationId
    r[`products.${i}.unit`] = productRowRules.unit
    r[`products.${i}.unitPrice`] = productRowRules.unitPrice
  })
  return r
})
const userList = ref([]);
const customerOption = ref([]);
@@ -774,7 +800,7 @@
        ElMessage.warning('请至少添加一个产品')
        return
      }
      // å®¡æ‰¹äººå¿…填校验
      const hasEmptyApprover = approverNodes.value.some(node => !node.userId)
      if (hasEmptyApprover) {
@@ -956,6 +982,17 @@
  padding: 8px 0;
}
.product-table-form-item {
  margin-bottom: 0;
  :deep(.el-form-item__content) {
    margin-left: 0 !important;
  }
  :deep(.el-form-item__label) {
    width: auto;
    min-width: auto;
  }
}
.approver-nodes-container {
  display: flex;
  flex-wrap: wrap;