zhangwencui
10 天以前 3ce715a4e66955eac86dcd2f5c8afcfdf8becb6d
Merge origin/dev_项目管理 into dev_项目管理

resolve conflict in multiple/config.json: keep both new tenant configs
已添加22个文件
已复制1个文件
已重命名1个文件
已修改44个文件
3026 ■■■■■ 文件已修改
multiple/assets/favicon/JMGCfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/OLDXCLfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/THZSfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/YXHJfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/ZGLTico.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/ZXDZfavicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/JMGCLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/OLDXCLLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/SHYY.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/THZSLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/YXHJLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/ZGLTLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/logo/ZXDZLogo.png 补丁 | 查看 | 原始文档 | blame | 历史
multiple/config.json 410 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/reportAnalysis/safetyDashboard.js 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/logo/SHYY.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/dateDisplay.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/index.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/ruoyi.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/summary/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/purchaseIn.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/reconciliation.vue 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/reconciliation.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Qualified.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Record.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Unqualified.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceListSearch.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/AttendManage/out-apply/index.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/index.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/thePaymentLedger/index.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/components/PIMTable.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/afterSalesTraceability/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/safetyDashboard/components/center-bottom.vue 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/safetyDashboard/components/center-top.vue 432 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/safetyDashboard/components/left-bottom.vue 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/safetyDashboard/components/left-top.vue 241 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/safetyDashboard/components/right-bottom.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/safetyDashboard/components/right-top.vue 275 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/safetyDashboard/index.vue 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safetyTrainingAssessment/detail.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/strategyControl/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
multiple/assets/favicon/JMGCfavicon.ico
multiple/assets/favicon/OLDXCLfavicon.ico
multiple/assets/favicon/THZSfavicon.ico
multiple/assets/favicon/YXHJfavicon.ico
multiple/assets/favicon/ZGLTico.ico
multiple/assets/favicon/ZXDZfavicon.ico
multiple/assets/logo/JMGCLogo.png
multiple/assets/logo/OLDXCLLogo.png
multiple/assets/logo/SHYY.png

multiple/assets/logo/THZSLogo.png
multiple/assets/logo/YXHJLogo.png
multiple/assets/logo/ZGLTLogo.png
multiple/assets/logo/ZXDZLogo.png
multiple/config.json
@@ -12,8 +12,350 @@
      "VITE_BASE_API": "http://1.15.17.182:9048",
      "VITE_JAVA_API": "http://1.15.17.182:9049"
    },
    "logo": "logo/XDRJ.png",
    "logo": "logo/SHYY.png",
    "favicon": "favicon/favicon.ico"
  },
  "BTYX": {
    "env": {
      "VITE_APP_TITLE": "河南帮太优选食品有限公司",
      "VITE_BASE_API": "http://1.15.17.182:9056",
      "VITE_JAVA_API": "http://1.15.17.182:9057"
    },
    "logo": "logo/BTYXLogo.png",
    "favicon": "favicon/BTYXfavicon.ico"
  },
  "ZXZN": {
    "env": {
      "VITE_APP_TITLE": "河南智芯智能机器人有限公司",
      "VITE_BASE_API": "http://127.0.0.1:9001",
      "VITE_JAVA_API": "http://127.0.0.1:9000"
    },
    "logo": "logo/ZXZNLogo.png",
    "favicon": "favicon/ZXZNfavicon.ico"
  },
  "SDTX": {
    "env": {
      "VITE_APP_TITLE": "河南善鼎通信科技有限公司",
      "VITE_BASE_API": "http://36.213.156.184:9001",
      "VITE_JAVA_API": "http://36.213.156.184:9000"
    },
    "logo": "logo/SDTXLogo.png",
    "favicon": "favicon/SDTXfavicon.ico"
  },
  "QXY": {
    "env": {
      "VITE_APP_TITLE": "强信宇电器管理系统",
      "VITE_BASE_API": "http://36.134.154.10:9001",
      "VITE_JAVA_API": "http://36.134.154.10:9000"
    },
    "logo": "logo/QXYLogo.png",
    "favicon": "favicon/QXYfavicon.ico"
  },
  "HQJC": {
    "env": {
      "VITE_APP_TITLE": "华强建材管理系统",
      "VITE_BASE_API": "http://36.134.77.64:9001",
      "VITE_JAVA_API": "http://36.134.77.64:9000"
    },
    "logo": "logo/HQJCLogo.png",
    "favicon": "favicon/HQJCfavicon.ico"
  },
  "XCDQ": {
    "env": {
      "VITE_APP_TITLE": "旭晨电器管理系统",
      "VITE_BASE_API": "http://36.133.45.183:9001",
      "VITE_JAVA_API": "http://36.133.45.183:9002"
    },
    "logo": "logo/XCDQLogo.png",
    "favicon": "favicon/XCDQfavicon.ico"
  },
  "BWSM": {
    "env": {
      "VITE_APP_TITLE": "八维商贸管理系统",
      "VITE_BASE_API": "http://1.15.17.182:9070",
      "VITE_JAVA_API": "http://1.15.17.182:9069"
    },
    "logo": "logo/BWSMLogo.png",
    "favicon": "favicon/BWSMfavicon.ico"
  },
  "CKGM": {
    "env": {
      "VITE_APP_TITLE": "宸康工贸管理系统",
      "VITE_BASE_API": "http://1.15.17.182:9072",
      "VITE_JAVA_API": "http://1.15.17.182:9071"
    },
    "logo": "logo/CKGMLogo.png",
    "favicon": "favicon/CKGMfavicon.ico"
  },
  "RFSY": {
    "env": {
      "VITE_APP_TITLE": "瑞丰盛业管理系统",
      "VITE_BASE_API": "http://1.15.17.182:9076",
      "VITE_JAVA_API": "http://1.15.17.182:9075"
    },
    "logo": "logo/RFSYLogo.png",
    "favicon": "favicon/RFSYfavicon.ico"
  },
  "ZQSY": {
    "env": {
      "VITE_APP_TITLE": "泽淇实业",
      "VITE_BASE_API": "http://36.213.128.159:9000",
      "VITE_JAVA_API": "http://36.213.128.159:9001"
    },
    "logo": "logo/ZQSYLogo.png",
    "favicon": "favicon/ZQSYfavicon.ico"
  },
  "JXJH": {
    "env": {
      "VITE_APP_TITLE": "浚县江海水泥制品有限公司",
      "VITE_BASE_API": "http://36.139.201.20:9000",
      "VITE_JAVA_API": "http://36.139.201.20:9001"
    },
    "logo": "logo/JXJHLogo.png",
    "favicon": "favicon/JXJHfavicon.ico"
  },
  "YTJZ": {
    "env": {
      "VITE_APP_TITLE": "豫泰建筑材料有限公司",
      "VITE_BASE_API": "http://36.139.201.181:9000",
      "VITE_JAVA_API": "http://36.139.201.181:9001"
    },
    "logo": "logo/YTJZLogo.png",
    "favicon": "favicon/YTJZfavicon.ico"
  },
  "HYLQ": {
    "env": {
      "VITE_APP_TITLE": "航逸路桥工程有限公司",
      "VITE_BASE_API": "http://36.139.202.111:9000",
      "VITE_JAVA_API": "http://36.139.202.111:9001"
    },
    "logo": "logo/HYLQLogo.png",
    "favicon": "favicon/HYLQfavicon.ico"
  },
  "HJGC": {
    "env": {
      "VITE_APP_TITLE": "环境工程管理系统",
      "VITE_BASE_API": "http://36.139.202.111:9014",
      "VITE_JAVA_API": "http://36.139.202.111:9013"
    },
    "logo": "logo/HJGCLogo.png",
    "favicon": "favicon/HJGCfavicon.ico"
  },
  "QYHY": {
    "env": {
      "VITE_APP_TITLE": "奇缘恒艺管理系统",
      "VITE_BASE_API": "http://36.139.202.111:9012",
      "VITE_JAVA_API": "http://36.139.202.111:9011"
    },
    "logo": "logo/QYHYLogo.png",
    "favicon": "favicon/QYHYfavicon.ico"
  },
  "QXY": {
    "env": {
      "VITE_APP_TITLE": "强信宇电器云主机",
      "VITE_BASE_API": "http://36.134.154.10:9000",
      "VITE_JAVA_API": "http://36.134.154.10:9001"
    },
    "logo": "logo/QXYLogo.png",
    "favicon": "favicon/QXYfavicon.ico"
  },
  "HYJC": {
    "env": {
      "VITE_APP_TITLE": "恒洋建材",
      "VITE_BASE_API": "http://36.138.94.178:9000",
      "VITE_JAVA_API": "http://36.138.94.178:9001"
    },
    "logo": "logo/HYJCLogo.png",
    "favicon": "favicon/HYJCfavicon.ico"
  },
  "JHY": {
    "env": {
      "VITE_APP_TITLE": "山西省榆社县晋和园食品有限公司",
      "VITE_BASE_API": "http://223.15.233.27:9001",
      "VITE_JAVA_API": "http://223.15.233.27:9002"
    },
    "logo": "logo/JHYLogo.png",
    "favicon": "favicon/JHYfavicon.ico"
  },
  "XCDQ": {
    "env": {
      "VITE_APP_TITLE": "旭晨电器管理系统",
      "VITE_BASE_API": "http://36.133.45.183:9001",
      "VITE_JAVA_API": "http://36.133.45.183:9002"
    },
    "logo": "logo/XCDQLogo.png",
    "favicon": "favicon/XCDQfavicon.ico"
  },
  "XHKS": {
    "env": {
      "VITE_APP_TITLE": "鑫铧矿山管理系统",
      "VITE_BASE_API": "http://36.133.45.183:9078",
      "VITE_JAVA_API": "http://36.133.45.183:9077"
    },
    "logo": "logo/XHKSLogo.png",
    "favicon": "favicon/XHKSfavicon.ico"
  },
  "QFSW": {
    "env": {
      "VITE_APP_TITLE": "乾丰生物管理系统",
      "VITE_BASE_API": "http://36.133.45.183:9080",
      "VITE_JAVA_API": "http://36.133.45.183:9079"
    },
    "logo": "logo/QFSWLogo.png",
    "favicon": "favicon/QFSWfavicon.ico"
  },
  "ZDJC": {
    "env": {
      "VITE_APP_TITLE": "正德新型建材管理系统",
      "VITE_BASE_API": "http://36.133.45.183:9082",
      "VITE_JAVA_API": "http://36.133.45.183:9081"
    },
    "logo": "logo/ZDJCLogo.png",
    "favicon": "favicon/ZDJCfavicon.ico"
  },
  "LSY": {
    "env": {
      "VITE_APP_TITLE": "山西林昇焱工贸有限公司",
      "VITE_BASE_API": "http://36.133.45.183:9084",
      "VITE_JAVA_API": "http://36.133.45.183:9083"
    },
    "logo": "logo/LSYLogo.png",
    "favicon": "favicon/LSYfavicon.ico"
  },
  "HTHB": {
    "env": {
      "VITE_APP_TITLE": "山西宏腾新型环保科技有限责任公司",
      "VITE_BASE_API": "http://36.133.45.183:9090",
      "VITE_JAVA_API": "http://36.133.45.183:9089"
    },
    "logo": "logo/HTHBLogo.png",
    "favicon": "favicon/HTHBfavicon.ico"
  },
  "YSS": {
    "env": {
      "VITE_APP_TITLE": "山西羿神晟农生物科技发展有限公司",
      "VITE_BASE_API": "http://36.133.45.183:9086",
      "VITE_JAVA_API": "http://36.133.45.183:9085"
    },
    "logo": "logo/YSSLogo.png",
    "favicon": "favicon/YSSfavicon.ico"
  },
  "KYHG": {
    "env": {
      "VITE_APP_TITLE": "山西坤源化工有限公司",
      "VITE_BASE_API": "http://36.137.13.29:9001",
      "VITE_JAVA_API": "http://36.137.13.29:9000"
    },
    "logo": "logo/KYHGLogo.png",
    "favicon": "favicon/KYHGfavicon.ico"
  },
  "JXSM": {
    "env": {
      "VITE_APP_TITLE": "襄垣县洁鑫商贸有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9001",
      "VITE_JAVA_API": "http://36.134.76.148:9002"
    },
    "logo": "logo/JXSMLogo.png",
    "favicon": "favicon/JXSMico.ico"
  },
  "DCHX": {
    "env": {
      "VITE_APP_TITLE": "山西大成化纤股份有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9088",
      "VITE_JAVA_API": "http://36.134.76.148:9087"
    },
    "logo": "logo/DCHXLogo.png",
    "favicon": "favicon/DCHXfavicon.ico"
  },
  "KHYY": {
    "env": {
      "VITE_APP_TITLE": "长治市坤皓液压制造有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9094",
      "VITE_JAVA_API": "http://36.134.76.148:9093"
    },
    "logo": "logo/KHYYLogo.png",
    "favicon": "favicon/KHYYfavicon.ico"
  },
  "XLGS": {
    "env": {
      "VITE_APP_TITLE": "长治市鑫浪供水设备有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9092",
      "VITE_JAVA_API": "http://36.134.76.148:9091"
    },
    "logo": "logo/XLGSLogo.png",
    "favicon": "favicon/XLGSfavicon.ico"
  },
  "NY": {
    "env": {
      "VITE_APP_TITLE": "山西省诺颢新材料有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9092",
      "VITE_JAVA_API": "http://36.134.76.148:9091"
    },
    "logo": "logo/NYLogo.png",
    "favicon": "favicon/NYfavicon.ico"
  },
  "JFHY": {
    "env": {
      "VITE_APP_TITLE": "山西金福恒远商贸有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9100",
      "VITE_JAVA_API": "http://36.134.76.148:9099"
    },
    "logo": "logo/JFHYLogo.png",
    "favicon": "favicon/JFHYfavicon.ico"
  },
  "BTW": {
    "env": {
      "VITE_APP_TITLE": "山西比特维科技有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9102",
      "VITE_JAVA_API": "http://36.134.76.148:9101"
    },
    "logo": "logo/BTWLogo.png",
    "favicon": "favicon/BTWfavicon.ico"
  },
  "BLSGR": {
    "env": {
      "VITE_APP_TITLE": "宁夏布里斯格尔羊绒制品有限公司",
      "VITE_BASE_API": "http://36.134.76.148:9102",
      "VITE_JAVA_API": "http://36.134.76.148:9101"
    },
    "logo": "logo/BLSGRLogo.png",
    "favicon": "favicon/BLSGRfavicon.ico"
  },
  "SDZZ": {
    "env": {
      "VITE_APP_TITLE": "平遥县盛达铸造厂",
      "VITE_BASE_API": "http://36.134.76.148:9102",
      "VITE_JAVA_API": "http://36.134.76.148:9101"
    },
    "logo": "logo/SDZZLogo.png",
    "favicon": "favicon/SDZZfavicon.ico"
  },
  "QLMC": {
    "env": {
      "VITE_APP_TITLE": "祁连牧场信息管理系统",
      "VITE_BASE_API": "http://114.132.189.42:9066",
      "VITE_JAVA_API": "http://114.132.189.42:9060"
    },
    "logo": "logo/QLMCLogo.png",
    "favicon": "favicon/QLMCico.ico"
  },
  "HYZC": {
    "env": {
      "VITE_APP_TITLE": "山西华亿众成建材有限公司",
      "VITE_BASE_API": "http://36.213.180.217:9009",
      "VITE_JAVA_API": "http://36.213.180.217:9008"
    },
    "logo": "logo/HYZCLogo.png",
    "favicon": "favicon/HYZCfavicon.ico"
  },
  "OLD": {
    "env": {
      "VITE_APP_TITLE": "青海欧莱德新材料科技有限公司",
      "VITE_BASE_API": "http://223.221.37.139:9000",
      "VITE_JAVA_API": "http://223.221.37.139:9000"
    },
    "logo": "logo/OLDLogo.png",
    "favicon": "favicon/OLD.ico"
  },
  "XJGM": {
    "env": {
@@ -24,7 +366,69 @@
    "logo": "logo/XJGMLogo.png",
    "favicon": "favicon/XJGM.ico"
  },
  "RBHB": {
    "env": {
      "VITE_APP_TITLE": "克拉玛依融邦环保科技有限公司",
      "VITE_BASE_API": "http://36.213.180.217:9017",
      "VITE_JAVA_API": "http://36.213.180.217:9018"
    },
    "logo": "logo/RBHBLogo.png",
    "favicon": "favicon/RBHB.ico"
  },
  "OLDXCL": {
    "env": {
      "VITE_APP_TITLE": "青海欧莱德新材料科技有限公司",
      "VITE_BASE_API": "http://36.213.90.123:9524",
      "VITE_JAVA_API": "http://36.213.90.123:9525"
    },
    "logo": "logo/OLDXCLLogo.png",
    "favicon": "favicon/OLDXCLfavicon.ico"
  },
    "YXHJ": {
    "env": {
      "VITE_APP_TITLE": "青海彦祥环境科技有限公司",
      "VITE_BASE_API": "http://36.213.90.123:9526",
      "VITE_JAVA_API": "http://36.213.90.123:9527"
    },
    "logo": "logo/YXHJLogo.png",
    "favicon": "favicon/YXHJfavicon.ico"
  },
    "ZXDZ": {
    "env": {
      "VITE_APP_TITLE": "青海准星电子衡器有限公司",
      "VITE_BASE_API": "http://36.213.90.123:9528",
      "VITE_JAVA_API": "http://36.213.90.123:9529"
    },
    "logo": "logo/ZXDZLogo.png",
    "favicon": "favicon/ZXDZfavicon.ico"
  },
    "THZS": {
    "env": {
      "VITE_APP_TITLE": "青海特弘再生物资回收有限公司",
      "VITE_BASE_API": "http://36.213.90.123:9530",
      "VITE_JAVA_API": "http://36.213.90.123:9531"
    },
    "logo": "logo/THZSLogo.png",
    "favicon": "favicon/THZSfavicon.ico"
  },
    "JMGC": {
    "env": {
      "VITE_APP_TITLE": "青海锦淼工程机械有限公司",
      "VITE_BASE_API": "http://36.213.90.123:9532",
      "VITE_JAVA_API": "http://36.213.90.123:9533"
    },
    "logo": "logo/JMGCLogo.png",
    "favicon": "favicon/JMGCfavicon.ico"
  },
  "ZGLT": {
    "env": {
      "VITE_APP_TITLE": "中国联通",
      "VITE_BASE_API": "http://36.213.180.217:9017",
      "VITE_JAVA_API": "http://36.213.180.217:9018"
    },
    "logo": "logo/ZGLTLogo.png",
    "favicon": "favicon/ZGLTico.ico"
  },
  "logo": "/src/assets/logo/logo.png",
  "favicon": "/public/favicon.ico"
}
}
src/api/reportAnalysis/safetyDashboard.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
import request from '@/utils/request'
// é¡¶éƒ¨æ€»è§ˆ
export function getSafetyOverview() {
  return request({
    url: '/safeDashboard/getOverview',
    method: 'get'
  })
}
// æœ€æ–°å®‰å…¨åŠ¨æ€
export function getLatestSafeEvents(params) {
  return request({
    url: '/safeDashboard/getLatestEvents',
    method: 'get',
    params
  })
}
// é𐿂£ç±»åž‹åˆ†å¸ƒ
export function getHiddenTypeDistribution() {
  return request({
    url: '/safeDashboard/getHiddenTypeDistribution',
    method: 'get'
  })
}
// è¿‘6个月隐患趋势
export function getHiddenTrend() {
  return request({
    url: '/safeDashboard/getHiddenTrend',
    method: 'get'
  })
}
// å±é™©æºé£Žé™©ç­‰çº§åˆ†å¸ƒ
export function getHazardRiskDistribution() {
  return request({
    url: '/safeDashboard/getHazardRiskDistribution',
    method: 'get'
  })
}
// å±é™©æºç±»åž‹æŽ’名
export function getHazardTypeRanking(params) {
  return request({
    url: '/safeDashboard/getHazardTypeRanking',
    method: 'get',
    params
  })
}
// é𐿂£æ•´æ”¹è´£ä»»äººæŽ’名
export function getHiddenRectifyRanking(params) {
  return request({
    url: '/safeDashboard/getHiddenRectifyRanking',
    method: 'get',
    params
  })
}
src/assets/logo/SHYY.png

src/components/PIMTable/PIMTable.vue
@@ -179,15 +179,19 @@
             class="cell link"
             style="width: 100%"
             @click="goLink(scope.row, item.linkMethod)">
          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
          <span v-if="!item.formatData">{{
            toDateOnly(scope.row[item.prop])
          }}</span>
        </div>
        <!-- é»˜è®¤çº¯å±•示数据 -->
        <div v-else
             class="cell"
             style="width: 100%">
          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
          <span v-if="!item.formatData">{{
            toDateOnly(scope.row[item.prop])
          }}</span>
          <span v-else>{{
            formatters(scope.row[item.prop], item.formatData)
            toDateOnly(formatters(scope.row[item.prop], item.formatData))
          }}</span>
        </div>
      </template>
@@ -205,6 +209,7 @@
  import pagination from "./Pagination.vue";
  import { computed, ref, inject, getCurrentInstance } from "vue";
  import { ElMessage } from "element-plus";
  import { toDateOnly } from "@/utils/dateDisplay";
  // èŽ·å–å…¨å±€çš„ uploadHeader
  const { proxy } = getCurrentInstance();
src/main.js
@@ -58,6 +58,7 @@
import PageHeader from "@/components/PageHeader/index.vue";
import { getToken } from "@/utils/auth";
import { toDateOnly } from "@/utils/dateDisplay";
import {
  calculateTaxExclusiveTotalPrice,
  summarizeTable,
@@ -80,6 +81,7 @@
app.config.globalProperties.addDateRange = addDateRange;
app.config.globalProperties.selectDictLabel = selectDictLabel;
app.config.globalProperties.selectDictLabels = selectDictLabels;
app.config.globalProperties.formatDateOnly = toDateOnly;
app.config.globalProperties.javaApi = __BASE_API__;
app.config.globalProperties.HaveJson = (val) => {
  return JSON.parse(JSON.stringify(val));
@@ -116,4 +118,19 @@
});
app._context.components.ElDialog.props.closeOnClickModal.default = false;
// åˆ—表时间统一只展示年月日:未单独配置 formatter çš„表格列走这里
const tableColumnFormatter =
  app._context.components.ElTableColumn?.props?.formatter;
if (tableColumnFormatter) {
  const formatCell = (row, column, cellValue) => toDateOnly(cellValue);
  if (typeof tableColumnFormatter === "function") {
    app._context.components.ElTableColumn.props.formatter = {
      type: Function,
      default: formatCell,
    };
  } else {
    tableColumnFormatter.default = formatCell;
  }
}
app.mount("#app");
src/utils/dateDisplay.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
/**
 * åˆ—表展示用的日期处理:带时分秒的日期时间只保留年月日,
 * æœ¬æ¥å°±æ˜¯æ—¥æœŸã€æ—¶é—´æˆ–其它内容的值原样返回。
 */
const DATE_TIME_PATTERN =
  /^(\d{4})[-/å¹´](\d{1,2})[-/月](\d{1,2})日?[ T]\d{1,2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/;
function pad2(value) {
  return String(value).padStart(2, "0");
}
export function toDateOnly(value) {
  if (value == null || value === "") return value;
  if (value instanceof Date) {
    if (Number.isNaN(value.getTime())) return value;
    return `${value.getFullYear()}-${pad2(value.getMonth() + 1)}-${pad2(
      value.getDate()
    )}`;
  }
  if (typeof value !== "string") {
    // å¯¹è±¡/数组按表格原有方式转成文本,避免把非字符串直接交给渲染层
    return typeof value === "object" ? String(value) : value;
  }
  const text = value.trim();
  const matched = text.match(DATE_TIME_PATTERN);
  if (!matched) return value;
  return `${matched[1]}-${pad2(matched[2])}-${pad2(matched[3])}`;
}
src/utils/index.js
@@ -12,14 +12,7 @@
      ? "0" + (date.getMonth() + 1)
      : date.getMonth() + 1;
  var day = date.getDate() < 10 ? "0" + date.getDate() : date.getDate();
  var hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours();
  var minutes =
    date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
  var seconds =
    date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
  return (
    year + "-" + month + "-" + day + " " + hours + ":" + minutes + ":" + seconds
  );
  return year + "-" + month + "-" + day;
}
/**
src/utils/ruoyi.js
@@ -8,7 +8,7 @@
  if (arguments.length === 0 || !time) {
    return null
  }
  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
  const format = pattern || '{y}-{m}-{d}'
  let date
  if (typeof time === 'object') {
    date = time
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue
@@ -430,7 +430,11 @@
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
  const matched = String(dateTime).match(
    /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/
  )
  if (!matched) return dateTime
  return `${matched[1]}-${matched[2].padStart(2, '0')}-${matched[3].padStart(2, '0')}`
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
@@ -305,7 +305,11 @@
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
  const matched = String(dateTime).match(
    /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/
  )
  if (!matched) return dateTime
  return `${matched[1]}-${matched[2].padStart(2, '0')}-${matched[3].padStart(2, '0')}`
}
// æäº¤å®¡æ‰¹
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
@@ -302,7 +302,11 @@
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
  const matched = String(dateTime).match(
    /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/
  )
  if (!matched) return dateTime
  return `${matched[1]}-${matched[2].padStart(2, '0')}-${matched[3].padStart(2, '0')}`
}
// æäº¤å‘布
src/views/collaborativeApproval/notificationManagement/summary/index.vue
@@ -332,7 +332,11 @@
// æ ¼å¼åŒ–日期时间
const formatDateTime = (dateTime) => {
  if (!dateTime) return ''
  return dateTime.replace(' ', '\n')
  const matched = String(dateTime).match(
    /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/
  )
  if (!matched) return dateTime
  return `${matched[1]}-${matched[2].padStart(2, '0')}-${matched[3].padStart(2, '0')}`
}
// é¡µé¢åŠ è½½æ—¶èŽ·å–æ•°æ®
src/views/equipmentManagement/inspectionManagement/index.vue
@@ -197,7 +197,7 @@
      formatData: cell => {
        if (!cell) return "-";
        try {
          return dayjs(cell).format("YYYY-MM-DD HH:mm:ss");
          return dayjs(cell).format("YYYY-MM-DD");
        } catch {
          return cell;
        }
src/views/equipmentManagement/spareParts/index.vue
@@ -388,13 +388,14 @@
const resetQuery = () => {
  queryParams.name = '';
  pagination.current = 1;
  pagination.size = 10;
  fetchListData();
}
// åˆ†é¡µå¤§å°æ”¹å˜
const handleSizeChange = (size) => {
  pagination.size = size;
  pagination.current = 1;
const handleSizeChange = ({ page, limit }) => {
  pagination.current = page;
  pagination.size = limit;
  fetchListData();
}
src/views/equipmentManagement/upkeep/index.vue
@@ -335,8 +335,7 @@
      prop: "registrationDate",
      label: "登记日期",
      minWidth: 100,
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
      formatData: cell => (cell ? dayjs(cell).format("YYYY-MM-DD") : "-"),
    },
    {
      fixed: "right",
@@ -417,8 +416,7 @@
      label: "实际保养日期",
      align: "center",
      prop: "maintenanceActuallyTime",
      formatData: cell =>
        cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
      formatData: cell => (cell ? dayjs(cell).format("YYYY-MM-DD") : "-"),
    },
    {
      label: "保养结果",
src/views/financialManagement/payable/purchaseIn.vue
@@ -58,7 +58,7 @@
        }"
                @pagination="changePage">
        <template #inboundDate="{ row }">
          {{ row.inboundDate ?? row.InboundDate ?? "" }}
          {{ formatDateOnly(row.inboundDate ?? row.InboundDate ?? "") }}
        </template>
      </PIMTable>
    </div>
src/views/financialManagement/payable/reconciliation.vue
@@ -11,10 +11,19 @@
          />
        </el-select>
      </el-form-item>
      <el-form-item label="对账期间:">
        <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
        <span style="margin: 0 10px;">至</span>
        <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
      <el-form-item label="对账日期:">
        <el-date-picker
          v-model="filters.dateRange"
          type="daterange"
          placeholder="请选择对账日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          range-separator="至"
          clearable
          style="width: 300px;"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="onSearch">搜索</el-button>
@@ -66,7 +75,7 @@
    <FormDialog title="对账明细" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
      <div class="statement-header">
        <h3>{{ currentSupplier }} åº”付对账单</h3>
        <p>对账期间: {{ currentPeriod }}</p>
        <p>对账日期: {{ currentPeriod }}</p>
      </div>
      <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
        <el-table-column prop="date" label="日期" width="120" />
@@ -124,12 +133,13 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="对账月份" prop="statementMonth">
            <el-form-item label="对账日期" prop="statementMonth">
              <el-date-picker
                v-model="generateForm.statementMonth"
                type="month"
                placeholder="选择月份"
                value-format="YYYY-MM"
                type="date"
                placeholder="选择对账日期"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                style="width: 100%;"
                @change="onStatementMonthChange"
              />
@@ -139,7 +149,7 @@
      </el-form>
      <div v-if="statementDetailLoaded" class="purchase-section">
        <div v-if="purchaseData.length > 0" class="section-title">本月采购数据</div>
        <div v-if="purchaseData.length > 0" class="section-title">对账日期采购数据</div>
        <el-table
          v-if="purchaseData.length > 0"
          ref="purchaseTableRef"
@@ -165,7 +175,7 @@
          </el-table-column>
          <el-table-column prop="remark" label="备注" />
        </el-table>
        <el-empty v-else description="该供应商本月暂无明细数据" :image-size="80" />
        <el-empty v-else description="该供应商当日暂无明细数据" :image-size="80" />
        <div class="summary-row">
          <span>期初余额: <strong class="text-primary">Â¥{{ formatMoney(generateForm.openingBalance) }}</strong></span>
@@ -176,7 +186,7 @@
      </div>
      <div v-else-if="generateForm.supplierId && generateForm.statementMonth && !purchaseLoading" class="empty-tip">
        <el-empty description="该供应商本月暂无采购数据" />
        <el-empty description="该供应商当日暂无采购数据" />
      </div>
      <template #footer>
@@ -209,8 +219,7 @@
const filters = reactive({
  supplierId: "",
  startMonth: "",
  endMonth: "",
  dateRange: [],
});
const pagination = reactive({
@@ -222,7 +231,7 @@
const columns = [
  { label: "对账单号", prop: "statementNumber", width: "150" },
  { label: "供应商", prop: "supplierName", width: "180" },
  { label: "对账期间", prop: "statementMonth", width: "150" },
  { label: "对账日期", prop: "statementMonth", width: "150" },
  { label: "期初余额", prop: "openingBalance", dataType: "slot", slot: "openingBalance" },
  { label: "本期应付", prop: "currentPlan", dataType: "slot", slot: "currentPlan" },
  { label: "本期付款", prop: "currentActually", dataType: "slot", slot: "currentActually" },
@@ -358,13 +367,9 @@
  if (filters.supplierId) {
    result.customerId = filters.supplierId;
  }
  if (filters.startMonth && filters.endMonth && filters.startMonth === filters.endMonth) {
    result.statementMonth = filters.startMonth;
  } else if (filters.startMonth) {
    result.startMonth = filters.startMonth;
  }
  if (filters.endMonth && filters.startMonth !== filters.endMonth) {
    result.endMonth = filters.endMonth;
  if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
    result.startDate = filters.dateRange[0];
    result.endDate = filters.dateRange[1];
  }
  return result;
};
@@ -466,8 +471,7 @@
const resetFilters = () => {
  filters.supplierId = "";
  filters.startMonth = "";
  filters.endMonth = "";
  filters.dateRange = [];
  pagination.currentPage = 1;
  getTableData();
};
@@ -671,7 +675,7 @@
const viewDetail = (row) => {
  const partnerId = row.customerId ?? row.supplierId;
  if (!partnerId || !row.statementMonth) {
    ElMessage.warning("缺少供应商或对账月份,无法查询明细");
    ElMessage.warning("缺少供应商或对账日期,无法查询明细");
    return;
  }
src/views/financialManagement/receivable/reconciliation.vue
@@ -6,10 +6,19 @@
          <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item label="对账期间:">
        <el-date-picker v-model="filters.startMonth" type="month" placeholder="开始月份" value-format="YYYY-MM" style="width: 140px;" />
        <span style="margin: 0 10px;">至</span>
        <el-date-picker v-model="filters.endMonth" type="month" placeholder="结束月份" value-format="YYYY-MM" style="width: 140px;" />
      <el-form-item label="对账日期:">
        <el-date-picker
          v-model="filters.dateRange"
          type="daterange"
          placeholder="请选择对账日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          range-separator="至"
          clearable
          style="width: 300px;"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="getTableData">搜索</el-button>
@@ -62,7 +71,7 @@
    <FormDialog title="对账明细" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
      <div class="statement-header">
        <h3>{{ currentCustomer }} åº”收对账单</h3>
        <p>对账期间: {{ currentPeriod }}</p>
        <p>对账日期: {{ currentPeriod }}</p>
      </div>
      <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
        <el-table-column prop="date" label="日期" width="120" />
@@ -114,15 +123,15 @@
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="对账月份" prop="statementMonth">
              <el-date-picker v-model="generateForm.statementMonth" type="month" placeholder="选择月份" value-format="YYYY-MM" style="width: 100%;" @change="onStatementMonthChange" />
            <el-form-item label="对账日期" prop="statementMonth">
              <el-date-picker v-model="generateForm.statementMonth" type="date" placeholder="选择对账日期" value-format="YYYY-MM-DD" format="YYYY-MM-DD" style="width: 100%;" @change="onStatementMonthChange" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div v-if="statementDetailLoaded" class="sales-section">
        <div v-if="salesData.length > 0" class="section-title">本月销售数据</div>
        <div v-if="salesData.length > 0" class="section-title">对账日期销售数据</div>
        <el-table
          v-if="salesData.length > 0"
          ref="salesTableRef"
@@ -148,7 +157,7 @@
          </el-table-column>
          <el-table-column prop="remark" label="备注" />
        </el-table>
        <el-empty v-else description="该客户本月暂无明细数据" :image-size="80" />
        <el-empty v-else description="该客户当日暂无明细数据" :image-size="80" />
        <div class="summary-row">
          <span>期初余额: <strong class="text-primary">Â¥{{ formatMoney(generateForm.openingBalance) }}</strong></span>
@@ -159,7 +168,7 @@
      </div>
      <div v-else-if="generateForm.customerId && generateForm.statementMonth && !salesLoading" class="empty-tip">
        <el-empty description="该客户本月暂无销售数据" />
        <el-empty description="该客户当日暂无销售数据" />
      </div>
      <template #footer>
@@ -192,8 +201,7 @@
const filters = reactive({
  customerId: "",
  startMonth: "",
  endMonth: "",
  dateRange: [],
});
const pagination = reactive({
@@ -205,7 +213,7 @@
const columns = [
  { label: "对账单号", prop: "statementNumber", width: "150" },
  { label: "客户名称", prop: "customerName", width: "180" },
  { label: "对账期间", prop: "statementMonth", width: "150" },
  { label: "对账日期", prop: "statementMonth", width: "150" },
  { label: "期初余额", prop: "openingBalance", dataType: "slot", slot: "openingBalance" },
  { label: "本期应收", prop: "currentPlan", dataType: "slot", slot: "currentPlan" },
  { label: "本期收款", prop: "currentActually", dataType: "slot", slot: "currentActually" },
@@ -341,13 +349,9 @@
  if (filters.customerId) {
    result.customerId = filters.customerId;
  }
  if (filters.startMonth && filters.endMonth && filters.startMonth === filters.endMonth) {
    result.statementMonth = filters.startMonth;
  } else if (filters.startMonth) {
    result.startMonth = filters.startMonth;
  }
  if (filters.endMonth && filters.startMonth !== filters.endMonth) {
    result.endMonth = filters.endMonth;
  if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
    result.startDate = filters.dateRange[0];
    result.endDate = filters.dateRange[1];
  }
  return result;
};
@@ -444,8 +448,7 @@
const resetFilters = () => {
  filters.customerId = "";
  filters.startMonth = "";
  filters.endMonth = "";
  filters.dateRange = [];
  pagination.currentPage = 1;
  getTableData();
};
@@ -645,7 +648,7 @@
const viewDetail = (row) => {
  if (!row.customerId || !row.statementMonth) {
    ElMessage.warning("缺少客户或对账月份,无法查询明细");
    ElMessage.warning("缺少客户或对账日期,无法查询明细");
    return;
  }
src/views/index.vue
@@ -222,7 +222,7 @@
            <li v-for="item in todoList" :key="item.id" class="todo-item">
              <div class="todo-head">
                <span class="todo-no">待办编号:{{ item.approveId }}</span>
                <span class="todo-time">{{ item.approveTime }}</span>
                <span class="todo-time">{{ formatDateOnly(item.approveTime) }}</span>
              </div>
              <div class="todo-desc">部门:{{ item.approveDeptName }}</div>
              <div class="todo-desc">事由:{{ item.approveReason }}</div>
@@ -289,7 +289,7 @@
              </div>
              <div class="plan-meta">
                <span>计划 {{ item.planQty }}</span>
                <span>交期 {{ item.deliveryDate }}</span>
                <span>交期 {{ formatDateOnly(item.deliveryDate) }}</span>
              </div>
            </li>
          </ul>
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue
@@ -54,7 +54,11 @@
            label="最近更新时间"
            prop="updateTime"
            show-overflow-tooltip
          />
          >
            <template #default="scope">
              {{ formatDateOnly(scope.row.updateTime || scope.row.createTime) }}
            </template>
          </el-table-column>
          <el-table-column fixed="right" label="操作" min-width="180" align="center">
            <template #default="scope">
              <el-button
src/views/inventoryManagement/stockManagement/Qualified.vue
@@ -31,7 +31,11 @@
        <el-table-column label="冻结数量" prop="lockedQuantity" show-overflow-tooltip />
        <el-table-column label="库存预警数量" prop="warnNum"  show-overflow-tooltip />
        <el-table-column label="备注" prop="remark"  show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip>
          <template #default="scope">
            {{ formatDateOnly(scope.row.updateTime || scope.row.createTime) }}
          </template>
        </el-table-column>
        <el-table-column fixed="right" label="操作" min-width="90" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="showSubtractModal(scope.row)" :disabled="scope.row.unLockedQuantity === 0">领用</el-button>
src/views/inventoryManagement/stockManagement/Record.vue
@@ -107,7 +107,11 @@
                         show-overflow-tooltip />
        <el-table-column label="最近更新时间"
                         prop="updateTime"
                         show-overflow-tooltip />
                         show-overflow-tooltip>
          <template #default="scope">
            {{ formatDateOnly(scope.row.updateTime || scope.row.createTime) }}
          </template>
        </el-table-column>
        <el-table-column fixed="right"
                         label="操作"
                         min-width="80"
src/views/inventoryManagement/stockManagement/Unqualified.vue
@@ -26,7 +26,11 @@
        <el-table-column label="库存数量" prop="qualitity" show-overflow-tooltip />
        <el-table-column label="冻结数量" prop="lockedQuantity" show-overflow-tooltip />
        <el-table-column label="备注" prop="remark"  show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip />
        <el-table-column label="最近更新时间" prop="updateTime" show-overflow-tooltip>
          <template #default="scope">
            {{ formatDateOnly(scope.row.updateTime || scope.row.createTime) }}
          </template>
        </el-table-column>
        <el-table-column fixed="right" label="操作" min-width="90" align="center">
          <template #default="scope">
            <el-button link type="primary" @click="showSubtractModal(scope.row)" :disabled="scope.row.unLockedQuantity === 0">领用</el-button>
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js
@@ -87,7 +87,15 @@
  const leadingCols = moduleKey && INSTANCE_NO_SEARCH_MODULE_KEYS.has(moduleKey) ? [INSTANCE_NO_TABLE_COLUMN] : [];
  return computed(() => {
    const formCols = getFormConfigFieldColumns(tableDataRef.value?.[0], { excludeKeys });
    // å›ºå®šåˆ—已展示的字段不再由 formConfig åŠ¨æ€åˆ—é‡å¤ç”Ÿæˆ
    const fixedProps = new Set(
      [...beforeFormColumns, ...extraColumns, ...afterFormColumns]
        .map(c => c?.prop)
        .filter(Boolean)
    );
    const formCols = getFormConfigFieldColumns(tableDataRef.value?.[0], {
      excludeKeys: new Set([...(excludeKeys || []), ...fixedProps]),
    });
    return [
      ...leadingCols,
      ...beforeFormColumns,
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceListSearch.js
@@ -8,6 +8,7 @@
  APPROVAL_MODULE_KEYS.WORK_HANDOVER,
  APPROVAL_MODULE_KEYS.LEAVE,
  APPROVAL_MODULE_KEYS.OVERTIME,
  APPROVAL_MODULE_KEYS.OUT_OFFICE,
]);
export const INSTANCE_NO_TABLE_COLUMN = {
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
@@ -9,6 +9,7 @@
  WORK_HANDOVER: "work_handover",
  LEAVE: "leave",
  OVERTIME: "overtime",
  OUT_OFFICE: "out_office",
  TRAVEL_REIMBURSE: "travel_reimburse",
  COST_REIMBURSE: "cost_reimburse",
  ENTERPRISE_NEWS: "enterprise_news",
@@ -21,6 +22,7 @@
  [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: 13,
  [APPROVAL_MODULE_KEYS.LEAVE]: 14,
  [APPROVAL_MODULE_KEYS.OVERTIME]: 15,
  [APPROVAL_MODULE_KEYS.OUT_OFFICE]: 1,
  [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: 16,
  [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: 17,
  [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: 18,
@@ -63,6 +65,12 @@
    businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.OVERTIME],
    typeLabels: ["加班", "加班申请", "加班审批"],
  },
  [APPROVAL_MODULE_KEYS.OUT_OFFICE]: {
    label: "公出申请",
    approvalType: "out_office",
    businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.OUT_OFFICE],
    typeLabels: ["公出", "公出申请", "公出管理"],
  },
  [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: {
    label: "差旅报销",
    approvalType: "travel_reimburse",
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -159,16 +159,16 @@
  if (val == null || val === "") return "";
  if (Array.isArray(val) && val.length >= 3) {
    const [y, m, d, h = 0, min = 0, s = 0] = val;
    return dayjs(new Date(y, m - 1, d, h, min, s)).format("YYYY-MM-DD HH:mm:ss");
    return dayjs(new Date(y, m - 1, d, h, min, s)).format("YYYY-MM-DD");
  }
  if (typeof val === "number") {
    const d = val > 1e12 ? dayjs(val) : dayjs.unix(val);
    return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : "";
    return d.isValid() ? d.format("YYYY-MM-DD") : "";
  }
  const s = String(val).trim();
  if (!s) return "";
  const parsed = dayjs(s.includes("T") ? s : s.replace(/-/g, "/"));
  return parsed.isValid() ? parsed.format("YYYY-MM-DD HH:mm:ss") : s;
  return parsed.isValid() ? parsed.format("YYYY-MM-DD") : s;
}
export function formatDisplayTime(val) {
@@ -270,6 +270,9 @@
  };
  const kw = (searchForm?.keyword || "").trim();
  if (kw) params.templateName = kw;
  if (searchForm?.businessType != null && searchForm.businessType !== "") {
    params.businessType = searchForm.businessType;
  }
  if (searchForm?.enabledOnly) params.enabled = "1";
  return params;
}
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -26,6 +26,22 @@
        />
        <span class="search_title" style="margin-left: 12px">模板类型:</span>
        <el-select
          v-model="searchForm.businessType"
          placeholder="请选择模板类型"
          clearable
          filterable
          style="width: 180px"
          @change="handleQuery"
        >
          <el-option
            v-for="opt in templateTypeOptions"
            :key="`search-biz-type-${opt.value}`"
            :label="opt.label"
            :value="opt.value"
          />
        </el-select>
        <el-checkbox v-model="searchForm.enabledOnly" class="ml12" @change="handleQuery">
          ä»…显示启用
@@ -422,6 +438,7 @@
import { ElMessage } from "element-plus";
import { computed, nextTick, onMounted, ref } from "vue";
import { useRoute } from "vue-router";
import { userListNoPageByTenantId } from "@/api/system/user.js";
@@ -495,6 +512,8 @@
} = useApproveTemplate();
const route = useRoute();
const flowUserOptions = ref([]);
@@ -614,11 +633,17 @@
onMounted(() => {
onMounted(async () => {
  loadUsers();
  loadTemplateTypeOptions();
  const queryBusinessType = route.query.businessType;
  if (queryBusinessType != null && queryBusinessType !== "") {
    searchForm.businessType = Number(queryBusinessType);
  }
  await loadTemplateTypeOptions();
  fetchTemplateList();
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -49,6 +49,7 @@
  const searchForm = reactive({
    keyword: "",
    businessType: "",
    enabledOnly: false,
  });
@@ -176,6 +177,7 @@
  function resetSearch() {
    searchForm.keyword = "";
    searchForm.businessType = "";
    searchForm.enabledOnly = false;
    handleQuery();
  }
src/views/officeProcessAutomation/AttendManage/out-apply/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,174 @@
<!--OA模块:公出申请-->
<template>
  <div class="app-container">
    <div class="search_form mb20">
      <div>
        <span class="search_title">审批单号:</span>
        <el-input
          v-model="searchForm.instanceNo"
          style="width: 220px"
          placeholder="请输入审批单号"
          clearable
          @keyup.enter="onSearch"
        />
        <span class="search_title" style="margin-left: 12px">申请人:</span>
        <el-input
          v-model="searchForm.applicantKeyword"
          style="width: 220px"
          placeholder="姓名或编号"
          clearable
          :prefix-icon="Search"
          @keyup.enter="onSearch"
        />
        <el-button type="primary" style="margin-left: 10px" @click="onSearch">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div class="search_actions">
        <el-button type="primary" @click="openAddWithTemplate">新增公出申请</el-button>
      </div>
    </div>
    <div class="table_list">
      <PIMTable
        rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="false"
        :tableLoading="tableLoading"
        @pagination="onPagination"
        :total="page.total"
      />
    </div>
    <ApprovalInstanceSubmitDialog
      v-model="submitDialog.visible"
      :title="submitDialogTitle"
      :form="submitForm"
      :rules="submitFormRules"
      :fields="submitFormFields"
      :active-template="activeTemplate"
      :user-options="flowUserOptions"
      :is-edit="isSubmitEdit"
      :saving="submitSaving"
      :form-ref="submitFormRef"
      @submit="onSubmit"
    />
    <ApprovalTemplateBindDialog
      v-model:visible="templateBindVisible"
      :module-key="APPROVAL_MODULE_KEYS.OUT_OFFICE"
      skip-form-confirm
      @confirm="onTemplateBound"
      @closed="onTemplateBindClosed"
    />
    <ApprovalInstanceDetailDialog
      v-model="detailDialog.visible"
      title="公出申请详情"
      :row="detailRow"
      @edit="openEditFromDetail"
    />
  </div>
</template>
<script setup>
import { Search } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { onMounted, reactive } from "vue";
import ApprovalInstanceDetailDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue";
import ApprovalInstanceSubmitDialog from "../../ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue";
import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
import { buildInstanceTableColumns } from "../../ApproveManage/approve-shared/approvalInstanceFormConfigTable.js";
import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
import { useApprovalInstanceModule } from "../../ApproveManage/approve-shared/useApprovalInstanceModule.js";
import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
const searchForm = reactive({
  instanceNo: "",
  applicantKeyword: "",
});
const mod = useApprovalInstanceModule({
  moduleKey: APPROVAL_MODULE_KEYS.OUT_OFFICE,
});
const {
  tableData,
  tableLoading,
  page,
  detailDialog,
  detailRow,
  submitDialog,
  submitForm,
  submitFormRef,
  submitSaving,
  isSubmitEdit,
  activeTemplate,
  submitFormFields,
  submitFormRules,
  submitDialogTitle,
  templateBindVisible,
  handleQuery,
  initModuleList,
  pagination,
  openAddWithTemplate,
  onTemplateBound,
  onTemplateBindClosed,
  openEditFromDetail,
  submitInstanceForm,
  buildTableActions,
} = mod;
const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, {
  moduleKey: APPROVAL_MODULE_KEYS.OUT_OFFICE,
});
function onSearch() {
  handleQuery(searchForm);
}
function resetSearch() {
  searchForm.instanceNo = "";
  searchForm.applicantKeyword = "";
  onSearch();
}
function onPagination(obj) {
  pagination(obj, searchForm);
}
async function onSubmit() {
  const ok = await submitInstanceForm({ skipValidate: true });
  if (ok) ElMessage.success(isSubmitEdit.value ? "修改成功" : "提交成功");
}
onMounted(async () => {
  loadFlowUsers();
  await initModuleList(searchForm);
});
</script>
<style scoped>
.mb20 {
  margin-bottom: 20px;
}
.search_form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.search_actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.search_title {
  font-size: 14px;
  color: var(--el-text-color-regular);
}
</style>
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
@@ -130,6 +130,14 @@
const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, {
  moduleKey: APPROVAL_MODULE_KEYS.REGULAR,
  beforeFormColumns: [
    {
      label: "申请人",
      prop: "applicantName",
      width: 120,
      showOverflowTooltip: true,
    },
  ],
});
function onSearch() {
src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue
@@ -17,6 +17,9 @@
              clearable
              style="width: 100%"
            >
              <el-option label="初中" value="middle_school" />
              <el-option label="高中" value="high_school" />
              <el-option label="中专" value="technical_secondary" />
              <el-option label="中专及以下" value="secondary" />
              <el-option label="大专" value="junior_college" />
              <el-option label="本科" value="bachelor" />
src/views/personnelManagement/contractManagement/index.vue
@@ -169,16 +169,6 @@
      prop: "phone",
      width: 150,
    },
    {
      label: "紧急联系人",
      prop: "emergencyContact",
      width: 120,
    },
    {
      label: "紧急联系人电话",
      prop: "emergencyContactPhone",
      width: 150,
    },
    // {
    //   label: "合同年限",
    //   prop: "contractTerm",
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue
@@ -17,6 +17,9 @@
              clearable
              style="width: 100%"
            >
              <el-option label="初中" value="middle_school" />
              <el-option label="高中" value="high_school" />
              <el-option label="中专" value="technical_secondary" />
              <el-option label="中专及以下" value="secondary" />
              <el-option label="大专" value="junior_college" />
              <el-option label="本科" value="bachelor" />
src/views/personnelManagement/monthlyStatistics/index.vue
@@ -93,6 +93,64 @@
      @close="auditDialogVisible = false"
      @success="handleAuditSuccess"
    />
    <el-dialog v-model="salaryDetailVisible" title="人员工资明细" width="1100px">
      <div class="salary-detail-title">
        {{ salaryDetailRow.salaryTitle || "-" }}({{ salaryDetailRow.salaryMonth || "-" }})
      </div>
      <el-table :data="salaryDetailRow.staffSalaryDetailList || []" border style="width: 100%">
        <el-table-column prop="staffName" label="姓名" width="90" />
        <el-table-column prop="deptName" label="部门" width="110" />
        <el-table-column prop="postName" label="岗位" width="110" />
        <el-table-column prop="basicSalary" label="基本工资" width="100" align="right" />
        <el-table-column prop="pieceSalary" label="计件工资" width="100" align="right" />
        <el-table-column prop="otherIncome" label="其他收入" width="100" align="right" />
        <el-table-column prop="socialPersonal" label="社保(个人)" width="100" align="right" />
        <el-table-column prop="fundPersonal" label="公积金(个人)" width="110" align="right" />
        <el-table-column prop="salaryTax" label="个税" width="90" align="right" />
        <el-table-column prop="grossSalary" label="应发工资" width="110" align="right">
          <template #default="{ row }">{{ formatAmount(row.grossSalary) }}</template>
        </el-table-column>
        <el-table-column prop="deductSalary" label="扣款合计" width="100" align="right" />
        <el-table-column prop="netSalary" label="实发工资" width="110" align="right">
          <template #default="{ row }">{{ formatAmount(row.netSalary) }}</template>
        </el-table-column>
      </el-table>
      <template #footer>
        <el-button @click="salaryDetailVisible = false">关闭</el-button>
      </template>
    </el-dialog>
    <el-dialog v-model="issueRecordVisible" title="发放记录" width="820px" append-to-body>
      <template v-if="issueRecordRow">
        <el-descriptions :column="2" border>
          <el-descriptions-item label="工资主题">{{ issueRecordRow.salaryTitle }}</el-descriptions-item>
          <el-descriptions-item label="工资月份">{{ issueRecordRow.salaryMonth }}</el-descriptions-item>
          <el-descriptions-item label="工资总额">{{ formatAmount(issueRecordRow.totalSalary) }}</el-descriptions-item>
          <el-descriptions-item label="发放银行">{{ issueRecordRow.payBank || "-" }}</el-descriptions-item>
          <el-descriptions-item label="发放时间">{{ issueRecordRow.payTime || "-" }}</el-descriptions-item>
          <el-descriptions-item label="审核人">{{ issueRecordRow.auditUserName || "-" }}</el-descriptions-item>
          <el-descriptions-item label="备注" :span="2">{{ issueRecordRow.remark || "-" }}</el-descriptions-item>
        </el-descriptions>
        <div class="issue-record-title">发放明细({{ issueRecordDetails.length }} äººï¼‰</div>
        <el-table :data="issueRecordDetails" border size="small" max-height="360">
          <el-table-column prop="staffName" label="姓名" width="90" />
          <el-table-column prop="deptName" label="部门" width="110" />
          <el-table-column prop="postName" label="岗位" width="110" />
          <el-table-column label="应发工资" width="110" align="right">
            <template #default="{ row }">{{ formatAmount(row.grossSalary) }}</template>
          </el-table-column>
          <el-table-column label="扣款合计" width="110" align="right">
            <template #default="{ row }">{{ formatAmount(row.deductSalary) }}</template>
          </el-table-column>
          <el-table-column label="实发工资" width="110" align="right">
            <template #default="{ row }">{{ formatAmount(row.netSalary) }}</template>
          </el-table-column>
          <el-table-column prop="remark" label="备注" show-overflow-tooltip />
        </el-table>
      </template>
      <template #footer>
        <el-button @click="issueRecordVisible = false">关闭</el-button>
      </template>
    </el-dialog>
  </div>
</template>
@@ -149,6 +207,7 @@
  },
  { label: "工资总额", prop: "totalSalary", width: 120 },
  { label: "支付银行", prop: "payBank", width: 120 },
  { label: "发放时间", prop: "payTime", width: 160, formatData: (v) => v || "-" },
  { label: "审核人", prop: "auditUserName", width: 110 },
  { label: "备注", prop: "remark", minWidth: 120 },
  {
@@ -156,8 +215,13 @@
    label: "操作",
    align: "center",
    fixed: "right",
    width: 180,
    width: 240,
    operation: [
      {
        name: "查看明细",
        type: "text",
        clickFun: (row) => openSalaryDetail(row),
      },
      {
        name: "编辑",
        type: "text",
@@ -180,6 +244,12 @@
        type: "text",
        disabled: (row) => Number(row?.status) !== 4,
        clickFun: (row) => openIssue(row),
      },
      {
        name: "发放记录",
        type: "text",
        disabled: (row) => Number(row?.status) !== 5,
        clickFun: (row) => openIssueRecord(row),
      },
    ],
  },
@@ -209,6 +279,28 @@
const auditDialogVisible = ref(false);
const auditRow = ref(null);
const auditDiaRef = ref(null);
const salaryDetailVisible = ref(false);
const salaryDetailRow = ref({});
const issueRecordVisible = ref(false);
const issueRecordRow = ref(null);
const issueRecordDetails = computed(() =>
  Array.isArray(issueRecordRow.value?.staffSalaryDetailList)
    ? issueRecordRow.value.staffSalaryDetailList
    : []
);
const formatAmount = (v) => (v == null || v === "" ? "-" : Number(v).toFixed(2));
const openIssueRecord = (row) => {
  issueRecordRow.value = row || null;
  issueRecordVisible.value = true;
};
const openSalaryDetail = (row) => {
  salaryDetailRow.value = row || {};
  salaryDetailVisible.value = true;
};
const issueBankOptions = computed(() => {
  const options = Array.isArray(bankSetting.value?.options) ? bankSetting.value.options : [];
@@ -410,4 +502,13 @@
.table_list {
  margin-top: 20px;
}
.salary-detail-title {
  margin-bottom: 12px;
  font-weight: 600;
}
.issue-record-title {
  margin: 16px 0 8px;
  font-weight: 600;
  font-size: 14px;
}
</style>
src/views/procurementManagement/thePaymentLedger/index.vue
@@ -55,8 +55,7 @@
  {
    label: "登记日期",
    prop: "registrationtDate",
    formatData: cell =>
      cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    formatData: cell => (cell ? dayjs(cell).format("YYYY-MM-DD") : "-"),
  },
]);
const tableData = ref([]);
src/views/productionManagement/workOrder/index.vue
@@ -12,6 +12,15 @@
                    prefix-icon="Search" />
        </div>
        <div class="search-item">
          <span class="search_title">生产订单号:</span>
          <el-input v-model="searchForm.productOrderNpsNo"
                    style="width: 240px"
                    placeholder="请输入"
                    @change="handleQuery"
                    clearable
                    prefix-icon="Search" />
        </div>
        <div class="search-item">
          <el-button type="primary"
                     @click="handleQuery">搜索</el-button>
        </div>
@@ -467,6 +476,7 @@
  const data = reactive({
    searchForm: {
      workOrderNo: "",
      productOrderNpsNo: "",
    },
  });
  const { searchForm } = toRefs(data);
src/views/productionPlan/productionPlan/components/PIMTable.vue
@@ -183,16 +183,20 @@
             class="cell link"
             style="width: 100%"
             @click="goLink(scope.row, item.linkMethod)">
          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
          <span v-if="!item.formatData">{{
            toDateOnly(scope.row[item.prop])
          }}</span>
        </div>
        <!-- é»˜è®¤çº¯å±•示数据 -->
        <div v-else
             class="cell"
             :class="item.className || ''"
             style="width: 100%">
          <span v-if="!item.formatData">{{ scope.row[item.prop] }}</span>
          <span v-if="!item.formatData">{{
            toDateOnly(scope.row[item.prop])
          }}</span>
          <span v-else>{{
            formatters(scope.row[item.prop], item.formatData)
            toDateOnly(formatters(scope.row[item.prop], item.formatData))
          }}</span>
        </div>
      </template>
@@ -210,6 +214,7 @@
  import pagination from "../../../../components/PIMTable/Pagination.vue";
  import { ref, inject, getCurrentInstance } from "vue";
  import { ElMessage } from "element-plus";
  import { toDateOnly } from "@/utils/dateDisplay";
  // èŽ·å–å…¨å±€çš„ uploadHeader
  const { proxy } = getCurrentInstance();
src/views/qualityManagement/afterSalesTraceability/index.vue
@@ -123,7 +123,7 @@
      <el-table-column label="联系方式" prop="contact" width="140" />
      <el-table-column label="反馈时间" prop="feedbackTime" width="160">
        <template #default="scope">
          <span>{{ scope.row.feedbackTime }}</span>
          <span>{{ formatDateOnly(scope.row.feedbackTime) }}</span>
        </template>
      </el-table-column>
      <el-table-column label="问题描述" prop="problemDesc" show-overflow-tooltip />
src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -173,6 +173,7 @@
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                :isShowPagination="false"
                height="400">
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
src/views/qualityManagement/processInspection/components/formDia.vue
@@ -188,6 +188,7 @@
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                :isShowPagination="false"
                height="400">
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -190,6 +190,7 @@
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                :isShowPagination="false"
                height="400">
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -197,11 +197,6 @@
      width: 120,
    },
    {
      label: "检测单位",
      prop: "checkCompany",
      width: 120,
    },
    {
      label: "检测结果",
      prop: "checkResult",
      dataType: "tag",
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
@@ -97,7 +97,7 @@
              >
                <div class="todo-title">申请类型:{{ item.approveTypeName }}</div>
                <div class="todo-division">申请部门:{{ item.approveDeptName }}</div>
                <div class="todo-time">{{ item.approveTime }}</div>
                <div class="todo-time">{{ formatDateOnly(item.approveTime) }}</div>
              </div>
              
            </div>
src/views/reportAnalysis/safetyDashboard/components/center-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,217 @@
<template>
  <div>
    <PanelHeader title="危险源风险等级分布" />
    <div class="panel-item-customers">
      <div class="pie-chart-wrapper">
        <div class="pie-background" :style="{ backgroundImage: `url(${roseBorderImg})` }"></div>
        <Echarts
          ref="echartsRef"
          :chartStyle="chartStyle"
          :legend="pieLegend"
          :series="pieSeries"
          :tooltip="pieTooltip"
          :color="pieColors"
          :options="pieOptions"
          style="height: 320px"
        />
        <div v-if="showEmpty" class="empty-tip">暂无数据</div>
      </div>
      <span v-if="refreshFailed" class="refresh-error">刷新失败</span>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, inject, watch } from 'vue'
import { getHazardRiskDistribution } from '@/api/reportAnalysis/safetyDashboard.js'
import PanelHeader from '@/views/reportAnalysis/dataDashboard/components/PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import roseBorderImg from '@/assets/BI/玫瑰图边框.png'
const echartsRef = ref(null)
const riskRows = ref([])
const loaded = ref(false)
const loading = ref(false)
const refreshFailed = ref(false)
const pieColors = ['#ff5676', '#ffce64', '#43e8fc', '#82BAFF', '#D1E4F5']
const chartStyle = {
  width: '100%',
  height: '100%'
}
const total = computed(() =>
  riskRows.value.reduce((sum, item) => sum + (Number(item.value) || 0), 0)
)
const showEmpty = computed(() => loaded.value && !loading.value && total.value === 0)
const objectData = computed(() => {
  const map = {}
  riskRows.value.forEach((item) => {
    map[item.name] = item
  })
  return map
})
const pieLegend = computed(() => ({
  orient: 'vertical',
  top: 'center',
  left: '50%',
  itemGap: 30,
  icon: 'circle',
  data: riskRows.value.map((item, idx) => ({
    name: item.name,
    icon: 'circle',
    textStyle: {
      fontSize: 18,
      color: pieColors[idx % pieColors.length]
    }
  })),
  formatter: function (name) {
    const item = objectData.value[name]
    if (!item) return name
    return `{title|${name}}{value|${item.value}}{unit|处}{percent|${item.rate ?? '0.00'}}{unit|%}`
  },
  textStyle: {
    rich: {
      value: {
        color: '#43e8fc',
        fontSize: 18,
        fontWeight: 600,
        padding: [0, 10, 0, 30]
      },
      unit: {
        color: '#82baff',
        fontSize: 14,
        fontWeight: 600,
        padding: [0, 50, 0, 0]
      },
      percent: {
        color: '#43e8fc',
        fontSize: 18,
        fontWeight: 600,
        padding: [0, 10, 0, 0]
      },
      title: {
        fontSize: 18,
        padding: [0, 0, 0, 0]
      }
    }
  }
}))
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b}:{c} å¤„({d}%)'
}
const pieSeries = computed(() => [
  {
    name: '危险源风险等级',
    type: 'pie',
    radius: '70%',
    center: ['20%', '50%'],
    itemStyle: {
      borderColor: '#0a1c3a',
      borderWidth: 5
    },
    label: {
      show: false
    },
    minAngle: 15,
    data: riskRows.value.map((item) => ({
      name: item.name,
      value: Number(item.value) || 0,
      rate: item.rate
    })),
    animationType: 'scale',
    animationEasing: 'elasticOut'
  }
])
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' }
}
const loadData = async () => {
  if (loading.value) return
  loading.value = true
  try {
    const res = await getHazardRiskDistribution()
    riskRows.value = Array.isArray(res?.data) ? res.data : []
    refreshFailed.value = false
    loaded.value = true
  } catch (e) {
    console.error('获取危险源风险等级分布失败:', e)
    refreshFailed.value = true
  } finally {
    loading.value = false
  }
}
const safetyDashboardRefreshTick = inject('safetyDashboardRefreshTick', null)
if (safetyDashboardRefreshTick) {
  watch(safetyDashboardRefreshTick, () => {
    loadData()
  })
}
onMounted(() => {
  loadData()
})
</script>
<style scoped>
.panel-item-customers {
  position: relative;
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 370px;
  box-sizing: border-box;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
  background: transparent;
}
.pie-background {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-113%, -50%);
  width: 360px;
  height: 360px;
  background-size: contain;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  pointer-events: none;
}
.empty-tip {
  position: absolute;
  left: 0;
  right: 0;
  top: 44%;
  width: 40%;
  text-align: center;
  font-size: 15px;
  color: #8aa4c8;
  pointer-events: none;
}
.refresh-error {
  position: absolute;
  right: 16px;
  top: 8px;
  font-size: 12px;
  color: #ff9a9a;
}
</style>
src/views/reportAnalysis/safetyDashboard/components/center-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,432 @@
<template>
  <div class="center-top">
    <div class="stats-cards">
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">隐患总数</span>
          <span class="card-value">{{ overviewValue(overview.hiddenTotal) }}</span>
          <div class="card-sub">
            <span>待整改 {{ overviewValue(overview.hiddenPending) }}</span>
            <span class="card-sub-divider">/</span>
            <span>已整改 {{ overviewValue(overview.hiddenRectified) }}</span>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">危险源总数</span>
          <span class="card-value">{{ overviewValue(overview.hazardTotal) }}</span>
          <div class="card-sub">
            <span>重大风险 {{ overviewValue(overview.hazardMajor) }}</span>
          </div>
        </div>
      </div>
      <div class="stat-card">
        <img src="@/assets/BI/icon@2x.png" alt="图标" class="card-icon" />
        <div class="card-content">
          <span class="card-label">事故总数</span>
          <span class="card-value">{{ overviewValue(overview.accidentTotal) }}</span>
          <div class="card-sub">
            <span>本年 {{ overviewValue(overview.accidentThisYear) }}</span>
          </div>
        </div>
      </div>
    </div>
    <div class="event-info">
      <div class="event-header">
        <img src="@/assets/BI/shijianmingxiicon@2x.png" alt="图标" class="event-icon" />
        <span class="event-title">安全动态</span>
        <span v-if="refreshFailed" class="refresh-error">刷新失败</span>
      </div>
      <div class="metrics-row">
        <div class="metric" v-for="item in metrics" :key="item.label">
          <span class="metric-value">{{ item.value }}</span>
          <span class="metric-label">{{ item.label }}</span>
        </div>
      </div>
      <div class="event-content">
        <ul class="event-list" v-if="displayEvents.length > 0" ref="listRef">
          <li v-for="(item, index) in displayEvents" :key="index">
            <div class="event-line">
              <span class="event-cat" :class="catClass(item.category)">{{ item.category }}</span>
              <span class="event-name">{{ item.title }}</span>
              <span class="event-status">{{ item.statusText }}</span>
            </div>
            <div class="event-meta">
              <span>{{ item.location || '—' }}</span>
              <span>{{ item.eventTime }}</span>
            </div>
          </li>
        </ul>
        <div v-else class="event-empty">
          {{ eventsFailed ? '数据加载失败,60秒后自动重试' : '暂无安全动态' }}
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, nextTick, inject, watch } from 'vue'
import {
  getSafetyOverview,
  getLatestSafeEvents
} from '@/api/reportAnalysis/safetyDashboard.js'
const overview = ref({
  hiddenTotal: 0,
  hiddenPending: 0,
  hiddenRectified: 0,
  hazardTotal: 0,
  hazardMajor: 0,
  accidentTotal: 0,
  accidentThisYear: 0,
  trainingCount: 0,
  trainingPersonTimes: 0,
  planCount: 0,
  certCount: 0
})
const overviewLoaded = ref(false)
const overviewFailed = ref(false)
const events = ref([])
const eventsFailed = ref(false)
const listRef = ref(null)
const loading = ref(false)
const refreshFailed = computed(() => overviewFailed.value || eventsFailed.value)
const overviewValue = (val) => (overviewLoaded.value ? Number(val) || 0 : '--')
const metrics = computed(() => [
  { label: '培训场次', value: overviewValue(overview.value.trainingCount) },
  { label: '参训人次', value: overviewValue(overview.value.trainingPersonTimes) },
  { label: '应急预案', value: overviewValue(overview.value.planCount) },
  { label: '安全资质', value: overviewValue(overview.value.certCount) }
])
const displayEvents = computed(() => {
  if (events.value.length >= 4) {
    return [...events.value, ...events.value]
  }
  return events.value
})
const catClass = (category) => {
  if (category === '事故') return 'cat-accident'
  if (category === '危险物料领用') return 'cat-material'
  return 'cat-hidden'
}
let rafId = null
const stopScroll = () => {
  if (rafId) {
    cancelAnimationFrame(rafId)
    rafId = null
  }
}
const startScroll = () => {
  stopScroll()
  nextTick(() => {
    const el = listRef.value
    if (!el || events.value.length < 4) return
    const half = el.scrollHeight / 2
    if (half <= el.clientHeight) return
    let position = 0
    const step = () => {
      position += 0.4
      if (position >= half) {
        position = 0
        el.scrollTop = 0
      } else {
        el.scrollTop = position
      }
      rafId = requestAnimationFrame(step)
    }
    rafId = requestAnimationFrame(step)
  })
}
const loadOverview = async () => {
  try {
    const res = await getSafetyOverview()
    overview.value = {
      hiddenTotal: 0,
      hiddenPending: 0,
      hiddenRectified: 0,
      hazardTotal: 0,
      hazardMajor: 0,
      accidentTotal: 0,
      accidentThisYear: 0,
      trainingCount: 0,
      trainingPersonTimes: 0,
      planCount: 0,
      certCount: 0,
      ...(res?.data || {})
    }
    overviewLoaded.value = true
    overviewFailed.value = false
  } catch (e) {
    console.error('获取安全总览失败:', e)
    overviewFailed.value = true
  }
}
const loadEvents = async () => {
  try {
    const res = await getLatestSafeEvents({ limit: 10 })
    stopScroll()
    events.value = Array.isArray(res?.data) ? res.data : []
    eventsFailed.value = false
    startScroll()
  } catch (e) {
    console.error('获取最新安全动态失败:', e)
    eventsFailed.value = true
  }
}
const refreshData = async () => {
  if (loading.value) return
  loading.value = true
  await Promise.allSettled([loadOverview(), loadEvents()])
  loading.value = false
}
const safetyDashboardRefreshTick = inject('safetyDashboardRefreshTick', null)
if (safetyDashboardRefreshTick) {
  watch(safetyDashboardRefreshTick, () => {
    refreshData()
  })
}
onMounted(() => {
  refreshData()
})
onBeforeUnmount(() => {
  stopScroll()
})
</script>
<style scoped>
.center-top {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 20px;
}
.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: 8px;
}
.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: 19px;
  color: rgba(208, 231, 255, 0.7);
}
.card-sub {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 14px;
  color: #d0e7ff;
  opacity: 0.9;
}
.card-sub-divider {
  opacity: 0.5;
}
.event-info {
  flex: 1;
  min-height: 220px;
  display: flex;
  flex-direction: column;
  background-image: url('@/assets/BI/shijianmingchengbeijing@2x.png');
  background-size: 100% 100%;
  background-position: center;
  background-repeat: no-repeat;
  padding: 12px 20px 18px;
}
.event-header {
  display: flex;
  align-items: center;
  position: relative;
}
.event-icon {
  width: 40px;
  height: 40px;
}
.event-title {
  font-weight: 500;
  font-size: 18px;
  color: #fffffe;
  line-height: 30px;
}
.refresh-error {
  margin-left: 12px;
  font-size: 12px;
  color: #ff9a9a;
}
.metrics-row {
  display: flex;
  gap: 12px;
  margin: 8px 0 12px;
}
.metric {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 0;
  border-radius: 6px;
  background: rgba(24, 93, 190, 0.18);
  border: 1px solid rgba(64, 140, 235, 0.25);
}
.metric-value {
  font-size: 22px;
  font-weight: 600;
  color: #43e8fc;
}
.metric-label {
  font-size: 13px;
  color: rgba(208, 231, 255, 0.75);
}
.event-content {
  flex: 1;
  min-height: 0;
  overflow: hidden;
}
.event-list {
  list-style: none;
  padding: 0;
  margin: 0;
  height: 100%;
  overflow: hidden;
}
.event-list li {
  padding: 8px 12px;
  margin-bottom: 8px;
  border-radius: 6px;
  background: rgba(24, 93, 190, 0.12);
}
.event-line {
  display: flex;
  align-items: center;
  gap: 10px;
}
.event-cat {
  flex: none;
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 12px;
  line-height: 18px;
}
.cat-hidden {
  color: #4ee4ff;
  background: rgba(78, 228, 255, 0.15);
}
.cat-accident {
  color: #ff7f97;
  background: rgba(255, 127, 151, 0.15);
}
.cat-material {
  color: #ffce64;
  background: rgba(255, 206, 100, 0.15);
}
.event-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 15px;
  color: #fffffe;
}
.event-status {
  flex: none;
  font-size: 13px;
  color: rgba(208, 231, 255, 0.75);
}
.event-meta {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  padding-left: 2px;
  font-size: 12px;
  color: rgba(184, 200, 224, 0.7);
}
.event-empty {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  color: #8aa4c8;
}
</style>
src/views/reportAnalysis/safetyDashboard/components/left-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,206 @@
<template>
  <div>
    <PanelHeader title="近6个月隐患排查与整改趋势" />
    <div class="panel-item-customers">
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="legend"
        :series="series"
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="chartOptions"
        style="height: 380px"
      />
      <div v-if="showEmpty" class="empty-tip">近6个月暂无隐患数据</div>
      <span v-if="refreshFailed" class="refresh-error">刷新失败</span>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '@/views/reportAnalysis/dataDashboard/components/PanelHeader.vue'
import { getHiddenTrend } from '@/api/reportAnalysis/safetyDashboard.js'
const chart = ref(null)
const trendData = ref([])
const loaded = ref(false)
const loading = ref(false)
const refreshFailed = ref(false)
const chartStyle = {
  width: '100%',
  height: '100%'
}
const chartOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' }
}
const showEmpty = computed(() => {
  if (!loaded.value || loading.value) return false
  return trendData.value.every(
    (item) => !Number(item.reportedCount) && !Number(item.completedCount)
  )
})
const categories = computed(() =>
  trendData.value.map((item) => {
    const month = String(item.month || '')
    return month.length >= 7 ? `${month.slice(5)}月` : month
  })
)
const series = computed(() => [
  {
    name: '上报数',
    type: 'bar',
    barWidth: 18,
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 1,
        x2: 0,
        y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(0,164,237,0)' },
          { offset: 1, color: '#4EE4FF' }
        ]
      }
    },
    data: trendData.value.map((item) => Number(item.reportedCount) || 0)
  },
  {
    name: '整改完成数',
    type: 'bar',
    barWidth: 18,
    itemStyle: {
      color: {
        type: 'linear',
        x: 0,
        y: 1,
        x2: 0,
        y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(38,255,203,0)' },
          { offset: 1, color: '#26FFCB' }
        ]
      }
    },
    data: trendData.value.map((item) => Number(item.completedCount) || 0)
  }
])
const legend = {
  show: true,
  right: '4%',
  top: '0%',
  itemWidth: 10,
  itemHeight: 10,
  icon: 'circle',
  textStyle: { color: '#B8C8E0', fontSize: 13 },
  data: ['上报数', '整改完成数']
}
const grid = {
  left: '3%',
  right: '4%',
  bottom: '3%',
  top: '18%',
  containLabel: true
}
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  formatter: function (params) {
    let result = `${params[0].axisValueLabel}<br/>`
    params.forEach((item) => {
      result += `<div>${item.marker} ${item.seriesName}:${item.value} é¡¹</div>`
    })
    return result
  }
}
const xAxis = computed(() => [
  {
    type: 'category',
    axisTick: { show: false },
    axisLine: { lineStyle: { color: 'rgba(255,255,255,0.2)' } },
    axisLabel: { color: '#B8C8E0' },
    data: categories.value
  }
])
const yAxis = [
  {
    type: 'value',
    minInterval: 1,
    axisLabel: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } }
  }
]
const loadData = async () => {
  if (loading.value) return
  loading.value = true
  try {
    const res = await getHiddenTrend()
    trendData.value = Array.isArray(res?.data) ? res.data : []
    refreshFailed.value = false
    loaded.value = true
  } catch (e) {
    console.error('获取隐患趋势失败:', e)
    refreshFailed.value = true
  } finally {
    loading.value = false
  }
}
const safetyDashboardRefreshTick = inject('safetyDashboardRefreshTick', null)
if (safetyDashboardRefreshTick) {
  watch(safetyDashboardRefreshTick, () => {
    loadData()
  })
}
onMounted(() => {
  loadData()
})
</script>
<style scoped>
.panel-item-customers {
  position: relative;
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 478px;
  box-sizing: border-box;
}
.empty-tip {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  text-align: center;
  font-size: 16px;
  color: #8aa4c8;
  pointer-events: none;
}
.refresh-error {
  position: absolute;
  right: 16px;
  top: 8px;
  font-size: 12px;
  color: #ff9a9a;
}
</style>
src/views/reportAnalysis/safetyDashboard/components/left-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,241 @@
<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="pieLegend"
          :series="pieSeries"
          :tooltip="pieTooltip"
          :color="pieColors"
          :options="pieOptions"
          style="height: 100%"
        />
        <div v-if="showEmpty" class="empty-tip">暂无数据</div>
      </div>
      <span v-if="refreshFailed" class="refresh-error">刷新失败</span>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, onBeforeUnmount, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '@/views/reportAnalysis/dataDashboard/components/PanelHeader.vue'
import { useChartBackground } from '@/hooks/useChartBackground.js'
import { getHiddenTypeDistribution } from '@/api/reportAnalysis/safetyDashboard.js'
const pieWrapperRef = ref(null)
const pieBackgroundRef = ref(null)
const chart = ref(null)
const dataList = ref([])
const loaded = ref(false)
const loading = ref(false)
const refreshFailed = ref(false)
const pieColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
const showEmpty = computed(() => loaded.value && !loading.value && dataList.value.length === 0)
const dotRich = pieColors.reduce((acc, color, idx) => {
  acc[`dot${idx}`] = {
    width: 8,
    height: 8,
    borderRadius: 8,
    backgroundColor: color,
    align: 'center'
  }
  return acc
}, {})
const pieLegend = {
  show: false,
  data: []
}
const pieTooltip = {
  trigger: 'item',
  formatter: function (params) {
    return `${params.name}:${params.value} é¡¹ï¼ˆ${params.percent}%)`
  }
}
const pieSeries = computed(() => {
  if (!dataList.value.length) return []
  return [
    {
      name: '隐患类型',
      type: 'pie',
      radius: ['35%', '55%'],
      center: ['50%', '50%'],
      label: {
        show: true,
        position: 'outside',
        color: '#fff',
        fontSize: 12,
        lineHeight: 18,
        rich: {
          ...dotRich,
          parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20, overflow: 'break' },
          child: { fontSize: 12, color: '#fff', lineHeight: 18 }
        },
        formatter: function (params) {
          const children = params?.data?.children || []
          const parentName = params?.data?.name || ''
          const parentValue = Number(params?.data?.value) || 0
          const dotKey = `dot${(params?.dataIndex || 0) % pieColors.length}`
          const parentLine = `{${dotKey}|} {parent|${parentName} (${parentValue}项)}`
          if (!children.length) return parentLine
          const childLines = children.map(
            (c) => `{child|${c.name} ${Number(c.value) || 0}项}`
          )
          return [parentLine, ...childLines].join('\n')
        }
      },
      labelLine: {
        show: true,
        length: 20,
        length2: 20,
        lineStyle: {
          color: '#B8C8E0'
        }
      },
      itemStyle: {
        color: function (params) {
          return pieColors[params.dataIndex % pieColors.length]
        }
      },
      data: dataList.value
    },
    {
      type: 'pie',
      radius: ['35%', '40%'],
      center: ['50%', '50%'],
      silent: true,
      label: { show: false },
      labelLine: { show: false },
      itemStyle: {
        color: 'rgba(0, 127, 255, 0.25)'
      },
      data: [1]
    }
  ]
})
const chartStyle = {
  width: '100%',
  height: '126%'
}
const pieOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' }
}
const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({
  wrapperRef: pieWrapperRef,
  backgroundRef: pieBackgroundRef,
  offsetX: '-51.5%',
  offsetY: '-39%',
  watchData: dataList
})
const loadData = async () => {
  if (loading.value) return
  loading.value = true
  try {
    const res = await getHiddenTypeDistribution()
    const items = res?.data || []
    dataList.value = items.map((it) => ({
      name: it.name || '未知类型',
      value: Number(it.value) || 0,
      rate: it.rate,
      children: Array.isArray(it.children) ? it.children.map((c) => ({
        name: c.name,
        value: Number(c.value) || 0,
        rate: c.rate
      })) : []
    }))
    refreshFailed.value = false
    loaded.value = true
    adjustBackgroundPosition()
  } catch (e) {
    console.error('获取隐患类型分布失败:', e)
    refreshFailed.value = true
  } finally {
    loading.value = false
  }
}
const safetyDashboardRefreshTick = inject('safetyDashboardRefreshTick', null)
if (safetyDashboardRefreshTick) {
  watch(safetyDashboardRefreshTick, () => {
    loadData()
  })
}
onMounted(() => {
  loadData()
  initBackground()
})
onBeforeUnmount(() => {
  cleanupBackground()
})
</script>
<style scoped>
.panel-item-customers {
  position: relative;
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 420px;
  box-sizing: border-box;
}
.pie-chart-wrapper {
  position: relative;
  width: 100%;
  height: 320px;
  background: transparent;
}
.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%);
}
.empty-tip {
  position: absolute;
  left: 0;
  right: 0;
  top: 48%;
  text-align: center;
  font-size: 16px;
  color: #8aa4c8;
  pointer-events: none;
}
.refresh-error {
  position: absolute;
  right: 16px;
  top: 8px;
  font-size: 12px;
  color: #ff9a9a;
}
</style>
src/views/reportAnalysis/safetyDashboard/components/right-bottom.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
<template>
  <div>
    <PanelHeader title="隐患整改责任人排名" />
    <div class="panel-item-customers">
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :series="series"
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="chartOptions"
        style="height: 390px"
      />
      <div v-if="showEmpty" class="empty-tip">暂无数据</div>
      <span v-if="refreshFailed" class="refresh-error">刷新失败</span>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '@/views/reportAnalysis/dataDashboard/components/PanelHeader.vue'
import { getHiddenRectifyRanking } from '@/api/reportAnalysis/safetyDashboard.js'
const chart = ref(null)
const rows = ref([])
const loaded = ref(false)
const loading = ref(false)
const refreshFailed = ref(false)
const aircraft =
  'path://M107.000,71.000 C104.936,71.000 102.665,70.806 100.273,70.467 C94.592,76.922 86.275,81.000 77.000,81.000 C70.794,81.000 65.020,79.170 60.172,76.029 C66.952,74.165 72.647,69.714 76.173,63.817 C69.821,61.362 64.063,58.593 60.000,56.039 L60.000,52.813 C70.456,53.950 80.723,55.000 83.000,55.000 C88.972,55.000 93.000,53.723 93.000,50.000 C93.000,47.071 89.222,45.000 83.000,45.000 C80.723,45.000 70.456,46.050 60.000,47.187 L60.000,43.989 C64.057,41.431 69.807,38.644 76.168,36.173 C72.641,30.281 66.948,25.834 60.172,23.971 C65.020,20.830 70.794,19.000 77.000,19.000 C86.270,19.000 94.584,23.074 100.265,29.524 C102.647,29.191 104.918,29.000 107.000,29.000 C129.644,29.000 148.000,50.000 148.000,50.000 C148.000,50.000 129.644,71.000 107.000,71.000 ZM113.000,38.000 C106.373,38.000 101.000,43.373 101.000,50.000 C101.000,56.627 106.373,62.000 113.000,62.000 C119.627,62.000 125.000,56.627 125.000,50.000 C125.000,43.373 119.627,38.000 113.000,38.000 ZM113.000,56.000 C109.686,56.000 107.000,53.314 107.000,50.000 C107.000,46.686 109.686,44.000 113.000,44.000 C116.314,44.000 119.000,46.686 119.000,50.000 C119.000,53.314 116.314,56.000 113.000,56.000 ZM110.500,19.000 C109.567,19.000 108.763,18.483 108.334,17.726 C100.231,9.857 89.187,5.000 77.000,5.000 C64.813,5.000 53.769,9.857 45.666,17.726 C45.237,18.483 44.433,19.000 43.500,19.000 C42.119,19.000 41.000,17.881 41.000,16.500 C41.000,15.847 41.256,15.259 41.665,14.813 L41.575,14.718 C50.629,5.628 63.156,-0.000 77.000,-0.000 C90.844,-0.000 103.371,5.628 112.425,14.718 L112.335,14.813 C112.744,15.259 113.000,15.847 113.000,16.500 C113.000,17.881 111.881,19.000 110.500,19.000 ZM53.000,49.484 C61.406,48.626 77.810,47.000 81.345,47.000 C87.353,47.000 91.000,48.243 91.000,50.000 C91.000,52.234 87.111,53.000 81.345,53.000 C77.810,53.000 61.406,51.374 53.000,50.516 L53.000,49.484 ZM53.000,47.000 L9.000,50.000 L53.000,53.000 L53.000,56.000 L-0.000,50.000 L53.000,44.000 L53.000,47.000 ZM43.500,81.000 C44.433,81.000 45.237,81.517 45.666,82.274 C53.769,90.143 64.813,95.000 77.000,95.000 C89.187,95.000 100.231,90.143 108.334,82.274 C108.763,81.517 109.567,81.000 110.500,81.000 C111.881,81.000 113.000,82.119 113.000,83.500 C113.000,84.153 112.744,84.741 112.335,85.187 L112.425,85.282 C103.371,94.372 90.844,100.000 77.000,100.000 C63.156,100.000 50.629,94.372 41.575,85.282 L41.665,85.187 C41.256,84.741 41.000,84.153 41.000,83.500 C41.000,82.119 42.119,81.000 43.500,81.000 Z'
const color = {
  0: '#ff5676',
  1: '#ffd83e',
  2: '#fbff94',
  3: '#7daeff'
}
const chartStyle = {
  width: '100%',
  height: '100%'
}
const chartOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' }
}
const sortedRows = computed(() =>
  [...rows.value].sort((a, b) => (Number(a.value) || 0) - (Number(b.value) || 0))
)
const showEmpty = computed(
  () => loaded.value && !loading.value && rows.value.length === 0
)
const dataNum = computed(() =>
  sortedRows.value.map((item, i) => {
    const index = sortedRows.value.length - i - 1
    const isTop3 = index < 3
    return {
      value: Number(item.value) || 0,
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            { offset: 0, color: isTop3 ? '#ffdae1' : '#ecf3ff' },
            { offset: 0.07, color: isTop3 ? color[index] : color[3] },
            { offset: 1, color: isTop3 ? 'rgba(255, 86, 118, .1)' : 'rgba(125,174,255, .1)' }
          ],
          global: false
        },
        barBorderRadius: [0, 20, 20, 0]
      },
      symbol: isTop3 ? aircraft : 'none',
      symbolPosition: 'end',
      symbolSize: [30, 25],
      symbolOffset: [35, 0]
    }
  })
)
const bgData = computed(() => {
  const maxValue = Math.max(1, ...dataNum.value.map((v) => v.value))
  return dataNum.value.map(() => maxValue * 1.25)
})
const tooltip = computed(() => ({
  trigger: 'axis',
  textStyle: { fontSize: '100%' },
  formatter: function (params) {
    const index = params[0]?.dataIndex ?? 0
    const row = sortedRows.value[index] || {}
    return `${params[0]?.axisValueLabel || ''}<br/>已整改:${row.value ?? 0} é¡¹<br/>占比:${row.rate ?? '0.00'}%`
  }
}))
const grid = { top: 0, left: '22%', right: '12%', bottom: 0 }
const xAxis = [
  {
    splitLine: { show: false },
    axisLine: { show: false },
    axisLabel: { show: false },
    axisTick: { show: false }
  }
]
const yAxis = computed(() => [
  {
    type: 'category',
    data: sortedRows.value.map((item) => item.name),
    axisLabel: {
      formatter: (value) => {
        if (!value) return ''
        const maxLen = 6
        if (value.length <= maxLen) return `{a|${value}}`
        const lines = []
        for (let i = 0; i < value.length; i += maxLen) {
          lines.push(value.slice(i, i + maxLen))
        }
        return lines.map((line) => `{a|${line}}`).join('\n')
      },
      rich: {
        a: {
          width: 120,
          fontSize: 14,
          color: '#fff',
          padding: [5, 4, 5, 0],
          align: 'right'
        }
      }
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false }
  },
  {
    type: 'category',
    data: sortedRows.value.map((item) => Number(item.value) || 0),
    axisLabel: {
      formatter: (params, index) => {
        const row = sortedRows.value[index] || {}
        const rank = sortedRows.value.length - index
        const isTop3 = rank < 4
        return `{a${isTop3 ? rank : ''}|${row.rate ?? '0.00'} }{b${isTop3 ? rank : ''}|%}`
      },
      rich: {
        a: { fontSize: 18, color: '#98bfff', verticalAlign: 'bottom' },
        a1: { fontSize: 18, color: '#ff7f97', verticalAlign: 'bottom' },
        a2: { fontSize: 18, color: '#ffce64', verticalAlign: 'bottom' },
        a3: { fontSize: 18, color: '#e8ed66', verticalAlign: 'bottom' },
        b: { fontSize: 12, color: '#98bfff', verticalAlign: 'bottom' },
        b1: { fontSize: 12, color: '#ff7f97', verticalAlign: 'bottom' },
        b2: { fontSize: 12, color: '#ffce64', verticalAlign: 'bottom' },
        b3: { fontSize: 12, color: '#e8ed66', verticalAlign: 'bottom' }
      }
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false }
  }
])
const series = computed(() => [
  {
    name: '已整改',
    z: 6,
    type: 'pictorialBar',
    data: dataNum.value
  },
  {
    name: '背景',
    z: 6,
    type: 'bar',
    barWidth: 22,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(255,255,255,.1)',
      barBorderRadius: [0, 20, 20, 0]
    },
    data: bgData.value
  },
  {
    name: '已整改渐变',
    type: 'bar',
    barWidth: 22,
    barGap: '-100%',
    tooltip: { show: false },
    itemStyle: {
      color: {
        type: 'linear',
        x: 1,
        y: 0,
        x2: 0,
        y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(255, 218, 220)' },
          { offset: 0.07, color: 'rgba(255, 86, 118)' },
          { offset: 1, color: 'rgba(255, 86, 118, 0)' }
        ],
        global: false
      },
      barBorderRadius: [0, 20, 20, 0]
    },
    data: dataNum.value
  }
])
const loadData = async () => {
  if (loading.value) return
  loading.value = true
  try {
    const res = await getHiddenRectifyRanking({ limit: 5 })
    rows.value = Array.isArray(res?.data) ? res.data : []
    refreshFailed.value = false
    loaded.value = true
  } catch (e) {
    console.error('获取隐患整改责任人排名失败:', e)
    refreshFailed.value = true
  } finally {
    loading.value = false
  }
}
const safetyDashboardRefreshTick = inject('safetyDashboardRefreshTick', null)
if (safetyDashboardRefreshTick) {
  watch(safetyDashboardRefreshTick, () => {
    loadData()
  })
}
onMounted(() => {
  loadData()
})
</script>
<style scoped>
.panel-item-customers {
  position: relative;
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
  box-sizing: border-box;
}
.empty-tip {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  text-align: center;
  font-size: 16px;
  color: #8aa4c8;
  pointer-events: none;
}
.refresh-error {
  position: absolute;
  right: 16px;
  top: 8px;
  font-size: 12px;
  color: #ff9a9a;
}
</style>
src/views/reportAnalysis/safetyDashboard/components/right-top.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,275 @@
<template>
  <div>
    <PanelHeader title="危险源类型排名" />
    <div class="panel-item-customers">
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :series="series"
        :tooltip="tooltip"
        :xAxis="xAxis"
        :yAxis="yAxis"
        :options="chartOptions"
        style="height: 390px"
      />
      <div v-if="showEmpty" class="empty-tip">暂无数据</div>
      <span v-if="refreshFailed" class="refresh-error">刷新失败</span>
    </div>
  </div>
</template>
<script setup>
import { ref, computed, onMounted, inject, watch } from 'vue'
import Echarts from '@/components/Echarts/echarts.vue'
import PanelHeader from '@/views/reportAnalysis/dataDashboard/components/PanelHeader.vue'
import { getHazardTypeRanking } from '@/api/reportAnalysis/safetyDashboard.js'
const chart = ref(null)
const rows = ref([])
const loaded = ref(false)
const loading = ref(false)
const refreshFailed = ref(false)
const aircraft =
  'path://M107.000,71.000 C104.936,71.000 102.665,70.806 100.273,70.467 C94.592,76.922 86.275,81.000 77.000,81.000 C70.794,81.000 65.020,79.170 60.172,76.029 C66.952,74.165 72.647,69.714 76.173,63.817 C69.821,61.362 64.063,58.593 60.000,56.039 L60.000,52.813 C70.456,53.950 80.723,55.000 83.000,55.000 C88.972,55.000 93.000,53.723 93.000,50.000 C93.000,47.071 89.222,45.000 83.000,45.000 C80.723,45.000 70.456,46.050 60.000,47.187 L60.000,43.989 C64.057,41.431 69.807,38.644 76.168,36.173 C72.641,30.281 66.948,25.834 60.172,23.971 C65.020,20.830 70.794,19.000 77.000,19.000 C86.270,19.000 94.584,23.074 100.265,29.524 C102.647,29.191 104.918,29.000 107.000,29.000 C129.644,29.000 148.000,50.000 148.000,50.000 C148.000,50.000 129.644,71.000 107.000,71.000 ZM113.000,38.000 C106.373,38.000 101.000,43.373 101.000,50.000 C101.000,56.627 106.373,62.000 113.000,62.000 C119.627,62.000 125.000,56.627 125.000,50.000 C125.000,43.373 119.627,38.000 113.000,38.000 ZM113.000,56.000 C109.686,56.000 107.000,53.314 107.000,50.000 C107.000,46.686 109.686,44.000 113.000,44.000 C116.314,44.000 119.000,46.686 119.000,50.000 C119.000,53.314 116.314,56.000 113.000,56.000 ZM110.500,19.000 C109.567,19.000 108.763,18.483 108.334,17.726 C100.231,9.857 89.187,5.000 77.000,5.000 C64.813,5.000 53.769,9.857 45.666,17.726 C45.237,18.483 44.433,19.000 43.500,19.000 C42.119,19.000 41.000,17.881 41.000,16.500 C41.000,15.847 41.256,15.259 41.665,14.813 L41.575,14.718 C50.629,5.628 63.156,-0.000 77.000,-0.000 C90.844,-0.000 103.371,5.628 112.425,14.718 L112.335,14.813 C112.744,15.259 113.000,15.847 113.000,16.500 C113.000,17.881 111.881,19.000 110.500,19.000 ZM53.000,49.484 C61.406,48.626 77.810,47.000 81.345,47.000 C87.353,47.000 91.000,48.243 91.000,50.000 C91.000,52.234 87.111,53.000 81.345,53.000 C77.810,53.000 61.406,51.374 53.000,50.516 L53.000,49.484 ZM53.000,47.000 L9.000,50.000 L53.000,53.000 L53.000,56.000 L-0.000,50.000 L53.000,44.000 L53.000,47.000 ZM43.500,81.000 C44.433,81.000 45.237,81.517 45.666,82.274 C53.769,90.143 64.813,95.000 77.000,95.000 C89.187,95.000 100.231,90.143 108.334,82.274 C108.763,81.517 109.567,81.000 110.500,81.000 C111.881,81.000 113.000,82.119 113.000,83.500 C113.000,84.153 112.744,84.741 112.335,85.187 L112.425,85.282 C103.371,94.372 90.844,100.000 77.000,100.000 C63.156,100.000 50.629,94.372 41.575,85.282 L41.665,85.187 C41.256,84.741 41.000,84.153 41.000,83.500 C41.000,82.119 42.119,81.000 43.500,81.000 Z'
const color = {
  0: '#ff5676',
  1: '#ffd83e',
  2: '#fbff94',
  3: '#7daeff'
}
const chartStyle = {
  width: '100%',
  height: '100%'
}
const chartOptions = {
  backgroundColor: 'transparent',
  textStyle: { color: '#B8C8E0' }
}
const sortedRows = computed(() =>
  [...rows.value].sort((a, b) => (Number(a.value) || 0) - (Number(b.value) || 0))
)
const showEmpty = computed(
  () => loaded.value && !loading.value && rows.value.length === 0
)
const dataNum = computed(() =>
  sortedRows.value.map((item, i) => {
    const index = sortedRows.value.length - i - 1
    const isTop3 = index < 3
    return {
      value: Number(item.value) || 0,
      itemStyle: {
        color: {
          type: 'linear',
          x: 1,
          y: 0,
          x2: 0,
          y2: 0,
          colorStops: [
            { offset: 0, color: isTop3 ? '#ffdae1' : '#ecf3ff' },
            { offset: 0.07, color: isTop3 ? color[index] : color[3] },
            { offset: 1, color: isTop3 ? 'rgba(255, 86, 118, .1)' : 'rgba(125,174,255, .1)' }
          ],
          global: false
        },
        barBorderRadius: [0, 20, 20, 0]
      },
      symbol: isTop3 ? aircraft : 'none',
      symbolPosition: 'end',
      symbolSize: [30, 25],
      symbolOffset: [35, 0]
    }
  })
)
const bgData = computed(() => {
  const maxValue = Math.max(1, ...dataNum.value.map((v) => v.value))
  return dataNum.value.map(() => maxValue * 1.25)
})
const tooltip = computed(() => ({
  trigger: 'axis',
  textStyle: { fontSize: '100%' },
  formatter: function (params) {
    const index = params[0]?.dataIndex ?? 0
    const row = sortedRows.value[index] || {}
    return `${params[0]?.axisValueLabel || ''}<br/>数量:${row.value ?? 0} å¤„<br/>占比:${row.rate ?? '0.00'}%`
  }
}))
const grid = { top: 0, left: '22%', right: '12%', bottom: 0 }
const xAxis = [
  {
    splitLine: { show: false },
    axisLine: { show: false },
    axisLabel: { show: false },
    axisTick: { show: false }
  }
]
const yAxis = computed(() => [
  {
    type: 'category',
    data: sortedRows.value.map((item) => item.name),
    axisLabel: {
      formatter: (value) => {
        if (!value) return ''
        const maxLen = 6
        if (value.length <= maxLen) return `{a|${value}}`
        const lines = []
        for (let i = 0; i < value.length; i += maxLen) {
          lines.push(value.slice(i, i + maxLen))
        }
        return lines.map((line) => `{a|${line}}`).join('\n')
      },
      rich: {
        a: {
          width: 120,
          fontSize: 14,
          color: '#fff',
          padding: [5, 4, 5, 0],
          align: 'right'
        }
      }
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false }
  },
  {
    type: 'category',
    data: sortedRows.value.map((item) => Number(item.value) || 0),
    axisLabel: {
      formatter: (params, index) => {
        const row = sortedRows.value[index] || {}
        const rank = sortedRows.value.length - index
        const isTop3 = rank < 4
        return `{a${isTop3 ? rank : ''}|${row.rate ?? '0.00'} }{b${isTop3 ? rank : ''}|%}`
      },
      rich: {
        a: { fontSize: 18, color: '#98bfff', verticalAlign: 'bottom' },
        a1: { fontSize: 18, color: '#ff7f97', verticalAlign: 'bottom' },
        a2: { fontSize: 18, color: '#ffce64', verticalAlign: 'bottom' },
        a3: { fontSize: 18, color: '#e8ed66', verticalAlign: 'bottom' },
        b: { fontSize: 12, color: '#98bfff', verticalAlign: 'bottom' },
        b1: { fontSize: 12, color: '#ff7f97', verticalAlign: 'bottom' },
        b2: { fontSize: 12, color: '#ffce64', verticalAlign: 'bottom' },
        b3: { fontSize: 12, color: '#e8ed66', verticalAlign: 'bottom' }
      }
    },
    axisLine: { show: false },
    axisTick: { show: false },
    splitLine: { show: false }
  }
])
const series = computed(() => [
  {
    name: '数量',
    z: 6,
    type: 'pictorialBar',
    data: dataNum.value
  },
  {
    name: '背景',
    z: 6,
    type: 'bar',
    barWidth: 22,
    tooltip: { show: false },
    itemStyle: {
      color: 'rgba(255,255,255,.1)',
      barBorderRadius: [0, 20, 20, 0]
    },
    data: bgData.value
  },
  {
    name: '数量渐变',
    type: 'bar',
    barWidth: 22,
    barGap: '-100%',
    tooltip: { show: false },
    itemStyle: {
      color: {
        type: 'linear',
        x: 1,
        y: 0,
        x2: 0,
        y2: 0,
        colorStops: [
          { offset: 0, color: 'rgba(255, 218, 220)' },
          { offset: 0.07, color: 'rgba(255, 86, 118)' },
          { offset: 1, color: 'rgba(255, 86, 118, 0)' }
        ],
        global: false
      },
      barBorderRadius: [0, 20, 20, 0]
    },
    data: dataNum.value
  }
])
const loadData = async () => {
  if (loading.value) return
  loading.value = true
  try {
    const res = await getHazardTypeRanking({ limit: 8 })
    rows.value = Array.isArray(res?.data) ? res.data : []
    refreshFailed.value = false
    loaded.value = true
  } catch (e) {
    console.error('获取危险源类型排名失败:', e)
    refreshFailed.value = true
  } finally {
    loading.value = false
  }
}
const safetyDashboardRefreshTick = inject('safetyDashboardRefreshTick', null)
if (safetyDashboardRefreshTick) {
  watch(safetyDashboardRefreshTick, () => {
    loadData()
  })
}
onMounted(() => {
  loadData()
})
</script>
<style scoped>
.panel-item-customers {
  position: relative;
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
  box-sizing: border-box;
}
.empty-tip {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  text-align: center;
  font-size: 16px;
  color: #8aa4c8;
  pointer-events: none;
}
.refresh-error {
  position: absolute;
  right: 16px;
  top: 8px;
  font-size: 12px;
  color: #ff9a9a;
}
</style>
src/views/reportAnalysis/safetyDashboard/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,256 @@
<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 />
          <LeftBottom />
        </div>
        <div class="center-panel">
          <CenterTop />
          <CenterBottom />
        </div>
        <div class="right-panel">
          <RightTop />
          <RightBottom />
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
import LeftTop from './components/left-top.vue'
import LeftBottom from './components/left-bottom.vue'
import CenterTop from './components/center-top.vue'
import CenterBottom from './components/center-bottom.vue'
import RightTop from './components/right-top.vue'
import RightBottom from './components/right-bottom.vue'
const isFullscreen = ref(false)
const scaleRatio = ref(1)
const designWidth = 1920
const designHeight = 1080
const DASHBOARD_REFRESH_MS = 60 * 1000
const safetyDashboardRefreshTick = ref(0)
provide('safetyDashboardRefreshTick', safetyDashboardRefreshTick)
let dashboardPollTimer = null
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 = () => {
  setTimeout(() => {
    calculateScale()
  }, 100)
}
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'))
  setTimeout(() => {
    calculateScale()
  }, 200)
}
onMounted(() => {
  nextTick(() => {
    calculateScale()
  })
  window.addEventListener('resize', handleResize)
  window.addEventListener('fullscreenchange', handleFullscreenChange)
  window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.addEventListener('MSFullscreenChange', handleFullscreenChange)
  dashboardPollTimer = setInterval(() => {
    safetyDashboardRefreshTick.value++
  }, DASHBOARD_REFRESH_MS)
})
onBeforeUnmount(() => {
  if (dashboardPollTimer) {
    clearInterval(dashboardPollTimer)
    dashboardPollTimer = null
  }
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('fullscreenchange', handleFullscreenChange)
  window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
  window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
})
</script>
<style scoped>
.scale-container {
  position: relative;
  width: 100%;
  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:fullscreen {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
}
.scale-container:-webkit-full-screen {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  background-color: #000;
  z-index: 9999;
}
.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: 46px;
  color: #FFFFFF;
  top: 16px;
  position: absolute;
  letter-spacing: 4px;
}
.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/safeProduction/safetyTrainingAssessment/detail.vue
@@ -155,9 +155,10 @@
  const getList = () => {
    tableLoading.value = true;
    userListNoPage({}).then(res => {
      console.log("res", res.data);
      tableData.value = res.data;
      tableDataCopy.value = res.data;
      // è¶…级管理员账号不是培训对象,不出现在培训记录人员列表里
      const list = (res.data || []).filter(item => item.userId !== 1);
      tableData.value = list;
      tableDataCopy.value = list;
      if (tableData.value.length > 0) {
        customerId.value = tableData.value[0].userId;
        receiptPaymentList(customerId.value);
src/views/salesManagement/salesLedger/index.vue
@@ -146,7 +146,7 @@
                <template #default="scope">
                  <div>
                    <div v-if="scope.row.shippingDate">
                      {{ scope.row.shippingDate }}
                      {{ formatDateOnly(scope.row.shippingDate) }}
                    </div>
                    <el-tag v-else type="info">- </el-tag>
                  </div>
src/views/salesManagement/strategyControl/index.vue
@@ -63,7 +63,7 @@
            </el-table-column>
            <el-table-column prop="validPeriod" label="有效期" width="200">
              <template #default="scope">
                {{ scope.row.startDate }} è‡³ {{ scope.row.endDate }}
                {{ formatDateOnly(scope.row.startDate) }} è‡³ {{ formatDateOnly(scope.row.endDate) }}
              </template>
            </el-table-column>
            <el-table-column prop="status" label="状态" width="80">
src/views/system/appVersion/index.vue
@@ -12,12 +12,12 @@
      <el-table-column label="版本号" prop="version" align="center" width="120"/>
      <el-table-column label="创建时间" prop="createTime" align="center" width="170">
        <template #default="scope">
          <span>{{ parseTime(scope.row.createTime, "{y}-{m}-{d} {h}:{i}:{s}") }}</span>
          <span>{{ parseTime(scope.row.createTime) }}</span>
        </template>
      </el-table-column>
      <el-table-column label="更新时间" prop="updateTime" align="center" width="170">
        <template #default="scope">
          <span>{{ parseTime(scope.row.updateTime, "{y}-{m}-{d} {h}:{i}:{s}") }}</span>
          <span>{{ parseTime(scope.row.updateTime) }}</span>
        </template>
      </el-table-column>
      <el-table-column label="创建人" prop="createUser" align="center" width="100"/>