From 23fc9eb0f2a9d3822cf21894ff25f551751f7d4e Mon Sep 17 00:00:00 2001
From: gongchunyi <deslre0381@gmail.com>
Date: 星期四, 28 五月 2026 21:37:57 +0800
Subject: [PATCH] Revert "feat: 合并"
---
src/assets/BI/yuancailiaoyijianicon@2x.png | 0
src/assets/icons/svg/eye.svg | 1
src/api/productionManagement/operationScheduling.js | 27
src/assets/icons/png/walletGreen@2x.png | 0
src/layout/index.vue | 171
src/views/lavorissue/ledger/filesDia.vue | 202
src/views/system/user/authRole.vue | 123
src/assets/img/emoji/new-moon-face.png | 0
src/views/fileManagement/document/attachmentManager.vue | 425
src/api/salesManagement/strategyControl.js | 202
src/api/procurementManagement/paymentLedger.js | 20
src/api/inspectionManagement/index.js | 61
src/assets/images/head.svg | 1
src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue | 181
src/views/personnelManagement/analytics/index.vue | 701
src/assets/icons/svg/tool.svg | 1
src/assets/icons/png/支出金额.png | 0
src/assets/img/fileImg/unknowfile.png | 0
src/views/reportAnalysis/projectProfit/index.vue | 126
src/views/officeProcessAutomation/HrManage/resign-apply/index.vue | 220
src/assets/BI/hetongicon.png | 0
src/utils/generator/html.js | 359
src/api/basicData/customer.js | 83
src/assets/BI/backImage@2x.png | 0
src/api/tool/gen.js | 85
src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue | 325
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceListSearch.js | 158
src/views/reportAnalysis/financialAnalysis/index.vue | 291
src/assets/icons/svg/time.svg | 1
src/components/TopNav/index.vue | 217
src/api/fileManagement/return.js | 61
src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue | 123
src/assets/icons/svg/upload.svg | 1
src/assets/system/gongyingshangdangan.svg | 1
src/views/basicData/supplierManage/index.vue | 43
src/views/collaborativeApproval/approvalProcess/index4.vue | 22
src/assets/icons/png/收入金额.png | 0
src/views/collaborativeApproval/approvalProcess/index3.vue | 22
src/views/lavorissue/statistics/index.vue | 285
src/api/productionManagement/productionCosting.js | 31
src/views/aiIndustrialBrain/MAINTAIN_RULES.md | 7
src/views/error/401.vue | 82
src/components/AttachmentPreview/image/index.vue | 76
src/views/financialManagement/receivable/reconciliation.vue | 738
src/assets/icons/svg/checkbox.svg | 1
src/api/financialManagement/accountPurchasePayment.js | 32
src/views/equipmentManagement/inspectionManagement/components/qrCodeDia.vue | 132
src/views/equipmentManagement/brand/index.vue | 217
src/views/productionManagement/productionReporting/Input.vue | 115
src/api/financialManagement/accountSalesInvoice.js | 41
src/utils/generator/render.js | 156
src/api/collaborativeApproval/shipmentReview.js | 21
src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js | 355
vite/plugins/setup-extend.js | 5
src/api/inventoryManagement/stockOut.js | 37
src/views/equipmentManagement/ledger/Modal.vue | 69
src/api/basicData/productModel.js | 17
src/plugins/download.js | 101
src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue | 14
src/assets/icons/svg/language.svg | 1
src/assets/icons/svg/github.svg | 1
src/components/DynamicTable/index.vue | 402
src/views/financialManagement/receivable/salesOut.vue | 180
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue | 629
src/api/salesManagement/salespersonManagement.js | 35
src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue | 347
src/assets/styles/btn.scss | 99
src/assets/icons/png/circleOrange@2x.png | 0
src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js | 124
src/utils/generator/icon.json | 1
src/views/productionManagement/productionProcess/Edit.vue | 168
src/views/safeProduction/safetyTrainingAssessment/detail.vue | 323
src/api/inventoryManagement/stockReport.js | 55
bin/package.bat | 12
src/assets/icons/svg/input.svg | 1
src/views/qualityManagement/visualization/qualityDashboard.vue | 307
src/assets/AI/采购助手.png | 0
src/views/salesManagement/deliveryLedger/index.vue | 917
src/views/productionManagement/operationScheduling/components/formDia.vue | 251
src/views/equipmentManagement/repair/Modal/MaintainModal.vue | 226
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue | 59
src/views/safeProduction/hazardSourceLedger/index.vue | 750
src/views/basicData/supplierManage/filesDia.vue | 203
src/assets/icons/svg/phone.svg | 1
src/views/qualityManagement/nonconformingManagement/components/formDia.vue | 296
src/assets/icons/svg/system.svg | 2
src/api/procurementManagement/arrivalManagement.js | 43
src/views/qualityManagement/processInspection/components/filesDia.vue | 190
src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue | 147
src/assets/img/fileImg/ppt.png | 0
src/views/monitorManagement/videoMonitor/index.vue | 990
src/views/collaborativeApproval/officeSupplies/index.vue | 512
src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue | 291
src/api/monitor/job.js | 70
src/api/financialManagement/accountPurchaseInvoice.js | 50
src/assets/BI/icon@2x.png | 0
src/api/publicApi/index.js | 42
src/assets/images/icon2.png | 0
src/components/AIChatSidebar/assistants/index.js | 17
src/views/energyManagement/energyTrends/index.vue | 137
src/views/collaborativeApproval/approvalProcess/index5.vue | 22
src/plugins/tab.js | 71
src/utils/errorCode.js | 6
src/components/HeaderSearch/index.vue | 245
src/components/Upload/index.js | 1
src/views/chatHome/chatHomeIndex/ai-jz.js | 3
src/views/qualityManagement/afterSalesTraceability/index.vue | 595
src/assets/icons/svg/international.svg | 1
src/components/RightToolbar/index.vue | 157
src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js | 35
src/views/tool/swagger/index.vue | 9
src/assets/icons/svg/search.svg | 1
src/assets/img/emoji/face-without-mouth.png | 0
src/views/personnelManagement/dimission/index.vue | 243
src/components/IconSelect/requireIcons.js | 8
src/components/Crontab/index.vue | 309
src/assets/images/chartCard3.svg | 1
src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue | 227
src/api/productionManagement/workOrder.js | 97
src/assets/icons/svg/shopping.svg | 1
src/views/financialManagement/payable/payment.vue | 299
src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue | 192
src/views/financialManagement/generalLedger/index.vue | 498
src/assets/AI/质量助手.png | 0
src/api/collaborativeApproval/sealManagement.js | 116
src/views/inventoryManagement/stockWarning/index.vue | 943
src/views/procurementManagement/thePaymentLedger/index.vue | 104
src/views/salesManagement/paymentShipping/index.vue | 497
src/api/basicData/supplierManageFile.js | 75
src/assets/icons/svg/peoples.svg | 1
src/store/modules/settings.js | 105
src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue | 260
src/views/customerService/expiryAfterSales/components/formDia.vue | 319
src/views/salesManagement/salespersonManagement/index.vue | 371
src/api/inventoryManagement/stockWarning.js | 84
src/views/productionManagement/processRoute/New.vue | 187
src/api/safeProduction/hazardSourceLedger.js | 36
src/views/personnelManagement/contractManagement/components/formDia.vue | 93
src/assets/icons/svg/online.svg | 1
src/assets/BI/hetongjineicon1@2x.png | 0
src/views/collaborativeApproval/shipmentReview/index.vue | 340
src/views/inventoryManagement/stockManagement/index.vue | 44
multiple/assets/favicon/HQJCfavicon.ico | 0
src/views/monitor/job/log.vue | 283
src/views/collaborativeApproval/meetingManagement/index.vue | 63
src/views/inventoryManagement/dispatchLog/index.vue | 55
src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue | 94
src/api/financialManagement/accountSubject.js | 46
src/store/modules/user.js | 150
src/api/system/dict/data.js | 52
src/assets/logo/南通云从工业互联网有限公司.png | 0
src/assets/img/fileImg/word.png | 0
src/api/procurementManagement/returnManagement.js | 35
src/layout/components/Sidebar/Link.vue | 40
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js | 628
src/api/basicData/common.js | 25
src/components/ParentView/index.vue | 3
src/plugins/index.js | 18
src/api/procurementManagement/procurementPlan.js | 47
src/utils/index.js | 411
src/api/monitor/cache.js | 57
src/api/personnelManagement/bank.js | 34
src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue | 495
src/views/inventoryManagement/stockReport/index.vue | 686
src/views/procurementManagement/returnManagement/index.vue | 271
src/assets/system/baogong.svg | 1
src/assets/images/login-background.png | 0
src/views/productionManagement/processRoute/processRouteItem/index.vue | 1864
src/views/collaborativeApproval/approvalProcess/index.vue | 774
src/components/Crontab/month.vue | 141
src/directive/index.js | 9
src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue | 189
src/assets/BI/jiantou.png | 0
src/assets/icons/svg/server.svg | 1
src/assets/images/icon1.png | 0
src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue | 33
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue | 82
src/views/safeProduction/safeQualifications/index.vue | 893
src/assets/icons/svg/table.svg | 1
src/views/fileManagement/bookshelf/index.vue | 695
src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue | 613
src/assets/styles/sidebar.scss | 669
src/assets/BI/shijianmingchengbeijing@2x.png | 0
src/views/collaborativeApproval/approvalProcess/index1.vue | 22
src/assets/styles/element-ui.scss | 261
src/views/basicData/customerFileOpenSea/index.vue | 1803
src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue | 262
src/api/equipmentManagement/repair.js | 85
src/api/collaborativeApproval/noticeManagement.js | 78
src/assets/icons/svg/star.svg | 1
src/api/equipmentManagement/deviceInfo.js | 10
src/assets/icons/svg/fullscreen.svg | 1
src/assets/icons/svg/dashboard.svg | 1
src/views/personnelManagement/attendanceCheckin/index.vue | 512
src/views/reportAnalysis/productionAnalysis/components/left-top.vue | 236
src/views/energyManagement/energyPower/index.vue | 322
src/views/energyManagement/meterCollection/index.vue | 556
src/api/financialManagement/ledger.js | 19
src/components/PageHeader/index.vue | 63
src/views/personnelManagement/contractManagement/index.vue | 333
src/views/inventoryManagement/vehicleFuelManagement/index.vue | 556
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue | 179
src/assets/images/chartCard2.svg | 1
src/views/fileManagement/statistics/index.vue | 539
src/assets/images/icon 3.png | 0
public/favicon.ico | 0
src/assets/icons/svg/nested.svg | 1
src/views/tool/build/index.vue | 653
src/permission.js | 69
src/assets/system/xiaoshoutuihuo.svg | 1
src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue | 414
src/utils/scroll-to.js | 58
src/components/SvgIcon/svgicon.js | 10
src/views/inventoryManagement/issueManagement/index.vue | 285
src/assets/icons/svg/time-range.svg | 1
src/views/salesManagement/salesQuotation/index.vue | 910
src/views/officeProcessAutomation/EnterpriseNews/news-manage/useEnterpriseNewsList.js | 55
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js | 160
src/views/energyManagement/gasManagement/index.vue | 624
src/assets/fonts/DIN Alternate Bold.ttf | 0
src/plugins/modal.js | 82
src/views/reportAnalysis/qualityAnalysis/index.vue | 288
src/views/productionManagement/productionDispatching/components/autoDispatchDia.vue | 153
src/assets/BI/hetongjineicon@2x.png | 0
src/main.js | 119
src/assets/icons/png/收入列帐.png | 0
src/layout/components/index.js | 5
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue | 67
src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue | 399
src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue | 306
src/views/equipmentManagement/measurementEquipment/components/rowClickData.vue | 128
src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue | 141
src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue | 288
src/views/tool/build/RightPanel.vue | 906
src/views/productionManagement/workOrderManagement/components/filesDia.vue | 201
src/assets/img/emoji/face-screaming-in-fear.png | 0
src/layout/components/Navbar.vue | 382
src/views/procurementManagement/purchaseOrder/index.vue | 200
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue | 634
src/views/productionManagement/productionOrder/New.vue | 204
src/components/FileCard.vue | 81
src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue | 347
src/assets/img/emoji/smiling-face-with-heart-eyes.png | 0
src/views/collaborativeApproval/approvalProcess/index2.vue | 22
src/assets/images/Rectangle 76@2x.png | 0
src/assets/img/emoji/pouting-face.png | 0
src/layout/components/Sidebar/SidebarItem.vue | 171
src/views/energyManagement/waterManagement/index.vue | 329
src/views/energyManagement/waterManagement/waterTrends.vue | 118
src/assets/icons/svg/swagger.svg | 1
src/store/modules/permission.js | 163
src/utils/validate.js | 114
src/utils/permission.js | 51
src/api/safeProduction/safeQualifications.js | 61
src/views/qualityManagement/metricMaintenance/StandardFormDialog.vue | 129
src/store/modules/dict.js | 57
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js | 313
src/assets/icons/svg/tree.svg | 1
src/api/equipmentManagement/brand.js | 93
src/directive/permission/hasRole.js | 28
src/assets/img/emoji/smiling-face.png | 0
src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue | 136
src/views/reportAnalysis/qualityAnalysis/components/center-top.vue | 149
src/api/financialManagement/accounting.js | 28
src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue | 275
src/api/safeProduction/emergencyPlanReview.js | 35
src/assets/icons/svg/component.svg | 1
src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue | 857
src/assets/icons/svg/download.svg | 1
src/views/monitor/operlog/index.vue | 313
src/views/tool/build/IconsDialog.vue | 115
src/api/inventoryManagement/stockInventory.js | 105
src/api/financialManagement/financialStatements.js | 13
vite/plugins/index.js | 15
src/api/equipmentManagement/spareParts.js | 58
src/api/monitor/jobLog.js | 26
src/assets/icons/png/1.png | 0
src/views/energyManagement/energyPower/components/formDia.vue | 228
src/views/personnelManagement/employeeRecord/components/RenewContract.vue | 141
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue | 74
src/assets/BI/guochengyijianicon@2x.png | 0
src/views/collaborativeApproval/meetingBoard/index.vue | 344
src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsUtils.js | 207
src/views/error/404.vue | 227
src/api/basicData/parameterMaintenance.js | 81
src/settings.js | 51
src/api/qualityManagement/metricMaintenance.js | 110
src/views/productionManagement/workOrderEdit/index.vue | 530
src/components/AIChatSidebar/assistants/financeAssistant.js | 28
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue | 284
src/layout/components/NotificationCenter/index.vue | 386
src/views/collaborativeApproval/warningSystem/index.vue | 305
src/api/reportAnalysis/qualityReport.js | 52
src/views/example/SimpleExample.vue | 135
src/api/qualityManagement/nonconformingManagement.js | 50
src/components/ProjectManagement/DiscussProgressDialog.vue | 141
src/assets/styles/mixin.scss | 66
src/views/tideLogin.vue | 15
src/directive/common/copyText.js | 66
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js | 189
src/views/salesManagement/returnOrder/components/detailDia.vue | 352
src/views/qualityManagement/metricMaintenance/ParamFormDialog.vue | 78
src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue | 511
src/api/qualityManagement/nearExpiryReturn.js | 46
src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue | 33
src/assets/icons/svg/date-range.svg | 1
src/assets/img/emoji/smiling-face-with-sunglasses.png | 0
src/views/financialManagement/inventoryAccounting/index.vue | 390
src/views/system/dict/data.vue | 362
src/views/reportAnalysis/PSIDataAnalysis/components/DateTypeSwitch.vue | 94
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue | 194
src/api/officeProcessAutomation/finReimbursement.js | 71
src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue | 211
src/views/inventoryManagement/index.vue | 309
src/assets/icons/svg/qq.svg | 1
src/components/QRCodeGenerator/index.vue | 566
src/views/collaborativeApproval/customerVisit/index.vue | 269
src/assets/img/head_portrait1.png | 0
src/plugins/auth.js | 60
src/views/energyManagement/energyCockpit/index.vue | 1380
src/assets/icons/png/2.png | 0
src/api/procurementManagement/procurementLedger.js | 134
src/assets/BI/玫瑰图边框.png | 0
src/views/salesManagement/returnOrder/index.vue | 219
src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue | 134
src/assets/images/denglu.png | 0
src/views/productionManagement/processStatistics/index.vue | 268
src/assets/BI/border@2x.png | 0
src/views/officeProcessAutomation/HrManage/staff-archive/index.vue | 360
src/views/salesManagement/customerManagement/index.vue | 423
src/assets/system/jichupeizhi.svg | 1
src/views/procurementManagement/procurementLedger/fileList.vue | 66
src/assets/AI/生产助手.png | 0
src/assets/icons/svg/number.svg | 1
src/views/energyManagement/energyArea/index.vue | 511
src/views/aiIndustrialBrain/index.vue | 1500
src/api/collaborativeApproval/rulesRegulationsManagementFile.js | 28
src/api/equipmentManagement/ledger.js | 44
src/assets/system/baogongtaizhang.svg | 1
src/router/index.js | 342
src/assets/icons/svg/money.svg | 1
src/assets/img/emoji/jack-o-lantern.png | 0
multiple/assets/favicon/CKGMfavicon.ico | 0
src/api/productionManagement/productionProductMain.js | 11
src/api/financialManagement/accountSalesCollection.js | 50
src/components/FileUpload/index.vue | 259
src/components/Dialog/FormDialog.vue | 86
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue | 112
src/assets/404_images/404.png | 0
src/assets/icons/png/walletYellow@2x.png | 0
src/api/monitor/online.js | 18
src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue | 225
src/api/productionPlan/productionPlan.js | 79
src/assets/system/BOM.svg | 1
src/utils/generator/css.js | 18
src/components/AttachmentUpload/image/index.vue | 335
src/views/equipmentManagement/iotMonitor/index.vue | 317
src/api/energyManagement/index.js | 127
src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js | 11
src/assets/img/emoji/face-with-tongue.png | 0
src/assets/icons/png/pink@2x.png | 0
src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue | 188
src/views/system/role/selectUser.vue | 144
src/assets/img/emoji/victory-hand-yellow.png | 0
src/views/procurementManagement/purchaseReturnOrder/index.vue | 481
src/api/financialManagement/accountPurchase.js | 19
src/assets/404_images/404_cloud.png | 0
src/assets/system/caigoubaobiao.svg | 1
src/api/collaborativeApproval/planTemplate.js | 64
src/components/Crontab/day.vue | 174
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue | 282
src/api/monitor/server.js | 9
src/views/monitorManagement/areaControl/index.vue | 264
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue | 176
src/views/productionManagement/processRoute/Edit.vue | 252
src/App.vue | 15
src/assets/system/ai.svg | 1
src/api/collaborativeApproval/approvalManagement.js | 20
src/assets/icons/svg/message.svg | 1
src/assets/icons/svg/drag.svg | 1
src/views/qualityManagement/rawMaterialInspection/index.vue | 490
src/assets/icons/png/walletOrange@2x.png | 0
src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue | 336
src/assets/BI/shujutongjiicon@2x.png | 0
src/views/qualityManagement/processInspection/index.vue | 484
src/assets/styles/ruoyi.scss | 289
src/assets/BI/shijianmingxiicon@2x.png | 0
src/views/officeProcessAutomation/HrManage/staff-contract/index.vue | 296
src/assets/icons/svg/list.svg | 1
src/api/qualityManagement/rawMaterialInspection.js | 57
src/views/energyManagement/waterManagement/components/waterBillForm.vue | 203
src/views/inventoryManagement/vehicleManagement/index.vue | 581
src/api/qualityManagement/qualityTestStandardBinding.js | 28
src/assets/system/fahuotaizhang.svg | 1
src/assets/BI/chuchangyijianicon@2x.png | 0
src/assets/icons/svg/eye-open.svg | 1
src/assets/system/组 210683.svg | 1
src/api/system/menu.js | 60
multiple/assets/screen/login-background.png | 0
src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue | 191
src/views/personnelManagement/monthlyStatistics/components/formDia.vue | 804
src/views/personnelManagement/employeeRecord/components/Show.vue | 73
src/utils/theme.js | 74
src/api/productionManagement/productionProductOutput.js | 11
src/api/financialManagement/invoiceApply.js | 59
src/components/AIChatSidebar/assistants/generalAssistant.js | 33
src/api/financialManagement/intangibleAsset.js | 50
src/views/tool/gen/index.vue | 437
src/views/reportAnalysis/financialAnalysis/components/left-top.vue | 134
src/assets/icons/svg/password.svg | 1
src/views/qualityManagement/nonconformingManagement/index.vue | 306
src/views/personnelManagement/monthlyStatistics/components/auditDia.vue | 216
src/assets/icons/svg/question.svg | 1
src/assets/img/emoji/shamrock.png | 0
src/api/financialManagement/voucher.js | 54
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue | 318
src/assets/icons/svg/edit.svg | 1
src/views/salesManagement/salesLedger/index.vue | 3178
src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js | 140
src/components/Editor/index.vue | 304
src/api/monitor/logininfor.js | 34
src/api/productionManagement/processRouteItem.js | 92
src/assets/icons/png/收入收款.png | 0
src/assets/system/shengchandingdan.svg | 1
src/assets/img/fileImg/zpi.png | 0
src/views/financialManagement/assets/intangibleAssets.vue | 493
src/assets/system/caigoupeizhi.svg | 1
src/views/salesManagement/salesLedger/fileList.vue | 43
src/api/collaborativeApproval/rpaManagement.js | 78
src/views/qualityManagement/processInspection/components/inspectionFormDia.vue | 139
src/api/salesManagement/salesQuotation.js | 112
src/views/equipmentManagement/spareParts/index.vue | 570
src/api/productionManagement/processRouteFile.js | 28
src/assets/images/profile.jpg | 0
src/api/procurementManagement/taxComparison.js | 10
src/views/qualityManagement/finalInspection/components/filesDia.vue | 182
src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue | 304
src/api/financialManagement/accountStatement.js | 41
src/views/personnelManagement/classsSheduling/index.vue | 1283
src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue | 227
src/api/safeProduction/hazardousMaterialsControl.js | 33
src/views/system/user/profile/resetPwd.vue | 59
src/layout/components/AppMain.vue | 92
src/views/collaborativeApproval/rpaManagement/index.vue | 366
vite/plugins/compression.js | 28
src/components/Crontab/second.vue | 128
src/assets/icons/svg/build.svg | 1
src/api/system/config.js | 60
src/api/inventoryManagement/stockIn.js | 161
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue | 167
src/assets/img/emoji/money-bag.png | 0
src/assets/system/shenpiguanli.svg | 1
src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue | 12
src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue | 315
src/components/Upload/FileUpload.vue | 100
src/assets/img/emoji/ok-hand-yellow.png | 0
src/views/lavorissue/ledger/index.vue | 300
src/assets/fonts/font.css | 7
src/components/AIChatSidebar/assistants/productionAssistant.js | 28
src/assets/images/pay.png | 0
src/views/equipmentManagement/repair/Modal/RepairModal.vue | 268
src/assets/icons/svg/example.svg | 1
src/utils/generator/js.js | 370
src/assets/AI/财务助手.png | 0
src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js | 301
src/views/financialManagement/payable/paymentApply.vue | 1060
src/views/equipmentManagement/measurementEquipment/index.vue | 354
src/views/financialManagement/payable/reconciliation.vue | 766
src/views/productionManagement/productionOrder/index.vue | 1025
src/api/system/message.js | 45
src/assets/icons/svg/lock.svg | 1
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue | 253
src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue | 257
src/views/productionManagement/productionProcess/New.vue | 129
src/assets/system/gongyingshangwanglai.svg | 1
src/components/AIChatSidebar/assistants/purchaseAssistant.js | 30
src/views/reportAnalysis/qualityAnalysis/components/center-center.vue | 355
src/components/ProcessParamListDialog.vue | 670
src/utils/jsencrypt.js | 30
src/components/Crontab/min.vue | 126
src/assets/icons/png/支出.png | 0
src/assets/icons/svg/bug.svg | 1
src/views/equipmentManagement/measurementEquipment/filesDia.vue | 176
src/views/projectManagement/Management/components/formDia.vue | 1506
src/assets/AI/仓储助手.png | 0
src/assets/icons/svg/email.svg | 1
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue | 550
src/assets/logo/敦煌鼎诚.png | 0
src/assets/icons/png/circleYellow@2x.png | 0
src/assets/icons/svg/color.svg | 1
src/assets/aiIndustrialBrain/reference-cards.png | 0
src/assets/img/emoji/tired-face.png | 0
src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue | 165
src/assets/icons/png/circleRed@2x.png | 0
src/assets/logo/logo.png | 0
src/api/menu.js | 9
src/api/fileManagement/document.js | 189
src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js | 334
src/api/equipmentManagement/measurementEquipment.js | 82
src/views/productionManagement/workOrder/components/filesDia.vue | 202
src/views/salesManagement/indicatorStats/index.vue | 715
src/views/energyManagement/carbonManagement/index.vue | 1553
src/components/Crontab/week.vue | 197
src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue | 550
src/assets/system/kehuwanglai.svg | 1
src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue | 819
src/api/basicData/enum.js | 49
src/assets/styles/index.scss | 225
src/views/tool/gen/createTable.vue | 46
src/api/basicData/product.js | 67
src/api/inventoryManagement/stockInRecord.js | 53
src/views/productionManagement/productionDispatching/index.vue | 630
src/assets/system/cangchuwuliu.svg | 1
src/api/system/notice.js | 44
src/assets/icons/svg/cascader.svg | 1
src/views/productionManagement/productStructure/index.vue | 538
src/api/publicApi/commonFile.js | 19
src/views/tool/build/CodeTypeDialog.vue | 71
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js | 634
src/views/inventoryManagement/receiptManagement/index.vue | 55
src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue | 93
src/views/financialManagement/voucher/generalLedger.vue | 312
src/api/login.js | 110
src/assets/icons/svg/radio.svg | 1
src/assets/icons/png/blue@2x.png | 0
src/views/personnelManagement/socialSecuritySet/components/formDia.vue | 470
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue | 448
src/assets/401_images/401.gif | 0
src/utils/generator/config.js | 452
src/assets/images/khtitle.png | 0
src/components/SizeSelect/index.vue | 45
bin/run-web.bat | 12
src/assets/system/yonghuguanli.svg | 1
src/api/salesManagement/salesLedger.js | 119
src/api/collaborativeApproval/attendanceManagement.js | 136
src/assets/system/caigoutuihuo.svg | 1
src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js | 45
src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue | 188
src/api/system/appVersion.js | 19
src/assets/img/emoji/rocket.png | 0
src/views/procurementManagement/purchaseReturnOrder/New.vue | 808
src/views/salesManagement/orderManagement/index.vue | 490
src/directive/permission/hasPermi.js | 28
src/views/collaborativeApproval/noticeManagement/index.vue | 960
src/assets/system/chukuguanli.svg | 1
src/views/equipmentManagement/upkeep/Form/PlanModal.vue | 217
src/views/reportAnalysis/dataDashboard/index.vue | 305
src/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue | 94
src/assets/styles/variables.module.scss | 268
src/views/personnelManagement/selfService/index.vue | 800
src/assets/icons/svg/theme.svg | 1
src/views/safeProduction/accidentReportingRecord/index.vue | 863
src/assets/BI/hetongjineback@2x.png | 0
src/views/fileManagement/bookshelf/detail.vue | 110
src/assets/icons/svg/guide.svg | 1
src/views/equipmentManagement/attendanceManagement/index.vue | 403
src/views/tool/gen/editTable.vue | 200
src/api/personnelManagement/socialSecuritySet.js | 46
src/views/tool/gen/importTable.vue | 126
src/assets/icons/svg/moon.svg | 1
src/assets/icons/svg/job.svg | 1
src/assets/system/xiaoshoutaizhang.svg | 1
src/views/collaborativeApproval/attendanceManagement/index.vue | 1244
src/views/energyManagement/energyPeriodTime/index.vue | 462
src/assets/img/emoji/star.png | 0
src/views/system/dept/index.vue | 289
src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue | 363
src/assets/icons/png/walletRed@2x.png | 0
src/views/equipmentManagement/deviceInfo/index.vue | 190
src/assets/indexViews/HYSNLogo.png | 0
src/assets/images/video.png | 0
src/assets/img/emoji/pile-of-poo.png | 0
src/assets/logo/新缆(江苏)数字科技有限公司.png | 0
src/views/reportAnalysis/taxComparison/index.vue | 118
src/assets/img/fileImg/pdf.png | 0
src/assets/icons/png/green@2x.png | 0
src/views/inventoryManagement/stockManagement/Subtract.vue | 219
src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue | 70
src/views/personnelManagement/monthlyStatistics/index.vue | 407
src/views/lavorissue/ledger/Modal.vue | 70
src/api/lavorissce/ledger.js | 55
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js | 904
src/assets/icons/svg/row.svg | 1
src/assets/icons/svg/link.svg | 1
src/assets/icons/svg/druid.svg | 1
src/views/register.vue | 220
src/api/personnelManagement/staffAnalytics.js | 26
src/layout/components/Sidebar/Logo.vue | 198
src/views/safeProduction/safeWorkApproval/index.vue | 371
src/assets/icons/svg/validCode.svg | 1
src/views/inventoryManagement/receiptManagement/Record.vue | 514
src/views/financialManagement/financialStatements/index.vue | 648
src/assets/icons/svg/icon.svg | 1
src/views/energyManagement/waterManagement/waterBill.vue | 181
src/assets/system/kucunguanli.svg | 1
src/views/personnelManagement/employeeRecord/index.vue | 416
src/views/tool/gen/basicInfoForm.vue | 48
src/views/productionPlan/productionPlan/components/PIMTable.vue | 471
src/assets/img/emoji/hibiscus.png | 0
src/assets/icons/svg/rate.svg | 1
src/api/productionManagement/productProcessRoute.js | 85
src/components/Pagination/index.vue | 105
src/views/inventoryManagement/stockManagement/Import.vue | 93
src/api/safeProduction/safetyTrainingAssessment.js | 112
src/assets/icons/svg/size.svg | 1
src/views/inventoryManagement/transportTaskManagement/index.vue | 692
src/components/PurchaseAIChatSidebar/index.vue | 10
src/views/projectManagement/roles/index.vue | 296
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue | 123
src/api/equipmentManagement/calibration.js | 35
src/assets/images/yuancailiao.png | 0
src/assets/img/emoji/smiling-face-with-horns.png | 0
src/assets/images/dark.svg | 39
src/views/inventoryManagement/dispatchLog/Record.vue | 869
src/components/RuoYi/Git/index.vue | 13
src/assets/images/Rectangle 77@2x.png | 0
src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue | 115
src/assets/BI/pieback@2x.png | 0
src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue | 263
src/api/collaborativeApproval/notificationManagement.js | 63
src/views/collaborativeApproval/enterpriseBook/index.vue | 798
src/assets/BI/kehuhetongback@2x.png | 0
src/views/monitor/job/index.vue | 501
src/assets/icons/svg/pdf.svg | 1
src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue | 152
src/assets/system/caigoutaizhang.svg | 1
src/views/procurementManagement/arrivalManagement/index.vue | 237
src/views/monitor/online/index.vue | 109
src/views/procurementManagement/priceManagement/index.vue | 273
src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js | 408
src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue | 144
src/views/tool/gen/genInfoForm.vue | 305
src/assets/system/shengchanpeizhi.svg | 1
src/layout/components/IframeToggle/index.vue | 25
src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js | 259
src/views/collaborativeApproval/notificationManagement/summary/index.vue | 397
src/api/salesManagement/invoiceLedger.js | 10
src/api/inventoryManagement/stockUninventory.js | 63
src/assets/BI/jiantou@2x.png | 0
src/views/basicData/product/index.vue | 663
src/views/qualityManagement/metricMaintenance/index0.vue | 415
src/api/collaborativeApproval/knowledgeBase.js | 55
src/views/qualityManagement/finalInspection/components/inspectionFormDia.vue | 139
src/api/equipmentManagement/upkeep.js | 104
src/assets/icons/svg/button.svg | 1
src/views/qualityManagement/finalInspection/index.vue | 522
src/assets/img/emoji/thumbs-up-yellow.png | 0
src/assets/icons/svg/tab.svg | 1
src/views/financialManagement/payable/input-invoice.vue | 945
src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue | 263
html/ie.html | 46
src/views/equipmentManagement/gasTank/simple.vue | 566
src/api/personnelManagement/payrollManagement.js | 35
src/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue | 33
src/views/equipmentManagement/ledger/index.vue | 439
src/views/safeProduction/safetyTrainingAssessment/index.vue | 1299
src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue | 304
src/assets/icons/svg/404.svg | 1
multiple/multiple-build.js | 152
src/api/inspectionUpload/index.js | 43
src/store/index.js | 3
multiple/assets/logo/BWSMLogo.png | 0
src/assets/aiIndustrialBrain/reference-chat.png | 0
src/utils/generator/drawingDefalut.js | 29
src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue | 306
src/assets/icons/svg/tree-table.svg | 1
src/api/basicData/customerFile.js | 39
src/assets/BI/shujutongji@2x.png | 0
src/api/procurementManagement/transferManagement.js | 34
src/views/financialManagement/receivable/receipt.vue | 877
src/api/procurementManagement/advancedPriceManagement.js | 38
src/assets/icons/svg/documentation.svg | 1
src/views/financialManagement/assets/fixedAssets.vue | 495
src/api/fileManagement/statistics.js | 75
src/views/energyManagement/waterManagement/components/formDia.vue | 214
src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue | 412
src/assets/icons/png/3.png | 0
src/api/personnelManagement/personalAttendanceRecords.js | 25
src/api/personnelManagement/staffSalaryMain.js | 43
src/views/qualityManagement/rawMaterialInspection/components/inspectionFormDia.vue | 139
src/assets/icons/svg/people.svg | 1
src/assets/images/chuchang.png | 0
src/views/monitor/cache/list.vue | 246
src/hooks/usePaginationApi.jsx | 145
src/views/financialManagement/voucher/index.vue | 1110
src/assets/BI/zonghetongbingtubiankuang@2x.png | 0
src/views/reportAnalysis/PSIDataAnalysis/index.vue | 305
src/api/productionManagement/productWorkOrderFile.js | 29
src/views/officeProcessAutomation/HrManage/regular-apply/index.vue | 174
src/views/system/menu/index.vue | 470
src/api/collaborativeApproval/approvalProcess.js | 63
src/api/personnelManagement/employeeRecord.js | 27
src/views/fileManagement/return/index.vue | 706
src/api/productionManagement/processRoute.js | 49
src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue | 94
src/layout/components/Sidebar/index.vue | 166
src/views/reportAnalysis/reportManagement.vue | 733
src/views/chatHome/chatHomeIndex/MobileChat.vue | 461
src/views/example/DynamicTableExample.vue | 354
src/views/personnelManagement/socialSecuritySet/index.vue | 212
src/api/system/user.js | 159
src/api/officeProcessAutomation/enterpriseNews.js | 38
src/api/financialManagement/fixedAsset.js | 50
src/views/equipmentManagement/defectManagement/index.vue | 221
src/components/PIMTable/PIMTable.vue | 528
src/assets/icons/svg/redis-list.svg | 2
src/assets/img/logo.png | 0
src/assets/img/emoji/ghost.png | 0
src/api/safeProduction/accidentReportingRecord.js | 36
src/views/qualityManagement/nearExpiryReturn/index.vue | 445
src/views/collaborativeApproval/notificationManagement/index.vue | 1200
src/assets/icons/svg/code.svg | 1
src/views/officeProcessAutomation/EnterpriseNews/news-manage/components/NewsDetailPanel.vue | 169
src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue | 362
src/api/basicData/productProcess.js | 10
src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue | 94
src/views/system/user/profile/userAvatar.vue | 168
src/components/Screenfull/index.vue | 22
src/assets/icons/svg/date.svg | 1
src/api/system/dept.js | 52
src/views/system/role/authUser.vue | 182
multiple/assets/logo/CKGMLogo.png | 0
src/views/procurementManagement/purchaseReturnOrder/ProductList.vue | 191
src/views/procurementManagement/advancedPriceManagement/index.vue | 773
src/assets/system/zhibiaotongji.svg | 1
src/assets/icons/png/4.png | 0
src/assets/img/emoji/rainbow.png | 0
src/assets/icons/svg/monitor.svg | 2
src/views/salesManagement/strategyControl/index.vue | 1587
src/views/system/user/profile/index.vue | 87
src/assets/styles/transition.scss | 49
src/views/productionManagement/operationScheduling/index.vue | 289
src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue | 198
src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue | 360
src/views/productionManagement/productionReporting/index.vue | 476
src/views/financialManagement/payable/purchaseIn.vue | 212
src/layout/components/TagsView/ScrollPane.vue | 107
src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue | 422
src/api/productionManagement/productionProductInput.js | 11
src/assets/logo/XDRJ.png | 0
src/assets/indexViews/LCLogo.png | 0
src/views/procurementManagement/qualityInspection/index.vue | 324
src/views/projectManagement/projectType/index.vue | 509
src/views/system/appVersion/index.vue | 270
src/views/officeProcessAutomation/HrManage/work-handover/index.vue | 249
src/api/equipmentManagement/sparePartsUsage.js | 36
src/assets/icons/png/5.png | 0
src/assets/icons/svg/redis.svg | 1
src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue | 181
src/components/DictTag/index.vue | 82
src/views/equipmentManagement/repair/index.vue | 365
src/components/AIChatSidebar/index.vue | 6513 +
src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue | 191
src/api/personnelManagement/monthlyStatistics.js | 65
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js | 696
src/api/system/post.js | 53
src/assets/icons/png/yellow@2x.png | 0
src/views/projectManagement/Management/projectDetail.vue | 532
src/api/officeProcessAutomation/approvalInstance.js | 47
src/assets/icons/svg/select.svg | 1
src/views/projectManagement/Management/index.vue | 428
src/views/energyManagement/dynamicEnergySaving/index.vue | 657
src/views/financialManagement/accounting/index.vue | 740
src/assets/icons/svg/clipboard.svg | 1
src/api/collaborativeApproval/enterpriseBook.js | 67
src/views/personnelManagement/scheduling/index.vue | 622
src/api/projectManagement/project.js | 118
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js | 116
src/assets/images/light.svg | 39
src/api/projectManagement/projectType.js | 27
src/views/collaborativeApproval/approvalManagement/index.vue | 881
src/views/collaborativeApproval/sealManagement/index.vue | 494
src/views/tool/build/DraggableItem.vue | 68
src/api/inventoryManagement/stockManage.js | 83
src/views/reportAnalysis/qualityAnalysis/components/right-top.vue | 132
src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue | 566
src/views/equipmentManagement/calibration/index.vue | 256
src/assets/images/Rectangle 77@2x(1).png | 0
src/api/procurementManagement/projectProfit.js | 10
src/views/monitor/cache/index.vue | 132
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js | 91
src/assets/system/shengchanpaichan.svg | 1
multiple/assets/favicon/BWSMfavicon.ico | 0
src/assets/system/chanpinweihu.svg | 1
src/api/productionManagement/productionReporting.js | 43
src/views/productionManagement/productStructure/StructureEdit.vue | 311
src/assets/icons/svg/user.svg | 1
src/components/Breadcrumb/index.vue | 119
src/views/financialManagement/receivable/salesReturn.vue | 171
src/api/officeProcessAutomation/approvalTemplate.js | 58
src/assets/icons/svg/form.svg | 1
src/store/modules/tagsView.js | 182
src/api/equipmentManagement/defectManagement.js | 44
src/api/viewIndex.js | 384
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue | 49
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue | 176
src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js | 609
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue | 168
src/assets/icons/svg/chart.svg | 1
src/assets/icons/svg/switch.svg | 1
src/assets/images/guocheng.png | 0
src/components/filePreview/index.vue | 202
src/views/reportAnalysis/dataDashboard/index0.vue | 2037
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue | 122
src/views/system/role/index.vue | 587
src/assets/icons/svg/log.svg | 1
src/utils/ruoyi.js | 228
src/components/Dialog/ImportDialog.vue | 172
src/views/qualityManagement/metricBinding/index.vue | 536
src/views/monitor/server/index.vue | 190
src/assets/icons/png/walletBlue@2x.png | 0
src/assets/img/emoji/thought-balloon.png | 0
src/utils/util.js | 121
src/views/collaborativeApproval/knowledgeBase/index.vue | 758
src/views/financialManagement/voucher/detailLedger.vue | 309
src/api/fileManagement/bookshelf.js | 129
src/components/AIChatSidebar/assistants/salesAssistant.js | 28
src/api/salesManagement/receiptPayment.js | 10
src/utils/summarizeTable.js | 57
src/views/fileManagement/document/index.vue | 1418
src/views/procurementManagement/procurementReport/index.vue | 411
src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue | 614
src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue | 85
src/views/lavorissue/ledger/Form.vue | 158
src/views/officeProcessAutomation/HrManage/post-manage/index.vue | 292
src/views/system/user/profile/userInfo.vue | 67
src/components/iFrame/index.vue | 31
src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue | 33
src/views/procurementManagement/transferManagement/index.vue | 431
src/components/RuoYi/Doc/index.vue | 13
src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue | 85
src/api/collaborativeApproval/customerVisit.js | 10
src/assets/system/xiaoshoubaojia.svg | 1
src/assets/system/zhushengchanjihua.svg | 1
src/views/productionManagement/productionReporting/components/formDia.vue | 185
src/api/personnelManagement/attendanceRules.js | 45
src/views/productionManagement/productionProcess/index.vue | 1111
src/assets/icons/svg/dict.svg | 1
src/views/basicData/product/ProductSelectDialog.vue | 187
src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue | 176
src/views/personnelManagement/payrollManagement/components/formDia.vue | 319
src/views/personnelManagement/payrollManagement/index.vue | 306
src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue | 320
src/assets/AI/待办助手.png | 0
src/assets/img/emoji/slightly-smiling-face.png | 0
src/api/procurementManagement/purchase_return_order.js | 47
src/api/productionManagement/productionOrder.js | 228
src/views/reportAnalysis/financialAnalysis/components/center-bottom.vue | 107
src/api/salesManagement/deliveryLedger.js | 61
src/views/chatHome/chatHomeIndex/home.vue | 175
src/views/officeProcessAutomation/NoticeAnnouncement/notice-manage/index.vue | 12
src/api/financialManagement/accountPaymentApplication.js | 59
src/assets/icons/svg/wechat.svg | 1
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue | 511
src/api/equipmentManagement/maintenanceTaskFile.js | 28
src/assets/system/gongxu.svg | 1
src/views/procurementManagement/procurementPlan/index.vue | 856
src/views/collaborativeApproval/planTemplate/index.vue | 867
src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue | 269
src/assets/system/rukuguanli.svg | 1
src/hooks/useFormData.js | 15
src/views/collaborativeApproval/shipmentReview/fileList.vue | 42
src/views/equipmentManagement/operationManagement/index.vue | 484
src/assets/img/emoji/thinking-face.png | 0
src/views/reportAnalysis/financialAnalysis/components/PanelHeader.vue | 33
src/views/equipmentManagement/inspectionManagement/components/viewQrCodeFiles.vue | 169
src/views/safeProduction/safeWorkApproval/components/approvalDia.vue | 530
src/assets/icons/svg/exit-fullscreen.svg | 1
src/assets/logo/上海郢昱网络科技有限公司.png | 0
src/assets/system/shengchanheduan.svg | 1
src/views/system/dict/index.vue | 326
src/components/Crontab/year.vue | 143
src/api/personnelManagement/scheduling.js | 32
src/hooks/useModal.js | 41
src/views/monitor/druid/index.vue | 13
src/api/qualityManagement/qualityInspectParam.js | 27
src/views/productionManagement/workOrder/index.vue | 874
src/views/procurementManagement/index.vue | 418
src/views/productionManagement/productionDispatching/components/formDia.vue | 192
src/assets/img/emoji/lips.png | 0
src/views/productionManagement/processRoute/ItemsForm.vue | 531
src/api/personnelManagement/staffContract.js | 10
src/api/safeProduction/dangerInvestigation.js | 61
src/components/ProjectManagement/ProgressReportDialog.vue | 242
src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js | 152
src/api/fileManagement/borrow.js | 47
src/utils/dynamicTitle.js | 15
src/views/redirect/index.vue | 14
src/api/productionManagement/productStructure.js | 47
src/views/reportAnalysis/financialAnalysis/components/ProductTypeSwitch.vue | 98
src/api/salesManagement/returnOrder.js | 82
src/api/productionManagement/productBom.js | 74
src/views/basicData/product/ImportExcel/index.vue | 115
src/components/Dialog/FileListDialog.vue | 329
src/assets/icons/svg/education.svg | 1
src/assets/img/emoji/shangchuan.png | 0
src/assets/img/emoji/loudly-crying-face.png | 0
src/api/salesManagement/paymentShipping.js | 35
src/views/productionManagement/productionCosting/index.vue | 394
src/components/SvgIcon/index.vue | 53
src/views/chatHome/chatHomeIndex/ai-wd.js | 393
src/views/financialManagement/receivable/invoiceApply.vue | 927
src/views/equipmentManagement/kplMonitor/index.vue | 714
src/assets/img/emoji/face-vomiting.png | 0
src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue | 115
src/views/productionManagement/productStructure/Detail/index.vue | 699
src/components/IconSelect/index.vue | 111
src/views/system/user/index.vue | 551
src/views/login.vue | 893
src/api/personnelManagement/staffLeave.js | 33
src/api/personnelManagement/staffOnJob.js | 63
src/components/AttachmentUpload/file/index.vue | 309
src/views/personnelManagement/contractManagement/filesDia.vue | 197
src/assets/images/kucun.png | 0
src/views/safeProduction/hazardousMaterialsControl/index.vue | 934
src/api/energyManagement/waterManagement.js | 93
src/api/salesManagement/indicatorStats.js | 38
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue | 144
src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue | 365
src/assets/icons/svg/textarea.svg | 1
src/assets/img/emoji/取消.png | 0
src/views/monitor/logininfor/index.vue | 233
src/assets/icons/svg/sunny.svg | 1
src/views/productionManagement/workOrderManagement/index.vue | 1040
src/views/reportAnalysis/productionAnalysis/components/right-top.vue | 188
src/api/personnelManagement/class.js | 118
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue | 427
src/api/financialManagement/revenueManagement.js | 78
src/assets/images/xioashoushuju.png | 0
src/views/reportAnalysis/PSIDataAnalysis/components/ProductTypeSwitch.vue | 85
src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue | 69
src/api/collaborativeApproval/officeSupplies.js | 37
src/components/Dialog/FileList.vue | 263
src/assets/icons/png/circlePink@2x.png | 0
vite/plugins/svg-icon.js | 10
src/api/personnelManagement/selfService.js | 71
src/views/reportAnalysis/financialAnalysis/components/center-center.vue | 191
multiple/assets/logo/HQJCLogo.png | 0
src/components/Hamburger/index.vue | 50
src/views/collaborativeApproval/approvalProcess/fileList.vue | 66
src/views/equipmentManagement/measurementEquipment/components/dialogForm.vue | 7
src/views/productionManagement/processRoute/index.vue | 298
src/assets/icons/svg/zip.svg | 1
src/components/Crontab/hour.vue | 133
src/components/Crontab/result.vue | 540
src/views/safeProduction/emergencyPlanReview/index.vue | 847
src/assets/icons/svg/skill.svg | 1
src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsMappers.js | 221
src/store/modules/app.js | 46
src/views/inventoryManagement/stockManagement/Unqualified.vue | 187
src/assets/AI/销售助手.png | 0
src/assets/BI/biaoti.png | 0
src/assets/icons/svg/excel.svg | 1
src/assets/system/xiaoshoupeizhi.svg | 1
src/assets/img/emoji/sparkles.png | 0
src/views/collaborativeApproval/reportGeneration/index.vue | 596
src/views/productionManagement/productionReporting/Output.vue | 106
src/assets/images/chartCard.svg | 1
src/layout/components/TagsView/index.vue | 391
src/views/inventoryManagement/stockManagement/Qualified.vue | 210
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue | 352
src/views/equipmentManagement/iotMonitor/indexWD.vue | 317
src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue | 12
src/utils/request.js | 157
src/views/reportAnalysis/financialAnalysis/components/center-top.vue | 327
src/views/customerService/expiryAfterSales/index.vue | 273
src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue | 198
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue | 314
src/api/basicData/storageAttachment.js | 29
src/hooks/useChartBackground.js | 133
src/assets/icons/svg/post.svg | 1
src/views/financialManagement/receivable/outputInvoice.vue | 608
src/views/financialManagement/payable/purchaseReturn.vue | 198
src/views/safeProduction/dangerInvestigation/index.vue | 1181
src/views/reportAnalysis/productionAnalysis/index.vue | 306
src/views/equipmentManagement/measurementEquipment/components/formDia.vue | 325
src/views/basicData/parameterMaintenance/index.vue | 811
src/assets/img/fileImg/txt.png | 0
src/utils/auth.js | 15
src/views/equipmentManagement/ledger/Form.vue | 333
src/assets/images/caigou.png | 0
src/layout/components/InnerLink/index.vue | 35
src/views/collaborativeApproval/purchaseApproval/index.vue | 1095
src/components/Echarts/echarts.vue | 223
src/views/fileManagement/borrow/index.vue | 658
src/api/system/dict/type.js | 60
src/assets/img/emoji/clown-face.png | 0
src/views/qualityManagement/metricMaintenance/index.vue | 839
src/views/projectManagement/projectType/components/ProjectTypeDialog.vue | 431
src/views/inventoryManagement/stockManagement/New.vue | 223
src/assets/BI/caiwufenxiback@2x.png | 0
src/views/system/post/index.vue | 290
src/assets/indexViews/login-background.png | 0
src/views/productionManagement/safetyMonitoring/index.vue | 873
src/views/reportAnalysis/productionAnalysis/components/center-top.vue | 144
src/assets/icons/svg/logininfor.svg | 1
src/views/collaborativeApproval/processTracking/index.vue | 498
vite/plugins/auto-import.js | 12
src/views/system/notice/index.vue | 295
src/views/safeProduction/safeWorkApproval/fileList.vue | 67
src/layout/components/Settings/index.vue | 287
src/api/financialManagement/accountSales.js | 19
src/api/qualityManagement/qualityInspectFile.js | 26
bin/build.bat | 12
src/assets/img/emoji/money-mouth-face.png | 0
src/views/system/config/index.vue | 316
src/api/system/role.js | 119
src/components/SearchPanel/index.vue | 257
src/assets/img/emoji/two-hearts.png | 0
src/assets/icons/png/circleBlue@2x.png | 0
src/assets/img/head_portrait.jpg | 0
src/views/productManagement/productIdentifier/index.vue | 834
src/views/reportAnalysis/productionAnalysis/components/center-center.vue | 200
src/views/tool/build/TreeNodeDialog.vue | 93
src/views/salesManagement/returnOrder/components/formDia.vue | 743
src/assets/img/fileImg/excel.png | 0
src/api/monitor/operlog.js | 26
src/utils/dict.js | 24
src/api/projectManagement/role.js | 35
src/views/demo/fakePage/index.vue | 248
src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue | 197
src/api/productionManagement/productionProcess.js | 104
src/assets/icons/svg/slider.svg | 1
src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue | 87
src/api/collaborativeApproval/meeting.js | 118
src/views/reportAnalysis/PSIDataAnalysis/components/CarouselCards.vue | 306
multiple/assets/logo/XDRJ.png | 0
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue | 267
src/views/personnelManagement/dimission/components/formDia.vue | 347
src/plugins/cache.js | 79
src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue | 85
src/assets/BI/hetongtitleback@2x.png | 0
src/assets/icons/png/circleGreen@2x.png | 0
src/views/productionPlan/productionPlan/index.vue | 1371
src/api/procurementManagement/procurementReport.js | 11
src/components/PIMTable/Pagination.vue | 99
src/assets/system/kehudangan.svg | 1
src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue | 537
1,038 files changed, 210,220 insertions(+), 0 deletions(-)
diff --git a/bin/build.bat b/bin/build.bat
new file mode 100644
index 0000000..a4cc0df
--- /dev/null
+++ b/bin/build.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [锟斤拷息] 锟斤拷锟絎eb锟斤拷锟教o拷锟斤拷锟斤拷dist锟侥硷拷锟斤拷
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn build:prod
+
+pause
\ No newline at end of file
diff --git a/bin/package.bat b/bin/package.bat
new file mode 100644
index 0000000..8693727
--- /dev/null
+++ b/bin/package.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 安装Web工程,生成node_modules文件。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn --registry=https://registry.npmmirror.com
+
+pause
\ No newline at end of file
diff --git a/bin/run-web.bat b/bin/run-web.bat
new file mode 100644
index 0000000..f9d3ae8
--- /dev/null
+++ b/bin/run-web.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 使用 Vite 命令运行 Web 工程。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn dev
+
+pause
\ No newline at end of file
diff --git a/html/ie.html b/html/ie.html
new file mode 100644
index 0000000..390ce8a
--- /dev/null
+++ b/html/ie.html
@@ -0,0 +1,46 @@
+
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8" />
+ <title>璇峰崌绾ф偍鐨勬祻瑙堝櫒</title>
+ <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" >
+ <meta name="renderer" content="webkit">
+ <base target="_blank" />
+ <style type="text/css">
+ html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{border:0;font-size:100%;font:inherit;vertical-align:baseline;margin:0;padding:0}article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:none}table{border-collapse:collapse;border-spacing:0}
+ a{text-decoration:none;color:#0072c6;}a:hover{text-decoration:none;color:#004d8c;}
+ body{width:960px;margin:0 auto;padding:10px;font-size:14px;line-height:24px;color:#454545;font-family:'Microsoft YaHei UI','Microsoft YaHei',DengXian,SimSun,'Segoe UI',Tahoma,Helvetica,sans-serif;overflow-y:scroll}
+ h1{font-size:40px;line-height:80px;font-weight:100;margin-bottom:10px;}
+ h2{font-size:20px;line-height:25px;font-weight:100;margin:10px 0;}
+ em{color:red}
+ p{margin-bottom:10px;}
+ hr{margin:20px 0;border:0;border-top:1px solid #dadada}
+ span{display:block;font-size:12px;line-height:12px;}
+ .clean{clear:both;}
+ .browser{padding:10px 10px;}
+ .browser li{width:auto;padding:0 80px;margin-top:30px;height:34px;line-height:22px;float:left;list-style:none;background:url(data:image/png;base64,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) no-repeat;padding-left:40px}
+ .browser .browser-firefox{background-position:0 -34px}
+ .browser .browser-ie{background-position:0 -68px;margin-left:0px}
+ .browser .browser-360{background-position:0 -170px;margin-left: -27px}
+ </style>
+</head>
+<body style="margin-top:50px">
+<h1>璇峰崌绾ф偍鐨勬祻瑙堝櫒锛屼互渚挎垜浠洿濂界殑涓烘偍鎻愪緵鏈嶅姟锛�</h1>
+<p>鎮ㄦ鍦ㄤ娇鐢� Internet Explorer 鐨勬棭鏈熺増鏈紙IE11浠ヤ笅鐗堟湰鎴栦娇鐢ㄨ鍐呮牳鐨勬祻瑙堝櫒锛夈�傝繖鎰忓懗鐫�鍦ㄥ崌绾ф祻瑙堝櫒鍓嶏紝鎮ㄥ皢鏃犳硶璁块棶姝ょ綉绔欍��</p>
+<hr>
+<h2>璇锋敞鎰忥細寰蒋鍏徃瀵筗indows XP 鍙� Internet Explorer 鏃╂湡鐗堟湰鐨勬敮鎸佸凡缁忕粨鏉�</h2>
+<p>鑷� 2016 骞� 1 鏈� 12 鏃ヨ捣锛孧icrosoft 涓嶅啀涓� IE 11 浠ヤ笅鐗堟湰鎻愪緵鐩稿簲鏀寔鍜屾洿鏂般�傛病鏈夊叧閿殑娴忚鍣ㄥ畨鍏ㄦ洿鏂帮紝鎮ㄧ殑鐢佃剳鍙兘鏄撳彈鏈夊鐥呮瘨銆侀棿璋嶈蒋浠跺拰鍏朵粬鎭舵剰杞欢鐨勬敾鍑伙紝瀹冧滑鍙互绐冨彇鎴栨崯瀹虫偍鐨勪笟鍔℃暟鎹拰淇℃伅銆傝鍙傞槄 <a href="https://www.microsoft.com/zh-cn/WindowsForBusiness/End-of-IE-support">寰蒋瀵� Internet Explorer 鏃╂湡鐗堟湰鐨勬敮鎸佸皢浜� 2016 骞� 1 鏈� 12 鏃ョ粨鏉熺殑璇存槑</a> 銆�</p>
+<hr>
+<h2>鎮ㄥ彲浠ラ�夋嫨鏇村厛杩涚殑娴忚鍣�</h2>
+<p>鎺ㄨ崘浣跨敤浠ヤ笅娴忚鍣ㄧ殑鏈�鏂扮増鏈�傚鏋滄偍鐨勭數鑴戝凡鏈変互涓嬫祻瑙堝櫒鐨勬渶鏂扮増鏈垯鐩存帴浣跨敤璇ユ祻瑙堝櫒璁块棶鍗冲彲銆�</p>
+<ul class="browser">
+ <li class="browser-chrome"><a href="https://www.google.cn/chrome/browser/desktop/index.html?hl=zh-CN&standalone=1"> 璋锋瓕娴忚鍣�<span>Google Chrome</span></a></li>
+ <li class="browser-firefox"><a href="https://www.mozilla.org/zh-CN/firefox/new/"> 鐏嫄娴忚鍣�<span>Mozilla Firefox</span></a></li>
+ <li class="browser-ie"><a href="https://windows.microsoft.com/zh-cn/internet-explorer/download-ie"> IE 11 娴忚鍣�<span>Internet Explorer</span></a></li>
+ <li class="browser-360"><a href="http://se.360.cn/"> 360瀹夊叏娴忚鍣�<span>360 Chrome</span></a></li>
+ <div class="clean"></div>
+</ul>
+<hr>
+</body>
+</html>
\ No newline at end of file
diff --git a/multiple/assets/favicon/BWSMfavicon.ico b/multiple/assets/favicon/BWSMfavicon.ico
new file mode 100644
index 0000000..12c7cd0
--- /dev/null
+++ b/multiple/assets/favicon/BWSMfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/CKGMfavicon.ico b/multiple/assets/favicon/CKGMfavicon.ico
new file mode 100644
index 0000000..7668e42
--- /dev/null
+++ b/multiple/assets/favicon/CKGMfavicon.ico
Binary files differ
diff --git a/multiple/assets/favicon/HQJCfavicon.ico b/multiple/assets/favicon/HQJCfavicon.ico
new file mode 100644
index 0000000..65e6942
--- /dev/null
+++ b/multiple/assets/favicon/HQJCfavicon.ico
Binary files differ
diff --git a/multiple/assets/logo/BWSMLogo.png b/multiple/assets/logo/BWSMLogo.png
new file mode 100644
index 0000000..044d31c
--- /dev/null
+++ b/multiple/assets/logo/BWSMLogo.png
Binary files differ
diff --git a/multiple/assets/logo/CKGMLogo.png b/multiple/assets/logo/CKGMLogo.png
new file mode 100644
index 0000000..cfa60b2
--- /dev/null
+++ b/multiple/assets/logo/CKGMLogo.png
Binary files differ
diff --git a/multiple/assets/logo/HQJCLogo.png b/multiple/assets/logo/HQJCLogo.png
new file mode 100644
index 0000000..5e21e9c
--- /dev/null
+++ b/multiple/assets/logo/HQJCLogo.png
Binary files differ
diff --git a/multiple/assets/logo/XDRJ.png b/multiple/assets/logo/XDRJ.png
new file mode 100644
index 0000000..5b49e96
--- /dev/null
+++ b/multiple/assets/logo/XDRJ.png
Binary files differ
diff --git a/multiple/assets/screen/login-background.png b/multiple/assets/screen/login-background.png
new file mode 100644
index 0000000..b935d71
--- /dev/null
+++ b/multiple/assets/screen/login-background.png
Binary files differ
diff --git a/multiple/multiple-build.js b/multiple/multiple-build.js
new file mode 100644
index 0000000..afcd4d5
--- /dev/null
+++ b/multiple/multiple-build.js
@@ -0,0 +1,152 @@
+import fs from "fs/promises";
+import fsSync from "fs";
+import path from "path";
+import { fileURLToPath } from "url";
+import { execSync } from "child_process";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const data = await fs.readFile(path.join(__dirname, "config.json"), "utf-8");
+const config = JSON.parse(data);
+
+const rootPath = path.resolve(__dirname, "..");
+const resourcePath = path.join(rootPath, "multiple", "assets");
+const replacePath = path.join(rootPath, "replace");
+const envFilePath = path.join(rootPath, ".env.production.local");
+
+const params = parseArgs(process.argv);
+const company = resolveCompany(params);
+const companyMap = config[company];
+
+if (!companyMap) {
+ const availableCompanies = Object.entries(config)
+ .filter(([, value]) => value && typeof value === "object" && value.env)
+ .map(([key]) => key)
+ .sort();
+ throw new Error(
+ `鏈煡 company: "${company}"銆傚彲閫夊��: ${availableCompanies.join(", ")}`
+ );
+}
+
+console.log(`褰撳墠 company: ${company}`);
+
+async function copyFileWithOverwrite(src, dest) {
+ await fs.mkdir(path.dirname(dest), { recursive: true });
+ if (fsSync.existsSync(dest)) {
+ try {
+ await fs.chmod(dest, 0o666);
+ } catch {
+ // Ignore chmod failure and continue.
+ }
+ await fs.rm(dest, { force: true });
+ }
+ await fs.copyFile(src, dest);
+}
+
+try {
+ console.log("=======鐢熸垚.env=======");
+ const envContent =
+ Object.entries(companyMap.env)
+ .map(([key, value]) => `${key}='${value}'`)
+ .join("\n") + "\n";
+ await fs.writeFile(envFilePath, envContent, "utf-8");
+
+ console.log("=======淇敼璧勬簮=======");
+ for (const [key] of Object.entries(companyMap)) {
+ if (key === "env") continue;
+
+ const originFile = path.join(rootPath, config[key]);
+ const backupFile = path.join(replacePath, config[key]);
+ const replaceFile = path.join(resourcePath, companyMap[key]);
+
+ await copyFileWithOverwrite(originFile, backupFile);
+ await copyFileWithOverwrite(replaceFile, originFile);
+ }
+
+ console.log("=====寮�濮嬫墦鍖�=====");
+ const buildEnv = createBuildEnv(companyMap.env);
+ execSync("vite build", { stdio: "inherit", cwd: rootPath, env: buildEnv });
+ console.log("=====鎵撳寘瀹屾垚======");
+} finally {
+ console.log("=====鎭㈠璧勬簮======");
+
+ if (fsSync.existsSync(envFilePath)) {
+ await fs.unlink(envFilePath);
+ console.log(`馃棏锔� 宸插垹闄� ${envFilePath}`);
+ }
+
+ if (fsSync.existsSync(replacePath)) {
+ for (const [key] of Object.entries(companyMap)) {
+ if (key === "env") continue;
+
+ const originFile = path.join(rootPath, config[key]);
+ const backupFile = path.join(replacePath, config[key]);
+ await copyFileWithOverwrite(backupFile, originFile);
+ }
+ await fs.rm(replacePath, { recursive: true, force: true });
+ console.log(`馃棏锔� 宸插垹闄� ${replacePath}`);
+ }
+}
+
+function parseArgs(argv) {
+ const params = {};
+ for (let index = 2; index < argv.length; index++) {
+ const arg = argv[index];
+ if (!arg.startsWith("--")) continue;
+
+ const normalized = arg.slice(2);
+ const equalIndex = normalized.indexOf("=");
+ if (equalIndex >= 0) {
+ const key = normalized.slice(0, equalIndex);
+ const value = normalized.slice(equalIndex + 1);
+ params[key] = value || true;
+ continue;
+ }
+
+ const nextArg = argv[index + 1];
+ if (nextArg && !nextArg.startsWith("--")) {
+ params[normalized] = nextArg;
+ index += 1;
+ continue;
+ }
+
+ params[normalized] = true;
+ }
+ return params;
+}
+
+function resolveCompany(parsedParams) {
+ const fromArg = parseValue(parsedParams.company);
+ if (fromArg) return fromArg;
+
+ const fromNpmConfig = parseValue(process.env.npm_config_company);
+ if (fromNpmConfig) return fromNpmConfig;
+
+ const fromEnv = parseValue(process.env.COMPANY ?? process.env.company);
+ if (fromEnv) return fromEnv;
+
+ return "default";
+}
+
+function parseValue(value) {
+ if (value == null || value === true) return undefined;
+ if (typeof value !== "string") return undefined;
+ const trimmed = value.trim();
+ if (!trimmed) return undefined;
+ return trimmed.replace(/^["']|["']$/g, "");
+}
+
+function createBuildEnv(companyEnv) {
+ const env = { ...process.env };
+ for (const key of Object.keys(env)) {
+ if (key.startsWith("VITE_")) {
+ delete env[key];
+ }
+ }
+ return {
+ ...env,
+ ...companyEnv,
+ VITE_APP_ENV: "production",
+ };
+}
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..ce783dc
--- /dev/null
+++ b/public/favicon.ico
Binary files differ
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..fd2b3d5
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,15 @@
+<template>
+ <router-view />
+</template>
+
+<script setup>
+import useSettingsStore from '@/store/modules/settings'
+import { handleThemeStyle } from '@/utils/theme'
+
+onMounted(() => {
+ nextTick(() => {
+ // 鍒濆鍖栦富棰樻牱寮�
+ handleThemeStyle(useSettingsStore().theme)
+ })
+})
+</script>
diff --git a/src/api/basicData/common.js b/src/api/basicData/common.js
new file mode 100644
index 0000000..547c1e1
--- /dev/null
+++ b/src/api/basicData/common.js
@@ -0,0 +1,25 @@
+import request from '@/utils/request'
+
+// 閫氱敤涓婁紶鎺ュ彛锛屾敮鎸� FormData 鎵归噺浼犳枃浠�
+export function uploadFile(data) {
+ return request({
+ url: '/common/upload',
+ method: 'post',
+ data,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+}
+
+// 閫氱敤涓婁紶鎺ュ彛锛屾敮鎸� FormData 鎵归噺浼犳枃浠�,姘镐笉杩囨湡锛屾厧鐢�
+export function uploadPublicFile(data) {
+ return request({
+ url: '/common/public/upload',
+ method: 'post',
+ data,
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ })
+}
diff --git a/src/api/basicData/customer.js b/src/api/basicData/customer.js
new file mode 100644
index 0000000..0934193
--- /dev/null
+++ b/src/api/basicData/customer.js
@@ -0,0 +1,83 @@
+import request from '@/utils/request'
+
+export function listCustomer(query) {
+ return request({
+ url: '/basic/customer/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鍒嗛厤瀹㈡埛
+export function assignCustomer(data) {
+ return request({
+ url: '/basic/customer/assignCustomer',
+ method: 'post',
+ data
+ })
+}
+
+// 鍥炴敹瀹㈡埛
+export function recycleCustomer(data) {
+ return request({
+ url: '/basic/customer/recycleCustomer',
+ method: 'post',
+ data
+ })
+}
+
+// 娴佸叆鍏捣
+export function backCustomer(id) {
+ return request({
+ url: '/basic/customer/back/' + id,
+ method: 'post'
+ })
+}
+
+export function shareCustomer(data) {
+ return request({
+ url: '/basic/customer/together',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getCustomer(id) {
+ return request({
+ url: '/basic/customer/' + id,
+ method: 'get'
+ })
+}
+
+export function addCustomer(data) {
+ return request({
+ url: '/basic/customer/addCustomer',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateCustomer(data) {
+ return request({
+ url: '/basic/customer/updateCustomer',
+ method: 'post',
+ data: data
+ })
+}
+
+export function exportCustomer(query) {
+ return request({
+ url: '/basic/customer/export',
+ method: 'get',
+ params: query,
+ responseType: 'blob'
+ })
+}
+
+export function delCustomer(ids) {
+ return request({
+ url: '/basic/customer/delCustomer',
+ method: 'delete',
+ data: ids
+ })
+}
diff --git a/src/api/basicData/customerFile.js b/src/api/basicData/customerFile.js
new file mode 100644
index 0000000..6602ddc
--- /dev/null
+++ b/src/api/basicData/customerFile.js
@@ -0,0 +1,39 @@
+import request from '@/utils/request'
+
+export function addCustomerFollow(data) {
+ return request({
+ url: '/basic/customer-follow/add',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateCustomerFollow(data) {
+ return request({
+ url: '/basic/customer-follow/edit',
+ method: 'put',
+ data: data,
+ })
+}
+
+export function delCustomerFollow(id) {
+ return request({
+ url: '/basic/customer-follow/' + id,
+ method: 'delete',
+ })
+}
+
+export function addReturnVisit(data) {
+ return request({
+ url: '/basic/customer-follow/return-visit',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getReturnVisit(id) {
+ return request({
+ url: '/basic/customer-follow/return-visit/' + id,
+ method: 'get'
+ })
+}
diff --git a/src/api/basicData/enum.js b/src/api/basicData/enum.js
new file mode 100644
index 0000000..9e8b503
--- /dev/null
+++ b/src/api/basicData/enum.js
@@ -0,0 +1,49 @@
+import request from "@/utils/request.js";
+
+/** 瀹℃壒妯℃澘绫诲瀷绛夐�氱敤鏋氫妇锛圱ypeEnums锛� */
+export function getTypeEnums() {
+ return request({
+ url: '/basic/enum/TypeEnums',
+ method: 'get'
+ })
+}
+
+export function findAllStockRecordTypeOptions() {
+ return request({
+ url: '/basic/enum/stockRecordType',
+ method: 'get'
+ })
+}
+
+// 鍚堟牸鍏ュ簱鏉ユ簮绫诲瀷
+export function findAllQualifiedStockInRecordTypeOptions() {
+ return request({
+ url: '/basic/enum/StockInQualifiedRecordTypeEnum',
+ method: 'get'
+ })
+}
+
+// 涓嶅悎鏍煎叆搴撴潵婧愮被鍨�
+export function findAllUnQualifiedStockInRecordTypeOptions() {
+ return request({
+ url: '/basic/enum/StockInUnQualifiedRecordTypeEnum',
+ method: 'get'
+ })
+}
+
+// 鍚堟牸鍑哄簱鏉ユ簮绫诲瀷
+export function findAllQualifiedStockOutRecordTypeOptions() {
+ return request({
+ url: '/basic/enum/StockOutQualifiedRecordTypeEnum',
+ method: 'get'
+ })
+}
+
+// 涓嶅悎鏍煎嚭搴撴潵婧愮被鍨�
+export function findAllUnQualifiedStockOutRecordTypeOptions() {
+ return request({
+ url: '/basic/enum/StockOutUnQualifiedRecordTypeEnum',
+ method: 'get'
+ })
+}
+
diff --git a/src/api/basicData/parameterMaintenance.js b/src/api/basicData/parameterMaintenance.js
new file mode 100644
index 0000000..86d889e
--- /dev/null
+++ b/src/api/basicData/parameterMaintenance.js
@@ -0,0 +1,81 @@
+// 鍙傛暟缁存姢椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鏌ヨ鍙傛暟鍒楄〃
+export function parameterListPage(query) {
+ return request({
+ url: "/basic/parameter/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鍙傛暟
+export function addParameter(data) {
+ return request({
+ url: "/basic/parameter/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缂栬緫鍙傛暟
+export function updateParameter(data) {
+ return request({
+ url: "/basic/parameter/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鍙傛暟
+export function delParameter(ids) {
+ return request({
+ url: "/basic/parameter/del",
+ method: "delete",
+ data: Array.isArray(ids) ? ids : [ids],
+ });
+}
+
+// 鑾峰彇浜у搧绫诲瀷鍒楄〃
+export function getProductTypes() {
+ return request({
+ url: "/basic/product/typeList",
+ method: "get",
+ });
+}
+
+// 鏂板鍩虹鍙傛暟
+export function addBaseParam(data) {
+ return request({
+ url: "/technologyParam/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缂栬緫鍩虹鍙傛暟
+export function editBaseParam(data) {
+ return request({
+ url: "/technologyParam/edit",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鏌ヨ鍩虹鍙傛暟鍒楄〃
+export function getBaseParamList(query) {
+ return request({
+ url: "/technologyParam/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍒犻櫎鍩虹鍙傛暟
+export function removeBaseParam(ids) {
+ return request({
+ url: `/technologyParam/remove/` + ids,
+ method: "delete",
+ });
+}
diff --git a/src/api/basicData/product.js b/src/api/basicData/product.js
new file mode 100644
index 0000000..d29a064
--- /dev/null
+++ b/src/api/basicData/product.js
@@ -0,0 +1,67 @@
+// 浜у搧缁存姢椤甸潰鎺ュ彛
+import request from '@/utils/request'
+
+// 浜у搧鏍戞煡璇�
+export function productTreeList(query) {
+ return request({
+ url: '/basic/product/list',
+ method: 'get',
+ params: query
+ })
+}
+// 浜у搧鏍戞柊澧炰慨鏀�
+export function addOrEditProduct(query) {
+ return request({
+ url: '/basic/product/addOrEditProduct',
+ method: 'post',
+ data: query
+ })
+}
+// 瑙勬牸鍨嬪彿鏂板淇敼
+export function addOrEditProductModel(query) {
+ return request({
+ url: '/basic/product/addOrEditProductModel',
+ method: 'post',
+ data: query
+ })
+}
+// 浜у搧鏍戝垹闄�
+export function delProduct(query) {
+ return request({
+ url: '/basic/product/delProduct',
+ method: 'delete',
+ data: query
+ })
+}
+// 瑙勬牸鍨嬪彿鍒犻櫎
+export function delProductModel(query) {
+ return request({
+ url: '/basic/product/delProductModel',
+ method: 'delete',
+ data: query
+ })
+}
+// 瑙勬牸鍨嬪彿鏌ヨ
+export function modelList(query) {
+ return request({
+ url: '/basic/product/modelList',
+ method: 'get',
+ params: query
+ })
+}
+export function modelListPage(query) {
+ return request({
+ url: '/basic/product/modelListPage',
+ method: 'get',
+ params: query
+ })
+}
+
+// 涓嬭浇浜у搧瀵煎叆妯℃澘
+export function downloadProductModelImportTemplate() {
+ return request({
+ url: '/basic/product/export',
+ method: 'get',
+ responseType: 'blob'
+ })
+}
\ No newline at end of file
diff --git a/src/api/basicData/productModel.js b/src/api/basicData/productModel.js
new file mode 100644
index 0000000..8acc2ef
--- /dev/null
+++ b/src/api/basicData/productModel.js
@@ -0,0 +1,17 @@
+import request from "@/utils/request.js";
+
+export function productModelList(query) {
+ return request({
+ url: '/basic/product/pageModel',
+ method: 'get',
+ params: query
+ })
+}
+
+export function productModelListByUrl(url, query) {
+ return request({
+ url,
+ method: 'get',
+ params: query
+ })
+}
\ No newline at end of file
diff --git a/src/api/basicData/productProcess.js b/src/api/basicData/productProcess.js
new file mode 100644
index 0000000..46356fd
--- /dev/null
+++ b/src/api/basicData/productProcess.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// 宸ュ簭鍒楄〃鍒嗛〉鏌ヨ
+export function productProcessListPage(query) {
+ return request({
+ url: "/technologyOperation/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/basicData/storageAttachment.js b/src/api/basicData/storageAttachment.js
new file mode 100644
index 0000000..3e241f6
--- /dev/null
+++ b/src/api/basicData/storageAttachment.js
@@ -0,0 +1,29 @@
+// 闄勪欢椤甸潰鎺ュ彛
+import request from '@/utils/request'
+
+// 闄勪欢鏌ヨ
+export function attachmentList(query) {
+ return request({
+ url: '/storageAttachment/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 闄勪欢鏂板
+export function createAttachment(data) {
+ return request({
+ url: '/storageAttachment/add',
+ method: 'post',
+ data
+ })
+}
+
+// 闄勪欢鍒犻櫎
+export function deleteAttachment(data) {
+ return request({
+ url: '/storageAttachment/delete',
+ method: 'delete',
+ data
+ })
+}
diff --git a/src/api/basicData/supplierManageFile.js b/src/api/basicData/supplierManageFile.js
new file mode 100644
index 0000000..6d3f28b
--- /dev/null
+++ b/src/api/basicData/supplierManageFile.js
@@ -0,0 +1,75 @@
+// 渚涘簲鍟嗘。妗堥〉闈㈡帴鍙�
+import request from '@/utils/request'
+
+// 鍒嗛〉鏌ヨ
+export function listSupplier(query) {
+ return request({
+ url: '/system/supplier/listPage',
+ method: 'get',
+ params: query
+ })
+}
+// 鏌ヨ渚涘簲鍟嗕俊鎭缁�
+export function getSupplier(id) {
+ return request({
+ url: '/system/supplier/' + id,
+ method: 'get'
+ })
+}
+// 鏂板渚涘簲鍟嗕俊鎭�
+export function addSupplier(data) {
+ return request({
+ url: '/system/supplier/add',
+ method: 'post',
+ data: data
+ })
+}
+// 淇敼渚涘簲鍟嗕俊鎭�
+export function updateSupplier(data) {
+ return request({
+ url: '/system/supplier/update',
+ method: 'post',
+ data: data
+ })
+}
+// 瀵煎嚭渚涘簲鍟嗕俊鎭�
+export function exportSupplier(query) {
+ return request({
+ url: '/system/supplier/export',
+ method: 'get',
+ params: query,
+ responseType: 'blob'
+ })
+}
+// 鍒犻櫎渚涘簲鍟嗕俊鎭�
+export function delSupplier(ids) {
+ return request({
+ url: '/system/supplier/del',
+ method: 'delete',
+ data: ids
+ })
+}
+// 鏌ヨ闄勪欢鍒楄〃
+export function fileListPage(query) {
+ return request({
+ url: "/basic/supplierManageFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 淇濆瓨闄勪欢鍒楄〃
+export function fileAdd(query) {
+ return request({
+ url: "/basic/supplierManageFile/add",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎闄勪欢鍒楄〃
+export function fileDel(query) {
+ return request({
+ url: "/basic/supplierManageFile/del",
+ method: "delete",
+ data: query,
+ });
+}
diff --git a/src/api/collaborativeApproval/approvalManagement.js b/src/api/collaborativeApproval/approvalManagement.js
new file mode 100644
index 0000000..c2ce4c7
--- /dev/null
+++ b/src/api/collaborativeApproval/approvalManagement.js
@@ -0,0 +1,20 @@
+// 瀹℃壒绠$悊閰嶇疆
+import request from "@/utils/request";
+
+// 鏌ヨ瀹℃壒娴佺▼閰嶇疆鑺傜偣鍒楄〃
+export function getApproveProcessConfigNodeList(type) {
+ return request({
+ url: '/approveProcessConfigNode/list',
+ method: 'get',
+ params: { type },
+ })
+}
+
+// 鏂板瀹℃壒娴佺▼閰嶇疆鑺傜偣
+export function addApproveProcessConfigNode(data) {
+ return request({
+ url: '/approveProcessConfigNode/add',
+ method: 'post',
+ data: data,
+ })
+}
diff --git a/src/api/collaborativeApproval/approvalProcess.js b/src/api/collaborativeApproval/approvalProcess.js
new file mode 100644
index 0000000..415bed8
--- /dev/null
+++ b/src/api/collaborativeApproval/approvalProcess.js
@@ -0,0 +1,63 @@
+// 鍗忓悓瀹℃壒
+import request from "@/utils/request";
+
+export function approveProcessListPage(query) {
+ return request({
+ url: '/approveProcess/list',
+ method: 'get',
+ params: query,
+ })
+}
+export function getDept(query) {
+ return request({
+ url: '/approveProcess/getDept',
+ method: 'get',
+ params: query,
+ })
+}
+export function approveProcessGetInfo(query) {
+ return request({
+ url: '/approveProcess/get',
+ method: 'get',
+ params: query,
+ })
+}
+// 鏂板瀹℃壒娴佺▼
+export function approveProcessAdd(query) {
+ return request({
+ url: '/approveProcess/add',
+ method: 'post',
+ data: query,
+ })
+}
+// 淇敼瀹℃壒娴佺▼
+export function approveProcessUpdate(query) {
+ return request({
+ url: '/approveProcess/update',
+ method: 'post',
+ data: query,
+ })
+}
+// 鎻愪氦瀹℃壒
+export function updateApproveNode(query) {
+ return request({
+ url: '/approveNode/updateApproveNode',
+ method: 'post',
+ data: query,
+ })
+}
+// 鍒犻櫎瀹℃壒娴佺▼
+export function approveProcessDelete(query) {
+ return request({
+ url: '/approveProcess/deleteIds',
+ method: 'delete',
+ data: query,
+ })
+}
+// 鏌ヨ瀹℃壒娴佺▼
+export function approveProcessDetails(query) {
+ return request({
+ url: '/approveNode/details/' + query,
+ method: 'get',
+ })
+}
\ No newline at end of file
diff --git a/src/api/collaborativeApproval/attendanceManagement.js b/src/api/collaborativeApproval/attendanceManagement.js
new file mode 100644
index 0000000..3c99775
--- /dev/null
+++ b/src/api/collaborativeApproval/attendanceManagement.js
@@ -0,0 +1,136 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍋囨湡璁剧疆鍒楄〃
+export function listHolidaySettings(query) {
+ return request({
+ url: "/holidaySettings/getList",
+ method: "get",
+ params: query,
+ });
+}
+//鏌ヨ骞村亣瑙勫垯鍒楄〃
+export function listAnnualLeaveSettingList(query) {
+ return request({
+ url: "/holidaySettings/getAnnualLeaveSettingList",
+ method: "get",
+ params: query,
+ });
+}
+//鏌ヨ鍔犵彮瑙勫垯鍒楄〃
+export function listOvertimeSettingList(query) {
+ return request({
+ url: "/holidaySettings/getOvertimeSettingList",
+ method: "get",
+ params: query,
+ });
+}
+//鏌ヨ宸ヤ綔鏃堕棿瑙勫垯鍒楄〃
+export function listWorkingHoursSettingList(query) {
+ return request({
+ url: "/holidaySettings/getWorkingHoursSettingList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鍋囨湡璁剧疆
+export function addHolidaySettings(data) {
+ return request({
+ url: "/holidaySettings/add",
+ method: "post",
+ data: data,
+ });
+}
+//鏂板骞村亣瑙勫垯
+export function addAnnualLeaveSetting(data) {
+ return request({
+ url: "/holidaySettings/addAnnualLeaveSetting",
+ method: "post",
+ data: data,
+ });
+}
+//鏂板鍔犵彮瑙勫垯
+export function addOvertimeSetting(data) {
+ return request({
+ url: "/holidaySettings/addOvertimeSetting",
+ method: "post",
+ data: data,
+ });
+}
+//鏂板宸ヤ綔鏃堕棿瑙勫垯
+export function addWorkingHoursSetting(data) {
+ return request({
+ url: "/holidaySettings/addWorkingHoursSetting",
+ method: "post",
+ data: data,
+ });
+}
+
+
+// 淇敼鍋囨湡璁剧疆
+export function updateHolidaySettings(data) {
+ return request({
+ url: "/holidaySettings/update",
+ method: "post",
+ data: data,
+ });
+}
+// 淇敼骞村亣瑙勫垯
+export function updateAnnualLeaveSetting(data) {
+ return request({
+ url: "/holidaySettings/updateAnnualLeaveSetting",
+ method: "post",
+ data: data,
+ });
+}
+// 淇敼鍔犵彮瑙勫垯
+export function updateOvertimeSetting(data) {
+ return request({
+ url: "/holidaySettings/updateOvertimeSetting",
+ method: "post",
+ data: data,
+ });
+}
+// 淇敼宸ヤ綔鏃堕棿瑙勫垯
+export function updateWorkingHoursSetting(data) {
+ return request({
+ url: "/holidaySettings/updateWorkingHoursSetting",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鎵归噺鍒犻櫎鍋囨湡璁剧疆
+export function delHolidaySettings(query) {
+ return request({
+ url: "/holidaySettings/delete",
+ method: "delete",
+ data: query,
+ });
+}
+// 鎵归噺鍒犻櫎骞村亣瑙勫垯
+export function delAnnualLeaveSetting(query) {
+ return request({
+ url: "/holidaySettings/deleteAnnualLeaveSetting",
+ method: "delete",
+ data: query,
+ });
+}
+// 鎵归噺鍒犻櫎鍔犵彮瑙勫垯
+export function delOvertimeSetting(query) {
+ return request({
+ url: "/holidaySettings/deleteOvertimeSetting",
+ method: "delete",
+ data: query,
+ });
+}
+// 鎵归噺鍒犻櫎宸ヤ綔鏃堕棿瑙勫垯
+export function delWorkingHoursSetting(query) {
+ return request({
+ url: "/holidaySettings/deleteWorkingHoursSetting",
+ method: "delete",
+ data: query,
+ });
+}
+
+
diff --git a/src/api/collaborativeApproval/customerVisit.js b/src/api/collaborativeApproval/customerVisit.js
new file mode 100644
index 0000000..dce1fdf
--- /dev/null
+++ b/src/api/collaborativeApproval/customerVisit.js
@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+// 鑾峰彇鎷滆璁板綍鍒楄〃
+export function getVisitRecords(query) {
+ return request({
+ url: '/customerVisits/listPage',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/src/api/collaborativeApproval/enterpriseBook.js b/src/api/collaborativeApproval/enterpriseBook.js
new file mode 100644
index 0000000..2140b89
--- /dev/null
+++ b/src/api/collaborativeApproval/enterpriseBook.js
@@ -0,0 +1,67 @@
+import request from '@/utils/request'
+
+// 鏌ヨ涓汉閫氳褰�
+// 涓汉閫氳褰曢�氬父鏄敤鎴锋敹钘忔垨棰戠箒鑱旂郴鐨勪汉鍛�
+export function getPersonalContacts(page,query) {
+ return request({
+ url: '/staffContactsPersonal/getList',
+ method: 'get',
+ params: {
+ ...page,
+ ...query
+ }
+ })
+}
+
+// 娣诲姞鑱旂郴浜哄埌涓汉閫氳褰�
+export function addPersonalContact(data) {
+ return request({
+ url: '/staffContactsPersonal/add',
+ method: 'post',
+ data: data
+ })
+}
+
+// 浠庝釜浜洪�氳褰曠Щ闄よ仈绯讳汉
+export function removePersonalContact(id) {
+ return request({
+ url: '/staffContactsPersonal/delete/' + id,
+ method: 'delete'
+ })
+}
+
+// 鏌ヨ鍏叡閫氳褰�
+// 鍏叡閫氳褰曢�氬父鏄墍鏈夊憳宸ュ彲瑙佺殑鑱旂郴鏂瑰紡
+export function getPublicContacts(query) {
+ return request({
+ url: '/staff/contacts/public/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ鍗曚綅閫氳褰�
+// 鍗曚綅閫氳褰曢�氬父鎸夐儴闂ㄧ粍缁囩殑鍛樺伐鑱旂郴鏂瑰紡
+export function getCompanyContacts(query) {
+ return request({
+ url: '/staff/contacts/company/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ閮ㄩ棬閫氳褰曟爲缁撴瀯
+export function getDepartmentTree() {
+ return request({
+ url: '/staff/contacts/department/tree',
+ method: 'get'
+ })
+}
+
+// 鑾峰彇鍛樺伐璇︾粏淇℃伅
+export function getEmployeeDetail(employeeId) {
+ return request({
+ url: '/staff/staffOnJob/' + employeeId,
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/src/api/collaborativeApproval/knowledgeBase.js b/src/api/collaborativeApproval/knowledgeBase.js
new file mode 100644
index 0000000..b195525
--- /dev/null
+++ b/src/api/collaborativeApproval/knowledgeBase.js
@@ -0,0 +1,55 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鐭ヨ瘑搴撳垪琛�
+export function listKnowledgeBase(query) {
+ return request({
+ url: "/knowledgeBase/getList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ鐭ヨ瘑搴撹缁�
+// export function getKnowledgeBase(knowledgeBaseId) {
+// return request({
+// url: "/collaborativeApproval/knowledgeBase/" + knowledgeBaseId,
+// method: "get",
+// });
+// }
+
+// 鏂板鐭ヨ瘑搴�
+export function addKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鐭ヨ瘑搴�
+export function updateKnowledgeBase(data) {
+ return request({
+ url: "/knowledgeBase/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鐭ヨ瘑搴�
+export function delKnowledgeBase(query) {
+ return request({
+ url: "/knowledgeBase/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 鎵归噺鍒犻櫎鐭ヨ瘑搴�
+export function delKnowledgeBaseBatch(knowledgeBaseIds) {
+ return request({
+ url: "/knowledgeBase/batch",
+ method: "delete",
+ data: knowledgeBaseIds,
+ });
+}
+
diff --git a/src/api/collaborativeApproval/meeting.js b/src/api/collaborativeApproval/meeting.js
new file mode 100644
index 0000000..20df8b1
--- /dev/null
+++ b/src/api/collaborativeApproval/meeting.js
@@ -0,0 +1,118 @@
+import request from "@/utils/request";
+
+export function getMeetingRoomList(data) {
+ return request({
+ url: "/meeting/roomList",
+ method: "post",
+ data: data,
+ });
+}
+
+export function saveRoom(data) {
+ return request({
+ url: "/meeting/saveRoom",
+ method: "post",
+ data: data,
+ });
+}
+
+export function delRoom(id) {
+ return request({
+ url: "/meeting/delRoom/"+id,
+ method: "delete",
+ });
+}
+
+export function getRoomEnum() {
+ return request({
+ url: "/meeting/roomEnum",
+ method: "get",
+ });
+}
+
+export function getDraftList(data){
+ return request({
+ url: "/meeting/draftList",
+ method: "post",
+ data: data,
+ });
+}
+
+export function saveDraft(data) {
+ return request({
+ url: "/meeting/saveDraft",
+ method: "post",
+ data: data,
+ });
+}
+
+export function delDraft(id) {
+ return request({
+ url: "/meeting/delDraft/"+id,
+ method: "delete",
+ });
+}
+
+export function saveMeetingApplication(data){
+ return request({
+ url: "/meeting/saveMeetingApplication",
+ method: "post",
+ data: data,
+ });
+}
+
+export function getExamineList(data) {
+ return request({
+ url: "/meeting/applicationList",
+ method: "post",
+ data: data,
+ });
+}
+
+
+export function getMeetingUseList(data){
+ return request({
+ url: "/meeting/meetingUseList",
+ method: "post",
+ data: data,
+ });
+}
+
+export function getMeetingPublish(data){
+ return request({
+ url: "/meeting/meetingPublishList",
+ method: "post",
+ data: data
+ });
+}
+
+
+export function getMeetingMinutesByMeetingId(id){
+ return request({
+ url: "/meeting/getMeetingMinutesByMeetingId/"+id,
+ method: "get",
+ });
+}
+
+export function saveMeetingMinutes(data){
+ return request({
+ url: "/meeting/saveMeetingMinutes",
+ method: "post",
+ data: data,
+ });
+}
+
+
+export function getMeetSummary(){
+ return request({
+ url: "/meeting/getMeetSummary",
+ method: "get",
+ });
+}
+
+export function getMeetSummaryItems(){
+ return request({
+ url: "/meeting/getMeetSummaryItems",
+ method: "get",
+ });
+}
diff --git a/src/api/collaborativeApproval/noticeManagement.js b/src/api/collaborativeApproval/noticeManagement.js
new file mode 100644
index 0000000..c89f6c4
--- /dev/null
+++ b/src/api/collaborativeApproval/noticeManagement.js
@@ -0,0 +1,78 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍏憡鍒楄〃
+export function listNotice(query) {
+ return request({
+ url: "/collaborativeApproval/notice/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ鍏憡璇︾粏
+export function getNotice(noticeId) {
+ return request({
+ url: "/collaborativeApproval/notice/" + noticeId,
+ method: "get",
+ });
+}
+
+// 鏂板鍏憡
+export function addNotice(data) {
+ return request({
+ url: "/collaborativeApproval/notice/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鍏憡
+export function updateNotice(data) {
+ return request({
+ url: "/collaborativeApproval/notice/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鍏憡
+export function delNotice(ids) {
+ return request({
+ url: "/collaborativeApproval/notice/" + ids,
+ method: "delete",
+ });
+}
+
+// 鑾峰彇鍏憡鏁伴噺
+export function getCount() {
+ return request({
+ url: "/collaborativeApproval/notice/count",
+ method: "get",
+ });
+}
+
+// 鏌ヨ鍏憡绫诲瀷鍒楄〃
+export function listNoticeType() {
+ return request({
+ url: "/noticeType/list",
+ method: "get",
+ });
+}
+
+// 鏂板鍏憡绫诲瀷
+export function addNoticeType(data) {
+ return request({
+ url: "/noticeType/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鍏憡绫诲瀷
+export function delNoticeType(id) {
+ return request({
+ url: "/noticeType/del",
+ method: "delete",
+ data: [id],
+ });
+}
diff --git a/src/api/collaborativeApproval/notificationManagement.js b/src/api/collaborativeApproval/notificationManagement.js
new file mode 100644
index 0000000..abaeaa4
--- /dev/null
+++ b/src/api/collaborativeApproval/notificationManagement.js
@@ -0,0 +1,63 @@
+import request from "@/utils/request";
+
+// 鏌ヨ閫氱煡鍒楄〃
+export function listNotification(query) {
+ return request({
+ url: "/notificationManagement/getList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板閫氱煡
+export function addNotification(data) {
+ return request({
+ url: "/notificationManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+//鏂板浼氳
+export function addOnlineMeeting(data) {
+ return request({
+ url: "/notificationManagement/addOnlineMeeting",
+ method: "post",
+ data: data,
+ });
+}
+//鏂板鏂囦欢鍏变韩
+export function addFileSharing(data) {
+ return request({
+ url: "/notificationManagement/addFileSharing",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼閫氱煡
+export function updateNotification(data) {
+ return request({
+ url: "/notificationManagement/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鎵归噺鍒犻櫎閫氱煡
+export function delNotification(query) {
+ return request({
+ url: "/notificationManagement/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// // 鎵归噺鍒犻櫎鐭ヨ瘑搴�
+// export function delKnowledgeBaseBatch(knowledgeBaseIds) {
+// return request({
+// url: "/knowledgeBase/batch",
+// method: "delete",
+// data: knowledgeBaseIds,
+// });
+// }
+
diff --git a/src/api/collaborativeApproval/officeSupplies.js b/src/api/collaborativeApproval/officeSupplies.js
new file mode 100644
index 0000000..340293b
--- /dev/null
+++ b/src/api/collaborativeApproval/officeSupplies.js
@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍔炲叕鐗╄祫鍒楄〃
+export function listPage(query) {
+ return request({
+ url: '/officeSupplies/listPage',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏂板鍔炲叕鐗╄祫
+export function add(data) {
+ return request({
+ url: '/officeSupplies/add',
+ method: 'post',
+ data
+ })
+}
+
+// 淇敼鍔炲叕鐗╄祫
+export function update(data) {
+ return request({
+ url: '/officeSupplies/update',
+ method: 'post',
+ data
+ })
+}
+
+// 鍒犻櫎鍔炲叕鐗╄祫
+export function deleteOff(data) {
+ return request({
+ url: '/officeSupplies/delete',
+ method: 'delete',
+ data
+ })
+}
diff --git a/src/api/collaborativeApproval/planTemplate.js b/src/api/collaborativeApproval/planTemplate.js
new file mode 100644
index 0000000..24a6ac4
--- /dev/null
+++ b/src/api/collaborativeApproval/planTemplate.js
@@ -0,0 +1,64 @@
+import request from "@/utils/request";
+
+// 鏌ヨ璁″垝鍒楄〃
+export function listDutyPlan(query) {
+ return request({
+ url: "/dutyPlan/getList",
+ method: "get",
+ params: query
+ });
+}
+//鏁版嵁
+export function NumDutyPlan(query) {
+ return request({
+ url: "/dutyPlan/getNum",
+ method: "get",
+ params: query
+ });
+}
+
+// 鏂板璁″垝
+export function addDutyPlan(data) {
+ return request({
+ url: "/dutyPlan/add",
+ method: "post",
+ data: data,
+ });
+}
+
+
+// 淇敼璁″垝
+export function updateDutyPlan(data) {
+ return request({
+ url: "/dutyPlan/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎璁″垝
+export function delDutyPlan(query) {
+ return request({
+ url: "/dutyPlan/delete",
+ method: "delete",
+ data: query,
+ });
+}
+// 瀵煎嚭璁″垝
+export function exportDutyPlan(query) {
+ return request({
+ url: "/dutyPlan/export",
+ method: "post",
+ params: query,
+ });
+}
+
+// // 鎵归噺鍒犻櫎璁″垝
+// export function delDutyPlanBatch(dutyPlanIds) {
+// return request({
+// url: "/dutyPlan/batch",
+// method: "delete",
+// data: dutyPlanIds,
+// });
+// }
+
diff --git a/src/api/collaborativeApproval/rpaManagement.js b/src/api/collaborativeApproval/rpaManagement.js
new file mode 100644
index 0000000..273bf9f
--- /dev/null
+++ b/src/api/collaborativeApproval/rpaManagement.js
@@ -0,0 +1,78 @@
+import request from "@/utils/request";
+
+// 鏌ヨRPA鍒楄〃
+export function listRpa(query) {
+ return request({
+ url: "/rpaProcessAutomation/getList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨRPA璇︾粏
+export function getRpa(rpaId) {
+ return request({
+ url: "/collaborativeApproval/rpa/" + rpaId,
+ method: "get",
+ });
+}
+
+// 鏂板RPA
+export function addRpa(data) {
+ return request({
+ url: "/rpaProcessAutomation/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼RPA
+export function updateRpa(data) {
+ return request({
+ url: "/rpaProcessAutomation/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎RPA
+export function delRpa(query) {
+ return request({
+ url: "/rpaProcessAutomation/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 鎵归噺鍒犻櫎RPA
+export function delRpaBatch(rpaIds) {
+ return request({
+ url: "/collaborativeApproval/rpa/batch",
+ method: "delete",
+ data: rpaIds,
+ });
+}
+
+// 鍚姩RPA
+export function startRpa(rpaId) {
+ return request({
+ url: "/collaborativeApproval/rpa/start/" + rpaId,
+ method: "post",
+ });
+}
+
+// 鍋滄RPA
+export function stopRpa(rpaId) {
+ return request({
+ url: "/collaborativeApproval/rpa/stop/" + rpaId,
+ method: "post",
+ });
+}
+
+// 鑾峰彇RPA鐘舵��
+export function getRpaStatus(rpaId) {
+ return request({
+ url: "/collaborativeApproval/rpa/status/" + rpaId,
+ method: "get",
+ });
+}
diff --git a/src/api/collaborativeApproval/rulesRegulationsManagementFile.js b/src/api/collaborativeApproval/rulesRegulationsManagementFile.js
new file mode 100644
index 0000000..791b6a7
--- /dev/null
+++ b/src/api/collaborativeApproval/rulesRegulationsManagementFile.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 闄勪欢鍒楄〃
+export function listRuleFiles(query) {
+ return request({
+ url: "/rulesRegulationsManagementFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板闄勪欢
+export function addRuleFile(data) {
+ return request({
+ url: "/rulesRegulationsManagementFile/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎闄勪欢锛堟敮鎸佷紶閫� id 鏁扮粍锛�
+export function delRuleFile(ids) {
+ return request({
+ url: "/rulesRegulationsManagementFile/del",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/api/collaborativeApproval/sealManagement.js b/src/api/collaborativeApproval/sealManagement.js
new file mode 100644
index 0000000..cb990b3
--- /dev/null
+++ b/src/api/collaborativeApproval/sealManagement.js
@@ -0,0 +1,116 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍗扮珷鐢宠鍒楄〃
+export function listSealApplication(page,query) {
+ return request({
+ url: "/sealApplicationManagement/getList",
+ method: "get",
+ params: {
+ ...page,
+ ...query},
+ });
+}
+// 鏌ヨ瑙勭珷鍒跺害鍒楄〃
+export function listRuleManagement(page,query) {
+ return request({
+ url: "/rulesRegulationsManagement/getList",
+ method: "get",
+ params: {
+ ...page,
+ ...query},
+ });
+}
+// 鏌ヨ闃呰鐘舵�佸垪琛�
+export function getReadingStatusList(page,query) {
+ return request({
+ url: "/rulesRegulationsManagement/getReadingStatusList",
+ method: "get",
+ params: {
+ ...page,
+ ...query},
+ });
+}
+// 鏍规嵁瑙勫垯id鏌ヨ闃呰鐘舵�佸垪琛�
+export function getReadingStatusByRuleId(id) {
+ return request({
+ url: "/rulesRegulationsManagement/getReadingStatusByRuleId/"+id,
+ method: "get"
+ });
+}
+
+// 鏂板鍗扮珷鐢宠
+export function addSealApplication(data) {
+ return request({
+ url: "/sealApplicationManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+// 鏂板瑙勭珷鍒跺害
+export function addRuleManagement(data) {
+ return request({
+ url: "/rulesRegulationsManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+// 鏂板闃呰鐘舵��
+export function addReadingStatus(data) {
+ return request({
+ url: "/rulesRegulationsManagement/addReadingStatus",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鍗扮珷鐢宠
+export function updateSealApplication(data) {
+ return request({
+ url: "/sealApplicationManagement/update",
+ method: "post",
+ data: data,
+ });
+}
+// 淇敼瑙勭珷鍒跺害
+export function updateRuleManagement(data) {
+ return request({
+ url: "/rulesRegulationsManagement/update",
+ method: "post",
+ data: data,
+ });
+}
+// 淇敼闃呰鐘舵��
+export function updateReadingStatus(data) {
+ return request({
+ url: "/rulesRegulationsManagement/updateReadingStatus",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鍗扮珷鐢宠
+export function delSealApplication(query) {
+ return request({
+ url: "/sealApplicationManagement/delete",
+ method: "delete",
+ data: query,
+ });
+}
+// 鍒犻櫎瑙勭珷鍒跺害
+export function delRuleManagement(query) {
+ return request({
+ url: "/rulesRegulationsManagement/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 鎵归噺鍒犻櫎鐭ヨ瘑搴�
+export function delKnowledgeBaseBatch(knowledgeBaseIds) {
+ return request({
+ url: "/knowledgeBase/batch",
+ method: "delete",
+ data: knowledgeBaseIds,
+ });
+}
+
diff --git a/src/api/collaborativeApproval/shipmentReview.js b/src/api/collaborativeApproval/shipmentReview.js
new file mode 100644
index 0000000..64fac69
--- /dev/null
+++ b/src/api/collaborativeApproval/shipmentReview.js
@@ -0,0 +1,21 @@
+// 鍙戣揣瀹℃壒
+import request from "@/utils/request";
+
+// 鑾峰彇鍙戣揣瀹℃壒鍒楄〃
+export function getShipmentApprovalList(query) {
+ return request({
+ url: '/shipmentApproval/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鍙戣揣鐢宠鎵瑰噯
+// /shipmentApproval/update
+export function approveShipment(query) {
+ return request({
+ url: '/shipmentApproval/update',
+ method: 'post',
+ data: query,
+ })
+}
\ No newline at end of file
diff --git a/src/api/energyManagement/index.js b/src/api/energyManagement/index.js
new file mode 100644
index 0000000..f2c5494
--- /dev/null
+++ b/src/api/energyManagement/index.js
@@ -0,0 +1,127 @@
+// 鑳芥簮绠$悊
+import request from "@/utils/request";
+
+// 璁惧鑳借��-鍒嗛〉鏌ヨ
+export function equipmentEnergyListPage(query) {
+ return request({
+ url: "/equipmentEnergyConsumption/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// -鑳芥簮瓒嬪娍-鍒嗛〉鏌ヨ
+export function listPageByTrend(query) {
+ return request({
+ url: "/equipmentEnergyConsumption/listPageByTrend",
+ method: "get",
+ params: query,
+ });
+}
+// 鍖哄煙-鍒嗛〉鏌ヨ
+export function areaListPage(query) {
+ return request({
+ url: "/electricityConsumptionArea/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍖哄煙-鏍�
+export function areaListTree(query) {
+ return request({
+ url: "/electricityConsumptionArea/list",
+ method: "get",
+ params: query,
+ });
+}
+// 鏃堕棿鍛ㄦ湡-鍒嗛〉鏌ヨ
+export function periodListPage(query) {
+ return request({
+ url: "/energyPeriod/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 璁惧鑳借��-鍒犻櫎
+export function equipmentEnergyDelete(query) {
+ return request({
+ url: "/equipmentEnergyConsumption/delete",
+ method: "delete",
+ data: query,
+ });
+}
+// 鍖哄煙-鍒犻櫎
+export function areaDelete(query) {
+ return request({
+ url: "/electricityConsumptionArea/delete",
+ method: "delete",
+ data: query,
+ });
+}
+// 鏃堕棿鍛ㄦ湡-鍒犻櫎
+export function periodDelete(query) {
+ return request({
+ url: "/energyPeriod/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 璁惧鑳借��-鏂板
+export function equipmentEnergyAdd(query) {
+ return request({
+ url: "/equipmentEnergyConsumption/add",
+ method: "post",
+ data: query,
+ });
+}
+// 鍖哄煙-鏂板
+export function areaAdd(query) {
+ return request({
+ url: "/electricityConsumptionArea/add",
+ method: "post",
+ data: query,
+ });
+}
+
+// 鏃堕棿鍛ㄦ湡-鏂板
+export function periodAdd(query) {
+ return request({
+ url: "/energyPeriod/add",
+ method: "post",
+ data: query,
+ });
+}
+// 璁惧鑳借��-淇敼
+export function equipmentEnergyUpdate(query) {
+ return request({
+ url: "/equipmentEnergyConsumption/update",
+ method: "post",
+ data: query,
+ });
+}
+//鍖哄煙-淇敼
+export function areaUpdate(query) {
+ return request({
+ url: "/electricityConsumptionArea/update",
+ method: "post",
+ data: query,
+ });
+}
+// 鏃堕棿鍛ㄦ湡-淇敼
+export function periodUpdate(query) {
+ return request({
+ url: "/energyPeriod/update",
+ method: "post",
+ data: query,
+ });
+}
+
+// 璁惧涓嬫媺妗嗘煡璇�
+export function deviceList(query) {
+ return request({
+ url: "/equipmentEnergyConsumption/deviceList",
+ method: "get",
+ });
+}
diff --git a/src/api/energyManagement/waterManagement.js b/src/api/energyManagement/waterManagement.js
new file mode 100644
index 0000000..6dbf115
--- /dev/null
+++ b/src/api/energyManagement/waterManagement.js
@@ -0,0 +1,93 @@
+// 鐢ㄦ按绠$悊
+import request from "@/utils/request";
+
+// 鐢ㄦ按璁惧-鍒嗛〉鏌ヨ
+export function waterEquipmentListPage(query) {
+ return request({
+ url: '/waterRecord/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鐢ㄦ按瓒嬪娍-鍒嗛〉鏌ヨ
+export function listPageByWaterTrend(query) {
+ return request({
+ url: '/waterRecord/listPageByTrend',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鐢ㄦ按璁惧-鍒犻櫎
+export function waterEquipmentDelete(query) {
+ return request({
+ url: '/waterRecord/delete',
+ method: 'delete',
+ data: query,
+ })
+}
+
+// 鐢ㄦ按璁惧-鏂板
+export function waterEquipmentAdd(query) {
+ return request({
+ url: '/waterRecord/add',
+ method: 'post',
+ data: query,
+ })
+}
+
+// 鐢ㄦ按璁惧-淇敼
+export function waterEquipmentUpdate(query) {
+ return request({
+ url: '/waterRecord/update',
+ method: 'post',
+ data: query,
+ })
+}
+
+// 鐢ㄦ按璁惧涓嬫媺妗嗘煡璇�
+export function waterDeviceList(query) {
+ return request({
+ url: '/device/ledger/page',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 姘磋垂绠$悊-鍒嗛〉鏌ヨ
+export function waterBillListPage(query) {
+ return request({
+ url: '/waterBill/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 姘磋垂绠$悊-鏂板
+export function waterBillAdd(query) {
+ return request({
+ url: '/waterBill/add',
+ method: 'post',
+ data: query,
+ })
+}
+
+// 姘磋垂绠$悊-淇敼
+export function waterBillUpdate(query) {
+ return request({
+ url: '/waterBill/update',
+ method: 'post',
+ data: query,
+ })
+}
+
+// 姘磋垂绠$悊-鍒犻櫎
+export function waterBillDelete(query) {
+ return request({
+ url: '/waterBill/delete',
+ method: 'delete',
+ data: query,
+ })
+}
+
diff --git a/src/api/equipmentManagement/brand.js b/src/api/equipmentManagement/brand.js
new file mode 100644
index 0000000..040cb38
--- /dev/null
+++ b/src/api/equipmentManagement/brand.js
@@ -0,0 +1,93 @@
+// 璁惧鍝佺墝绠$悊 - 鏈湴鍋囨暟鎹� API锛堜娇鐢� localStorage 鎸佷箙鍖栵級
+
+const STORAGE_KEY = 'EQUIPMENT_BRANDS';
+
+function readStore() {
+ try {
+ const raw = localStorage.getItem(STORAGE_KEY);
+ if (raw) {
+ const parsed = JSON.parse(raw);
+ if (Array.isArray(parsed)) return parsed;
+ }
+ } catch (e) {
+ // ignore
+ }
+ // 鍒濆鍖栦竴浜涚ず渚嬫暟鎹�
+ const initial = [
+ { id: 1, name: '瑗块棬瀛�', country: '寰峰浗', description: '宸ヤ笟鑷姩鍖栦笌鐢垫皵宸ョ▼鍝佺墝', createdAt: Date.now() - 86400000 * 10 },
+ { id: 2, name: '鏂借�愬痉', country: '娉曞浗', description: '鑳芥簮绠$悊涓庤嚜鍔ㄥ寲', createdAt: Date.now() - 86400000 * 7 },
+ { id: 3, name: '涓夎彵鐢垫満', country: '鏃ユ湰', description: '鐢垫皵涓庤嚜鍔ㄥ寲璁惧', createdAt: Date.now() - 86400000 * 3 },
+ ];
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(initial));
+ return initial;
+}
+
+function writeStore(list) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
+}
+
+function nextId(list) {
+ const maxId = list.reduce((max, item) => Math.max(max, Number(item.id) || 0), 0);
+ return maxId + 1;
+}
+
+export function getBrandPage(params = {}) {
+ const { current = 1, size = 10, name } = params;
+ const list = readStore();
+ let filtered = list;
+ if (name) {
+ const kw = String(name).trim();
+ filtered = filtered.filter((b) =>
+ (b.name && b.name.includes(kw)) || (b.country && b.country.includes(kw))
+ );
+ }
+ const start = (current - 1) * size;
+ const end = start + Number(size);
+ const records = filtered.slice(start, end);
+ return Promise.resolve({
+ code: 200,
+ data: {
+ total: filtered.length,
+ records,
+ },
+ msg: 'ok',
+ });
+}
+
+export function getBrandById(id) {
+ const list = readStore();
+ const item = list.find((i) => String(i.id) === String(id));
+ return Promise.resolve({ code: 200, data: item || null, msg: 'ok' });
+}
+
+export function addBrand(data) {
+ const list = readStore();
+ const item = { ...data };
+ item.id = nextId(list);
+ item.createdAt = Date.now();
+ list.unshift(item);
+ writeStore(list);
+ return Promise.resolve({ code: 200, data: item, msg: '鏂板鎴愬姛' });
+}
+
+export function editBrand(data) {
+ const list = readStore();
+ const index = list.findIndex((i) => String(i.id) === String(data.id));
+ if (index !== -1) {
+ list[index] = { ...list[index], ...data };
+ writeStore(list);
+ return Promise.resolve({ code: 200, data: list[index], msg: '淇敼鎴愬姛' });
+ }
+ return Promise.resolve({ code: 404, data: null, msg: '鏈壘鍒拌鍝佺墝' });
+}
+
+export function delBrand(idOrIds) {
+ const list = readStore();
+ const ids = Array.isArray(idOrIds) ? idOrIds.map(String) : [String(idOrIds)];
+ const newList = list.filter((i) => !ids.includes(String(i.id)));
+ writeStore(newList);
+ return Promise.resolve({ code: 200, data: null, msg: '鍒犻櫎鎴愬姛' });
+}
+
+
+
diff --git a/src/api/equipmentManagement/calibration.js b/src/api/equipmentManagement/calibration.js
new file mode 100644
index 0000000..b0a9844
--- /dev/null
+++ b/src/api/equipmentManagement/calibration.js
@@ -0,0 +1,35 @@
+// 妫�瀹氭牎鍑嗚褰�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function ledgerRecordListPage(query) {
+ return request({
+ url: "/measuringInstrumentLedgerRecord/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏍″噯
+export function ledgerRecordVerifying(query) {
+ return request({
+ url: "/measuringInstrumentLedger/verifying",
+ method: "post",
+ data: query,
+ });
+}
+// 淇敼鏍″噯
+export function ledgerRecordUpdate(query) {
+ return request({
+ url: "/measuringInstrumentLedgerRecord/update",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎璁板綍
+export function ledgerRecordDelete(ids) {
+ return request({
+ url: "/measuringInstrumentLedgerRecord/delete",
+ method: "delete",
+ data: ids,
+ });
+}
\ No newline at end of file
diff --git a/src/api/equipmentManagement/defectManagement.js b/src/api/equipmentManagement/defectManagement.js
new file mode 100644
index 0000000..c52eff9
--- /dev/null
+++ b/src/api/equipmentManagement/defectManagement.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request';
+
+// 鐧昏缂洪櫡
+export function registerDefect(data) {
+ return request({
+ url: '/defect/add',
+ method: 'post',
+ data
+ });
+}
+
+// 鑾峰彇缂洪櫡鍒楄〃
+export function getDefectList() {
+ return request({
+ url: '/defect/page',
+ method: 'get'
+ });
+}
+
+// 娑堥櫎缂洪櫡-淇敼鐘舵��
+export function eliminateDefect(data) {
+ return request({
+ url: '/defect/update',
+ method: 'post',
+ data
+ });
+}
+//鍒犻櫎
+export function deleteDefect(id) {
+ return request({
+ url: '/defect/delete',
+ method: 'delete',
+ id
+ });
+}
+
+
+// 鑾峰彇缂洪櫡璁惧鍙拌处
+export function getDefectLedger(deviceLedgerId) {
+ return request({
+ url: '/defect//find/' + deviceLedgerId,
+ method: 'get'
+ });
+}
\ No newline at end of file
diff --git a/src/api/equipmentManagement/deviceInfo.js b/src/api/equipmentManagement/deviceInfo.js
new file mode 100644
index 0000000..d71b713
--- /dev/null
+++ b/src/api/equipmentManagement/deviceInfo.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// 鑾峰彇璁惧鍩烘湰淇℃伅
+export function getDeviceInfo(params) {
+ return request({
+ url: "/device/ledger/scanDevice",
+ method: "get",
+ params,
+ });
+}
diff --git a/src/api/equipmentManagement/ledger.js b/src/api/equipmentManagement/ledger.js
new file mode 100644
index 0000000..d1b65b0
--- /dev/null
+++ b/src/api/equipmentManagement/ledger.js
@@ -0,0 +1,44 @@
+import request from "@/utils/request";
+
+export const getLedgerPage = (params) => {
+ return request({
+ url: "/device/ledger/page",
+ method: "get",
+ params,
+ });
+};
+export const getLedgerById = (id) => {
+ return request({
+ url: `/device/ledger/${id}`,
+ method: "get",
+ });
+};
+
+export const addLedger = (data) => {
+ return request({
+ url: "/device/ledger",
+ method: "post",
+ data,
+ });
+};
+export const editLedger = (data) => {
+ return request({
+ url: "/device/ledger",
+ method: "put",
+ data,
+ });
+};
+
+export const delLedger = (id) => {
+ return request({
+ url: `/device/ledger/${id}`,
+ method: "delete",
+ });
+};
+
+export const getDeviceLedger = () => {
+ return request({
+ url: "/device/ledger/getDeviceLedger",
+ method: "get",
+ });
+};
diff --git a/src/api/equipmentManagement/maintenanceTaskFile.js b/src/api/equipmentManagement/maintenanceTaskFile.js
new file mode 100644
index 0000000..8373ae3
--- /dev/null
+++ b/src/api/equipmentManagement/maintenanceTaskFile.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 鏌ヨ淇濆吇浠诲姟闄勪欢鍒楄〃
+export function listMaintenanceTaskFiles(query) {
+ return request({
+ url: "/maintenanceTaskFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板淇濆吇浠诲姟闄勪欢
+export function addMaintenanceTaskFile(data) {
+ return request({
+ url: "/maintenanceTaskFile/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎淇濆吇浠诲姟闄勪欢
+export function delMaintenanceTaskFile(id) {
+ return request({
+ url: "/maintenanceTaskFile/del",
+ method: "delete",
+ data: Array.isArray(id) ? id : [id],
+ });
+}
diff --git a/src/api/equipmentManagement/measurementEquipment.js b/src/api/equipmentManagement/measurementEquipment.js
new file mode 100644
index 0000000..ba73317
--- /dev/null
+++ b/src/api/equipmentManagement/measurementEquipment.js
@@ -0,0 +1,82 @@
+// 璁¢噺鍣ㄥ叿鍙拌处
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function measuringInstrumentListPage(query) {
+ return request({
+ url: "/measuringInstrumentLedger/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鍒犻櫎
+export function measuringInstrumentDelete(query) {
+ return request({
+ url: "/measuringInstrumentLedger/delete",
+ method: "delete",
+ data: query,
+ });
+}
+// 鏂板
+export function measuringInstrumentAdd(query) {
+ return request({
+ url: "/measuringInstrumentLedger/add",
+ method: "post",
+ data: query,
+ });
+}
+// 淇敼
+export function measuringInstrumentUpdate(query) {
+ return request({
+ url: "/measuringInstrumentLedger/update",
+ method: "post",
+ data: query,
+ });
+}
+
+// 璁¢噺鍣ㄥ叿鍙拌处-鏂板
+// /measuringInstrumentLedger/add
+export function addMeasuringInstrumentLedger(data){
+ return request({
+ url:"/measuringInstrumentLedger/add",
+ method:"post",
+ data
+ })
+}
+
+// 璁¢噺鍣ㄥ叿鍙拌处-缂栬緫
+// /measuringInstrumentLedger/update
+export function updateMeasuringInstrumentLedger(data){
+ return request({
+ url:"/measuringInstrumentLedger/update",
+ method:"post",
+ data
+ })
+}
+
+// 閫氱敤闄勪欢鏌ヨ
+export function getStorageAttachmentList(query) {
+ return request({
+ url: "/storageAttachment/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閫氱敤闄勪欢淇濆瓨
+export function addStorageAttachment(data) {
+ return request({
+ url: "/storageAttachment/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 閫氱敤闄勪欢鍒犻櫎
+export function delStorageAttachment(ids) {
+ return request({
+ url: "/storageAttachment/delete",
+ method: "delete",
+ data: ids,
+ });
+}
\ No newline at end of file
diff --git a/src/api/equipmentManagement/repair.js b/src/api/equipmentManagement/repair.js
new file mode 100644
index 0000000..16bfd28
--- /dev/null
+++ b/src/api/equipmentManagement/repair.js
@@ -0,0 +1,85 @@
+import request from "@/utils/request";
+
+/**
+ * @desc 璁惧鎶ヤ慨鍒楄〃
+ * @param {鍒嗛〉鏌ヨ} params
+ * @returns
+ */
+export const getRepairPage = (params) => {
+ return request({
+ url: "/device/repair/page",
+ method: "get",
+ params,
+ });
+};
+
+/**
+ * @desc 鏂板鎶ヤ慨
+ * @param {鎶ヤ慨鍙傛暟} data
+ * @returns
+ */
+export const addRepair = (data) => {
+ return request({
+ url: "/device/repair",
+ method: "post",
+ data,
+ });
+};
+
+/**
+ * @desc 缂栬緫鎶ヤ慨
+ * @param {鎶ヤ慨鍙傛暟} data
+ * @returns
+ */
+export const editRepair = (data) => {
+ return request({
+ url: "/device/repair",
+ method: "put",
+ data,
+ });
+};
+
+/**
+ * @desc 鏍规嵁id鏌ヨ涓�鏉℃姤淇�
+ * @param {鎶ヤ慨id} id
+ * @returns
+ */
+export const getRepairById = (id) => {
+ return request({
+ url: `/device/repair/${id}`,
+ method: "get",
+ });
+};
+
+/**
+ * @desc 鍒犻櫎鎶ヤ慨
+ * @param {缂栧彿} ids
+ * @returns
+ */
+export const delRepair = (ids) => {
+ return request({
+ url: `/device/repair/${ids}`,
+ method: "delete",
+ });
+};
+
+export const addMaintain = (data) => {
+ return request({
+ url: `/device/repair/repair`,
+ method: "post",
+ data,
+ });
+};
+
+/**
+ * @desc 楠屾敹瀹℃壒
+ * @param {楠屾敹鍙傛暟} data
+ * @returns
+ */
+export const repairAcceptance = (data) => {
+ return request({
+ url: `/device/repair/acceptance`,
+ method: "post",
+ data,
+ });
+};
diff --git a/src/api/equipmentManagement/spareParts.js b/src/api/equipmentManagement/spareParts.js
new file mode 100644
index 0000000..2b64689
--- /dev/null
+++ b/src/api/equipmentManagement/spareParts.js
@@ -0,0 +1,58 @@
+import request from "@/utils/request";
+/**
+ * 澶囦欢鍒嗙被-鏍戝垪琛�
+ */
+export const getSparePartsTree = (params) => {
+ return request({
+ url: "/spareParts/getTree",
+ method: "get",
+ params,
+ });
+};
+/**
+ * 澶囦欢鍒嗙被-鍒嗛〉鏌ヨ鍒楄〃
+ */
+export const getSparePartsList = (params) => {
+ return request({
+ url: "/spareParts/listPage",
+ method: "get",
+ params,
+ });
+};
+
+/**
+ * @desc 鏂板
+ */
+export const addSparePart = (data) => {
+ return request({
+ url: "/spareParts/add",
+ method: "post",
+ data,
+ });
+};
+
+/**
+ * @desc 缂栬緫
+ */
+export const editSparePart = (data) => {
+ return request({
+ url: "/spareParts/update",
+ method: "post",
+ data,
+ });
+};
+
+/**
+ * @desc 鍒犻櫎鎶ヤ慨
+ * @param {缂栧彿} ids
+ * @returns
+ */
+export const delSparePart = (id) => {
+ return request({
+ url: '/spareParts/delete/'+id,
+ method: "delete",
+
+ });
+};
+
+
diff --git a/src/api/equipmentManagement/sparePartsUsage.js b/src/api/equipmentManagement/sparePartsUsage.js
new file mode 100644
index 0000000..e9384aa
--- /dev/null
+++ b/src/api/equipmentManagement/sparePartsUsage.js
@@ -0,0 +1,36 @@
+import request from "@/utils/request";
+
+/**
+ * 澶囦欢棰嗙敤璁板綍 - 鍒嗛〉鏌ヨ
+ * params: { current, size, sparePartId?, sparePartName?, source?, deviceId?, startTime?, endTime? }
+ */
+export const getSparePartsUsagePage = (params) => {
+ return request({
+ url: "/sparePartsRequisitionRecord/listPage",
+ method: "get",
+ params,
+ });
+};
+
+/**
+ * 澶囦欢棰嗙敤璁板綍 - 鏂板
+ * data 绀轰緥锛�
+ * {
+ * source: "repair" | "upkeep" | "manual",
+ * sourceId?: number | string,
+ * deviceId?: number | string,
+ * deviceName?: string,
+ * operatorId?: number | string,
+ * operator?: string,
+ * useTime?: string, // YYYY-MM-DD HH:mm:ss
+ * items: [{ sparePartId: number|string, qty: number }]
+ * }
+ */
+export const addSparePartsUsage = (data) => {
+ return request({
+ url: "/sparePartsUsage/add",
+ method: "post",
+ data,
+ });
+};
+
diff --git a/src/api/equipmentManagement/upkeep.js b/src/api/equipmentManagement/upkeep.js
new file mode 100644
index 0000000..234d2a5
--- /dev/null
+++ b/src/api/equipmentManagement/upkeep.js
@@ -0,0 +1,104 @@
+import request from "@/utils/request";
+
+/**
+ * @desc 璁惧淇濆吇鍒楄〃鍒嗛〉鏌ヨ
+ * @param {鍒嗛〉鏌ヨ鍏ュ弬} params
+ * @returns
+ */
+export const getUpkeepPage = (params) => {
+ return request({
+ url: "/device/maintenance/page",
+ method: "get",
+ params,
+ });
+};
+
+/**
+ * @desc 璁惧淇濆吇璇︽儏
+ * @param {淇濆吇浣嗙紪鍙穧 id
+ * @returns
+ */
+export const getUpkeepById = (id) => {
+ return request({
+ url: `/device/maintenance/${id}`,
+ method: "get",
+ });
+};
+
+/**
+ * @desc 璁惧淇濆吇鏂板
+ * @param {鏂板淇濆吇琛ㄥ崟} data
+ * @returns
+ */
+export const addUpkeep = (data) => {
+ return request({
+ url: "/device/maintenance",
+ method: "post",
+ data,
+ });
+};
+
+/**
+ * @desc 璁惧淇濆吇缂栬緫
+ * @param {缂栬緫淇濆吇琛ㄥ崟} data
+ * @returns
+ */
+export const editUpkeep = (data) => {
+ return request({
+ url: "/device/maintenance",
+ method: "put",
+ data,
+ });
+};
+
+/**
+ * @desc 鏂板淇濆吇琛ㄥ崟
+ * @param {鏂板淇濆吇琛ㄥ崟} data
+ * @returns
+ */
+export const addMaintenance = (data) => {
+ return request({
+ url: "/device/maintenance/maintenance",
+ method: "post",
+ data,
+ });
+};
+
+export const delUpkeep = (id) => {
+ return request({
+ url: `/device/maintenance/${id}`,
+ method: "delete",
+ });
+};
+// 娣诲姞璁惧淇濆吇瀹氭椂浠诲姟
+export const deviceMaintenanceTaskAdd = (params) => {
+ return request({
+ url: '/deviceMaintenanceTask/add',
+ method: "post",
+ data: params,
+ });
+};
+// 淇敼璁惧淇濆吇瀹氭椂浠诲姟
+export const deviceMaintenanceTaskEdit = (params) => {
+ return request({
+ url: '/deviceMaintenanceTask/update',
+ method: "post",
+ data: params,
+ });
+};
+// 璁惧淇濆吇瀹氭椂浠诲姟鍒楄〃
+export const deviceMaintenanceTaskList = (params) => {
+ return request({
+ url: '/deviceMaintenanceTask/listPage',
+ method: "get",
+ params: params,
+ });
+};
+// 璁惧淇濆吇瀹氭椂浠诲姟鍒楄〃
+export const deviceMaintenanceTaskDel = (params) => {
+ return request({
+ url: '/deviceMaintenanceTask/delete',
+ method: "delete",
+ data: params,
+ });
+};
diff --git a/src/api/fileManagement/bookshelf.js b/src/api/fileManagement/bookshelf.js
new file mode 100644
index 0000000..eb3a88f
--- /dev/null
+++ b/src/api/fileManagement/bookshelf.js
@@ -0,0 +1,129 @@
+import request from "@/utils/request";
+
+/**
+ * 涔︽灦绠$悊鐩稿叧API鎺ュ彛
+ * 鍖呭惈浠撳簱绠$悊銆佽揣鏋剁鐞嗐�佸浘涔︾鐞嗙瓑鍔熻兘鐨勬帴鍙�
+ */
+
+/**
+ * 鑾峰彇浠撳簱鍒楄〃
+ * @description 鑾峰彇鎵�鏈変粨搴撶殑鍩烘湰淇℃伅鍒楄〃
+ * @returns {Promise} 杩斿洖浠撳簱鍒楄〃鏁版嵁
+ */
+export function getWarehouseList() {
+ return request({
+ url: "/warehouse/tree",
+ method: "get",
+ });
+}
+
+/**
+ * 鏂板浠撳簱
+ * @description 鍒涘缓鏂扮殑浠撳簱璁板綍
+ * @param {Object} data 浠撳簱淇℃伅瀵硅薄锛屽寘鍚粨搴撳悕绉扮瓑瀛楁
+ * @returns {Promise} 杩斿洖鏂板缁撴灉
+ */
+export function addWarehouse(data) {
+ return request({
+ url: "/warehouse/add",
+ method: "post",
+ data,
+ });
+}
+
+/**
+ * 鏇存柊浠撳簱淇℃伅
+ * @description 淇敼鐜版湁浠撳簱鐨勫熀鏈俊鎭�
+ * @param {Object} data 浠撳簱淇℃伅瀵硅薄锛屽繀椤诲寘鍚粨搴揑D
+ * @returns {Promise} 杩斿洖鏇存柊缁撴灉
+ */
+export function updateWarehouse(data) {
+ return request({
+ url: "/warehouse/update",
+ method: "put",
+ data,
+ });
+}
+
+/**
+ * 鍒犻櫎浠撳簱
+ * @description 鏍规嵁浠撳簱ID鍒犻櫎鎸囧畾鐨勪粨搴撹褰�
+ * @param {string|number} id 浠撳簱ID
+ * @returns {Promise} 杩斿洖鍒犻櫎缁撴灉
+ */
+export function deleteWarehouse(data) {
+ return request({
+ url: `/warehouse/delete/`,
+ method: "delete",
+ data,
+ });
+}
+
+/**
+ * 鑾峰彇璐ф灦鍒楄〃
+ * @description 鏍规嵁浠撳簱ID鑾峰彇璇ヤ粨搴撲笅鐨勬墍鏈夎揣鏋朵俊鎭�
+ * @param {string|number} warehouseId 浠撳簱ID
+ * @returns {Promise} 杩斿洖璐ф灦鍒楄〃鏁版嵁
+ */
+export function getShelfList(warehouseId) {
+ return request({
+ url: `/shelf/list/${warehouseId}`,
+ method: "get",
+ });
+}
+
+/**
+ * 鏂板璐ф灦
+ * @description 鍦ㄦ寚瀹氫粨搴撲笅鍒涘缓鏂扮殑璐ф灦璁板綍
+ * @param {Object} data 璐ф灦淇℃伅瀵硅薄锛屽寘鍚揣鏋跺悕绉般�佸眰鏁般�佸垪鏁扮瓑瀛楁
+ * @returns {Promise} 杩斿洖鏂板缁撴灉
+ */
+export function addShelf(data) {
+ return request({
+ url: "/warehouse/goodsShelves/add",
+ method: "post",
+ data,
+ });
+}
+
+/**
+ * 鏇存柊璐ф灦淇℃伅
+ * @description 淇敼鐜版湁璐ф灦鐨勫熀鏈俊鎭�
+ * @param {Object} data 璐ф灦淇℃伅瀵硅薄锛屽繀椤诲寘鍚揣鏋禝D
+ * @returns {Promise} 杩斿洖鏇存柊缁撴灉
+ */
+export function updateShelf(data) {
+ return request({
+ url: "/warehouse/goodsShelves/update",
+ method: "put",
+ data,
+ });
+}
+
+/**
+ * 鍒犻櫎璐ф灦
+ * @description 鏍规嵁璐ф灦ID鍒犻櫎鎸囧畾鐨勮揣鏋惰褰曪紝鍚庣瑕佹眰浼犲叆 ID 鏁扮粍锛堟敮鎸佹壒閲忥級
+ * @param {Array<string|number>} data 璐ф灦ID鏁扮粍
+ * @returns {Promise} 杩斿洖鍒犻櫎缁撴灉
+ */
+export function deleteShelf(data) {
+ return request({
+ url: `/warehouse/goodsShelves/delete/`,
+ method: "delete",
+ data,
+ });
+}
+
+/**
+ * 鑾峰彇浠撳簱缁撴瀯
+ * @description 鑾峰彇鎸囧畾浠撳簱鐨勫畬鏁寸粨鏋勪俊鎭紝鍖呮嫭璐ф灦銆佸眰鏁般�佸垪鏁扮瓑
+ * @param {string|number} warehouseId 浠撳簱ID
+ * @returns {Promise} 杩斿洖浠撳簱鐨勫畬鏁寸粨鏋勬暟鎹�
+ */
+export function getWarehouseStructure(data) {
+ return request({
+ url: `/warehouse/goodsShelvesRowcol/list`,
+ method: "get",
+ params: data,
+ });
+}
diff --git a/src/api/fileManagement/borrow.js b/src/api/fileManagement/borrow.js
new file mode 100644
index 0000000..1f4c72c
--- /dev/null
+++ b/src/api/fileManagement/borrow.js
@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+
+// 鏂囨。鍊熼槄绠$悊鐩稿叧鎺ュ彛
+
+// 鑾峰彇鏂囨。鍒楄〃锛堢敤浜庡�熼槄涔︾睄閫夋嫨锛�
+export function getDocumentList() {
+ return request({
+ url: "/documentation/list",
+ method: "get",
+ });
+}
+
+// 鍊熼槄鍒嗛〉鏌ヨ
+export function getBorrowList(params) {
+ return request({
+ url: "/documentationBorrowManagement/listPage",
+ method: "get",
+ params: params,
+ });
+}
+
+// 鏂板鍊熼槄
+export function addBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鏇存柊鍊熼槄
+export function updateBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鍊熼槄
+export function deleteBorrow(ids) {
+ return request({
+ url: "/documentationBorrowManagement/delete",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/api/fileManagement/document.js b/src/api/fileManagement/document.js
new file mode 100644
index 0000000..f3d5f4f
--- /dev/null
+++ b/src/api/fileManagement/document.js
@@ -0,0 +1,189 @@
+import request from "@/utils/request";
+
+// 鑾峰彇鍒嗙被鏍�
+export function getCategoryTree() {
+ return request({
+ url: "/warehouse/documentClassification/getList",
+ method: "get",
+ });
+}
+
+// 鏂板鍒嗙被
+export function addCategory(data) {
+ return request({
+ url: "/warehouse/documentClassification/add",
+ method: "post",
+ data: {
+ category: data.category,
+ parentId: data.parentId,
+ },
+ });
+}
+
+// 淇敼鍒嗙被
+export function updateCategory(data) {
+ return request({
+ url: "/warehouse/documentClassification/update",
+ method: "put",
+ data: {
+ id: data.id,
+ category: data.category,
+ },
+ });
+}
+
+// 鍒犻櫎鍒嗙被
+export function deleteCategory(ids) {
+ return request({
+ url: "/warehouse/documentClassification/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鑾峰彇鏂囨。鍒楄〃锛堝垎椤碉級
+export function getDocumentList(query) {
+ return request({
+ url: "/documentation/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鏂囨。
+export function addDocument(data) {
+ return request({
+ url: "/documentation/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鏂囨。
+export function updateDocument(data) {
+ return request({
+ url: "/documentation/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鏂囨。
+export function deleteDocument(ids) {
+ return request({
+ url: "/documentation/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鑾峰彇鏂囨。璇︽儏
+export function getDocumentDetail(id) {
+ return request({
+ url: "/document/" + id,
+ method: "get",
+ });
+}
+
+// 鎼滅储鏂囨。
+export function searchDocument(query) {
+ return request({
+ url: "/document/search",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鑾峰彇浠撳簱缁撴瀯
+export function getWarehouseStructure() {
+ return request({
+ url: "/document/warehouse/structure",
+ method: "get",
+ });
+}
+
+// 闄勪欢绠$悊鐩稿叧鎺ュ彛
+// 娣诲姞闄勪欢
+export function addDocumentationFile(data) {
+ return request({
+ url: "/documentation/documentationFile/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鑾峰彇闄勪欢鍒楄〃
+export function getDocumentationFileList(params) {
+ return request({
+ url: "/documentation/documentationFile/listPage",
+ method: "get",
+ params: params,
+ });
+}
+
+// 鍒犻櫎闄勪欢
+export function deleteDocumentationFile(ids) {
+ return request({
+ url: "/documentation/documentationFile/del",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鏂囨。鍊熼槄绠$悊鐩稿叧鎺ュ彛
+export function getBorrowList(params) {
+ return request({
+ url: "/documentationBorrowManagement/listPage",
+ method: "get",
+ params: params,
+ });
+}
+
+export function addBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+
+export function updateBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/update",
+ method: "put",
+ data: data,
+ });
+}
+
+export function deleteBorrow(ids) {
+ return request({
+ url: "/documentationBorrowManagement/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 缁熻鐩稿叧鎺ュ彛
+// 鑾峰彇鎬讳綋缁熻鏁版嵁
+export function getDocumentationOverview() {
+ return request({
+ url: "/documentation/overview",
+ method: "get",
+ });
+}
+
+// 鑾峰彇鍒嗙被缁熻鏁版嵁
+export function getDocumentationCategoryStats() {
+ return request({
+ url: "/documentation/category",
+ method: "get",
+ });
+}
+
+// 鑾峰彇鐘舵�佺粺璁℃暟鎹�
+export function getDocumentationStatusStats() {
+ return request({
+ url: "/documentation/status",
+ method: "get",
+ });
+}
diff --git a/src/api/fileManagement/return.js b/src/api/fileManagement/return.js
new file mode 100644
index 0000000..9021ac9
--- /dev/null
+++ b/src/api/fileManagement/return.js
@@ -0,0 +1,61 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ褰掕繕璁板綍
+export function getReturnListPage(query) {
+ return request({
+ url: "/documentationBorrowManagement/listPageReturn",
+ method: "get",
+ params: query,
+ });
+}
+
+// 褰掕繕鎿嶄綔
+export function returnDocument(data) {
+ return request({
+ url: "/documentationBorrowManagement/revent",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎褰掕繕璁板綍
+export function deleteReturn(ids) {
+ return request({
+ url: "/documentationBorrowManagement/reventDelete",
+ method: "delete",
+ data: ids,
+ });
+}
+//鏍规嵁涔︾睄id鏌ヨ鍊熼槄璁板綍
+export function getBorrowListByDocumentationId(id) {
+ return request({
+ url: "/documentationBorrowManagement/getByDocumentationId/"+id,
+ method: "get"
+ });
+}
+
+// 鏇存柊鍊熼槄璁板綍
+export function updateBorrow(data) {
+ return request({
+ url: "/documentationBorrowManagement/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 褰掕繕鏇存柊
+export function reventUpdate(data) {
+ return request({
+ url: "/documentationBorrowManagement/reventUpdate",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鑾峰彇鏂囨。鍒楄〃
+export function getDocumentList() {
+ return request({
+ url: "/documentationBorrowManagement/list",
+ method: "get",
+ });
+}
diff --git a/src/api/fileManagement/statistics.js b/src/api/fileManagement/statistics.js
new file mode 100644
index 0000000..d77375c
--- /dev/null
+++ b/src/api/fileManagement/statistics.js
@@ -0,0 +1,75 @@
+import request from "@/utils/request";
+
+// 鑾峰彇妗f鎬讳綋缁熻
+export function getDocumentStatistics() {
+ return request({
+ url: "/fileManagement/statistics/overview",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鍒嗙被缁熻
+export function getCategoryStatistics() {
+ return request({
+ url: "/fileManagement/statistics/category",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鐘舵�佺粺璁�
+export function getStatusStatistics() {
+ return request({
+ url: "/fileManagement/statistics/status",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鍊熼槄缁熻
+export function getBorrowStatistics() {
+ return request({
+ url: "/fileManagement/statistics/borrow",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f骞村害缁熻
+export function getYearStatistics() {
+ return request({
+ url: "/fileManagement/statistics/year",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f浣嶇疆缁熻
+export function getLocationStatistics() {
+ return request({
+ url: "/fileManagement/statistics/location",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f瓒嬪娍缁熻
+export function getTrendStatistics(params) {
+ return request({
+ url: "/fileManagement/statistics/trend",
+ method: "get",
+ params: params,
+ });
+}
+
+// 鑾峰彇妗f鍊熼槄鎺掕
+export function getBorrowRanking() {
+ return request({
+ url: "/fileManagement/statistics/borrowRanking",
+ method: "get",
+ });
+}
+
+// 鑾峰彇妗f鍒嗙被璇︽儏缁熻
+export function getCategoryDetailStatistics(categoryId) {
+ return request({
+ url: `/fileManagement/statistics/categoryDetail/${categoryId}`,
+ method: "get",
+ });
+}
+
diff --git a/src/api/financialManagement/accountPaymentApplication.js b/src/api/financialManagement/accountPaymentApplication.js
new file mode 100644
index 0000000..0d5e438
--- /dev/null
+++ b/src/api/financialManagement/accountPaymentApplication.js
@@ -0,0 +1,59 @@
+import request from "@/utils/request";
+
+/** 鏍规嵁渚涘簲鍟嗘煡璇㈠彲鍏宠仈鍏ュ簱鍗� */
+export function getInboundBatchesBySupplier(params) {
+ return request({
+ url: "/accountPaymentApplication/getInboundBatchesBySupplier",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板浠樻鐢宠 */
+export function addAccountPaymentApplication(data) {
+ return request({
+ url: "/accountPaymentApplication/addAccountPaymentApplication",
+ method: "post",
+ data,
+ });
+}
+
+/** 浠樻鐢宠鍒嗛〉鍒楄〃 */
+export function listPageAccountPaymentApplication(params) {
+ return request({
+ url: "/accountPaymentApplication/listPageAccountPaymentApplication",
+ method: "get",
+ params,
+ });
+}
+
+/** 淇敼浠樻鐢宠 */
+export function updateAccountPaymentApplication(data) {
+ return request({
+ url: "/accountPaymentApplication/updateAccountPaymentApplication",
+ method: "put",
+ data,
+ });
+}
+
+/** 瀹℃牳浠樻鐢宠 */
+export function auditAccountPaymentApplication(data) {
+ return request({
+ url: "/accountPaymentApplication/auditAccountPaymentApplication",
+ method: "put",
+ data,
+ });
+}
+
+/** 鍒犻櫎浠樻鐢宠锛圫pring 瑕佹眰 ids=1&ids=2 鏌ヨ鍙傛暟锛� */
+export function deleteAccountPaymentApplication(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter((id) => id !== undefined && id !== null && id !== "")
+ .map((id) => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/accountPaymentApplication/deleteAccountPaymentApplication?${query}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/financialManagement/accountPurchase.js b/src/api/financialManagement/accountPurchase.js
new file mode 100644
index 0000000..9e2d508
--- /dev/null
+++ b/src/api/financialManagement/accountPurchase.js
@@ -0,0 +1,19 @@
+import request from "@/utils/request";
+
+/** 閲囪喘鍏ュ簱鍒嗛〉鍒楄〃 */
+export const listPageAccountPurchase = (params) => {
+ return request({
+ url: "/accountPurchase/listPageAccountPurchase",
+ method: "get",
+ params,
+ });
+};
+
+/** 閲囪喘閫�璐у垎椤靛垪琛� */
+export const listPageAccountPurchaseReturn = (params) => {
+ return request({
+ url: "/accountPurchase/listPageAccountPurchaseReturn",
+ method: "get",
+ params,
+ });
+};
diff --git a/src/api/financialManagement/accountPurchaseInvoice.js b/src/api/financialManagement/accountPurchaseInvoice.js
new file mode 100644
index 0000000..af391da
--- /dev/null
+++ b/src/api/financialManagement/accountPurchaseInvoice.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+/** 鏍规嵁渚涘簲鍟嗘煡璇㈠彲鍏宠仈鍏ュ簱鍗� */
+export function getInboundBatchesBySupplier(params) {
+ return request({
+ url: "/accountPurchaseInvoice/getInboundBatchesBySupplier",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板杩涢」鍙戠エ */
+export function addAccountPurchaseInvoice(data) {
+ return request({
+ url: "/accountPurchaseInvoice/addAccountPurchaseInvoice",
+ method: "post",
+ data,
+ });
+}
+
+/** 杩涢」鍙戠エ鍒嗛〉鍒楄〃 */
+export function listPageAccountPurchaseInvoice(params) {
+ return request({
+ url: "/accountPurchaseInvoice/listPageAccountPurchaseInvoice",
+ method: "get",
+ params,
+ });
+}
+
+/** 浣滃簾杩涢」鍙戠エ */
+export function cancelAccountPurchaseInvoice(data) {
+ return request({
+ url: "/accountPurchaseInvoice/cancelAccountPurchaseInvoice",
+ method: "put",
+ data,
+ });
+}
+
+/** 鍒犻櫎杩涢」鍙戠エ锛圫pring 瑕佹眰 ids=1&ids=2 鏌ヨ鍙傛暟锛� */
+export function deleteAccountPurchaseInvoice(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter((id) => id !== undefined && id !== null && id !== "")
+ .map((id) => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/accountPurchaseInvoice/deleteAccountPurchaseInvoice?${query}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/financialManagement/accountPurchasePayment.js b/src/api/financialManagement/accountPurchasePayment.js
new file mode 100644
index 0000000..a10f05a
--- /dev/null
+++ b/src/api/financialManagement/accountPurchasePayment.js
@@ -0,0 +1,32 @@
+import request from "@/utils/request";
+
+/** 鏂板浠樻鍗曪紙鍏宠仈浠樻鐢宠锛� */
+export function addAccountPurchasePayment(data) {
+ return request({
+ url: "/accountPurchasePayment/addAccountPurchasePayment",
+ method: "post",
+ data,
+ });
+}
+
+/** 浠樻鍗曞垎椤靛垪琛� */
+export function listPageAccountPurchasePayment(params) {
+ return request({
+ url: "/accountPurchasePayment/listPageAccountPurchasePayment",
+ method: "get",
+ params,
+ });
+}
+
+/** 鍒犻櫎浠樻鍗曪紙Spring 瑕佹眰 ids=1&ids=2 鏌ヨ鍙傛暟锛� */
+export function deleteAccountPurchasePayment(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter((id) => id !== undefined && id !== null && id !== "")
+ .map((id) => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/accountPurchasePayment/deleteAccountPurchasePayment?${query}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/financialManagement/accountSales.js b/src/api/financialManagement/accountSales.js
new file mode 100644
index 0000000..e56d50f
--- /dev/null
+++ b/src/api/financialManagement/accountSales.js
@@ -0,0 +1,19 @@
+import request from "@/utils/request";
+
+/** 閿�鍞嚭搴撳垎椤靛垪琛� */
+export const listPageAccountSales = (params) => {
+ return request({
+ url: "/accountSales/listPageAccountSales",
+ method: "get",
+ params,
+ });
+};
+
+/** 閿�鍞��璐у垎椤靛垪琛� */
+export const listPageAccountSalesReturn = (params) => {
+ return request({
+ url: "/accountSales/listPageAccountSalesReturn",
+ method: "get",
+ params,
+ });
+};
diff --git a/src/api/financialManagement/accountSalesCollection.js b/src/api/financialManagement/accountSalesCollection.js
new file mode 100644
index 0000000..abeb977
--- /dev/null
+++ b/src/api/financialManagement/accountSalesCollection.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+/** 鏍规嵁瀹㈡埛鏌ヨ鍙叧鑱斿嚭搴撳崟 */
+export function getOutboundBatchesByCustomer(params) {
+ return request({
+ url: "/accountSalesCollection/getOutboundBatchesByCustomer",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板鏀舵鍗� */
+export function addAccountSalesCollection(data) {
+ return request({
+ url: "/accountSalesCollection/addAccountSalesCollection",
+ method: "post",
+ data,
+ });
+}
+
+/** 鏀舵鍗曞垎椤靛垪琛� */
+export function listPageAccountSalesCollection(params) {
+ return request({
+ url: "/accountSalesCollection/listPageAccountSalesCollection",
+ method: "get",
+ params,
+ });
+}
+
+/** 淇敼鏀舵鍗� */
+export function updateAccountSalesCollection(data) {
+ return request({
+ url: "/accountSalesCollection/updateAccountSalesCollection",
+ method: "put",
+ data,
+ });
+}
+
+/** 鍒犻櫎鏀舵鍗曪紙Spring 瑕佹眰 ids=1&ids=2 鏌ヨ鍙傛暟锛� */
+export function deleteAccountSalesCollection(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter((id) => id !== undefined && id !== null && id !== "")
+ .map((id) => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/accountSalesCollection/deleteAccountSalesCollection?${query}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/financialManagement/accountSalesInvoice.js b/src/api/financialManagement/accountSalesInvoice.js
new file mode 100644
index 0000000..6e74c53
--- /dev/null
+++ b/src/api/financialManagement/accountSalesInvoice.js
@@ -0,0 +1,41 @@
+import request from "@/utils/request";
+
+/** 鏂板閿�椤瑰彂绁� */
+export function addAccountSalesInvoice(data) {
+ return request({
+ url: "/accountSalesInvoice/addAccountSalesInvoice",
+ method: "post",
+ data,
+ });
+}
+
+/** 閿�椤瑰彂绁ㄥ垎椤靛垪琛� */
+export function listPageAccountSalesInvoice(params) {
+ return request({
+ url: "/accountSalesInvoice/listPageAccountSalesInvoice",
+ method: "get",
+ params,
+ });
+}
+
+/** 浣滃簾閿�椤瑰彂绁� */
+export function cancelAccountSalesInvoice(data) {
+ return request({
+ url: "/accountSalesInvoice/cancelAccountSalesInvoice",
+ method: "put",
+ data,
+ });
+}
+
+/** 鍒犻櫎閿�椤瑰彂绁紙Spring 瑕佹眰 ids=1&ids=2 鏌ヨ鍙傛暟锛� */
+export function deleteAccountSalesInvoice(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter((id) => id !== undefined && id !== null && id !== "")
+ .map((id) => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/accountSalesInvoice/deleteAccountSalesInvoice?${query}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/financialManagement/accountStatement.js b/src/api/financialManagement/accountStatement.js
new file mode 100644
index 0000000..bf80101
--- /dev/null
+++ b/src/api/financialManagement/accountStatement.js
@@ -0,0 +1,41 @@
+import request from "@/utils/request";
+
+/** 鎸夋湀浠芥煡璇㈠璐﹀崟鏄庣粏锛堢敓鎴愬墠棰勮锛� */
+export function getAccountStatementDetailsByMonth(params) {
+ return request({
+ url: "/accountStatement/getAccountStatementDetailsByMonth",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板瀵硅处鍗� */
+export function addAccountStatement(data) {
+ return request({
+ url: "/accountStatement/addAccountStatement",
+ method: "post",
+ data,
+ });
+}
+
+/** 瀵硅处鍗曞垎椤靛垪琛� */
+export function listPageAccountStatement(params) {
+ return request({
+ url: "/accountStatement/listPageAccountStatement",
+ method: "get",
+ params,
+ });
+}
+
+/** 鍒犻櫎瀵硅处鍗曪紙Spring 瑕佹眰 ids=1&ids=2 鏌ヨ鍙傛暟锛� */
+export function deleteAccountStatement(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter((id) => id !== undefined && id !== null && id !== "")
+ .map((id) => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/accountStatement/deleteAccountStatement?${query}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/financialManagement/accountSubject.js b/src/api/financialManagement/accountSubject.js
new file mode 100644
index 0000000..e54de63
--- /dev/null
+++ b/src/api/financialManagement/accountSubject.js
@@ -0,0 +1,46 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鎬诲笎绉戠洰鍒楄〃
+export function listAccountSubject(query) {
+ return request({
+ url: "/accountSubject/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鎬诲笎绉戠洰
+export function addAccountSubject(data) {
+ return request({
+ url: "/accountSubject/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鎬诲笎绉戠洰
+export function updateAccountSubject(data) {
+ return request({
+ url: "/accountSubject/edit",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鎬诲笎绉戠洰
+export function delAccountSubject(ids) {
+ return request({
+ url: "/accountSubject/remove/" + ids,
+ method: "delete",
+ });
+}
+
+// 瀵煎嚭鎬诲笎绉戠洰
+export function exportAccountSubject(data) {
+ return request({
+ url: "/accountSubject/export",
+ method: "post",
+ data: data,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/financialManagement/accounting.js b/src/api/financialManagement/accounting.js
new file mode 100644
index 0000000..69bc7cd
--- /dev/null
+++ b/src/api/financialManagement/accounting.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 鑾峰彇鍥哄畾璧勪骇姹囨�讳俊鎭�
+export const getAccountingTotal = (params) => {
+ return request({
+ url: "/accounting/total",
+ method: "get",
+ params,
+ });
+};
+
+// 鑾峰彇璁惧绫诲瀷鍒嗗竷鏁版嵁锛堥ゼ鍥惧拰鎶樼嚎鍥撅級
+export const getDeviceTypeDistribution = (params) => {
+ return request({
+ url: "/accounting/deviceTypeDistribution",
+ method: "get",
+ params,
+ });
+};
+
+// 鑾峰彇鎶樻棫璁$畻鏁版嵁锛堣〃鏍兼暟鎹級
+export const getCalculateDepreciation = (params) => {
+ return request({
+ url: "/accounting/calculateDepreciation",
+ method: "get",
+ params,
+ });
+};
diff --git a/src/api/financialManagement/financialStatements.js b/src/api/financialManagement/financialStatements.js
new file mode 100644
index 0000000..6c3b306
--- /dev/null
+++ b/src/api/financialManagement/financialStatements.js
@@ -0,0 +1,13 @@
+import request from "@/utils/request";
+
+/**
+ * 鑾峰彇璐㈠姟鎶ヨ〃鏈堝害鏄庣粏
+ * @param {Object} params { entryDateStart, entryDateEnd }
+ */
+export function accountStatementDetailsByMonth(params) {
+ return request({
+ url: "/accounting/accountStatementDetailsByMonth",
+ method: "get",
+ params,
+ });
+}
diff --git a/src/api/financialManagement/fixedAsset.js b/src/api/financialManagement/fixedAsset.js
new file mode 100644
index 0000000..5c28db4
--- /dev/null
+++ b/src/api/financialManagement/fixedAsset.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+// 鍥哄畾璧勪骇鍒嗛〉鏌ヨ锛坈urrent/size锛�
+export function listFixedAssetPage(params) {
+ return request({
+ url: "/financial/fixedAsset/page",
+ method: "get",
+ params,
+ });
+}
+
+// 鏂板鍥哄畾璧勪骇
+export function addFixedAsset(data) {
+ return request({
+ url: "/financial/fixedAsset/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼鍥哄畾璧勪骇
+export function updateFixedAsset(data) {
+ return request({
+ url: "/financial/fixedAsset/update",
+ method: "put",
+ data,
+ });
+}
+
+// 鍒犻櫎鍥哄畾璧勪骇锛堝悗绔姹� ids=1&ids=2 褰㈠紡锛�
+export function deleteFixedAsset(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter(id => id !== undefined && id !== null && id !== "")
+ .map(id => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/financial/fixedAsset/delete?${query}`,
+ method: "delete",
+ });
+}
+
+// 鎶樻棫璁℃彁锛坽} 琛ㄧず鍏ㄩ儴鍦ㄧ敤璧勪骇锛�
+export function depreciateFixedAsset(data = {}) {
+ return request({
+ url: "/financial/fixedAsset/depreciate",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/financialManagement/intangibleAsset.js b/src/api/financialManagement/intangibleAsset.js
new file mode 100644
index 0000000..802e649
--- /dev/null
+++ b/src/api/financialManagement/intangibleAsset.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+// 鏃犲舰璧勪骇鍒嗛〉鏌ヨ锛坈urrent/size锛�
+export function listIntangibleAssetPage(params) {
+ return request({
+ url: "/financial/intangibleAsset/page",
+ method: "get",
+ params,
+ });
+}
+
+// 鏂板鏃犲舰璧勪骇
+export function addIntangibleAsset(data) {
+ return request({
+ url: "/financial/intangibleAsset/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼鏃犲舰璧勪骇
+export function updateIntangibleAsset(data) {
+ return request({
+ url: "/financial/intangibleAsset/update",
+ method: "put",
+ data,
+ });
+}
+
+// 鍒犻櫎鏃犲舰璧勪骇锛堝悗绔姹� ids=1&ids=2 褰㈠紡锛�
+export function deleteIntangibleAsset(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter(id => id !== undefined && id !== null && id !== "")
+ .map(id => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/financial/intangibleAsset/delete?${query}`,
+ method: "delete",
+ });
+}
+
+// 鎽婇攢璁℃彁锛坽} 琛ㄧず鍏ㄩ儴鍦ㄧ敤璧勪骇锛�
+export function amortizeIntangibleAsset(data = {}) {
+ return request({
+ url: "/financial/intangibleAsset/amortize",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/financialManagement/invoiceApply.js b/src/api/financialManagement/invoiceApply.js
new file mode 100644
index 0000000..a567d1c
--- /dev/null
+++ b/src/api/financialManagement/invoiceApply.js
@@ -0,0 +1,59 @@
+import request from "@/utils/request";
+
+/** 鏍规嵁瀹㈡埛鏌ヨ鍙紑绁ㄥ嚭搴撳崟鍙峰垪琛� */
+export function getOutboundBatchesByCustomer(params) {
+ return request({
+ url: "/accountInvoiceApplication/getOutboundBatchesByCustomer",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板寮�绁ㄧ敵璇� */
+export function addAccountInvoiceApplication(data) {
+ return request({
+ url: "/accountInvoiceApplication/addAccountInvoiceApplication",
+ method: "post",
+ data,
+ });
+}
+
+/** 寮�绁ㄧ敵璇峰垎椤靛垪琛� */
+export function listPageAccountInvoiceApplication(params) {
+ return request({
+ url: "/accountInvoiceApplication/listPageAccountInvoiceApplication",
+ method: "get",
+ params,
+ });
+}
+
+/** 寮�绁ㄧ敵璇峰鎵� */
+export function auditAccountInvoiceApplication(data) {
+ return request({
+ url: "/accountInvoiceApplication/auditAccountInvoiceApplication",
+ method: "put",
+ data,
+ });
+}
+
+/** 淇敼寮�绁ㄧ敵璇� */
+export function updateAccountInvoiceApplication(data) {
+ return request({
+ url: "/accountInvoiceApplication/updateAccountInvoiceApplication",
+ method: "put",
+ data,
+ });
+}
+
+/** 鍒犻櫎寮�绁ㄧ敵璇凤紙Spring 瑕佹眰 ids=1&ids=2 鏌ヨ鍙傛暟锛� */
+export function deleteAccountInvoiceApplication(ids) {
+ const idList = Array.isArray(ids) ? ids : [ids];
+ const query = idList
+ .filter((id) => id !== undefined && id !== null && id !== "")
+ .map((id) => `ids=${encodeURIComponent(id)}`)
+ .join("&");
+ return request({
+ url: `/accountInvoiceApplication/deleteAccountInvoiceApplication?${query}`,
+ method: "delete",
+ });
+}
\ No newline at end of file
diff --git a/src/api/financialManagement/ledger.js b/src/api/financialManagement/ledger.js
new file mode 100644
index 0000000..17e62fc
--- /dev/null
+++ b/src/api/financialManagement/ledger.js
@@ -0,0 +1,19 @@
+import request from "@/utils/request";
+
+// 绉戠洰鎬昏处
+export function getGeneralLedger(params) {
+ return request({
+ url: "/financial/ledger/general",
+ method: "get",
+ params,
+ });
+}
+
+// 绉戠洰鏄庣粏璐�
+export function getDetailLedger(params) {
+ return request({
+ url: "/financial/ledger/detail",
+ method: "get",
+ params,
+ });
+}
diff --git a/src/api/financialManagement/revenueManagement.js b/src/api/financialManagement/revenueManagement.js
new file mode 100644
index 0000000..090ddf8
--- /dev/null
+++ b/src/api/financialManagement/revenueManagement.js
@@ -0,0 +1,78 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍒楄〃
+export const listPage = (params) => {
+ return request({
+ url: "/account/accountIncome/listPage",
+ method: "get",
+ params,
+ });
+};
+
+// 鏂板
+export function add(data) {
+ return request({
+ url: "/account/accountIncome/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缂栬緫
+export function update(data) {
+ return request({
+ url: "/account/accountIncome/update",
+ method: "post",
+ data: data,
+ });
+}
+
+//瀵煎嚭
+export const exportAccountIncome = (query) => {
+ return request({
+ url: "/account/accountIncome/export",
+ method: "post",
+ data: query,
+ responseType: "blob",
+ });
+};
+
+export const delAccountIncome = (query) => {
+ return request({
+ url: `account/accountIncome/del`,
+ method: "delete",
+ data: query,
+ });
+};
+
+export const getAccountIncome = (id) => {
+ return request({
+ url: `/account/accountIncome/${id}`,
+ method: "get",
+ });
+};
+
+// 鏌ヨ闄勪欢鍒楄〃
+export function fileListPage(query) {
+ return request({
+ url: "/account/accountFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 淇濆瓨闄勪欢鍒楄〃
+export function fileAdd(query) {
+ return request({
+ url: "/account/accountFile/add",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎闄勪欢鍒楄〃
+export function fileDel(query) {
+ return request({
+ url: "/account/accountFile/del",
+ method: "delete",
+ data: query,
+ });
+}
diff --git a/src/api/financialManagement/voucher.js b/src/api/financialManagement/voucher.js
new file mode 100644
index 0000000..ccb0908
--- /dev/null
+++ b/src/api/financialManagement/voucher.js
@@ -0,0 +1,54 @@
+import request from "@/utils/request";
+
+// 鍑瘉鍒嗛〉鏌ヨ锛坈urrent/size + 杩囨护鏉′欢锛�
+export function listVoucherPage(params) {
+ return request({
+ url: "/financial/voucher/page",
+ method: "get",
+ params,
+ });
+}
+
+// 鏂板鍑瘉
+export function addVoucher(data) {
+ return request({
+ url: "/financial/voucher/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼鍑瘉锛堜粎鏈繃璐︼級
+export function updateVoucher(data) {
+ return request({
+ url: "/financial/voucher/update",
+ method: "put",
+ data,
+ });
+}
+
+// 杩囪处
+export function postVoucher(data) {
+ return request({
+ url: "/financial/voucher/post",
+ method: "post",
+ data,
+ });
+}
+
+// 浣滃簾
+export function cancelVoucher(data) {
+ return request({
+ url: "/financial/voucher/cancel",
+ method: "post",
+ data,
+ });
+}
+
+// 璇︽儏
+export function getVoucherDetail(id) {
+ return request({
+ url: `/financial/voucher/detail/${id}`,
+ method: "get",
+ });
+}
diff --git a/src/api/inspectionManagement/index.js b/src/api/inspectionManagement/index.js
new file mode 100644
index 0000000..d0c444a
--- /dev/null
+++ b/src/api/inspectionManagement/index.js
@@ -0,0 +1,61 @@
+// 宸℃绠$悊
+import request from '@/utils/request'
+
+// 宸℃浠诲姟琛ㄨ〃鏌ヨ
+export function inspectionTaskList(query) {
+ return request({
+ url: '/inspectionTask/list',
+ method: 'get',
+ params: query
+ })
+}
+// 宸℃浠诲姟琛ㄦ柊澧炰慨鏀�
+export function addOrEditInspectionTask(query) {
+ return request({
+ url: '/inspectionTask/addOrEditInspectionTask',
+ method: 'post',
+ data: query
+ })
+}
+// 宸℃浠诲姟琛ㄥ垹闄�
+export function delInspectionTask(query) {
+ return request({
+ url: '/inspectionTask/delInspectionTask',
+ method: 'delete',
+ data: query
+ })
+}
+// 瀹氭椂宸℃浠诲姟琛ㄥ垹闄�
+export function delTimingTask(query) {
+ return request({
+ url: '/timingTask/delTimingTask',
+ method: 'delete',
+ data: query
+ })
+}
+
+// /inspectionTask/addOrEditInspectionTask
+// 宸℃涓婁紶
+export function uploadInspectionTask(query) {
+ return request({
+ url: '/inspectionTask/addOrEditInspectionTask',
+ method: 'post',
+ data: query
+ })
+}
+// 瀹氭椂宸℃浠诲姟琛ㄦ煡璇�
+export function timingTaskList(query) {
+ return request({
+ url: '/timingTask/list',
+ method: 'get',
+ params: query
+ })
+}
+// 瀹氭椂宸℃浠诲姟琛ㄦ柊澧炰慨鏀�
+export function addOrEditTimingTask(query) {
+ return request({
+ url: '/timingTask/addOrEditTimingTask',
+ method: 'post',
+ data: query
+ })
+}
\ No newline at end of file
diff --git a/src/api/inspectionUpload/index.js b/src/api/inspectionUpload/index.js
new file mode 100644
index 0000000..0d954e2
--- /dev/null
+++ b/src/api/inspectionUpload/index.js
@@ -0,0 +1,43 @@
+// 宸℃涓婁紶
+import request from '@/utils/request'
+
+// 浜岀淮鐮佺鐞嗚〃鏌ヨ
+export function qrCodeList(query) {
+ return request({
+ url: '/qrCode/list',
+ method: 'get',
+ params: query
+ })
+}
+// 浜岀淮鐮佹壂鐮佽褰曡〃鏌ヨ
+export function qrCodeScanRecordList(query) {
+ return request({
+ url: '/qrCodeScanRecord/list',
+ method: 'get',
+ params: query
+ })
+}
+// 浜岀淮鐮佺鐞嗚〃鏂板淇敼
+export function addOrEditQrCode(query) {
+ return request({
+ url: '/qrCode/addOrEditQrCode',
+ method: 'post',
+ data: query
+ })
+}
+// 浜岀淮鐮佹壂鐮佽褰曡〃鏂板淇敼
+export function addOrEditQrCodeRecord(query) {
+ return request({
+ url: '/qrCodeScanRecord/addOrEditQrCodeRecord',
+ method: 'post',
+ data: query
+ })
+}
+// 浜岀淮鐮佹壂鐮佽褰曡〃鏂板淇敼
+export function delQrCode(query) {
+ return request({
+ url: '/qrCode/delQrCode',
+ method: 'delete',
+ data: query
+ })
+}
\ No newline at end of file
diff --git a/src/api/inventoryManagement/stockIn.js b/src/api/inventoryManagement/stockIn.js
new file mode 100644
index 0000000..3481415
--- /dev/null
+++ b/src/api/inventoryManagement/stockIn.js
@@ -0,0 +1,161 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍏ュ簱淇℃伅鍒楄〃
+export const getStockInPage = (params) => {
+ return request({
+ url: "/stockin/listPage",
+ method: "get",
+ params,
+ });
+};
+
+// 鏌ヨ鐢熶骇鍏ュ簱淇℃伅鍒楄〃
+export const getStockInPageByProduction = (params) => {
+ return request({
+ url: "/stockin/listPageByProduction",
+ method: "get",
+ params,
+ });
+};
+
+// 鏌ヨ鐢熶骇鍏ュ簱淇℃伅鍒楄〃
+export const getStockInPageByProductProduction = (params) => {
+ return request({
+ url: "/stockin/listPageByProductProduction",
+ method: "get",
+ params,
+ });
+};
+
+// 鍑哄簱鍙拌处-鏌ヨ鑷畾涔夊叆搴撲俊鎭垪琛�
+export const getStockInPageByCustom = (params) => {
+ return request({
+ url: "/stockmanagement/listPageByCustom",
+ method: "get",
+ params,
+ });
+};
+// 鍏ュ簱绠$悊-鏌ヨ鑷畾涔夊叆搴撲俊鎭垪琛�
+export const getInPageByCustom = (params) => {
+ return request({
+ url: "/stockin/listPageByCustom",
+ method: "get",
+ params,
+ });
+};
+
+// 鍑哄簱鍙拌处-鏌ヨ鐢熶骇鍑哄簱淇℃伅鍒楄〃
+export const getStockInPageByProduct = (params) => {
+ return request({
+ url: "/stockmanagement/listPageByProduct",
+ method: "get",
+ params,
+ });
+};
+
+// 淇敼鍏ュ簱瀛樹俊鎭�
+export const updateStockIn = (data) => {
+ return request({
+ url: "/stockin/update",
+ method: "post",
+ data,
+ });
+};
+
+// 淇敼搴撳瓨淇℃伅
+export const updateManagement = (data) => {
+ return request({
+ url: "/stockin/updateManagement",
+ method: "post",
+ data,
+ });
+};
+// 淇敼鏉愭枡搴撳瓨淇℃伅
+export const updateManagementByCustom = (data) => {
+ return request({
+ url: "/stockin/updateManagementByCustom ",
+ method: "post",
+ data,
+ });
+};
+
+// 鏂板鍟嗗搧鍏ュ簱淇℃伅
+export function addSutockIn(data) {
+ return request({
+ url: '/stockin/add',
+ method: 'post',
+ data: data
+ })
+}
+
+// 鏂板鑷畾涔夊叆搴撲俊鎭�
+export function addStockInCustom(data) {
+ return request({
+ url: '/stockin/addCustom',
+ method: 'post',
+ data: data
+ })
+}
+
+// 缂栬緫鑷畾涔夊叆搴撲俊鎭�
+export function updateStockInCustom(data) {
+ return request({
+ url: '/stockin/updateCustom',
+ method: 'post',
+ data: data
+ })
+}
+// 缂栬緫鎴愬搧鍏ュ簱淇℃伅
+export function updateProduct(data) {
+ return request({
+ url: '/stockin/update',
+ method: 'post',
+ data: data
+ })
+}
+
+// 鍒犻櫎鍏ュ簱淇℃伅
+export function delStockIn(ids) {
+ return request({
+ url: '/stockin/del',
+ method: 'post',
+ data: ids
+ })
+}
+
+// 鍒犻櫎鑷畾涔夊叆搴撲俊鎭�
+export function delStockInCustom(ids) {
+ return request({
+ url: '/stockin/delteCustom',
+ method: 'post',
+ data: ids
+ })
+}
+
+// 瀵煎嚭鍏ュ簱淇℃伅
+export function exportStockIn(query) {
+ return request({
+ url: '/stockin/export',
+ method: 'get',
+ params: query,
+ responseType: 'blob'
+ })
+}
+
+export function selectProductRecordListByPuechaserId(query) {
+ return request({
+ url: '/stockin/productlist',
+ method: 'get',
+ params: query
+ })
+}
+
+
+//鏌ヨ搴撳瓨鍥捐〃鏁版嵁
+export function getStockInChartData() {
+ return request({
+ url: '/stockin/listReport',
+ method: 'get'
+ })
+}
+
diff --git a/src/api/inventoryManagement/stockInRecord.js b/src/api/inventoryManagement/stockInRecord.js
new file mode 100644
index 0000000..255c1c9
--- /dev/null
+++ b/src/api/inventoryManagement/stockInRecord.js
@@ -0,0 +1,53 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍏ュ簱淇℃伅鍒楄〃
+export const getStockInRecordListPage = (params) => {
+ return request({
+ url: "/stockInRecord/listPage",
+ method: "get",
+ params,
+ });
+};
+
+
+export const updateStockInRecord = (id, data) => {
+ return request({
+ url: "/stockInRecord/" + id,
+ method: "put",
+ data: data,
+ });
+};
+
+export const batchDeleteStockInRecords = (ids) => {
+ return request({
+ url: "/stockInRecord",
+ method: "delete",
+ data: ids,
+ });
+};
+
+export const batchDeletePendingStockInRecords = (ids) => {
+ return request({
+ url: "/stockInRecord/pending",
+ method: "delete",
+ data: ids,
+ });
+};
+
+// 鎵归噺瀹℃壒鍏ュ簱璁板綍锛坅pprovalStatus: approved/rejected锛�
+export const batchApproveStockInRecords = (data) => {
+ return request({
+ url: "/stockInRecord/approve",
+ method: "post",
+ data,
+ });
+};
+
+// 鎵归噺鍙嶅鍏ュ簱璁板綍锛堜粎椹冲洖鐘舵�佸彲鍙嶅锛�
+export const batchUnapproveStockInRecords = (data) => {
+ return request({
+ url: "/stockInRecord/reAudit",
+ method: "post",
+ data,
+ });
+};
\ No newline at end of file
diff --git a/src/api/inventoryManagement/stockInventory.js b/src/api/inventoryManagement/stockInventory.js
new file mode 100644
index 0000000..539eedc
--- /dev/null
+++ b/src/api/inventoryManagement/stockInventory.js
@@ -0,0 +1,105 @@
+import request from "@/utils/request.js";
+// 鍒嗛〉鏌ヨ搴撳瓨璁板綍鍒楄〃
+export const getStockInventoryListPage = (params) => {
+ return request({
+ url: "/stockInventory/pagestockInventory",
+ method: "get",
+ params,
+ });
+};
+
+// 鍒嗛〉鏌ヨ鑱斿悎搴撳瓨璁板綍鍒楄〃锛堝寘鍚晢鍝佷俊鎭級
+export const getStockInventoryListPageCombined = (params) => {
+ return request({
+ url: "/stockInventory/pageListCombinedStockInventory",
+ method: "get",
+ params,
+ });
+};
+
+export const getStockInventoryBatchNoQty = (params) => {
+ return request({
+ url: "/stockInventory/getBatchNoQty",
+ method: "get",
+ params,
+ });
+};
+
+// 鍒涘缓搴撳瓨璁板綍
+export const createStockInventory = (params) => {
+ return request({
+ url: "/stockInventory/addstockInventory",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鍑忓皯搴撳瓨璁板綍
+export const subtractStockInventory = (params) => {
+ return request({
+ url: "/stockInventory/subtractStockInventory",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鏂板鍏ュ簱璁板綍锛堜粎鍒涘缓璁板綍锛屼笉璋冩暣搴撳瓨锛�
+export const addStockInRecordOnly = (params) => {
+ return request({
+ url: "/stockInventory/addStockInRecordOnly",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鏂板鍑哄簱璁板綍锛堜粎鍒涘缓璁板綍锛屼笉璋冩暣搴撳瓨锛�
+export const addStockOutRecordOnly = (params) => {
+ return request({
+ url: "/stockInventory/addStockOutRecordOnly",
+ method: "post",
+ data: params,
+ });
+};
+
+export const getStockInventoryReportList = (params) => {
+ return request({
+ url: "/stockInventory/stockInventoryPage",
+ method: "get",
+ params,
+ });
+};
+
+export const getStockInventoryInAndOutReportList = (params) => {
+ return request({
+ url: "/stockInventory/stockInAndOutRecord",
+ method: "get",
+ params,
+ });
+};
+
+// 鍐荤粨搴撳瓨璁板綍
+export const frozenStockInventory = (params) => {
+ return request({
+ url: "/stockInventory/frozenStock",
+ method: "post",
+ data: params,
+ });
+};
+
+// 瑙e喕搴撳瓨璁板綍
+export const thawStockInventory = (params) => {
+ return request({
+ url: "/stockInventory/thawStock",
+ method: "post",
+ data: params,
+ });
+};
+
+export const getStockInventoryByModelId = (productModelId) => {
+ return request({
+ url: "/stockInventory/getByModelId",
+ method: "get",
+ params: { productModelId },
+ });
+};
+
diff --git a/src/api/inventoryManagement/stockManage.js b/src/api/inventoryManagement/stockManage.js
new file mode 100644
index 0000000..e2a4ebf
--- /dev/null
+++ b/src/api/inventoryManagement/stockManage.js
@@ -0,0 +1,83 @@
+import request from "@/utils/request";
+
+// 鏌ヨ搴撳瓨淇℃伅鍒楄〃
+export const getStockManagePage = (params) => {
+ return request({
+ url: "/stockin/listPageCopy",
+ method: "get",
+ params,
+ });
+};
+
+// 鏌ヨ鐢熶骇鍏ュ簱搴撳瓨淇℃伅鍒楄〃
+export const getStockManagePageByProduction = (params) => {
+ return request({
+ url: "/stockin/listPageCopyByProduction",
+ method: "get",
+ params,
+ });
+};
+// 鏌ヨ鎴愬搧搴撳瓨淇℃伅鍒楄〃
+export const getStockManageProduction = (params) => {
+ return request({
+ url: "/stockin/listPageProductionStock",
+ method: "get",
+ params,
+ });
+};
+// 鏌ヨ鑷畾涔夊叆搴撳簱瀛樹俊鎭垪琛�
+export const getStockManagePageByCustom = (params) => {
+ return request({
+ url: "/stockin/listPageCopyByCustom",
+ method: "get",
+ params,
+ });
+};
+
+
+// 淇敼搴撳瓨淇℃伅
+export const updateStockManage = (data) => {
+ return request({
+ url: "/stockmanagement/update",
+ method: "put",
+ data,
+ });
+};
+
+// 鍒犻櫎搴撳瓨淇℃伅
+export function delStockManage(ids) {
+ return request({
+ url: '/stockin/del',
+ method: 'post',
+ data: ids
+ })
+}
+
+// 瀵煎嚭搴撳瓨淇℃伅
+export function exportStockManage(query) {
+ return request({
+ url: '/stockmanagement/export',
+ method: 'get',
+ params: query,
+ responseType: 'blob'
+ })
+}
+
+// 鍑哄簱绠$悊-棰嗙敤鎺ュ彛
+export const stockOut = (data) => {
+ return request({
+ url: '/stockmanagement/stockout',
+ method: 'post',
+ data: data
+ })
+}
+
+//鏍规嵁id鑾峰彇搴撳瓨淇℃伅
+export function getStockManageById(id) {
+ return request({
+ url: '/stockmanagement/' + id,
+ method: 'get'
+ })
+}
+
+//
\ No newline at end of file
diff --git a/src/api/inventoryManagement/stockOut.js b/src/api/inventoryManagement/stockOut.js
new file mode 100644
index 0000000..004ba99
--- /dev/null
+++ b/src/api/inventoryManagement/stockOut.js
@@ -0,0 +1,37 @@
+import request from "@/utils/request";
+
+// 鍑哄簱鍙拌处-閲囪喘鍑哄簱鏌ヨ鍑哄簱鍒楄〃
+export const getStockOutPage = (params) => {
+ return request({
+ url: "/stockOutRecord/listPage",
+ method: "get",
+ params,
+ });
+};
+
+//鍒犻櫎鍑哄簱淇℃伅
+export const delStockOut = (ids) => {
+ return request({
+ url: "/stockOutRecord",
+ method: "delete",
+ data: ids,
+ });
+}
+
+//鍒犻櫎寰呭鎵瑰嚭搴撲俊鎭�
+export const delPendingStockOut = (ids) => {
+ return request({
+ url: "/stockOutRecord/pending",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鎵归噺瀹℃壒鍑哄簱璁板綍锛坅pprovalStatus: approved/rejected锛�
+export const batchApproveStockOutRecords = (data) => {
+ return request({
+ url: "/stockOutRecord/approve",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/inventoryManagement/stockReport.js b/src/api/inventoryManagement/stockReport.js
new file mode 100644
index 0000000..6d1a3ce
--- /dev/null
+++ b/src/api/inventoryManagement/stockReport.js
@@ -0,0 +1,55 @@
+import request from "@/utils/request";
+
+// 鑾峰彇搴撳瓨鏃ユ姤缁熻
+export const getStockDailyReport = (params) => {
+ return request({
+ url: "/stockin/getReportList",
+ method: "get",
+ params,
+ });
+};
+// 鑾峰彇搴撳瓨鏈堟姤缁熻
+export const getStockMonthlyReport = (params) => {
+ return request({
+ url: "/stockin/getReportList",
+ method: "get",
+ params,
+ });
+};
+
+// 鑾峰彇浣滀笟鎶ヨ〃缁熻
+export const getWorkReport = (params) => {
+ return request({
+ url: "/stockin/getReportList",
+ method: "get",
+ params,
+ });
+};
+
+// 鑾峰彇搴撳瓨杩涘嚭瀛樼粺璁�
+export const getStockInOutReport = (params) => {
+ return request({
+ url: "/stockin/getReportList",
+ method: "get",
+ params,
+ });
+};
+
+// 瀵煎嚭搴撳瓨鎶ヨ〃
+export const exportStockReport = (params) => {
+ return request({
+ url: "/stockin/exportCopy",
+ method: "post",
+ params,
+ responseType: 'blob'
+ });
+};
+
+// 鑾峰彇搴撳瓨瓒嬪娍鏁版嵁
+export const getStockTrendData = (params) => {
+ return request({
+ url: "/stockreport/trend",
+ method: "get",
+ params,
+ });
+};
diff --git a/src/api/inventoryManagement/stockUninventory.js b/src/api/inventoryManagement/stockUninventory.js
new file mode 100644
index 0000000..a80474e
--- /dev/null
+++ b/src/api/inventoryManagement/stockUninventory.js
@@ -0,0 +1,63 @@
+import request from "@/utils/request.js";
+// 鍒嗛〉鏌ヨ搴撳瓨璁板綍鍒楄〃
+export const getStockUninventoryListPage = (params) => {
+ return request({
+ url: "/stockUninventory/pagestockUninventory",
+ method: "get",
+ params,
+ });
+};
+
+// 鍒涘缓搴撳瓨璁板綍
+export const createStockUnInventory = (params) => {
+ return request({
+ url: "/stockUninventory/addstockUninventory",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鍑忓皯搴撳瓨璁板綍
+export const subtractStockUnInventory = (params) => {
+ return request({
+ url: "/stockUninventory/subtractstockUninventory",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鏂板鍏ュ簱璁板綍锛堜粎鍒涘缓璁板綍锛屼笉璋冩暣搴撳瓨锛�
+export const addUnqualifiedStockInRecordOnly = (params) => {
+ return request({
+ url: "/stockUninventory/addStockInRecordOnly",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鏂板鍑哄簱璁板綍锛堜粎鍒涘缓璁板綍锛屼笉璋冩暣搴撳瓨锛�
+export const addUnqualifiedStockOutRecordOnly = (params) => {
+ return request({
+ url: "/stockUninventory/addStockOutRecordOnly",
+ method: "post",
+ data: params,
+ });
+};
+
+// 鍐荤粨搴撳瓨璁板綍
+export const frozenStockUninventory = (params) => {
+ return request({
+ url: "/stockUninventory/frozenStock",
+ method: "post",
+ data: params,
+ });
+};
+
+// 瑙e喕搴撳瓨璁板綍
+export const thawStockUninventory = (params) => {
+ return request({
+ url: "/stockUninventory/thawStock",
+ method: "post",
+ data: params,
+ });
+};
diff --git a/src/api/inventoryManagement/stockWarning.js b/src/api/inventoryManagement/stockWarning.js
new file mode 100644
index 0000000..65e641a
--- /dev/null
+++ b/src/api/inventoryManagement/stockWarning.js
@@ -0,0 +1,84 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鍌ㄦ皵缃愰璀﹀垪琛�
+export const getStockWarningPage = (page, params) => {
+ return request({
+ url: "/gasTankWarning/listPage",
+ method: "get",
+ params: {
+ ...page,
+ ...params
+ },
+ });
+};
+
+// 鏂板鍌ㄦ皵缃愰璀﹁鍒�
+export const addStockWarning = (data) => {
+ return request({
+ url: "/gasTankWarning/add",
+ method: "post",
+ data: data,
+ });
+};
+
+// 淇敼鍌ㄦ皵缃愰璀﹁鍒�
+export const updateStockWarning = (data) => {
+ return request({
+ url: "/gasTankWarning/update",
+ method: "post",
+ data: data,
+ });
+};
+
+// 鍒犻櫎鍌ㄦ皵缃愰璀﹁鍒�
+export const deleteStockWarning = (ids) => {
+ return request({
+ url: "/gasTankWarning/delete",
+ method: "delete",
+ data: ids,
+ });
+};
+
+// 鎵归噺澶勭悊鍌ㄦ皵缃愰璀�
+export const batchProcessStockWarning = (data) => {
+ return request({
+ url: "/gasTankWarning/batchProcess",
+ method: "post",
+ data,
+ });
+};
+
+// 瀵煎嚭鍌ㄦ皵缃愰璀︽暟鎹�
+export const exportStockWarning = (params) => {
+ return request({
+ url: "/gasTankWarning/export",
+ method: "get",
+ params,
+ responseType: "blob",
+ });
+};
+
+// 鏍规嵁ID鑾峰彇鍌ㄦ皵缃愰璀﹁鎯�
+export const getStockWarningById = (id) => {
+ return request({
+ url: `/gasTankWarning/${id}`,
+ method: "get",
+ });
+};
+
+// 鍚敤/绂佺敤棰勮瑙勫垯
+export const toggleStockWarningStatus = (data) => {
+ return request({
+ url: "/gasTankWarning/toggleStatus",
+ method: "put",
+ data,
+ });
+};
+
+// 鑾峰彇棰勮缁熻淇℃伅
+export const getStockWarningStatistics = () => {
+ return request({
+ url: "/gasTankWarning/statistics",
+ method: "get",
+ });
+};
diff --git a/src/api/lavorissce/ledger.js b/src/api/lavorissce/ledger.js
new file mode 100644
index 0000000..f4f710c
--- /dev/null
+++ b/src/api/lavorissce/ledger.js
@@ -0,0 +1,55 @@
+import request from '@/utils/request'
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: '/lavorIssue/listPage',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鍒嗛〉鏌ヨ
+export function statistics(params) {
+ return request({
+ url: '/lavorIssue/statistics',
+ method: 'get',
+ params
+ })
+}
+
+export function statisticsList(params) {
+ return request({
+ url: '/lavorIssue/statisticsList',
+ method: 'get',
+ params
+ })
+}
+
+// 娣诲姞
+export function add(data) {
+ return request({
+ url: '/lavorIssue/add',
+ method: 'post',
+ data
+ })
+}
+
+// 淇敼
+export function update(data) {
+ return request({
+ url: '/lavorIssue/update',
+ method: 'post',
+ data
+ })
+}
+
+// 鍒犻櫎
+export function deleteLedger(data) {
+ return request({
+ url: '/lavorIssue/delete',
+ method: 'delete',
+ data
+ })
+}
+
diff --git a/src/api/login.js b/src/api/login.js
new file mode 100644
index 0000000..4cc0a5d
--- /dev/null
+++ b/src/api/login.js
@@ -0,0 +1,110 @@
+import request from '@/utils/request'
+import { getToken } from '@/utils/auth'
+
+// 鐧诲綍鏂规硶
+export function login(username, password, code, uuid) {
+ const data = {
+ username,
+ password,
+ code,
+ uuid
+ }
+ return request({
+ url: '/login',
+ headers: {
+ isToken: false,
+ repeatSubmit: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+// 娉ㄥ唽鏂规硶
+export function register(data) {
+ return request({
+ url: '/register',
+ headers: {
+ isToken: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+// 鑾峰彇鐢ㄦ埛璇︾粏淇℃伅
+export function getInfo() {
+ const token = getToken()
+ return request({
+ url: '/getInfo',
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
+ method: 'get'
+ })
+}
+
+// 閫�鍑烘柟娉�
+export function logout() {
+ return request({
+ url: '/logout',
+ method: 'post'
+ })
+}
+
+// 鑾峰彇楠岃瘉鐮�
+export function getCodeImg() {
+ return request({
+ url: '/captchaImage',
+ headers: {
+ isToken: false
+ },
+ method: 'get',
+ timeout: 20000
+ })
+}
+
+// 鐧诲綍鏍¢獙
+export function loginCheck(username, password) {
+ const data = {
+ username,
+ password
+ }
+ return request({
+ url: '/loginCheck',
+ headers: {
+ isToken: false,
+ repeatSubmit: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+// 鐧诲綍鏂规硶
+export function loginCheckFactory(username, password, factoryId) {
+ const data = {
+ username,
+ password,
+ factoryId
+ }
+ return request({
+ url: '/loginCheckFactory',
+ headers: {
+ isToken: false,
+ repeatSubmit: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+export function tideLogin(data) {
+ return request({
+ url: '/tide/tideLogin',
+ headers: {
+ isToken: false,
+ repeatSubmit: false
+ },
+ method: 'post',
+ data: data
+ })
+}
diff --git a/src/api/menu.js b/src/api/menu.js
new file mode 100644
index 0000000..6e52e6e
--- /dev/null
+++ b/src/api/menu.js
@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 鑾峰彇璺敱
+export const getRouters = () => {
+ return request({
+ url: '/getRouters',
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/src/api/monitor/cache.js b/src/api/monitor/cache.js
new file mode 100644
index 0000000..e1f2c87
--- /dev/null
+++ b/src/api/monitor/cache.js
@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+// 鏌ヨ缂撳瓨璇︾粏
+export function getCache() {
+ return request({
+ url: '/monitor/cache',
+ method: 'get'
+ })
+}
+
+// 鏌ヨ缂撳瓨鍚嶇О鍒楄〃
+export function listCacheName() {
+ return request({
+ url: '/monitor/cache/getNames',
+ method: 'get'
+ })
+}
+
+// 鏌ヨ缂撳瓨閿悕鍒楄〃
+export function listCacheKey(cacheName) {
+ return request({
+ url: '/monitor/cache/getKeys/' + cacheName,
+ method: 'get'
+ })
+}
+
+// 鏌ヨ缂撳瓨鍐呭
+export function getCacheValue(cacheName, cacheKey) {
+ return request({
+ url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
+ method: 'get'
+ })
+}
+
+// 娓呯悊鎸囧畾鍚嶇О缂撳瓨
+export function clearCacheName(cacheName) {
+ return request({
+ url: '/monitor/cache/clearCacheName/' + cacheName,
+ method: 'delete'
+ })
+}
+
+// 娓呯悊鎸囧畾閿悕缂撳瓨
+export function clearCacheKey(cacheKey) {
+ return request({
+ url: '/monitor/cache/clearCacheKey/' + cacheKey,
+ method: 'delete'
+ })
+}
+
+// 娓呯悊鍏ㄩ儴缂撳瓨
+export function clearCacheAll() {
+ return request({
+ url: '/monitor/cache/clearCacheAll',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/job.js b/src/api/monitor/job.js
new file mode 100644
index 0000000..84b7b5a
--- /dev/null
+++ b/src/api/monitor/job.js
@@ -0,0 +1,70 @@
+import request from "@/utils/request";
+
+// 鏌ヨ瀹氭椂浠诲姟璋冨害鍒楄〃
+export function listJob(query) {
+ return request({
+ url: "/monitor/job/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ瀹氭椂浠诲姟璋冨害璇︾粏
+export function getJob(jobId) {
+ return request({
+ url: "/monitor/job/" + jobId,
+ method: "get",
+ });
+}
+
+// 鏂板瀹氭椂浠诲姟璋冨害
+export function addJob(data) {
+ return request({
+ url: "/monitor/job",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼瀹氭椂浠诲姟璋冨害
+export function updateJob(data) {
+ return request({
+ url: "/monitor/job",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鍒犻櫎瀹氭椂浠诲姟璋冨害
+export function delJob(jobId) {
+ return request({
+ url: "/monitor/job/" + jobId,
+ method: "delete",
+ });
+}
+
+// 浠诲姟鐘舵�佷慨鏀�
+export function changeJobStatus(jobId, status) {
+ const data = {
+ jobId,
+ status,
+ };
+ return request({
+ url: "/monitor/job/changeStatus",
+ method: "put",
+ data: data,
+ });
+}
+
+// 瀹氭椂浠诲姟绔嬪嵆鎵ц涓�娆�
+export function runJob(jobId, jobGroup) {
+ const data = {
+ jobId,
+ jobGroup,
+ };
+ return request({
+ url: "/monitor/job/run",
+ method: "put",
+ data: data,
+ });
+}
diff --git a/src/api/monitor/jobLog.js b/src/api/monitor/jobLog.js
new file mode 100644
index 0000000..654bbae
--- /dev/null
+++ b/src/api/monitor/jobLog.js
@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 鏌ヨ璋冨害鏃ュ織鍒楄〃
+export function listJobLog(query) {
+ return request({
+ url: '/monitor/jobLog/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鍒犻櫎璋冨害鏃ュ織
+export function delJobLog(jobLogId) {
+ return request({
+ url: '/monitor/jobLog/' + jobLogId,
+ method: 'delete'
+ })
+}
+
+// 娓呯┖璋冨害鏃ュ織
+export function cleanJobLog() {
+ return request({
+ url: '/monitor/jobLog/clean',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/logininfor.js b/src/api/monitor/logininfor.js
new file mode 100644
index 0000000..c49a40e
--- /dev/null
+++ b/src/api/monitor/logininfor.js
@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鐧诲綍鏃ュ織鍒楄〃
+export function list(query) {
+ return request({
+ url: '/monitor/logininfor/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鍒犻櫎鐧诲綍鏃ュ織
+export function delLogininfor(infoId) {
+ return request({
+ url: '/monitor/logininfor/' + infoId,
+ method: 'delete'
+ })
+}
+
+// 瑙i攣鐢ㄦ埛鐧诲綍鐘舵��
+export function unlockLogininfor(userName) {
+ return request({
+ url: '/monitor/logininfor/unlock/' + userName,
+ method: 'get'
+ })
+}
+
+// 娓呯┖鐧诲綍鏃ュ織
+export function cleanLogininfor() {
+ return request({
+ url: '/monitor/logininfor/clean',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/online.js b/src/api/monitor/online.js
new file mode 100644
index 0000000..288ebe0
--- /dev/null
+++ b/src/api/monitor/online.js
@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍦ㄧ嚎鐢ㄦ埛鍒楄〃
+export function list(query) {
+ return request({
+ url: '/monitor/online/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 寮洪��鐢ㄦ埛
+export function forceLogout(tokenId) {
+ return request({
+ url: '/monitor/online/' + tokenId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/operlog.js b/src/api/monitor/operlog.js
new file mode 100644
index 0000000..6e881df
--- /dev/null
+++ b/src/api/monitor/operlog.js
@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鎿嶄綔鏃ュ織鍒楄〃
+export function list(query) {
+ return request({
+ url: '/monitor/operlog/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鍒犻櫎鎿嶄綔鏃ュ織
+export function delOperlog(operId) {
+ return request({
+ url: '/monitor/operlog/' + operId,
+ method: 'delete'
+ })
+}
+
+// 娓呯┖鎿嶄綔鏃ュ織
+export function cleanOperlog() {
+ return request({
+ url: '/monitor/operlog/clean',
+ method: 'delete'
+ })
+}
diff --git a/src/api/monitor/server.js b/src/api/monitor/server.js
new file mode 100644
index 0000000..cac7791
--- /dev/null
+++ b/src/api/monitor/server.js
@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 鑾峰彇鏈嶅姟淇℃伅
+export function getServer() {
+ return request({
+ url: '/monitor/server',
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/src/api/officeProcessAutomation/approvalInstance.js b/src/api/officeProcessAutomation/approvalInstance.js
new file mode 100644
index 0000000..054861c
--- /dev/null
+++ b/src/api/officeProcessAutomation/approvalInstance.js
@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+
+/** 鍒嗛〉鏌ヨ瀹℃壒瀹炰緥 */
+export function listApprovalInstancePage(params) {
+ return request({
+ url: "/approvalInstance/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 鎻愪氦/淇濆瓨瀹℃壒瀹炰緥 */
+export function saveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/save",
+ method: "post",
+ data: approvalInstanceDto,
+ });
+}
+
+/** 鏇存柊瀹℃壒瀹炰緥 */
+export function updateApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/update",
+ method: "put",
+ data: approvalInstanceDto,
+ });
+}
+
+/** 瀹℃壒锛堥�氳繃/椹冲洖锛� */
+export function approveApprovalInstance(approvalInstanceDto) {
+ return request({
+ url: "/approvalInstance/approve",
+ method: "post",
+ data: approvalInstanceDto,
+ });
+}
+
+/** 鍒犻櫎瀹℃壒瀹炰緥锛坆ody 涓� ID 鏁扮粍锛� */
+export function deleteApprovalInstance(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== "");
+ return request({
+ url: "/approvalInstance/delete",
+ method: "delete",
+ data: idList,
+ });
+}
diff --git a/src/api/officeProcessAutomation/approvalTemplate.js b/src/api/officeProcessAutomation/approvalTemplate.js
new file mode 100644
index 0000000..3ade018
--- /dev/null
+++ b/src/api/officeProcessAutomation/approvalTemplate.js
@@ -0,0 +1,58 @@
+import request from "@/utils/request";
+
+/** 妯℃澘绫诲瀷锛�0 绯荤粺鍐呯疆锛�1 鑷畾涔夛紙涓庡悗绔� templateType 涓�鑷达級 */
+export const TEMPLATE_TYPE_BUILTIN = 0;
+export const TEMPLATE_TYPE_CUSTOM = 1;
+
+/** 鏌ヨ鎵�鏈夊鎵规ā鏉� */
+export function listApprovalTemplate(type) {
+ return request({
+ url: `/approvalTemplate/list/${type}`,
+ method: "get",
+ });
+}
+
+/** 鍒嗛〉鏌ヨ瀹℃壒妯℃澘 */
+export function listApprovalTemplatePage(params) {
+ return request({
+ url: "/approvalTemplate/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏌ヨ瀹℃壒妯℃澘璇︽儏 */
+export function getApprovalTemplateDetail(id) {
+ return request({
+ url: `/approvalTemplate/detail/${id}`,
+ method: "get",
+ });
+}
+
+/** 鏂板瀹℃壒妯℃澘锛坆ody 涓� ApprovalTemplateDto锛� */
+export function addApprovalTemplate(approvalTemplateDto) {
+ return request({
+ url: "/approvalTemplate/add",
+ method: "post",
+ data: approvalTemplateDto,
+ });
+}
+
+/** 淇敼瀹℃壒妯℃澘锛坆ody 涓� ApprovalTemplateDto锛� */
+export function updateApprovalTemplate(approvalTemplateDto) {
+ return request({
+ url: "/approvalTemplate/update",
+ method: "put",
+ data: approvalTemplateDto,
+ });
+}
+
+/** 鍒犻櫎瀹℃壒妯℃澘锛坆ody 涓烘ā鏉� ID 鏁扮粍锛� */
+export function deleteApprovalTemplate(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== "");
+ return request({
+ url: "/approvalTemplate/delete",
+ method: "post",
+ data: idList,
+ });
+}
diff --git a/src/api/officeProcessAutomation/enterpriseNews.js b/src/api/officeProcessAutomation/enterpriseNews.js
new file mode 100644
index 0000000..52f345d
--- /dev/null
+++ b/src/api/officeProcessAutomation/enterpriseNews.js
@@ -0,0 +1,38 @@
+import request from "@/utils/request";
+
+/** 鍒嗛〉鏌ヨ浼佷笟鏂伴椈 */
+export function listEnterpriseNewsPage(params) {
+ return request({
+ url: "/enterpriseNews/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 鏂板浼佷笟鏂伴椈 */
+export function saveEnterpriseNews(enterpriseNewsDto) {
+ return request({
+ url: "/enterpriseNews/save",
+ method: "post",
+ data: enterpriseNewsDto,
+ });
+}
+
+/** 淇敼浼佷笟鏂伴椈 */
+export function updateEnterpriseNews(enterpriseNewsDto) {
+ return request({
+ url: "/enterpriseNews/update",
+ method: "put",
+ data: enterpriseNewsDto,
+ });
+}
+
+/** 鍒犻櫎浼佷笟鏂伴椈锛坆ody 涓� ID 鏁扮粍锛� */
+export function deleteEnterpriseNews(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter((id) => id != null && id !== "");
+ return request({
+ url: "/enterpriseNews/delete",
+ method: "delete",
+ data: idList,
+ });
+}
diff --git a/src/api/officeProcessAutomation/finReimbursement.js b/src/api/officeProcessAutomation/finReimbursement.js
new file mode 100644
index 0000000..84c3560
--- /dev/null
+++ b/src/api/officeProcessAutomation/finReimbursement.js
@@ -0,0 +1,71 @@
+import request from "@/utils/request";
+
+/** 鍒嗛〉鏌ヨ璐㈠姟鎶ラ攢 GET /finReimbursement/listPage */
+export function listFinReimbursementPage(params) {
+ return request({
+ url: "/finReimbursement/listPage",
+ method: "get",
+ params,
+ });
+}
+
+/** 璇︽儏 query锛歋pring 缁戝畾 finReimbursementDto.id锛屽嬁鐢� finReimbursementDto[id] */
+function buildFinReimbursementDetailParams(idOrDto) {
+ const raw =
+ typeof idOrDto === "object" && idOrDto !== null
+ ? idOrDto.id ?? idOrDto.reimbursementId
+ : idOrDto;
+ return {
+ "finReimbursementDto.id": raw,
+ id: raw,
+ };
+}
+
+/** 鏌ヨ璐㈠姟鎶ラ攢璇︽儏 GET /finReimbursement/detail */
+export function getFinReimbursementDetail(idOrDto) {
+ return request({
+ url: "/finReimbursement/detail",
+ method: "get",
+ params: buildFinReimbursementDetailParams(idOrDto),
+ });
+}
+
+/** 鏂板璐㈠姟鎶ラ攢 POST /finReimbursement/save */
+export function saveFinReimbursement(finReimbursementDto) {
+ return request({
+ url: "/finReimbursement/save",
+ method: "post",
+ data: finReimbursementDto,
+ });
+}
+
+/** 淇敼璐㈠姟鎶ラ攢 POST /finReimbursement/update */
+export function updateFinReimbursement(finReimbursementDto) {
+ return request({
+ url: "/finReimbursement/update",
+ method: "post",
+ data: finReimbursementDto,
+ });
+}
+
+/** 鍒犻櫎璐㈠姟鎶ラ攢 DELETE /finReimbursement/delete锛坆ody 涓� ID 鏁扮粍锛� */
+export function deleteFinReimbursement(ids) {
+ const idList = (Array.isArray(ids) ? ids : [ids]).filter(
+ (id) => id != null && id !== ""
+ );
+ return request({
+ url: "/finReimbursement/delete",
+ method: "delete",
+ data: idList,
+ });
+}
+
+/** 鏂板璧� save锛屼慨鏀硅蛋 update锛堜笌鎺ュ彛鏂囨。涓�鑷达級 */
+export function persistFinReimbursement(finReimbursementDto, isEdit = false) {
+ if (isEdit) {
+ return updateFinReimbursement(finReimbursementDto);
+ }
+ const payload = { ...finReimbursementDto };
+ delete payload.id;
+ return saveFinReimbursement(payload);
+}
diff --git a/src/api/personnelManagement/attendanceRules.js b/src/api/personnelManagement/attendanceRules.js
new file mode 100644
index 0000000..5b07b8b
--- /dev/null
+++ b/src/api/personnelManagement/attendanceRules.js
@@ -0,0 +1,45 @@
+import request from "@/utils/request";
+
+// 鑾峰彇鐝鍒楄〃
+export function getAttendanceRules(query) {
+ return request({
+ url: "/personalAttendanceLocationConfig/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鐝
+export function addAttendanceRule(data) {
+ return request({
+ url: "/personalAttendanceLocationConfig/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鏇存柊鐝
+export function updateAttendanceRule(data) {
+ return request({
+ url: "/attendanceRules/update",
+ method: "put",
+ data,
+ });
+}
+
+// 鍒犻櫎鐝
+export function deleteAttendanceRule(ids) {
+ return request({
+ url: `/personalAttendanceLocationConfig/del`,
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鑾峰彇鍗曚釜鐝璇︽儏
+export function getAttendanceRuleDetail(id) {
+ return request({
+ url: `/attendanceRules/detail/${id}`,
+ method: "get",
+ });
+}
diff --git a/src/api/personnelManagement/bank.js b/src/api/personnelManagement/bank.js
new file mode 100644
index 0000000..5e83e27
--- /dev/null
+++ b/src/api/personnelManagement/bank.js
@@ -0,0 +1,34 @@
+import request from "@/utils/request";
+
+// 閾惰绠$悊
+export function bankList() {
+ return request({
+ url: "/bank/list",
+ method: "get",
+ });
+}
+
+export function bankAdd(data) {
+ return request({
+ url: "/bank/add",
+ method: "post",
+ data,
+ });
+}
+
+export function bankUpdate(data) {
+ return request({
+ url: "/bank/update",
+ method: "post",
+ data,
+ });
+}
+
+export function bankDelete(ids) {
+ return request({
+ url: "/bank/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
diff --git a/src/api/personnelManagement/class.js b/src/api/personnelManagement/class.js
new file mode 100644
index 0000000..b254c4a
--- /dev/null
+++ b/src/api/personnelManagement/class.js
@@ -0,0 +1,118 @@
+// 鐝鐩稿叧鎺ュ彛
+
+import request from "@/utils/request";
+
+// 缁╂晥绠$悊-鐝-鍒嗛〉鏌ヨ
+export function page(query) {
+ return request({
+ url: "/personalShift/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-骞翠唤鍒嗛〉鏌ヨ
+export function pageYear(query) {
+ return request({
+ url: "/personalShift/pageYear",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鎺掔彮
+export function add(data) {
+ return request({
+ url: "/personalShift/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-鏌ヨ鏃堕棿閰嶇疆淇℃伅
+export function list(query) {
+ return request({
+ url: "/shiftTime/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-鏂板
+export function shiftAdd(data) {
+ return request({
+ url: "/shiftTime/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-淇敼
+export function shiftUpdate(data) {
+ return request({
+ url: "/shiftTime/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鏃堕棿閰嶇疆-鍒犻櫎
+export function shiftRemove(query) {
+ return request({
+ url: "/shiftTime/remove",
+ method: "delete",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-瀵煎嚭
+export function exportFile(query) {
+ return request({
+ url: "/personalShift/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// 缁╂晥绠$悊-鐝-瀵煎嚭
+export function obtainItemParameterList(query) {
+ return request({
+ url: "/laboratoryScope/obtainItemParameterList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 缁╂晥绠$悊-鐝-鐝鐘舵�佷慨鏀�
+export function update(data) {
+ return request({
+ url: "/personalShift/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鑾峰彇鐢ㄦ埛鍒楄〃
+// export function selectUserCondition(query) {
+// return request({
+// url: "/system/newUser/selectUserCondition",
+// method: "get",
+// params: query,
+// });
+// }
+export function selectUserCondition() {
+ return request({
+ url: '/system/user/userListNoPage',
+ method: 'get'
+ })
+}
+
+// 鏌ヨ鍦ㄨ亴鍛樺伐鍙拌处
+export function staffOnJobListPage(query) {
+ return request({
+ url: '/staff/staffOnJob/listPage',
+ method: 'get',
+ params: query,
+ })
+}
diff --git a/src/api/personnelManagement/employeeRecord.js b/src/api/personnelManagement/employeeRecord.js
new file mode 100644
index 0000000..a4ad34b
--- /dev/null
+++ b/src/api/personnelManagement/employeeRecord.js
@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍦ㄨ亴鍛樺伐鍙拌处
+export function staffOnJobListPage(query) {
+ return request({
+ url: '/staff/staffOnJob/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+// 鏌ヨ鍛樺伐鍏ヨ亴淇℃伅
+export function staffOnJobInfo(query) {
+ return request({
+ url: '/staff/staffOnJob/staffNo',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 瀵煎嚭鍚堝悓鍓湰
+export function staffOnJobExportCopy(data) {
+ return request({
+ url: '/staff/staffOnJob/exportCopy',
+ method: 'post',
+ data: data,
+ })
+}
\ No newline at end of file
diff --git a/src/api/personnelManagement/monthlyStatistics.js b/src/api/personnelManagement/monthlyStatistics.js
new file mode 100644
index 0000000..a070d0f
--- /dev/null
+++ b/src/api/personnelManagement/monthlyStatistics.js
@@ -0,0 +1,65 @@
+import request from "@/utils/request";
+
+// 浜哄憳钖祫鍙拌处鍒楄〃
+export function monthlyStatisticsListPage(query) {
+ return request({
+ url: "/compensationPerformance/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 浜哄憳钖祫鍙拌处璇︽儏
+export function monthlyStatisticsGet(id) {
+ return request({
+ url: "/monthlyStatistics/get",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板浜哄憳钖祫鍙拌处
+export function monthlyStatisticsAdd(data) {
+ return request({
+ url: "/compensationPerformance/add",
+ method: "post",
+ data,
+ });
+}
+
+// 缂栬緫浜哄憳钖祫鍙拌处
+export function monthlyStatisticsUpdate(data) {
+ return request({
+ url: "/compensationPerformance/update",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎浜哄憳钖祫鍙拌处
+export function monthlyStatisticsDelete(ids) {
+ return request({
+ url: "/compensationPerformance/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 瀵煎嚭浜哄憳钖祫鍙拌处
+export function monthlyStatisticsExport(query) {
+ return request({
+ url: "/compensationPerformance/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// 浜哄憳鍒楄〃
+export function staffOnJobList(query) {
+ return request({
+ url: "/staff/staffOnJob/list",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/personnelManagement/payrollManagement.js b/src/api/personnelManagement/payrollManagement.js
new file mode 100644
index 0000000..c29a6b1
--- /dev/null
+++ b/src/api/personnelManagement/payrollManagement.js
@@ -0,0 +1,35 @@
+// 钖叕绠$悊
+import request from "@/utils/request";
+
+// 鏌ヨ鍒楄〃
+export function compensationListPage(query) {
+ return request({
+ url: "/compensationPerformance/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板
+export function compensationAdd(query) {
+ return request({
+ url: "/compensationPerformance/add",
+ method: "post",
+ data: query,
+ });
+}
+// 淇敼
+export function compensationUpdate(query) {
+ return request({
+ url: "/compensationPerformance/update",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎
+export function compensationDelete(query) {
+ return request({
+ url: "/compensationPerformance/delete",
+ method: "delete",
+ data: query,
+ });
+}
\ No newline at end of file
diff --git a/src/api/personnelManagement/personalAttendanceRecords.js b/src/api/personnelManagement/personalAttendanceRecords.js
new file mode 100644
index 0000000..bdd9e1c
--- /dev/null
+++ b/src/api/personnelManagement/personalAttendanceRecords.js
@@ -0,0 +1,25 @@
+import request from "@/utils/request.js";
+
+export function createPersonalAttendanceRecord(params) {
+ return request({
+ url: "/personalAttendanceRecords",
+ method: "post",
+ data: params,
+ });
+}
+
+export function findPersonalAttendanceRecords(query) {
+ return request({
+ url: "/personalAttendanceRecords/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function findTodayPersonalAttendanceRecord(query) {
+ return request({
+ url: "/personalAttendanceRecords/today",
+ method: "get",
+ params: query,
+ });
+}
\ No newline at end of file
diff --git a/src/api/personnelManagement/scheduling.js b/src/api/personnelManagement/scheduling.js
new file mode 100644
index 0000000..8e4b058
--- /dev/null
+++ b/src/api/personnelManagement/scheduling.js
@@ -0,0 +1,32 @@
+// 鎺掔彮绠$悊
+import request from "@/utils/request";
+
+export function save(data) {
+ return request({
+ url: "/staff/staffScheduling/save",
+ method: "post",
+ data: data,
+ });
+}
+
+export function del(id) {
+ return request({
+ url: "/staff/staffScheduling/del/"+id,
+ method: "delete",
+ });
+}
+
+export function delByIds(data) {
+ return request({
+ url: "/staff/staffScheduling/save",
+ method: "post",
+ data: data,
+ });
+}
+export function listPage(data){
+ return request({
+ url: "/staff/staffScheduling/listPage",
+ method: "post",
+ data: data
+ })
+}
diff --git a/src/api/personnelManagement/selfService.js b/src/api/personnelManagement/selfService.js
new file mode 100644
index 0000000..c95436a
--- /dev/null
+++ b/src/api/personnelManagement/selfService.js
@@ -0,0 +1,71 @@
+// 钖叕绠$悊
+import request from "@/utils/request";
+
+// 鏌ヨ鑰冨嫟鍒楄〃
+export function personalAttendanceRecordsListPage(query) {
+ return request({
+ url: "/staff/personalAttendanceRecords/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏌ヨ鍋囨湡鐢宠鍒楄〃
+export function holidayApplicationListPage(query) {
+ return request({
+ url: "/staff/holidayApplication/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板
+export function personalAttendanceRecordsAdd(query) {
+ return request({
+ url: "/staff/personalAttendanceRecords/add",
+ method: "post",
+ data: query,
+ });
+}
+// 鏂板鍋囨湡鐢宠
+export function holidayApplicationAdd(query) {
+ return request({
+ url: "/staff/holidayApplication/add",
+ method: "post",
+ data: query,
+ });
+}
+// 淇敼
+export function personalAttendanceRecordsUpdate(query) {
+ return request({
+ url: "/staff/personalAttendanceRecords/update",
+ method: "put",
+ data: query,
+ });
+}
+// 淇敼鍋囨湡鐢宠
+export function holidayApplicationUpdate(query) {
+ return request({
+ url: "/staff/holidayApplication/update",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎
+export function personalAttendanceRecordsDelete(id) {
+ return request({
+ url: "/staff/personalAttendanceRecords/delete/"+id,
+ method: "delete",
+ });
+}
+// 鍒犻櫎鍋囨湡鐢宠
+export function holidayApplicationDelete(id) {
+ return request({
+ url: "/staff/holidayApplication/delete/"+id,
+ method: "delete",
+ });
+}
+// export function del(id) {
+// return request({
+// url: "/staff/staffScheduling/del/"+id,
+// method: "delete",
+// });
+// }
\ No newline at end of file
diff --git a/src/api/personnelManagement/socialSecuritySet.js b/src/api/personnelManagement/socialSecuritySet.js
new file mode 100644
index 0000000..29637f0
--- /dev/null
+++ b/src/api/personnelManagement/socialSecuritySet.js
@@ -0,0 +1,46 @@
+// 绀句細淇濋櫓璁剧疆
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ鍒楄〃
+export function socialSecurityListPage(query) {
+ return request({
+ url: "/schemeApplicableStaff/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ璇︽儏
+export function socialSecurityInfo(id) {
+ return request({
+ url: "/schemeApplicableStaff/" + id,
+ method: "get",
+ });
+}
+
+// 鏂板
+export function socialSecurityAdd(data) {
+ return request({
+ url: "/schemeApplicableStaff/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼
+export function socialSecurityUpdate(data) {
+ return request({
+ url: "/schemeApplicableStaff/updateSchemeApplicableStaff",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎
+export function socialSecurityDelete(ids) {
+ return request({
+ url: "/schemeApplicableStaff/delete",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/api/personnelManagement/staffAnalytics.js b/src/api/personnelManagement/staffAnalytics.js
new file mode 100644
index 0000000..83eb375
--- /dev/null
+++ b/src/api/personnelManagement/staffAnalytics.js
@@ -0,0 +1,26 @@
+import request from "@/utils/request.js";
+
+// 绂昏亴鍘熷洜鍒嗘瀽
+export function findStaffLeaveReasonAnalysis() {
+ return request({
+ url: "/staff/analytics/reason",
+ method: "get"
+ });
+}
+
+// 12涓湀鍛樺伐娴佸姩娴佸け鐜囧垎鏋�
+export function findStaffAnalysisMonthlyTurnoverRateFor12Months() {
+ return request({
+ url: "/staff/analytics/monthly_turnover_rate",
+ method: "get"
+ });
+}
+
+export function findStaffAnalysisTotalStatistic() {
+ return request({
+ url: "/staff/analytics/total_statistic",
+ method: "get"
+ });
+}
+
+
diff --git a/src/api/personnelManagement/staffContract.js b/src/api/personnelManagement/staffContract.js
new file mode 100644
index 0000000..a6b71cb
--- /dev/null
+++ b/src/api/personnelManagement/staffContract.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request.js";
+
+
+export function findStaffContractListPage(query) {
+ return request({
+ url: "/staff/staffContract/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/personnelManagement/staffLeave.js b/src/api/personnelManagement/staffLeave.js
new file mode 100644
index 0000000..d675996
--- /dev/null
+++ b/src/api/personnelManagement/staffLeave.js
@@ -0,0 +1,33 @@
+import request from "@/utils/request.js";
+
+export function findStaffLeaveListPage(query) {
+ return request({
+ url: "/staff/staffLeave/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function createStaffLeave(data) {
+ return request({
+ url: "/staff/staffLeave",
+ method: "post",
+ data: data,
+ });
+}
+
+export function updateStaffLeave(id, data) {
+ return request({
+ url: "/staff/staffLeave/" + id,
+ method: "put",
+ data: data,
+ });
+}
+
+export function batchDeleteStaffLeaves(data) {
+ return request({
+ url: "/staff/staffLeave/del",
+ method: "delete",
+ data: data,
+ });
+}
diff --git a/src/api/personnelManagement/staffOnJob.js b/src/api/personnelManagement/staffOnJob.js
new file mode 100644
index 0000000..7a14391
--- /dev/null
+++ b/src/api/personnelManagement/staffOnJob.js
@@ -0,0 +1,63 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍦ㄨ亴鍛樺伐鍙拌处
+export function staffOnJobListPage(query) {
+ return request({
+ url: '/staff/staffOnJob/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+// 鏌ヨ鍛樺伐鍏ヨ亴淇℃伅
+export function staffOnJobInfo(id, query) {
+ return request({
+ url: '/staff/staffOnJob/' + id,
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鏌ヨ鍛樺伐鍏ヨ亴淇℃伅
+export function getStaffOnJobInfoByUserName(query) {
+ return request({
+ url: '/staff/staffOnJob/byUserName',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鏂板鍛樺伐
+export function createStaffOnJob(params) {
+ return request({
+ url: "/staff/staffOnJob",
+ method: "post",
+ data: params,
+ });
+}
+
+// 淇敼鍛樺伐
+export function updateStaffOnJob(id, params) {
+ return request({
+ url: "/staff/staffOnJob/" + id,
+ method: "put",
+ data: params,
+ });
+}
+
+// 鍒犻櫎鍛樺伐
+export function batchDeleteStaffOnJobs(query) {
+ return request({
+ url: "/staff/staffOnJob/del",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 缁鍚堝悓
+export function renewContract(id, params) {
+ return request({
+ url: "/staff/staffOnJob/renewContract/" + id,
+ method: "post",
+ data: params,
+ });
+}
diff --git a/src/api/personnelManagement/staffSalaryMain.js b/src/api/personnelManagement/staffSalaryMain.js
new file mode 100644
index 0000000..a42f96b
--- /dev/null
+++ b/src/api/personnelManagement/staffSalaryMain.js
@@ -0,0 +1,43 @@
+import request from "@/utils/request";
+
+// 鍛樺伐宸ヨ祫涓昏〃
+export function staffSalaryMainListPage(params) {
+ return request({
+ url: "/staffSalaryMain/listPage",
+ method: "get",
+ params,
+ });
+}
+
+export function staffSalaryMainCalculateSalary(ids) {
+ return request({
+ url: "/staffSalaryMain/calculateSalary",
+ method: "post",
+ data: ids,
+ });
+}
+
+export function staffSalaryMainAdd(data) {
+ return request({
+ url: "/staffSalaryMain/add",
+ method: "post",
+ data,
+ });
+}
+
+export function staffSalaryMainUpdate(data) {
+ return request({
+ url: "/staffSalaryMain/update",
+ method: "post",
+ data,
+ });
+}
+
+export function staffSalaryMainDelete(ids) {
+ return request({
+ url: "/staffSalaryMain/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
diff --git a/src/api/procurementManagement/advancedPriceManagement.js b/src/api/procurementManagement/advancedPriceManagement.js
new file mode 100644
index 0000000..a30921c
--- /dev/null
+++ b/src/api/procurementManagement/advancedPriceManagement.js
@@ -0,0 +1,38 @@
+// 楂樼骇閲囪喘浠锋牸绠$悊API鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ浠锋牸鍒楄〃
+export function listPage(query) {
+ return request({
+ url: "/procurementPriceManagement/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板浠锋牸
+export function add(data) {
+ return request({
+ url: "/procurementPriceManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鏇存柊浠锋牸
+export function update(data) {
+ return request({
+ url: "/procurementPriceManagement/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎浠锋牸
+export function del(data) {
+ return request({
+ url: `/procurementPriceManagement/del`,
+ method: "delete",
+ data
+ });
+}
diff --git a/src/api/procurementManagement/arrivalManagement.js b/src/api/procurementManagement/arrivalManagement.js
new file mode 100644
index 0000000..107fc3c
--- /dev/null
+++ b/src/api/procurementManagement/arrivalManagement.js
@@ -0,0 +1,43 @@
+// 閿�鍞彴璐﹂〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/inboundManagement/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function listPageCopy(query) {
+ return request({
+ url: "/inboundManagement/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板
+export function add(data) {
+ return request({
+ url: "/inboundManagement/add",
+ method: "post",
+ data
+ });
+}
+// 淇敼
+export function update(data) {
+ return request({
+ url: "/inboundManagement/update",
+ method: "post",
+ data
+ });
+}
+// 鍒犻櫎閿�鍞彴璐�
+export function del(data) {
+ return request({
+ url: "/inboundManagement/del",
+ method: "delete",
+ data
+ });
+}
\ No newline at end of file
diff --git a/src/api/procurementManagement/paymentLedger.js b/src/api/procurementManagement/paymentLedger.js
new file mode 100644
index 0000000..6c5d9de
--- /dev/null
+++ b/src/api/procurementManagement/paymentLedger.js
@@ -0,0 +1,20 @@
+// 閲囪喘鍙拌处椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+/** 浠樻鍙拌处 - 渚涘簲鍟嗗線鏉ユ眹鎬� */
+export function paymentLedgerList(query) {
+ return request({
+ url: "/purchase/report/supplierTransactions",
+ method: "get",
+ params: query,
+ });
+}
+
+/** 浠樻鍙拌处 - 渚涘簲鍟嗗線鏉ユ槑缁� */
+export function paymentRecordList(params) {
+ return request({
+ url: "/purchase/report/supplierTransactionsDetails",
+ method: "get",
+ params,
+ });
+}
diff --git a/src/api/procurementManagement/procurementLedger.js b/src/api/procurementManagement/procurementLedger.js
new file mode 100644
index 0000000..88f4ce0
--- /dev/null
+++ b/src/api/procurementManagement/procurementLedger.js
@@ -0,0 +1,134 @@
+// 閲囪喘鍙拌处椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function purchaseList(query) {
+ return request({
+ url: "/purchase/ledger/list",
+ method: "get",
+ params: query,
+ });
+}
+// 鏌ヨ鍚堝悓鍙�
+export function getSalesNo(query) {
+ return request({
+ url: "/purchase/ledger/getSalesNo",
+ method: "get",
+ params: query,
+ });
+}
+// 瀛愯〃鏍兼煡璇�
+export function productList(query) {
+ return request({
+ url: "/sales/product/list",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板銆佺紪杈�
+export function addOrEditPurchase(query) {
+ return request({
+ url: "/purchase/ledger/addOrEditPurchase",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎
+export function delPurchase(query) {
+ return request({
+ url: "/purchase/ledger/delPurchase",
+ method: "delete",
+ data: query,
+ });
+}
+// 鏌ヨ璇︽儏
+export function getPurchaseById(query) {
+ return request({
+ url: "/purchase/ledger/getPurchaseById",
+ method: "get",
+ params: query,
+ });
+}
+// 鏌ヨ璇︽儏
+export function getOptions(query) {
+ return request({
+ url: "/system/supplier/getOptions",
+ method: "get",
+ params: query,
+ });
+}
+// 鏌ヨ閲囪喘鍙拌处鍒楄〃
+export function purchaseListPage(query) {
+ return request({
+ url: "/purchase/ledger/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function createPurchaseNo(entryDate) {
+ return request({
+ url: "/purchase/ledger/createPurchaseNo",
+ method: "get",
+ params: { entryDate },
+ });
+}
+export function updateApprovalStatus(query) {
+ return request({
+ url: "/purchase/ledger/updateApprovalStatus",
+ method: "post",
+ data: query,
+ });
+}
+
+// 淇濆瓨閲囪喘妯℃澘
+export function addPurchaseTemplate(data) {
+ return request({
+ url: "/purchaseLedgerTemplate/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼閲囪喘妯℃澘
+export function updatePurchaseTemplate(data) {
+ return request({
+ url: "/purchaseLedgerTemplate/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鏌ヨ閲囪喘妯℃澘
+export function getPurchaseTemplateList(query) {
+ return request({
+ url: "/purchase/ledger/getPurchaseTemplateList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍒犻櫎閲囪喘妯℃澘
+export function delPurchaseTemplate(id) {
+ return request({
+ url: "/purchaseLedgerTemplate/delete",
+ method: "delete",
+ data: id,
+ });
+}
+// 鏌ヨ閲囪喘璇︽儏
+export function getPurchaseByCode(id) {
+ return request({
+ url: "/purchase/ledger/getPurchaseByCode",
+ method: "get",
+ params: id,
+ });
+}
+
+export function batchGeneratePurchaseInboundSteps(query) {
+ return request({
+ url: "/purchase/ledger/batchInsertPurchaseSteps",
+ method: "post",
+ data: query,
+ });
+}
diff --git a/src/api/procurementManagement/procurementPlan.js b/src/api/procurementManagement/procurementPlan.js
new file mode 100644
index 0000000..48a1a74
--- /dev/null
+++ b/src/api/procurementManagement/procurementPlan.js
@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ閲囪喘璁″垝鍒楄〃
+export function listPage(query) {
+ return request({
+ url: "/procurementPlan/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板閲囪喘璁″垝
+export function add(data) {
+ return request({
+ url: "/procurementPlan/add",
+ method: "post",
+ data,
+ });
+}
+
+// 淇敼閲囪喘璁″垝
+export function update(data) {
+ return request({
+ url: "/procurementPlan/update",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎閲囪喘璁″垝
+export function del(data) {
+ return request({
+ url: "/procurementPlan/del",
+ method: "delete",
+ data,
+ });
+}
+
+// 鍒犻櫎閲囪喘璁″垝
+export function listPageCopy(query) {
+ return request({
+ url: "/stockin/listPageCopy",
+ method: "get",
+ params: query,
+ });
+}
+
diff --git a/src/api/procurementManagement/procurementReport.js b/src/api/procurementManagement/procurementReport.js
new file mode 100644
index 0000000..32c50c7
--- /dev/null
+++ b/src/api/procurementManagement/procurementReport.js
@@ -0,0 +1,11 @@
+// 閲囪喘鎶ヨ〃椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 閲囪喘涓氬姟姹囨�昏〃鍒嗛〉鏌ヨ
+export function procurementBusinessSummaryListPage(query) {
+ return request({
+ url: "/procurementBusinessSummary/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/procurementManagement/projectProfit.js b/src/api/procurementManagement/projectProfit.js
new file mode 100644
index 0000000..7fb1660
--- /dev/null
+++ b/src/api/procurementManagement/projectProfit.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function getPurchaseList(query) {
+ return request({
+ url: "/purchase/report/list",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/procurementManagement/purchase_return_order.js b/src/api/procurementManagement/purchase_return_order.js
new file mode 100644
index 0000000..2705dde
--- /dev/null
+++ b/src/api/procurementManagement/purchase_return_order.js
@@ -0,0 +1,47 @@
+import request from "@/utils/request";
+// 閲囪喘閫�璐у崟
+
+// 鍒嗛〉鏌ヨ
+export function findPurchaseReturnOrderListPage(query) {
+ return request({
+ url: "/purchaseReturnOrders/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板
+export function createPurchaseReturnOrder(data) {
+ return request({
+ url: "/purchaseReturnOrders/add",
+ method: "post",
+ data
+ });
+}
+
+// 鏍规嵁閲囪喘鍙拌处 ID 鏌ヨ鍙��浜у搧绛変俊鎭�
+export function getPurchaseReturnOrderByPurchaseLedgerId(query) {
+ return request({
+ url: "/purchaseReturnOrders/getByPurchaseLedgerId",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ョ湅璇︽儏
+// purchaseReturnOrders/selectById/xxx
+export function getPurchaseReturnOrderDetail(id) {
+ return request({
+ url: "/purchaseReturnOrders/selectById/" + id,
+ method: "get",
+ });
+}
+
+// 閲囪喘閫�璐у崟鍒犻櫎
+// POST purchaseReturnOrders/deleteById/xxx
+export function deletePurchaseReturnOrder(id) {
+ return request({
+ url: "/purchaseReturnOrders/deleteById/" + id,
+ method: "post",
+ });
+}
\ No newline at end of file
diff --git a/src/api/procurementManagement/returnManagement.js b/src/api/procurementManagement/returnManagement.js
new file mode 100644
index 0000000..e765701
--- /dev/null
+++ b/src/api/procurementManagement/returnManagement.js
@@ -0,0 +1,35 @@
+// 閿�鍞彴璐﹂〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/returnManagement/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板
+export function add(data) {
+ return request({
+ url: "/returnManagement/add",
+ method: "post",
+ data
+ });
+}
+// 淇敼
+export function update(data) {
+ return request({
+ url: "/returnManagement/update",
+ method: "post",
+ data
+ });
+}
+// 鍒犻櫎閿�鍞彴璐�
+export function del(data) {
+ return request({
+ url: "/returnManagement/del",
+ method: "delete",
+ data
+ });
+}
\ No newline at end of file
diff --git a/src/api/procurementManagement/taxComparison.js b/src/api/procurementManagement/taxComparison.js
new file mode 100644
index 0000000..726a27f
--- /dev/null
+++ b/src/api/procurementManagement/taxComparison.js
@@ -0,0 +1,10 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function getTaxList(query) {
+ return request({
+ url: "/purchase/report/listVat",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/procurementManagement/transferManagement.js b/src/api/procurementManagement/transferManagement.js
new file mode 100644
index 0000000..fa404d1
--- /dev/null
+++ b/src/api/procurementManagement/transferManagement.js
@@ -0,0 +1,34 @@
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function getPurchaseOrders(query) {
+ return request({
+ url: "/purchase/ledger/listPage",
+ method: "get",
+ params: query,
+ });
+}
+//
+export function confirmReceipt(query) {
+ return request({
+ url: "",
+ method: "post",
+ data: query,
+ });
+}
+// 澧炴坊閲囪喘寮傚父璁板綍
+export function addPurchaseException(query) {
+ return request({
+ url: "/procurementExceptionRecord/add",
+ method: "post",
+ data: query,
+ });
+}
+// 淇敼閲囪喘寮傚父璁板綍
+export function updatePurchaseException(query) {
+ return request({
+ url: "/procurementExceptionRecord/update",
+ method: "post",
+ data: query,
+ });
+}
\ No newline at end of file
diff --git a/src/api/productionManagement/operationScheduling.js b/src/api/productionManagement/operationScheduling.js
new file mode 100644
index 0000000..75ffe2a
--- /dev/null
+++ b/src/api/productionManagement/operationScheduling.js
@@ -0,0 +1,27 @@
+// 宸ュ簭鎺掍骇椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPageProcess(query) {
+ return request({
+ url: "/salesLedger/scheduling/listPageProcess",
+ method: "get",
+ params: query,
+ });
+}
+// 鍙栨秷鎺掍骇
+export function productionDispatchDelete(query) {
+ return request({
+ url: "/salesLedger/scheduling/productionDispatchDelete",
+ method: "delete",
+ data: query,
+ });
+}
+// 鍙栨秷鎺掍骇
+export function processScheduling(query) {
+ return request({
+ url: "/salesLedger/scheduling/processScheduling",
+ method: "post",
+ data: query,
+ });
+}
\ No newline at end of file
diff --git a/src/api/productionManagement/processRoute.js b/src/api/productionManagement/processRoute.js
new file mode 100644
index 0000000..a0cb6c8
--- /dev/null
+++ b/src/api/productionManagement/processRoute.js
@@ -0,0 +1,49 @@
+// 宸ヨ壓璺嚎椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/technologyRouting/page",
+ method: "get",
+ params: query,
+ });
+}
+
+export function add(data) {
+ return request({
+ url: "/technologyRouting/addTechRoute",
+ method: "post",
+ data: data,
+ });
+}
+
+// export function del(ids) {
+// return request({
+// url: "/processRoute/" + ids,
+// method: "delete",
+// });
+// }
+export function del(ids) {
+ return request({
+ url: "/technologyRouting/delete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+export function update(data) {
+ return request({
+ url: "/technologyRouting/editTechRoute",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鑾峰彇璇︽儏
+export function getById(id) {
+ return request({
+ url: `/processRoute/${id}`,
+ method: "get",
+ });
+}
diff --git a/src/api/productionManagement/processRouteFile.js b/src/api/productionManagement/processRouteFile.js
new file mode 100644
index 0000000..e3a9fb4
--- /dev/null
+++ b/src/api/productionManagement/processRouteFile.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 闄勪欢鍒楄〃
+export function listProcessRouteFiles(query) {
+ return request({
+ url: "/technologyRoutingFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板闄勪欢
+export function addProcessRouteFile(data) {
+ return request({
+ url: "/technologyRoutingFile/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎闄勪欢
+export function delProcessRouteFile(ids) {
+ return request({
+ url: "/technologyRoutingFile/del",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/api/productionManagement/processRouteItem.js b/src/api/productionManagement/processRouteItem.js
new file mode 100644
index 0000000..3bf4a6e
--- /dev/null
+++ b/src/api/productionManagement/processRouteItem.js
@@ -0,0 +1,92 @@
+// 宸ヨ壓璺嚎椤圭洰椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒楄〃鏌ヨ
+export function findProcessRouteItemList(query) {
+ return request({
+ url: "/technologyRoutingOperation/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function addOrUpdateProcessRouteItem(data) {
+ return request({
+ url: "/technologyRoutingOperation/add",
+ method: "post",
+ data: data,
+ });
+}
+export function addOrUpdateProcessRouteItem1(data) {
+ return request({
+ url: "/technologyRoutingOperation",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鎺掑簭鎺ュ彛
+export function sortProcessRouteItem(data) {
+ return request({
+ url: "/technologyRoutingOperation/sort",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鎵归噺鍒犻櫎鎺ュ彛
+export function batchDeleteProcessRouteItem(ids) {
+ // 灏唅d鏁扮粍杞崲涓洪�楀彿鍒嗛殧鐨勫瓧绗︿覆锛屾嫾鎺ュ埌URL鍚庨潰
+ const idsStr = Array.isArray(ids) ? ids.join(",") : ids;
+ return request({
+ url: `/technologyRoutingOperation/${idsStr}`,
+ method: "delete",
+ });
+}
+// 鑾峰彇宸ュ簭鍙傛暟鍒楄〃
+export function getProcessParamList(query) {
+ return request({
+ url: `/technologyRoutingOperationParam/list`,
+ method: "get",
+ params: query,
+ });
+}
+// 宸ヨ壓璺嚎鍙傛暟鏂板
+export function addProcessRouteItemParam(data) {
+ return request({
+ url: "/technologyRoutingOperationParam/add",
+ method: "post",
+ data: data,
+ });
+}
+// 宸ヨ壓璺嚎鍙傛暟淇敼
+export function editProcessRouteItemParam(data) {
+ return request({
+ url: "/technologyRoutingOperationParam",
+ method: "put",
+ data: data,
+ });
+}
+// 宸ヨ壓璺嚎鍙傛暟鍒犻櫎
+export function delProcessRouteItemParam(id) {
+ return request({
+ url: `/technologyRoutingOperationParam/${id}`,
+ method: "delete",
+ });
+}
+// 鎸夊伐鑹鸿矾绾垮伐搴忓悓姝ュ伐搴忓弬鏁�
+export function syncProcessParamItem(data) {
+ return request({
+ url: "/technologyRoutingOperationParam/sync",
+ method: "post",
+ data: data,
+ });
+}
+// 鎸夊伐鑹鸿矾绾垮伐搴忓悓姝ュ伐搴忓弬鏁�-鐢熶骇璁㈠崟
+export function syncProcessParamItemOrder(data) {
+ return request({
+ url: "/productionOrderRoutingOperationParam/sync",
+ method: "post",
+ data: data,
+ });
+}
diff --git a/src/api/productionManagement/productBom.js b/src/api/productionManagement/productBom.js
new file mode 100644
index 0000000..517208b
--- /dev/null
+++ b/src/api/productionManagement/productBom.js
@@ -0,0 +1,74 @@
+// 浜у搧BOM椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/technologyBom/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板
+export function add(data) {
+ return request({
+ url: "/technologyBom/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 澶嶅埗
+export function copy(data) {
+ return request({
+ url: "/technologyBom/copy",
+ method: "post",
+ data: data,
+ });
+}
+// 淇敼
+export function update(data) {
+ return request({
+ url: "/technologyBom/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 鎵归噺鍒犻櫎
+export function batchDelete(ids) {
+ return request({
+ url: "/technologyBom/batchDelete",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鏍规嵁浜у搧鍨嬪彿ID鏌ヨBOM
+export function getByModel(productModelId) {
+ return request({
+ url: "/technologyBom/getByModel",
+ method: "get",
+ params: { productModelId },
+ });
+}
+
+// 瀵煎嚭BOM
+export function exportBom(bomId) {
+ return request({
+ url: "/technologyBom/exportBom",
+ method: "post",
+ params: { bomId },
+ responseType: "blob",
+ });
+}
+
+// 涓嬭浇妯℃澘
+export function downloadTemplate() {
+ return request({
+ url: "/technologyBom/downloadTemplate",
+ method: "get",
+ responseType: "blob",
+ });
+}
diff --git a/src/api/productionManagement/productProcessRoute.js b/src/api/productionManagement/productProcessRoute.js
new file mode 100644
index 0000000..d75c239
--- /dev/null
+++ b/src/api/productionManagement/productProcessRoute.js
@@ -0,0 +1,85 @@
+// 宸ヨ壓璺嚎椤圭洰椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒楄〃鏌ヨ
+export function findProductProcessRouteItemList(query) {
+ return request({
+ url: "/productionOrderRouting/list",
+ method: "get",
+ params: query,
+ });
+}
+
+export function addOrUpdateProductProcessRouteItem(data) {
+ return request({
+ url: "/productionOrderRouting/updateRouteItem",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鐢熶骇璁㈠崟涓嬶細鏂板宸ヨ壓璺嚎椤圭洰
+export function addRouteItem(data) {
+ return request({
+ url: "/productionOrderRouting/addRouteItem",
+ method: "post",
+ data,
+ });
+}
+
+// 鑾峰彇鐢熶骇璁㈠崟鍏宠仈鐨勫伐鑹鸿矾绾夸富淇℃伅
+export function listMain(orderId) {
+ return request({
+ url: "/productionOrderRouting/listMain",
+ method: "get",
+ params: { orderId },
+ });
+}
+
+// 鍒犻櫎宸ヨ壓璺嚎椤圭洰锛堣矾鐢卞悗鎷兼帴 id锛�
+export function deleteRouteItem(id) {
+ return request({
+ url: `/productionOrderRouting/deleteRouteItem/${id}`,
+ method: "delete",
+ });
+}
+
+// 鐢熶骇璁㈠崟涓嬶細鎺掑簭宸ヨ壓璺嚎椤圭洰
+export function sortRouteItem(data) {
+ return request({
+ url: "/productionOrderRouting/sortRouteItem",
+ method: "post",
+ data,
+ });
+}
+// 鑾峰彇宸ュ簭鍙傛暟鍒楄〃-鐢熶骇璁㈠崟
+export function findProcessParamListOrder(query) {
+ return request({
+ url: `/productionOrderRoutingOperationParam/list`,
+ method: "get",
+ params: query,
+ });
+}
+// 宸ヨ壓璺嚎鍙傛暟鏂板-鐢熶骇璁㈠崟
+export function addProcessRouteItemParamOrder(data) {
+ return request({
+ url: "/productionOrderRoutingOperationParam",
+ method: "post",
+ data: data,
+ });
+}
+// 宸ヨ壓璺嚎鍙傛暟淇敼-鐢熶骇璁㈠崟
+export function editProcessRouteItemParamOrder(data) {
+ return request({
+ url: "/productionOrderRoutingOperationParam",
+ method: "put",
+ data: data,
+ });
+}
+// 宸ヨ壓璺嚎鍙傛暟鍒犻櫎-鐢熶骇璁㈠崟
+export function delProcessRouteItemParamOrder(id) {
+ return request({
+ url: `/productionOrderRoutingOperationParam/${id}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/productionManagement/productStructure.js b/src/api/productionManagement/productStructure.js
new file mode 100644
index 0000000..c55c93e
--- /dev/null
+++ b/src/api/productionManagement/productStructure.js
@@ -0,0 +1,47 @@
+// 浜у搧缁撴瀯椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function queryList(id) {
+ return request({
+ url: "/technologyBomStructure/listByBomId/" + id,
+ method: "get",
+ });
+}
+// 鍒嗛〉鏌ヨ-浜у搧璁㈠崟
+export function queryList2(id) {
+ return request({
+ url: "/productionBomStructure/listByBomId/" + id,
+ method: "get",
+ });
+}
+export function add(data) {
+ return request({
+ url: "/productStructure/" + data.bomId,
+ method: "post",
+ data: data.children,
+ });
+}
+
+export function addBomDetail(data) {
+ return request({
+ url: "/technologyBomStructure",
+ method: "post",
+ data: data,
+ });
+}
+// 鍒嗛〉鏌ヨ-浜у搧璁㈠崟
+// export function queryList2(id) {
+// return request({
+// url: "/productionOrderStructure/getBomStructs/" + id,
+// method: "get",
+// });
+// }
+
+export function add2(data) {
+ return request({
+ url: "/productionBomStructure/addOrUpdateBomStructs",
+ method: "post",
+ data: data,
+ });
+}
diff --git a/src/api/productionManagement/productWorkOrderFile.js b/src/api/productionManagement/productWorkOrderFile.js
new file mode 100644
index 0000000..9fc04a9
--- /dev/null
+++ b/src/api/productionManagement/productWorkOrderFile.js
@@ -0,0 +1,29 @@
+import request from "@/utils/request";
+
+// 鏌ヨ宸ュ崟闄勪欢鍒楄〃
+export function productWorkOrderFileListPage(query) {
+ return request({
+ url: "/productWorkOrderFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板宸ュ崟闄勪欢
+export function productWorkOrderFileAdd(data) {
+ return request({
+ url: "/productWorkOrderFile/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎宸ュ崟闄勪欢
+export function productWorkOrderFileDel(data) {
+ return request({
+ url: "/productWorkOrderFile/del",
+ method: "delete",
+ data,
+ });
+}
+
diff --git a/src/api/productionManagement/productionCosting.js b/src/api/productionManagement/productionCosting.js
new file mode 100644
index 0000000..81e6b1b
--- /dev/null
+++ b/src/api/productionManagement/productionCosting.js
@@ -0,0 +1,31 @@
+// 鐢熶骇鏍哥畻椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function productionAccountingListPage(query) {
+ return request({
+ url: "/salesLedger/productionAccounting/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 宸﹁竟琛ㄦ牸鐨勬帴鍙�
+// salesLedger/productionAccounting/page
+export function salesLedgerProductionAccountingList(query) {
+ return request({
+ url: "/productionAccount/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍙宠竟琛ㄦ牸鐨勬帴鍙�
+//
+export function salesLedgerProductionAccountingListProductionDetails(query) {
+ return request({
+ url: "/productionAccount/listProductionDetails",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionOrder.js b/src/api/productionManagement/productionOrder.js
new file mode 100644
index 0000000..b84a188
--- /dev/null
+++ b/src/api/productionManagement/productionOrder.js
@@ -0,0 +1,228 @@
+// 鐢熶骇璁㈠崟椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function schedulingListPage(query) {
+ return request({
+ url: "/salesLedger/scheduling/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function productOrderListPage(query) {
+ return request({
+ url: "/productionOrder/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鐢熶骇璁㈠崟-鎸変骇鍝佸瀷鍙锋煡璇㈠彲鐢ㄥ伐鑹鸿矾绾垮垪琛�
+export function listProcessRoute(query) {
+ return request({
+ url: "/productOrder/listProcessRoute",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鐢熶骇璁㈠崟-缁戝畾宸ヨ壓璺嚎
+export function bindingRoute(data) {
+ return request({
+ url: "/productionOrder/bindingRoute",
+ method: "post",
+ data,
+ });
+}
+
+// 鐢熶骇璁㈠崟-鏂板
+export function addProductOrder(data) {
+ return request({
+ url: "/productionOrder/addOrder",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鐢熶骇璁㈠崟-淇敼
+export function updateProductOrder(data) {
+ return request({
+ url: "/productionOrder/updateOrder",
+ method: "post",
+ data: data,
+ });
+}
+
+export function delProductOrder(ids) {
+ return request({
+ url: `/productionOrder/delete`,
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鐢熶骇璁㈠崟-鏌ヨ浜у搧缁撴瀯鍒楄〃
+export function listProcessBom(query) {
+ return request({
+ url: "/productOrder/listProcessBom",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鐢熶骇璁㈠崟-棰嗘枡鍙拌处鍒楄〃
+export function listMaterialPickingLedger(query) {
+ return request({
+ url: "/productOrderMaterial/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鐢熶骇璁㈠崟-淇濆瓨棰嗘枡鍙拌处
+// export function saveMaterialPickingLedger(data) {
+// return request({
+// url: "/productOrderMaterial/save",
+// method: "post",
+// data,
+// });
+// }
+export function saveMaterialPickingLedger(data) {
+ return request({
+ url: "/productionOrderPick/savePick",
+ method: "post",
+ data,
+ });
+}
+export function updateMaterialPickingLedger(data) {
+ return request({
+ url: "/productionOrderPick/updatePick",
+ method: "post",
+ data,
+ });
+}
+
+// 鐢熶骇璁㈠崟婧簮璇︽儏
+export function getOrderDetail(npsNo) {
+ return request({
+ url: "/productionOrder/ordeDetail",
+ method: "get",
+ params: { npsNo },
+ });
+}
+// 鐢熶骇璁㈠崟-棰嗘枡璇︽儏鍒楄〃
+// export function listMaterialPickingDetail(query) {
+// return request({
+// url: "/productOrderMaterial/detailList",
+// method: "get",
+// params: query,
+// });
+// }
+export function listMaterialPickingBom(productionOrderId) {
+ return request({
+ url: "/productionOrder/pick/" + productionOrderId,
+ method: "get",
+ });
+}
+export function listMaterialPickingDetail(productionOrderId) {
+ return request({
+ url: "/productionOrderPick/detail/" + productionOrderId,
+ method: "get",
+ });
+}
+// 鐢熶骇璁㈠崟-琛ユ枡璁板綍鍒楄〃
+export function listMaterialSupplementRecord(query) {
+ return request({
+ url: "/productionOrderPickRecord/feeding",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鐢熶骇璁㈠崟-鑾峰彇鏉ユ簮鏁版嵁
+export function getProductOrderSource(id) {
+ return request({
+ url: `/productionOrder/source/${id}`,
+ method: "get",
+ });
+}
+
+// 鐢熶骇璁㈠崟-閫�鏂欑‘璁�
+export function confirmMaterialReturn(data) {
+ return request({
+ url: "/productOrderMaterial/confirmReturn",
+ method: "post",
+ data,
+ });
+}
+
+// 鑾峰彇鐐掓満姝e湪宸ヤ綔閲忔暟鎹�
+export function schedulingList(query) {
+ return request({
+ url: "/salesLedger/scheduling/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 淇濆瓨鐐掓満璁剧疆
+export function addSpeculatTrading(data) {
+ return request({
+ url: "/salesLedger/scheduling/addSpeculatTrading",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鐐掓満璁剧疆
+export function updateSpeculatTrading(data) {
+ return request({
+ url: "/salesLedger/scheduling/updateSpeculatTrading",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鐢熶骇娲惧伐
+export function productionDispatch(query) {
+ return request({
+ url: "/salesLedger/scheduling/productionDispatch",
+ method: "post",
+ data: query,
+ });
+}
+// 鑷姩娲惧伐
+export function productionDispatchList(query) {
+ return request({
+ url: "/salesLedger/scheduling/productionDispatchList",
+ method: "post",
+ data: query,
+ });
+}
+
+// 鏌ヨ鎹熻�楃巼
+export function getLossRate() {
+ return request({
+ url: "/salesLedger/scheduling/loss",
+ method: "get",
+ });
+}
+
+// 鏂板鎹熻�楃巼
+export function addLossRate(data) {
+ return request({
+ url: "/salesLedger/scheduling/addLoss",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鎹熻�楃巼
+export function updateLossRate(data) {
+ return request({
+ url: "/salesLedger/scheduling/updateLoss",
+ method: "post",
+ data: data,
+ });
+}
diff --git a/src/api/productionManagement/productionProcess.js b/src/api/productionManagement/productionProcess.js
new file mode 100644
index 0000000..783f584
--- /dev/null
+++ b/src/api/productionManagement/productionProcess.js
@@ -0,0 +1,104 @@
+// 宸ュ簭椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/technologyOperation/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function processList(query) {
+ return request({
+ url: "/technologyOperation/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 宸ュ簭鏌ヨ
+export function list(query) {
+ return request({
+ url: "/technologyOperation/listPage",
+ method: "get",
+ params: query,
+ });
+}
+export function add(data) {
+ return request({
+ url: "/technologyOperation/add",
+ method: "post",
+ data: data,
+ });
+}
+
+export function del(data) {
+ return request({
+ url: "/technologyOperation/batchDelete",
+ method: "delete",
+ data: data,
+ });
+}
+
+export function update(data) {
+ return request({
+ url: "/technologyOperation/update",
+ method: "put",
+ data: data,
+ });
+}
+
+// 瀵煎叆鏁版嵁
+export function importData(data) {
+ return request({
+ url: "/technologyOperation/importData",
+ method: "post",
+ data: data,
+ });
+}
+
+// 涓嬭浇妯℃澘
+export function downloadTemplate() {
+ return request({
+ url: "/technologyOperation/downloadTemplate",
+ method: "post",
+ responseType: "blob",
+ });
+}
+
+// 鑾峰彇宸ュ簭鍙傛暟鍒楄〃
+export function getProcessParamList(params) {
+ return request({
+ url: `/technologyOperationParam/list`,
+ method: "get",
+ params,
+ });
+}
+
+// 娣诲姞宸ュ簭鍙傛暟
+export function addProcessParam(data) {
+ return request({
+ url: "/technologyOperationParam/",
+ method: "post",
+ data: data,
+ });
+}
+
+// 缂栬緫宸ュ簭鍙傛暟
+export function editProcessParam(data) {
+ return request({
+ url: "/technologyOperationParam/",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎宸ュ簭鍙傛暟
+export function deleteProcessParam(id) {
+ return request({
+ url: `/technologyOperationParam/batchDelete/${id}`,
+ method: "delete",
+ });
+}
diff --git a/src/api/productionManagement/productionProductInput.js b/src/api/productionManagement/productionProductInput.js
new file mode 100644
index 0000000..f72cd9b
--- /dev/null
+++ b/src/api/productionManagement/productionProductInput.js
@@ -0,0 +1,11 @@
+// 鐢熶骇鎶曞叆椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function productionProductInputListPage(query) {
+ return request({
+ url: "/productionProductInput/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionProductMain.js b/src/api/productionManagement/productionProductMain.js
new file mode 100644
index 0000000..0493f8b
--- /dev/null
+++ b/src/api/productionManagement/productionProductMain.js
@@ -0,0 +1,11 @@
+// 鐢熶骇鎶ュ伐椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function productionProductMainListPage(query) {
+ return request({
+ url: "/productionProductMain/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionProductOutput.js b/src/api/productionManagement/productionProductOutput.js
new file mode 100644
index 0000000..10095e9
--- /dev/null
+++ b/src/api/productionManagement/productionProductOutput.js
@@ -0,0 +1,11 @@
+// 鐢熶骇浜у嚭椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function productionProductOutputListPage(query) {
+ return request({
+ url: "/productionProductOutput/listPage",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionManagement/productionReporting.js b/src/api/productionManagement/productionReporting.js
new file mode 100644
index 0000000..3e29943
--- /dev/null
+++ b/src/api/productionManagement/productionReporting.js
@@ -0,0 +1,43 @@
+// 鐢熶骇鎶ュ伐椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function workListPage(query) {
+ return request({
+ url: "/salesLedger/work/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 瀛愯〃鏍兼煡璇�
+export function workListPageById(query) {
+ return request({
+ url: "/salesLedger/work/list",
+ method: "get",
+ params: query,
+ });
+}
+// 鐢熶骇鎶ュ伐
+export function productionReport(query) {
+ return request({
+ url: "/salesLedger/work/productionReport",
+ method: "post",
+ data: query,
+ });
+}
+// 鐢熶骇鎶ュ伐-缂栬緫
+export function productionReportUpdate(query) {
+ return request({
+ url: "/salesLedger/work/productionReportUpdate",
+ method: "post",
+ data: query,
+ });
+}
+// 鐢熶骇鎶ュ伐-鍒犻櫎
+export function productionReportDelete(query) {
+ return request({
+ url: "/productionProductMain/delete",
+ method: "delete",
+ data: query,
+ });
+}
diff --git a/src/api/productionManagement/workOrder.js b/src/api/productionManagement/workOrder.js
new file mode 100644
index 0000000..b3050c9
--- /dev/null
+++ b/src/api/productionManagement/workOrder.js
@@ -0,0 +1,97 @@
+import request from "@/utils/request";
+
+export function productWorkOrderPage(query) {
+ return request({
+ url: "/productionOperationTask/page",
+ method: "get",
+ params: query,
+ });
+}
+
+export function updateProductWorkOrder(data) {
+ return request({
+ url: "/productionOperationTask/updateProductWorkOrder",
+ method: "post",
+ data: data,
+ });
+}
+
+export function addProductMain(data) {
+ return request({
+ url: "/productionProductMain/addProductMain",
+ method: "post",
+ data: data,
+ });
+}
+
+export function assignProductWorkOrder(data) {
+ return request({
+ url: "/productionOperationTask/assign",
+ method: "post",
+ data: data,
+ });
+}
+
+// 涓嬭浇宸ュ崟娴佽浆鍗★紙杩斿洖鏂囦欢娴侊級
+export function downProductWorkOrder(id) {
+ return request({
+ url: "/productionOperationTask/down",
+ method: "post",
+ data: { id },
+ responseType: "blob",
+ });
+}
+
+// 宸ュ崟-褰撳墠宸ュ簭鐗╂枡鍙拌处
+export function listWorkOrderMaterialLedger(query) {
+ return request({
+ url: "/productOrderMaterial/reportMaterials",
+ method: "get",
+ params: query,
+ });
+}
+
+// 宸ュ崟-琛ユ枡
+export function addWorkOrderMaterialSupplement(data) {
+ return request({
+ url: "/productionOperationTask/material/supplement",
+ method: "post",
+ data,
+ });
+}
+
+// 宸ュ崟-閫�鏂�
+export function addWorkOrderMaterialReturn(data) {
+ return request({
+ url: "/productionOperationTask/material/return",
+ method: "post",
+ data,
+ });
+}
+
+// 宸ュ崟-琛ユ枡璁板綍
+export function listWorkOrderMaterialSupplementRecord(query) {
+ return request({
+ url: "/productionOperationTask/material/supplementRecord",
+ method: "get",
+ params: query,
+ });
+}
+
+// 宸ュ崟-棰嗙敤锛堟彁浜ゅ疄闄呴鐢ㄦ暟閲忥級
+export function pickWorkOrderMaterial(data) {
+ return request({
+ url: "/productionOperationTask/material/pick",
+ method: "post",
+ data,
+ });
+}
+
+// 鑾峰彇宸ュ簭缁熻鏁版嵁
+export function getOperationStatistics(query) {
+ return request({
+ url: "/productionOperationTask/getOperation",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/productionPlan/productionPlan.js b/src/api/productionPlan/productionPlan.js
new file mode 100644
index 0000000..5bcff27
--- /dev/null
+++ b/src/api/productionPlan/productionPlan.js
@@ -0,0 +1,79 @@
+// 鐢熶骇璁㈠崟椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+export function productionPlanListPage(query) {
+ return request({
+ url: "/productionPlan/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鎷夊彇鏁版嵁
+export function loadProdData(query) {
+ return request({
+ url: "/productionPlan/loadProdData",
+ method: "get",
+ params: query,
+ });
+}
+
+export function summaryByProductType(query) {
+ return request({
+ url: "/productionPlan/summaryByProductType",
+ method: "get",
+ params: query,
+ });
+}
+
+// 瀵煎嚭鐢熶骇璁″垝
+export function exportProductionPlan(bomId) {
+ return request({
+ url: "/productionPlan/export",
+ method: "post",
+ params: { bomId },
+ responseType: "blob",
+ });
+}
+
+// 鐢熶骇璁″垝-鏂板淇敼
+export function productionPlanAdd(query) {
+ return request({
+ url: "/productionPlan/addProductionPlan",
+ method: "post",
+ data: query,
+ });
+}
+export function productionPlanUpdate(query) {
+ return request({
+ url: "/productionPlan/updateProductionPlan",
+ method: "put",
+ data: query,
+ });
+}
+
+// 鐢熶骇璁″垝-鍒犻櫎
+export function productionPlanDelete(data) {
+ return request({
+ url: "/productionPlan/deleteProductionPlan",
+ method: "delete",
+ data,
+ });
+}
+// 鍚堝苟涓嬪彂
+export function productionPlanCombine(query) {
+ return request({
+ url: "/productionPlan/combine",
+ method: "post",
+ data: query,
+ });
+}
+
+// 杩借釜杩涘害
+export function trackProgressByNo(query) {
+ return request({
+ url: "/track/trackProgressByNo",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/projectManagement/project.js b/src/api/projectManagement/project.js
new file mode 100644
index 0000000..2d6eff1
--- /dev/null
+++ b/src/api/projectManagement/project.js
@@ -0,0 +1,118 @@
+import request from '@/utils/request'
+
+export function listProject(data) {
+ return request({
+ url: '/projectManagement/info/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+export function getProject(id) {
+ return request({
+ url: `/projectManagement/info/${id}`,
+ method: 'post'
+ })
+}
+
+export function addProject(data) {
+ return request({
+ url: '/projectManagement/info/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function updateProject(data) {
+ return request({
+ url: '/projectManagement/info/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function delProject(ids) {
+ return request({
+ url: '/projectManagement/info/remove',
+ method: 'delete',
+ data: ids
+ })
+}
+
+export function updateStatus(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: data
+ })
+}
+
+export function submitProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 0 }
+ })
+}
+
+export function auditProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 1 }
+ })
+}
+
+export function reverseAuditProject(data) {
+ return request({
+ url: '/projectManagement/info/updateStatus',
+ method: 'post',
+ data: { ...data, reviewStatus: 0 }
+ })
+}
+
+// 闃舵
+export function saveStage(data) {
+ return request({
+ url: '/projectManagement/info/saveStage',
+ method: 'post',
+ data: data
+ })
+}
+
+export function listStage(projectId) {
+ return request({
+ url: `/projectManagement/info/listStage/${projectId}`,
+ method: 'post'
+ })
+}
+
+export function deleteStage(stageId) {
+ return request({
+ url: `/projectManagement/info/deleteStage/${stageId}`,
+ method: 'post'
+ })
+}
+
+export function listPlan(data) {
+ return request({
+ url: '/projectManagement/plan/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+export function addPlan(data) {
+ return request({
+ url: '/projectManagement/plan/save',
+ method: 'post',
+ data: data
+ })
+}
+
+export function delPlan(id) {
+ return request({
+ url: `/projectManagement/plan/delete/${id}`,
+ method: 'post'
+ })
+}
diff --git a/src/api/projectManagement/projectType.js b/src/api/projectManagement/projectType.js
new file mode 100644
index 0000000..8777e59
--- /dev/null
+++ b/src/api/projectManagement/projectType.js
@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+// 鏌ヨ椤圭洰绫诲瀷鍒楄〃
+export function listPlan(data) {
+ return request({
+ url: '/projectManagement/plan/listPage',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇濆瓨椤圭洰绫诲瀷锛堟柊澧�/淇敼锛�
+export function savePlan(data) {
+ return request({
+ url: '/projectManagement/plan/save',
+ method: 'post',
+ data: data
+ })
+}
+
+// 鍒犻櫎椤圭洰绫诲瀷
+export function deletePlan(id) {
+ return request({
+ url: `/projectManagement/plan/delete/${id}`,
+ method: 'post'
+ })
+}
diff --git a/src/api/projectManagement/role.js b/src/api/projectManagement/role.js
new file mode 100644
index 0000000..8d97a2a
--- /dev/null
+++ b/src/api/projectManagement/role.js
@@ -0,0 +1,35 @@
+import request from "@/utils/request";
+// 椤圭洰瑙掕壊
+
+// 鍒嗛〉鏌ヨ
+export function findRoleListPage(query) {
+ return request({
+ url: "/projectManagement/roles/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+export function createRole(params) {
+ return request({
+ url: "/projectManagement/roles/add",
+ method: "post",
+ data: params,
+ });
+}
+
+export function updateRole(params) {
+ return request({
+ url: "/projectManagement/roles/update",
+ method: "post",
+ data: params,
+ });
+}
+
+export function deleteRoles(params) {
+ return request({
+ url: "/projectManagement/roles/delete",
+ method: "delete",
+ data: params,
+ });
+}
\ No newline at end of file
diff --git a/src/api/publicApi/commonFile.js b/src/api/publicApi/commonFile.js
new file mode 100644
index 0000000..5157304
--- /dev/null
+++ b/src/api/publicApi/commonFile.js
@@ -0,0 +1,19 @@
+// 鍏叡鏂囦欢绠$悊鎺ュ彛
+import request from '@/utils/request'
+
+// 鍒犻櫎鍏叡鏂囦欢
+export function delCommonFile(ids) {
+ return request({
+ url: '/commonFile/delCommonFile',
+ method: 'delete',
+ data: ids
+ })
+}
+// 寮�绁ㄥ彴璐︽枃浠跺垹闄�
+export function delCommonFileInvoiceLedger(ids) {
+ return request({
+ url: '/invoiceLedger/delFile',
+ method: 'delete',
+ data: ids
+ })
+}
\ No newline at end of file
diff --git a/src/api/publicApi/index.js b/src/api/publicApi/index.js
new file mode 100644
index 0000000..8022592
--- /dev/null
+++ b/src/api/publicApi/index.js
@@ -0,0 +1,42 @@
+// 鏂囨。绠$悊
+import request from '@/utils/request'
+
+
+// /system/user/listAll
+// 鏌ヨ鎵�鏈夌敤鎴峰垪琛�
+export function userListAll() {
+ return request({
+ url: '/system/user/listAll',
+ method: 'get'
+ })
+}
+
+// /equipmentManagement/equipmentList
+// 鏌ヨ璁惧鍒楄〃
+export function getEquipmentList(query) {
+ return request({
+ url: '/equipmentManagement/equipmentList',
+ method: 'get',
+ params: query
+ })
+}
+
+// /coalInfo/coalInfoList
+// 鏌ヨ鐓ょ鍒楄〃
+export function getCoalInfoList(query) {
+ return request({
+ url: '/coalInfo/coalInfoList',
+ method: 'get',
+ params: query
+ })
+}
+
+// /coalField/coalFieldList
+// 鏌ヨ鐓よ川瀛楁鍒楄〃
+export function getCoalFieldList(query) {
+ return request({
+ url: '/coalField/coalFieldList',
+ method: 'get',
+ params: query
+ })
+}
\ No newline at end of file
diff --git a/src/api/qualityManagement/metricMaintenance.js b/src/api/qualityManagement/metricMaintenance.js
new file mode 100644
index 0000000..1ee9cad
--- /dev/null
+++ b/src/api/qualityManagement/metricMaintenance.js
@@ -0,0 +1,110 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鎸囨爣鍒楄〃
+export function qualityTestStandardListPage(query) {
+ return request({
+ url: "/qualityTestStandard/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鎸囨爣鍒楄〃
+export function qualityTestStandardAdd(query) {
+ return request({
+ url: "/qualityTestStandard/add",
+ method: "post",
+ data: query,
+ });
+}
+
+// 淇敼鎸囨爣鍒楄〃
+export function qualityTestStandardUpdate(query) {
+ return request({
+ url: "/qualityTestStandard/update",
+ method: "post",
+ data: query,
+ });
+}
+
+// 鍒犻櫎鎸囨爣鍒楄〃
+export function qualityTestStandardDel(query) {
+ return request({
+ url: "/qualityTestStandard/del",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 鍒犻櫎鎸囨爣鍒楄〃
+export function qualityInspectDetailByProductId(params) {
+ return request({
+ url: "/qualityTestStandard/getQualityTestStandardByProductId",
+ method: "get",
+ params: params,
+ });
+}
+
+// 澶嶅埗鏍囧噯鍙傛暟
+export function qualityTestStandardCopyParam(id) {
+ return request({
+ url: "/qualityTestStandard/copyParam",
+ method: "post",
+ data: { id },
+ });
+}
+
+// 鎵归噺瀹℃牳锛堢姸鎬侊細1=閫氳繃/鎵瑰噯锛�2=鎾ら攢锛�
+// 浼犲弬锛歔{ id, state }]
+export function qualityTestStandardAudit(data) {
+ return request({
+ url: "/qualityTestStandard/qualityTestStandardAudit",
+ method: "post",
+ data,
+ });
+}
+
+// 鏍囧噯鍙傛暟锛氬垪琛紙涓嶅垎椤碉級
+export function qualityTestStandardParamList(query) {
+ return request({
+ url: "/qualityTestStandardParam/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏍囧噯鍙傛暟锛氭柊澧�
+export function qualityTestStandardParamAdd(data) {
+ return request({
+ url: "/qualityTestStandardParam/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鏍囧噯鍙傛暟锛氫慨鏀�
+export function qualityTestStandardParamUpdate(data) {
+ return request({
+ url: "/qualityTestStandardParam/update",
+ method: "post",
+ data,
+ });
+}
+
+// 鏍囧噯鍙傛暟锛氬垹闄わ紙浼� id 鏁扮粍锛�
+export function qualityTestStandardParamDel(ids) {
+ return request({
+ url: "/qualityTestStandardParam/del",
+ method: "delete",
+ data: ids,
+ });
+}
+
+// 鏍规嵁鏍囧噯ID鑾峰彇鏍囧噯鍙傛暟
+export function getQualityTestStandardParamByTestStandardId(testStandardId) {
+ return request({
+ url: "/qualityTestStandard/getQualityTestStandardParamByTestStandardId",
+ method: "get",
+ params: { testStandardId },
+ });
+}
diff --git a/src/api/qualityManagement/nearExpiryReturn.js b/src/api/qualityManagement/nearExpiryReturn.js
new file mode 100644
index 0000000..4b70de5
--- /dev/null
+++ b/src/api/qualityManagement/nearExpiryReturn.js
@@ -0,0 +1,46 @@
+import request from '@/utils/request'
+
+// 鏌ヨ涓存湡閫�鍥炲彴璐﹀垪琛�
+export function nearExpiryReturnListPage(query) {
+ return request({
+ url: '/quality/nearExpiryReturn/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鏂板涓存湡閫�鍥炲彴璐�
+export function nearExpiryReturnAdd(data) {
+ return request({
+ url: '/quality/nearExpiryReturn/add',
+ method: 'post',
+ data: data,
+ })
+}
+
+// 淇敼涓存湡閫�鍥炲彴璐�
+export function nearExpiryReturnUpdate(data) {
+ return request({
+ url: '/quality/nearExpiryReturn/update',
+ method: 'post',
+ data: data,
+ })
+}
+
+// 鍒犻櫎涓存湡閫�鍥炲彴璐�
+export function nearExpiryReturnDel(ids) {
+ return request({
+ url: '/quality/nearExpiryReturn/del',
+ method: 'delete',
+ data: ids,
+ })
+}
+
+// 鑾峰彇涓存湡閫�鍥炲彴璐﹁鎯�
+export function nearExpiryReturnDetail(id) {
+ return request({
+ url: '/quality/nearExpiryReturn/' + id,
+ method: 'get',
+ })
+}
+
diff --git a/src/api/qualityManagement/nonconformingManagement.js b/src/api/qualityManagement/nonconformingManagement.js
new file mode 100644
index 0000000..50a1b74
--- /dev/null
+++ b/src/api/qualityManagement/nonconformingManagement.js
@@ -0,0 +1,50 @@
+import request from "@/utils/request";
+
+// 鏌ヨ涓嶅悎鏍肩鐞嗗垪琛�
+export function qualityUnqualifiedListPage(query) {
+ return request({
+ url: "/quality/qualityUnqualified/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板涓嶅悎鏍肩鐞嗗垪琛�
+export function qualityUnqualifiedAdd(query) {
+ return request({
+ url: "/quality/qualityUnqualified/add",
+ method: "post",
+ data: query,
+ });
+}
+// 淇敼涓嶅悎鏍肩鐞嗗垪琛�
+export function qualityUnqualifiedUpdate(query) {
+ return request({
+ url: "/quality/qualityUnqualified/update",
+ method: "post",
+ data: query,
+ });
+}
+// 涓嶅悎鏍煎鐞�
+export function qualityUnqualifiedDeal(query) {
+ return request({
+ url: "/quality/qualityUnqualified/deal",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎涓嶅悎鏍肩鐞嗗垪琛�
+export function qualityUnqualifiedDel(query) {
+ return request({
+ url: "/quality/qualityUnqualified/del",
+ method: "delete",
+ data: query,
+ });
+}
+// 鏌ヨ涓嶅悎鏍肩鐞嗕俊鎭�
+export function getQualityUnqualifiedInfo(query) {
+ return request({
+ url: "/quality/qualityUnqualified/" + query,
+ method: "get",
+ data: query,
+ });
+}
diff --git a/src/api/qualityManagement/qualityInspectFile.js b/src/api/qualityManagement/qualityInspectFile.js
new file mode 100644
index 0000000..36b72cb
--- /dev/null
+++ b/src/api/qualityManagement/qualityInspectFile.js
@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 鏌ヨ闄勪欢鍒楄〃
+export function qualityInspectFileListPage(query) {
+ return request({
+ url: '/quality/qualityInspectFile/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+// 淇濆瓨闄勪欢鍒楄〃
+export function qualityInspectFileAdd(query) {
+ return request({
+ url: '/quality/qualityInspectFile/add',
+ method: 'post',
+ data: query,
+ })
+}
+// 鍒犻櫎闄勪欢鍒楄〃
+export function qualityInspectFileDel(query) {
+ return request({
+ url: '/quality/qualityInspectFile/del',
+ method: 'delete',
+ data: query,
+ })
+}
\ No newline at end of file
diff --git a/src/api/qualityManagement/qualityInspectParam.js b/src/api/qualityManagement/qualityInspectParam.js
new file mode 100644
index 0000000..4618820
--- /dev/null
+++ b/src/api/qualityManagement/qualityInspectParam.js
@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+// 鏌ヨ妫�楠屾寚鏍�
+export function qualityInspectParamInfo(query) {
+ return request({
+ url: '/quality/qualityInspectParam/' + query,
+ method: 'get',
+ data: query,
+ })
+}
+// 鎻愪氦妫�楠�
+export function qualityInspectParamUpdate(query) {
+ return request({
+ url: '/quality/qualityInspectParam/update',
+ method: 'post',
+ data: query,
+ })
+}
+
+// 鍒犻櫎妫�楠岃褰�
+export function qualityInspectParamDel(query) {
+ return request({
+ url: '/quality/qualityInspectParam/del',
+ method: 'delete',
+ data: query,
+ })
+}
\ No newline at end of file
diff --git a/src/api/qualityManagement/qualityTestStandardBinding.js b/src/api/qualityManagement/qualityTestStandardBinding.js
new file mode 100644
index 0000000..e4432a6
--- /dev/null
+++ b/src/api/qualityManagement/qualityTestStandardBinding.js
@@ -0,0 +1,28 @@
+import request from "@/utils/request";
+
+// 缁戝畾鍒楄〃锛堜笉鍒嗛〉锛�
+export function qualityTestStandardBindingList(query) {
+ return request({
+ url: "/qualityTestStandardBinding/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板缁戝畾锛堟敮鎸佹壒閲忥級
+export function qualityTestStandardBindingAdd(data) {
+ return request({
+ url: "/qualityTestStandardBinding/add",
+ method: "post",
+ data,
+ });
+}
+
+// 鍒犻櫎缁戝畾锛堜紶 id 鏁扮粍锛�
+export function qualityTestStandardBindingDel(ids) {
+ return request({
+ url: "/qualityTestStandardBinding/del",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/api/qualityManagement/rawMaterialInspection.js b/src/api/qualityManagement/rawMaterialInspection.js
new file mode 100644
index 0000000..dcb3869
--- /dev/null
+++ b/src/api/qualityManagement/rawMaterialInspection.js
@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍘熸潗鏂欐楠屽垪琛�
+export function qualityInspectListPage(query) {
+ return request({
+ url: '/quality/qualityInspect/listPage',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 鏂板鍘熸潗鏂欐楠�
+export function qualityInspectAdd(query) {
+ return request({
+ url: '/quality/qualityInspect/add',
+ method: 'post',
+ data: query,
+ })
+}
+
+// 淇敼鍘熸潗鏂欐楠�
+export function qualityInspectUpdate(query) {
+ return request({
+ url: '/quality/qualityInspect/update',
+ method: 'post',
+ data: query,
+ })
+}
+
+// 鍒犻櫎鍘熸潗鏂欐楠�
+export function qualityInspectDel(query) {
+ return request({
+ url: '/quality/qualityInspect/del',
+ method: 'delete',
+ data: query,
+ })
+}
+
+// 鎻愪氦鍘熸潗鏂欐楠�
+export function submitQualityInspect(data) {
+ return request({
+ url: '/quality/qualityInspect/submit',
+ method: 'post',
+ data: data,
+ })
+}
+
+// 鎻愪氦鍘熸潗鏂欐楠�
+export function downloadQualityInspect(data) {
+ return request({
+ url: '/quality/qualityInspect/down',
+ method: 'post',
+ data: data,
+ responseType: "blob",
+ })
+}
+
diff --git a/src/api/reportAnalysis/qualityReport.js b/src/api/reportAnalysis/qualityReport.js
new file mode 100644
index 0000000..66a7540
--- /dev/null
+++ b/src/api/reportAnalysis/qualityReport.js
@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 鑾峰彇鍚勭被鍨嬪畬鎴愭暟閲�
+export function getInspectStatistics() {
+ return request({
+ url: '/qualityReport/getInspectStatistics',
+ method: 'get'
+ })
+}
+
+// 鑾峰彇璐ㄦ鍚堟牸鐜囩粺璁�
+export function getPassRateStatistics() {
+ return request({
+ url: '/qualityReport/getPassRateStatistics',
+ method: 'get'
+ })
+}
+
+// 鑾峰彇鏈堝害鍚堟牸鐜囩粺璁℃暟鎹�
+export function getMonthlyPassRateStatistics(year) {
+ return request({
+ url: '/qualityReport/getMonthlyPassRateStatistics',
+ method: 'get',
+ params: { year }
+ })
+}
+
+// 鑾峰彇骞村害鎬诲悎鏍肩巼缁熻鏁版嵁
+export function getYearlyPassRateStatistics(year) {
+ return request({
+ url: '/qualityReport/getYearlyPassRateStatistics',
+ method: 'get',
+ params: { year }
+ })
+}
+// 鑾峰彇鏈堝害瀹屾垚鏄庣粏鏁版嵁
+export function getMonthlyCompletionDetails(year) {
+ return request({
+ url: '/qualityReport/getMonthlyCompletionDetails',
+ method: 'get',
+ params: { year }
+ })
+}
+
+// 鑾峰彇鐑偣妫�娴嬫寚鏍囩粺璁�
+export function getTopParameters(modelType) {
+ return request({
+ url: '/qualityReport/getTopParameters',
+ method: 'get',
+ params: { modelType }
+ })
+}
diff --git a/src/api/safeProduction/accidentReportingRecord.js b/src/api/safeProduction/accidentReportingRecord.js
new file mode 100644
index 0000000..1314f8d
--- /dev/null
+++ b/src/api/safeProduction/accidentReportingRecord.js
@@ -0,0 +1,36 @@
+
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function safeAccidentListPage(query) {
+ return request({
+ url: "/safeAccident/page",
+ method: "get",
+ params: query,
+ });
+}
+
+
+export function safeAccidentAdd(query) {
+ return request({
+ url: '/safeAccident',
+ method: 'post',
+ data: query
+ })
+}
+
+export function safeAccidentUpdate(query) {
+ return request({
+ url: '/safeAccident',
+ method: 'put',
+ data: query
+ })
+}
+
+export function safeAccidentDel(ids) {
+ return request({
+ url: '/safeAccident/' + ids,
+ method: 'delete',
+ data: ids
+ })
+}
diff --git a/src/api/safeProduction/dangerInvestigation.js b/src/api/safeProduction/dangerInvestigation.js
new file mode 100644
index 0000000..e6ec3d0
--- /dev/null
+++ b/src/api/safeProduction/dangerInvestigation.js
@@ -0,0 +1,61 @@
+// 鍙戣揣鍙拌处椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function dangerInvestigationListPage(query) {
+ return request({
+ url: "/safeHidden/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板瀹夊叏瑙勭▼涓庤祫璐ㄧ鐞�
+export function safeHiddenAdd(query) {
+ return request({
+ url: '/safeHidden',
+ method: 'post',
+ data: query
+ })
+}
+// 淇敼瀹夊叏瑙勭▼涓庤祫璐ㄧ鐞�
+export function safeHiddenUpdate(query) {
+ return request({
+ url: '/safeHidden',
+ method: 'put',
+ data: query
+ })
+}
+// 鍒犻櫎瀹夊叏瑙勭▼涓庤祫璐ㄧ鐞�
+export function safeHiddenDel(ids) {
+ return request({
+ url: '/safeHidden/' + ids,
+ method: 'delete',
+ data: ids
+ })
+}
+
+// 鏌ヨ闄勪欢鍒楄〃
+export function fileListPage(query) {
+ return request({
+ url: "/safeHiddenFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 娣诲姞闄勪欢
+export function safeHiddenFileAdd(query) {
+ return request({
+ url: '/safeHiddenFile/add',
+ method: 'post',
+ data: query
+ })
+}
+// 鍒犻櫎闄勪欢
+export function safeHiddenFileDel(ids) {
+ return request({
+ url: '/safeHiddenFile/del',
+ method: 'delete',
+ data: ids
+ })
+}
\ No newline at end of file
diff --git a/src/api/safeProduction/emergencyPlanReview.js b/src/api/safeProduction/emergencyPlanReview.js
new file mode 100644
index 0000000..7eac73e
--- /dev/null
+++ b/src/api/safeProduction/emergencyPlanReview.js
@@ -0,0 +1,35 @@
+// 鍙戣揣鍙拌处椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function safeContingencyPlanListPage(query) {
+ return request({
+ url: "/safeContingencyPlan/page",
+ method: "get",
+ params: query,
+ });
+}
+
+export function safeContingencyPlanAdd(query) {
+ return request({
+ url: '/safeContingencyPlan',
+ method: 'post',
+ data: query
+ })
+}
+
+export function safeContingencyPlanUpdate(query) {
+ return request({
+ url: '/safeContingencyPlan',
+ method: 'put',
+ data: query
+ })
+}
+
+export function safeContingencyPlanDel(ids) {
+ return request({
+ url: '/safeContingencyPlan/' + ids,
+ method: 'delete',
+ data: ids
+ })
+}
\ No newline at end of file
diff --git a/src/api/safeProduction/hazardSourceLedger.js b/src/api/safeProduction/hazardSourceLedger.js
new file mode 100644
index 0000000..546415c
--- /dev/null
+++ b/src/api/safeProduction/hazardSourceLedger.js
@@ -0,0 +1,36 @@
+// 鍙戣揣鍙拌处椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function safeHazardListPage(query) {
+ return request({
+ url: "/safeHazard/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鍗遍櫓婧愬彴璐�
+export function safeHazardAdd(query) {
+ return request({
+ url: '/safeHazard',
+ method: 'post',
+ data: query
+ })
+}
+// 淇敼鍗遍櫓婧愬彴璐�
+export function safeHazardUpdate(query) {
+ return request({
+ url: '/safeHazard',
+ method: 'put',
+ data: query
+ })
+}
+// 鍒犻櫎鍗遍櫓婧愬彴璐�
+export function safeHazardDel(ids) {
+ return request({
+ url: '/safeHazard/' + ids,
+ method: 'delete',
+ data: ids
+ })
+}
\ No newline at end of file
diff --git a/src/api/safeProduction/hazardousMaterialsControl.js b/src/api/safeProduction/hazardousMaterialsControl.js
new file mode 100644
index 0000000..0f5557f
--- /dev/null
+++ b/src/api/safeProduction/hazardousMaterialsControl.js
@@ -0,0 +1,33 @@
+import request from "@/utils/request";
+
+export function safeHazardRecordListPage(query) {
+ return request({
+ url: "/safeHazardRecord/page",
+ method: "get",
+ params: query,
+ });
+}
+
+export function safeHazardRecordDel(ids) {
+ return request({
+ url: '/safeHazardRecord/' + ids,
+ method: 'delete',
+ data: ids
+ })
+}
+// 鏂板鍗遍櫓婧愬彴璐�
+export function safeHazardRecordAdd(query) {
+ return request({
+ url: '/safeHazardRecord/borrow',
+ method: 'post',
+ data: query
+ })
+}
+
+export function safeHazardRecordUpdate(query) {
+ return request({
+ url: '/safeHazardRecord/return',
+ method: 'put',
+ data: query
+ })
+}
\ No newline at end of file
diff --git a/src/api/safeProduction/safeQualifications.js b/src/api/safeProduction/safeQualifications.js
new file mode 100644
index 0000000..bde2443
--- /dev/null
+++ b/src/api/safeProduction/safeQualifications.js
@@ -0,0 +1,61 @@
+// 鍙戣揣鍙拌处椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function qualificationsListPage(query) {
+ return request({
+ url: "/safeCertification/page",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板瀹夊叏瑙勭▼涓庤祫璐ㄧ鐞�
+export function safeCertificationAdd(query) {
+ return request({
+ url: '/safeCertification',
+ method: 'post',
+ data: query
+ })
+}
+// 淇敼瀹夊叏瑙勭▼涓庤祫璐ㄧ鐞�
+export function safeCertificationUpdate(query) {
+ return request({
+ url: '/safeCertification',
+ method: 'put',
+ data: query
+ })
+}
+// 鍒犻櫎瀹夊叏瑙勭▼涓庤祫璐ㄧ鐞�
+export function safeCertificationDel(ids) {
+ return request({
+ url: '/safeCertification/' + ids,
+ method: 'delete',
+ data: ids
+ })
+}
+
+// 鏌ヨ闄勪欢鍒楄〃
+export function fileListPage(query) {
+ return request({
+ url: "/safeCertificationFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 娣诲姞闄勪欢
+export function safeCertificationFileAdd(query) {
+ return request({
+ url: '/safeCertificationFile/add',
+ method: 'post',
+ data: query
+ })
+}
+// 鍒犻櫎闄勪欢
+export function safeCertificationFileDel(ids) {
+ return request({
+ url: '/safeCertificationFile/del',
+ method: 'delete',
+ data: ids
+ })
+}
\ No newline at end of file
diff --git a/src/api/safeProduction/safetyTrainingAssessment.js b/src/api/safeProduction/safetyTrainingAssessment.js
new file mode 100644
index 0000000..336e17a
--- /dev/null
+++ b/src/api/safeProduction/safetyTrainingAssessment.js
@@ -0,0 +1,112 @@
+
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function safeTrainingListPage(query) {
+ return request({
+ url: "/safeTraining/page",
+ method: "get",
+ params: query,
+ });
+}
+
+
+export function safeTrainingAdd(query) {
+ return request({
+ url: '/safeTraining',
+ method: 'post',
+ data: query
+ })
+}
+
+export function safeTrainingUpdate(query) {
+ return request({
+ url: '/safeTraining',
+ method: 'put',
+ data: query
+ })
+}
+
+export function safeTrainingDel(ids) {
+ return request({
+ url: '/safeTraining/' + ids,
+ method: 'delete',
+ data: ids
+ })
+}
+// 瀵煎嚭
+export function safeTrainingExport(query) {
+ return request({
+ url: '/safeTraining/export',
+ method: 'post',
+ data: query,
+ responseType: 'blob'
+ })
+}
+
+// 鏌ヨ闄勪欢鍒楄〃
+export function safeTrainingFileListPage(query) {
+ return request({
+ url: "/safeTrainingFile/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 娣诲姞闄勪欢
+export function safeTrainingFileAdd(query) {
+ return request({
+ url: '/safeTrainingFile/add',
+ method: 'post',
+ data: query
+ })
+}
+// 鍒犻櫎闄勪欢
+export function safeTrainingFileDel(ids) {
+ return request({
+ url: '/safeTrainingFile/del',
+ method: 'delete',
+ data: ids
+ })
+}
+// 绛惧埌
+export function safeTrainingSign(query) {
+ return request({
+ url: '/safeTraining/sign',
+ method: 'post',
+ data: query
+ })
+}
+// 鏌ヨ璇︽儏
+export function safeTrainingGet(query) {
+ return request({
+ url: '/safeTraining/getSafeTraining',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鎻愪氦
+export function safeTrainingSave(query) {
+ return request({
+ url: '/safeTraining/saveSafeTraining',
+ method: 'post',
+ data: query
+ })
+}
+
+export function safeTrainingDetailListPage(query) {
+ return request({
+ url: "/safeTrainingDetails/page",
+ method: "get",
+ params: query,
+ });
+}
+// 瀵煎嚭
+export function safeTrainingDetailExport(query) {
+ return request({
+ url: '/safeTrainingDetails/export',
+ method: 'post',
+ data: query,
+ responseType: 'blob'
+ })
+}
diff --git a/src/api/salesManagement/deliveryLedger.js b/src/api/salesManagement/deliveryLedger.js
new file mode 100644
index 0000000..bcc47a3
--- /dev/null
+++ b/src/api/salesManagement/deliveryLedger.js
@@ -0,0 +1,61 @@
+// 鍙戣揣鍙拌处椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function deliveryLedgerListPage(query) {
+ return request({
+ url: "/shippingInfo/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 淇敼鍙戣揣鍙拌处
+export function getDeliveryDetail(id) {
+ return request({
+ url: `/shippingInfo/getDateil/${id}`,
+ method: "get",
+ });
+}
+// 淇敼鍙戣揣鍙拌处
+export function getDeliveryDetailByShippingNo(query) {
+ return request({
+ url: "/shippingInfo/getDateilByShippingNo",
+ method: "get",
+ params: query,
+ });
+}
+
+export function addOrUpdateDeliveryLedger(query) {
+ return request({
+ url: "/shippingInfo/update",
+ method: "post",
+ data: query,
+ });
+}
+// 淇敼鍙戣揣鍙拌处
+export function deductStock(query) {
+ return request({
+ url: "/shippingInfo/deductStock",
+ method: "post",
+ data: query,
+ });
+}
+
+// 鍒犻櫎鍙戣揣鍙拌处
+export function delDeliveryLedger(query) {
+ return request({
+ url: "/shippingInfo/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 鏂板鍙戣揣淇℃伅
+export function addShippingInfo(data) {
+ return request({
+ url: "/shippingInfo/add",
+ method: "post",
+ data,
+ });
+}
diff --git a/src/api/salesManagement/indicatorStats.js b/src/api/salesManagement/indicatorStats.js
new file mode 100644
index 0000000..c29eac3
--- /dev/null
+++ b/src/api/salesManagement/indicatorStats.js
@@ -0,0 +1,38 @@
+// 鎸囨爣缁熻椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// 澶撮儴缁熻鎺ュ彛
+export function getTotalStatistics(query) {
+ return request({
+ url: "/metricStatistics/total",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌辩姸鍥炬暟鎹帴鍙�
+export function getStatisticsTable(query) {
+ return request({
+ url: "/metricStatistics/statisticsTable",
+ method: "get",
+ params: query,
+ });
+}
+
+// 瀹㈡埛寰�鏉ュ垪琛�
+export function customewTransactions(query) {
+ return request({
+ url: "/metricStatistics/customewTransactions",
+ method: "get",
+ params: query,
+ });
+}
+
+// 瀹㈡埛寰�鏉ユ槑缁�
+export function customewTransactionsDetails(query) {
+ return request({
+ url: "/metricStatistics/customewTransactionsDetails",
+ method: "get",
+ params: query,
+ });
+}
diff --git a/src/api/salesManagement/invoiceLedger.js b/src/api/salesManagement/invoiceLedger.js
new file mode 100644
index 0000000..9545ec0
--- /dev/null
+++ b/src/api/salesManagement/invoiceLedger.js
@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+/** 鍥炴鍙拌处 - 瀹㈡埛閿�鍞处鎴峰垎椤� */
+export function invoiceLedgerSalesAccount(query) {
+ return request({
+ url: '/invoiceLedger/salesAccount',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/src/api/salesManagement/paymentShipping.js b/src/api/salesManagement/paymentShipping.js
new file mode 100644
index 0000000..c163540
--- /dev/null
+++ b/src/api/salesManagement/paymentShipping.js
@@ -0,0 +1,35 @@
+// 閿�鍞彴璐﹂〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/paymentShipping/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板
+export function add(data) {
+ return request({
+ url: "/paymentShipping/add",
+ method: "post",
+ data
+ });
+}
+// 淇敼
+export function update(data) {
+ return request({
+ url: "/paymentShipping/update",
+ method: "post",
+ data
+ });
+}
+// 鍒犻櫎閿�鍞彴璐�
+export function deletePaymentShipping(data) {
+ return request({
+ url: "/paymentShipping/delete",
+ method: "delete",
+ data
+ });
+}
\ No newline at end of file
diff --git a/src/api/salesManagement/receiptPayment.js b/src/api/salesManagement/receiptPayment.js
new file mode 100644
index 0000000..7fdb9a7
--- /dev/null
+++ b/src/api/salesManagement/receiptPayment.js
@@ -0,0 +1,10 @@
+import request from '@/utils/request'
+
+/** 鍥炴鍙拌处 - 瀹㈡埛寰�鏉ヨ褰� */
+export function customerInteractions(query) {
+ return request({
+ url: '/receiptPayment/customerInteractions',
+ method: 'get',
+ params: query
+ })
+}
diff --git a/src/api/salesManagement/returnOrder.js b/src/api/salesManagement/returnOrder.js
new file mode 100644
index 0000000..f945fc9
--- /dev/null
+++ b/src/api/salesManagement/returnOrder.js
@@ -0,0 +1,82 @@
+import request from "@/utils/request";
+
+
+// 閿�鍞��璐�-鏌ヨ
+// /returnManagement/listPage
+export function returnManagementList(query) {
+ return request({
+ url: "/returnManagement/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閿�鍞��璐�-娣诲姞
+// /returnManagement/add
+export function returnManagementAdd(data) {
+ return request({
+ url: "/returnManagement/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 閿�鍞��璐�-淇敼
+// /returnManagement/update
+export function returnManagementUpdate(data) {
+ return request({
+ url: "/returnManagement/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 閿�鍞��璐�-鍒犻櫎
+// /returnManagement/del
+export function returnManagementDel(data) {
+ return request({
+ url: "/returnManagement/del",
+ method: "delete",
+ data,
+ });
+}
+
+// 閿�鍞��璐�-鏌ヨ
+// /returnManagement/getById
+export function returnManagementGetById(query) {
+ return request({
+ url: "/returnManagement/getById",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閿�鍞��璐�-鏍规嵁鍑哄簱鍗曟煡璇㈤攢鍞鍗曚互鍙婁骇鍝佷俊鎭�
+// /returnManagement/getByShippingId
+export function returnManagementGetByShippingId(query) {
+ return request({
+ url: "/returnManagement/getByShippingId",
+ method: "get",
+ params: query,
+ });
+}
+
+// 閫氳繃瀹㈡埛鍚嶇О鏌ヨ
+// /shippingInfo/getByCustomerName
+export function getSalesLedger(query) {
+ return request({
+ url: '/shippingInfo/getByCustomerName',
+ method: 'get',
+ params: query,
+ })
+}
+
+// 澶勭悊
+// /returnManagement/handle
+export function returnManagementHandle(data) {
+ return request({
+ url: "/returnManagement/handle",
+ method: "get",
+ params: data,
+ });
+}
diff --git a/src/api/salesManagement/salesLedger.js b/src/api/salesManagement/salesLedger.js
new file mode 100644
index 0000000..6548927
--- /dev/null
+++ b/src/api/salesManagement/salesLedger.js
@@ -0,0 +1,119 @@
+// 閿�鍞彴璐﹂〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function ledgerList(query) {
+ return request({
+ url: "/sales/ledger/list",
+ method: "get",
+ params: query,
+ });
+}
+// 瀛愯〃鏍兼煡璇�
+export function productList(query) {
+ return request({
+ url: "/sales/product/list",
+ method: "get",
+ params: query,
+ });
+}
+// 鏌ヨ瀹㈡埛鍚嶇О鍒楄〃
+export function customerList(query) {
+ return request({
+ url: "/basic/customer/customerList",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板銆佷慨鏀归攢鍞彴璐�
+export function addOrUpdateSalesLedger(query) {
+ return request({
+ url: "/sales/ledger/addOrUpdateSalesLedger",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎閿�鍞彴璐�
+export function delLedger(query) {
+ return request({
+ url: "/sales/ledger/delLedger",
+ method: "delete",
+ data: query,
+ });
+}
+// 鏌ヨ閿�鍞彴璐﹁鎯�
+export function getSalesLedgerWithProducts(query) {
+ return request({
+ url: "/sales/ledger/getSalesLedgerWithProducts",
+ method: "get",
+ params: query,
+ });
+}
+// 瀹炴椂淇敼浜у搧淇℃伅
+export function addOrUpdateSalesLedgerProduct(query) {
+ return request({
+ url: "/sales/product/addOrUpdateSalesLedgerProduct",
+ method: "post",
+ data: query,
+ });
+}
+// 鍒犻櫎浜у搧
+export function delProduct(query) {
+ return request({
+ url: "/sales/product/delProduct",
+ method: "delete",
+ data: query,
+ });
+}
+// 涓婁紶闄勪欢
+export function upload(query) {
+ return request({
+ url: "/file/upload",
+ method: "post",
+ data: query,
+ responseType: "blob",
+ });
+}
+// 缂栬緫鏃跺垹闄ら檮浠�
+export function delLedgerFile(query) {
+ return request({
+ url: "/sales/ledger/delLedgerFile",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 閿�鍞笉鍒嗛〉鏌ヨ
+export function ledgerListNoPage(query) {
+ return request({
+ url: "/sales/ledger/listNoPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍒嗛〉鏌ヨ
+export function ledgerListPage(query) {
+ return request({
+ url: "/sales/ledger/listPage",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏍规嵁閿�鍞悎鍚屽彿鏌ヤ骇鍝佷俊鎭�
+export function getProductInfoByContractNo(query) {
+ return request({
+ url: "/purchase/ledger/getProductBySalesNo",
+ method: "get",
+ params: query,
+ });
+}
+// 閿�鍞彴璐﹂〉闈㈠彂璐э紝鏌ヨ搴撳瓨鏄惁鍏呰冻
+export function getProductInventory(query) {
+ return request({
+ url: "/sales/ledger/getProductInventory",
+ method: "get",
+ params: query,
+ });
+}
\ No newline at end of file
diff --git a/src/api/salesManagement/salesQuotation.js b/src/api/salesManagement/salesQuotation.js
new file mode 100644
index 0000000..4329dd9
--- /dev/null
+++ b/src/api/salesManagement/salesQuotation.js
@@ -0,0 +1,112 @@
+// 閿�鍞姤浠烽〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ鎶ヤ环鍗曞垪琛�
+export function getQuotationList(query) {
+ return request({
+ url: "/sales/quotation/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ鎶ヤ环鍗曡鎯�
+export function getQuotationDetail(query) {
+ return request({
+ url: "/sales/quotation/detail",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏂板鎶ヤ环鍗�
+export function addQuotation(data) {
+ return request({
+ url: "/sales/quotation/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼鎶ヤ环鍗�
+export function updateQuotation(data) {
+ return request({
+ url: "/sales/quotation/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎鎶ヤ环鍗�
+export function deleteQuotation(query) {
+ return request({
+ url: "/sales/quotation/delete",
+ method: "delete",
+ data: query,
+ });
+}
+
+// 鍙戦�佹姤浠峰崟
+export function sendQuotation(data) {
+ return request({
+ url: "/sales/quotation/send",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鎶ヤ环鍗曡浆璁㈠崟
+export function convertToOrder(data) {
+ return request({
+ url: "/sales/quotation/convertToOrder",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鏌ヨ瀹㈡埛鍒楄〃
+export function getCustomerList(query) {
+ return request({
+ url: "/basic/customer/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ浜у搧鍒楄〃
+export function getProductList(query) {
+ return request({
+ url: "/basic/product/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ涓氬姟鍛樺垪琛�
+export function getSalespersonList(query) {
+ return request({
+ url: "/system/user/salespersonList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 瀵煎嚭鎶ヤ环鍗�
+export function exportQuotation(query) {
+ return request({
+ url: "/sales/quotation/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// 鎵撳嵃鎶ヤ环鍗�
+export function printQuotation(query) {
+ return request({
+ url: "/sales/quotation/print",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
diff --git a/src/api/salesManagement/salespersonManagement.js b/src/api/salesManagement/salespersonManagement.js
new file mode 100644
index 0000000..5c5cf66
--- /dev/null
+++ b/src/api/salesManagement/salespersonManagement.js
@@ -0,0 +1,35 @@
+// 閿�鍞彴璐﹂〉闈㈡帴鍙�
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ
+export function listPage(query) {
+ return request({
+ url: "/salespersonManagement/listPage",
+ method: "get",
+ params: query,
+ });
+}
+// 鏂板
+export function add(data) {
+ return request({
+ url: "/salespersonManagement/add",
+ method: "post",
+ data
+ });
+}
+// 淇敼
+export function update(data) {
+ return request({
+ url: "/salespersonManagement/update",
+ method: "post",
+ data
+ });
+}
+// 鍒犻櫎閿�鍞彴璐�
+export function deleteSalespersonManagement(data) {
+ return request({
+ url: "/salespersonManagement/delete",
+ method: "delete",
+ data
+ });
+}
\ No newline at end of file
diff --git a/src/api/salesManagement/strategyControl.js b/src/api/salesManagement/strategyControl.js
new file mode 100644
index 0000000..d86864e
--- /dev/null
+++ b/src/api/salesManagement/strategyControl.js
@@ -0,0 +1,202 @@
+// 绛栫暐绠℃帶椤甸潰鎺ュ彛
+import request from "@/utils/request";
+
+// ========== 浠锋牸绛栫暐閰嶇疆 ==========
+
+// 鍒嗛〉鏌ヨ浠锋牸绛栫暐鍒楄〃
+export function getPriceStrategyList(query) {
+ return request({
+ url: "/sales/priceStrategy/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ浠锋牸绛栫暐璇︽儏
+export function getPriceStrategyDetail(id) {
+ return request({
+ url: "/sales/priceStrategy/detail",
+ method: "get",
+ params: { id },
+ });
+}
+
+// 鏂板浠锋牸绛栫暐
+export function addPriceStrategy(data) {
+ return request({
+ url: "/sales/priceStrategy/add",
+ method: "post",
+ data: data,
+ });
+}
+
+// 淇敼浠锋牸绛栫暐
+export function updatePriceStrategy(data) {
+ return request({
+ url: "/sales/priceStrategy/update",
+ method: "post",
+ data: data,
+ });
+}
+
+// 鍒犻櫎浠锋牸绛栫暐
+export function deletePriceStrategy(id) {
+ return request({
+ url: "/sales/priceStrategy/delete",
+ method: "delete",
+ params: { id },
+ });
+}
+
+// 鍚敤/绂佺敤浠锋牸绛栫暐
+export function togglePriceStrategy(data) {
+ return request({
+ url: "/sales/priceStrategy/toggle",
+ method: "post",
+ data: data,
+ });
+}
+
+// ========== 鍚堝悓鎵ц鐩戞帶 ==========
+
+// 鑾峰彇鍚堝悓鎵ц缁熻鏁版嵁
+export function getContractStats(query) {
+ return request({
+ url: "/sales/contract/stats",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍒嗛〉鏌ヨ鍚堝悓鎵ц鍒楄〃
+export function getContractExecutionList(query) {
+ return request({
+ url: "/sales/contract/executionList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ鍚堝悓鎵ц璇︽儏
+export function getContractExecutionDetail(contractNo) {
+ return request({
+ url: "/sales/contract/executionDetail",
+ method: "get",
+ params: { contractNo },
+ });
+}
+
+// 鏇存柊鍚堝悓鎵ц杩涘害
+export function updateContractProgress(data) {
+ return request({
+ url: "/sales/contract/updateProgress",
+ method: "post",
+ data: data,
+ });
+}
+
+// ========== 鍘嗗彶姣斾环鍒嗘瀽 ==========
+
+// 鏌ヨ鍘嗗彶浠锋牸瀵规瘮鏁版嵁
+export function getPriceComparisonList(query) {
+ return request({
+ url: "/sales/priceComparison/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鑾峰彇浠锋牸瓒嬪娍鍥捐〃鏁版嵁
+export function getPriceTrendChart(query) {
+ return request({
+ url: "/sales/priceComparison/trendChart",
+ method: "get",
+ params: query,
+ });
+}
+
+// 瀵煎嚭鍘嗗彶姣斾环鏁版嵁
+export function exportPriceComparison(query) {
+ return request({
+ url: "/sales/priceComparison/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// ========== 鍒╂鼎鍒嗘瀽 ==========
+
+// 鑾峰彇鍒╂鼎缁熻鏁版嵁
+export function getProfitStats(query) {
+ return request({
+ url: "/sales/profit/stats",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鍒嗛〉鏌ヨ鍒╂鼎鍒嗘瀽鍒楄〃
+export function getProfitAnalysisList(query) {
+ return request({
+ url: "/sales/profit/analysisList",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鑾峰彇鍒╂鼎瓒嬪娍鍥捐〃鏁版嵁
+export function getProfitTrendChart(query) {
+ return request({
+ url: "/sales/profit/trendChart",
+ method: "get",
+ params: query,
+ });
+}
+
+// 璁$畻姣涘埄鐜�
+export function calculateGrossProfit(data) {
+ return request({
+ url: "/sales/profit/calculate",
+ method: "post",
+ data: data,
+ });
+}
+
+// 瀵煎嚭鍒╂鼎鍒嗘瀽鎶ヨ〃
+export function exportProfitAnalysis(query) {
+ return request({
+ url: "/sales/profit/export",
+ method: "get",
+ params: query,
+ responseType: "blob",
+ });
+}
+
+// ========== 鍏叡鎺ュ彛 ==========
+
+// 鏌ヨ瀹㈡埛鍒楄〃锛堢敤浜庝笅鎷夐�夋嫨锛�
+export function getCustomerOptions() {
+ return request({
+ url: "/basic/customer/options",
+ method: "get",
+ });
+}
+
+// 鏌ヨ浜у搧鍒楄〃锛堢敤浜庝笅鎷夐�夋嫨锛�
+export function getProductOptions(query) {
+ return request({
+ url: "/basic/product/options",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ閿�鍞尯鍩熷垪琛�
+export function getRegionOptions() {
+ return request({
+ url: "/basic/region/options",
+ method: "get",
+ });
+}
+
diff --git a/src/api/system/appVersion.js b/src/api/system/appVersion.js
new file mode 100644
index 0000000..fc7bdc5
--- /dev/null
+++ b/src/api/system/appVersion.js
@@ -0,0 +1,19 @@
+import request from "@/utils/request";
+
+// 鏌ヨ APP 鐗堟湰鍒嗛〉鍒楄〃
+export function listAppVersion(params) {
+ return request({
+ url: "/app/getAllVersion",
+ method: "get",
+ params,
+ });
+}
+
+// 涓婁紶 APK
+export function add(data) {
+ return request({
+ url: "/app/add",
+ method: "post",
+ data
+ });
+}
diff --git a/src/api/system/config.js b/src/api/system/config.js
new file mode 100644
index 0000000..7858c69
--- /dev/null
+++ b/src/api/system/config.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍙傛暟鍒楄〃
+export function listConfig(query) {
+ return request({
+ url: '/system/config/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ鍙傛暟璇︾粏
+export function getConfig(configId) {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'get'
+ })
+}
+
+// 鏍规嵁鍙傛暟閿悕鏌ヨ鍙傛暟鍊�
+export function getConfigKey(configKey) {
+ return request({
+ url: '/system/config/configKey/' + configKey,
+ method: 'get'
+ })
+}
+
+// 鏂板鍙傛暟閰嶇疆
+export function addConfig(data) {
+ return request({
+ url: '/system/config',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼鍙傛暟閰嶇疆
+export function updateConfig(data) {
+ return request({
+ url: '/system/config',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎鍙傛暟閰嶇疆
+export function delConfig(configId) {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'delete'
+ })
+}
+
+// 鍒锋柊鍙傛暟缂撳瓨
+export function refreshCache() {
+ return request({
+ url: '/system/config/refreshCache',
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/dept.js b/src/api/system/dept.js
new file mode 100644
index 0000000..9ca6966
--- /dev/null
+++ b/src/api/system/dept.js
@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 鏌ヨ閮ㄩ棬鍒楄〃
+export function listDept(query) {
+ return request({
+ url: '/system/dept/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ閮ㄩ棬鍒楄〃锛堟帓闄よ妭鐐癸級
+export function listDeptExcludeChild(deptId) {
+ return request({
+ url: '/system/dept/list/exclude/' + deptId,
+ method: 'get'
+ })
+}
+
+// 鏌ヨ閮ㄩ棬璇︾粏
+export function getDept(deptId) {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'get'
+ })
+}
+
+// 鏂板閮ㄩ棬
+export function addDept(data) {
+ return request({
+ url: '/system/dept',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼閮ㄩ棬
+export function updateDept(data) {
+ return request({
+ url: '/system/dept',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎閮ㄩ棬
+export function delDept(deptId) {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/system/dict/data.js b/src/api/system/dict/data.js
new file mode 100644
index 0000000..2a6e481
--- /dev/null
+++ b/src/api/system/dict/data.js
@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 鏌ヨ瀛楀吀鏁版嵁鍒楄〃
+export function listData(query) {
+ return request({
+ url: '/system/dict/data/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ瀛楀吀鏁版嵁璇︾粏
+export function getData(dictCode) {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'get'
+ })
+}
+
+// 鏍规嵁瀛楀吀绫诲瀷鏌ヨ瀛楀吀鏁版嵁淇℃伅
+export function getDicts(dictType) {
+ return request({
+ url: '/system/dict/data/type/' + dictType,
+ method: 'get'
+ })
+}
+
+// 鏂板瀛楀吀鏁版嵁
+export function addData(data) {
+ return request({
+ url: '/system/dict/data',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼瀛楀吀鏁版嵁
+export function updateData(data) {
+ return request({
+ url: '/system/dict/data',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎瀛楀吀鏁版嵁
+export function delData(dictCode) {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/dict/type.js b/src/api/system/dict/type.js
new file mode 100644
index 0000000..d89dbd1
--- /dev/null
+++ b/src/api/system/dict/type.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 鏌ヨ瀛楀吀绫诲瀷鍒楄〃
+export function listType(query) {
+ return request({
+ url: '/system/dict/type/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ瀛楀吀绫诲瀷璇︾粏
+export function getType(dictId) {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'get'
+ })
+}
+
+// 鏂板瀛楀吀绫诲瀷
+export function addType(data) {
+ return request({
+ url: '/system/dict/type',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼瀛楀吀绫诲瀷
+export function updateType(data) {
+ return request({
+ url: '/system/dict/type',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎瀛楀吀绫诲瀷
+export function delType(dictId) {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'delete'
+ })
+}
+
+// 鍒锋柊瀛楀吀缂撳瓨
+export function refreshCache() {
+ return request({
+ url: '/system/dict/type/refreshCache',
+ method: 'delete'
+ })
+}
+
+// 鑾峰彇瀛楀吀閫夋嫨妗嗗垪琛�
+export function optionselect() {
+ return request({
+ url: '/system/dict/type/optionselect',
+ method: 'get'
+ })
+}
diff --git a/src/api/system/menu.js b/src/api/system/menu.js
new file mode 100644
index 0000000..97258ee
--- /dev/null
+++ b/src/api/system/menu.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鑿滃崟鍒楄〃
+export function listMenu(query) {
+ return request({
+ url: '/system/menu/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ鑿滃崟璇︾粏
+export function getMenu(menuId) {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'get'
+ })
+}
+
+// 鏌ヨ鑿滃崟涓嬫媺鏍戠粨鏋�
+export function treeselect() {
+ return request({
+ url: '/system/menu/treeselect',
+ method: 'get'
+ })
+}
+
+// 鏍规嵁瑙掕壊ID鏌ヨ鑿滃崟涓嬫媺鏍戠粨鏋�
+export function roleMenuTreeselect(roleId) {
+ return request({
+ url: '/system/menu/roleMenuTreeselect/' + roleId,
+ method: 'get'
+ })
+}
+
+// 鏂板鑿滃崟
+export function addMenu(data) {
+ return request({
+ url: '/system/menu',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼鑿滃崟
+export function updateMenu(data) {
+ return request({
+ url: '/system/menu',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎鑿滃崟
+export function delMenu(menuId) {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/system/message.js b/src/api/system/message.js
new file mode 100644
index 0000000..dd81cc5
--- /dev/null
+++ b/src/api/system/message.js
@@ -0,0 +1,45 @@
+import request from "@/utils/request";
+
+// 鏌ヨ娑堟伅閫氱煡鍒楄〃
+export function listMessage(query) {
+ return request({
+ url: "/system/notice/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 鏌ヨ鏈娑堟伅鏁伴噺
+export function getUnreadCount(consigneeId) {
+ return request({
+ url: "/system/notice/getCount",
+ method: "get",
+ params: { consigneeId },
+ });
+}
+
+// 鏍囪娑堟伅涓哄凡璇�
+export function markAsRead(noticeId, status) {
+ return request({
+ url: "/system/notice",
+ method: "put",
+ data: { noticeId, status },
+ });
+}
+
+// 涓�閿爣璁版墍鏈夋秷鎭负宸茶
+export function markAllAsRead() {
+ return request({
+ url: "/system/notice/readAll",
+ method: "post",
+ });
+}
+
+// 纭娑堟伅
+export function confirmMessage(noticeId, status) {
+ return request({
+ url: "/system/notice",
+ method: "put",
+ data: { noticeId, status },
+ });
+}
diff --git a/src/api/system/notice.js b/src/api/system/notice.js
new file mode 100644
index 0000000..737fc16
--- /dev/null
+++ b/src/api/system/notice.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鍏憡鍒楄〃
+export function listNotice(query) {
+ return request({
+ url: '/system/notice/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ鍏憡璇︾粏
+export function getNotice(noticeId) {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'get'
+ })
+}
+
+// 鏂板鍏憡
+export function addNotice(data) {
+ return request({
+ url: '/system/notice',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼鍏憡
+export function updateNotice(data) {
+ return request({
+ url: '/system/notice',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎鍏憡
+export function delNotice(noticeId) {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/src/api/system/post.js b/src/api/system/post.js
new file mode 100644
index 0000000..fcb5bba
--- /dev/null
+++ b/src/api/system/post.js
@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 鏌ヨ宀椾綅鍒楄〃
+export function listPost(query) {
+ return request({
+ url: '/system/post/list',
+ method: 'get',
+ params: query
+ })
+}
+
+export function findPostOptions(query) {
+ return request({
+ url: '/system/post/optionselect',
+ method: 'get',
+ params: query
+ })
+}
+
+
+// 鏌ヨ宀椾綅璇︾粏
+export function getPost(postId) {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'get'
+ })
+}
+
+// 鏂板宀椾綅
+export function addPost(data) {
+ return request({
+ url: '/system/post',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼宀椾綅
+export function updatePost(data) {
+ return request({
+ url: '/system/post',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎宀椾綅
+export function delPost(postId) {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'delete'
+ })
+}
diff --git a/src/api/system/role.js b/src/api/system/role.js
new file mode 100644
index 0000000..528cd18
--- /dev/null
+++ b/src/api/system/role.js
@@ -0,0 +1,119 @@
+import request from '@/utils/request'
+
+// 鏌ヨ瑙掕壊鍒楄〃
+export function listRole(query) {
+ return request({
+ url: '/system/role/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ瑙掕壊璇︾粏
+export function getRole(roleId) {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'get'
+ })
+}
+
+// 鏂板瑙掕壊
+export function addRole(data) {
+ return request({
+ url: '/system/role',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼瑙掕壊
+export function updateRole(data) {
+ return request({
+ url: '/system/role',
+ method: 'put',
+ data: data
+ })
+}
+
+// 瑙掕壊鏁版嵁鏉冮檺
+export function dataScope(data) {
+ return request({
+ url: '/system/role/dataScope',
+ method: 'put',
+ data: data
+ })
+}
+
+// 瑙掕壊鐘舵�佷慨鏀�
+export function changeRoleStatus(roleId, status) {
+ const data = {
+ roleId,
+ status
+ }
+ return request({
+ url: '/system/role/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎瑙掕壊
+export function delRole(roleId) {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'delete'
+ })
+}
+
+// 鏌ヨ瑙掕壊宸叉巿鏉冪敤鎴峰垪琛�
+export function allocatedUserList(query) {
+ return request({
+ url: '/system/role/authUser/allocatedList',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ瑙掕壊鏈巿鏉冪敤鎴峰垪琛�
+export function unallocatedUserList(query) {
+ return request({
+ url: '/system/role/authUser/unallocatedList',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鍙栨秷鐢ㄦ埛鎺堟潈瑙掕壊
+export function authUserCancel(data) {
+ return request({
+ url: '/system/role/authUser/cancel',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鎵归噺鍙栨秷鐢ㄦ埛鎺堟潈瑙掕壊
+export function authUserCancelAll(data) {
+ return request({
+ url: '/system/role/authUser/cancelAll',
+ method: 'put',
+ params: data
+ })
+}
+
+// 鎺堟潈鐢ㄦ埛閫夋嫨
+export function authUserSelectAll(data) {
+ return request({
+ url: '/system/role/authUser/selectAll',
+ method: 'put',
+ params: data
+ })
+}
+
+// 鏍规嵁瑙掕壊ID鏌ヨ閮ㄩ棬鏍戠粨鏋�
+export function deptTreeSelect(roleId) {
+ return request({
+ url: '/system/role/deptTree/' + roleId,
+ method: 'get'
+ })
+}
diff --git a/src/api/system/user.js b/src/api/system/user.js
new file mode 100644
index 0000000..431f6b0
--- /dev/null
+++ b/src/api/system/user.js
@@ -0,0 +1,159 @@
+import request from '@/utils/request'
+import { parseStrEmpty } from "@/utils/ruoyi";
+
+// 鏌ヨ鐢ㄦ埛鍒楄〃
+export function listUser(query) {
+ return request({
+ url: '/system/user/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ鐢ㄦ埛璇︾粏
+export function getUser(userId) {
+ return request({
+ url: '/system/user/' + parseStrEmpty(userId),
+ method: 'get'
+ })
+}
+
+// 鏂板鐢ㄦ埛
+export function addUser(data) {
+ return request({
+ url: '/system/user',
+ method: 'post',
+ data: data
+ })
+}
+
+// 淇敼鐢ㄦ埛
+export function updateUser(data) {
+ return request({
+ url: '/system/user',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鍒犻櫎鐢ㄦ埛
+export function delUser(userId) {
+ return request({
+ url: '/system/user/' + userId,
+ method: 'delete'
+ })
+}
+
+// 鐢ㄦ埛瀵嗙爜閲嶇疆
+export function resetUserPwd(userId, password) {
+ const data = {
+ userId,
+ password
+ }
+ return request({
+ url: '/system/user/resetPwd',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鐢ㄦ埛鐘舵�佷慨鏀�
+export function changeUserStatus(userId, status) {
+ const data = {
+ userId,
+ status
+ }
+ return request({
+ url: '/system/user/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鏌ヨ鐢ㄦ埛涓汉淇℃伅
+export function getUserProfile() {
+ return request({
+ url: '/system/user/profile',
+ method: 'get'
+ })
+}
+
+// 淇敼鐢ㄦ埛涓汉淇℃伅
+export function updateUserProfile(data) {
+ return request({
+ url: '/system/user/profile',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鐢ㄦ埛瀵嗙爜閲嶇疆
+export function updateUserPwd(oldPassword, newPassword) {
+ const data = {
+ oldPassword,
+ newPassword
+ }
+ return request({
+ url: '/system/user/profile/updatePwd',
+ method: 'put',
+ data: data
+ })
+}
+
+// 鐢ㄦ埛澶村儚涓婁紶
+export function uploadAvatar(data) {
+ return request({
+ url: '/system/user/profile/avatar',
+ method: 'post',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ data: data
+ })
+}
+
+// 鏌ヨ鎺堟潈瑙掕壊
+export function getAuthRole(userId) {
+ return request({
+ url: '/system/user/authRole/' + userId,
+ method: 'get'
+ })
+}
+
+// 淇濆瓨鎺堟潈瑙掕壊
+export function updateAuthRole(data) {
+ return request({
+ url: '/system/user/authRole',
+ method: 'put',
+ params: data
+ })
+}
+
+// 鏌ヨ閮ㄩ棬涓嬫媺鏍戠粨鏋�
+export function deptTreeSelect() {
+ return request({
+ url: '/system/user/deptTree',
+ method: 'get'
+ })
+}
+// 鏌ヨ鐢ㄦ埛鍒楄〃
+export function userListNoPageByTenantId() {
+ return request({
+ url: '/system/user/userListNoPageByTenantId',
+ method: 'get'
+ })
+}
+
+// 鏌ヨ鐢ㄦ埛鍒楄〃
+export function userListNoPage() {
+ return request({
+ url: '/system/user/userListNoPage',
+ method: 'get'
+ })
+}
+// 閮ㄩ棬鍒楄〃
+export function userLoginFacotryList(params) {
+ return request({
+ url: '/userLoginFacotryList',
+ method: 'get',
+ params: params
+ })
+}
diff --git a/src/api/tool/gen.js b/src/api/tool/gen.js
new file mode 100644
index 0000000..5728980
--- /dev/null
+++ b/src/api/tool/gen.js
@@ -0,0 +1,85 @@
+import request from '@/utils/request'
+
+// 鏌ヨ鐢熸垚琛ㄦ暟鎹�
+export function listTable(query) {
+ return request({
+ url: '/tool/gen/list',
+ method: 'get',
+ params: query
+ })
+}
+// 鏌ヨdb鏁版嵁搴撳垪琛�
+export function listDbTable(query) {
+ return request({
+ url: '/tool/gen/db/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 鏌ヨ琛ㄨ缁嗕俊鎭�
+export function getGenTable(tableId) {
+ return request({
+ url: '/tool/gen/' + tableId,
+ method: 'get'
+ })
+}
+
+// 淇敼浠g爜鐢熸垚淇℃伅
+export function updateGenTable(data) {
+ return request({
+ url: '/tool/gen',
+ method: 'put',
+ data: data
+ })
+}
+
+// 瀵煎叆琛�
+export function importTable(data) {
+ return request({
+ url: '/tool/gen/importTable',
+ method: 'post',
+ params: data
+ })
+}
+
+// 鍒涘缓琛�
+export function createTable(data) {
+ return request({
+ url: '/tool/gen/createTable',
+ method: 'post',
+ params: data
+ })
+}
+
+// 棰勮鐢熸垚浠g爜
+export function previewTable(tableId) {
+ return request({
+ url: '/tool/gen/preview/' + tableId,
+ method: 'get'
+ })
+}
+
+// 鍒犻櫎琛ㄦ暟鎹�
+export function delTable(tableId) {
+ return request({
+ url: '/tool/gen/' + tableId,
+ method: 'delete'
+ })
+}
+
+// 鐢熸垚浠g爜锛堣嚜瀹氫箟璺緞锛�
+export function genCode(tableName) {
+ return request({
+ url: '/tool/gen/genCode/' + tableName,
+ method: 'get'
+ })
+}
+
+// 鍚屾鏁版嵁搴�
+export function synchDb(tableName) {
+ return request({
+ url: '/tool/gen/synchDb/' + tableName,
+ method: 'get'
+ })
+}
diff --git a/src/api/viewIndex.js b/src/api/viewIndex.js
new file mode 100644
index 0000000..9cae219
--- /dev/null
+++ b/src/api/viewIndex.js
@@ -0,0 +1,384 @@
+// 棣栭〉鎺ュ彛
+import request from "@/utils/request";
+
+// 宸ュ簭鏁版嵁鐢熶骇缁熻鏄庣粏
+export const processDataProductionStatistics = (params) => {
+ return request({
+ url: "/home/processDataProductionStatistics",
+ method: "get",
+ params,
+ });
+};
+
+// 璐ㄩ噺缁熻
+export const qualityInspectionStatistics = (params) => {
+ return request({
+ url: "/home/qualityInspectionStatistics",
+ method: "get",
+ params,
+ });
+};
+
+// 鍘熸潗鏂欐娴�
+export const rawMaterialDetection = (query) => {
+ return request({
+ url: "/home/rawMaterialDetection",
+ method: "get",
+ params: query,
+ });
+};
+
+// 杩囩▼妫�娴�
+export const processDetection = (query) => {
+ return request({
+ url: "/home/processDetection",
+ method: "get",
+ params: query,
+ });
+};
+
+// 鎴愬搧鍑哄巶妫�娴�
+export const factoryDetection = (query) => {
+ return request({
+ url: "/home/factoryDetection",
+ method: "get",
+ params: query,
+ });
+};
+
+// 妫�楠屾暟閲�
+export const qualityInspectionCount = () => {
+ return request({
+ url: "/home/qualityInspectionCount",
+ method: "get",
+ });
+};
+
+// 涓嶅悎鏍奸璀�
+export const nonComplianceWarning = () => {
+ return request({
+ url: "/home/nonComplianceWarning",
+ method: "get",
+ });
+};
+
+// 瀹屾垚妫�楠屾暟
+export const completedInspectionCount = () => {
+ return request({
+ url: "/home/completedInspectionCount",
+ method: "get",
+ });
+};
+
+// 涓嶅悎鏍间骇鍝佹帓鍚�
+export const unqualifiedProductRanking = () => {
+ return request({
+ url: "/home/unqualifiedProductRanking",
+ method: "get",
+ });
+};
+
+// 涓嶅悎鏍兼鍝佸鐞嗗垎鏋�
+export const unqualifiedProductProcessingAnalysis = () => {
+ return request({
+ url: "/home/unqualifiedProductProcessingAnalysis",
+ method: "get",
+ });
+};
+
+// 閿�鍞�-閲囪喘-搴撳瓨鏁版嵁
+export const getBusiness = () => {
+ return request({
+ url: "/home/business",
+ method: "get",
+ });
+};
+// 瀹㈡埛鍚堝悓閲戦鍒嗘瀽
+export const analysisCustomerContractAmounts = () => {
+ return request({
+ url: "/home/analysisCustomerContractAmounts",
+ method: "get",
+ });
+};
+// 璐ㄦ鍒嗘瀽锛堝彲浼� dateType: 1鍛� 2鏈� 3瀛e害锛�
+export const qualityStatistics = (params) => {
+ return request({
+ url: "/home/qualityStatistics",
+ method: "get",
+ params,
+ });
+};
+
+// 宸ュ崟鎵ц鏁堢巼鍒嗘瀽锛坉ateType: 1鍛� 2鏈� 3瀛e害锛�
+export const workOrderEfficiencyAnalysis = (params) => {
+ return request({
+ url: "/home/workOrderEfficiencyAnalysis",
+ method: "get",
+ params,
+ });
+};
+
+// 鐢熶骇鏍哥畻鍒嗘瀽
+export const productionAccountingAnalysis = (query) => {
+ return request({
+ url: "/home/productionAccountingAnalysis",
+ method: "get",
+ params: query,
+ });
+};
+// 搴旀敹搴斾粯缁熻
+export const statisticsReceivablePayable = (query) => {
+ return request({
+ url: "/home/statisticsReceivablePayable",
+ method: "get",
+ params: query,
+ });
+};
+// 寰呭姙浜嬮」
+export const homeTodos = () => {
+ return request({
+ url: "/home/todos",
+ method: "get",
+ });
+};
+
+// 绾垮舰鍥�
+export const getAmountHalfYear = () => {
+ return request({
+ url: "/sales/ledger/getAmountHalfYear",
+ method: "get",
+ });
+};
+
+// 鍚勭敓浜ц鍗曠殑瀹屾垚杩涘害缁熻
+// /home/progressStatistics
+export const getProgressStatistics = () => {
+ return request({
+ url: "/home/progressStatistics",
+ method: "get",
+ });
+};
+
+// 璁㈠崟鏁伴噺缁熻锛堢敓浜ц鍗曟暟銆佸凡瀹屾垚璁㈠崟鏁般�佸緟鐢熶骇璁㈠崟鏁帮級
+export const orderCount = () => {
+ return request({
+ url: "/home/orderCount",
+ method: "get",
+ });
+};
+
+//鍦ㄥ埗鍝佸懆杞儏鍐�
+//home/workInProcessTurnover
+export const getWorkInProcessTurnover = () => {
+ return request({
+ url: "/home/workInProcessTurnover",
+ method: "get",
+ });
+};
+
+// 瀹㈡埛钀ユ敹璐$尞鏁板�煎垎鏋�
+export const customerRevenueAnalysis = (params) => {
+ return request({
+ url: "/home/customerRevenueAnalysis",
+ method: "get",
+ params,
+ });
+};
+
+// 鍛樺伐-瀹㈡埛-渚涘簲鍟嗘�绘暟
+export const summaryStatistics = () => {
+ return request({
+ url: "/home/summaryStatistics",
+ method: "get",
+ });
+};
+
+// 鍚勯儴闂ㄤ汉鍛樺垎甯�
+export const deptStaffDistribution = () => {
+ return request({
+ url: "/home/deptStaffDistribution",
+ method: "get",
+ });
+};
+
+// 渚涘簲鍟嗛噰璐帓鍚�
+export const supplierPurchaseRanking = (query) => {
+ return request({
+ url: "/home/supplierPurchaseRanking",
+ method: "get",
+ params: query,
+ });
+};
+
+// 瀹㈡埛閲戦璐$尞鎺掑悕
+export const customerContributionRanking = (query) => {
+ return request({
+ url: "/home/customerContributionRanking",
+ method: "get",
+ params: query,
+ });
+};
+
+// 鍚勪骇鍝佸ぇ绫诲垎甯�
+export const productCategoryDistribution = () => {
+ return request({
+ url: "/home/productCategoryDistribution",
+ method: "get",
+ });
+};
+
+// 浜у搧閿�鍞噾棰濆垎鏋�
+export const productSalesAnalysis = () => {
+ return request({
+ url: "/home/productSalesAnalysis",
+ method: "get",
+ });
+};
+
+// 宸ュ簭浜у嚭鍒嗘瀽锛坉ateType: 1鍛� 2鏈� 3瀛e害锛�
+export const processOutputAnalysis = (params) => {
+ return request({
+ url: "/home/processOutputAnalysis",
+ method: "get",
+ params,
+ });
+};
+
+// 鍘熸潗鏂欓噰璐噾棰濆崰姣�
+export const rawMaterialPurchaseAmountRatio = () => {
+ return request({
+ url: "/home/rawMaterialPurchaseAmountRatio",
+ method: "get",
+ });
+};
+
+// 璐圭敤鏋勬垚鍒嗘瀽锛坱ype: 1 鎴� 2锛�
+export const expenseCompositionAnalysis = (params) => {
+ return request({
+ url: "/home/expenseCompositionAnalysis",
+ method: "get",
+ params,
+ });
+};
+
+// 閿�鍞�/閲囪喘/鍌ㄥ瓨浜у搧鏁�
+export const salesPurchaseStorageProductCount = () => {
+ return request({
+ url: "/home/salesPurchaseStorageProductCount",
+ method: "get",
+ });
+};
+
+// 浜у搧鍑哄叆搴撳垎鏋愶紙鍙紶 productType: 1 鍘熸潗鏂� 2 鍗婃垚鍝� 3 鎴愬搧锛�
+export const productInOutAnalysis = (params) => {
+ return request({
+ url: "/home/productInOutAnalysis",
+ method: "get",
+ params,
+ });
+};
+
+// 鎶曞叆浜у嚭鍒嗘瀽
+export const inputOutputAnalysis = (params) => {
+ return request({
+ url: "/home/inputOutputAnalysis",
+ method: "get",
+ params,
+ });
+};
+
+// 浜у搧鍛ㄨ浆澶╂暟
+export const productTurnoverDays = () => {
+ return request({
+ url: "/home/productTurnoverDays",
+ method: "get",
+ });
+};
+
+// 鏀舵敮瀵规瘮鍒嗘瀽
+export const incomeExpenseAnalysis = () => {
+ return request({
+ url: "/home/incomeExpenseAnalysis",
+ method: "get",
+ });
+};
+
+// 鍒╂鼎瓒嬪娍鍒嗘瀽
+export const profitTrendAnalysis = () => {
+ return request({
+ url: "/home/profitTrendAnalysis",
+ method: "get",
+ });
+};
+
+// 鏈堝害鏀跺叆
+export const getMonthlyIncome = () => {
+ return request({
+ url: "/home/monthlyIncome",
+ method: "get",
+ });
+};
+
+// 鏈堝害鏀嚭
+export const getMonthlyExpenditure = () => {
+ return request({
+ url: "/home/monthlyExpenditure",
+ method: "get",
+ });
+};
+
+export const productionOverview = () => {
+ return request({
+ url: "/home/productionOverview",
+ method: "get",
+ headers: {
+ handleAuthError: false,
+ },
+ });
+};
+
+export const productionRealtimeBoard = () => {
+ return request({
+ url: "/home/productionRealtimeBoard",
+ method: "get",
+ headers: {
+ handleAuthError: false,
+ },
+ });
+};
+
+export const productionOrderProgress = (params = {}) => {
+ const safePageNum = Math.max(1, Number(params.pageNum || 1));
+ const safePageSize = Math.min(50, Math.max(1, Number(params.pageSize || 10)));
+ const safeTab = ["all", "inProgress", "completed", "paused"].includes(params.tab)
+ ? params.tab
+ : "all";
+ return request({
+ url: "/home/productionOrderProgress",
+ method: "get",
+ params: {
+ ...params,
+ tab: safeTab,
+ pageNum: safePageNum,
+ pageSize: safePageSize,
+ },
+ headers: {
+ handleAuthError: false,
+ },
+ });
+};
+
+export const todayProductionPlan = (params = {}) => {
+ const safeLimit = Math.min(20, Math.max(1, Number(params.limit || 4)));
+ return request({
+ url: "/home/todayProductionPlan",
+ method: "get",
+ params: {
+ ...params,
+ limit: safeLimit,
+ },
+ headers: {
+ handleAuthError: false,
+ },
+ });
+};
diff --git a/src/assets/401_images/401.gif b/src/assets/401_images/401.gif
new file mode 100644
index 0000000..cd6e0d9
--- /dev/null
+++ b/src/assets/401_images/401.gif
Binary files differ
diff --git a/src/assets/404_images/404.png b/src/assets/404_images/404.png
new file mode 100644
index 0000000..3d8e230
--- /dev/null
+++ b/src/assets/404_images/404.png
Binary files differ
diff --git a/src/assets/404_images/404_cloud.png b/src/assets/404_images/404_cloud.png
new file mode 100644
index 0000000..c6281d0
--- /dev/null
+++ b/src/assets/404_images/404_cloud.png
Binary files differ
diff --git "a/src/assets/AI/\344\273\223\345\202\250\345\212\251\346\211\213.png" "b/src/assets/AI/\344\273\223\345\202\250\345\212\251\346\211\213.png"
new file mode 100644
index 0000000..3aae283
--- /dev/null
+++ "b/src/assets/AI/\344\273\223\345\202\250\345\212\251\346\211\213.png"
Binary files differ
diff --git "a/src/assets/AI/\345\276\205\345\212\236\345\212\251\346\211\213.png" "b/src/assets/AI/\345\276\205\345\212\236\345\212\251\346\211\213.png"
new file mode 100644
index 0000000..54f4d32
--- /dev/null
+++ "b/src/assets/AI/\345\276\205\345\212\236\345\212\251\346\211\213.png"
Binary files differ
diff --git "a/src/assets/AI/\347\224\237\344\272\247\345\212\251\346\211\213.png" "b/src/assets/AI/\347\224\237\344\272\247\345\212\251\346\211\213.png"
new file mode 100644
index 0000000..998b9df
--- /dev/null
+++ "b/src/assets/AI/\347\224\237\344\272\247\345\212\251\346\211\213.png"
Binary files differ
diff --git "a/src/assets/AI/\350\264\242\345\212\241\345\212\251\346\211\213.png" "b/src/assets/AI/\350\264\242\345\212\241\345\212\251\346\211\213.png"
new file mode 100644
index 0000000..8932bff
--- /dev/null
+++ "b/src/assets/AI/\350\264\242\345\212\241\345\212\251\346\211\213.png"
Binary files differ
diff --git "a/src/assets/AI/\350\264\250\351\207\217\345\212\251\346\211\213.png" "b/src/assets/AI/\350\264\250\351\207\217\345\212\251\346\211\213.png"
new file mode 100644
index 0000000..e3ab7e1
--- /dev/null
+++ "b/src/assets/AI/\350\264\250\351\207\217\345\212\251\346\211\213.png"
Binary files differ
diff --git "a/src/assets/AI/\351\207\207\350\264\255\345\212\251\346\211\213.png" "b/src/assets/AI/\351\207\207\350\264\255\345\212\251\346\211\213.png"
new file mode 100644
index 0000000..aea0fd0
--- /dev/null
+++ "b/src/assets/AI/\351\207\207\350\264\255\345\212\251\346\211\213.png"
Binary files differ
diff --git "a/src/assets/AI/\351\224\200\345\224\256\345\212\251\346\211\213.png" "b/src/assets/AI/\351\224\200\345\224\256\345\212\251\346\211\213.png"
new file mode 100644
index 0000000..b8087db
--- /dev/null
+++ "b/src/assets/AI/\351\224\200\345\224\256\345\212\251\346\211\213.png"
Binary files differ
diff --git a/src/assets/BI/backImage@2x.png b/src/assets/BI/backImage@2x.png
new file mode 100644
index 0000000..5c62ec6
--- /dev/null
+++ b/src/assets/BI/backImage@2x.png
Binary files differ
diff --git a/src/assets/BI/biaoti.png b/src/assets/BI/biaoti.png
new file mode 100644
index 0000000..3c5ccb9
--- /dev/null
+++ b/src/assets/BI/biaoti.png
Binary files differ
diff --git a/src/assets/BI/border@2x.png b/src/assets/BI/border@2x.png
new file mode 100644
index 0000000..07555cb
--- /dev/null
+++ b/src/assets/BI/border@2x.png
Binary files differ
diff --git a/src/assets/BI/caiwufenxiback@2x.png b/src/assets/BI/caiwufenxiback@2x.png
new file mode 100644
index 0000000..3f242d4
--- /dev/null
+++ b/src/assets/BI/caiwufenxiback@2x.png
Binary files differ
diff --git a/src/assets/BI/chuchangyijianicon@2x.png b/src/assets/BI/chuchangyijianicon@2x.png
new file mode 100644
index 0000000..bc3677a
--- /dev/null
+++ b/src/assets/BI/chuchangyijianicon@2x.png
Binary files differ
diff --git a/src/assets/BI/guochengyijianicon@2x.png b/src/assets/BI/guochengyijianicon@2x.png
new file mode 100644
index 0000000..0c64bdc
--- /dev/null
+++ b/src/assets/BI/guochengyijianicon@2x.png
Binary files differ
diff --git a/src/assets/BI/hetongicon.png b/src/assets/BI/hetongicon.png
new file mode 100644
index 0000000..60c2f0e
--- /dev/null
+++ b/src/assets/BI/hetongicon.png
Binary files differ
diff --git a/src/assets/BI/hetongjineback@2x.png b/src/assets/BI/hetongjineback@2x.png
new file mode 100644
index 0000000..c15ed6a
--- /dev/null
+++ b/src/assets/BI/hetongjineback@2x.png
Binary files differ
diff --git a/src/assets/BI/hetongjineicon1@2x.png b/src/assets/BI/hetongjineicon1@2x.png
new file mode 100644
index 0000000..38c86da
--- /dev/null
+++ b/src/assets/BI/hetongjineicon1@2x.png
Binary files differ
diff --git a/src/assets/BI/hetongjineicon@2x.png b/src/assets/BI/hetongjineicon@2x.png
new file mode 100644
index 0000000..60c2f0e
--- /dev/null
+++ b/src/assets/BI/hetongjineicon@2x.png
Binary files differ
diff --git a/src/assets/BI/hetongtitleback@2x.png b/src/assets/BI/hetongtitleback@2x.png
new file mode 100644
index 0000000..c15ed6a
--- /dev/null
+++ b/src/assets/BI/hetongtitleback@2x.png
Binary files differ
diff --git a/src/assets/BI/icon@2x.png b/src/assets/BI/icon@2x.png
new file mode 100644
index 0000000..267a738
--- /dev/null
+++ b/src/assets/BI/icon@2x.png
Binary files differ
diff --git a/src/assets/BI/jiantou.png b/src/assets/BI/jiantou.png
new file mode 100644
index 0000000..7bba1a9
--- /dev/null
+++ b/src/assets/BI/jiantou.png
Binary files differ
diff --git a/src/assets/BI/jiantou@2x.png b/src/assets/BI/jiantou@2x.png
new file mode 100644
index 0000000..38c86da
--- /dev/null
+++ b/src/assets/BI/jiantou@2x.png
Binary files differ
diff --git a/src/assets/BI/kehuhetongback@2x.png b/src/assets/BI/kehuhetongback@2x.png
new file mode 100644
index 0000000..22a7ead
--- /dev/null
+++ b/src/assets/BI/kehuhetongback@2x.png
Binary files differ
diff --git a/src/assets/BI/pieback@2x.png b/src/assets/BI/pieback@2x.png
new file mode 100644
index 0000000..c8930cc
--- /dev/null
+++ b/src/assets/BI/pieback@2x.png
Binary files differ
diff --git a/src/assets/BI/shijianmingchengbeijing@2x.png b/src/assets/BI/shijianmingchengbeijing@2x.png
new file mode 100644
index 0000000..0ed2813
--- /dev/null
+++ b/src/assets/BI/shijianmingchengbeijing@2x.png
Binary files differ
diff --git a/src/assets/BI/shijianmingxiicon@2x.png b/src/assets/BI/shijianmingxiicon@2x.png
new file mode 100644
index 0000000..3c3a7b7
--- /dev/null
+++ b/src/assets/BI/shijianmingxiicon@2x.png
Binary files differ
diff --git a/src/assets/BI/shujutongji@2x.png b/src/assets/BI/shujutongji@2x.png
new file mode 100644
index 0000000..69b6834
--- /dev/null
+++ b/src/assets/BI/shujutongji@2x.png
Binary files differ
diff --git a/src/assets/BI/shujutongjiicon@2x.png b/src/assets/BI/shujutongjiicon@2x.png
new file mode 100644
index 0000000..780fc22
--- /dev/null
+++ b/src/assets/BI/shujutongjiicon@2x.png
Binary files differ
diff --git a/src/assets/BI/yuancailiaoyijianicon@2x.png b/src/assets/BI/yuancailiaoyijianicon@2x.png
new file mode 100644
index 0000000..dc4a72c
--- /dev/null
+++ b/src/assets/BI/yuancailiaoyijianicon@2x.png
Binary files differ
diff --git a/src/assets/BI/zonghetongbingtubiankuang@2x.png b/src/assets/BI/zonghetongbingtubiankuang@2x.png
new file mode 100644
index 0000000..a322aa5
--- /dev/null
+++ b/src/assets/BI/zonghetongbingtubiankuang@2x.png
Binary files differ
diff --git "a/src/assets/BI/\347\216\253\347\221\260\345\233\276\350\276\271\346\241\206.png" "b/src/assets/BI/\347\216\253\347\221\260\345\233\276\350\276\271\346\241\206.png"
new file mode 100644
index 0000000..b4d06d3
--- /dev/null
+++ "b/src/assets/BI/\347\216\253\347\221\260\345\233\276\350\276\271\346\241\206.png"
Binary files differ
diff --git a/src/assets/aiIndustrialBrain/reference-cards.png b/src/assets/aiIndustrialBrain/reference-cards.png
new file mode 100644
index 0000000..72fdb93
--- /dev/null
+++ b/src/assets/aiIndustrialBrain/reference-cards.png
Binary files differ
diff --git a/src/assets/aiIndustrialBrain/reference-chat.png b/src/assets/aiIndustrialBrain/reference-chat.png
new file mode 100644
index 0000000..d026388
--- /dev/null
+++ b/src/assets/aiIndustrialBrain/reference-chat.png
Binary files differ
diff --git a/src/assets/fonts/DIN Alternate Bold.ttf b/src/assets/fonts/DIN Alternate Bold.ttf
new file mode 100644
index 0000000..81f2f7a
--- /dev/null
+++ b/src/assets/fonts/DIN Alternate Bold.ttf
Binary files differ
diff --git a/src/assets/fonts/font.css b/src/assets/fonts/font.css
new file mode 100644
index 0000000..1a3894a
--- /dev/null
+++ b/src/assets/fonts/font.css
@@ -0,0 +1,7 @@
+@font-face {
+ font-family: "MyCustomFont";
+ src: url("./DIN Alternate Bold.ttf") format("truetype");
+ font-weight: 700; /* 绮椾綋 */
+ font-style: normal;
+ font-display: swap;
+}
diff --git a/src/assets/icons/png/1.png b/src/assets/icons/png/1.png
new file mode 100644
index 0000000..1acfa67
--- /dev/null
+++ b/src/assets/icons/png/1.png
Binary files differ
diff --git a/src/assets/icons/png/2.png b/src/assets/icons/png/2.png
new file mode 100644
index 0000000..cebdf2c
--- /dev/null
+++ b/src/assets/icons/png/2.png
Binary files differ
diff --git a/src/assets/icons/png/3.png b/src/assets/icons/png/3.png
new file mode 100644
index 0000000..719912b
--- /dev/null
+++ b/src/assets/icons/png/3.png
Binary files differ
diff --git a/src/assets/icons/png/4.png b/src/assets/icons/png/4.png
new file mode 100644
index 0000000..b5f5861
--- /dev/null
+++ b/src/assets/icons/png/4.png
Binary files differ
diff --git a/src/assets/icons/png/5.png b/src/assets/icons/png/5.png
new file mode 100644
index 0000000..5467146
--- /dev/null
+++ b/src/assets/icons/png/5.png
Binary files differ
diff --git a/src/assets/icons/png/blue@2x.png b/src/assets/icons/png/blue@2x.png
new file mode 100644
index 0000000..6a33bfd
--- /dev/null
+++ b/src/assets/icons/png/blue@2x.png
Binary files differ
diff --git a/src/assets/icons/png/circleBlue@2x.png b/src/assets/icons/png/circleBlue@2x.png
new file mode 100644
index 0000000..55811dc
--- /dev/null
+++ b/src/assets/icons/png/circleBlue@2x.png
Binary files differ
diff --git a/src/assets/icons/png/circleGreen@2x.png b/src/assets/icons/png/circleGreen@2x.png
new file mode 100644
index 0000000..a1505a7
--- /dev/null
+++ b/src/assets/icons/png/circleGreen@2x.png
Binary files differ
diff --git a/src/assets/icons/png/circleOrange@2x.png b/src/assets/icons/png/circleOrange@2x.png
new file mode 100644
index 0000000..ba6807d
--- /dev/null
+++ b/src/assets/icons/png/circleOrange@2x.png
Binary files differ
diff --git a/src/assets/icons/png/circlePink@2x.png b/src/assets/icons/png/circlePink@2x.png
new file mode 100644
index 0000000..6958062
--- /dev/null
+++ b/src/assets/icons/png/circlePink@2x.png
Binary files differ
diff --git a/src/assets/icons/png/circleRed@2x.png b/src/assets/icons/png/circleRed@2x.png
new file mode 100644
index 0000000..3d9a33c
--- /dev/null
+++ b/src/assets/icons/png/circleRed@2x.png
Binary files differ
diff --git a/src/assets/icons/png/circleYellow@2x.png b/src/assets/icons/png/circleYellow@2x.png
new file mode 100644
index 0000000..7b873a0
--- /dev/null
+++ b/src/assets/icons/png/circleYellow@2x.png
Binary files differ
diff --git a/src/assets/icons/png/green@2x.png b/src/assets/icons/png/green@2x.png
new file mode 100644
index 0000000..4e9debb
--- /dev/null
+++ b/src/assets/icons/png/green@2x.png
Binary files differ
diff --git a/src/assets/icons/png/pink@2x.png b/src/assets/icons/png/pink@2x.png
new file mode 100644
index 0000000..a6de60f
--- /dev/null
+++ b/src/assets/icons/png/pink@2x.png
Binary files differ
diff --git a/src/assets/icons/png/walletBlue@2x.png b/src/assets/icons/png/walletBlue@2x.png
new file mode 100644
index 0000000..e0dc792
--- /dev/null
+++ b/src/assets/icons/png/walletBlue@2x.png
Binary files differ
diff --git a/src/assets/icons/png/walletGreen@2x.png b/src/assets/icons/png/walletGreen@2x.png
new file mode 100644
index 0000000..c68f20f
--- /dev/null
+++ b/src/assets/icons/png/walletGreen@2x.png
Binary files differ
diff --git a/src/assets/icons/png/walletOrange@2x.png b/src/assets/icons/png/walletOrange@2x.png
new file mode 100644
index 0000000..aacddc3
--- /dev/null
+++ b/src/assets/icons/png/walletOrange@2x.png
Binary files differ
diff --git a/src/assets/icons/png/walletRed@2x.png b/src/assets/icons/png/walletRed@2x.png
new file mode 100644
index 0000000..cd45c0b
--- /dev/null
+++ b/src/assets/icons/png/walletRed@2x.png
Binary files differ
diff --git a/src/assets/icons/png/walletYellow@2x.png b/src/assets/icons/png/walletYellow@2x.png
new file mode 100644
index 0000000..9d828cf
--- /dev/null
+++ b/src/assets/icons/png/walletYellow@2x.png
Binary files differ
diff --git a/src/assets/icons/png/yellow@2x.png b/src/assets/icons/png/yellow@2x.png
new file mode 100644
index 0000000..3c458c8
--- /dev/null
+++ b/src/assets/icons/png/yellow@2x.png
Binary files differ
diff --git "a/src/assets/icons/png/\346\224\257\345\207\272.png" "b/src/assets/icons/png/\346\224\257\345\207\272.png"
new file mode 100644
index 0000000..fc253ae
--- /dev/null
+++ "b/src/assets/icons/png/\346\224\257\345\207\272.png"
Binary files differ
diff --git "a/src/assets/icons/png/\346\224\257\345\207\272\351\207\221\351\242\235.png" "b/src/assets/icons/png/\346\224\257\345\207\272\351\207\221\351\242\235.png"
new file mode 100644
index 0000000..b0db95a
--- /dev/null
+++ "b/src/assets/icons/png/\346\224\257\345\207\272\351\207\221\351\242\235.png"
Binary files differ
diff --git "a/src/assets/icons/png/\346\224\266\345\205\245\345\210\227\345\270\220.png" "b/src/assets/icons/png/\346\224\266\345\205\245\345\210\227\345\270\220.png"
new file mode 100644
index 0000000..782bd2f
--- /dev/null
+++ "b/src/assets/icons/png/\346\224\266\345\205\245\345\210\227\345\270\220.png"
Binary files differ
diff --git "a/src/assets/icons/png/\346\224\266\345\205\245\346\224\266\346\254\276.png" "b/src/assets/icons/png/\346\224\266\345\205\245\346\224\266\346\254\276.png"
new file mode 100644
index 0000000..a1d3272
--- /dev/null
+++ "b/src/assets/icons/png/\346\224\266\345\205\245\346\224\266\346\254\276.png"
Binary files differ
diff --git "a/src/assets/icons/png/\346\224\266\345\205\245\351\207\221\351\242\235.png" "b/src/assets/icons/png/\346\224\266\345\205\245\351\207\221\351\242\235.png"
new file mode 100644
index 0000000..b83863a
--- /dev/null
+++ "b/src/assets/icons/png/\346\224\266\345\205\245\351\207\221\351\242\235.png"
Binary files differ
diff --git a/src/assets/icons/svg/404.svg b/src/assets/icons/svg/404.svg
new file mode 100644
index 0000000..6df5019
--- /dev/null
+++ b/src/assets/icons/svg/404.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/bug.svg b/src/assets/icons/svg/bug.svg
new file mode 100644
index 0000000..05a150d
--- /dev/null
+++ b/src/assets/icons/svg/bug.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M127.88 73.143c0 1.412-.506 2.635-1.518 3.669-1.011 1.033-2.209 1.55-3.592 1.55h-17.887c0 9.296-1.783 17.178-5.35 23.645l16.609 17.044c1.011 1.034 1.517 2.257 1.517 3.67 0 1.412-.506 2.635-1.517 3.668-.958 1.033-2.155 1.55-3.593 1.55-1.438 0-2.635-.517-3.593-1.55l-15.811-16.063a15.49 15.49 0 0 1-1.196 1.06c-.532.434-1.65 1.208-3.353 2.322a50.104 50.104 0 0 1-5.192 2.974c-1.758.87-3.94 1.658-6.546 2.364-2.607.706-5.189 1.06-7.748 1.06V47.044H58.89v73.062c-2.716 0-5.417-.367-8.106-1.102-2.688-.734-5.003-1.631-6.945-2.692a66.769 66.769 0 0 1-5.268-3.179c-1.571-1.057-2.73-1.94-3.476-2.65L33.9 109.34l-14.611 16.877c-1.066 1.14-2.344 1.711-3.833 1.711-1.277 0-2.422-.434-3.434-1.304-1.012-.978-1.557-2.187-1.635-3.627-.079-1.44.333-2.705 1.236-3.794l16.129-18.51c-3.087-6.197-4.63-13.644-4.63-22.342H5.235c-1.383 0-2.58-.517-3.592-1.55S.125 74.545.125 73.132c0-1.412.506-2.635 1.518-3.668 1.012-1.034 2.21-1.55 3.592-1.55h17.887V43.939L9.308 29.833c-1.012-1.033-1.517-2.256-1.517-3.669 0-1.412.505-2.635 1.517-3.668 1.012-1.034 2.21-1.55 3.593-1.55s2.58.516 3.593 1.55l13.813 14.106h67.396l13.814-14.106c1.012-1.034 2.21-1.55 3.592-1.55 1.384 0 2.581.516 3.593 1.55 1.012 1.033 1.518 2.256 1.518 3.668 0 1.413-.506 2.636-1.518 3.67l-13.814 14.105v23.975h17.887c1.383 0 2.58.516 3.593 1.55 1.011 1.033 1.517 2.256 1.517 3.668l-.005.01zM89.552 26.175H38.448c0-7.23 2.489-13.386 7.466-18.469C50.892 2.623 56.92.082 64 .082c7.08 0 13.108 2.541 18.086 7.624 4.977 5.083 7.466 11.24 7.466 18.469z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/build.svg b/src/assets/icons/svg/build.svg
new file mode 100644
index 0000000..97c4688
--- /dev/null
+++ b/src/assets/icons/svg/build.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1568899741379" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2054" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M960 591.424V368.96c0-0.288 0.16-0.512 0.16-0.768S960 367.68 960 367.424V192a32 32 0 0 0-32-32H96a32 32 0 0 0-32 32v175.424c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768v222.464c0 0.288-0.16 0.512-0.16 0.768s0.16 0.48 0.16 0.768V864a32 32 0 0 0 32 32h832a32 32 0 0 0 32-32v-271.04c0-0.288 0.16-0.512 0.16-0.768S960 591.68 960 591.424z m-560-31.232v-160H608v160h-208z m208 64V832h-208v-207.808H608z m-480-224h208v160H128v-160z m544 0h224v160h-224v-160zM896 224v112.192H128V224h768zM128 624.192h208V832H128v-207.808zM672 832v-207.808h224V832h-224z" p-id="2055"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/button.svg b/src/assets/icons/svg/button.svg
new file mode 100644
index 0000000..904fddc
--- /dev/null
+++ b/src/assets/icons/svg/button.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1588670460195" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1314" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M230.4 307.712c13.824 0 25.088-11.264 25.088-25.088 0-100.352 81.92-182.272 182.272-182.272s182.272 81.408 182.272 182.272c0 13.824 11.264 25.088 25.088 25.088s25.088-11.264 24.576-25.088c0-127.488-103.936-231.936-231.936-231.936S205.824 154.624 205.824 282.624c-0.512 14.336 10.752 25.088 24.576 25.088z m564.736 234.496c-11.264 0-21.504 2.048-31.232 6.144 0-44.544-40.448-81.92-88.064-81.92-14.848 0-28.16 3.584-39.936 10.24-13.824-28.16-44.544-48.128-78.848-48.128-12.288 0-24.576 2.56-35.328 7.68V284.16c0-45.568-37.888-81.92-84.48-81.92s-84.48 36.864-84.48 81.92v348.672l-69.12-112.64c-18.432-28.16-58.368-36.864-91.136-19.968-26.624 14.336-46.592 47.104-30.208 88.064 3.072 8.192 76.8 205.312 171.52 311.296 0 0 28.16 24.576 43.008 58.88 4.096 9.728 13.312 15.36 22.528 15.36 3.072 0 6.656-0.512 9.728-2.048 12.288-5.12 18.432-19.968 12.8-32.256-19.456-44.544-53.76-74.752-53.76-74.752C281.6 768 209.408 573.44 208.384 570.88c-5.12-12.8-2.56-20.992 7.168-26.112 9.216-4.608 21.504-4.608 26.112 2.56l113.152 184.32c4.096 8.704 12.8 14.336 22.528 14.336 13.824 0 25.088-10.752 25.088-25.088V284.16c0-17.92 15.36-32.256 34.816-32.256s34.816 14.336 34.816 32.256v284.16c0 13.824 10.24 25.088 24.576 25.088 13.824 0 25.088-11.264 25.088-25.088v-57.344c0-17.92 15.36-32.768 34.816-32.768 19.968 0 37.376 15.36 37.376 32.768v95.232c0 7.168 3.072 13.312 7.68 17.92 4.608 4.608 10.752 7.168 17.92 7.168 13.824 0 24.576-11.264 24.576-25.088V547.84c0-18.432 13.824-32.256 32.256-32.256 20.48 0 38.912 15.36 38.912 32.256v95.232c0 13.824 11.264 25.088 25.088 25.088s24.576-11.264 25.088-25.088v-18.944c0-18.944 12.8-32.256 30.72-32.256 18.432 0 22.528 18.944 22.528 31.744 0 1.024-11.776 99.84-50.688 173.056-30.72 58.368-45.056 112.128-51.2 146.944-2.56 13.312 6.656 26.112 19.968 28.672 1.536 0 3.072 0.512 4.608 0.512 11.776 0 22.016-8.192 24.064-20.48 5.632-31.232 18.432-79.36 46.08-132.608 43.52-81.92 55.808-186.88 56.32-193.536-0.512-50.688-29.696-83.968-72.704-83.968z"></path></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/cascader.svg b/src/assets/icons/svg/cascader.svg
new file mode 100644
index 0000000..e256024
--- /dev/null
+++ b/src/assets/icons/svg/cascader.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1576153230908" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="971" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M772.87036133 734.06115723c-43.34106445 0-80.00793458 27.93273926-93.76831055 66.57714843H475.90991211c-56.60705567 0-102.66723633-46.06018067-102.66723633-102.66723633V600.82446289h305.859375c13.76037598 38.64440918 50.42724609 66.57714844 93.76831055 66.57714844 55.12390137 0 99.94812012-44.82421875 99.94812012-99.94812012S827.9942627 467.50537109 772.87036133 467.50537109c-43.34106445 0-80.00793458 27.93273926-93.76831055 66.57714844H373.24267578V401.01062011h321.92687989c55.12390137 0 99.94812012-44.82421875 99.94812011-99.94812011V190.07312011C795.11767578 134.94921875 750.29345703 90.125 695.16955567 90.125H251.12963867C196.0057373 90.125 151.18151855 134.94921875 151.18151855 190.07312011V301.0625c0 55.12390137 44.82421875 99.94812012 99.94812012 99.94812012h55.53588867v296.96044921c0 93.35632325 75.97045898 169.32678223 169.32678224 169.32678223h203.19213866c13.76037598 38.64440918 50.42724609 66.57714844 93.76831055 66.57714844 55.12390137 0 99.94812012-44.82421875 99.94812012-99.94812012s-44.90661622-99.86572266-100.03051758-99.86572265z m0-199.89624024c18.37463379 0 33.28857422 14.91394043 33.28857422 33.28857423s-14.91394043 33.28857422-33.28857422 33.28857421-33.28857422-14.91394043-33.28857422-33.28857421 14.91394043-33.28857422 33.28857422-33.28857422zM217.75866699 301.0625V190.07312011c0-18.37463379 14.91394043-33.28857422 33.28857423-33.28857421h444.03991698c18.37463379 0 33.28857422 14.91394043 33.28857422 33.28857422V301.0625c0 18.37463379-14.91394043 33.28857422-33.28857422 33.28857422H251.12963867c-18.37463379 0-33.37097168-14.91394043-33.37097168-33.28857422z m555.11169434 566.23535156c-18.37463379 0-33.28857422-14.91394043-33.28857422-33.28857422 0-18.37463379 14.91394043-33.28857422 33.28857422-33.28857422s33.28857422 14.91394043 33.28857422 33.28857422c0.08239747 18.29223633-14.91394043 33.28857422-33.28857422 33.28857422z" p-id="972"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/chart.svg b/src/assets/icons/svg/chart.svg
new file mode 100644
index 0000000..27728fb
--- /dev/null
+++ b/src/assets/icons/svg/chart.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h36.571V128H0V54.857zM91.429 27.43H128V128H91.429V27.429zM45.714 0h36.572v128H45.714V0z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/checkbox.svg b/src/assets/icons/svg/checkbox.svg
new file mode 100644
index 0000000..013fd3a
--- /dev/null
+++ b/src/assets/icons/svg/checkbox.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575982282951" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="902" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M828.40625 90.125H195.59375C137.375 90.125 90.125 137.375 90.125 195.59375v632.8125c0 58.21875 47.25 105.46875 105.46875 105.46875h632.8125c58.21875 0 105.46875-47.25 105.46875-105.46875V195.59375c0-58.21875-47.25-105.46875-105.46875-105.46875z m52.734375 738.28125c0 29.16-23.57015625 52.734375-52.734375 52.734375H195.59375c-29.109375 0-52.734375-23.574375-52.734375-52.734375V195.59375c0-29.109375 23.625-52.734375 52.734375-52.734375h632.8125c29.16 0 52.734375 23.625 52.734375 52.734375v632.8125z" p-id="903"></path><path d="M421.52890625 709.55984375a36.28125 36.28125 0 0 1-27.55265625-12.66890625L205.17453125 476.613125a36.28546875 36.28546875 0 0 1 55.10109375-47.22890625l164.986875 192.4846875 342.16171875-298.48078125a36.2896875 36.2896875 0 0 1 47.70984375 54.68765625L445.3859375 700.6203125a36.3234375 36.3234375 0 0 1-23.85703125 8.93953125z" p-id="904"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/clipboard.svg b/src/assets/icons/svg/clipboard.svg
new file mode 100644
index 0000000..90923ff
--- /dev/null
+++ b/src/assets/icons/svg/clipboard.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.857 118.857h64V73.143H89.143c-1.902 0-3.52-.668-4.855-2.002-1.335-1.335-2.002-2.954-2.002-4.855V36.57H54.857v82.286zM73.143 16v-4.571a2.2 2.2 0 0 0-.677-1.61 2.198 2.198 0 0 0-1.609-.676H20.571c-.621 0-1.158.225-1.609.676a2.198 2.198 0 0 0-.676 1.61V16a2.2 2.2 0 0 0 .676 1.61c.451.45.988.676 1.61.676h50.285c.622 0 1.158-.226 1.61-.677.45-.45.676-.987.676-1.609zm18.286 48h21.357L91.43 42.642V64zM128 73.143v48c0 1.902-.667 3.52-2.002 4.855-1.335 1.335-2.953 2.002-4.855 2.002H52.57c-1.901 0-3.52-.667-4.854-2.002-1.335-1.335-2.003-2.953-2.003-4.855v-11.429H6.857c-1.902 0-3.52-.667-4.855-2.002C.667 106.377 0 104.759 0 102.857v-96c0-1.902.667-3.52 2.002-4.855C3.337.667 4.955 0 6.857 0h77.714c1.902 0 3.52.667 4.855 2.002 1.335 1.335 2.003 2.953 2.003 4.855V30.29c1 .622 1.856 1.29 2.569 2.003l29.147 29.147c1.335 1.335 2.478 3.145 3.429 5.43.95 2.287 1.426 4.383 1.426 6.291v-.018z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/code.svg b/src/assets/icons/svg/code.svg
new file mode 100644
index 0000000..5f9c5ab
--- /dev/null
+++ b/src/assets/icons/svg/code.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1546567861908" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2422" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M318.577778 819.2L17.066667 512l301.511111-307.2 45.511111 45.511111L96.711111 512l267.377778 261.688889zM705.422222 819.2l-45.511111-45.511111L927.288889 512l-267.377778-261.688889 45.511111-45.511111L1006.933333 512zM540.785778 221.866667l55.751111 11.150222L483.157333 802.133333l-55.751111-11.093333z" p-id="2423"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/color.svg b/src/assets/icons/svg/color.svg
new file mode 100644
index 0000000..44a81aa
--- /dev/null
+++ b/src/assets/icons/svg/color.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577252187056" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2508" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M747.59340925 691.12859384c11.51396329 0.25305413 22.43746719-0.21087818 40.74171707-1.51832482 29.35428085-2.10878421 35.84933734-2.36183835 46.47761114-0.8856895 24.71495444 3.37405491 41.12129828 21.76265671 32.47528161 47.95376084-85.57447632 258.19957947-442.00123984 249.76444099-628.67084683 50.73735554-153.47733892-159.33976008-153.09775772-414.41833795 0.92786545-573.42069196 159.71934128-162.67163983 424.03439521-166.59397897 565.78689185 0.63263534 80.38686649 94.81095318 108.34934958 169.16669549 89.11723508 230.57450162-15.01454608 47.99593598-50.61082928 77.68762207-119.77896259 114.63352789-4.89237973 2.65706845-29.35428085 15.52065436-35.84933652 19.02123633-46.94154346 25.30541465-63.51659033 41.20565021-62.20914449 58.45550757 2.95229856 39.13904114 24.16667102 52.7196135 70.98168823 53.81618115z m44.41100207 50.10472101c-19.82257471 1.43397372-32.05352527 1.940082-45.63409763 1.6448519-70.34905207-1.60267593-115.98314969-30.91478165-121.38163769-101.64341492-3.45840683-46.05585397 24.7571304-73.13264758 89.24376132-107.96976837 6.7902866-3.66928501 31.37871396-16.57504688 36.06021551-19.06341229 57.69634516-30.83042972 85.15271997-53.73183005 94.76877722-84.47790866 12.77923398-40.78389304-9.10994898-98.94417051-79.24812286-181.6507002-121.17075953-142.97559219-350.14258521-139.60153647-489.2380134 2.06660824-134.49827774 138.84237405-134.79350784 362.12048163-0.42175717 501.637667 158.53842169 168.99799328 451.9968783 181.18676788 534.57688175-11.80919339-4.68150156 0.2952301-10.71262573 0.67481131-18.72600705 1.26527069z" p-id="2509"></path><path d="M346.03865637 637.18588562a78.82636652 78.82636652 0 0 0 78.32025825-79.29029883c0-43.69401562-35.005823-79.29029883-78.32025825-79.29029882a78.82636652 78.82636652 0 0 0-78.36243338 79.29029882c0 43.69401562 35.005823 79.29029883 78.36243338 79.29029883z m0-51.7495729a27.07679361 27.07679361 0 0 1-26.5706845-27.54072593c0-15.30977536 11.97789643-27.54072593 26.5706845-27.54072592 14.55061295 0 26.57068533 12.23095057 26.57068533 27.54072592a27.07679361 27.07679361 0 0 1-26.57068533 27.54072593zM475.7289063 807.11174353a78.82636652 78.82636652 0 0 0 78.3624334-79.29029882c0-43.69401562-34.96364785-79.29029883-78.32025825-79.29029883a78.82636652 78.82636652 0 0 0-78.32025742 79.29029883c0 43.69401562 34.96364785 79.29029883 78.32025742 79.29029882z m0-51.74957208a27.07679361 27.07679361 0 0 1-26.57068532-27.54072674c0-15.30977536 12.06224753-27.54072593 26.57068532-27.54072593 14.59278892 0 26.57068533 12.23095057 26.57068453 27.54072593a27.07679361 27.07679361 0 0 1-26.57068453 27.54072674zM601.24376214 377.21492718a78.82636652 78.82636652 0 0 0 78.32025742-79.29029883c0-43.69401562-34.96364785-79.29029883-78.32025742-79.29029882a78.82636652 78.82636652 0 0 0-78.32025823 79.29029883c0 43.69401562 34.96364785 79.29029883 78.32025824 79.29029883z m1e-8-51.74957208a27.07679361 27.07679361 0 0 1-26.57068534-27.54072675c0-15.30977536 11.97789643-27.54072593 26.57068534-27.54072591 14.55061295 0 26.57068533 12.23095057 26.57068451 27.54072592a27.07679361 27.07679361 0 0 1-26.57068451 27.54072674zM378.80916809 433.85687983a78.82636652 78.82636652 0 0 0 78.32025824-79.29029883c0-43.69401562-34.96364785-79.29029883-78.32025824-79.29029802a78.82636652 78.82636652 0 0 0-78.32025742 79.29029802c0 43.69401562 34.96364785 79.29029883 78.32025742 79.29029883z m0-51.74957209a27.07679361 27.07679361 0 0 1-26.57068451-27.54072674c0-15.30977536 11.97789643-27.54072593 26.57068451-27.54072593 14.55061295 0 26.57068533 12.23095057 26.57068533 27.54072593a27.07679361 27.07679361 0 0 1-26.57068533 27.54072674z" p-id="2510"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/component.svg b/src/assets/icons/svg/component.svg
new file mode 100644
index 0000000..29c3458
--- /dev/null
+++ b/src/assets/icons/svg/component.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575804206892" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3145" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M826.56 470.016c-32.896 0-64.384 12.288-89.984 35.52l0-104.96c0-62.208-50.496-112.832-112.64-113.088L623.936 287.04 519.552 287.104C541.824 262.72 554.56 230.72 554.56 197.12c0-73.536-59.904-133.44-133.504-133.44-73.472 0-133.376 59.904-133.376 133.44 0 32.896 12.224 64.256 35.52 89.984L175.232 287.104l0 0.576C113.728 288.704 64 338.88 64 400.576l0.32 0 0.32 116.48C60.864 544.896 70.592 577.728 100.8 588.48c12.736 4.608 37.632 7.488 60.864-25.28 12.992-18.368 34.24-29.248 56.64-29.248 38.336 0 69.504 31.104 69.504 69.312 0 38.4-31.168 69.504-69.504 69.504-22.656 0-44.032-11.264-57.344-30.4C138.688 610.112 112.576 615.36 102.464 619.136c-29.824 10.752-39.104 43.776-38.144 67.392l0 160.384L64 846.912C64 909.248 114.752 960 177.216 960l446.272 0c62.4 0 113.152-50.752 113.152-113.152l0-145.024c24.384 22.272 56.384 35.008 89.984 35.008 73.536 0 133.44-59.904 133.44-133.504C960 529.92 900.096 470.016 826.56 470.016zM826.56 672.896c-22.72 0-44.032-11.264-57.344-30.4-22.272-32.384-48.448-27.136-58.56-23.36-29.824 10.752-39.04 43.776-38.08 67.392l0 160.384c0 27.136-22.016 49.152-49.152 49.152L177.216 896.064C150.08 896 128 873.984 128 846.848l0.32 0 0-145.024c24.384 22.272 56.384 35.008 89.984 35.008 73.6 0 133.504-59.904 133.504-133.504 0-73.472-59.904-133.376-133.504-133.376-32.896 0-64.32 12.288-89.984 35.52l0-104.96L128 400.512c0-27.072 22.08-49.152 49.216-49.152L177.216 351.04 334.656 350.72c3.776 0.512 7.616 0.832 11.52 0.832 24.896 0 50.752-10.816 60.032-37.056 4.544-12.736 7.424-37.568-25.344-60.736C362.624 240.768 351.68 219.52 351.68 197.12c0-38.272 31.104-69.44 69.376-69.44 38.336 0 69.504 31.168 69.504 69.44 0 22.72-11.264 44.032-30.528 57.472C427.968 276.736 433.088 302.784 436.8 313.024c10.752 29.888 43.072 39.232 67.392 38.08l119.232 0 0 0.384c27.136 0 49.152 22.08 49.152 49.152l0.256 116.48c-3.776 27.84 6.016 60.736 36.224 71.488 12.736 4.608 37.632 7.488 60.8-25.28 13.056-18.368 34.24-29.248 56.704-29.248C864.832 534.016 896 565.12 896 603.392 896 641.728 864.832 672.896 826.56 672.896z" p-id="3146"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/dashboard.svg b/src/assets/icons/svg/dashboard.svg
new file mode 100644
index 0000000..5317d37
--- /dev/null
+++ b/src/assets/icons/svg/dashboard.svg
@@ -0,0 +1 @@
+<svg width="128" height="100" xmlns="http://www.w3.org/2000/svg"><path d="M27.429 63.638c0-2.508-.893-4.65-2.679-6.424-1.786-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.465 2.662-1.785 1.774-2.678 3.916-2.678 6.424 0 2.508.893 4.65 2.678 6.424 1.786 1.775 3.94 2.662 6.465 2.662 2.524 0 4.678-.887 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm13.714-31.801c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM71.714 65.98l7.215-27.116c.285-1.23.107-2.378-.536-3.443-.643-1.064-1.56-1.762-2.75-2.094-1.19-.33-2.333-.177-3.429.462-1.095.639-1.81 1.573-2.143 2.804l-7.214 27.116c-2.857.237-5.405 1.266-7.643 3.088-2.238 1.822-3.738 4.152-4.5 6.992-.952 3.644-.476 7.098 1.429 10.364 1.905 3.265 4.69 5.37 8.357 6.317 3.667.947 7.143.474 10.429-1.42 3.285-1.892 5.404-4.66 6.357-8.305.762-2.84.619-5.607-.429-8.305-1.047-2.697-2.762-4.85-5.143-6.46zm47.143-2.342c0-2.508-.893-4.65-2.678-6.424-1.786-1.775-3.94-2.662-6.465-2.662-2.524 0-4.678.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.786 1.775 3.94 2.662 6.464 2.662 2.524 0 4.679-.887 6.465-2.662 1.785-1.775 2.678-3.916 2.678-6.424zm-45.714-45.43c0-2.509-.893-4.65-2.679-6.425C68.68 10.01 66.524 9.122 64 9.122c-2.524 0-4.679.887-6.464 2.661-1.786 1.775-2.679 3.916-2.679 6.425 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zm32 13.629c0-2.508-.893-4.65-2.679-6.424-1.785-1.775-3.94-2.662-6.464-2.662-2.524 0-4.679.887-6.464 2.662-1.786 1.774-2.679 3.916-2.679 6.424 0 2.508.893 4.65 2.679 6.424 1.785 1.774 3.94 2.662 6.464 2.662 2.524 0 4.679-.888 6.464-2.662 1.786-1.775 2.679-3.916 2.679-6.424zM128 63.638c0 12.351-3.357 23.78-10.071 34.286-.905 1.372-2.19 2.058-3.858 2.058H13.93c-1.667 0-2.953-.686-3.858-2.058C3.357 87.465 0 76.037 0 63.638c0-8.613 1.69-16.847 5.071-24.703C8.452 31.08 13 24.312 18.714 18.634c5.715-5.68 12.524-10.199 20.429-13.559C47.048 1.715 55.333.035 64 .035c8.667 0 16.952 1.68 24.857 5.04 7.905 3.36 14.714 7.88 20.429 13.559 5.714 5.678 10.262 12.446 13.643 20.301 3.38 7.856 5.071 16.09 5.071 24.703z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/date-range.svg b/src/assets/icons/svg/date-range.svg
new file mode 100644
index 0000000..fda571e
--- /dev/null
+++ b/src/assets/icons/svg/date-range.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774833889" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1376" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M887.466667 192.853333h-100.693334V119.466667c0-10.24-6.826667-17.066667-17.066666-17.066667s-17.066667 6.826667-17.066667 17.066667v73.386666H303.786667V119.466667c0-10.24-6.826667-17.066667-17.066667-17.066667s-17.066667 6.826667-17.066667 17.066667v73.386666H168.96c-46.08 0-85.333333 37.546667-85.333333 85.333334V836.266667c0 46.08 37.546667 85.333333 85.333333 85.333333H887.466667c46.08 0 85.333333-37.546667 85.333333-85.333333V278.186667c0-47.786667-37.546667-85.333333-85.333333-85.333334z m-718.506667 34.133334h100.693333v66.56c0 10.24 6.826667 17.066667 17.066667 17.066666s17.066667-6.826667 17.066667-17.066666v-66.56h450.56v66.56c0 10.24 6.826667 17.066667 17.066666 17.066666s17.066667-6.826667 17.066667-17.066666v-66.56H887.466667c27.306667 0 51.2 22.186667 51.2 51.2v88.746666H117.76v-88.746666c0-29.013333 22.186667-51.2 51.2-51.2zM887.466667 887.466667H168.96c-27.306667 0-51.2-22.186667-51.2-51.2V401.066667H938.666667V836.266667c0 27.306667-22.186667 51.2-51.2 51.2z" p-id="1377"></path><path d="M858.453333 493.226667H327.68c-10.24 0-17.066667 6.826667-17.066667 17.066666v114.346667h-116.053333c-10.24 0-17.066667 6.826667-17.066667 17.066667v133.12c0 10.24 6.826667 17.066667 17.066667 17.066666H460.8c10.24 0 17.066667-6.826667 17.066667-17.066666v-114.346667h380.586666c10.24 0 17.066667-6.826667 17.066667-17.066667v-133.12c0-10.24-6.826667-17.066667-17.066667-17.066666z m-413.013333 34.133333v97.28h-98.986667v-97.28h98.986667z m-230.4 131.413333h98.986667v98.986667h-98.986667v-98.986667z m131.413333 97.28v-97.28h98.986667v97.28h-98.986667z m133.12-228.693333h97.28v98.986667h-97.28v-98.986667z m131.413334 0h98.986666v98.986667h-98.986666v-98.986667z m230.4 97.28h-98.986667v-98.986667h98.986667v98.986667z" p-id="1378"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/date.svg b/src/assets/icons/svg/date.svg
new file mode 100644
index 0000000..52dc73e
--- /dev/null
+++ b/src/assets/icons/svg/date.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577186573535" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1068" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M479.85714249 608.42857168h64.28571502c19.28571417 0 32.14285751-12.85714249 32.14285664-32.14285751s-12.85714249-32.14285751-32.14285664-32.14285664h-64.28571504c-19.28571417 0-32.14285751 12.85714249-32.14285664 32.14285662s12.85714249 32.14285751 32.14285664 32.14285753z m-2e-8 122.14285665h64.28571504c19.28571417 0 32.14285751-12.85714249 32.14285664-32.14285665s-12.85714249-32.14285751-32.14285664-32.14285751h-64.28571504c-19.28571417 0-32.14285751 12.85714249-32.14285664 32.14285751s12.85714249 32.14285751 32.14285664 32.14285664z m353.57142921-559.28571416h-128.57142921v-32.14285664c0-19.28571417-12.85714249-32.14285751-32.14285664-32.14285753s-32.14285751 12.85714249-32.14285751 32.14285753v32.14285664h-257.14285665v-32.14285664c0-19.28571417-12.85714249-32.14285751-32.14285752-32.14285753s-32.14285751 12.85714249-32.14285664 32.14285753v32.14285664h-128.57142919c-70.71428585 0-128.57142832 57.85714249-128.57142832 122.14285751v501.42857081c0 70.71428585 57.85714249 128.57142832 128.57142832 122.14285751h642.85714335c70.71428585 0 128.57142832-57.85714249 128.57142833-122.14285751v-501.42857081c0-70.71428585-57.85714249-122.14285753-128.57142833-122.14285751z m64.28571415 623.57142832c0 32.14285751-32.14285751 64.28571415-64.28571416 64.28571504h-642.85714335c-32.14285751 0-64.28571415-25.71428583-64.28571417-64.28571504v-372.85714249h771.42857168v372.85714249z m0-437.14285664h-771.42857168v-64.28571417c0-32.14285751 32.14285751-64.28571415 64.28571417-64.28571415h128.57142919v32.14285664c0 19.28571417 12.85714249 32.14285751 32.14285664 32.14285751s32.14285751-12.85714249 32.14285753-32.14285751v-32.14285664h257.14285665v32.14285664c0 19.28571417 12.85714249 32.14285751 32.1428575 32.14285751s32.14285751-12.85714249 32.14285664-32.14285751v-32.14285664h128.57142921c32.14285751 0 64.28571415 25.71428583 64.28571415 64.28571415v64.28571417z m-610.71428583 372.85714247h64.28571415c19.28571417 0 32.14285751-12.85714249 32.14285753-32.14285664s-12.85714249-32.14285751-32.14285753-32.14285751h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285751 32.14285751s12.85714249 32.14285751 32.14285751 32.14285665z m385.71428583-122.14285664h64.28571417c19.28571417 0 32.14285751-12.85714249 32.14285751-32.14285751s-12.85714249-32.14285751-32.14285751-32.14285664h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285753 32.14285664s12.85714249 32.14285751 32.14285753 32.14285751z m-385.71428583 0h64.28571415c19.28571417 0 32.14285751-12.85714249 32.14285753-32.14285751s-12.85714249-32.14285751-32.14285753-32.14285664h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285751 32.14285664s12.85714249 32.14285751 32.14285751 32.14285751z m385.71428583 122.14285665h64.28571417c19.28571417 0 32.14285751-12.85714249 32.14285751-32.14285665s-12.85714249-32.14285751-32.14285751-32.14285751h-64.28571415c-19.28571417 0-32.14285751 12.85714249-32.14285753 32.14285751s12.85714249 32.14285751 32.14285753 32.14285665z" p-id="1069"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/dict.svg b/src/assets/icons/svg/dict.svg
new file mode 100644
index 0000000..4849377
--- /dev/null
+++ b/src/assets/icons/svg/dict.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035680909" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3601" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1002.0848 744.672l-33.568 10.368c0.96 7.264 2.144 14.304 2.144 21.76 0 7.328-1.184 14.432-2.368 21.568l33.792 10.56c7.936 2.24 14.496 7.616 18.336 14.752 3.84 7.328 4.672 15.808 1.952 23.552-5.376 16-23.168 24.672-39.936 19.68l-34.176-10.624c-7.136 12.8-15.776 24.672-26.208 35.2l20.8 27.488a28.96 28.96 0 0 1 5.824 22.816 29.696 29.696 0 0 1-12.704 19.616 32.544 32.544 0 0 1-44.416-6.752l-20.8-27.552c-13.696 6.56-28.192 11.2-43.008 13.888v33.632c0 16.736-14.112 30.432-31.648 30.432-17.6 0-31.872-13.696-31.872-30.432v-33.632a167.616 167.616 0 0 1-42.88-13.888l-20.928 27.552c-10.72 13.76-30.08 16.64-44.288 6.752a29.632 29.632 0 0 1-12.704-19.616 29.28 29.28 0 0 1 5.696-22.816l20.896-27.808a166.72 166.72 0 0 1-27.008-34.688l-33.376 10.432c-16.8 5.184-34.56-3.552-39.936-19.616a29.824 29.824 0 0 1 20.224-38.24l33.472-10.432c-0.8-7.264-2.016-14.304-2.016-21.824 0-7.36 1.184-14.496 2.304-21.632l-33.792-10.368c-16.672-5.376-25.632-22.496-20.224-38.432 5.376-16 23.136-24.672 39.936-19.68l34.016 10.752c7.328-12.672 15.84-24.8 26.336-35.328l-20.8-27.552a29.44 29.44 0 0 1 6.944-42.432 32.704 32.704 0 0 1 44.384 6.752l20.832 27.616c13.696-6.432 28.224-11.2 43.104-13.952v-33.568c0-16.736 14.048-30.432 31.648-30.432 17.536 0 31.808 13.568 31.808 30.432v33.504c15.072 2.688 29.344 7.808 42.848 14.016l20.992-27.616a32.48 32.48 0 0 1 44.224-6.752 29.568 29.568 0 0 1 7.136 42.432l-21.024 27.808c10.432 10.432 19.872 21.888 27.04 34.752l33.376-10.432c16.768-5.12 34.56 3.68 39.936 19.68 5.536 15.936-3.712 33.056-20.32 38.304z m-206.016-74.432c-61.344 0-111.136 47.808-111.136 106.56 0 58.88 49.792 106.496 111.136 106.496 61.312 0 111.104-47.616 111.104-106.496 0-58.752-49.792-106.56-111.104-106.56z" p-id="3602"></path><path d="M802.7888 57.152h-76.448c0-22.08-21.024-38.24-42.848-38.24H39.3968a39.68 39.68 0 0 0-39.36 40.032v795.616s41.888 120.192 110.752 120.192H673.2848a227.488 227.488 0 0 1-107.04-97.44H117.6368s-40.608-13.696-40.608-41.248l470.304-0.256 1.664 3.36a227.68 227.68 0 0 1-12.64-73.632c0-60.576 24-118.624 66.88-161.44a228.352 228.352 0 0 1 123.552-63.392l-3.2 0.288 2.144-424.672h38.208l0.576 421.024c27.04 0 52.672 4.8 76.64 13.344V101.536c0.032 0-6.304-44.384-38.368-44.384zM149.7648 514.336H72.3888v-77.408H149.7648v77.408z m0-144.32H72.3888v-77.44H149.7648v77.44z m0-137.248H72.3888v-77.44H149.7648v77.44z m501.856 281.568H206.0848v-77.408h445.536v77.408z m0-144.32H206.0848v-77.44h445.536v77.44z m0-137.248H206.0848v-77.44h445.536v77.44z" p-id="3603"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/documentation.svg b/src/assets/icons/svg/documentation.svg
new file mode 100644
index 0000000..7043122
--- /dev/null
+++ b/src/assets/icons/svg/documentation.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M71.984 44.815H115.9L71.984 9.642v35.173zM16.094.05h63.875l47.906 38.37v76.74c0 3.392-1.682 6.645-4.677 9.044-2.995 2.399-7.056 3.746-11.292 3.746H16.094c-4.236 0-8.297-1.347-11.292-3.746-2.995-2.399-4.677-5.652-4.677-9.044V12.84C.125 5.742 7.23.05 16.094.05zm71.86 102.32V89.58h-71.86v12.79h71.86zm23.952-25.58V64H16.094v12.79h95.812z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/download.svg b/src/assets/icons/svg/download.svg
new file mode 100644
index 0000000..c896951
--- /dev/null
+++ b/src/assets/icons/svg/download.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1569915748289" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3062" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M768.35456 416a256 256 0 1 0-512 0 192 192 0 1 0 0 384v64a256 256 0 0 1-58.88-505.216 320.128 320.128 0 0 1 629.76 0A256.128 256.128 0 0 1 768.35456 864v-64a192 192 0 0 0 0-384z m-512 384h64v64H256.35456v-64z m448 0h64v64h-64v-64z" fill="#333333" p-id="3063"></path><path d="M539.04256 845.248V512.192a32.448 32.448 0 0 0-32-32.192c-17.664 0-32 14.912-32 32.192v333.056l-36.096-36.096a32.192 32.192 0 0 0-45.056 0.192 31.616 31.616 0 0 0-0.192 45.056l90.88 90.944a31.36 31.36 0 0 0 22.528 9.088 30.08 30.08 0 0 0 22.4-9.088l90.88-90.88a32.192 32.192 0 0 0-0.192-45.12 31.616 31.616 0 0 0-45.056-0.192l-36.096 36.096z" fill="#333333" p-id="3064"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/drag.svg b/src/assets/icons/svg/drag.svg
new file mode 100644
index 0000000..4185d3c
--- /dev/null
+++ b/src/assets/icons/svg/drag.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M73.137 29.08h-9.209 29.7L63.886.093 34.373 29.08h20.49v27.035H27.238v17.948h27.625v27.133h18.274V74.063h27.41V56.115h-27.41V29.08zm-9.245 98.827l27.518-26.711H36.59l27.302 26.71zM.042 64.982l27.196 27.029V38.167L.042 64.982zm100.505-26.815V92.01l27.41-27.029-27.41-26.815z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/druid.svg b/src/assets/icons/svg/druid.svg
new file mode 100644
index 0000000..a2b4b4e
--- /dev/null
+++ b/src/assets/icons/svg/druid.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036347051" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5853" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M832 128H192a64.19 64.19 0 0 0-64 64v640a64.19 64.19 0 0 0 64 64h640a64.19 64.19 0 0 0 64-64V192a64.19 64.19 0 0 0-64-64z m0 703.89l-0.11 0.11H192.11l-0.11-0.11V768h640zM832 544H720L605.6 696.54 442.18 435.07 333.25 544H192v-64h114.75l147.07-147.07L610.4 583.46 688 480h144z m0-288H192v-63.89l0.11-0.11h639.78l0.11 0.11z" p-id="5854"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/edit.svg b/src/assets/icons/svg/edit.svg
new file mode 100644
index 0000000..d26101f
--- /dev/null
+++ b/src/assets/icons/svg/edit.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M106.133 67.2a4.797 4.797 0 0 0-4.8 4.8c0 .187.014.36.027.533h-.027V118.4H9.6V26.667h50.133c2.654 0 4.8-2.147 4.8-4.8 0-2.654-2.146-4.8-4.8-4.8H9.6a9.594 9.594 0 0 0-9.6 9.6V118.4c0 5.307 4.293 9.6 9.6 9.6h91.733c5.307 0 9.6-4.293 9.6-9.6V72.533h-.026c.013-.173.026-.346.026-.533 0-2.653-2.146-4.8-4.8-4.8z"/><path d="M125.16 13.373L114.587 2.8c-3.747-3.747-9.854-3.72-13.6.027l-52.96 52.96a4.264 4.264 0 0 0-.907 1.36L33.813 88.533c-.746 1.76-.226 3.534.907 4.68 1.133 1.147 2.92 1.667 4.693.92l31.4-13.293c.507-.213.96-.52 1.36-.907l52.96-52.96c3.747-3.746 3.774-9.853.027-13.6zM66.107 72.4l-18.32 7.76 7.76-18.32L92.72 24.667l10.56 10.56L66.107 72.4zm52.226-52.227l-8.266 8.267-10.56-10.56 8.266-8.267.027-.026 10.56 10.56-.027.026z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/education.svg b/src/assets/icons/svg/education.svg
new file mode 100644
index 0000000..7bfb01d
--- /dev/null
+++ b/src/assets/icons/svg/education.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M88.883 119.565c-7.284 0-19.434 2.495-21.333 8.25v.127c-4.232.13-5.222 0-7.108 0-1.895-5.76-14.045-8.256-21.333-8.256H0V0h42.523c9.179 0 17.109 5.47 21.47 13.551C68.352 5.475 76.295 0 85.478 0H128v119.57l-39.113-.005h-.004zM60.442 24.763c0-9.651-8.978-16.507-17.777-16.507H7.108V111.43H39.11c7.054-.14 18.177.082 21.333 6.12v-4.628c-.134-5.722-.004-13.522 0-13.832V27.413l.004-2.655-.004.005zm60.442-16.517h-35.55c-8.802 0-17.78 6.856-17.78 16.493v74.259c.004.32.138 8.115 0 13.813v4.627c3.155-6.022 14.279-6.26 21.333-6.114h32V8.25l-.003-.005z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/email.svg b/src/assets/icons/svg/email.svg
new file mode 100644
index 0000000..74d25e2
--- /dev/null
+++ b/src/assets/icons/svg/email.svg
@@ -0,0 +1 @@
+<svg width="128" height="96" xmlns="http://www.w3.org/2000/svg"><path d="M64.125 56.975L120.188.912A12.476 12.476 0 0 0 115.5 0h-103c-1.588 0-3.113.3-4.513.838l56.138 56.137z"/><path d="M64.125 68.287l-62.3-62.3A12.42 12.42 0 0 0 0 12.5v71C0 90.4 5.6 96 12.5 96h103c6.9 0 12.5-5.6 12.5-12.5v-71a12.47 12.47 0 0 0-1.737-6.35L64.125 68.287z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/example.svg b/src/assets/icons/svg/example.svg
new file mode 100644
index 0000000..46f42b5
--- /dev/null
+++ b/src/assets/icons/svg/example.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M96.258 57.462h31.421C124.794 27.323 100.426 2.956 70.287.07v31.422a32.856 32.856 0 0 1 25.971 25.97zm-38.796-25.97V.07C27.323 2.956 2.956 27.323.07 57.462h31.422a32.856 32.856 0 0 1 25.97-25.97zm12.825 64.766v31.421c30.46-2.885 54.507-27.253 57.713-57.712H96.579c-2.886 13.466-13.146 23.726-26.292 26.291zM31.492 70.287H.07c2.886 30.46 27.253 54.507 57.713 57.713V96.579c-13.466-2.886-23.726-13.146-26.291-26.292z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/excel.svg b/src/assets/icons/svg/excel.svg
new file mode 100644
index 0000000..74d97b8
--- /dev/null
+++ b/src/assets/icons/svg/excel.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.208 16.576v8.384h38.72v5.376h-38.72v8.704h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.576h38.72v5.376h-38.72v8.512h38.72v5.376h-38.72v11.136H128v-94.72H78.208zM0 114.368L72.128 128V0L0 13.632v100.736z"/><path d="M28.672 82.56h-11.2l14.784-23.488-14.08-22.592h11.52l8.192 14.976 8.448-14.976h11.136l-14.08 22.208L58.368 82.56H46.656l-8.768-15.68z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/exit-fullscreen.svg b/src/assets/icons/svg/exit-fullscreen.svg
new file mode 100644
index 0000000..485c128
--- /dev/null
+++ b/src/assets/icons/svg/exit-fullscreen.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M49.217 41.329l-.136-35.24c-.06-2.715-2.302-4.345-5.022-4.405h-3.65c-2.712-.06-4.866 2.303-4.806 5.016l.152 19.164-24.151-23.79a6.698 6.698 0 0 0-9.499 0 6.76 6.76 0 0 0 0 9.526l23.93 23.713-18.345.074c-2.712-.069-5.228 1.813-5.64 5.02v3.462c.069 2.721 2.31 4.97 5.022 5.03l35.028-.207c.052.005.087.025.133.025l2.457.054a4.626 4.626 0 0 0 3.436-1.38c.88-.874 1.205-2.096 1.169-3.462l-.262-2.465c0-.048.182-.081.182-.136h.002zm52.523 51.212l18.32-.073c2.713.06 5.224-1.609 5.64-4.815v-3.462c-.068-2.722-2.317-4.97-5.021-5.04l-34.58.21c-.053 0-.086-.021-.138-.021l-2.451-.06a4.64 4.64 0 0 0-3.445 1.381c-.885.868-1.201 2.094-1.174 3.46l.27 2.46c.005.06-.177.095-.177.141l.141 34.697c.069 2.713 2.31 4.338 5.022 4.397l3.45.006c2.705.062 4.867-2.31 4.8-5.026l-.153-18.752 24.151 23.946a6.69 6.69 0 0 0 9.494 0 6.747 6.747 0 0 0 0-9.523L101.74 92.54v.001zM48.125 80.662a4.636 4.636 0 0 0-3.437-1.382l-2.457.06c-.05 0-.082.022-.137.022l-35.025-.21c-2.712.07-4.957 2.318-5.022 5.04v3.462c.409 3.206 2.925 4.874 5.633 4.814l18.554.06-24.132 23.928c-2.62 2.626-2.62 6.89 0 9.524a6.694 6.694 0 0 0 9.496 0l24.155-23.79-.155 18.866c-.06 2.722 2.094 5.093 4.801 5.025h3.65c2.72-.069 4.962-1.685 5.022-4.406l.141-34.956c0-.05-.182-.082-.182-.136l.262-2.46c.03-1.366-.286-2.592-1.166-3.46h-.001zM80.08 47.397a4.62 4.62 0 0 0 3.443 1.374l2.45-.054c.055 0 .088-.02.143-.028l35.08.21c2.712-.062 4.953-2.312 5.021-5.033l.009-3.463c-.417-3.211-2.937-5.084-5.64-5.025l-18.615-.073 23.917-23.715c2.63-2.623 2.63-6.879.008-9.513a6.691 6.691 0 0 0-9.494 0L92.251 26.016l.155-19.312c.065-2.713-2.097-5.085-4.802-5.025h-3.45c-2.713.069-4.954 1.693-5.022 4.406l-.139 35.247c0 .054.18.088.18.136l-.267 2.465c-.028 1.366.288 2.588 1.174 3.463v.001z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/eye-open.svg b/src/assets/icons/svg/eye-open.svg
new file mode 100644
index 0000000..88dcc98
--- /dev/null
+++ b/src/assets/icons/svg/eye-open.svg
@@ -0,0 +1 @@
+<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><defs><style/></defs><path d="M512 128q69.675 0 135.51 21.163t115.498 54.997 93.483 74.837 73.685 82.006 51.67 74.837 32.17 54.827L1024 512q-2.347 4.992-6.315 13.483T998.87 560.17t-31.658 51.669-44.331 59.99-56.832 64.34-69.504 60.16-82.347 51.5-94.848 34.687T512 896q-69.675 0-135.51-21.163t-115.498-54.826-93.483-74.326-73.685-81.493-51.67-74.496-32.17-54.997L0 513.707q2.347-4.992 6.315-13.483t18.816-34.816 31.658-51.84 44.331-60.33 56.832-64.683 69.504-60.331 82.347-51.84 94.848-34.816T512 128.085zm0 85.333q-46.677 0-91.648 12.331t-81.152 31.83-70.656 47.146-59.648 54.485-48.853 57.686-37.675 52.821-26.325 43.99q12.33 21.674 26.325 43.52t37.675 52.351 48.853 57.003 59.648 53.845T339.2 767.02t81.152 31.488T512 810.667t91.648-12.331 81.152-31.659 70.656-46.848 59.648-54.186 48.853-57.344 37.675-52.651T927.957 512q-12.33-21.675-26.325-43.648t-37.675-52.65-48.853-57.345-59.648-54.186-70.656-46.848-81.152-31.659T512 213.334zm0 128q70.656 0 120.661 50.006T682.667 512 632.66 632.661 512 682.667 391.339 632.66 341.333 512t50.006-120.661T512 341.333zm0 85.334q-35.328 0-60.33 25.002T426.666 512t25.002 60.33T512 597.334t60.33-25.002T597.334 512t-25.002-60.33T512 426.666z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/eye.svg b/src/assets/icons/svg/eye.svg
new file mode 100644
index 0000000..16ed2d8
--- /dev/null
+++ b/src/assets/icons/svg/eye.svg
@@ -0,0 +1 @@
+<svg width="128" height="64" xmlns="http://www.w3.org/2000/svg"><path d="M127.072 7.994c1.37-2.208.914-5.152-.914-6.87-2.056-1.717-4.797-1.226-6.396.982-.229.245-25.586 32.382-55.74 32.382-29.24 0-55.74-32.382-55.968-32.627-1.6-1.963-4.57-2.208-6.397-.49C-.17 3.086-.399 6.275 1.2 8.238c.457.736 5.94 7.36 14.62 14.72L4.17 35.96c-1.828 1.963-1.6 5.152.228 6.87.457.98 1.6 1.471 2.742 1.471s2.284-.49 3.198-1.472l12.564-13.983c5.94 4.416 13.021 8.587 20.788 11.53l-4.797 17.418c-.685 2.699.686 5.397 3.198 6.133h1.37c2.057 0 3.884-1.472 4.341-3.68L52.6 42.83c3.655.736 7.538 1.227 11.422 1.227 3.883 0 7.767-.49 11.422-1.227l4.797 17.173c.457 2.208 2.513 3.68 4.34 3.68.457 0 .914 0 1.143-.246 2.513-.736 3.883-3.434 3.198-6.133l-4.797-17.172c7.767-2.944 14.848-7.114 20.788-11.53l12.336 13.738c.913.981 2.056 1.472 3.198 1.472s2.284-.49 3.198-1.472c1.828-1.963 1.828-4.906.228-6.87l-11.65-13.001c9.366-7.36 14.849-14.474 14.849-14.474z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/form.svg b/src/assets/icons/svg/form.svg
new file mode 100644
index 0000000..dcbaa18
--- /dev/null
+++ b/src/assets/icons/svg/form.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.068 23.784c-1.02 0-1.877-.32-2.572-.96a8.588 8.588 0 0 1-1.738-2.237 11.524 11.524 0 0 1-1.042-2.621c-.232-.895-.348-1.641-.348-2.238V0h.278c.834 0 1.622.085 2.363.256.742.17 1.645.575 2.711 1.214 1.066.64 2.363 1.535 3.892 2.686 1.53 1.15 3.453 2.664 5.77 4.54 2.502 2.045 4.494 3.771 5.977 5.178 1.483 1.406 2.618 2.6 3.406 3.58.787.98 1.274 1.812 1.46 2.494.185.682.277 1.278.277 1.79v2.046H84.068zM127.3 84.01c.278.682.464 1.535.556 2.558.093 1.023-.37 2.003-1.39 2.94-.463.427-.88.832-1.25 1.215-.372.384-.696.704-.974.96a6.69 6.69 0 0 1-.973.767l-11.816-10.741a44.331 44.331 0 0 0 1.877-1.535 31.028 31.028 0 0 1 1.737-1.406c1.112-.938 2.317-1.343 3.615-1.215 1.297.128 2.363.405 3.197.83.927.427 1.923 1.173 2.989 2.239 1.065 1.065 1.876 2.195 2.432 3.388zM78.23 95.902c2.038 0 3.752-.511 5.143-1.534l-26.969 25.83H18.037c-1.761 0-3.684-.47-5.77-1.407a24.549 24.549 0 0 1-5.838-3.709 21.373 21.373 0 0 1-4.518-5.306c-1.204-2.003-1.807-4.07-1.807-6.202V16.495c0-1.79.44-3.665 1.32-5.626A18.41 18.41 0 0 1 5.04 5.562a21.798 21.798 0 0 1 5.213-3.964C12.198.533 14.237 0 16.37 0h53.24v15.984c0 1.62.278 3.367.834 5.242a16.704 16.704 0 0 0 2.572 5.179c1.159 1.577 2.665 2.898 4.518 3.964 1.853 1.066 4.078 1.598 6.673 1.598h20.295v42.325L85.458 92.45c1.02-1.364 1.529-2.856 1.529-4.476 0-2.216-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1c-2.409 0-4.448.789-6.116 2.366-1.668 1.577-2.502 3.474-2.502 5.69 0 2.217.834 4.092 2.502 5.626 1.668 1.535 3.707 2.302 6.117 2.302h52.13zM26.1 47.951c-2.41 0-4.449.789-6.117 2.366-1.668 1.577-2.502 3.473-2.502 5.69 0 2.216.834 4.092 2.502 5.626 1.668 1.534 3.707 2.302 6.117 2.302h52.13c2.409 0 4.47-.768 6.185-2.302 1.715-1.534 2.572-3.41 2.572-5.626 0-2.217-.857-4.113-2.572-5.69-1.714-1.577-3.776-2.366-6.186-2.366H26.1zm52.407 64.063l1.807-1.663 3.476-3.196a479.75 479.75 0 0 0 4.587-4.284 500.757 500.757 0 0 1 5.004-4.667c3.985-3.666 8.48-7.758 13.485-12.276l11.677 10.741-13.485 12.404-5.004 4.603-4.587 4.22a179.46 179.46 0 0 0-3.267 3.068c-.88.853-1.367 1.322-1.46 1.407-.463.341-.973.703-1.529 1.087-.556.383-1.112.703-1.668.959-.556.256-1.413.575-2.572.959a83.5 83.5 0 0 1-3.545 1.087 72.2 72.2 0 0 1-3.475.895c-1.112.256-1.946.426-2.502.511-1.112.17-1.854.043-2.224-.383-.371-.426-.464-1.151-.278-2.174.092-.511.278-1.279.556-2.302.278-1.023.602-2.067.973-3.132l1.042-3.005c.325-.938.58-1.577.765-1.918a10.157 10.157 0 0 1 2.224-2.941z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/fullscreen.svg b/src/assets/icons/svg/fullscreen.svg
new file mode 100644
index 0000000..0e86b6f
--- /dev/null
+++ b/src/assets/icons/svg/fullscreen.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M38.47 52L52 38.462l-23.648-23.67L43.209 0H.035L0 43.137l14.757-14.865L38.47 52zm74.773 47.726L89.526 76 76 89.536l23.648 23.672L84.795 128h43.174L128 84.863l-14.757 14.863zM89.538 52l23.668-23.648L128 43.207V.038L84.866 0 99.73 14.76 76 38.472 89.538 52zM38.46 76L14.792 99.651 0 84.794v43.173l43.137.033-14.865-14.757L52 89.53 38.46 76z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/github.svg b/src/assets/icons/svg/github.svg
new file mode 100644
index 0000000..db0a0d4
--- /dev/null
+++ b/src/assets/icons/svg/github.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1581238998885" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4187" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M511.542857 14.057143C228.914286 13.942857 0 242.742857 0 525.142857 0 748.457143 143.2 938.285714 342.628571 1008c26.857143 6.742857 22.742857-12.342857 22.742858-25.371429v-88.571428c-155.085714 18.171429-161.371429-84.457143-171.771429-101.6C172.571429 756.571429 122.857143 747.428571 137.714286 730.285714c35.314286-18.171429 71.314286 4.571429 113.028571 66.171429 30.171429 44.685714 89.028571 37.142857 118.857143 29.714286 6.514286-26.857143 20.457143-50.857143 39.657143-69.485715-160.685714-28.8-227.657143-126.857143-227.657143-243.428571 0-56.571429 18.628571-108.571429 55.2-150.514286-23.314286-69.142857 2.171429-128.342857 5.6-137.142857 66.4-5.942857 135.428571 47.542857 140.8 51.771429 37.714286-10.171429 80.8-15.542857 129.028571-15.542858 48.457143 0 91.657143 5.6 129.714286 15.885715 12.914286-9.828571 76.914286-55.771429 138.628572-50.171429 3.314286 8.8 28.228571 66.628571 6.285714 134.857143 37.028571 42.057143 55.885714 94.514286 55.885714 151.2 0 116.8-67.428571 214.971429-228.571428 243.314286a145.714286 145.714286 0 0 1 43.542857 104v128.571428c0.914286 10.285714 0 20.457143 17.142857 20.457143 202.4-68.228571 348.114286-259.428571 348.114286-484.685714 0-282.514286-229.028571-511.2-511.428572-511.2z" p-id="4188"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/guide.svg b/src/assets/icons/svg/guide.svg
new file mode 100644
index 0000000..b271001
--- /dev/null
+++ b/src/assets/icons/svg/guide.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.482 70.131l36.204 16.18 69.932-65.485-61.38 70.594 46.435 18.735c1.119.425 2.397-.17 2.797-1.363v-.085L127.998.047 1.322 65.874c-1.12.597-1.519 1.959-1.04 3.151.32.511.72.937 1.2 1.107zm44.676 57.821L64.22 107.26l-18.062-7.834v28.527z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/icon.svg b/src/assets/icons/svg/icon.svg
new file mode 100644
index 0000000..82be8ee
--- /dev/null
+++ b/src/assets/icons/svg/icon.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.147.062a13 13 0 0 1 4.94.945c1.55.63 2.907 1.526 4.069 2.688a13.148 13.148 0 0 1 2.761 4.069c.678 1.55 1.017 3.245 1.017 5.086v102.3c0 3.681-1.187 6.733-3.56 9.155-2.373 2.422-5.352 3.633-8.937 3.633H12.992c-3.875 0-7-1.26-9.373-3.779-2.373-2.518-3.56-5.667-3.56-9.445V12.704c0-3.39 1.163-6.345 3.488-8.863C5.872 1.32 8.972.062 12.847.062h102.3zM81.434 109.047c1.744 0 3.003-.412 3.778-1.235.775-.824 1.163-1.914 1.163-3.27 0-1.26-.388-2.325-1.163-3.197-.775-.872-2.034-1.307-3.778-1.307H72.57c.097-.194.145-.485.145-.872V27.09h9.01c1.743 0 2.954-.436 3.633-1.308.678-.872 1.017-1.938 1.017-3.197 0-1.26-.34-2.325-1.017-3.197-.679-.872-1.89-1.308-3.633-1.308H46.268c-1.743 0-2.954.436-3.632 1.308-.678.872-1.018 1.938-1.018 3.197 0 1.26.34 2.325 1.018 3.197.678.872 1.889 1.308 3.632 1.308h8.138v72.075c0 .193.024.339.073.436.048.096.072.242.072.436H46.56c-1.744 0-3.003.435-3.778 1.307-.775.872-1.163 1.938-1.163 3.197 0 1.356.388 2.446 1.163 3.27.775.823 2.034 1.235 3.778 1.235h34.875z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/input.svg b/src/assets/icons/svg/input.svg
new file mode 100644
index 0000000..ab91381
--- /dev/null
+++ b/src/assets/icons/svg/input.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802859706" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3102" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 224H128c-35.2 0-64 28.8-64 64v448c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V288c0-35.2-28.8-64-64-64z m0 480c0 19.2-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V320c0-19.2 12.8-32 32-32h704c19.2 0 32 12.8 32 32v384z" p-id="3103"></path><path d="M224 352c-19.2 0-32 12.8-32 32v256c0 16 12.8 32 32 32s32-12.8 32-32V384c0-16-12.8-32-32-32z" p-id="3104"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/international.svg b/src/assets/icons/svg/international.svg
new file mode 100644
index 0000000..e9b56ee
--- /dev/null
+++ b/src/assets/icons/svg/international.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M83.287 103.01c-1.57-3.84-6.778-10.414-15.447-19.548-2.327-2.444-2.182-4.306-1.338-9.862v-.64c.553-3.81 1.513-6.05 14.313-8.087 6.516-1.018 8.203 1.57 10.589 5.178l.785 1.193a12.625 12.625 0 0 0 6.43 5.207c1.134.524 2.53 1.164 4.421 2.24 4.596 2.53 4.596 5.41 4.596 11.753v.727a26.91 26.91 0 0 1-5.178 17.454 59.055 59.055 0 0 1-19.025 11.026c3.49-6.546.814-14.313 0-16.553l-.146-.087zM64 5.12a58.502 58.502 0 0 1 25.484 5.818 54.313 54.313 0 0 0-12.859 10.327c-.93 1.28-1.716 2.473-2.472 3.579-2.444 3.694-3.637 5.352-5.818 5.614a25.105 25.105 0 0 1-4.219 0c-4.276-.29-10.094-.64-11.956 4.422-1.193 3.23-1.396 11.956 2.444 16.495.66 1.077.778 2.4.32 3.578a7.01 7.01 0 0 1-2.066 3.229 18.938 18.938 0 0 1-2.909-2.91 18.91 18.91 0 0 0-8.32-6.603c-1.25-.349-2.647-.64-3.985-.93-3.782-.786-8.03-1.688-9.019-3.812a14.895 14.895 0 0 1-.727-5.818 21.935 21.935 0 0 0-1.396-9.25 8.873 8.873 0 0 0-5.557-4.946A58.705 58.705 0 0 1 64 5.12zM0 64c0 35.346 28.654 64 64 64 35.346 0 64-28.654 64-64 0-35.346-28.654-64-64-64C28.654 0 0 28.654 0 64z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/job.svg b/src/assets/icons/svg/job.svg
new file mode 100644
index 0000000..2a93a25
--- /dev/null
+++ b/src/assets/icons/svg/job.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036191400" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5472" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M934.912 1016.832H192c-14.336 0-25.6-11.264-25.6-25.6v-189.44c0-14.336 11.264-25.6 25.6-25.6s25.6 11.264 25.6 25.6v163.84h691.712V64H217.6v148.48c0 14.336-11.264 25.6-25.6 25.6s-25.6-11.264-25.6-25.6v-174.08c0-14.336 11.264-25.6 25.6-25.6h742.912c14.336 0 25.6 11.264 25.6 25.6v952.832c0 14.336-11.264 25.6-25.6 25.6z" p-id="5473"></path><path d="M232.96 371.2h-117.76c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h117.76c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6zM232.96 540.16h-117.76c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h117.76c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6zM232.96 698.88h-117.76c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h117.76c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6zM574.464 762.88c-134.144 0-243.2-109.056-243.2-243.2S440.32 276.48 574.464 276.48s243.2 109.056 243.2 243.2-109.056 243.2-243.2 243.2z m0-435.2c-105.984 0-192 86.016-192 192S468.48 711.68 574.464 711.68s192-86.016 192-192S680.448 327.68 574.464 327.68z" p-id="5474"></path><path d="M663.04 545.28h-87.04c-14.336 0-25.6-11.264-25.6-25.6s11.264-25.6 25.6-25.6h87.04c14.336 0 25.6 11.264 25.6 25.6s-11.264 25.6-25.6 25.6z" p-id="5475"></path><path d="M576 545.28c-14.336 0-25.6-11.264-25.6-25.6v-87.04c0-14.336 11.264-25.6 25.6-25.6s25.6 11.264 25.6 25.6v87.04c0 14.336-11.264 25.6-25.6 25.6z" p-id="5476"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/language.svg b/src/assets/icons/svg/language.svg
new file mode 100644
index 0000000..0082b57
--- /dev/null
+++ b/src/assets/icons/svg/language.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M84.742 36.8c2.398 7.2 5.595 12.8 11.19 18.4 4.795-4.8 7.992-11.2 10.39-18.4h-21.58zm-52.748 40h20.78l-10.39-28-10.39 28z"/><path d="M111.916 0H16.009C7.218 0 .025 7.2.025 16v96c0 8.8 7.193 16 15.984 16h95.907c8.791 0 15.984-7.2 15.984-16V16c0-8.8-6.394-16-15.984-16zM72.754 103.2c-1.598 1.6-3.197 1.6-4.795 1.6-.8 0-2.398 0-3.197-.8-.8-.8-1.599 0-1.599-.8s-.799-1.6-1.598-3.2c-.8-1.6-.8-2.4-1.599-4l-3.196-8.8H28.797L25.6 96c-1.598 3.2-2.398 5.6-3.197 7.2-.8 1.6-2.398 1.6-4.795 1.6-1.599 0-3.197-.8-4.796-1.6-1.598-1.6-2.397-2.4-2.397-4 0-.8 0-1.6.799-3.2.8-1.6.8-2.4 1.598-4l17.583-44.8c.8-1.6.8-3.2 1.599-4.8.799-1.6 1.598-3.2 2.397-4 .8-.8 1.599-2.4 3.197-3.2 1.599-.8 3.197-.8 4.796-.8 1.598 0 3.196 0 4.795.8 1.598.8 2.398 1.6 3.197 3.2.799.8 1.598 2.4 2.397 4 .8 1.6 1.599 3.2 2.398 5.6l17.583 44c1.598 3.2 2.398 5.6 2.398 7.2-.8.8-1.599 2.4-2.398 4zM116.711 72c-8.791-3.2-15.185-7.2-20.78-12-5.594 5.6-12.787 9.6-21.579 12l-2.397-4c8.791-2.4 15.984-5.6 21.579-11.2C87.939 51.2 83.144 44 81.545 36h-7.992v-3.2h21.58c-1.6-2.4-3.198-5.6-4.796-8l2.397-.8c1.599 2.4 3.997 5.6 5.595 8.8h19.98v4h-7.992c-2.397 8-6.393 15.2-11.189 20 5.595 4.8 11.988 8.8 20.78 11.2l-3.197 4z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/link.svg b/src/assets/icons/svg/link.svg
new file mode 100644
index 0000000..48197ba
--- /dev/null
+++ b/src/assets/icons/svg/link.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M115.625 127.937H.063V12.375h57.781v12.374H12.438v90.813h90.813V70.156h12.374z"/><path d="M116.426 2.821l8.753 8.753-56.734 56.734-8.753-8.745z"/><path d="M127.893 37.982h-12.375V12.375H88.706V0h39.187z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/list.svg b/src/assets/icons/svg/list.svg
new file mode 100644
index 0000000..20259ed
--- /dev/null
+++ b/src/assets/icons/svg/list.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M1.585 12.087c0 6.616 3.974 11.98 8.877 11.98 4.902 0 8.877-5.364 8.877-11.98 0-6.616-3.975-11.98-8.877-11.98-4.903 0-8.877 5.364-8.877 11.98zM125.86.107H35.613c-1.268 0-2.114 1.426-2.114 2.852v18.255c0 1.712 1.057 2.853 2.114 2.853h90.247c1.268 0 2.114-1.426 2.114-2.853V2.96c0-1.711-1.057-2.852-2.114-2.852zM.106 62.86c0 6.615 3.974 11.979 8.876 11.979 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zM124.17 50.88H33.921c-1.268 0-2.114 1.425-2.114 2.851v18.256c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852V53.73c0-1.426-.846-2.852-2.114-2.852zM.106 115.913c0 6.616 3.974 11.98 8.876 11.98 4.903 0 8.877-5.364 8.877-11.98 0-6.616-3.974-11.98-8.877-11.98-4.902 0-8.876 5.364-8.876 11.98zm124.064-11.98H33.921c-1.268 0-2.114 1.426-2.114 2.853v18.255c0 1.711 1.057 2.852 2.114 2.852h90.247c1.268 0 2.114-1.426 2.114-2.852v-18.255c0-1.427-.846-2.853-2.114-2.853z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/lock.svg b/src/assets/icons/svg/lock.svg
new file mode 100644
index 0000000..74fee54
--- /dev/null
+++ b/src/assets/icons/svg/lock.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M119.88 49.674h-7.987V39.52C111.893 17.738 90.45.08 63.996.08 37.543.08 16.1 17.738 16.1 39.52v10.154H8.113c-4.408 0-7.987 2.94-7.987 6.577v65.13c0 3.637 3.57 6.577 7.987 6.577H119.88c4.407 0 7.987-2.94 7.987-6.577v-65.13c-.008-3.636-3.58-6.577-7.987-6.577zm-23.953 0H32.065V39.52c0-14.524 14.301-26.295 31.931-26.295 17.63 0 31.932 11.777 31.932 26.295v10.153z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/log.svg b/src/assets/icons/svg/log.svg
new file mode 100644
index 0000000..d879d33
--- /dev/null
+++ b/src/assets/icons/svg/log.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035943711" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4805" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M208.736 566.336H64.384v59.328h144.352v-59.328z m0-336.096H165.44V74.592c0-7.968 4.896-14.848 10.464-14.848h502.016V0.448H175.936c-38.72 1.248-69.248 34.368-68.192 74.144v155.648H64.384V289.6h144.352V230.24z m0 168.096H64.384v59.328h144.352v-59.328z m714.656 76.576h-57.76v474.496c0 7.936-4.896 14.848-10.464 14.848H175.936c-5.568 0-10.464-6.912-10.464-14.848v-155.68h43.296v-59.296H64.384v59.296h43.328v155.68c-1.024 39.776 29.472 72.896 68.192 74.144h679.232c38.72-1.184 69.248-34.368 68.256-74.144V474.912z m14.944-290.336l-83.072-85.312a71.264 71.264 0 0 0-52.544-21.728 71.52 71.52 0 0 0-51.616 23.872L386.528 507.264a30.496 30.496 0 0 0-6.176 10.72L308.16 740.512a30.016 30.016 0 0 0 6.976 30.24c7.712 7.968 19.2 10.752 29.568 7.2l216.544-74.112a28.736 28.736 0 0 0 12.128-7.936L940.448 287.456a75.552 75.552 0 0 0-2.112-102.88z m-557.12 518.272l39.104-120.64 78.336 80.416-117.44 40.224z m170.048-70.016l-103.552-106.016 200.16-222.4 103.52 106.304-200.128 222.112zM897.952 247.072l-0.256 0.224-107.136 119.168-103.52-106.528 106.432-118.624a14.144 14.144 0 0 1 10.304-4.736 13.44 13.44 0 0 1 10.464 4.288l83.264 85.696c5.472 5.6 5.664 14.72 0.448 20.512z" p-id="4806"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/logininfor.svg b/src/assets/icons/svg/logininfor.svg
new file mode 100644
index 0000000..267f844
--- /dev/null
+++ b/src/assets/icons/svg/logininfor.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036016814" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5261" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 128h-85.333333a42.666667 42.666667 0 0 0 0 85.333333h42.666666v640H170.666667V213.333333h42.666666a42.666667 42.666667 0 0 0 0-85.333333H128a42.666667 42.666667 0 0 0-42.666667 42.666667v725.333333a42.666667 42.666667 0 0 0 42.666667 42.666667h768a42.666667 42.666667 0 0 0 42.666667-42.666667V170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667z" p-id="5262"></path><path d="M341.333333 298.666667a42.666667 42.666667 0 0 0 42.666667-42.666667V128a42.666667 42.666667 0 0 0-85.333333 0v128a42.666667 42.666667 0 0 0 42.666666 42.666667zM512 298.666667a42.666667 42.666667 0 0 0 42.666667-42.666667V128a42.666667 42.666667 0 0 0-85.333334 0v128a42.666667 42.666667 0 0 0 42.666667 42.666667zM682.666667 298.666667a42.666667 42.666667 0 0 0 42.666666-42.666667V128a42.666667 42.666667 0 0 0-85.333333 0v128a42.666667 42.666667 0 0 0 42.666667 42.666667zM341.333333 768a42.666667 42.666667 0 0 0 42.666667-42.666667 128 128 0 0 1 256 0 42.666667 42.666667 0 0 0 85.333333 0 213.333333 213.333333 0 0 0-107.52-184.32A128 128 0 0 0 640 469.333333a128 128 0 0 0-256 0 128 128 0 0 0 22.186667 71.68A213.333333 213.333333 0 0 0 298.666667 725.333333a42.666667 42.666667 0 0 0 42.666666 42.666667z m128-298.666667a42.666667 42.666667 0 1 1 42.666667 42.666667 42.666667 42.666667 0 0 1-42.666667-42.666667z" p-id="5263"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/message.svg b/src/assets/icons/svg/message.svg
new file mode 100644
index 0000000..14ca817
--- /dev/null
+++ b/src/assets/icons/svg/message.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 20.967v59.59c0 11.59 8.537 20.966 19.075 20.966h28.613l1 26.477L76.8 101.523h32.125c10.538 0 19.075-9.377 19.075-20.966v-59.59C128 9.377 119.463 0 108.925 0h-89.85C8.538 0 0 9.377 0 20.967zm82.325 33.1c0-5.524 4.013-9.935 9.037-9.935 5.026 0 9.038 4.41 9.038 9.934 0 5.524-4.025 9.934-9.038 9.934-5.024 0-9.037-4.41-9.037-9.934zm-27.613 0c0-5.524 4.013-9.935 9.038-9.935s9.037 4.41 9.037 9.934c0 5.524-4.025 9.934-9.037 9.934-5.025 0-9.038-4.41-9.038-9.934zm-27.1 0c0-5.524 4.013-9.935 9.038-9.935s9.038 4.41 9.038 9.934c0 5.524-4.026 9.934-9.05 9.934-5.013 0-9.025-4.41-9.025-9.934z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/money.svg b/src/assets/icons/svg/money.svg
new file mode 100644
index 0000000..c1580de
--- /dev/null
+++ b/src/assets/icons/svg/money.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M54.122 127.892v-28.68H7.513V87.274h46.609v-12.4H7.513v-12.86h38.003L.099 0h22.6l32.556 45.07c3.617 5.144 6.44 9.611 8.487 13.385 1.788-3.05 4.89-7.779 9.301-14.186L103.93 0h24.01L82.385 62.013h38.34v12.862h-46.41v12.4h46.41v11.937h-46.41v28.68H54.123z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/monitor.svg b/src/assets/icons/svg/monitor.svg
new file mode 100644
index 0000000..70db62b
--- /dev/null
+++ b/src/assets/icons/svg/monitor.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543827393750" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4695" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: rbicon; src: url("chrome-extension://dipiagiiohfljcicegpgffpbnjmgjcnf/fonts/rbicon.woff2") format("woff2"); font-weight: normal; font-style: normal; }
+</style></defs><path d="M64 64V640H896V64H64zM0 0h960v704H0V0z" p-id="4696"></path><path d="M192 896H768v64H192zM448 640H512v256h-64z" p-id="4697"></path><path d="M479.232 561.604267l309.9904-348.330667-47.803733-42.5472-259.566934 291.669333L303.957333 240.008533 163.208533 438.6048l52.224 37.009067 91.6224-129.28z" p-id="4698"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/moon.svg b/src/assets/icons/svg/moon.svg
new file mode 100644
index 0000000..ec72d77
--- /dev/null
+++ b/src/assets/icons/svg/moon.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303018722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M368.832 67.2c51.328-16.384 89.216 34.112 75.712 76.416a346.816 346.816 0 0 0 435.84 435.84c42.304-13.44 92.8 24.384 76.48 75.712A467.968 467.968 0 1 1 368.832 67.2z m-35.776 122.688a368.832 368.832 0 1 0 501.056 501.056 445.952 445.952 0 0 1-501.056-501.056z" p-id="1448"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/nested.svg b/src/assets/icons/svg/nested.svg
new file mode 100644
index 0000000..06713a8
--- /dev/null
+++ b/src/assets/icons/svg/nested.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.002 9.2c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-5.043-3.58-9.132-7.997-9.132S.002 4.157.002 9.2zM31.997.066h95.981V18.33H31.997V.066zm0 45.669c0 5.044 3.58 9.132 7.998 9.132 4.417 0 7.997-4.088 7.997-9.132 0-3.263-1.524-6.278-3.998-7.91-2.475-1.63-5.524-1.63-7.998 0-2.475 1.632-4 4.647-4 7.91zM63.992 36.6h63.986v18.265H63.992V36.6zm-31.995 82.2c0 5.043 3.58 9.132 7.998 9.132 4.417 0 7.997-4.089 7.997-9.132 0-5.044-3.58-9.133-7.997-9.133s-7.998 4.089-7.998 9.133zm31.995-9.131h63.986v18.265H63.992V109.67zm0-27.404c0 5.044 3.58 9.133 7.998 9.133 4.417 0 7.997-4.089 7.997-9.133 0-3.263-1.524-6.277-3.998-7.909-2.475-1.631-5.524-1.631-7.998 0-2.475 1.632-4 4.646-4 7.91zm31.995-9.13h31.991V91.4H95.987V73.135z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/number.svg b/src/assets/icons/svg/number.svg
new file mode 100644
index 0000000..ad5ce9a
--- /dev/null
+++ b/src/assets/icons/svg/number.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802851180" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2867" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M279.272727 791.272727h512a46.545455 46.545455 0 0 1 0 93.090909H279.272727a46.545455 46.545455 0 0 1 0-93.090909z m33.838546-617.984V651.636364H193.722182V395.170909c0-37.003636-0.884364-59.298909-2.653091-66.746182a24.948364 24.948364 0 0 0-14.615273-16.989091c-8.005818-3.863273-25.786182-5.771636-53.341091-5.771636h-11.822545v-55.854545c57.716364-12.381091 101.562182-37.888 131.490909-76.520728h70.283636z m303.709091 396.8V651.636364H354.164364v-68.235637c77.777455-127.255273 124.043636-206.010182 138.705454-236.218182 14.661818-30.254545 22.016-53.853091 22.016-70.74909 0-13.032727-2.234182-22.714182-6.656-29.137455-4.421818-6.376727-11.170909-9.588364-20.247273-9.588364a22.248727 22.248727 0 0 0-20.200727 10.612364c-4.468364 7.121455-6.656 21.178182-6.656 42.263273v45.521454H354.164364v-17.454545c0-26.763636 1.396364-47.941818 4.142545-63.348364 2.746182-15.499636 9.541818-30.72 20.386909-45.661091 10.798545-14.987636 24.901818-26.298182 42.216727-33.978182 17.361455-7.68 38.167273-11.543273 62.37091-11.543272 47.476364 0 83.316364 11.776 107.706181 35.328 24.296727 23.552 36.445091 53.341091 36.445091 89.367272 0 27.368727-6.842182 56.32-20.48 86.853819-13.730909 30.533818-54.039273 95.325091-121.018182 194.420363h130.885819z m270.615272-189.393454c18.152727 6.097455 31.650909 16.104727 40.494546 29.975272 8.843636 13.917091 13.312 46.452364 13.312 97.652364 0 38.027636-4.328727 67.490909-13.032727 88.529455-8.657455 20.945455-23.598545 36.910545-44.869819 47.848727-21.271273 10.938182-48.593455 16.384-81.873454 16.384-37.794909 0-67.490909-6.330182-89.088-19.083636-21.550545-12.660364-35.746909-28.253091-42.542546-46.638546-6.795636-18.432-10.193455-50.362182-10.193454-95.883636v-37.841455h119.389091v77.730909c0 20.666182 1.210182 33.838545 3.723636 39.424 2.420364 5.585455 7.912727 8.424727 16.337455 8.424728 9.309091 0 15.36-3.537455 18.338909-10.612364 2.932364-7.121455 4.421818-25.6 4.421818-55.575273v-33.047273c0-18.338909-2.048-31.744-6.190546-40.215272a30.72 30.72 0 0 0-18.338909-16.709818c-8.052364-2.653091-23.738182-4.189091-46.964363-4.561455V357.050182c28.392727 0 45.893818-1.070545 52.596363-3.258182a22.946909 22.946909 0 0 0 14.475637-14.149818c2.932364-7.307636 4.421818-18.711273 4.421818-34.257455v-26.624c0-16.756364-1.722182-27.741091-5.12-33.047272-3.490909-5.352727-8.843636-8.005818-16.151273-8.005819-8.285091 0-13.963636 2.792727-16.989091 8.378182-3.025455 5.632-4.561455 17.640727-4.561454 35.933091v39.284364h-119.389091v-40.773818c0-45.661091 10.472727-76.567273 31.325091-92.625455 20.898909-16.058182 54.085818-24.064 99.607272-24.064 56.878545 0 95.511273 11.170909 115.805091 33.373091 20.293818 22.248727 30.394182 53.201455 30.394182 92.765091 0 26.810182-3.630545 46.173091-10.891636 58.088727-7.307636 11.915636-20.107636 22.807273-38.446546 32.628364z" p-id="2868"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/online.svg b/src/assets/icons/svg/online.svg
new file mode 100644
index 0000000..330a202
--- /dev/null
+++ b/src/assets/icons/svg/online.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1568899557259" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="535" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M356.246145 681.56286c-68.156286-41.949414-107.246583-103.84102-107.246583-169.805384 0-65.966411 39.090297-127.860063 107.246583-169.809477 12.046361-7.414877 15.800871-23.190165 8.385994-35.236526-7.413853-12.046361-23.191188-15.801894-35.236526-8.387018-39.640836 24.399713-72.539106 56.044434-95.137801 91.515297-23.86657 37.461193-36.481889 79.620385-36.481889 121.917724 0 42.297338 12.615319 84.454484 36.481889 121.914654 22.598694 35.469839 55.496965 67.11456 95.137801 91.51325 4.185322 2.576685 8.821923 3.804652 13.400195 3.804652 8.598842 0 16.998139-4.329609 21.836331-12.190647C372.047016 704.752002 368.291482 688.976714 356.246145 681.56286zM263.943926 754.580874c-92.603071-61.111846-145.713686-149.623739-145.713686-242.840794 0-93.195565 53.094242-181.682899 145.667637-242.774279 11.805884-7.79043 15.061021-23.677259 7.269567-35.483142-7.79043-11.805884-23.677259-15.062044-35.483142-7.269567C128.487861 296.954249 67.006602 401.024489 67.006602 511.74008c0 110.73708 61.496609 214.830857 168.721703 285.593504 4.343935 2.867304 9.240455 4.238534 14.08274 4.238534 8.317433 0 16.476253-4.046153 21.400403-11.507078C279.003923 778.258133 275.748786 762.372328 263.943926 754.580874zM788.660552 226.213092c-11.80486-7.791453-27.692712-4.536316-35.483142 7.269567-7.79043 11.805884-4.536316 27.692712 7.269567 35.483142 92.575442 61.092403 145.670707 149.579737 145.670707 242.774279 0 93.216032-53.111638 181.727924-145.715733 242.840794-11.805884 7.79043-15.059997 23.678282-7.269567 35.484166 4.925173 7.461949 13.081946 11.507078 21.400403 11.507078 4.841262 0 9.739828-1.37123 14.083763-4.238534 107.22714-70.761624 168.724773-174.857447 168.724773-285.593504C957.341323 401.025513 895.860063 296.955272 788.660552 226.213092zM790.090111 633.67213c23.865547-37.459147 36.480866-79.617315 36.480866-121.914654 0-42.298362-12.615319-84.45653-36.480866-121.917724-22.598694-35.470863-55.496965-67.115584-95.139847-91.515297-12.047384-7.413853-27.821649-3.659343-35.236526 8.387018-7.414877 12.045337-3.659343 27.821649 8.385994 35.236526 68.156286 41.949414 107.247606 103.842043 107.247606 169.809477 0 65.964364-39.090297 127.85597-107.247606 169.804361-12.045337 7.414877-15.800871 23.190165-8.385994 35.237549 4.838192 7.861038 13.236466 12.190647 21.835308 12.190647 4.579295 0 9.215896-1.227967 13.400195-3.804652C734.591099 700.786691 767.490394 669.142993 790.090111 633.67213zM567.129086 518.274914c24.12342-17.150612 39.887452-45.305859 39.887452-77.07133 0-52.128241-42.452881-94.538143-94.634334-94.538143-52.18043 0-94.633311 42.408879-94.633311 94.538143 0 31.695886 15.696494 59.797921 39.730886 76.958766-49.875944 21.128203-84.917018 70.234621-84.917018 127.301338 0 2.366907 0.061398 4.762467 0.182149 7.119141l1.249457 24.296359 276.373515 0 1.238201-24.308639c0.119727-2.358721 0.181125-4.750187 0.181125-7.106862C651.786185 588.497255 616.865861 539.465538 567.129086 518.274914zM512.381182 397.889079c23.937179 0 43.411719 19.430538 43.411719 43.314505 0 23.882943-19.47454 43.313481-43.411719 43.313481-23.936155 0-43.409672-19.430538-43.409672-43.313481C468.971509 417.320641 488.445026 397.889079 512.381182 397.889079zM426.08884 625.656573c9.119705-38.542828 44.254923-67.337641 86.085634-67.337641s76.966952 28.794813 86.085634 67.337641L426.08884 625.656573z" p-id="536"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/password.svg b/src/assets/icons/svg/password.svg
new file mode 100644
index 0000000..6c64def
--- /dev/null
+++ b/src/assets/icons/svg/password.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802846045" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2750" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M868.593046 403.832442c-30.081109-28.844955-70.037123-44.753273-112.624057-44.753273L265.949606 359.079168c-42.554188 0-82.510202 15.908318-112.469538 44.690852-30.236652 28.782533-46.857191 67.222007-46.857191 108.198258l0 294.079782c0 40.977273 16.619516 79.414701 46.702672 108.136859 29.959336 28.844955 70.069869 44.814672 112.624057 44.814672l490.019383 0c42.585911 0 82.696444-15.969717 112.624057-44.814672 30.082132-28.844955 46.579875-67.222007 46.579875-108.136859L915.172921 511.968278C915.171897 471.053426 898.675178 432.677397 868.593046 403.832442zM841.821309 806.049083c0 22.098297-8.882298 42.772152-25.099654 58.306964-16.154935 15.661701-37.81935 24.203238-60.752666 24.203238L265.949606 888.559285c-22.934339 0-44.567032-8.54256-60.877509-24.264637-16.186657-15.474436-25.067932-36.148291-25.067932-58.246589L180.004165 511.968278c0-22.035876 8.881274-42.772152 25.192775-58.307987 16.186657-15.536858 37.81935-24.139793 60.753689-24.139793l490.019383 0c22.933315 0 44.597731 8.602935 60.752666 24.139793 16.21838 15.535835 25.099654 36.272112 25.099654 58.307987L841.822332 806.049083zM510.974136 135.440715c114.914216 0 208.318536 89.75214 208.318536 200.055338l73.350588 0c0-149.113109-126.366036-270.496667-281.669124-270.496667-155.333788 0-281.699824 121.383558-281.699824 270.496667l73.350588 0C302.623877 225.193879 396.059919 135.440715 510.974136 135.440715zM474.299865 747.244792l73.350588 0L547.650453 629.576859l-73.350588 0L474.299865 747.244792z" p-id="2751"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/pdf.svg b/src/assets/icons/svg/pdf.svg
new file mode 100644
index 0000000..957aa0c
--- /dev/null
+++ b/src/assets/icons/svg/pdf.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="128" height="128"><path d="M869.073 277.307H657.111V65.344l211.962 211.963zm-238.232 26.27V65.344l-476.498-.054v416.957h714.73v-178.67H630.841zm-335.836 360.57c-5.07-3.064-10.944-5.133-17.61-6.201-6.67-1.064-13.603-1.6-20.81-1.6h-48.821v85.641h48.822c7.206 0 14.14-.532 20.81-1.6 6.665-1.065 12.54-3.133 17.609-6.202 5.064-3.063 9.134-7.406 12.208-13.007 3.065-5.602 4.6-12.937 4.6-22.011 0-9.07-1.535-16.408-4.6-22.01-3.074-5.603-7.144-9.94-12.208-13.01zM35.82 541.805v416.904h952.358V541.805H35.821zm331.421 191.179c-3.6 11.071-9.343 20.879-17.209 29.413-7.874 8.542-18.078 15.408-30.617 20.61-12.544 5.206-27.747 7.807-45.621 7.807h-66.036v102.45h-62.831V607.517h128.867c17.874 0 33.077 2.6 45.62 7.802 12.541 5.207 22.745 12.076 30.618 20.615 7.866 8.538 13.604 18.277 17.21 29.212 3.6 10.943 5.401 22.278 5.401 34.018 0 11.477-1.8 22.752-5.402 33.819zM644.9 806.417c-5.343 17.61-13.408 32.818-24.212 45.627-10.807 12.803-24.283 22.879-40.423 30.213-16.146 7.343-35.155 11.007-57.03 11.007h-123.26V607.518h123.26c18.41 0 35.552 2.941 51.428 8.808 15.873 5.869 29.618 14.671 41.22 26.412 11.608 11.744 20.674 26.411 27.217 44.02 6.535 17.61 9.803 38.288 9.803 62.035 0 20.81-2.67 40.02-8.003 57.624zm245.362-146.07h-138.07v66.03h119.66v48.829h-119.66v118.058h-62.83V607.518h200.9v52.829h-.001zm-318.2 25.611c-6.402-8.266-14.877-14.604-25.412-19.01-10.544-4.402-23.551-6.602-39.019-6.602h-44.825v180.088h56.029c9.07 0 17.872-1.463 26.415-4.401 8.535-2.932 16.14-7.802 22.812-14.609 6.665-6.8 12.007-15.667 16.007-26.61 4.003-10.94 6.003-24.275 6.003-40.021 0-14.408-1.4-27.416-4.202-39.019-2.8-11.607-7.406-21.542-13.808-29.816zm0 0"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/people.svg b/src/assets/icons/svg/people.svg
new file mode 100644
index 0000000..2bd54ae
--- /dev/null
+++ b/src/assets/icons/svg/people.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M104.185 95.254c8.161 7.574 13.145 17.441 13.145 28.28 0 1.508-.098 2.998-.285 4.466h-10.784c.238-1.465.403-2.948.403-4.465 0-8.983-4.36-17.115-11.419-23.216C86 104.66 75.355 107.162 64 107.162c-11.344 0-21.98-2.495-31.22-6.83-7.064 6.099-11.444 14.218-11.444 23.203 0 1.517.165 3 .403 4.465H10.955a35.444 35.444 0 0 1-.285-4.465c0-10.838 4.974-20.713 13.127-28.291C9.294 85.42.003 70.417.003 53.58.003 23.99 28.656.001 64 .001s63.997 23.988 63.997 53.58c0 16.842-9.299 31.85-23.812 41.673zM64 36.867c-29.454 0-53.33-10.077-53.33 15.342 0 25.418 23.876 46.023 53.33 46.023 29.454 0 53.33-20.605 53.33-46.023 0-25.419-23.876-15.342-53.33-15.342zm24.888 25.644c-3.927 0-7.111-2.665-7.111-5.953 0-3.288 3.184-5.954 7.11-5.954 3.928 0 7.111 2.666 7.111 5.954s-3.183 5.953-7.11 5.953zm-3.556 16.372c0 4.11-9.55 7.442-21.332 7.442-11.781 0-21.332-3.332-21.332-7.442 0-1.06.656-2.064 1.8-2.976 3.295 2.626 10.79 4.465 19.532 4.465 8.743 0 16.237-1.84 19.531-4.465 1.145.912 1.801 1.916 1.801 2.976zm-46.22-16.372c-3.927 0-7.11-2.665-7.11-5.953 0-3.288 3.183-5.954 7.11-5.954 3.927 0 7.111 2.666 7.111 5.954s-3.184 5.953-7.11 5.953z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/peoples.svg b/src/assets/icons/svg/peoples.svg
new file mode 100644
index 0000000..aab852e
--- /dev/null
+++ b/src/assets/icons/svg/peoples.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M95.648 118.762c0 5.035-3.563 9.121-7.979 9.121H7.98c-4.416 0-7.979-4.086-7.979-9.121C0 100.519 15.408 83.47 31.152 76.75c-9.099-6.43-15.216-17.863-15.216-30.987v-9.128c0-20.16 14.293-36.518 31.893-36.518s31.894 16.358 31.894 36.518v9.122c0 13.137-6.123 24.556-15.216 30.993 15.738 6.726 31.141 23.769 31.141 42.012z"/><path d="M106.032 118.252h15.867c3.376 0 6.101-3.125 6.101-6.972 0-13.957-11.787-26.984-23.819-32.123 6.955-4.919 11.638-13.66 11.638-23.704v-6.985c0-15.416-10.928-27.926-24.39-27.926-1.674 0-3.306.193-4.89.561 1.936 4.713 3.018 9.974 3.018 15.526v9.121c0 13.137-3.056 23.111-11.066 30.993 14.842 4.41 27.312 23.42 27.541 41.509z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/phone.svg b/src/assets/icons/svg/phone.svg
new file mode 100644
index 0000000..ab8e8c4
--- /dev/null
+++ b/src/assets/icons/svg/phone.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1567417214476" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2266" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M761.503029 2.90619 242.121921 2.90619c-32.405037 0-58.932204 26.060539-58.932204 58.527998l0 902.302287c0 32.156374 26.217105 58.216913 58.932204 58.216913l519.381108 0c32.344662 0 58.591443-26.060539 58.591443-58.216913L820.094472 61.123103C820.094472 28.966729 793.847691 2.90619 761.503029 2.90619M452.878996 61.123103l98.147344 0c6.780427 0 12.31549 5.536087 12.31549 12.253068 0 6.748704-5.535063 12.253068-12.31549 12.253068l-98.147344 0c-6.779404 0-12.345166-5.504364-12.345166-12.253068C440.532807 66.659189 446.099592 61.123103 452.878996 61.123103M501.641583 980.593398c-29.636994 0-53.987588-23.946388-53.987588-53.677527 0-29.356608 24.039509-53.614082 53.987588-53.614082 29.91738 0 53.987588 23.883967 53.987588 53.614082C555.629171 956.647009 531.559986 980.593398 501.641583 980.593398M766.35657 803.142893c0 16.23373-13.186324 29.107945-29.233811 29.107945l-470.618521 0c-16.35755 0-29.325909-13.186324-29.325909-29.107945L237.178329 163.500794c0-16.232706 13.279445-29.138644 29.325909-29.138644l470.246037 0c16.420995 0 29.357632 13.1853 29.357632 29.138644l0 639.642099L766.35657 803.142893zM766.35657 803.142893" p-id="2267"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/post.svg b/src/assets/icons/svg/post.svg
new file mode 100644
index 0000000..2922c61
--- /dev/null
+++ b/src/assets/icons/svg/post.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566035724641" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3998" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M136.4 434.3h77.7c21.5 0 38.9-17.4 38.9-38.9s-17.4-38.9-38.9-38.9h-77.7c-21.5 0-38.9 17.4-38.9 38.9s17.4 38.9 38.9 38.9zM252.9 628.6c0-21.5-17.4-38.9-38.9-38.9h-77.7c-21.5 0-38.9 17.4-38.9 38.9s17.4 38.9 38.9 38.9H214c21.5-0.1 38.9-17.5 38.9-38.9z" p-id="3999"></path><path d="M874.7 97.5H227c-28.6 0-51.8 23.2-51.8 51.8v194.3h38.9c28.6 0 51.8 23.2 51.8 51.8 0 28.6-23.2 51.8-51.8 51.8h-38.9v129.5h38.9c28.6 0 51.8 23.2 51.8 51.8 0 28.6-23.2 51.8-51.8 51.8h-38.9v194.3c0 28.6 23.2 51.8 51.8 51.8h647.7c28.6 0 51.8-23.2 51.8-51.8V149.3c0-28.6-23.2-51.8-51.8-51.8z m-311.3 723c-15.6 0-146.7-71.6-146.7-91 0-19.4 102-368.6 102-368.6l-83.6-104s-12.3-23.1 24.6-23.1h208.9c36.9 0 18.4 23.1 18.4 23.1l-79 104s102 351.3 102 368.6c0.1 17.3-131 91-146.6 91z m169.2-253.6l-27.9 40.2-74.5-240 103.4 171.7c4.6 7.9 4.2 20.6-1 28.1z" p-id="4000"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/qq.svg b/src/assets/icons/svg/qq.svg
new file mode 100644
index 0000000..ee13d4e
--- /dev/null
+++ b/src/assets/icons/svg/qq.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M18.448 57.545l-.244-.744-.198-.968-.132-.53v-2.181l.236-.859.24-.908.317-.953.428-1.06.561-1.103.794-1.104v-.773l.077-.724.123-.984.34-1.106.313-1.194.25-.548.289-.511.371-.569.405-.423v-2.73l.234-1.407.236-1.633.42-1.955.577-2.035.43-1.118.426-1.217.468-1.135.559-1.216.57-1.332.655-1.247.737-1.331.929-1.33.43-.762.457-.624.995-1.406 1.025-1.403 1.163-1.444 1.246-1.405 1.352-1.384 1.41-1.423 1.708-1.536 1.083-.934 1.322-1.008 1.34-.89 1.448-.855 1.392-.76 1.57-.63 1.667-.775 1.657-.532 1.653-.552 1.787-.548 1.785-.417 1.876-.347L59.128.68l1.879-.245 1.876-.252 2.002-.106h5.912l1.97.243 1.981.231 2.019.207 1.874.441 1.979.413 1.857.475 2.035.53 1.862.646 1.782.738 1.904.78 1.736.853 1.689.95 1.655 1.044 1.425.971.662.548.693.401 1.323 1.1 1.115 1.064 1.112 1.1 1.083 1.214.894 1.178 1.064 1.217.74 1.306.752 1.162.798 1.352.661 1.175 1.113 2.489.546 1.286.428 1.192.428 1.294.384 1.217.267 1.047.347 1.231.607 2.198.388 1.924.253 1.861.217 1.497.342 2.28.077.362.274.41.737 1.18.473.8.42.832.534.892.472 1.07.307 1.093.334 1.2.252 1.232.115.605.106.746v.648l-.106.643v.8l-.192.774-.35 1.5-.403.76-.299.852v.213l.142.264.4.623 1.746 2.53 1.377 1.9.66 1.267.889 1.389.774 1.52.893 1.627.894 1.828 1.006 2.069.567 1.268.518 1.239.447 1.307.44 1.175.336 1.235.342 1.16.432 2.261.343 2.31.235 2.05v2.891l-.158 1.025-.226 1.768-.308 1.59-.48 1.44-.18.588-.336.707-.28.493-.375.607-.33.383-.42.494-.375.4-.401.34-.48.207-.432.207-.355.114h-.543l-.346-.114-.66-.32-.302-.212-.317-.223-.347-.304-.35-.342-.579-.63-.684-.89-.539-.917-.538-.734-.526-.855-.741-1.517-.833-1.579-.098-.055h-.138l-.338.247-.196.415-.326.516-.567 1.533-.856 2.182-1.096 2.626-.824 1.308-.864 1.366-1.027 1.536-1.09 1.503-.557.68-.676.743-1.555 1.497.136.135.21.214.777.446 3.235 1.524 1.41.779 1.347.756 1.332.953 1.187.982.574.443.432.511.445.593.367.643.198.533.242.64.105.554.115.647-.115.433v.44l-.105.454-.242.415-.092.325-.22.394-.587.784-.543.627-.42.47-.35.348-.893.638-1.01.556-1.077.532-1.155.511-1.287.495-.693.207-.608.167-1.496.342-1.545.325-1.552.323-1.689.27-1.74.072-1.785.21h-5.539l-1.998-.114-1.86-.168-2.005-.27-1.99-.209-2.095-.286-2.03-.495-1.981-.374-1.968-.552-2.019-.707-1.98-.585-1.044-.342-.927-.323-.586-.223-.582-.12h-1.647l-1.904-.131-.962-.096-1.24-.135-.795.705-1.085.665-1.471.701-1.628.875-.99.475-1.033.376-2.281.914-1.24.305-1.3.343-1.803.344-1.13.086-1.193.1-1.246.135-1.45.053h-5.926l-3.346-.053-3.25-.321-1.644-.23-1.589-.23-1.546-.227-1.547-.305-1.442-.456-1.434-.325-1.294-.51-1.223-.474-1.142-.533-.99-.583-.984-.71-.336-.343-.44-.415-.334-.362-.3-.417-.278-.415-.215-.42-.311-.89-.109-.46-.138-.51v-.473l.138-.533v-.53l.109-.53v-1.069l.052-.564.259-.647.215-.646.39-.779.286-.3.236-.348.615-.738.49-.38.464-.266.428-.338.676-.21.543-.324.676-.341.77-.227.775-.231.897-.192.85-.11 1.008-.13 1.093-.081.284-.092h.063l.137-.115v-.13l-.2-.266-.58-.27-1.45-1.231-.975-.761-1.127-.967-1.136-1.082-1.181-1.382-1.36-1.558-.508-.843-.672-.87-.58-1.007-.522-1.1-.704-1.047-.459-1.194-.547-1.192-.546-1.33-.397-1.273-.378-1.575-.112-.057h-.115l-.059-.113h-.14l-.23.113-.114.057-.158.264-.057.321-.119.286-.206.477-.664 1.157-.345.701-.546.612-.58.736-.641.816-.677.724-.795.701-.734.658-.814.524-.89.546-.855.325-1.008.247-.99.095h-.233l-.228-.095-.18-.384-.29-.188-.38-.912-.237-.493-.255-.707-.21-.734-.113-.724-.313-1.648-.12-.972v-3.185l.12-2.379.196-1.214.23-1.252.21-1.347.374-1.254.42-1.443.431-1.407.578-1.448.545-1.38.754-1.4.699-1.52.855-1.425 1.006-1.538 1.023-1.382 1.069-1.538.891-1.071 1.142-1.227 1.202-1.237.56-.59.678-.662.985-.836 1.012-.853 1.647-1.446 1.242-.889z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/question.svg b/src/assets/icons/svg/question.svg
new file mode 100644
index 0000000..cf75bd4
--- /dev/null
+++ b/src/assets/icons/svg/question.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1581238842264" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1409" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M512 0C229.233778 0 0 229.233778 0 512s229.233778 512 512 512 512-229.233778 512-512A512 512 0 0 0 512 0z m0 938.666667C276.366222 938.666667 85.333333 747.633778 85.333333 512 85.333333 276.366222 276.366222 85.333333 512 85.333333c235.633778 0 426.666667 191.032889 426.666667 426.666667a426.666667 426.666667 0 0 1-426.666667 426.666667z m0-717.653334a170.666667 170.666667 0 0 0-170.666667 170.666667 42.666667 42.666667 0 0 0 85.333334 0 85.333333 85.333333 0 1 1 85.333333 85.333333 42.666667 42.666667 0 0 0-42.666667 42.666667v111.36a42.666667 42.666667 0 0 0 85.333334 0v-74.24A170.666667 170.666667 0 0 0 512 221.013333z m-42.666667 542.293334a42.666667 42.666667 0 1 0 85.333334 0 42.666667 42.666667 0 0 0-85.333334 0z" p-id="1410"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/radio.svg b/src/assets/icons/svg/radio.svg
new file mode 100644
index 0000000..0cde345
--- /dev/null
+++ b/src/assets/icons/svg/radio.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575966775973" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="879" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M507.39346659 71.84873358c241.53533667 0 437.39770766 195.85422109 437.39770767 437.37442191 0 241.53766571-195.86237099 437.38955776-437.39770767 437.38955776-241.50040803 0-437.34997219-195.85189205-437.34997219-437.38955776C70.0434944 267.70295467 265.89189347 71.84873358 507.39346659 71.84873358L507.39346659 71.84873358zM507.39346659 282.81899805c-125.00686734 0-226.37039389 101.38914133-226.37039388 226.41813048 0 125.01268821 101.36352768 226.39717262 226.37039388 226.39717262 125.04295993 0 226.42395136-101.38448441 226.42395136-226.39717262C733.81625401 384.20813938 632.43642653 282.81899805 507.39346659 282.81899805L507.39346659 282.81899805zM507.39346659 120.78172615c-214.46664192 0-388.42047261 173.95150279-388.4204726 388.44026539 0 214.51204949 173.95499463 388.46122325 388.4204726 388.46122325 214.52369237 0 388.46005817-173.94800981 388.46005818-388.46122325C895.85236082 294.73322894 721.91715897 120.78172615 507.39346659 120.78172615z" p-id="880"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/rate.svg b/src/assets/icons/svg/rate.svg
new file mode 100644
index 0000000..aa3b14d
--- /dev/null
+++ b/src/assets/icons/svg/rate.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577246781606" class="icon" viewBox="0 0 1069 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1098" xmlns:xlink="http://www.w3.org/1999/xlink" width="84.5595703125" height="81"><defs><style type="text/css"></style></defs><path d="M633.72929961 378.02038203l9.49872568 18.68789795 20.78025469 2.79745225 206.61592412 27.33248408a11.46496817 11.46496817 0 0 1 6.6095543 19.47324902l-147.2675168 147.35350284-14.89299345 14.89299345 3.8006376 20.68280244 37.84585956 204.89044571a11.46496817 11.46496817 0 0 1-16.4808914 12.2961788L554.68980898 751.84713388l-18.68789794-9.49299345-18.48726123 9.99171915-183.23885392 99.34968163a11.46496817 11.46496817 0 0 1-16.78471347-11.8662416l32.5433127-205.79617881 3.29617793-20.78598692-15.19108243-14.49172002-151.03375839-143.48407587a11.46496817 11.46496817 0 0 1 6.09936328-19.63949062l205.79617881-32.63503185 20.78598691-3.2961788L428.87898125 380.72038203 518.59235674 192.64331182a11.46496817 11.46496817 0 0 1 20.56815264-0.26369385l94.56879023 185.63503183zM496.64840732 85.52038203l-121.75796162 254.98089229L95.76433145 384.76178369A34.3949045 34.3949045 0 0 0 77.46050938 443.66879023l204.87324901 194.66369385-44.16879023 279.1146498a34.3949045 34.3949045 0 0 0 50.36560489 35.61592325l248.4-134.67898038 251.84522285 128.27579591a34.3949045 34.3949045 0 0 0 49.43694287-36.89426777l-51.30573223-277.85350284 199.73120977-199.90891758a34.3949045 34.3949045 0 0 0-19.82866201-58.40827998l-280.11783428-37.03184736L558.32993633 84.71210205a34.3949045 34.3949045 0 0 0-61.68152901 0.80254775z" p-id="1099"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/redis-list.svg b/src/assets/icons/svg/redis-list.svg
new file mode 100644
index 0000000..98a15b2
--- /dev/null
+++ b/src/assets/icons/svg/redis-list.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1656035183065" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3395" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css">@font-face { font-family: feedback-iconfont; src: url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff2?t=1630033759944") format("woff2"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.woff?t=1630033759944") format("woff"), url("//at.alicdn.com/t/font_1031158_u69w8yhxdu.ttf?t=1630033759944") format("truetype"); }
+</style></defs><path d="M958.88 730.06H65.12c-18.28 0-33.12-14.82-33.12-33.12V68.91c0-18.29 14.83-33.12 33.12-33.12h893.77c18.28 0 33.12 14.82 33.12 33.12v628.03c-0.01 18.3-14.84 33.12-33.13 33.12zM98.23 663.83h827.53v-561.8H98.23v561.8z" p-id="3396"></path><path d="M512 954.55c-18.28 0-33.12-14.82-33.12-33.12V733.92c0-18.29 14.83-33.12 33.12-33.12s33.12 14.82 33.12 33.12v187.51c0 18.3-14.84 33.12-33.12 33.12z" p-id="3397"></path><path d="M762.01 988.21H261.99c-18.28 0-33.12-14.82-33.12-33.12 0-18.29 14.83-33.12 33.12-33.12h500.03c18.28 0 33.12 14.82 33.12 33.12-0.01 18.29-14.84 33.12-33.13 33.12zM514.74 578.55c-21.63 0-43.31-3.87-64.21-11.65-45.95-17.13-82.49-51.13-102.86-95.74-5.07-11.08-0.19-24.19 10.89-29.26 11.08-5.09 24.19-0.18 29.26 10.91 15.5 33.88 43.25 59.7 78.14 72.71 34.93 12.99 72.79 11.64 106.66-3.85 33.22-15.17 58.8-42.26 72.03-76.3 4.42-11.37 17.21-17.01 28.57-12.58 11.36 4.42 16.99 17.22 12.57 28.58-17.42 44.82-51.1 80.5-94.82 100.47-24.34 11.12-50.25 16.71-76.23 16.71z" p-id="3398"></path><path d="M325.27 528.78c-1.66 0-3.34-0.18-5.02-0.57-11.88-2.77-19.28-14.63-16.49-26.51l18.84-81c1.34-5.82 5-10.84 10.13-13.92 5.09-3.09 11.3-3.96 17.03-2.41l80.51 21.43c11.79 3.14 18.8 15.23 15.67 27.02-3.15 11.79-15.42 18.75-27.02 15.65l-58.49-15.57-13.69 58.81c-2.37 10.2-11.45 17.07-21.47 17.07zM360.8 351.01c-2.65 0-5.37-0.49-8-1.51-11.36-4.41-16.99-17.21-12.59-28.57 17.4-44.79 51.06-80.47 94.8-100.48 92.15-42.06 201.25-1.39 243.31 90.68 5.07 11.08 0.19 24.19-10.89 29.26-11.13 5.07-24.19 0.17-29.26-10.91-31.97-69.91-114.9-100.82-184.79-68.86-33.22 15.19-58.8 42.28-71.99 76.29-3.41 8.74-11.75 14.1-20.59 14.1z" p-id="3399"></path><path d="M684.68 376.74c-1.47 0-2.95-0.15-4.42-0.44l-81.61-16.68c-11.94-2.45-19.64-14.11-17.21-26.06 2.44-11.96 14.1-19.64 26.04-17.22l59.29 12.12 10.23-59.5c2.05-12 13.52-20.19 25.48-18.01 12.03 2.06 20.09 13.48 18.02 25.5l-14.08 81.96a22.089 22.089 0 0 1-9.29 14.49c-3.7 2.51-8.03 3.84-12.45 3.84z" p-id="3400"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/redis.svg b/src/assets/icons/svg/redis.svg
new file mode 100644
index 0000000..2f1d62d
--- /dev/null
+++ b/src/assets/icons/svg/redis.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1605865043777" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="856" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M1023.786667 611.84c-0.426667 9.770667-13.354667 20.693333-39.893334 34.56-54.613333 28.458667-337.749333 144.896-397.994666 176.298667-60.288 31.402667-93.738667 31.104-141.354667 8.32-47.616-22.741333-348.842667-144.469333-403.114667-170.368-27.093333-12.970667-40.917333-23.893333-41.386666-34.218667v103.509333c0 10.325333 14.250667 21.290667 41.386666 34.261334 54.272 25.941333 355.541333 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.362667 60.245333-31.402667 343.338667-147.797333 397.994666-176.298667 27.776-14.464 40.106667-25.728 40.106667-35.925333v-102.058667l-0.213333-0.085333z m0-168.746667c-0.512 9.770667-13.397333 20.650667-39.893334 34.517334-54.613333 28.458667-337.749333 144.896-397.994666 176.298666-60.288 31.402667-93.738667 31.104-141.354667 8.362667-47.616-22.741333-348.842667-144.469333-403.114667-170.410667-27.093333-12.928-40.917333-23.893333-41.386666-34.176v103.509334c0 10.325333 14.250667 21.248 41.386666 34.218666 54.272 25.941333 355.498667 147.626667 403.114667 170.368 47.616 22.784 81.066667 23.082667 141.354667-8.32 60.245333-31.402667 343.338667-147.84 397.994666-176.298666 27.776-14.506667 40.106667-25.770667 40.106667-35.968v-102.058667l-0.256-0.042667z m0-175.018666c0.469333-10.410667-13.141333-19.541333-40.533334-29.610667-53.248-19.498667-334.634667-131.498667-388.522666-151.253333-53.888-19.712-75.818667-18.901333-139.093334 3.84C392.234667 113.706667 92.629333 231.253333 39.338667 252.074667c-26.666667 10.496-39.68 20.181333-39.253334 30.506666V386.133333c0 10.325333 14.250667 21.248 41.386667 34.218667 54.272 25.941333 355.498667 147.669333 403.114667 170.410667 47.616 22.741333 81.066667 23.04 141.354666-8.362667 60.245333-31.402667 343.338667-147.84 397.994667-176.298667 27.776-14.506667 40.106667-25.770667 40.106667-35.968V268.074667h-0.341334zM366.677333 366.08l237.269334-36.437333-71.68 105.088-165.546667-68.650667z m524.8-94.634667l-140.330666 55.466667-15.232 5.973333-140.245334-55.466666 155.392-61.44 140.373334 55.466666z m-411.989333-101.674666l-22.954667-42.325334 71.594667 27.989334 67.498667-22.101334-18.261334 43.733334 68.778667 25.770666-88.704 9.216-19.882667 47.786667-32.085333-53.290667-102.4-9.216 76.416-27.562666z m-176.768 59.733333c70.058667 0 126.805333 21.973333 126.805333 49.109333s-56.746667 49.152-126.805333 49.152-126.848-22.058667-126.848-49.152c0-27.136 56.789333-49.152 126.848-49.152z" p-id="857"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/row.svg b/src/assets/icons/svg/row.svg
new file mode 100644
index 0000000..0780992
--- /dev/null
+++ b/src/assets/icons/svg/row.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579339929870" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1182" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M152 854.856875h325.7146875V237.715625H134.856875v600q0 6.99375 5.0746875 12.0684375T152 854.856875z m737.143125-17.1421875v-600H546.284375v617.1421875H872q6.99375 0 12.0684375-5.07375t5.0746875-12.0684375z m68.5715625-651.429375V837.715625q0 35.3821875-25.16625 60.5484375T872 923.4284375H152q-35.383125 0-60.5484375-25.1653125T66.284375 837.7146875V186.284375q0-35.3821875 25.16625-60.5484375T152 100.5715625h720q35.383125 0 60.5484375 25.1653125t25.16625 60.5484375z" p-id="1183"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/search.svg b/src/assets/icons/svg/search.svg
new file mode 100644
index 0000000..84233dd
--- /dev/null
+++ b/src/assets/icons/svg/search.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M124.884 109.812L94.256 79.166c-.357-.357-.757-.629-1.129-.914a50.366 50.366 0 0 0 8.186-27.59C101.327 22.689 78.656 0 50.67 0 22.685 0 0 22.688 0 50.663c0 27.989 22.685 50.663 50.656 50.663 10.186 0 19.643-3.03 27.6-8.201.286.385.557.771.9 1.114l30.628 30.632a10.633 10.633 0 0 0 7.543 3.129c2.728 0 5.457-1.043 7.543-3.115 4.171-4.157 4.171-10.915.014-15.073M50.671 85.338C31.557 85.338 16 69.78 16 50.663c0-19.102 15.557-34.661 34.67-34.661 19.115 0 34.657 15.559 34.657 34.675 0 19.102-15.557 34.661-34.656 34.661"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/select.svg b/src/assets/icons/svg/select.svg
new file mode 100644
index 0000000..d628382
--- /dev/null
+++ b/src/assets/icons/svg/select.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575803481213" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="804" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M62 511.97954521C62 263.86590869 263.90681826 62 511.97954521 62s449.97954521 201.825 449.97954521 449.97954521c0 248.19545479-201.90681826 449.97954521-449.97954521 449.97954521C263.90681826 962 62 760.175 62 511.97954521M901.98636348 511.97954521c0-215.24318174-175.00909131-390.41590869-390.00681827-390.41590869-215.03863652 0-389.96590869 175.17272695-389.96590868 390.41590869 0 215.28409131 175.00909131 390.45681826 389.96590868 390.45681826C727.01818174 902.47727305 901.98636348 727.30454521 901.98636348 511.97954521M264.17272695 430.28409131c0-5.76818174 2.12727305-11.51590869 6.64772696-15.87272696 8.71363652-8.75454521 22.88863652-8.75454521 31.725 0l209.4340913 208.22727305L721.45454521 414.53409131c8.75454521-8.71363652 22.97045479-8.71363652 31.90909132 0 8.71363652 8.75454521 8.71363652 22.88863652 0 31.60227304L511.97954521 685.74090869 270.71818174 446.01363653C266.27954521 441.77954521 264.17272695 436.05227305 264.17272695 430.28409131" p-id="805"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/server.svg b/src/assets/icons/svg/server.svg
new file mode 100644
index 0000000..eb287e3
--- /dev/null
+++ b/src/assets/icons/svg/server.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1547360688278" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M890 120H134a70 70 0 0 0-70 70v500a70 70 0 0 0 70 70h756a70 70 0 0 0 70-70V190a70 70 0 0 0-70-70z m-10 520a40 40 0 0 1-40 40H712V448a40 40 0 0 0-80 0v232h-80V368a40 40 0 0 0-80 0v312h-80V512a40 40 0 0 0-80 0v168H184a40 40 0 0 1-40-40V240a40 40 0 0 1 40-40h656a40 40 0 0 1 40 40zM696 824H328a40 40 0 0 0 0 80h368a40 40 0 0 0 0-80z" p-id="6718"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/shopping.svg b/src/assets/icons/svg/shopping.svg
new file mode 100644
index 0000000..87513e7
--- /dev/null
+++ b/src/assets/icons/svg/shopping.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M42.913 101.36c1.642 0 3.198.332 4.667.996a12.28 12.28 0 0 1 3.89 2.772c1.123 1.184 1.987 2.582 2.592 4.193.605 1.612.908 3.318.908 5.118 0 1.8-.303 3.507-.908 5.118-.605 1.611-1.469 3.01-2.593 4.194a13.3 13.3 0 0 1-3.889 2.843 10.582 10.582 0 0 1-4.667 1.066c-1.729 0-3.306-.355-4.732-1.066a13.604 13.604 0 0 1-3.825-2.843c-1.123-1.185-1.988-2.583-2.593-4.194a14.437 14.437 0 0 1-.907-5.118c0-1.8.302-3.506.907-5.118.605-1.61 1.47-3.009 2.593-4.193a12.515 12.515 0 0 1 3.825-2.772c1.426-.664 3.003-.996 4.732-.996zm53.932.285c1.643 0 3.22.331 4.733.995a11.386 11.386 0 0 1 3.889 2.772c1.08 1.185 1.945 2.583 2.593 4.194.648 1.61.972 3.317.972 5.118 0 1.8-.324 3.506-.972 5.117-.648 1.611-1.513 3.01-2.593 4.194a12.253 12.253 0 0 1-3.89 2.843 11 11 0 0 1-4.732 1.066 10.58 10.58 0 0 1-4.667-1.066 12.478 12.478 0 0 1-3.824-2.843c-1.08-1.185-1.945-2.583-2.593-4.194a13.581 13.581 0 0 1-.973-5.117c0-1.801.325-3.507.973-5.118.648-1.611 1.512-3.01 2.593-4.194a11.559 11.559 0 0 1 3.824-2.772 11.212 11.212 0 0 1 4.667-.995zm21.781-80.747c2.42 0 4.3.355 5.64 1.066 1.34.71 2.29 1.587 2.852 2.63a6.427 6.427 0 0 1 .778 3.34c-.044 1.185-.195 2.204-.454 3.057-.26.853-.8 2.606-1.62 5.26a589.268 589.268 0 0 1-2.788 8.743 1236.373 1236.373 0 0 0-3.047 9.453c-.994 3.128-1.75 5.592-2.269 7.393-1.123 3.79-2.55 6.42-4.278 7.89-1.728 1.469-3.846 2.203-6.352 2.203H39.023l1.945 12.795h65.342c4.148 0 6.223 1.943 6.223 5.828 0 1.896-.41 3.53-1.232 4.905-.821 1.374-2.442 2.061-4.862 2.061H38.505c-1.729 0-3.176-.426-4.343-1.28-1.167-.852-2.14-1.966-2.917-3.34a21.277 21.277 0 0 1-1.88-4.478 44.128 44.128 0 0 1-1.102-4.55c-.087-.568-.324-1.942-.713-4.122-.39-2.18-.865-4.904-1.426-8.174l-1.88-10.947c-.692-4.027-1.383-8.079-2.075-12.154-1.642-9.572-3.5-20.234-5.574-31.986H6.87c-1.296 0-2.377-.356-3.24-1.067a9.024 9.024 0 0 1-2.14-2.558 10.416 10.416 0 0 1-1.167-3.2C.108 8.53 0 7.488 0 6.54c0-1.896.583-3.46 1.75-4.69C2.917.615 4.494 0 6.482 0h13.095c1.728 0 3.111.284 4.148.853 1.037.569 1.858 1.28 2.463 2.132a8.548 8.548 0 0 1 1.297 2.701c.26.948.475 1.754.648 2.417.173.758.346 1.825.519 3.199.173 1.374.345 2.772.518 4.193.26 1.706.519 3.507.778 5.403h88.678z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/size.svg b/src/assets/icons/svg/size.svg
new file mode 100644
index 0000000..ddb25b8
--- /dev/null
+++ b/src/assets/icons/svg/size.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M0 54.857h54.796v18.286H36.531V128H18.265V73.143H0V54.857zm127.857-36.571H91.935V128H72.456V18.286H36.534V0h91.326l-.003 18.286z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/skill.svg b/src/assets/icons/svg/skill.svg
new file mode 100644
index 0000000..a3b7312
--- /dev/null
+++ b/src/assets/icons/svg/skill.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M31.652 93.206h33.401c1.44 2.418 3.077 4.663 4.93 6.692h-38.33v-6.692zm0-10.586h28.914a44.8 44.8 0 0 1-1.264-6.688h-27.65v6.688zm0-17.27H59.39c.288-2.286.714-4.532 1.34-6.687H31.65v6.687h.003zm53.913 44.84v5.85c0 2.798-2.095 5.075-4.667 5.075h-70.07c-2.576 0-4.663-2.277-4.663-5.075V31.26l23.22-20.96v22.25H17.16v6.688h18.39V6.688h45.348c2.576 0 4.667 2.277 4.667 5.066v20.009c1.987-.675 4.053-1.128 6.17-1.445v-18.56C91.738 5.28 86.874 0 80.902 0H31.15L0 28.118v87.917c0 6.48 4.859 11.759 10.832 11.759h70.07c5.974 0 10.837-5.27 10.837-11.759v-4.41c-2.117-.312-4.183-.765-6.17-1.435h-.004zM23.279 58.667h-7.96v6.688h7.96v-6.688zm-7.956 41.23h7.96v-6.691h-7.96v6.692zm7.956-23.96h-7.96v6.687h7.96v-6.688zm89.718-15.042l-4.896-4.07-12.447 17.613-11.19-9.305-3.762 5.311 16.091 13.38 16.204-22.929zM128 70.978c0-18.632-13.97-33.782-31.147-33.782-17.168 0-31.135 15.155-31.135 33.782 0 18.628 13.97 33.783 31.135 33.783 17.172 0 31.143-15.15 31.143-33.783H128zm-6.17 0c0 14.933-11.203 27.1-24.981 27.1-13.77 0-24.987-12.158-24.987-27.1 0-14.941 11.195-27.099 24.987-27.099 13.778 0 24.982 12.158 24.982 27.1z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/slider.svg b/src/assets/icons/svg/slider.svg
new file mode 100644
index 0000000..fbe4f39
--- /dev/null
+++ b/src/assets/icons/svg/slider.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577185310368" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1238" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M951.453125 476.84375H523.671875a131.8359375 131.8359375 0 0 0-254.1796875 0H72.546875v70.3125h196.9453125a131.8359375 131.8359375 0 0 0 254.1796875 0H951.453125z" p-id="1239"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/star.svg b/src/assets/icons/svg/star.svg
new file mode 100644
index 0000000..6cf86e6
--- /dev/null
+++ b/src/assets/icons/svg/star.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M70.66 4.328l14.01 29.693c1.088 2.29 3.177 3.882 5.603 4.25l31.347 4.76c6.087.926 8.528 8.756 4.117 13.247L103.05 79.395c-1.75 1.78-2.544 4.352-2.132 6.867l5.352 32.641c1.043 6.337-5.33 11.182-10.778 8.19l-28.039-15.409a7.13 7.13 0 0 0-6.91 0l-28.039 15.41c-5.448 2.99-11.821-1.854-10.777-8.19l5.352-32.642c.415-2.515-.387-5.088-2.136-6.867L2.264 56.278C-2.146 51.787.286 43.957 6.38 43.031l31.343-4.76c2.419-.368 4.51-1.96 5.595-4.25L57.334 4.328c2.728-5.77 10.605-5.77 13.325 0z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/sunny.svg b/src/assets/icons/svg/sunny.svg
new file mode 100644
index 0000000..cc628bf
--- /dev/null
+++ b/src/assets/icons/svg/sunny.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303115132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12397" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 890.432c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 0 1-66.816 0v-66.752c0-18.432 14.976-33.408 33.408-33.408z m-267.52-110.848a33.408 33.408 0 0 1 0 47.232l-47.296 47.232a33.408 33.408 0 0 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0z m582.336 0l47.232 47.232a33.408 33.408 0 0 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 1 1 47.232-47.232zM512 200.32a311.68 311.68 0 1 1 0 623.296 311.68 311.68 0 0 1 0-623.36z m0 66.752a244.864 244.864 0 1 0 0 489.728 244.864 244.864 0 0 0 0-489.728zM100.16 478.592a33.408 33.408 0 1 1 0 66.816H33.408a33.408 33.408 0 0 1 0-66.816h66.752z m890.432 0a33.408 33.408 0 0 1 0 66.816h-66.752a33.408 33.408 0 1 1 0-66.816h66.752zM197.184 149.952l47.232 47.232a33.408 33.408 0 1 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 0 1 47.232-47.232z m676.864 0a33.408 33.408 0 0 1 0 47.232l-47.232 47.232a33.408 33.408 0 1 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0zM512 0c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 1 1-66.816 0V33.408C478.592 14.976 493.568 0 512 0z" p-id="12398"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/swagger.svg b/src/assets/icons/svg/swagger.svg
new file mode 100644
index 0000000..05d4e7b
--- /dev/null
+++ b/src/assets/icons/svg/swagger.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1566036776944" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6463" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M64 223.995345h168.001164v47.997673c0 26.428509 18.878836 47.997673 41.984 47.997673h140.036654c23.095855 0 41.984-21.569164 41.984-47.997673v-47.997673h504.003491a32.004655 32.004655 0 0 0 0-64.009309H455.996509V111.988364c0-26.428509-18.878836-47.997673-41.984-47.997673H273.985164c-23.095855 0-41.984 21.569164-41.984 47.997673v47.997672H64a32.004655 32.004655 0 0 0 0 64.009309zM288.004655 128h111.997672V256H288.004655V128zM960 479.995345H791.998836v-47.997672c0-26.372655-18.878836-47.997673-41.984-47.997673H609.978182c-23.095855 0-41.984 21.634327-41.984 47.997673v47.997672H64a32.004655 32.004655 0 0 0 0 64.00931h504.003491v47.997672c0 26.363345 18.878836 47.997673 41.984 47.997673h140.036654c23.095855 0 41.984-21.634327 41.984-47.997673v-47.997672h168.001164a32.004655 32.004655 0 1 0-0.009309-64.00931zM735.995345 576H623.997673v-128h111.997672v128zM960 800.293236v-0.288581H455.996509v-47.997673c0-26.363345-18.878836-47.997673-41.984-47.997673H274.050327c-23.105164 0-41.984 21.634327-41.984 47.997673v47.997673H64v0.288581a32.004655 32.004655 0 0 0 0 64.009309c0.986764 0 1.917673-0.195491 2.885818-0.288581h165.115346v47.997672c0 26.363345 18.878836 47.997673 41.984 47.997673h140.036654c23.095855 0 41.984-21.634327 41.984-47.997673v-47.997672h501.108364c0.968145 0.093091 1.899055 0.288582 2.895127 0.288581a32.004655 32.004655 0 1 0-0.009309-64.009309zM400.002327 896H288.004655V768h111.997672v128z" fill="" p-id="6464"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/switch.svg b/src/assets/icons/svg/switch.svg
new file mode 100644
index 0000000..0ba61e3
--- /dev/null
+++ b/src/assets/icons/svg/switch.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1576042673958" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1110" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M692 792H332c-150 0-270-120-270-270s120-270 270-270h360c150 0 270 120 270 270 0 147-120 270-270 270zM332 312c-117 0-210 93-210 210s93 210 210 210h360c117 0 210-93 210-210s-93-210-210-210H332z" p-id="1111"></path><path d="M341 522m-150 0a150 150 0 1 0 300 0 150 150 0 1 0-300 0Z" p-id="1112"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/system.svg b/src/assets/icons/svg/system.svg
new file mode 100644
index 0000000..76d41ba
--- /dev/null
+++ b/src/assets/icons/svg/system.svg
@@ -0,0 +1,2 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1543827724451" class="icon" style="" viewBox="0 0 1084 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10233" xmlns:xlink="http://www.w3.org/1999/xlink" width="211.71875" height="200"><defs><style type="text/css">@font-face { font-family: rbicon; src: url("chrome-extension://dipiagiiohfljcicegpgffpbnjmgjcnf/fonts/rbicon.woff2") format("woff2"); font-weight: normal; font-style: normal; }
+</style></defs><path d="M1080.09609 434.500756c-4.216302-23.731757-26.9241-47.945376-50.595623-53.185637l-17.648235-4.095836a175.940257 175.940257 0 0 1-101.612877-80.832531 177.807476 177.807476 0 0 1-18.732427-129.801867l5.541425-16.684509c7.10748-23.129428-2.108151-54.992624-20.599646-70.833873 0 0-16.624276-14.094495-63.244529-41.199293-46.800951-26.984332-66.858502-34.513443-66.858502-34.513443-22.76803-8.372371-54.631227-0.361397-71.255503 17.407304l-12.287509 13.251234a173.470708 173.470708 0 0 1-120.465769 48.065842A174.13327 174.13327 0 0 1 421.329029 33.590675L409.583617 20.761071C393.140039 2.99237 361.096144-4.898138 338.267881 3.353767c0 0-20.358715 7.529111-67.099434 34.513443-46.800951 27.34573-63.244529 41.440225-63.244529 41.440225-18.431263 15.66055-27.646894 47.222582-20.539413 70.592941l5.059562 16.865207a178.048407 178.048407 0 0 1-18.672194 129.621169 174.916297 174.916297 0 0 1-102.275439 81.073463l-17.045906 3.854904c-23.310126 5.42096-46.258856 29.333415-50.595623 53.185637 0 0-3.854905 21.382674-3.854905 75.712737 0 54.330062 3.854905 75.712736 3.854905 75.712736 4.216302 23.972688 26.9241 47.945376 50.595623 53.185637l16.624276 3.854905a174.253736 174.253736 0 0 1 102.395904 81.314394c23.310126 40.837896 28.911785 87.337683 18.732427 129.801867l-4.81863 16.443578c-7.10748 23.129428 2.108151 54.992624 20.599646 70.833872 0 0 16.624276 14.094495 63.244529 41.199293 46.800951 27.104798 66.918735 34.513443 66.918735 34.513443 22.707798 8.372371 54.631227 0.361397 71.255503-17.407303l11.624947-12.588673a175.096996 175.096996 0 0 1 242.256662 0.120465l11.624947 12.648906c16.383345 17.708468 48.427239 25.598976 71.255503 17.347071 0 0 20.358715-7.529111 67.159666-34.513443 46.740719-27.104798 63.124063-41.199293 63.124064-41.199293 18.491496-15.600317 27.707127-47.463513 20.599646-70.833873l-5.059562-17.106139a176.723284 176.723284 0 0 1 18.672194-129.139305 176.060722 176.060722 0 0 1 102.395904-81.314394l16.68451-3.854905c23.310126-5.42096 46.258856-29.333415 50.595623-53.185637 0 0 3.854905-21.382674 3.854904-75.712737-0.240932-54.330062-4.095836-75.833202-4.095836-75.833202z m-537.819428 293.334149c-119.261112 0-216.175824-97.336342-216.175824-217.621412a216.657687 216.657687 0 0 1 216.236057-217.320249c119.200879 0 216.115591 97.276109 216.11559 217.56118-0.240932 120.044139-96.974945 217.320248-216.175823 217.320249z" p-id="10234"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tab.svg b/src/assets/icons/svg/tab.svg
new file mode 100644
index 0000000..b4b48e4
--- /dev/null
+++ b/src/assets/icons/svg/tab.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.921.052H49.08c-1.865 0-3.198 1.599-3.198 3.464v6.661c0 1.865 1.6 3.464 3.198 3.464h29.84c1.865 0 3.198-1.599 3.198-3.464V3.516C82.385 1.65 80.786.052 78.92.052zm45.563 0H94.642c-1.865 0-3.464 1.599-3.464 3.464v6.661c0 1.865 1.599 3.464 3.464 3.464h29.842c1.865-.266 3.464-1.599 3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464zm0 22.382H40.02c-1.866 0-3.464-1.599-3.464-3.464V3.516c0-1.865-1.599-3.464-3.464-3.464H3.516C1.65.052.052 1.651.052 3.516V124.75c0 1.598 1.599 3.197 3.464 3.197h120.968c1.865 0 3.464-1.599 3.464-3.464V25.898c0-1.865-1.599-3.464-3.464-3.464z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/table.svg b/src/assets/icons/svg/table.svg
new file mode 100644
index 0000000..0e3dc9d
--- /dev/null
+++ b/src/assets/icons/svg/table.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/><path d="M.006.064h127.988v31.104H.006V.064zm0 38.016h38.396v41.472H.006V38.08zm0 48.384h38.396v41.472H.006V86.464zM44.802 38.08h38.396v41.472H44.802V38.08zm0 48.384h38.396v41.472H44.802V86.464zM89.598 38.08h38.396v41.472H89.598zm0 48.384h38.396v41.472H89.598z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/textarea.svg b/src/assets/icons/svg/textarea.svg
new file mode 100644
index 0000000..2709f29
--- /dev/null
+++ b/src/assets/icons/svg/textarea.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1575802855098" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2984" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M896 160H128c-35.2 0-64 28.8-64 64v576c0 35.2 28.8 64 64 64h768c35.2 0 64-28.8 64-64V224c0-35.2-28.8-64-64-64z m0 608c0 16-12.8 32-32 32H160c-19.2 0-32-12.8-32-32V256c0-16 12.8-32 32-32h704c19.2 0 32 12.8 32 32v512z" p-id="2985"></path><path d="M224 288c-19.2 0-32 12.8-32 32v256c0 16 12.8 32 32 32s32-12.8 32-32V320c0-16-12.8-32-32-32z m608 480c19.2 0 32-12.8 32-32V608L704 768h128z" p-id="2986"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/theme.svg b/src/assets/icons/svg/theme.svg
new file mode 100644
index 0000000..5982a2f
--- /dev/null
+++ b/src/assets/icons/svg/theme.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M125.5 36.984L95.336 2.83C93.735 1.018 91.565 0 89.3 0c-2.263 0-4.433 1.018-6.033 2.83l-3.786 4.286c-1.6 1.812-3.77 2.83-6.032 2.831H54.553c-2.263 0-4.434-1.018-6.033-2.83L44.734 2.83C43.134 1.018 40.964 0 38.701 0c-2.263 0-4.434 1.018-6.034 2.83L2.5 36.984C.9 38.796 0 41.254 0 43.815c0 2.562.899 5.02 2.5 6.831L14.565 64.31c2.178 2.468 5.367 3.403 8.33 2.444 1.35-.435 2.709.592 2.709 2.18v49.407c0 5.313 3.84 9.66 8.532 9.66h59.726c4.693 0 8.532-4.347 8.532-9.66V68.934c0-1.59 1.36-2.616 2.71-2.181 2.962.96 6.15.024 8.329-2.444L125.5 50.646c1.6-1.811 2.499-4.269 2.499-6.83 0-2.563-.899-5.02-2.5-6.832z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/time-range.svg b/src/assets/icons/svg/time-range.svg
new file mode 100644
index 0000000..13c1202
--- /dev/null
+++ b/src/assets/icons/svg/time-range.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1579774825624" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1248" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M498.595712 482.290351 345.420077 482.290351l0 57.307194 210.477712 0L555.897789 274.196942l-57.301054 0L498.596735 482.290351zM498.595712 482.290351" p-id="1249"></path><path d="M577.685002 644.98478l379.879913 0 0 57.302077L577.685002 702.286858 577.685002 644.98478 577.685002 644.98478zM577.685002 644.98478" p-id="1250"></path><path d="M577.685002 773.764795l379.879913 0 0 57.307194L577.685002 831.071989 577.685002 773.764795 577.685002 773.764795zM577.685002 773.764795" p-id="1251"></path><path d="M577.685002 902.549927l379.879913 0 0 57.307194L577.685002 959.857121 577.685002 902.549927 577.685002 902.549927zM577.685002 902.549927" p-id="1252"></path><path d="M102.523001 382.290823c4.450359 2.615571 9.470699 3.954055 14.530948 3.954055 2.969635 0 5.952572-0.461511 8.836249-1.394766l190.809767-61.886489c15.052834-4.882194 23.297612-21.040199 18.415418-36.08894-4.882194-15.052834-21.040199-23.297612-36.093033-18.415418L175.676092 308.458257c15.994276-26.115797 35.170011-50.537 57.370639-72.743768 73.767074-73.767074 171.845857-114.388237 276.16783-114.388237 104.32095 0 202.39564 40.622186 276.16169 114.388237s114.393353 171.845857 114.393353 276.16783c0 26.427906-2.615571 52.449559-7.709589 77.780481l58.302871 0c4.464685-25.499767 6.708795-51.470255 6.708795-77.780481 0-60.449767-11.845793-119.102608-35.204803-174.336584-22.559808-53.334719-54.850236-101.226472-95.968725-142.349055-41.122583-41.122583-89.017406-73.408917-142.348032-95.968725C628.317169 75.866898 569.659211 64.021106 509.215584 64.021106c-60.448744 0-119.106702 11.845793-174.336584 35.207873-53.334719 22.559808-101.230566 54.846142-142.349055 95.968725-23.980157 23.980157-44.934398 50.278103-62.727647 78.601172l-20.738323-105.655342c-3.043313-15.527648-18.105357-25.642007-33.631982-22.599717-15.527648 3.048429-25.64303 18.105357-22.599717 33.637098l36.102243 183.932126C90.51348 371.153158 95.460142 378.13313 102.523001 382.290823L102.523001 382.290823zM102.523001 382.290823" p-id="1253"></path><path d="M126.020158 587.9416 67.768453 587.9416c5.759167 33.679054 15.368012 66.544579 28.789697 98.278327 22.559808 53.333696 54.850236 101.225449 95.971795 142.348032 41.122583 41.122583 89.014336 73.408917 142.349055 95.968725 54.112432 22.88829 111.517863 34.71157 170.668031 35.18229L505.547031 902.395408c-102.94972-0.941442-199.594851-41.445948-272.499277-114.349351C177.545672 732.543975 140.810003 663.275355 126.020158 587.9416L126.020158 587.9416zM126.020158 587.9416" p-id="1254"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/time.svg b/src/assets/icons/svg/time.svg
new file mode 100644
index 0000000..b376e32
--- /dev/null
+++ b/src/assets/icons/svg/time.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577099827399" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1008" xmlns:xlink="http://www.w3.org/1999/xlink" width="81" height="81"><defs><style type="text/css"></style></defs><path d="M520 559h204c17.673 0 32 14.327 32 32 0 17.673-14.327 32-32 32H488c-17.673 0-32-14.327-32-32 0-0.167 0.001-0.334 0.004-0.5a32.65 32.65 0 0 1-0.004-0.5V277c0-17.673 14.327-32 32-32 17.673 0 32 14.327 32 32v282z m-8 401C264.576 960 64 759.424 64 512S264.576 64 512 64s448 200.576 448 448-200.576 448-448 448z m0-64c212.077 0 384-171.923 384-384S724.077 128 512 128 128 299.923 128 512s171.923 384 384 384z" p-id="1009"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tool.svg b/src/assets/icons/svg/tool.svg
new file mode 100644
index 0000000..48e0e35
--- /dev/null
+++ b/src/assets/icons/svg/tool.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1553828490559" class="icon" style="" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1684" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M898.831744 900.517641 103.816972 900.517641c-36.002982 0-65.363683-29.286-65.363683-65.313541l0-554.949184c0-36.041868 29.361725-65.326844 65.363683-65.326844l795.015795 0c36.002982 0 65.198931 29.284977 65.198931 65.326844l0 554.949184C964.030675 871.231641 934.834726 900.517641 898.831744 900.517641L898.831744 900.517641zM103.816972 255.593236c-13.576203 0-24.711821 11.085476-24.711821 24.662703l0 554.949184c0 13.576203 11.136641 24.662703 24.711821 24.662703l795.015795 0c13.577227 0 24.547069-11.086499 24.547069-24.662703l0-554.949184c0-13.577227-10.970866-24.662703-24.547069-24.662703L103.816972 255.593236 103.816972 255.593236zM664.346245 251.774257c-11.161201 0-20.332071-9.080819-20.332071-20.332071l0-101.278661c0-13.576203-11.047614-24.623817-24.699542-24.623817L383.181611 105.539708c-13.576203 0-24.712845 11.04659-24.712845 24.623817l0 101.278661c0 11.252275-9.041934 20.332071-20.332071 20.332071-11.20111 0-20.319791-9.080819-20.319791-20.332071l0-101.278661c0-35.989679 29.323862-65.275679 65.364707-65.275679l236.133022 0c36.06745 0 65.402569 29.284977 65.402569 65.275679l0 101.278661C684.717202 242.694461 675.636383 251.774257 664.346245 251.774257L664.346245 251.774257zM413.233044 521.725502 75.694471 521.725502c-11.163247 0-20.333094-9.117658-20.333094-20.35663 0-11.252275 9.169847-20.332071 20.333094-20.332071l337.538573 0c11.277858 0 20.319791 9.080819 20.319791 20.332071C433.552835 512.607844 424.510902 521.725502 413.233044 521.725502L413.233044 521.725502zM912.894018 521.725502 575.367725 521.725502c-11.213389 0-20.332071-9.117658-20.332071-20.35663 0-11.252275 9.118682-20.332071 20.332071-20.332071l337.526293 0c11.290137 0 20.332071 9.080819 20.332071 20.332071C933.226089 512.607844 924.184155 521.725502 912.894018 521.725502L912.894018 521.725502zM557.56322 634.217552 445.085496 634.217552c-11.213389 0-20.332071-9.079796-20.332071-20.331048l0-168.763658c0-11.251252 9.118682-20.332071 20.332071-20.332071l112.478747 0c11.290137 0 20.370956 9.080819 20.370956 20.332071l0 168.763658C577.934177 625.137757 568.853357 634.217552 557.56322 634.217552L557.56322 634.217552zM465.417567 593.514525l71.827909 0L537.245476 465.454918l-71.827909 0L465.417567 593.514525 465.417567 593.514525z" p-id="1685"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tree-table.svg b/src/assets/icons/svg/tree-table.svg
new file mode 100644
index 0000000..8aafdb8
--- /dev/null
+++ b/src/assets/icons/svg/tree-table.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M44.8 0h79.543C126.78 0 128 1.422 128 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H44.8c-2.438 0-3.657-1.422-3.657-4.267V4.267C41.143 1.422 42.362 0 44.8 0zm22.857 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 80 64 78.578 64 75.733V52.267C64 49.422 65.219 48 67.657 48zm0 48h56.686c2.438 0 3.657 1.422 3.657 4.267v23.466c0 2.845-1.219 4.267-3.657 4.267H67.657C65.22 128 64 126.578 64 123.733v-23.466C64 97.422 65.219 96 67.657 96zM50.286 68.267c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V32h6.4c2.02 0 3.658-1.91 3.658-4.267V4.267C27.429 1.91 25.79 0 23.77 0H3.657C1.637 0 0 1.91 0 4.267v23.466C0 30.09 1.637 32 3.657 32h6.4v80c0 2.356 1.638 4.267 3.657 4.267h36.572c2.02 0 3.657-1.91 3.657-4.267 0-2.356-1.638-4.267-3.657-4.267H17.37V68.267h32.915z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/tree.svg b/src/assets/icons/svg/tree.svg
new file mode 100644
index 0000000..dd4b7dd
--- /dev/null
+++ b/src/assets/icons/svg/tree.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M126.713 90.023c.858.985 1.287 2.134 1.287 3.447v29.553c0 1.423-.429 2.6-1.287 3.53-.858.93-1.907 1.395-3.146 1.395H97.824c-1.145 0-2.146-.465-3.004-1.395-.858-.93-1.287-2.107-1.287-3.53V93.47c0-.875.19-1.696.572-2.462.382-.766.906-1.368 1.573-1.806a3.84 3.84 0 0 1 2.146-.657h9.725V69.007a3.84 3.84 0 0 0-.43-1.806 3.569 3.569 0 0 0-1.143-1.313 2.714 2.714 0 0 0-1.573-.492h-36.47v23.149h9.725c1.144 0 2.145.492 3.004 1.478.858.985 1.287 2.134 1.287 3.447v29.553c0 .876-.191 1.696-.573 2.463-.38.766-.905 1.368-1.573 1.806a3.84 3.84 0 0 1-2.145.656H51.915a3.84 3.84 0 0 1-2.145-.656c-.668-.438-1.216-1.04-1.645-1.806a4.96 4.96 0 0 1-.644-2.463V93.47c0-1.313.43-2.462 1.288-3.447.858-.986 1.907-1.478 3.146-1.478h9.582v-23.15h-37.9c-.953 0-1.74.356-2.359 1.068-.62.711-.93 1.56-.93 2.544v19.538h9.726c1.239 0 2.264.492 3.074 1.478.81.985 1.216 2.134 1.216 3.447v29.553c0 1.423-.405 2.6-1.216 3.53-.81.93-1.835 1.395-3.074 1.395H4.29c-.476 0-.93-.082-1.358-.246a4.1 4.1 0 0 1-1.144-.657 4.658 4.658 0 0 1-.93-1.067 5.186 5.186 0 0 1-.643-1.395 5.566 5.566 0 0 1-.215-1.56V93.47c0-.437.048-.875.143-1.313a3.95 3.95 0 0 1 .429-1.15c.19-.328.429-.656.715-.984.286-.329.572-.602.858-.821.286-.22.62-.383 1.001-.493.382-.11.763-.164 1.144-.164h9.726V61.619c0-.985.31-1.833.93-2.544.619-.712 1.358-1.068 2.216-1.068h44.335V39.62h-9.582c-1.24 0-2.288-.492-3.146-1.477a5.09 5.09 0 0 1-1.287-3.448V5.14c0-1.423.429-2.627 1.287-3.612.858-.985 1.907-1.477 3.146-1.477h25.743c.763 0 1.478.246 2.145.739a5.17 5.17 0 0 1 1.573 1.888c.382.766.573 1.587.573 2.462v29.553c0 1.313-.43 2.463-1.287 3.448-.859.985-1.86 1.477-3.004 1.477h-9.725v18.389h42.762c.954 0 1.74.355 2.36 1.067.62.711.93 1.56.93 2.545v26.925h9.582c1.239 0 2.288.492 3.146 1.478z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/upload.svg b/src/assets/icons/svg/upload.svg
new file mode 100644
index 0000000..bae49c0
--- /dev/null
+++ b/src/assets/icons/svg/upload.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1577540289643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7922" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M530.944 458.24l4.8 3.456 122.176 106.816a32 32 0 0 1-37.44 51.584l-4.672-3.392L546.56 556.16v280.704a32 32 0 0 1-26.24 31.488l-5.76 0.512a32 32 0 0 1-31.424-26.24l-0.512-5.76-0.064-280.704-69.12 60.48a32 32 0 0 1-40.96 0.896l-4.16-3.968a32 32 0 0 1-0.96-40.96l4.032-4.16 122.176-106.816a32 32 0 0 1 37.312-3.456zM497.92 128c128.128 0 239.168 82.304 275.52 199.04 123.968 11.264 221.312 113.088 221.312 237.44 0 128.128-103.68 232.96-234.88 238.272h-5.888l-35.52 0.192a32 32 0 0 1-0.192-64l35.264-0.128 4.672-0.064c96.384-3.84 172.544-80.896 172.544-174.272 0-96.128-80.512-174.464-179.584-174.464h-1.984a32 32 0 0 1-32-25.28C695.872 264.96 604.736 192 497.92 192 381.824 192 285.44 277.76 274.816 388.48a32 32 0 0 1-28.352 28.8c-83.968 9.152-147.84 78.208-147.84 159.552l0.192 7.936c3.84 85.76 77.056 154.112 166.592 154.112h45.632a32 32 0 0 1 0 64h-45.632C142.016 802.944 40.32 708.032 34.88 586.88l-0.192-9.28c0-106.88 76.352-197.184 179.968-219.904C239.488 226.112 357.76 128 497.856 128z" p-id="7923"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/user.svg b/src/assets/icons/svg/user.svg
new file mode 100644
index 0000000..0ba0716
--- /dev/null
+++ b/src/assets/icons/svg/user.svg
@@ -0,0 +1 @@
+<svg width="130" height="130" xmlns="http://www.w3.org/2000/svg"><path d="M63.444 64.996c20.633 0 37.359-14.308 37.359-31.953 0-17.649-16.726-31.952-37.359-31.952-20.631 0-37.36 14.303-37.358 31.952 0 17.645 16.727 31.953 37.359 31.953zM80.57 75.65H49.434c-26.652 0-48.26 18.477-48.26 41.27v2.664c0 9.316 21.608 9.325 48.26 9.325H80.57c26.649 0 48.256-.344 48.256-9.325v-2.663c0-22.794-21.605-41.271-48.256-41.271z" stroke="#979797"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/validCode.svg b/src/assets/icons/svg/validCode.svg
new file mode 100644
index 0000000..cfb1021
--- /dev/null
+++ b/src/assets/icons/svg/validCode.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1569580729849" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1939" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M513.3 958.5c-142.2 0-397.9-222.1-401.6-440.5V268c1.7-39.6 31.7-72.3 71.1-77.3 49-4.6 97.1-16.5 142.7-35.3 47.8-14 91.9-38.3 129.4-71.1 30.3-24.4 72.9-26.3 105.3-4.6 39.9 30.7 83.8 55.9 130.5 74.6 48.6 14.7 98.2 25.9 148.4 33.7 38.5 7.6 67.1 40.3 69.5 79.5 3.3 84.9 2.5 169.9-2.6 254.7-33.7 281.6-253.7 436.4-392.7 436.3z m-0.1-813.7c-7.2-0.2-14.3 2-20 6.4-39.7 35.2-86.8 61.1-137.7 75.7-46.8 19.2-96.2 31-146.6 35.2-11 3.2-18.8 13-19.5 24.4v230.1c3.5 180.3 223.3 361 323.9 361s287.3-120.2 317.6-360.5c7.3-142.7 0-228.6 0-229.6-1.3-13.3-11-24.3-24-27.3-49.6-7.7-98.6-19-146.5-33.7-46.3-19.5-89.7-45.3-129-76.7-5.8-3.8-12.7-5.5-19.5-4.9l1.3-0.1z" fill="#C6CCDA" p-id="1940"></path><path d="M750.1 428L490.7 673.2c-11.7 11.1-29.5 12.9-43.1 4.2l-6.8-5.8-141.2-149.4c-9.3-9.3-12.7-22.9-9-35.5 3.8-12.6 14.1-22.1 27-24.8 12.9-2.7 26.1 1.9 34.6 11.9L469 597.5l233.7-221c14.6-12.8 36.8-11.6 49.9 2.7 13.2 14.2 11.5 35.3-2.5 48.8" fill="#C6CCDA" p-id="1941"></path></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/wechat.svg b/src/assets/icons/svg/wechat.svg
new file mode 100644
index 0000000..c586e55
--- /dev/null
+++ b/src/assets/icons/svg/wechat.svg
@@ -0,0 +1 @@
+<svg width="128" height="110" xmlns="http://www.w3.org/2000/svg"><path d="M86.635 33.334c1.467 0 2.917.113 4.358.283C87.078 14.392 67.58.111 45.321.111 20.44.111.055 17.987.055 40.687c0 13.104 6.781 23.863 18.115 32.209l-4.527 14.352 15.82-8.364c5.666 1.182 10.207 2.395 15.858 2.395 1.42 0 2.829-.073 4.227-.189-.886-3.19-1.398-6.53-1.398-9.996 0-20.845 16.98-37.76 38.485-37.76zm-24.34-12.936c3.407 0 5.665 2.363 5.665 5.954 0 3.576-2.258 5.97-5.666 5.97-3.392 0-6.795-2.395-6.795-5.97 0-3.591 3.403-5.954 6.795-5.954zM30.616 32.323c-3.393 0-6.818-2.395-6.818-5.971 0-3.591 3.425-5.954 6.818-5.954 3.392 0 5.65 2.363 5.65 5.954 0 3.576-2.258 5.97-5.65 5.97z"/><path d="M127.945 70.52c0-19.075-18.108-34.623-38.448-34.623-21.537 0-38.5 15.548-38.5 34.623 0 19.108 16.963 34.622 38.5 34.622 4.508 0 9.058-1.2 13.584-2.395l12.414 7.167-3.404-11.923c9.087-7.184 15.854-16.712 15.854-27.471zm-50.928-5.97c-2.254 0-4.53-2.362-4.53-4.773 0-2.378 2.276-4.771 4.53-4.771 3.422 0 5.665 2.393 5.665 4.771 0 2.41-2.243 4.773-5.665 4.773zm24.897 0c-2.24 0-4.498-2.362-4.498-4.773 0-2.378 2.258-4.771 4.498-4.771 3.392 0 5.665 2.393 5.665 4.771 0 2.41-2.273 4.773-5.665 4.773z"/></svg>
\ No newline at end of file
diff --git a/src/assets/icons/svg/zip.svg b/src/assets/icons/svg/zip.svg
new file mode 100644
index 0000000..f806fc4
--- /dev/null
+++ b/src/assets/icons/svg/zip.svg
@@ -0,0 +1 @@
+<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M78.527 116.793c.178.008.348.024.527.024h40.233c4.711-.005 8.53-3.677 8.534-8.21V18.895c-.004-4.532-3.823-8.204-8.534-8.209H79.054c-.179 0-.353.016-.527.024V0L0 10.082v107.406l78.527 10.342v-11.037zm0-101.362c.174-.024.348-.052.527-.052h40.233c2.018 0 3.659 1.578 3.659 3.52v89.713c-.003 1.942-1.64 3.517-3.659 3.519H79.054c-.179 0-.353-.028-.527-.052V15.431zM30.262 75.757l-18.721-.46V72.37l11.3-16.673v-.148l-10.266.164v-4.51l17.504-.44v3.264L18.696 70.76v.144l11.566.176v4.678zm9.419.231l-5.823-.144V50.671l5.823-.144v25.461zm22.255-11.632c-2.168 1.922-5.353 2.76-9.02 2.736-.702.004-1.402-.04-2.097-.131v9.303l-5.997-.148V50.743c1.852-.352 4.473-.647 8.218-.743 3.838-.096 6.608.539 8.48 1.913 1.807 1.306 3.032 3.5 3.032 6.112s-.926 4.833-2.612 6.331h-.004zM53.36 54.45c-.856-.01-1.71.083-2.541.275v7.682c.523.116 1.167.152 2.06.152 3.301-.004 5.36-1.614 5.36-4.314 0-2.425-1.772-3.843-4.875-3.791l-.004-.004zm39.847-37.066h9.564v3.795h-9.564v-3.795zm-9.568 5.68h9.564v3.8h-9.564v-3.8zm9.568 6.216h9.564v3.799h-9.564V29.28zm0 12h9.564v3.794h-9.564V41.28zm-9.568-6.096h9.564v3.795h-9.564v-3.795zm9.472 47.064c2.512 0 4.921-.96 6.697-2.67 1.776-1.708 2.773-4.026 2.772-6.442l-1.748-15.263c0-5.033-2.492-9.112-7.725-9.112-5.232 0-7.72 4.079-7.72 9.112l-1.752 15.263c-.001 2.417.996 4.735 2.773 6.444 1.777 1.71 4.187 2.669 6.7 2.668h.003zm-3.135-16.75h6.27v12.743h-6.27V65.5z"/></svg>
\ No newline at end of file
diff --git a/src/assets/images/Rectangle 76@2x.png b/src/assets/images/Rectangle 76@2x.png
new file mode 100644
index 0000000..3aa9a4b
--- /dev/null
+++ b/src/assets/images/Rectangle 76@2x.png
Binary files differ
diff --git "a/src/assets/images/Rectangle 77@2x\0501\051.png" "b/src/assets/images/Rectangle 77@2x\0501\051.png"
new file mode 100644
index 0000000..995a340
--- /dev/null
+++ "b/src/assets/images/Rectangle 77@2x\0501\051.png"
Binary files differ
diff --git a/src/assets/images/Rectangle 77@2x.png b/src/assets/images/Rectangle 77@2x.png
new file mode 100644
index 0000000..3957c66
--- /dev/null
+++ b/src/assets/images/Rectangle 77@2x.png
Binary files differ
diff --git a/src/assets/images/caigou.png b/src/assets/images/caigou.png
new file mode 100644
index 0000000..3d54f1a
--- /dev/null
+++ b/src/assets/images/caigou.png
Binary files differ
diff --git a/src/assets/images/chartCard.svg b/src/assets/images/chartCard.svg
new file mode 100644
index 0000000..32d48b1
--- /dev/null
+++ b/src/assets/images/chartCard.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="40" height="40" viewBox="0 0 40 40"><defs><mask id="master_svg0_88_35670" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><ellipse cx="20" cy="20" rx="20" ry="20" fill="#FFFFFF" fill-opacity="1"/></mask><clipPath id="master_svg1_88_35666"><rect x="7" y="7" width="27" height="27" rx="0"/></clipPath><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg2_88_26531"><stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"/><stop offset="98.57142567634583%" stop-color="#F0FBFF" stop-opacity="1"/></linearGradient><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg3_88_26531"><stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"/><stop offset="98.57142567634583%" stop-color="#F0FBFF" stop-opacity="1"/></linearGradient></defs><g mask="url(#master_svg0_88_35670)"><ellipse cx="20" cy="20" rx="20" ry="20" fill="#0092FF" fill-opacity="1"/><g clip-path="url(#master_svg1_88_35666)"><path d="M21.175671875,27.58515925L14.263672875000001,27.58515925C13.750673275,27.58515925,13.426672974999999,27.24765625,13.426672974999999,26.74815725C13.426672974999999,26.23515525,13.764173075,25.911160250000002,14.263672875000001,25.911160250000002L21.351173875,25.911160250000002C21.688676875,24.89865825,22.188173875,23.88615425,22.863174875,23.211156250000002L14.263672875000001,23.211156250000002C13.750673275,23.211156250000002,13.426672974999999,22.87365525,13.426672974999999,22.37415225C13.426672974999999,21.87465325,13.764173075,21.537155249999998,14.263672875000001,21.537155249999998L25.738675875,21.537155249999998C26.251674875,21.37515325,26.751174875,21.37515325,27.088676875,21.37515325C28.438678875,21.37515325,29.626676875,21.88815525,30.625678875,22.549656249999998L30.625678875,13.072656349999999C30.625678875,11.38515625,29.275674875,10.03515625,27.588174875,10.03515625L27.075177875,10.03515625L27.075177875,13.24815675C27.075177875,14.935656550000001,25.725173875,16.285657450000002,24.037676875000002,16.285657450000002L16.113174475,16.285657450000002C14.425674475000001,16.272157149999998,13.075673375000001,14.922158249999999,13.075673375000001,13.23465635L13.075673375000001,10.03515625L12.238672475,10.03515625C10.551171974999999,10.03515625,9.201171875,11.38515625,9.201171875,13.072656349999999L9.201171875,29.94765825C9.201171875,31.63515625,10.551171974999999,32.985161250000004,12.238672475,32.985161250000004L25.576673875,32.985161250000004C23.200674875,32.485662250000004,21.337675875000002,30.28515825,21.175671875,27.58515925Z" fill="url(#master_svg2_88_26531)" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M16.1124145625,14.764538762499999L24.0504169625,14.764538762499999C24.8874170625,14.764538762499999,25.5624140625,14.0895385625,25.5624140625,13.252537762500001L25.5624140625,10.0395388625L22.5249171625,10.0395388625C22.3629159625,8.8650390625,21.3369150625,7.8525390625,19.986915562500002,7.8525390625C18.7989153625,7.8525390625,17.7864150625,8.8650390625,17.6244149625,10.0395388625L14.5869140625,10.0395388625L14.5869140625,13.252537762500001C14.5869140625,14.0895385625,15.2619143725,14.764538762499999,16.1124145625,14.764538762499999ZM30.7869150625,24.3900370625C29.9499160625,23.3775360625,28.5999220625,22.7025380625,27.2499170625,22.7025380625L26.412916062500003,22.7025380625C25.8999180625,22.7025380625,25.5759160625,22.8780390625,25.0629190625,23.2155400625C24.0504169625,23.7285370625,23.1999158625,24.7275330625,22.700415562499998,25.9155390625C22.5384173625,26.4285390625,22.5384173625,26.9280380625,22.5384173625,27.4275380625L22.5384173625,27.5895390625C22.700415562499998,30.1275410625,24.7254170625,31.9770390625,27.1014200625,31.9770390625C28.4514180625,31.9770390625,29.8014230625,31.3020400625,30.6384180625,30.2895320625C31.3134210625,29.4525340625,31.6509170625,28.4265380625,31.6509170625,27.2520330625C31.8129160625,26.2395310625,31.2999250625,25.2270370625,30.7869150625,24.3900370625ZM29.7879200625,26.5770380625L27.0879160625,29.2770390625C26.7504160625,29.6145400625,26.412916062500003,29.6145400625,26.0754160625,29.2770390625L24.387915562499998,27.5895390625C24.0504169625,27.2520370625,24.0504169625,26.9145390625,24.387915562499998,26.5770380625C24.725415062499998,26.2395380625,25.0629150625,26.2395400625,25.4004160625,26.5770380625L26.2374170625,27.4140400625L26.5749150625,27.7515370625L28.7619150625,25.5645350625C29.0994140625,25.2270370625,29.4369190625,25.2270370625,29.774416062500002,25.5645350625C30.1119160625,25.9020370625,30.1119160625,26.2395380625,29.7879200625,26.5770380625Z" fill="url(#master_svg3_88_26531)" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/assets/images/chartCard2.svg b/src/assets/images/chartCard2.svg
new file mode 100644
index 0000000..ff67331
--- /dev/null
+++ b/src/assets/images/chartCard2.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="40" height="40" viewBox="0 0 40 40"><defs><mask id="master_svg0_88_35670" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><ellipse cx="20" cy="20" rx="20" ry="20" fill="#FFFFFF" fill-opacity="1"/></mask><clipPath id="master_svg1_88_35666"><rect x="7" y="7" width="27" height="27" rx="0"/></clipPath><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg2_88_26531"><stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"/><stop offset="98.57142567634583%" stop-color="#F0FBFF" stop-opacity="1"/></linearGradient><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg3_88_26531"><stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"/><stop offset="98.57142567634583%" stop-color="#F0FBFF" stop-opacity="1"/></linearGradient></defs><g mask="url(#master_svg0_88_35670)"><ellipse cx="20" cy="20" rx="20" ry="20" fill="#5EB334" fill-opacity="1"/><g clip-path="url(#master_svg1_88_35666)"><path d="M21.175671875,27.58515925L14.263672875000001,27.58515925C13.750673275,27.58515925,13.426672974999999,27.24765625,13.426672974999999,26.74815725C13.426672974999999,26.23515525,13.764173075,25.911160250000002,14.263672875000001,25.911160250000002L21.351173875,25.911160250000002C21.688676875,24.89865825,22.188173875,23.88615425,22.863174875,23.211156250000002L14.263672875000001,23.211156250000002C13.750673275,23.211156250000002,13.426672974999999,22.87365525,13.426672974999999,22.37415225C13.426672974999999,21.87465325,13.764173075,21.537155249999998,14.263672875000001,21.537155249999998L25.738675875,21.537155249999998C26.251674875,21.37515325,26.751174875,21.37515325,27.088676875,21.37515325C28.438678875,21.37515325,29.626676875,21.88815525,30.625678875,22.549656249999998L30.625678875,13.072656349999999C30.625678875,11.38515625,29.275674875,10.03515625,27.588174875,10.03515625L27.075177875,10.03515625L27.075177875,13.24815675C27.075177875,14.935656550000001,25.725173875,16.285657450000002,24.037676875000002,16.285657450000002L16.113174475,16.285657450000002C14.425674475000001,16.272157149999998,13.075673375000001,14.922158249999999,13.075673375000001,13.23465635L13.075673375000001,10.03515625L12.238672475,10.03515625C10.551171974999999,10.03515625,9.201171875,11.38515625,9.201171875,13.072656349999999L9.201171875,29.94765825C9.201171875,31.63515625,10.551171974999999,32.985161250000004,12.238672475,32.985161250000004L25.576673875,32.985161250000004C23.200674875,32.485662250000004,21.337675875000002,30.28515825,21.175671875,27.58515925Z" fill="url(#master_svg2_88_26531)" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M16.1124145625,14.764538762499999L24.0504169625,14.764538762499999C24.8874170625,14.764538762499999,25.5624140625,14.0895385625,25.5624140625,13.252537762500001L25.5624140625,10.0395388625L22.5249171625,10.0395388625C22.3629159625,8.8650390625,21.3369150625,7.8525390625,19.986915562500002,7.8525390625C18.7989153625,7.8525390625,17.7864150625,8.8650390625,17.6244149625,10.0395388625L14.5869140625,10.0395388625L14.5869140625,13.252537762500001C14.5869140625,14.0895385625,15.2619143725,14.764538762499999,16.1124145625,14.764538762499999ZM30.7869150625,24.3900370625C29.9499160625,23.3775360625,28.5999220625,22.7025380625,27.2499170625,22.7025380625L26.412916062500003,22.7025380625C25.8999180625,22.7025380625,25.5759160625,22.8780390625,25.0629190625,23.2155400625C24.0504169625,23.7285370625,23.1999158625,24.7275330625,22.700415562499998,25.9155390625C22.5384173625,26.4285390625,22.5384173625,26.9280380625,22.5384173625,27.4275380625L22.5384173625,27.5895390625C22.700415562499998,30.1275410625,24.7254170625,31.9770390625,27.1014200625,31.9770390625C28.4514180625,31.9770390625,29.8014230625,31.3020400625,30.6384180625,30.2895320625C31.3134210625,29.4525340625,31.6509170625,28.4265380625,31.6509170625,27.2520330625C31.8129160625,26.2395310625,31.2999250625,25.2270370625,30.7869150625,24.3900370625ZM29.7879200625,26.5770380625L27.0879160625,29.2770390625C26.7504160625,29.6145400625,26.412916062500003,29.6145400625,26.0754160625,29.2770390625L24.387915562499998,27.5895390625C24.0504169625,27.2520370625,24.0504169625,26.9145390625,24.387915562499998,26.5770380625C24.725415062499998,26.2395380625,25.0629150625,26.2395400625,25.4004160625,26.5770380625L26.2374170625,27.4140400625L26.5749150625,27.7515370625L28.7619150625,25.5645350625C29.0994140625,25.2270370625,29.4369190625,25.2270370625,29.774416062500002,25.5645350625C30.1119160625,25.9020370625,30.1119160625,26.2395380625,29.7879200625,26.5770380625Z" fill="url(#master_svg3_88_26531)" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/assets/images/chartCard3.svg b/src/assets/images/chartCard3.svg
new file mode 100644
index 0000000..0e8ce16
--- /dev/null
+++ b/src/assets/images/chartCard3.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="40" height="40" viewBox="0 0 40 40"><defs><mask id="master_svg0_88_35670" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="40" height="40"><ellipse cx="20" cy="20" rx="20" ry="20" fill="#FFFFFF" fill-opacity="1"/></mask><clipPath id="master_svg1_88_35666"><rect x="7" y="7" width="27" height="27" rx="0"/></clipPath><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg2_88_26531"><stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"/><stop offset="98.57142567634583%" stop-color="#F0FBFF" stop-opacity="1"/></linearGradient><linearGradient x1="0.5" y1="0" x2="0.5" y2="1" id="master_svg3_88_26531"><stop offset="0%" stop-color="#FFFFFF" stop-opacity="1"/><stop offset="98.57142567634583%" stop-color="#F0FBFF" stop-opacity="1"/></linearGradient></defs><g mask="url(#master_svg0_88_35670)"><ellipse cx="20" cy="20" rx="20" ry="20" fill="#8000FF" fill-opacity="1"/><g clip-path="url(#master_svg1_88_35666)"><path d="M21.175671875,27.58515925L14.263672875000001,27.58515925C13.750673275,27.58515925,13.426672974999999,27.24765625,13.426672974999999,26.74815725C13.426672974999999,26.23515525,13.764173075,25.911160250000002,14.263672875000001,25.911160250000002L21.351173875,25.911160250000002C21.688676875,24.89865825,22.188173875,23.88615425,22.863174875,23.211156250000002L14.263672875000001,23.211156250000002C13.750673275,23.211156250000002,13.426672974999999,22.87365525,13.426672974999999,22.37415225C13.426672974999999,21.87465325,13.764173075,21.537155249999998,14.263672875000001,21.537155249999998L25.738675875,21.537155249999998C26.251674875,21.37515325,26.751174875,21.37515325,27.088676875,21.37515325C28.438678875,21.37515325,29.626676875,21.88815525,30.625678875,22.549656249999998L30.625678875,13.072656349999999C30.625678875,11.38515625,29.275674875,10.03515625,27.588174875,10.03515625L27.075177875,10.03515625L27.075177875,13.24815675C27.075177875,14.935656550000001,25.725173875,16.285657450000002,24.037676875000002,16.285657450000002L16.113174475,16.285657450000002C14.425674475000001,16.272157149999998,13.075673375000001,14.922158249999999,13.075673375000001,13.23465635L13.075673375000001,10.03515625L12.238672475,10.03515625C10.551171974999999,10.03515625,9.201171875,11.38515625,9.201171875,13.072656349999999L9.201171875,29.94765825C9.201171875,31.63515625,10.551171974999999,32.985161250000004,12.238672475,32.985161250000004L25.576673875,32.985161250000004C23.200674875,32.485662250000004,21.337675875000002,30.28515825,21.175671875,27.58515925Z" fill="url(#master_svg2_88_26531)" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M16.1124145625,14.764538762499999L24.0504169625,14.764538762499999C24.8874170625,14.764538762499999,25.5624140625,14.0895385625,25.5624140625,13.252537762500001L25.5624140625,10.0395388625L22.5249171625,10.0395388625C22.3629159625,8.8650390625,21.3369150625,7.8525390625,19.986915562500002,7.8525390625C18.7989153625,7.8525390625,17.7864150625,8.8650390625,17.6244149625,10.0395388625L14.5869140625,10.0395388625L14.5869140625,13.252537762500001C14.5869140625,14.0895385625,15.2619143725,14.764538762499999,16.1124145625,14.764538762499999ZM30.7869150625,24.3900370625C29.9499160625,23.3775360625,28.5999220625,22.7025380625,27.2499170625,22.7025380625L26.412916062500003,22.7025380625C25.8999180625,22.7025380625,25.5759160625,22.8780390625,25.0629190625,23.2155400625C24.0504169625,23.7285370625,23.1999158625,24.7275330625,22.700415562499998,25.9155390625C22.5384173625,26.4285390625,22.5384173625,26.9280380625,22.5384173625,27.4275380625L22.5384173625,27.5895390625C22.700415562499998,30.1275410625,24.7254170625,31.9770390625,27.1014200625,31.9770390625C28.4514180625,31.9770390625,29.8014230625,31.3020400625,30.6384180625,30.2895320625C31.3134210625,29.4525340625,31.6509170625,28.4265380625,31.6509170625,27.2520330625C31.8129160625,26.2395310625,31.2999250625,25.2270370625,30.7869150625,24.3900370625ZM29.7879200625,26.5770380625L27.0879160625,29.2770390625C26.7504160625,29.6145400625,26.412916062500003,29.6145400625,26.0754160625,29.2770390625L24.387915562499998,27.5895390625C24.0504169625,27.2520370625,24.0504169625,26.9145390625,24.387915562499998,26.5770380625C24.725415062499998,26.2395380625,25.0629150625,26.2395400625,25.4004160625,26.5770380625L26.2374170625,27.4140400625L26.5749150625,27.7515370625L28.7619150625,25.5645350625C29.0994140625,25.2270370625,29.4369190625,25.2270370625,29.774416062500002,25.5645350625C30.1119160625,25.9020370625,30.1119160625,26.2395380625,29.7879200625,26.5770380625Z" fill="url(#master_svg3_88_26531)" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></svg>
\ No newline at end of file
diff --git a/src/assets/images/chuchang.png b/src/assets/images/chuchang.png
new file mode 100644
index 0000000..81d3ee1
--- /dev/null
+++ b/src/assets/images/chuchang.png
Binary files differ
diff --git a/src/assets/images/dark.svg b/src/assets/images/dark.svg
new file mode 100644
index 0000000..36b58b5
--- /dev/null
+++ b/src/assets/images/dark.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+ <feMerge>
+ <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+ <feMergeNode in="SourceGraphic"></feMergeNode>
+ </feMerge>
+ </filter>
+ <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+ <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+ </filter>
+ </defs>
+ <g id="閰嶇疆闈㈡澘" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+ <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+ <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+ <mask id="mask-3" fill="white">
+ <use xlink:href="#path-2"></use>
+ </mask>
+ <g id="Rectangle-18">
+ <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+ <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+ </g>
+ <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+ <rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/src/assets/images/denglu.png b/src/assets/images/denglu.png
new file mode 100644
index 0000000..0a04a27
--- /dev/null
+++ b/src/assets/images/denglu.png
Binary files differ
diff --git a/src/assets/images/guocheng.png b/src/assets/images/guocheng.png
new file mode 100644
index 0000000..45d88df
--- /dev/null
+++ b/src/assets/images/guocheng.png
Binary files differ
diff --git a/src/assets/images/head.svg b/src/assets/images/head.svg
new file mode 100644
index 0000000..e49d541
--- /dev/null
+++ b/src/assets/images/head.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="452" height="285" viewBox="0 0 452 285"><defs><pattern x="0" y="80.0157699584961" width="452" height="204.98422241210938" patternUnits="userSpaceOnUse" id="master_svg0_143_34844"><image x="-0.11198217826978407" y="0" width="452.22396435653957" height="204.98422241210938" xlink:href="data:image/png;base64,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"/></pattern><pattern x="111.42742919921875" y="0" width="206.67991638183594" height="206.78233337402344" patternUnits="userSpaceOnUse" id="master_svg1_143_34837"><image x="-0.05120849609375" y="0" width="206.78233337402344" height="206.78233337402344" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAMgCAYAAADbcAZoAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAACAASURBVHic7N17nN1ldS/+z2c9371nkpAwyVxygWpQERWrVdSD1nNUilYuggZjJdTaXyNQ5CZarWJfnrHnVw6tRe4WIrRVC1gwKAjGFk6hN6UKre0RBbVCAXOZS+7JzOz9fdY6fzzfPbMnJJm990wyk8x6v155JZnZ+7uf/d179jzr+zxrLcA555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzE+B0D8A559zM0H3xc1URyWr/t5j+Jglw/K8LM9v7QWT/v1ZIpuMBUNXnfQ0AiDDu+GYGkjAzWN3xzQxUg4ik/+/xvfq/SULNlg1c071h4jPhnHPuQPIAxDnnHLo+0T9fdo9sh9QFA1r3K6IWANRN6PdqggCkFiygLgDBnkGIyWjQQRKqmgIQ4fMDi7oApDbcWrCCuuCldmzStmx8prMbdzE2d4acc85NFQ9AnHPuENB9/n8eSwlXAjhZVUHwQUA/2X/zi386VY+x8Pf/88jS7iOWI68YQ8lopgCw8calj0/VY0yHro/0Lw0ivwDAWmCiJl8ZuGbRb0332JxzbjbyAMQ552a4rgt+fhyVj1DYgWLlgCSi6VYRnjjwZy96crrHeCjo/tjAB6n4S9StoIhkZ276/MJ7p3tszjk3m3gA4pybFTpW/+BrGXEaRKAKTRt2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"/></pattern></defs><rect x="0" y="80.0157699584961" width="452" height="204.98422241210938" rx="0" fill="url(#master_svg0_143_34844)" fill-opacity="1"/><rect x="111.42742919921875" y="0" width="206.67991638183594" height="206.78233337402344" rx="0" fill="url(#master_svg1_143_34837)" fill-opacity="1"/><g><path d="M171.28799999,112L197.784,47.968002L213.431999,47.968002L239.928001,112L223.99200100000002,112L217.751999,97.216003L193.464001,97.216003L187.224001,112L171.28799999,112ZM205.656002,63.424L196.535999,85.792L214.776001,85.792L205.656002,63.424ZM246.840004,112L246.840004,47.968002L260.472,47.968002L260.472,112L246.840004,112Z" fill="#FFFFFF" fill-opacity="1"/><path d="M217.751999,97.216003L223.99200100000002,112L239.928001,112L239.514206,111L213.431999,47.968002L197.784002,47.968002L171.70179313,111L171.28799999,112L187.224003,112L193.464001,97.216003L217.751999,97.216003ZM186.560656,111L192.800652,96.216003L218.41535199999998,96.216003L224.655346,111L238.43197600000002,111L212.763565,48.968002L198.452438,48.968002L172.7840257,111L186.560656,111ZM246.840012,111L246.840012,112L260.472008,112L260.472008,47.968002L246.840012,47.968002L246.840012,111ZM247.840012,111L259.472008,111L259.472008,48.968002L247.840012,48.968002L247.840012,111ZM206.581993,63.046455L204.730011,63.046455L195.048351,86.79200399999999L216.263653,86.79200399999999L206.581993,63.046455ZM205.116039,64.748333L205.656002,63.424004L206.195965,64.748333L214.368279,84.79200399999999L214.776001,85.79200399999999L196.536001,85.79200399999999L196.943726,84.79200399999999L205.116039,64.748333Z" fill-rule="evenodd" fill="#E2E6F7" fill-opacity="1"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/images/icon 3.png b/src/assets/images/icon 3.png
new file mode 100644
index 0000000..08c335b
--- /dev/null
+++ b/src/assets/images/icon 3.png
Binary files differ
diff --git a/src/assets/images/icon1.png b/src/assets/images/icon1.png
new file mode 100644
index 0000000..adc9abc
--- /dev/null
+++ b/src/assets/images/icon1.png
Binary files differ
diff --git a/src/assets/images/icon2.png b/src/assets/images/icon2.png
new file mode 100644
index 0000000..a736c6c
--- /dev/null
+++ b/src/assets/images/icon2.png
Binary files differ
diff --git a/src/assets/images/khtitle.png b/src/assets/images/khtitle.png
new file mode 100644
index 0000000..81cb4bf
--- /dev/null
+++ b/src/assets/images/khtitle.png
Binary files differ
diff --git a/src/assets/images/kucun.png b/src/assets/images/kucun.png
new file mode 100644
index 0000000..d89d059
--- /dev/null
+++ b/src/assets/images/kucun.png
Binary files differ
diff --git a/src/assets/images/light.svg b/src/assets/images/light.svg
new file mode 100644
index 0000000..efd52c6
--- /dev/null
+++ b/src/assets/images/light.svg
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+ <feMerge>
+ <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+ <feMergeNode in="SourceGraphic"></feMergeNode>
+ </feMerge>
+ </filter>
+ <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+ <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+ <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+ <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+ <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+ </filter>
+ </defs>
+ <g id="閰嶇疆闈㈡澘" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
+ <g id="Group-8" transform="translate(1167.000000, 0.000000)">
+ <g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
+ <mask id="mask-3" fill="white">
+ <use xlink:href="#path-2"></use>
+ </mask>
+ <g id="Rectangle-18">
+ <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+ <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+ </g>
+ <rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+ <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+ </g>
+ </g>
+ </g>
+ </g>
+</svg>
\ No newline at end of file
diff --git a/src/assets/images/login-background.png b/src/assets/images/login-background.png
new file mode 100644
index 0000000..ace9d53
--- /dev/null
+++ b/src/assets/images/login-background.png
Binary files differ
diff --git a/src/assets/images/pay.png b/src/assets/images/pay.png
new file mode 100644
index 0000000..bb8b967
--- /dev/null
+++ b/src/assets/images/pay.png
Binary files differ
diff --git a/src/assets/images/profile.jpg b/src/assets/images/profile.jpg
new file mode 100644
index 0000000..b3a940b
--- /dev/null
+++ b/src/assets/images/profile.jpg
Binary files differ
diff --git a/src/assets/images/video.png b/src/assets/images/video.png
new file mode 100644
index 0000000..7a90175
--- /dev/null
+++ b/src/assets/images/video.png
Binary files differ
diff --git a/src/assets/images/xioashoushuju.png b/src/assets/images/xioashoushuju.png
new file mode 100644
index 0000000..a7f1588
--- /dev/null
+++ b/src/assets/images/xioashoushuju.png
Binary files differ
diff --git a/src/assets/images/yuancailiao.png b/src/assets/images/yuancailiao.png
new file mode 100644
index 0000000..6ec429f
--- /dev/null
+++ b/src/assets/images/yuancailiao.png
Binary files differ
diff --git a/src/assets/img/emoji/clown-face.png b/src/assets/img/emoji/clown-face.png
new file mode 100644
index 0000000..fd77228
--- /dev/null
+++ b/src/assets/img/emoji/clown-face.png
Binary files differ
diff --git a/src/assets/img/emoji/face-screaming-in-fear.png b/src/assets/img/emoji/face-screaming-in-fear.png
new file mode 100644
index 0000000..d6332ce
--- /dev/null
+++ b/src/assets/img/emoji/face-screaming-in-fear.png
Binary files differ
diff --git a/src/assets/img/emoji/face-vomiting.png b/src/assets/img/emoji/face-vomiting.png
new file mode 100644
index 0000000..dcc556f
--- /dev/null
+++ b/src/assets/img/emoji/face-vomiting.png
Binary files differ
diff --git a/src/assets/img/emoji/face-with-tongue.png b/src/assets/img/emoji/face-with-tongue.png
new file mode 100644
index 0000000..b2e228e
--- /dev/null
+++ b/src/assets/img/emoji/face-with-tongue.png
Binary files differ
diff --git a/src/assets/img/emoji/face-without-mouth.png b/src/assets/img/emoji/face-without-mouth.png
new file mode 100644
index 0000000..e48a05a
--- /dev/null
+++ b/src/assets/img/emoji/face-without-mouth.png
Binary files differ
diff --git a/src/assets/img/emoji/ghost.png b/src/assets/img/emoji/ghost.png
new file mode 100644
index 0000000..8a4b03a
--- /dev/null
+++ b/src/assets/img/emoji/ghost.png
Binary files differ
diff --git a/src/assets/img/emoji/hibiscus.png b/src/assets/img/emoji/hibiscus.png
new file mode 100644
index 0000000..5fddeb5
--- /dev/null
+++ b/src/assets/img/emoji/hibiscus.png
Binary files differ
diff --git a/src/assets/img/emoji/jack-o-lantern.png b/src/assets/img/emoji/jack-o-lantern.png
new file mode 100644
index 0000000..7014639
--- /dev/null
+++ b/src/assets/img/emoji/jack-o-lantern.png
Binary files differ
diff --git a/src/assets/img/emoji/lips.png b/src/assets/img/emoji/lips.png
new file mode 100644
index 0000000..40915bd
--- /dev/null
+++ b/src/assets/img/emoji/lips.png
Binary files differ
diff --git a/src/assets/img/emoji/loudly-crying-face.png b/src/assets/img/emoji/loudly-crying-face.png
new file mode 100644
index 0000000..d72008d
--- /dev/null
+++ b/src/assets/img/emoji/loudly-crying-face.png
Binary files differ
diff --git a/src/assets/img/emoji/money-bag.png b/src/assets/img/emoji/money-bag.png
new file mode 100644
index 0000000..df46b05
--- /dev/null
+++ b/src/assets/img/emoji/money-bag.png
Binary files differ
diff --git a/src/assets/img/emoji/money-mouth-face.png b/src/assets/img/emoji/money-mouth-face.png
new file mode 100644
index 0000000..f7c4cdf
--- /dev/null
+++ b/src/assets/img/emoji/money-mouth-face.png
Binary files differ
diff --git a/src/assets/img/emoji/new-moon-face.png b/src/assets/img/emoji/new-moon-face.png
new file mode 100644
index 0000000..8942b8b
--- /dev/null
+++ b/src/assets/img/emoji/new-moon-face.png
Binary files differ
diff --git a/src/assets/img/emoji/ok-hand-yellow.png b/src/assets/img/emoji/ok-hand-yellow.png
new file mode 100644
index 0000000..4dbd427
--- /dev/null
+++ b/src/assets/img/emoji/ok-hand-yellow.png
Binary files differ
diff --git a/src/assets/img/emoji/pile-of-poo.png b/src/assets/img/emoji/pile-of-poo.png
new file mode 100644
index 0000000..28f149a
--- /dev/null
+++ b/src/assets/img/emoji/pile-of-poo.png
Binary files differ
diff --git a/src/assets/img/emoji/pouting-face.png b/src/assets/img/emoji/pouting-face.png
new file mode 100644
index 0000000..0265aa2
--- /dev/null
+++ b/src/assets/img/emoji/pouting-face.png
Binary files differ
diff --git a/src/assets/img/emoji/rainbow.png b/src/assets/img/emoji/rainbow.png
new file mode 100644
index 0000000..7794ded
--- /dev/null
+++ b/src/assets/img/emoji/rainbow.png
Binary files differ
diff --git a/src/assets/img/emoji/rocket.png b/src/assets/img/emoji/rocket.png
new file mode 100644
index 0000000..04a7619
--- /dev/null
+++ b/src/assets/img/emoji/rocket.png
Binary files differ
diff --git a/src/assets/img/emoji/shamrock.png b/src/assets/img/emoji/shamrock.png
new file mode 100644
index 0000000..8fe0836
--- /dev/null
+++ b/src/assets/img/emoji/shamrock.png
Binary files differ
diff --git a/src/assets/img/emoji/shangchuan.png b/src/assets/img/emoji/shangchuan.png
new file mode 100644
index 0000000..af16fb4
--- /dev/null
+++ b/src/assets/img/emoji/shangchuan.png
Binary files differ
diff --git a/src/assets/img/emoji/slightly-smiling-face.png b/src/assets/img/emoji/slightly-smiling-face.png
new file mode 100644
index 0000000..be7dd9f
--- /dev/null
+++ b/src/assets/img/emoji/slightly-smiling-face.png
Binary files differ
diff --git a/src/assets/img/emoji/smiling-face-with-heart-eyes.png b/src/assets/img/emoji/smiling-face-with-heart-eyes.png
new file mode 100644
index 0000000..c838d4f
--- /dev/null
+++ b/src/assets/img/emoji/smiling-face-with-heart-eyes.png
Binary files differ
diff --git a/src/assets/img/emoji/smiling-face-with-horns.png b/src/assets/img/emoji/smiling-face-with-horns.png
new file mode 100644
index 0000000..b79738c
--- /dev/null
+++ b/src/assets/img/emoji/smiling-face-with-horns.png
Binary files differ
diff --git a/src/assets/img/emoji/smiling-face-with-sunglasses.png b/src/assets/img/emoji/smiling-face-with-sunglasses.png
new file mode 100644
index 0000000..58b8604
--- /dev/null
+++ b/src/assets/img/emoji/smiling-face-with-sunglasses.png
Binary files differ
diff --git a/src/assets/img/emoji/smiling-face.png b/src/assets/img/emoji/smiling-face.png
new file mode 100644
index 0000000..3c2915b
--- /dev/null
+++ b/src/assets/img/emoji/smiling-face.png
Binary files differ
diff --git a/src/assets/img/emoji/sparkles.png b/src/assets/img/emoji/sparkles.png
new file mode 100644
index 0000000..4d11a1b
--- /dev/null
+++ b/src/assets/img/emoji/sparkles.png
Binary files differ
diff --git a/src/assets/img/emoji/star.png b/src/assets/img/emoji/star.png
new file mode 100644
index 0000000..44f80bb
--- /dev/null
+++ b/src/assets/img/emoji/star.png
Binary files differ
diff --git a/src/assets/img/emoji/thinking-face.png b/src/assets/img/emoji/thinking-face.png
new file mode 100644
index 0000000..33d791d
--- /dev/null
+++ b/src/assets/img/emoji/thinking-face.png
Binary files differ
diff --git a/src/assets/img/emoji/thought-balloon.png b/src/assets/img/emoji/thought-balloon.png
new file mode 100644
index 0000000..892eedc
--- /dev/null
+++ b/src/assets/img/emoji/thought-balloon.png
Binary files differ
diff --git a/src/assets/img/emoji/thumbs-up-yellow.png b/src/assets/img/emoji/thumbs-up-yellow.png
new file mode 100644
index 0000000..c30b6ae
--- /dev/null
+++ b/src/assets/img/emoji/thumbs-up-yellow.png
Binary files differ
diff --git a/src/assets/img/emoji/tired-face.png b/src/assets/img/emoji/tired-face.png
new file mode 100644
index 0000000..d1deba8
--- /dev/null
+++ b/src/assets/img/emoji/tired-face.png
Binary files differ
diff --git a/src/assets/img/emoji/two-hearts.png b/src/assets/img/emoji/two-hearts.png
new file mode 100644
index 0000000..4259b46
--- /dev/null
+++ b/src/assets/img/emoji/two-hearts.png
Binary files differ
diff --git a/src/assets/img/emoji/victory-hand-yellow.png b/src/assets/img/emoji/victory-hand-yellow.png
new file mode 100644
index 0000000..fc46ce2
--- /dev/null
+++ b/src/assets/img/emoji/victory-hand-yellow.png
Binary files differ
diff --git "a/src/assets/img/emoji/\345\217\226\346\266\210.png" "b/src/assets/img/emoji/\345\217\226\346\266\210.png"
new file mode 100644
index 0000000..30df0a9
--- /dev/null
+++ "b/src/assets/img/emoji/\345\217\226\346\266\210.png"
Binary files differ
diff --git a/src/assets/img/fileImg/excel.png b/src/assets/img/fileImg/excel.png
new file mode 100644
index 0000000..ae8929c
--- /dev/null
+++ b/src/assets/img/fileImg/excel.png
Binary files differ
diff --git a/src/assets/img/fileImg/pdf.png b/src/assets/img/fileImg/pdf.png
new file mode 100644
index 0000000..3653045
--- /dev/null
+++ b/src/assets/img/fileImg/pdf.png
Binary files differ
diff --git a/src/assets/img/fileImg/ppt.png b/src/assets/img/fileImg/ppt.png
new file mode 100644
index 0000000..2362d61
--- /dev/null
+++ b/src/assets/img/fileImg/ppt.png
Binary files differ
diff --git a/src/assets/img/fileImg/txt.png b/src/assets/img/fileImg/txt.png
new file mode 100644
index 0000000..0ce91dc
--- /dev/null
+++ b/src/assets/img/fileImg/txt.png
Binary files differ
diff --git a/src/assets/img/fileImg/unknowfile.png b/src/assets/img/fileImg/unknowfile.png
new file mode 100644
index 0000000..75e22fb
--- /dev/null
+++ b/src/assets/img/fileImg/unknowfile.png
Binary files differ
diff --git a/src/assets/img/fileImg/word.png b/src/assets/img/fileImg/word.png
new file mode 100644
index 0000000..97e8a69
--- /dev/null
+++ b/src/assets/img/fileImg/word.png
Binary files differ
diff --git a/src/assets/img/fileImg/zpi.png b/src/assets/img/fileImg/zpi.png
new file mode 100644
index 0000000..de3c681
--- /dev/null
+++ b/src/assets/img/fileImg/zpi.png
Binary files differ
diff --git a/src/assets/img/head_portrait.jpg b/src/assets/img/head_portrait.jpg
new file mode 100644
index 0000000..dd345b3
--- /dev/null
+++ b/src/assets/img/head_portrait.jpg
Binary files differ
diff --git a/src/assets/img/head_portrait1.png b/src/assets/img/head_portrait1.png
new file mode 100644
index 0000000..3134bd5
--- /dev/null
+++ b/src/assets/img/head_portrait1.png
Binary files differ
diff --git a/src/assets/img/logo.png b/src/assets/img/logo.png
new file mode 100644
index 0000000..b53ef8a
--- /dev/null
+++ b/src/assets/img/logo.png
Binary files differ
diff --git a/src/assets/indexViews/HYSNLogo.png b/src/assets/indexViews/HYSNLogo.png
new file mode 100644
index 0000000..70148cc
--- /dev/null
+++ b/src/assets/indexViews/HYSNLogo.png
Binary files differ
diff --git a/src/assets/indexViews/LCLogo.png b/src/assets/indexViews/LCLogo.png
new file mode 100644
index 0000000..d18f9fd
--- /dev/null
+++ b/src/assets/indexViews/LCLogo.png
Binary files differ
diff --git a/src/assets/indexViews/login-background.png b/src/assets/indexViews/login-background.png
new file mode 100644
index 0000000..ace9d53
--- /dev/null
+++ b/src/assets/indexViews/login-background.png
Binary files differ
diff --git a/src/assets/logo/XDRJ.png b/src/assets/logo/XDRJ.png
new file mode 100644
index 0000000..5b49e96
--- /dev/null
+++ b/src/assets/logo/XDRJ.png
Binary files differ
diff --git a/src/assets/logo/logo.png b/src/assets/logo/logo.png
new file mode 100644
index 0000000..a5831b8
--- /dev/null
+++ b/src/assets/logo/logo.png
Binary files differ
diff --git "a/src/assets/logo/\344\270\212\346\265\267\351\203\242\346\230\261\347\275\221\347\273\234\347\247\221\346\212\200\346\234\211\351\231\220\345\205\254\345\217\270.png" "b/src/assets/logo/\344\270\212\346\265\267\351\203\242\346\230\261\347\275\221\347\273\234\347\247\221\346\212\200\346\234\211\351\231\220\345\205\254\345\217\270.png"
new file mode 100644
index 0000000..f469ba7
--- /dev/null
+++ "b/src/assets/logo/\344\270\212\346\265\267\351\203\242\346\230\261\347\275\221\347\273\234\347\247\221\346\212\200\346\234\211\351\231\220\345\205\254\345\217\270.png"
Binary files differ
diff --git "a/src/assets/logo/\345\215\227\351\200\232\344\272\221\344\273\216\345\267\245\344\270\232\344\272\222\350\201\224\347\275\221\346\234\211\351\231\220\345\205\254\345\217\270.png" "b/src/assets/logo/\345\215\227\351\200\232\344\272\221\344\273\216\345\267\245\344\270\232\344\272\222\350\201\224\347\275\221\346\234\211\351\231\220\345\205\254\345\217\270.png"
new file mode 100644
index 0000000..d7ecf59
--- /dev/null
+++ "b/src/assets/logo/\345\215\227\351\200\232\344\272\221\344\273\216\345\267\245\344\270\232\344\272\222\350\201\224\347\275\221\346\234\211\351\231\220\345\205\254\345\217\270.png"
Binary files differ
diff --git "a/src/assets/logo/\346\225\246\347\205\214\351\274\216\350\257\232.png" "b/src/assets/logo/\346\225\246\347\205\214\351\274\216\350\257\232.png"
new file mode 100644
index 0000000..139bdd1
--- /dev/null
+++ "b/src/assets/logo/\346\225\246\347\205\214\351\274\216\350\257\232.png"
Binary files differ
diff --git "a/src/assets/logo/\346\226\260\347\274\206\357\274\210\346\261\237\350\213\217\357\274\211\346\225\260\345\255\227\347\247\221\346\212\200\346\234\211\351\231\220\345\205\254\345\217\270.png" "b/src/assets/logo/\346\226\260\347\274\206\357\274\210\346\261\237\350\213\217\357\274\211\346\225\260\345\255\227\347\247\221\346\212\200\346\234\211\351\231\220\345\205\254\345\217\270.png"
new file mode 100644
index 0000000..4481de2
--- /dev/null
+++ "b/src/assets/logo/\346\226\260\347\274\206\357\274\210\346\261\237\350\213\217\357\274\211\346\225\260\345\255\227\347\247\221\346\212\200\346\234\211\351\231\220\345\205\254\345\217\270.png"
Binary files differ
diff --git a/src/assets/styles/btn.scss b/src/assets/styles/btn.scss
new file mode 100644
index 0000000..46a41f3
--- /dev/null
+++ b/src/assets/styles/btn.scss
@@ -0,0 +1,99 @@
+@import './variables.module.scss';
+
+@mixin colorBtn($color) {
+ background: $color;
+
+ &:hover {
+ color: $color;
+
+ &:before,
+ &:after {
+ background: $color;
+ }
+ }
+}
+
+.blue-btn {
+ @include colorBtn($blue)
+}
+
+.light-blue-btn {
+ @include colorBtn($light-blue)
+}
+
+.red-btn {
+ @include colorBtn($red)
+}
+
+.pink-btn {
+ @include colorBtn($pink)
+}
+
+.green-btn {
+ @include colorBtn($green)
+}
+
+.tiffany-btn {
+ @include colorBtn($tiffany)
+}
+
+.yellow-btn {
+ @include colorBtn($yellow)
+}
+
+.pan-btn {
+ font-size: 14px;
+ color: #fff;
+ padding: 14px 36px;
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ transition: 600ms ease all;
+ position: relative;
+ display: inline-block;
+
+ &:hover {
+ background: #fff;
+
+ &:before,
+ &:after {
+ width: 100%;
+ transition: 600ms ease all;
+ }
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 2px;
+ width: 0;
+ transition: 400ms ease all;
+ }
+
+ &::after {
+ right: inherit;
+ top: inherit;
+ left: 0;
+ bottom: 0;
+ }
+}
+
+.custom-button {
+ display: inline-block;
+ line-height: 1;
+ white-space: nowrap;
+ cursor: pointer;
+ background: #fff;
+ color: #fff;
+ -webkit-appearance: none;
+ text-align: center;
+ box-sizing: border-box;
+ outline: 0;
+ margin: 0;
+ padding: 10px 15px;
+ font-size: 14px;
+ border-radius: 4px;
+}
diff --git a/src/assets/styles/element-ui.scss b/src/assets/styles/element-ui.scss
new file mode 100644
index 0000000..75c83ab
--- /dev/null
+++ b/src/assets/styles/element-ui.scss
@@ -0,0 +1,261 @@
+// cover some element-ui styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+ font-weight: 400 !important;
+}
+
+.el-upload {
+ input[type="file"] {
+ display: none !important;
+ }
+}
+
+.el-upload__input {
+ display: none;
+}
+
+.cell {
+ .el-tag {
+ margin-right: 0px;
+ }
+}
+
+.small-padding {
+ .cell {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
+.fixed-width {
+ .el-button--mini {
+ padding: 7px 10px;
+ width: 60px;
+ }
+}
+
+.status-col {
+ .cell {
+ padding: 0 10px;
+ text-align: center;
+
+ .el-tag {
+ margin-right: 0px;
+ }
+ }
+}
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+ transform: none;
+ left: 0;
+ position: relative;
+ margin: 0 auto;
+ border-radius: 16px;
+ padding: 0 !important;
+ border: 1px solid var(--surface-border);
+ box-shadow: var(--shadow-md);
+ background: rgba(255, 255, 255, 0.95);
+}
+.el-dialog__header {
+ background: linear-gradient(180deg, rgba(248, 251, 255, 1), rgba(242, 247, 255, 0.98));
+ padding: 18px 24px 14px;
+ border-bottom: 1px solid var(--surface-border);
+ border-radius: 14px 14px 0 0;
+}
+.el-dialog__title {
+ font-weight: 600;
+ font-size: 17px;
+ color: var(--text-primary);
+}
+.el-dialog__body {
+ padding: 24px 24px 0;
+ max-height: 74vh;
+ overflow-y: auto;
+}
+.el-dialog__footer {
+ text-align: center;
+ padding: 18px 24px 24px;
+}
+.el-message-box {
+ padding: 0 !important;
+ border-radius: 16px;
+ border: 1px solid var(--surface-border);
+ box-shadow: var(--shadow-md);
+ background: rgba(255, 255, 255, 0.96);
+}
+.el-message-box__header {
+ background: linear-gradient(180deg, rgba(248, 251, 255, 1), rgba(242, 247, 255, 0.98));
+ padding: 18px 24px 14px;
+ border-bottom: 1px solid var(--surface-border);
+ border-radius: 14px 14px 0 0;
+}
+.el-message-box__title {
+ font-weight: 600;
+ font-size: 17px;
+ color: var(--text-primary);
+}
+.el-message-box__content {
+ padding: 24px 24px 0;
+}
+.el-message-box__container {
+ justify-content: center;
+}
+.el-message-box__btns {
+ text-align: center;
+ padding: 16px;
+ display: flex;
+ flex-direction: row-reverse;
+ justify-content: center;
+ align-items: center;
+ .el-button--primary {
+ margin-right: 12px;
+ }
+}
+.el-table__expanded-cell {
+ padding: 0 !important;
+ .el-table__header-wrapper {
+ background-color: var(--surface-soft) !important;
+ }
+}
+
+// refine element ui upload
+.upload-container {
+ .el-upload {
+ width: 100%;
+
+ .el-upload-dragger {
+ width: 100%;
+ height: 200px;
+ }
+ }
+}
+
+// dropdown
+.el-dropdown-menu {
+ a {
+ display: block;
+ }
+}
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+ display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+ box-sizing: content-box;
+}
+
+.el-menu--collapse
+ > div
+ > .el-submenu
+ > .el-submenu__title
+ .el-submenu__icon-arrow {
+ display: none;
+}
+
+.el-dropdown .el-dropdown-link {
+ color: var(--el-color-primary) !important;
+}
+
+.el-button {
+ border-radius: 8px;
+ font-weight: 600;
+ box-shadow: none !important;
+}
+
+.el-button--primary {
+ --el-button-bg-color: var(--el-color-primary);
+ --el-button-border-color: var(--el-color-primary);
+ --el-button-hover-bg-color: var(--el-color-primary-light-3);
+ --el-button-hover-border-color: var(--el-color-primary-light-3);
+ --el-button-active-bg-color: var(--el-color-primary-dark-2);
+ --el-button-active-border-color: var(--el-color-primary-dark-2);
+}
+
+.el-input__wrapper,
+.el-textarea__inner,
+.el-select__wrapper,
+.el-date-editor.el-input__wrapper,
+.el-date-editor .el-input__wrapper {
+ border-radius: 10px;
+ box-shadow: 0 0 0 1px rgba(148, 163, 184, 0.28) inset !important;
+ background: rgba(255, 255, 255, 0.92);
+ color: var(--text-primary);
+}
+
+.el-input__wrapper.is-focus,
+.el-select__wrapper.is-focused,
+.el-textarea__inner:focus {
+ box-shadow: 0 0 0 1px rgba(37, 99, 235, 0.56) inset !important;
+}
+
+.el-card {
+ border: 1px solid var(--surface-border);
+ box-shadow: var(--shadow-sm);
+ background: var(--panel-mask);
+}
+
+.el-table {
+ --el-table-border-color: var(--surface-border);
+ --el-table-header-bg-color: #f2f7ff;
+ --el-table-row-hover-bg-color: #f8fbff;
+ --el-table-current-row-bg-color: #edf4ff;
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.94) !important;
+}
+
+.el-table th.el-table__cell {
+ background: #f2f7ff !important;
+ color: #3b4f6c;
+ font-weight: 600;
+}
+
+.el-table tr,
+.el-table td.el-table__cell,
+.el-table__body tr > td.el-table__cell {
+ background: rgba(255, 255, 255, 0.92) !important;
+ color: var(--text-secondary);
+}
+
+.el-table .el-table__body tr:hover > td.el-table__cell {
+ background: var(--el-table-row-hover-bg-color) !important;
+}
+
+.el-table .el-table__body tr.current-row > td.el-table__cell {
+ background: var(--el-table-current-row-bg-color) !important;
+}
+
+.el-table .el-table__footer-wrapper {
+ border-top: 1px solid var(--surface-border);
+}
+
+.el-table .el-table__footer-wrapper tbody td.el-table__cell,
+.el-table .el-table__footer-wrapper tfoot td.el-table__cell {
+ background: var(--surface-base) !important;
+ border-top: 1px solid var(--surface-border);
+ font-weight: 600;
+}
+
+.el-pagination {
+ margin-top: 18px;
+}
+
+.el-empty__description p,
+.el-form-item__label,
+.el-radio-button__inner,
+.el-checkbox__label,
+.el-tabs__item,
+.el-select-dropdown__item,
+.el-dropdown-menu__item {
+ color: var(--text-secondary);
+}
+
+.el-date-editor .el-range-input,
+.el-input__inner,
+.el-textarea__inner {
+ color: var(--text-primary);
+}
diff --git a/src/assets/styles/index.scss b/src/assets/styles/index.scss
new file mode 100644
index 0000000..39d03cf
--- /dev/null
+++ b/src/assets/styles/index.scss
@@ -0,0 +1,225 @@
+@import './variables.module.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+@import './btn.scss';
+@import './ruoyi.scss';
+
+body {
+ height: 100%;
+ margin: 0;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
+ background:
+ radial-gradient(circle at 9% -6%, rgba(59, 130, 246, 0.14), transparent 36%),
+ radial-gradient(circle at 88% -8%, rgba(56, 189, 248, 0.12), transparent 30%),
+ linear-gradient(165deg, #f3f7fc 0%, #eef5ff 54%, #f8fbff 100%);
+ color: var(--text-primary);
+}
+
+label {
+ font-weight: 600;
+ color: var(--text-secondary);
+}
+
+html {
+ height: 100%;
+ box-sizing: border-box;
+}
+
+#app {
+ height: 100%;
+}
+
+html,
+body,
+#app {
+ background-color: var(--app-bg);
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+.no-padding {
+ padding: 0px !important;
+}
+
+.padding-content {
+ padding: 4px 0;
+}
+
+a:focus,
+a:active {
+ outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+ cursor: pointer;
+ color: inherit;
+ text-decoration: none;
+}
+
+div:focus {
+ outline: none;
+}
+
+.fr {
+ float: right;
+}
+
+.fl {
+ float: left;
+}
+
+.pr-5 {
+ padding-right: 5px;
+}
+
+.pl-5 {
+ padding-left: 5px;
+}
+
+.block {
+ display: block;
+}
+
+.pointer {
+ cursor: pointer;
+}
+
+.inlineBlock {
+ display: block;
+}
+
+.clearfix {
+ &:after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+}
+
+aside {
+ background: rgba(255, 255, 255, 0.84);
+ padding: 8px 24px;
+ margin-bottom: 20px;
+ border-radius: 12px;
+ border: 1px solid var(--surface-border);
+ display: block;
+ line-height: 32px;
+ font-size: 16px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ color: var(--text-secondary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ a {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+ }
+}
+
+//main-container鍏ㄥ眬鏍峰紡
+.app-container {
+ padding: 20px 24px 24px;
+}
+.search_form {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ .search_title {
+ font-size: 14px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ color: var(--text-secondary);
+ }
+}
+.table_list {
+ background: var(--panel-mask);
+ border: 1px solid var(--surface-border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-sm);
+ backdrop-filter: blur(12px);
+ padding: 18px;
+}
+.components-container {
+ margin: 30px 50px;
+ position: relative;
+}
+
+.text-center {
+ text-align: center
+}
+
+.sub-navbar {
+ height: 50px;
+ line-height: 50px;
+ position: relative;
+ width: 100%;
+ text-align: right;
+ padding-right: 20px;
+ transition: 600ms ease position;
+ background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+ .subtitle {
+ font-size: 20px;
+ color: #fff;
+ }
+
+ &.draft {
+ background: #d0d0d0;
+ }
+
+ &.deleted {
+ background: #d0d0d0;
+ }
+}
+
+.link-type,
+.link-type:focus {
+ color: var(--accent-light);
+ cursor: pointer;
+
+ &:hover {
+ color: #2563eb;
+ }
+}
+
+.filter-container {
+ padding-bottom: 10px;
+
+ .filter-item {
+ display: inline-block;
+ vertical-align: middle;
+ margin-bottom: 10px;
+ }
+}
+
+.app-container,
+.table_list,
+.components-container {
+ .el-card,
+ .el-dialog,
+ .el-drawer,
+ .el-table,
+ .el-descriptions,
+ .el-collapse-item__wrap,
+ .el-tabs__content {
+ border-radius: var(--radius-md);
+ }
+}
diff --git a/src/assets/styles/mixin.scss b/src/assets/styles/mixin.scss
new file mode 100644
index 0000000..64d9cf6
--- /dev/null
+++ b/src/assets/styles/mixin.scss
@@ -0,0 +1,66 @@
+@mixin clearfix {
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+}
+
+@mixin scrollBar {
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+}
+
+@mixin relative {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+@mixin pct($pct) {
+ width: #{$pct};
+ position: relative;
+ margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+ $width: $width/2;
+ $color-border-style: $height solid $color;
+ $transparent-border-style: $width solid transparent;
+ height: 0;
+ width: 0;
+
+ @if $direction==up {
+ border-bottom: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==right {
+ border-left: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+
+ @else if $direction==down {
+ border-top: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==left {
+ border-right: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+}
diff --git a/src/assets/styles/ruoyi.scss b/src/assets/styles/ruoyi.scss
new file mode 100644
index 0000000..d20ba26
--- /dev/null
+++ b/src/assets/styles/ruoyi.scss
@@ -0,0 +1,289 @@
+/**
+ * 閫氱敤css鏍峰紡甯冨眬澶勭悊
+ * Copyright (c) 2019 ruoyi
+ */
+
+/** 鍩虹閫氱敤 **/
+.pt5 {
+ padding-top: 5px;
+}
+.pr5 {
+ padding-right: 5px;
+}
+.pb5 {
+ padding-bottom: 5px;
+}
+.mt5 {
+ margin-top: 5px;
+}
+.mr5 {
+ margin-right: 5px;
+}
+.mb5 {
+ margin-bottom: 5px;
+}
+.mb8 {
+ margin-bottom: 8px;
+}
+.ml5 {
+ margin-left: 5px;
+}
+.mt10 {
+ margin-top: 10px;
+}
+.mr10 {
+ margin-right: 10px;
+}
+.mb10 {
+ margin-bottom: 10px;
+}
+.ml10 {
+ margin-left: 10px;
+}
+.mt20 {
+ margin-top: 20px;
+}
+.mr20 {
+ margin-right: 20px;
+}
+.mb20 {
+ margin-bottom: 20px;
+}
+.ml20 {
+ margin-left: 20px;
+}
+
+.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
+ font-family: inherit;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit;
+}
+
+.el-form .el-form-item__label {
+ font-weight: 700;
+}
+.el-dialog:not(.is-fullscreen) {
+ margin-top: 6vh !important;
+}
+
+.el-dialog.scrollbar .el-dialog__body {
+ overflow: auto;
+ overflow-x: hidden;
+ max-height: 70vh;
+}
+
+.el-table {
+ .el-table__header-wrapper, .el-table__fixed-header-wrapper {
+ th {
+ word-break: break-word;
+ background-color: #F0F1F5 !important;
+ color: #515a6e;
+ height: 40px !important;
+ font-size: 13px;
+ }
+ }
+ .el-table__body-wrapper {
+ .el-button [class*="el-icon-"] + span {
+ margin-left: 1px;
+ }
+ }
+}
+
+/** 琛ㄥ崟甯冨眬 **/
+.form-header {
+ font-size:15px;
+ color:#6379bb;
+ border-bottom:1px solid #ddd;
+ margin:8px 10px 25px 10px;
+ padding-bottom:5px
+}
+
+/** 琛ㄦ牸甯冨眬 **/
+.pagination-container {
+ display: flex;
+ justify-content: flex-end;
+ margin-top: 20px;
+ background-color: transparent !important;
+}
+
+/* 寮圭獥涓殑鍒嗛〉鍣� */
+.el-dialog .pagination-container {
+ position: static !important;
+ margin: 10px 0 0 0;
+ padding: 0 !important;
+
+ .el-pagination {
+ position: static;
+ }
+}
+
+/* 绉诲姩绔�傞厤 */
+@media (max-width: 768px) {
+ .pagination-container {
+ .el-pagination {
+ > .el-pagination__jump {
+ display: none !important;
+ }
+ > .el-pagination__sizes {
+ display: none !important;
+ }
+ }
+ }
+}
+
+/* tree border */
+.tree-border {
+ margin-top: 5px;
+ border: 1px solid var(--el-border-color-light, #e5e6e7);
+ background: var(--el-bg-color, #FFFFFF) none;
+ border-radius:4px;
+ width: 100%;
+}
+
+.el-table .fixed-width .el-button--small {
+ padding-left: 0;
+ padding-right: 0;
+ width: inherit;
+}
+
+/** 琛ㄦ牸鏇村鎿嶄綔涓嬫媺鏍峰紡 */
+.el-table .el-dropdown-link {
+ cursor: pointer;
+ color: #2C51D9;
+ margin-left: 10px;
+}
+
+.el-table .el-dropdown, .el-icon-arrow-down {
+ font-size: 12px;
+}
+
+.el-tree-node__content > .el-checkbox {
+ margin-right: 8px;
+}
+
+.list-group-striped > .list-group-item {
+ border-left: 0;
+ border-right: 0;
+ border-radius: 0;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.list-group {
+ padding-left: 0px;
+ list-style: none;
+}
+
+.list-group-item {
+ border-bottom: 1px solid #e7eaec;
+ border-top: 1px solid #e7eaec;
+ margin-bottom: -1px;
+ padding: 11px 0px;
+ font-size: 13px;
+}
+
+.pull-right {
+ float: right !important;
+}
+
+.el-card__header {
+ padding: 14px 15px 7px !important;
+ min-height: 40px;
+}
+
+.el-card__body {
+ padding: 15px 20px 20px 20px !important;
+}
+
+.card-box {
+ margin-bottom: 10px;
+}
+
+/* button color */
+.el-button--cyan.is-active,
+.el-button--cyan:active {
+ background: #20B2AA;
+ border-color: #20B2AA;
+ color: #FFFFFF;
+}
+
+.el-button--cyan:focus,
+.el-button--cyan:hover {
+ background: #48D1CC;
+ border-color: #48D1CC;
+ color: #FFFFFF;
+}
+
+.el-button--cyan {
+ background-color: #20B2AA;
+ border-color: #20B2AA;
+ color: #FFFFFF;
+}
+
+/* text color */
+.text-navy {
+ color: #1ab394;
+}
+
+.text-primary {
+ color: inherit;
+}
+
+.text-success {
+ color: #1c84c6;
+}
+
+.text-info {
+ color: #23c6c8;
+}
+
+.text-warning {
+ color: #f8ac59;
+}
+
+.text-danger {
+ color: #ed5565;
+}
+
+.text-muted {
+ color: #888888;
+}
+
+/* image */
+.img-circle {
+ border-radius: 50%;
+}
+
+.img-lg {
+ width: 120px;
+ height: 120px;
+}
+
+.avatar-upload-preview {
+ position: absolute;
+ top: 50%;
+ transform: translate(50%, -50%);
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #ccc;
+ overflow: hidden;
+}
+
+/* 鎷栨嫿鍒楁牱寮� */
+.sortable-ghost{
+ opacity: .8;
+ color: #fff!important;
+ background: #42b983!important;
+}
+
+/* 琛ㄦ牸鍙充晶宸ュ叿鏍忔牱寮� */
+.top-right-btn {
+ margin-left: auto;
+}
+
+/* 鍒嗗壊闈㈡澘鏍峰紡 */
+.splitpanes.default-theme .splitpanes__pane {
+ background-color: var(--splitpanes-default-bg) !important;
+}
diff --git a/src/assets/styles/sidebar.scss b/src/assets/styles/sidebar.scss
new file mode 100644
index 0000000..5da0e59
--- /dev/null
+++ b/src/assets/styles/sidebar.scss
@@ -0,0 +1,669 @@
+#app {
+ .main-container {
+ min-height: 100vh;
+ margin-left: var(--sidebar-width);
+ transition: margin-left 0.25s ease;
+ position: relative;
+ background: transparent;
+ }
+
+ .sidebarHide {
+ margin-left: 0 !important;
+ }
+
+ .sidebar-container {
+ transition: width 0.25s ease;
+ width: var(--sidebar-width) !important;
+ height: 100vh;
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 1001;
+ overflow: hidden;
+ padding: 0;
+ font-size: 0;
+ background: var(--sidebar-bg);
+ border-right: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: 8px 0 24px rgba(15, 23, 42, 0.08);
+ isolation: isolate;
+
+ &::before {
+ content: "";
+ position: absolute;
+ inset: -28% -52% -18% -38%;
+ z-index: 0;
+ pointer-events: none;
+ background:
+ radial-gradient(circle at 9% 12%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.62), transparent 44%),
+ radial-gradient(circle at 87% 18%, rgba(56, 189, 248, 0.4), transparent 48%),
+ radial-gradient(circle at 20% 82%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3), transparent 43%),
+ radial-gradient(circle at 66% 62%, rgba(125, 211, 252, 0.24), transparent 50%),
+ conic-gradient(
+ from 210deg at 58% 38%,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 0deg,
+ rgba(56, 189, 248, 0.05) 76deg,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.16) 180deg,
+ rgba(125, 211, 252, 0.04) 290deg,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 360deg
+ );
+ filter: blur(7px) saturate(1.24) contrast(1.05);
+ opacity: 0.96;
+ transform: translate3d(0, 0, 0);
+ transform-origin: 44% 58%;
+ animation:
+ sidebarAuroraDrift 17.9s cubic-bezier(0.31, 0.03, 0.18, 0.99) infinite,
+ sidebarAuroraBreath 9.7s ease-in-out infinite,
+ sidebarAuroraSkew 6.9s steps(23, end) infinite;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ background:
+ linear-gradient(
+ 108deg,
+ transparent 10%,
+ rgba(255, 255, 255, 0.17) 34%,
+ rgba(255, 255, 255, 0.04) 48%,
+ transparent 72%
+ ),
+ linear-gradient(
+ 202deg,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24) 0%,
+ transparent 34%,
+ rgba(56, 189, 248, 0.18) 66%,
+ transparent 100%
+ ),
+ radial-gradient(circle at 74% 12%, rgba(125, 211, 252, 0.25), transparent 50%),
+ radial-gradient(circle at 22% 84%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14), transparent 56%);
+ background-size: 236% 100%, 186% 186%, 164% 164%, 180% 180%;
+ background-position: 224% 0, 14% 16%, 78% 10%, 18% 82%;
+ opacity: 0.52;
+ transform: translate3d(0, 0, 0);
+ animation:
+ sidebarSheenSweep 13.1s linear infinite,
+ sidebarSheenJitter 4.7s steps(31, end) infinite;
+ }
+
+ > * {
+ position: relative;
+ z-index: 1;
+ }
+
+ .horizontal-collapse-transition {
+ transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+ }
+
+ .scrollbar-wrapper {
+ overflow-x: hidden !important;
+ overflow-y: auto !important;
+ }
+
+ .el-scrollbar__bar.is-vertical {
+ right: 2px;
+ }
+
+ .el-scrollbar {
+ height: 100%;
+ }
+
+ &.has-logo {
+ .el-scrollbar {
+ height: calc(100% - 78px);
+ }
+ }
+
+ .is-horizontal {
+ display: none;
+ }
+
+ a {
+ display: inline-block;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ .el-menu {
+ border: none !important;
+ height: 100%;
+ width: 100% !important;
+ padding: 10px 0 16px;
+ border-radius: 0;
+ background: transparent !important;
+ box-shadow: none;
+ backdrop-filter: none;
+ }
+
+ .el-menu-item,
+ .el-sub-menu__title,
+ .menu-title {
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+ }
+
+ .el-menu-item .el-menu-tooltip__trigger {
+ display: inline-flex !important;
+ width: 100%;
+ align-items: center;
+ }
+
+ .submenu-title-noDropdown,
+ .el-sub-menu__title,
+ .el-menu-item {
+ min-width: 0 !important;
+ width: calc(100% - 24px) !important;
+ margin: 0 12px 8px !important;
+ height: 50px;
+ line-height: 50px;
+ border-radius: 14px;
+ padding-left: 16px !important;
+ padding-right: 36px !important;
+ box-sizing: border-box;
+ transition: all 0.28s ease;
+ color: var(--sidebar-text);
+ background: linear-gradient(128deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.01));
+ border: 1px solid rgba(255, 255, 255, 0.06) !important;
+ position: relative;
+ overflow: hidden;
+ }
+
+ .submenu-title-noDropdown::after,
+ .el-sub-menu__title::after,
+ .el-menu-item::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(115deg, transparent 12%, rgba(255, 255, 255, 0.16), transparent 78%);
+ transform: translateX(-100%);
+ opacity: 0;
+ transition: transform 0.45s ease, opacity 0.26s ease;
+ pointer-events: none;
+ }
+
+ .submenu-title-noDropdown:hover,
+ .el-sub-menu__title:hover,
+ .el-menu-item:hover {
+ background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.28), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.08)) !important;
+ border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.32) !important;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 8px 18px rgba(8, 36, 76, 0.24);
+ transform: translateX(3px);
+ }
+
+ .submenu-title-noDropdown:hover::after,
+ .el-sub-menu__title:hover::after,
+ .el-menu-item:hover::after,
+ .el-menu-item.is-active::after,
+ .el-sub-menu.is-active > .el-sub-menu__title::after {
+ transform: translateX(100%);
+ opacity: 1;
+ }
+
+ & .theme-light .is-active > .el-sub-menu__title,
+ & .theme-dark .is-active > .el-sub-menu__title,
+ & .el-menu-item.is-active {
+ color: #fff !important;
+ background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
+ background-size: 180% 180%;
+ box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
+ border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.5) !important;
+ animation: sidebarActiveFlow 4.6s ease infinite;
+ }
+
+ & .nest-menu .el-sub-menu > .el-sub-menu__title,
+ & .el-sub-menu .el-menu-item {
+ min-width: 0 !important;
+ width: calc(100% - 32px) !important;
+ margin: 0 16px 6px !important;
+ height: 40px;
+ line-height: 40px;
+ padding-left: 12px !important;
+ padding-right: 12px !important;
+ border-radius: 8px;
+ transition: all 0.24s ease;
+ color: var(--sidebar-text);
+ border: none !important;
+ background: transparent;
+ font-size: 13px;
+
+ &:hover {
+ background: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.12) !important;
+ transform: translateX(4px);
+ }
+
+ &.is-active {
+ background: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.85) !important;
+ color: #fff !important;
+ font-weight: 500;
+ box-shadow: 0 4px 12px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.25);
+ }
+ }
+
+ // 瀛愯彍鍗曞鍣ㄦ牱寮� - 澧炲姞缂╄繘鍜岃瑙夊眰娆�
+ & .el-sub-menu .el-menu {
+ padding: 4px 0 8px;
+ margin-left: 8px;
+ border-left: 2px solid rgba(255, 255, 255, 0.08);
+ }
+ }
+
+ .hideSidebar {
+ .sidebar-container {
+ width: var(--sidebar-collapsed-width) !important;
+ }
+
+ .main-container {
+ margin-left: var(--sidebar-collapsed-width);
+ }
+
+ .submenu-title-noDropdown {
+ padding: 0 !important;
+ position: relative;
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+
+ .svg-icon {
+ margin-right: 0;
+ }
+
+ .el-tooltip {
+ padding: 0 !important;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+
+ .svg-icon {
+ margin-left: 0;
+ }
+ }
+
+ .el-menu-tooltip__trigger {
+ width: 100%;
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+
+ .svg-icon {
+ width: 22px;
+ height: 22px;
+ margin-right: 0;
+ flex-shrink: 0;
+ }
+ }
+ }
+
+ .el-sub-menu {
+ overflow: hidden;
+
+ & > .el-sub-menu__title {
+ padding: 0 !important;
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+
+ .svg-icon {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+ }
+
+ .el-menu--collapse {
+ width: 100% !important;
+ padding: 12px 0 16px;
+
+ > .el-menu-item,
+ .el-sub-menu {
+ & > .el-sub-menu__title,
+ &.el-menu-item {
+ width: calc(100% - 12px) !important;
+ margin: 0 6px 8px !important;
+ padding-left: 0 !important;
+ padding-right: 0 !important;
+ box-sizing: border-box;
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+
+ .svg-icon {
+ width: 22px;
+ height: 22px;
+ margin-right: 0;
+ flex-shrink: 0;
+ }
+
+ & > span {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+
+ .el-menu--collapse .el-menu .el-sub-menu {
+ min-width: var(--sidebar-width) !important;
+ }
+
+ .mobile {
+ .main-container {
+ margin-left: 0;
+ }
+
+ .sidebar-container {
+ transition: transform 0.25s;
+ width: var(--sidebar-width) !important;
+ }
+
+ &.hideSidebar {
+ .sidebar-container {
+ pointer-events: none;
+ transition-duration: 0.3s;
+ transform: translate3d(calc(-1 * var(--sidebar-width)), 0, 0);
+ }
+ }
+ }
+
+ .withoutAnimation {
+ .main-container,
+ .sidebar-container {
+ transition: none;
+ }
+ }
+}
+
+.el-menu--vertical {
+ & > .el-menu {
+ .svg-icon {
+ margin-right: 10px;
+ }
+ }
+
+ .nest-menu .el-sub-menu > .el-sub-menu__title,
+ .el-menu-item {
+ min-width: 0 !important;
+ margin: 0 10px 8px;
+ width: calc(100% - 20px);
+ height: 46px;
+ line-height: 46px;
+ padding-left: 12px !important;
+ padding-right: 12px !important;
+ box-sizing: border-box;
+ border-radius: 12px;
+ color: var(--sidebar-text);
+ border: 1px solid rgba(255, 255, 255, 0.06) !important;
+ background: linear-gradient(128deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.01));
+ transition: all 0.24s ease;
+
+ &:hover {
+ background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.07)) !important;
+ border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3) !important;
+ transform: translateX(2px);
+ }
+
+ &.is-active {
+ background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
+ background-size: 180% 180%;
+ color: #fff !important;
+ border-radius: 12px;
+ box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
+ border-color: rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.5) !important;
+ animation: sidebarActiveFlow 4.6s ease infinite;
+ }
+ }
+
+ > .el-menu--popup {
+ max-height: 100vh;
+ overflow: hidden;
+ padding: 10px;
+ border-radius: 14px;
+ position: relative;
+ isolation: isolate;
+ border: 1px solid rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.26);
+ box-shadow:
+ 0 18px 40px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.16),
+ var(--shadow-md);
+ background: var(--sidebar-bg);
+ backdrop-filter: blur(16px);
+
+ &::before {
+ content: "";
+ position: absolute;
+ inset: -28% -52% -18% -38%;
+ z-index: 0;
+ pointer-events: none;
+ background:
+ radial-gradient(circle at 9% 12%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.62), transparent 44%),
+ radial-gradient(circle at 87% 18%, rgba(56, 189, 248, 0.4), transparent 48%),
+ radial-gradient(circle at 20% 82%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.3), transparent 43%),
+ radial-gradient(circle at 66% 62%, rgba(125, 211, 252, 0.24), transparent 50%),
+ conic-gradient(
+ from 210deg at 58% 38%,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 0deg,
+ rgba(56, 189, 248, 0.05) 76deg,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.16) 180deg,
+ rgba(125, 211, 252, 0.04) 290deg,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14) 360deg
+ );
+ filter: blur(7px) saturate(1.24) contrast(1.05);
+ opacity: 0.96;
+ transform: translate3d(0, 0, 0);
+ transform-origin: 44% 58%;
+ animation:
+ sidebarAuroraDrift 17.9s cubic-bezier(0.31, 0.03, 0.18, 0.99) infinite,
+ sidebarAuroraBreath 9.7s ease-in-out infinite,
+ sidebarAuroraSkew 6.9s steps(23, end) infinite;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 0;
+ pointer-events: none;
+ background:
+ linear-gradient(
+ 108deg,
+ transparent 10%,
+ rgba(255, 255, 255, 0.17) 34%,
+ rgba(255, 255, 255, 0.04) 48%,
+ transparent 72%
+ ),
+ linear-gradient(
+ 202deg,
+ rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.24) 0%,
+ transparent 34%,
+ rgba(56, 189, 248, 0.18) 66%,
+ transparent 100%
+ ),
+ radial-gradient(circle at 74% 12%, rgba(125, 211, 252, 0.25), transparent 50%),
+ radial-gradient(circle at 22% 84%, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.14), transparent 56%);
+ background-size: 236% 100%, 186% 186%, 164% 164%, 180% 180%;
+ background-position: 224% 0, 14% 16%, 78% 10%, 18% 82%;
+ opacity: 0.52;
+ transform: translate3d(0, 0, 0);
+ animation:
+ sidebarSheenSweep 13.1s linear infinite,
+ sidebarSheenJitter 4.7s steps(31, end) infinite;
+ }
+
+ > * {
+ position: relative;
+ z-index: 1;
+ }
+
+ > .el-menu {
+ max-height: calc(100vh - 20px);
+ overflow-y: auto;
+ overflow-x: hidden;
+
+ &::-webkit-scrollbar-track-piece {
+ background: var(--surface-muted);
+ }
+
+ &::-webkit-scrollbar {
+ width: 5px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--accent-light);
+ border-radius: 10px;
+ }
+ }
+ }
+}
+
+@keyframes sidebarActiveFlow {
+ 0% {
+ background-position: 0% 50%;
+ }
+ 50% {
+ background-position: 100% 50%;
+ }
+ 100% {
+ background-position: 0% 50%;
+ }
+}
+
+@keyframes sidebarAuroraDrift {
+ 0% {
+ transform: translate3d(-6.3%, -1.8%, 0) scale(1.05) rotate(-1.8deg);
+ }
+ 6% {
+ transform: translate3d(2.2%, -4.6%, 0) scale(1.08) rotate(0.7deg);
+ }
+ 17% {
+ transform: translate3d(-3.7%, 4.4%, 0) scale(1.11) rotate(2deg);
+ }
+ 27% {
+ transform: translate3d(5.6%, 1.2%, 0) scale(1.03) rotate(-1deg);
+ }
+ 39% {
+ transform: translate3d(-4.8%, -3.1%, 0) scale(1.09) rotate(1.5deg);
+ }
+ 52% {
+ transform: translate3d(2.9%, 4.8%, 0) scale(1.04) rotate(-1.4deg);
+ }
+ 64% {
+ transform: translate3d(-6.4%, 0.3%, 0) scale(1.08) rotate(0.5deg);
+ }
+ 73% {
+ transform: translate3d(4.8%, -3.9%, 0) scale(1.05) rotate(1.6deg);
+ }
+ 81% {
+ transform: translate3d(-2.4%, 2.9%, 0) scale(1.1) rotate(-0.8deg);
+ }
+ 92% {
+ transform: translate3d(3.7%, -1.7%, 0) scale(1.06) rotate(-1.6deg);
+ }
+ 100% {
+ transform: translate3d(-5.9%, 0.8%, 0) scale(1.08) rotate(1.2deg);
+ }
+}
+
+@keyframes sidebarAuroraBreath {
+ 0% {
+ opacity: 0.76;
+ filter: blur(5px) saturate(1.08);
+ }
+ 15% {
+ opacity: 1;
+ filter: blur(7px) saturate(1.28);
+ }
+ 37% {
+ opacity: 0.84;
+ filter: blur(8px) saturate(1.12);
+ }
+ 61% {
+ opacity: 0.98;
+ filter: blur(6px) saturate(1.24);
+ }
+ 83% {
+ opacity: 0.86;
+ filter: blur(7px) saturate(1.16);
+ }
+ 100% {
+ opacity: 0.94;
+ filter: blur(6px) saturate(1.2);
+ }
+}
+
+@keyframes sidebarAuroraSkew {
+ 0% {
+ transform-origin: 44% 58%;
+ }
+ 21% {
+ transform-origin: 62% 42%;
+ }
+ 43% {
+ transform-origin: 31% 66%;
+ }
+ 66% {
+ transform-origin: 68% 74%;
+ }
+ 100% {
+ transform-origin: 39% 45%;
+ }
+}
+
+@keyframes sidebarSheenSweep {
+ 0% {
+ background-position: 232% 0, 10% 18%, 80% 12%, 20% 82%;
+ }
+ 8% {
+ background-position: 186% 0, 16% 30%, 74% 18%, 28% 74%;
+ }
+ 21% {
+ background-position: 116% 0, 34% 10%, 62% 26%, 18% 64%;
+ }
+ 37% {
+ background-position: 52% 0, 50% 24%, 46% 12%, 32% 58%;
+ }
+ 52% {
+ background-position: -4% 0, 34% 54%, 22% 22%, 12% 46%;
+ }
+ 69% {
+ background-position: -62% 0, 14% 36%, 32% 34%, 24% 56%;
+ }
+ 84% {
+ background-position: -106% 0, 20% 20%, 46% 20%, 34% 70%;
+ }
+ 100% {
+ background-position: -136% 0, 10% 18%, 80% 12%, 20% 82%;
+ }
+}
+
+@keyframes sidebarSheenJitter {
+ 0% {
+ opacity: 0.28;
+ transform: translate3d(0, 0, 0);
+ }
+ 17% {
+ opacity: 0.56;
+ transform: translate3d(1.8%, -0.5%, 0);
+ }
+ 38% {
+ opacity: 0.34;
+ transform: translate3d(-1.2%, 0.8%, 0);
+ }
+ 63% {
+ opacity: 0.6;
+ transform: translate3d(2.3%, -0.3%, 0);
+ }
+ 81% {
+ opacity: 0.3;
+ transform: translate3d(-1.6%, 0.7%, 0);
+ }
+ 100% {
+ opacity: 0.52;
+ transform: translate3d(2%, -0.1%, 0);
+ }
+}
diff --git a/src/assets/styles/transition.scss b/src/assets/styles/transition.scss
new file mode 100644
index 0000000..7e1b103
--- /dev/null
+++ b/src/assets/styles/transition.scss
@@ -0,0 +1,49 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+ opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform--move,
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+ transition: all .5s;
+}
+
+.fade-transform-enter {
+ opacity: 0;
+ transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+ opacity: 0;
+ transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+ transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+ opacity: 0;
+ transform: translateX(20px);
+}
+
+.breadcrumb-move {
+ transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+ position: absolute;
+}
diff --git a/src/assets/styles/variables.module.scss b/src/assets/styles/variables.module.scss
new file mode 100644
index 0000000..461b545
--- /dev/null
+++ b/src/assets/styles/variables.module.scss
@@ -0,0 +1,268 @@
+// base color
+$blue: #324157;
+$light-blue: #333c46;
+$red: #c03639;
+$pink: #e65d6e;
+$green: #30b08f;
+$tiffany: #4ab7bd;
+$yellow: #fec171;
+$panGreen: #30b08f;
+
+// menu palette - 浣跨敤涓婚鑹�
+$menuText: #5a6478;
+$menuActiveText: #ffffff;
+$menuBg: #f8fafb;
+$menuHover: rgba(var(--el-color-primary-rgb, 13, 148, 136), 0.08);
+
+// light theme - 浣跨敤涓婚鑹�
+$menuLightBg: #f8fafb;
+$menuLightHover: rgba(var(--el-color-primary-rgb, 13, 148, 136), 0.08);
+$menuLightText: #3d4858;
+$menuLightActiveText: #ffffff;
+
+// layout
+$base-sidebar-width: 216px;
+$base-sidebar-collapsed-width: 72px;
+$sideBarWidth: 216px;
+
+// sidebar - 浼樺寲鍚庣殑渚ц竟鏍忛厤鑹�
+$base-menu-color: #5a6478;
+$base-menu-color-active: #0d9488;
+$base-menu-background: #f8fafb;
+$base-sub-menu-background: #f0f5f4;
+$base-sub-menu-hover: #ffffff;
+
+// component - 浼樺寲鍚庣殑涓婚鑹�
+$--color-primary: #0d9488;
+$--color-success: #67c23a;
+$--color-warning: #d89b41;
+$--color-danger: #d25b52;
+$--color-info: #7d8797;
+
+:export {
+ menuText: $menuText;
+ menuActiveText: $menuActiveText;
+ menuBg: $menuBg;
+ menuHover: $menuHover;
+ menuLightBg: $menuLightBg;
+ menuLightHover: $menuLightHover;
+ menuLightText: $menuLightText;
+ menuLightActiveText: $menuLightActiveText;
+ sideBarWidth: $sideBarWidth;
+ blue: $blue;
+ lightBlue: $light-blue;
+ red: $red;
+ pink: $pink;
+ green: $green;
+ tiffany: $tiffany;
+ yellow: $yellow;
+ panGreen: $panGreen;
+ colorPrimary: $--color-primary;
+ colorSuccess: $--color-success;
+ colorWarning: $--color-warning;
+ colorDanger: $--color-danger;
+ colorInfo: $--color-info;
+}
+
+:root {
+ --sidebar-width: 216px;
+ --sidebar-collapsed-width: 72px;
+ --topbar-height: 64px;
+ --tagsbar-height: 40px;
+ --content-gap: 16px;
+ --content-radius: 16px;
+ --layout-header-z: 20;
+
+ --el-color-primary: #2563eb;
+ --el-color-primary-rgb: 37, 99, 235;
+ --el-color-success: #14b8a6;
+ --el-color-warning: #f59e0b;
+ --el-color-danger: #ef4444;
+
+ --sidebar-bg: linear-gradient(180deg, #0e2a4f 0%, #123e69 55%, #0e2a4f 100%);
+ --sidebar-text: rgba(234, 242, 255, 0.82);
+ --sidebar-muted: rgba(234, 242, 255, 0.82);
+ --menu-hover: rgba(147, 197, 253, 0.2);
+ --menu-active-bg: linear-gradient(135deg, #2f80ff 0%, #38bdf8 100%);
+ --menu-active-text: #f8fbff;
+ --menu-surface: linear-gradient(180deg, rgba(13, 43, 79, 0.97) 0%, rgba(8, 28, 52, 0.94) 100%);
+ --menu-active-glow: 0 8px 18px rgba(56, 139, 255, 0.28);
+
+ --app-bg: #f3f7fc;
+ --app-bg-accent: #eef5ff;
+ --surface-base: rgba(255, 255, 255, 0.92);
+ --surface-soft: rgba(255, 255, 255, 0.88);
+ --surface-muted: #f5f9ff;
+ --surface-border: rgba(148, 163, 184, 0.18);
+ --surface-border-strong: rgba(96, 165, 250, 0.34);
+ --text-primary: #1e293b;
+ --text-secondary: #334155;
+ --text-tertiary: #64748b;
+ --shadow-sm: 0 12px 32px rgba(15, 23, 42, 0.06);
+ --shadow-md: 0 20px 42px rgba(15, 23, 42, 0.1);
+ --shadow-menu: 0 16px 36px rgba(8, 27, 58, 0.26);
+ --radius-lg: 20px;
+ --radius-md: 16px;
+ --radius-sm: 12px;
+ --radius-xs: 10px;
+
+ --navbar-bg: rgba(255, 255, 255, 0.86);
+ --navbar-text: #1f3658;
+ --navbar-hover: rgba(37, 99, 235, 0.08);
+
+ --tags-bg: transparent;
+ --tags-item-bg: rgba(255, 255, 255, 0.9);
+ --tags-item-border: rgba(148, 163, 184, 0.22);
+ --tags-item-text: #334155;
+ --tags-item-hover: #f4f8ff;
+ --tags-close-hover: rgba(37, 99, 235, 0.16);
+
+ --accent-primary: #2563eb;
+ --accent-light: #3b82f6;
+ --accent-lighter: #60a5fa;
+
+ --panel-mask: rgba(255, 255, 255, 0.88);
+ --panel-glow: inset 0 1px 0 rgba(255, 255, 255, 0.58);
+ --splitpanes-default-bg: #f3f7fc;
+}
+
+html.dark {
+ --el-bg-color: #f8fbff;
+ --el-bg-color-overlay: #f3f7fd;
+ --el-text-color-primary: #1e293b;
+ --el-text-color-regular: #475569;
+ --el-border-color: rgba(148, 163, 184, 0.2);
+ --el-border-color-light: rgba(148, 163, 184, 0.18);
+
+ --sidebar-bg: linear-gradient(180deg, #0e2a4f 0%, #123e69 55%, #0e2a4f 100%);
+ --sidebar-text: rgba(234, 242, 255, 0.82);
+ --sidebar-muted: rgba(234, 242, 255, 0.82);
+ --menu-hover: rgba(147, 197, 253, 0.2);
+ --menu-active-bg: linear-gradient(135deg, #2f80ff 0%, #38bdf8 100%);
+ --menu-active-text: #f8fbff;
+ --menu-surface: linear-gradient(180deg, rgba(13, 43, 79, 0.97) 0%, rgba(8, 28, 52, 0.94) 100%);
+
+ --text-primary: #1e293b;
+ --text-secondary: #334155;
+ --text-tertiary: #64748b;
+ --accent-primary: #2563eb;
+ --accent-light: #3b82f6;
+
+ --navbar-bg: rgba(255, 255, 255, 0.86);
+ --navbar-text: #1f3658;
+ --navbar-hover: rgba(37, 99, 235, 0.08);
+
+ --tags-bg: transparent;
+ --tags-item-bg: rgba(255, 255, 255, 0.9);
+ --tags-item-border: rgba(148, 163, 184, 0.22);
+ --tags-item-text: #334155;
+ --tags-item-hover: #f4f8ff;
+ --tags-close-hover: rgba(37, 99, 235, 0.16);
+
+ --splitpanes-bg: #f3f7fc;
+ --splitpanes-border: rgba(148, 163, 184, 0.22);
+ --splitpanes-splitter-bg: #e7eef8;
+ --splitpanes-splitter-hover-bg: #d9e6f7;
+
+ --blockquote-bg: #f3f7ff;
+ --blockquote-border: rgba(59, 130, 246, 0.36);
+ --blockquote-text: #334155;
+ --cron-border: rgba(148, 163, 184, 0.22);
+ --splitpanes-default-bg: #f3f7fc;
+
+ .sidebar-container {
+ .el-menu-item,
+ .menu-title {
+ color: var(--sidebar-text);
+ }
+
+ .el-menu-item.is-active,
+ .el-menu-item.is-active .menu-title {
+ color: var(--menu-active-text) !important;
+ }
+
+ & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
+ & .theme-dark .el-sub-menu .el-menu-item {
+ background-color: var(--el-bg-color) !important;
+ }
+
+ & .theme-dark .el-sub-menu .el-menu-item.is-active {
+ background-color: var(--menu-active-bg) !important;
+ }
+ }
+
+ .el-menu--horizontal {
+ .el-menu-item {
+ &:not(.is-disabled) {
+ &:hover,
+ &:focus {
+ background-color: var(--navbar-hover) !important;
+ }
+ }
+ }
+ }
+
+ .splitpanes {
+ background-color: var(--splitpanes-bg);
+
+ .splitpanes__pane {
+ background-color: var(--splitpanes-bg);
+ border-color: var(--splitpanes-border);
+ }
+
+ .splitpanes__splitter {
+ background-color: var(--splitpanes-splitter-bg);
+ border-color: var(--splitpanes-border);
+
+ &:hover {
+ background-color: var(--splitpanes-splitter-hover-bg);
+ }
+
+ &:before,
+ &:after {
+ background-color: var(--splitpanes-border);
+ }
+ }
+ }
+
+ .el-table {
+ --el-table-header-bg-color: var(--el-bg-color-overlay) !important;
+ --el-table-header-text-color: var(--el-text-color-regular) !important;
+ --el-table-border-color: var(--el-border-color-light) !important;
+ --el-table-row-hover-bg-color: var(--el-bg-color-overlay) !important;
+
+ .el-table__header-wrapper,
+ .el-table__fixed-header-wrapper {
+ th {
+ background-color: var(--el-bg-color-overlay, #f0f1f5) !important;
+ color: var(--el-text-color-regular, #515a6e);
+ }
+ }
+ }
+
+ .el-tree {
+ .el-tree-node.is-current > .el-tree-node__content {
+ background-color: var(--el-bg-color-overlay) !important;
+ color: var(--el-color-primary);
+ }
+
+ .el-tree-node__content:hover {
+ background-color: var(--el-bg-color-overlay);
+ }
+ }
+
+ .el-dropdown-menu__item:not(.is-disabled):focus,
+ .el-dropdown-menu__item:not(.is-disabled):hover {
+ background-color: var(--navbar-hover) !important;
+ }
+
+ blockquote {
+ background-color: var(--blockquote-bg) !important;
+ border-left-color: var(--blockquote-border) !important;
+ color: var(--blockquote-text) !important;
+ }
+
+ .popup-result .title {
+ background: var(--cron-border);
+ }
+}
diff --git a/src/assets/system/BOM.svg b/src/assets/system/BOM.svg
new file mode 100644
index 0000000..9c7f434
--- /dev/null
+++ b/src/assets/system/BOM.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="62.90625" viewBox="0 0 59.5 62.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35778"><rect x="14" y="0" width="32" height="32" rx="0"/></clipPath></defs><path d="M1.204819142818451,26.096379L28.69276814281845,42.307213000000004L58.29516614281845,26.096379L28.69276814281845,12L1.204819142818451,26.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,26.065809L28.43877614281845,42.737896L28.68361514281845,42.882288L59.39288714281845,26.065309L28.684561142818453,11.44229615L0.16870074281845082,26.065809ZM28.70192014281845,41.732138L2.240937742818451,26.12695L28.70097614281845,12.55770427L57.19744514281845,26.127449L28.70192014281845,41.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,26.80120277404785L0.5,45.83131777404785L28.692764,62.042150774047855L28.692764,43.01204077404785L0.5,26.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,62.906411774047854L29.192764,42.722777774047856L28.941999,42.578587774047854L-5.999999996841865e-8,25.93693918404785L0,46.12058077404785L29.192764,62.906411774047854ZM28.192764,43.30130377404785L28.192764,61.17788877404785L1,45.542054774047855L1,27.66546636404785L28.192764,43.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,26.80120277404785L59,45.12649377404785L88.5,62.00042177404785L88.5,42.26604677404785L59,26.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,62.86243677404785L89,41.96362277404785L88.732151,41.82320777404785L58.50000003,25.97454732404785L58.50000003,45.41651177404785L89,62.86243677404785ZM88,42.568469774047855L88,61.13840677404785L59.5,44.836475774047855L59.5,27.627858224047852L88,42.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35778)"><path d="M16.87011742591858,20.681764625C16.9285550115,21.747077625,17.8163673875,22.577077625,18.8835551875,22.563638625L29.2344931875,22.563638625L29.2344931875,25.430513625L25.2735547875,25.430513625C24.8566794875,25.430513625,24.5185546875,25.768640625,24.5185546875,26.185512625C24.5185546875,26.602386625,24.8566794875,26.940511625,25.2735547875,26.940511625L34.7276171875,26.940511625C35.144493187500004,26.940511625,35.4826161875,26.602386625,35.4826161875,26.185512625C35.4826161875,25.768640625,35.1444911875,25.430513625,34.7276171875,25.430513625L30.7663681875,25.430513625L30.7663681875,22.563638625L41.117616187500005,22.563638625C42.1844921875,22.577075625,43.072304187499995,21.747075625,43.1310541875,20.681764625L43.1310541875,18.953014625L16.87011742591858,18.953014625L16.87011742591858,20.681764625ZM41.1173041875,5.05676746367L18.883554687500002,5.05676746367C17.7919921875,5.044579985,16.8941795825,5.913329955,16.8701171875,7.004579925L16.8701171875,17.268015625L43.1307411875,17.268015625L43.1307411875,7.004579925C43.106680187500004,5.913017635,42.2088661875,5.044579985,41.1173041875,5.05676746367Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/ai.svg b/src/assets/system/ai.svg
new file mode 100644
index 0000000..5f45ee2
--- /dev/null
+++ b/src/assets/system/ai.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="380.89990234375" height="372.80078125" viewBox="0 0 380.89990234375 372.80078125"><defs><pattern x="13.89990234375" y="167.900390625" width="367" height="166" patternUnits="userSpaceOnUse" id="master_svg0_143_34844"><image x="0" y="-0.17693836978131117" width="367" height="166.35387673956262" xlink:href="data:image/png;base64,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"/></pattern><pattern x="79.32733154296875" y="74.900390625" width="206.67991638183594" height="206.78233337402344" patternUnits="userSpaceOnUse" id="master_svg1_143_34837"><image x="-0.05120849609375" y="0" width="206.78233337402344" height="206.78233337402344" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAyAAAAMgCAYAAADbcAZoAAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAACAASURBVHic7N17nN1ldS/+z2c9371nkpAwyVxygWpQERWrVdSD1nNUilYuggZjJdTaXyNQ5CZarWJfnrHnVw6tRe4WIrRVC1gwKAjGFk6hN6UKre0RBbVCAXOZS+7JzOz9fdY6fzzfPbMnJJm990wyk8x6v155JZnZ+7uf/d179jzr+zxrLcA555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzzjnnnHPOOeecc84555xzE+B0D8A559zM0H3xc1URyWr/t5j+Jglw/K8LM9v7QWT/v1ZIpuMBUNXnfQ0AiDDu+GYGkjAzWN3xzQxUg4ik/+/xvfq/SULNlg1c071h4jPhnHPuQPIAxDnnHLo+0T9fdo9sh9QFA1r3K6IWANRN6PdqggCkFiygLgDBnkGIyWjQQRKqmgIQ4fMDi7oApDbcWrCCuuCldmzStmx8prMbdzE2d4acc85NFQ9AnHPuENB9/n8eSwlXAjhZVUHwQUA/2X/zi386VY+x8Pf/88jS7iOWI68YQ8lopgCw8calj0/VY0yHro/0Lw0ivwDAWmCiJl8ZuGbRb0332JxzbjbyAMQ552a4rgt+fhyVj1DYgWLlgCSi6VYRnjjwZy96crrHeCjo/tjAB6n4S9StoIhkZ276/MJ7p3tszjk3m3gA4pybFTpW/+BrGXEaRKAKTRt2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"/></pattern></defs><rect x="13.89990234375" y="167.900390625" width="367" height="166" rx="0" fill="url(#master_svg0_143_34844)" fill-opacity="1"/><rect x="79.32733154296875" y="74.900390625" width="206.67991638183594" height="206.78233337402344" rx="0" fill="url(#master_svg1_143_34837)" fill-opacity="1"/><g><path d="M139.18790233375,186.900390625L165.68390234375,122.868392625L181.33190134375,122.868392625L207.82790334375,186.900390625L191.89190334375002,186.900390625L185.65190134375,172.116393625L161.36390334375,172.116393625L155.12390334375,186.900390625L139.18790233375,186.900390625ZM173.55590434375,138.324390625L164.43590134375,160.692390625L182.67590334375,160.692390625L173.55590434375,138.324390625ZM214.73990634375,186.900390625L214.73990634375,122.868392625L228.37190234374998,122.868392625L228.37190234374998,186.900390625L214.73990634375,186.900390625Z" fill="#FFFFFF" fill-opacity="1"/><path d="M185.65190134375,172.116393625L191.89190334375002,186.900390625L207.82790334375,186.900390625L207.41410834375,185.900390625L181.33190134375,122.868392625L165.68390434375,122.868392625L139.60169547375,185.900390625L139.18790233375,186.900390625L155.12390534375,186.900390625L161.36390334375,172.116393625L185.65190134375,172.116393625ZM154.46055834375,185.900390625L160.70055434375,171.116393625L186.31525434374998,171.116393625L192.55524834375,185.900390625L206.33187834375002,185.900390625L180.66346734375,123.868392625L166.35234034375,123.868392625L140.68392804375,185.900390625L154.46055834375,185.900390625ZM214.73991434375,185.900390625L214.73991434375,186.900390625L228.37191034375002,186.900390625L228.37191034375002,122.868392625L214.73991434375,122.868392625L214.73991434375,185.900390625ZM215.73991434375,185.900390625L227.37191034375002,185.900390625L227.37191034375002,123.868392625L215.73991434375,123.868392625L215.73991434375,185.900390625ZM174.48189534375,137.946845625L172.62991334375,137.946845625L162.94825334375,161.692394625L184.16355534375,161.692394625L174.48189534375,137.946845625ZM173.01594134375,139.648723625L173.55590434375,138.324394625L174.09586734375,139.648723625L182.26818134375,159.692394625L182.67590334375,160.692394625L164.43590334375,160.692394625L164.84362834375,159.692394625L173.01594134375,139.648723625Z" fill-rule="evenodd" fill="#E2E6F7" fill-opacity="1"/></g><path d="M181.03737234375,51.027744395Q182.03354234375,50.984443635,183.03024234375,50.955862269L183.05888234375,51.955462325Q182.06956234375,51.983808525,181.08081234375,52.026801425L181.03737234375,51.027744395ZM172.04366234375,51.720317005Q174.02491234375,51.500955875,176.01177234375,51.340186175L176.09243234375,52.336925725Q174.12028234375,52.496511825,172.15371234375,52.714242925L172.04366234375,51.720317005ZM167.09549234374998,52.360937525Q168.08639234375,52.213984525,169.07935234375,52.081752025L169.21139234375,53.072996125Q168.22574234375,53.204284225,167.24216234375,53.350121525L167.09549234374998,52.360937525ZM158.23325234375,53.979113625Q160.17093234375,53.558054225,162.12014234375,53.194085825L162.30369234375001,54.177096325Q160.36891234375,54.538365325,158.44559234374998,54.956299325L158.23325234375,53.979113625ZM153.36297234375002,55.132375225Q154.33571234375,54.882904325,155.31206234375,54.647931825L155.54604234375,55.620178225000004Q154.57692234375,55.853413625,153.61136234375,56.101017025L153.36297234375002,55.132375225ZM144.72354134375001,57.657829325Q146.61345634375,57.036274425,148.52086634375001,56.470681625L148.80514534374998,57.429419525Q146.91186534374998,57.990807025,145.03596534374998,58.607769925L144.72354134375001,57.657829325ZM139.99185134375,59.311944025Q140.93434934375,58.962696125,141.88194234374998,58.627520525L142.21542334375,59.570271525Q141.27481834374998,59.902988425000004,140.33931734375,60.249637625L139.99185134375,59.311944025ZM131.66394834375,62.717643625Q133.48324634375,61.901856625,135.32585134375,61.140151625L135.70787834375,62.064305625Q133.87901334375,62.820330625,132.07310534375,63.630112625L131.66394834375,62.717643625ZM127.12737234375,64.853932625Q128.02841234375,64.409004625,128.93596634375,63.977537625L129.36532634374998,64.880674625Q128.46451534375,65.308926625,127.57015234375,65.750575625L127.12737234375,64.853932625ZM119.19712834375,69.10426662500001Q120.92459834375,68.10291062499999,122.68093134375,67.153081625L123.15663134375,68.03269362500001Q121.41317734375,68.975562625,119.69862334375,69.969421625L119.19712834375,69.10426662500001ZM114.90772234375001,71.698261625Q115.75703434375001,71.162862625,116.61421234375,70.640155625L117.13485734375,71.493924625Q116.28401934375,72.01277962500001,115.44097934375,72.54420462499999L114.90772234375001,71.698261625ZM107.45834334375,76.748943625Q109.07444034375,75.57273062499999,110.72465134375,74.444879625L111.28891034374999,75.270475625Q109.65093634375,76.389959625,108.04680234375,77.557472625L107.45834334375,76.748943625ZM103.46362734375,79.771280625Q104.25187634375,79.151578625,105.04924034375,78.543651625L105.65554034375,79.338893625Q104.86407134375,79.942317625,104.08168034375001,80.557422625L103.46362734375,79.771280625ZM96.57452734374999,85.569305625Q98.06130934375,84.23079662500001,99.58710134374999,82.93692762500001L100.23386734375,83.699607625Q98.71938334375,84.983920625,97.24359134375,86.312503625L96.57452734374999,85.569305625ZM92.91767834375,88.986293625Q93.63644434375,88.289275625,94.36546734375,87.602996625L95.05091834375,88.331107625Q94.32731634375,89.01231362499999,93.61383034375001,89.704189625L92.91767834375,88.986293625ZM86.66326934375,95.470214625Q88.00439434375,93.983653625,89.38901134375,92.53751762499999L90.11130934375001,93.229106625Q88.73696134375001,94.66448162500001,87.40576934375,96.14006062499999L86.66326934375,95.470214625ZM83.38322434375,99.244186625Q84.02494034375,98.477615625,84.67793634374999,97.720634625L85.43513534375,98.373809625Q84.78699534375,99.125167625,84.15001334375,99.88608562499999L83.38322434375,99.244186625ZM77.83154634375,106.34478762500001Q79.01226634375,104.72605862500001,80.24050334374999,103.14307762499999L81.03057434375,103.75609162500001Q79.81140534375,105.327400625,78.63946334375,106.934093625L77.83154634375,106.34478762500001ZM74.96307034375,110.434200625Q75.52103234375,109.606593625,76.09117134375,108.787322625L76.91197734375,109.358531625Q76.34598734375,110.171840625,75.79222834375,110.99320962499999L74.96307034375,110.434200625ZM70.17459834375,118.075591625Q71.18179334375,116.342231625,72.24001134375,114.639537625L73.08934234375,115.167388625Q72.03900134375,116.857421625,71.03923034375,118.578002625L70.17459834375,118.075591625ZM67.74809834375,122.435066625Q68.21651034375,121.555656625,68.69785134375,120.683250625L69.57342334375,121.166335625Q69.09561934375,122.032340625,68.63070634375,122.905181625L67.74809834375,122.435066625ZM63.77491234375,130.535812625Q64.59721934375,128.707084625,65.47347634375001,126.903587625L66.37293334375,127.340598625Q65.50319334375,129.130691625,64.68694834375,130.94592262499998L63.77491234375,130.535812625ZM61.81617734375,135.11592862499998Q62.19018134375,134.194740625,62.57771334375,133.279167625L63.49861634375,133.668960625Q63.11394434375,134.57776662499998,62.74272734375,135.492103625L61.81617734375,135.11592862499998ZM58.70139644375,143.590309625Q59.32948014375,141.68723262499998,60.01377584375,139.803634625L60.95367434375,140.145095625Q60.27444074375,142.01474762499998,59.65101524375,143.90371662500002L58.70139644375,143.590309625ZM57.23094944375,148.337608625Q57.50677584375,147.385436625,57.79655554375,146.437423625L58.75287584375,146.729743625Q58.46523574375,147.670760625,58.19145874375,148.615852625L57.23094944375,148.337608625ZM55.00851894375,157.09652062499998Q55.43540334375,155.141840625,55.92002534375,153.200670625L56.89024594375,153.44289062500002Q56.40922264375,155.369640625,55.98549174375,157.309880625L55.00851894375,157.09652062499998ZM54.03744124375,161.972030625Q54.21408894375,160.991200625,54.40519834375,160.013070625L55.38664004375,160.204830625Q55.19694904375,161.17569062500002,55.02160814375,162.14928062500002L54.03744124375,161.972030625ZM52.73529070375,170.905160625Q52.95680414375,168.92325062499998,53.23676284375,166.948740625L54.22686024375,167.08912062500002Q53.94897554375,169.049010625,53.72910284375,171.016230625L52.73529070375,170.905160625ZM52.27231326375,175.879800625Q52.34762379375,174.86800062499998,52.43813353375,173.85744062499998L53.43414644375,173.946640625Q53.34430824375,174.949710625,53.26955454375,175.954020625L52.27231326375,175.879800625ZM51.90390639705,184.860640625Q51.91918294475,182.877210625,51.99272362175,180.895080625L52.99203594375,180.932160625Q52.91903994375,182.899610625,52.90387664375,184.868350625L51.90390639705,184.860640625ZM51.95618113875,189.798100625Q51.92755823775,188.807130625,51.91349196075,187.815860625L52.91340004375,187.801680625Q52.92734464375,188.785600625,52.95577414375,189.769210625L51.95618113875,189.798100625ZM52.51921534375,198.816700625Q52.32639241375,196.810580625,52.19353938375,194.799590625L53.19136834375,194.733670625Q53.32323644375,196.729800625,53.51463374375,198.721020625L52.51921534375,198.816700625ZM53.08498834375,203.748890625Q52.95320044375,202.760830625,52.83601707375,201.770950625L53.82908034375,201.653400625Q53.94539304375,202.635990625,54.07620764375,203.616670625L53.08498834375,203.748890625ZM54.57651444375,212.649610625Q54.18028684375,210.689540625,53.84228794375,208.718610625L54.82789354375,208.549590625Q55.16338734375,210.505920625,55.55669664375,212.451460625L54.57651444375,212.649610625ZM55.65406724375,217.515750625Q55.41985654375,216.543460625,55.20007654375,215.567810625L56.17564344375,215.348070625Q56.39379784375,216.316590625,56.62625454375,217.281550625L55.65406724375,217.515750625ZM58.05708834375,226.205840625Q57.46272754375,224.305880625,56.92464824375,222.389210625L57.88740964375,222.118910625Q58.42152504375,224.021390625,59.011475043749996,225.907290625L58.05708834375,226.205840625ZM59.63648034375,230.945050625Q59.30222274375,230.000690625,58.98201604375,229.051450625L59.92957304375,228.731840625Q60.24734024375,229.673920625,60.57916064375,230.611370625L59.63648034375,230.945050625ZM62.92490434375,239.334460625Q62.13796734375,237.508650625,61.40513894375,235.660460625L62.33471534375,235.291850625Q63.06212434375,237.126330625,63.84324234375,238.938640625L62.92490434375,239.334460625ZM64.98919934375,243.889880625Q64.55863934375,242.984710625,64.14156734375,242.073260625L65.05088434375,241.657150625Q65.46478034375,242.561610625,65.89225234375,243.460340625L64.98919934375,243.889880625ZM69.12807034375,251.891270625Q68.15404134375,250.152950625,67.23174234375,248.386640625L68.11816634375,247.923780625Q69.03356134375,249.676860625,70.00045034375,251.402430625L69.12807034375,251.891270625ZM71.65441534375,256.21025062499996Q71.13243834375,255.354690625,70.62322434375,254.491450625L71.48452334375,253.983350625Q71.99026334375,254.840650625,72.50808734374999,255.689480625L71.65441534375,256.21025062499996ZM76.59985134375,263.739320625Q75.44887934375001,262.108350625,74.34663434375,260.44408062499997L75.18037234375001,259.89191062500004Q76.27441234375,261.54384062500003,77.41688134374999,263.162720625L76.59985134375,263.739320625ZM79.56030434375,267.77353062500003Q78.95281234375,266.977080625,78.35720634375,266.171690625L79.16120534375,265.577090625Q79.75263434375,266.37678062500004,80.35541334375,267.167070625L79.56030434375,267.77353062500003ZM85.25969334375,274.750090625Q83.94377134375,273.244720625,82.67300834375,271.701050625L83.44507234375,271.065490625Q84.70643634375,272.597760625,86.01258834375,274.091920625L85.25969334375,274.750090625ZM88.62186434374999,278.455380625Q87.93557734375,277.726670625,87.26017334375,276.987840625L87.99826834375,276.313120625Q88.66878134375,277.04663062500003,89.34984234375,277.769770625L88.62186434374999,278.455380625ZM95.01414134375,284.804410625Q93.54703134375,283.441420625,92.12099034375001,282.03549062499997L92.82307434375,281.323390625Q94.23854834375,282.718950625,95.69476734375,284.071780625L95.01414134375,284.804410625ZM98.74149334375,288.140760625Q97.98397034375,287.48758062499996,97.23621034375,286.82324062500004L97.90038334375001,286.075650625Q98.64258534375,286.735050625,99.39452734375,287.383420625L98.74149334375,288.140760625ZM105.75791534375,293.793760625Q104.15506734375,292.588320625,102.58873034375,291.335800625L103.21324134375,290.55481062499996Q104.76791734375,291.797970625,106.35897434374999,292.994540625L105.75791534375,293.793760625ZM109.80996334375,296.725200625Q108.98950534375,296.154490625,108.17758934375,295.571700625L108.76071134375,294.759310625Q109.56671134375,295.337840625,110.38101234375,295.904280625L109.80996334375,296.725200625ZM117.37522134375,301.621250625Q115.65377434375,300.58702062500004,113.96385534375,299.502030625L114.50412734375,298.660540625Q116.18145034375,299.737440625,117.89021334375,300.764070625L117.37522134375,301.621250625ZM121.70740534375,304.116030625Q120.83309934375,303.63388062499996,119.96601834375,303.138840625L120.46184534375,302.27042062500004Q121.32270834375,302.761920625,122.19030034375,303.240360625L121.70740534375,304.116030625ZM129.74089034374998,308.202670625Q127.91979234375,307.351560625,126.12483234375,306.446610625L126.57503534375,305.553660625Q128.35720834375002,306.45219062499996,130.16427634375,307.296720625L129.74089034374998,308.202670625ZM134.30453534375,310.233520625Q133.38627634375,309.845090625,132.47385434375002,309.443110625L132.87699934375001,308.527980625Q133.78177634374998,308.926570625,134.69414534375,309.312530625L134.30453534375,310.233520625ZM142.72155034374998,313.467130625Q140.82155634375,312.809140625,138.94199334375,312.094850625L139.29724834375,311.160030625Q141.16293334375,311.869050625,143.04880534375002,312.522190625L142.72155034374998,313.467130625ZM147.46373034375,315.011930625Q146.51221434375,314.721280625,145.56508634375,314.416660625L145.87127734375,313.464690625Q146.81035634375002,313.766720625,147.75585934375,314.055540625L147.46373034375,315.011930625ZM156.17633234375,317.357850625Q154.22004234374998,316.900630625,152.27823234375,316.385410625L152.53469234375,315.418850625Q154.46216234374998,315.930270625,156.40391234375,316.384090625L156.17633234375,317.357850625ZM161.03998234375,318.400450625Q160.06669234375,318.210480625,159.09630234374998,318.006260625L159.30227234375002,317.027710625Q160.26522234375,317.230410625,161.23153234375002,317.418980625L161.03998234375,318.400450625ZM169.95750234374998,319.833040625Q167.96923234374998,319.581510625,165.98929234374998,319.271090625L166.14419234375,318.283140625Q168.10985234375,318.591340625,170.08301234375,318.840940625L169.95750234374998,319.833040625ZM174.88553234375001,320.364710625Q173.90076234375,320.276730625,172.91739234375,320.174320625L173.02094234375,319.179690625Q173.99721234375,319.281340625,174.97455234375002,319.368680625L174.88553234375001,320.364710625ZM183.91084234375,320.867310625Q181.91636234375,320.823120625,179.92407234375,320.720030625L179.97575234375,319.721370625Q181.95305234375,319.823700625,183.93298234375,319.867520625L183.91084234375,320.867310625ZM188.84326234375,320.886410625Q187.87163234375,320.900390625,186.89990234375,320.900390625L186.87219234375,320.900390625L186.87219234375,319.900390625L186.89990234375,319.900390625Q187.86360234375,319.900390625,188.82887234375,319.886500625L188.84326234375,320.886410625ZM197.84508234375,320.455960625Q195.83415234375,320.619540625,193.81924234375,320.722960625L193.76797234375,319.724270625Q195.76820234375,319.621610625,197.76401234375,319.459260625L197.84508234375,320.455960625ZM202.79832234375,319.960970625Q201.81343234375,320.077790625,200.82692234375,320.180080625L200.72372234375,319.185450625Q201.70291234375,319.083860625,202.68057234375,318.967930625L202.79832234375,319.960970625ZM211.73442234375,318.596470625Q209.74951234375,318.967930625,207.75439234375,319.279880625L207.59993234375,318.291870625Q209.58032234375,317.982270625,211.55046234375,317.613520625L211.73442234375,318.596470625ZM216.59475234375,317.594020625Q215.62327234375,317.813080625,214.64868234375,318.017790625L214.44311234375,317.039150625Q215.41093234375,316.835880625,216.37478234375,316.618530625L216.59475234375,317.594020625ZM225.33255234375,315.314180625Q223.40765234375,315.885830625,221.46661234375,316.399990625L221.21054234375,315.433350625Q223.13707234375,314.923000625,225.04788234375,314.355590625L225.33255234375,315.314180625ZM230.07766234375,313.809260625Q229.13174234375,314.128570625,228.18120234375,314.433870625L227.87542234375,313.481750625Q228.81894234375,313.178740625,229.75781234375,312.861790625L230.07766234375,313.809260625ZM238.52577234375,310.639190625Q236.67615234375,311.404690625,234.80462234375,312.115050625L234.44978234375,311.180080625Q236.30800234375,310.474820625,238.14335234375,309.715210625L238.52577234375,310.639190625ZM243.09843234375,308.646970625Q242.18945234375,309.063140625,241.27440234375,309.465820625L240.87161234375,308.550540625Q241.77916234375,308.151150625,242.68216234375,307.737730625L243.09843234375,308.646970625ZM251.16830234375,304.620960625Q249.40996234375,305.572810625,247.62420234375,306.47218062499996L247.17436234375,305.579090625Q248.94716234375,304.686170625,250.69227234375,303.741530625L251.16830234375,304.620960625ZM255.51530234375,302.162700625Q254.65378234375,302.67116062499997,253.78480234375,303.166790625L253.28937234375,302.298140625Q254.15130234375,301.80652062499996,255.00708234375,301.301500625L255.51530234375,302.162700625ZM263.12166234375,297.324110625Q261.47130234375004,298.453080625,259.78822234375,299.53267062500004L259.24832234375003,298.690950625Q260.91876234375,297.619480625,262.55702234374996,296.498750625L263.12166234375,297.324110625ZM267.19391234374996,294.426300625Q266.38952234375,295.021420625,265.57641234375,295.604570625L264.99363234375,294.791920625Q265.80020234375,294.213500625,266.59909234375,293.622390625L267.19391234374996,294.426300625ZM274.25570234375,288.827380625Q272.73015234375,290.122150625,271.16688234375,291.371120625L270.54269234375,290.58986062500003Q272.09428234375,289.350200625,273.60862234374997,288.064960625L274.25570234375,288.827380625ZM278.00819234375,285.521070625Q277.26976234375,286.196400625,276.52141234375,286.860700625L275.85756234375003,286.112840625Q276.60016234374996,285.453660625,277.33328234375,284.783140625L278.00819234375,285.521070625ZM284.44983234375,279.222470625Q283.06537234375,280.669660625,281.63859234375,282.07513062500004L280.93681234375003,281.36273062500004Q282.35322234374996,279.967440625,283.72722234375,278.531190625L284.44983234375,279.222470625ZM287.84149234375,275.54298062500004Q287.17701234375,276.29121062499996,286.50152234375,277.029510625L285.76372234375003,276.35449062500004Q286.43422234375,275.62163062499997,287.09379234375,274.878950625L287.84149234375,275.54298062500004ZM293.59396234375004,268.613020625Q292.36571234375003,270.19737062499996,291.09096234375,271.744570625L290.31918234374996,271.108670625Q291.58462234374997,269.572810625,292.80362234375,268.00034062500004L293.59396234375004,268.613020625ZM296.58766234375,264.59979062499997Q296.00433234375,265.412800625,295.40902234375,266.217070625L294.60524234375,265.622150625Q295.19630234375,264.823580625,295.77514234374996,264.016830625L296.58766234375,264.59979062499997ZM301.58969234375,257.113110625Q300.53100234375,258.818150625,299.42212234375,260.490950625L298.58861234375,259.938450625Q299.68930234375,258.27797062499997,300.74013234375,256.585600625L301.58969234375,257.113110625ZM304.15233234375,252.809780625Q303.65651234375,253.678660625,303.14787234375,254.540080625L302.28679234375,254.031630625Q302.79158234375,253.176760625,303.28378234375,252.314180625L304.15233234375,252.809780625ZM308.35110234375,244.846190625Q307.47333234375003,246.654740625,306.54210234375,248.436340625L305.65585234375,247.973110625Q306.58026234375,246.204590625,307.45146234375,244.409560625L308.35110234375,244.846190625ZM310.45398234375,240.300670625Q310.05111234375,241.215670625,309.63473234375,242.124590625L308.72558234375,241.708100625Q309.13904234375,240.805590625,309.53875234375,239.897720625L310.45398234375,240.300670625ZM313.80539234375,231.943860625Q313.11786234375,233.838820625,312.37405234375,235.712400625L311.44461234375,235.343430625Q312.18292234375,233.483570625,312.86535234375,231.602780625L313.80539234375,231.943860625ZM315.42477234375,227.208450625Q315.11929234375,228.158980625,314.79974234375,229.104870625L313.85232234375,228.784840625Q314.16986234375,227.844820625,314.47275234375,226.902450625L315.42477234375,227.208450625ZM317.89349234375,218.545130625Q317.40573234375,220.502360625,316.85971234375,222.444150625L315.89706234375,222.173450625Q316.43878234375,220.247040625,316.92318234375,218.303330625L317.89349234375,218.545130625ZM319.01153234375,213.676570625Q318.80661234375,214.651200625,318.58734234375,215.622700625L317.61188234375,215.402540625Q317.82943234375,214.438660625,318.03293234375,213.470810625L319.01153234375,213.676570625ZM320.57095234375,204.796100625Q320.28909234375,206.790110625,319.94787234375,208.774830625L318.96231234375,208.605380625Q319.30108234375,206.635070625,319.58081234375,204.656130625L320.57095234375,204.796100625ZM321.17672234375,199.855150625Q321.07419234375,200.841690625,320.95718234375,201.826630625L319.96417234375,201.708660625Q320.08035234375,200.730570625,320.18210234375,199.751770625L321.17672234375,199.855150625ZM321.80914234375,190.849490625Q321.73559234375,192.854460625,321.60251234375,194.856350625L320.60467234375,194.789990625Q320.73684234375,192.802700625,320.80981234375,190.812850625L321.80914234375,190.849490625ZM321.86984234375,183.051150625Q321.89990234375,184.475620625,321.89990234375,185.900390625Q321.89990234375,186.886050625,321.88550234375,187.871600625L320.88562234375,187.856980625Q320.89990234375,186.879780625,320.89990234375,185.900390625Q320.89990234375,184.486590625,320.87005234375,183.072250625L321.86984234375,183.051150625ZM321.67221234375,178.06314062500002Q321.73044234375,179.064220625,321.77377234375,180.066070625L320.77472234375,180.109340625Q320.73169234375,179.115890625,320.67389234375,178.12115062499998L321.67221234375,178.06314062500002ZM320.85013234375,169.09751062499998Q321.09698234375,171.065310625,321.28592234375,173.039520625L320.29043234375,173.134800625Q320.10284234375,171.17481062500002,319.85791234375,169.221950625L320.85013234375,169.09751062499998ZM320.13376234375,164.13487062500002Q320.29605234375,165.12831062499998,320.44351234375,166.124050625L319.45431234375,166.27058062499998Q319.30786234375,165.281900625,319.14685234375,164.296070625L320.13376234375,164.13487062500002ZM318.38940234375,155.314210625Q318.84057234375,157.253910625,319.23437234375,159.206070625L318.25412234375,159.403780625Q317.86340234375,157.466600625,317.41537234375,155.540810625L318.38940234375,155.314210625ZM317.15988234375,150.441406625Q317.42471234375,151.414280625,317.67499234375,152.390990625L316.70624234375,152.639160625Q316.45791234375,151.66979062500002,316.19497234375,150.704131625L317.15988234375,150.441406625ZM314.51282234375,141.855598625Q315.16418234375,143.742866625,315.75949234375,145.64857462499998L314.80493234375,145.946716625Q314.21417234375,144.055267625,313.56753234375,142.181877625L314.51282234375,141.855598625ZM312.78381234375,137.133163625Q313.14807234375,138.073417625,313.49823234375,139.01901262500002L312.56042234375,139.366226625Q312.21329234375,138.428642625,311.85135234375,137.494430625L312.78381234375,137.133163625ZM309.26199234375,128.869521625Q310.10660234375,130.681671625,310.89718234375,132.51803562499998L309.97870234375,132.913474625Q309.19403234375,131.090965625,308.35559234375,129.29195362500002L309.26199234375,128.869521625ZM307.05388234375,124.353484625Q307.51312234375,125.250045625,307.95895234375,126.153358625L307.06222234375,126.595939625Q306.61969234375,125.699310625,306.16385234375,124.809387625L307.05388234375,124.353484625ZM302.69351234375,116.496940625Q303.72204234375,118.212959625,304.69928234375004,119.958701625L303.82669234375,120.447166625Q302.85669234375,118.714378625,301.83577234375,117.011031625L302.69351234375,116.496940625ZM300.03236234375004,112.238689625Q300.58107234375,113.081424625,301.11717234375,113.932243625L300.27113234374997,114.465373625Q299.73877234375,113.620567625,299.19432234375,112.784294625L300.03236234375004,112.238689625ZM294.87814234375,104.871375625Q296.07930234375,106.472026625,297.23248234375,108.107574625L296.41520234375,108.683818625Q295.27049234375,107.060287625,294.07829234375004,105.47159962500001L294.87814234375,104.871375625ZM291.79469234375,100.917560625Q292.42644234375,101.697326625,293.04652234375,102.486396625L292.26025234375004,103.104270625Q291.64509234375,102.321449625,291.01770234375,101.547077625L291.79469234375,100.917560625ZM285.89998234375,94.118125625Q287.26066234375,95.585819625,288.57725234375,97.09320062500001L287.82406234375003,97.75103362499999Q286.51741234375004,96.254978625,285.16664234375,94.798004625L285.89998234375,94.118125625ZM282.42921234375,90.510925625Q283.13675234375,91.219489625,283.83368234375,91.938472625L283.11564234375,92.63448762499999Q282.42401234375,91.920959625,281.72159234375,91.21751362500001L282.42921234375,90.510925625ZM275.85577234375,84.353099625Q277.36130234375,85.671939625,278.82705234375,87.03483162500001L278.14612234375,87.76716962500001Q276.69136234375003,86.414512625,275.19684234375,85.10530862499999L275.85577234375,84.353099625ZM272.03647234375,81.130332625Q272.81181234375003,81.76038162500001,273.57774234375,82.401836625L272.93567234375,83.168491625Q272.17499234375,82.53142162500001,271.40584234375,81.906410625L272.03647234375,81.130332625ZM264.85399234375,75.681697625Q266.48802234375,76.837394625,268.08706234375,78.041021625L267.48568234375,78.839986625Q265.89856234375,77.645337625,264.27655234375004,76.498132625L264.85399234375,75.681697625ZM260.72816234375,72.876554625Q261.56288234375,73.42179862500001,262.38945234375,73.979310625L261.83027234375004,74.808360625Q261.00979234375,74.254962625,260.18129234375,73.713767625L260.72816234375,72.876554625ZM253.01359234375,68.197461625Q254.75825234375,69.177434625,256.47310234375004,70.208686625L255.95775234375,71.065662625Q254.25560234375,70.042055625,252.52386234375,69.069333625L253.01359234375,68.197461625ZM248.62404234375,65.837362625Q249.51043234375,66.293059625,250.39001234375,66.761793625L249.91971234375,67.644298625Q249.04683234375,67.179111625,248.16684234375,66.72672862499999L248.62404234375,65.837362625ZM240.46307234375,61.981116625Q242.29878234375,62.774583625,244.11017234375,63.622079625L243.68638234375,64.527844625Q241.88835234375,63.686595625,240.06631234375,62.899036625L240.46307234375,61.981116625ZM235.85312234375,60.088695525Q236.78447234375,60.451081325000004,237.71034234375,60.827215225L237.33398234375,61.753682624999996Q236.41508234375,61.380392625,235.49049234375,61.020627625L235.85312234375,60.088695525ZM227.33920234375,57.099525425Q229.24470234375,57.697792525,231.13168234375,58.352166625L230.80404234375,59.296970325000004Q228.93109234375,58.647457125,227.03966234375,58.053608925L227.33920234375,57.099525425ZM222.55142234375,55.692982625Q223.52249234375,55.958869025,224.48950234375,56.239209224999996L224.21106234375,57.199662724999996Q223.25126234375,56.921408225,222.28734234375,56.657480225L222.55142234375,55.692982625ZM213.78616234375,53.604778725Q215.73858234375,54.001567125,217.67845234375,54.455798125L217.45047234375,55.429462425Q215.52498234375,54.978601425,213.58700234375,54.584746125L213.78616234375,53.604778725ZM208.86210234375,52.698808025Q209.86056234375,52.863432325,210.85643234375,53.043005725L210.67897234375,54.027135325Q209.69044234375,53.848882725,208.69943234375,53.685486525L208.86210234375,52.698808025ZM199.95469234375,51.533086775Q201.92964234375,51.724967955,203.89810234375,51.974809725L203.77218234375,52.966851425Q201.81830234375,52.718860725,199.85798234375,52.528399425L199.95469234375,51.533086775ZM194.93542234375,51.139749575Q195.94269234375,51.199810865,196.94894234375,51.274922165L196.87451234375,52.272147925Q195.87575234375,52.197595625,194.87590234375,52.137977025L194.93542234375,51.139749575ZM189.94488234375,50.934735443L189.92233234375,51.934480825Q188.41130234375,51.900390625,186.90245234375,51.900387405000004L186.21744234375,51.902124525L186.00497234375,51.903372425L185.99910234375,50.9033960113Q186.44949234375,50.90039062500003,186.89990234375,50.900390625Q188.42259234375,50.900390625,189.94488234375,50.934735443Z" fill-rule="evenodd" fill="#67E4F4" fill-opacity="1"/><ellipse cx="305.89990234375" cy="124.900390625" rx="5" ry="5" fill="#67E4F4" fill-opacity="1"/><ellipse cx="67.89990234375" cy="124.900390625" rx="5" ry="5" fill="#67E4F4" fill-opacity="1"/><ellipse cx="187.89990234375" cy="51.900390625" rx="5" ry="5" fill="#67E4F4" fill-opacity="1"/><ellipse cx="59.89990234375" cy="231.900390625" rx="5" ry="5" fill="#67E4F4" fill-opacity="1"/><ellipse cx="311.89990234375" cy="238.900390625" rx="5" ry="5" fill="#67E4F4" fill-opacity="1"/><ellipse cx="191.89990234375" cy="320.900390625" rx="5" ry="5" fill="#67E4F4" fill-opacity="1"/><path d="M362.89990234375,87.300391925Q363.04237334375,87.300391925,363.18415034375,87.286427525Q363.32593534375,87.272463325,363.46566034375,87.244669225Q363.60539234375,87.216875125,363.74172234375,87.175519025Q363.87805934375,87.134162425,364.00967834375,87.079642325Q364.14130434375,87.025122425,364.26695234375,86.957963425Q364.39260134375,86.890804525,364.51105534375,86.811653425Q364.62951634374997,86.732502425,364.73964734375,86.642121825Q364.84977334375003,86.551741125,364.95050834375,86.451001125Q365.05125034375,86.350261125,365.14163234375,86.240131825Q365.23201034375,86.130002625,365.31115734375,86.011544825Q365.39031234375,85.893087125,365.45747034375,85.767441725Q365.52462734375,85.641796225,365.57914734375,85.510172925Q365.63366734375,85.378549815,365.67502634375,85.242216585Q365.71638534375,85.105883245,365.74417834375,84.966152725Q365.77197634375,84.826422335,365.78594234375,84.684640435Q365.79990434375,84.542858545,365.79990434375,84.400390625Q365.79990434375,84.257922705,365.78594234375,84.116140815Q365.77197634375,83.974358915,365.74417834375,83.834628525Q365.71638534375,83.694898005,365.67502634375,83.558564725Q365.63366734375,83.422231435,365.57914734375,83.290608325Q365.52462734375,83.158985025,365.45747034375,83.033339625Q365.39031234375,82.907694125,365.31116134375,82.789236525Q365.23201334375,82.670778725,365.14163634375,82.560649425Q365.05125434375,82.450520125,364.95051234375,82.349780125Q364.84977734375,82.249040125,364.73964734375,82.158659225Q364.62951634374997,82.068278525,364.51105534375,81.989127625Q364.39260134375,81.909976525,364.26695234375,81.842817825Q364.14131134375,81.775658825,364.00968134375,81.721138925Q363.87805934375,81.666618825,363.74172934375,81.625262225Q363.60539234375,81.583906125,363.46566034375,81.556112025Q363.32593534375,81.528317925,363.18415034375,81.514353525Q363.04237334375,81.500389325,362.89990234375,81.500389325Q362.75743534375,81.500389325,362.61565034375,81.514353725Q362.47386934375,81.528317925,362.33413734375,81.556112025Q362.19440834375,81.583906125,362.05807534375,81.625262225Q361.92173734375,81.666618825,361.79011134375,81.721138925Q361.65849334375,81.775658825,361.53284834375,81.842817825Q361.40720334375,81.909976725,361.28874234375,81.989127825Q361.17028834375003,82.068278825,361.06015734375,82.158659425Q360.95003134374997,82.249040125,360.84928934375,82.349780125Q360.74855034375,82.450520125,360.65816834375,82.560649425Q360.56778734375,82.670778625,360.48863234375,82.789236425Q360.40948534375,82.907694125,360.34232734375,83.033339525Q360.27516934375,83.158985025,360.22064634375,83.290608325Q360.16613034375,83.422231435,360.12477534375,83.558564665Q360.08341934375,83.694898005,360.05562234375,83.834628525Q360.02783234375,83.974358915,360.01386634375,84.116140815Q359.99990034375,84.257922705,359.99990034375,84.400390625Q359.99990034375,84.542858545,360.01386634375,84.684640435Q360.02783234375,84.826422335,360.05562234375,84.966152725Q360.08341934375,85.105883245,360.12477534375,85.242216525Q360.16613034375,85.378549815,360.22064634375,85.510172925Q360.27516934375,85.641796225,360.34232334375,85.767441625Q360.40948534375,85.893087125,360.48863234375,86.011544725Q360.56778734375,86.130002525,360.65816834375,86.240131825Q360.74855034375,86.350261125,360.84928934375,86.451001125Q360.95003134374997,86.551741125,361.06016134375,86.642122025Q361.17028834375003,86.732502725,361.28874634375,86.811653625Q361.40720334375,86.890804725,361.53285234375,86.957963425Q361.65849334375,87.025122425,361.79011534375,87.079642325Q361.92174534375,87.134162425,362.05807834375,87.175519025Q362.19441234375,87.216875125,362.33413734375,87.244669225Q362.47386934375,87.272463325,362.61565434375,87.286427725Q362.75743534375,87.300391925,362.89990234375,87.300391925ZM359.11136234375,87.747195725L360.74371734375,86.570725225L360.15903134375003,85.759468125L358.52667634375,86.935938825L359.11136234375,87.747195725ZM355.84665634375,90.100137225L357.47901134375,88.923666525L356.89432534375,88.112409325L355.26197034375,89.288879825L355.84665634375,90.100137225ZM352.58195134375,92.453078225L354.21430234375,91.276608025L353.62961534375,90.465350625L351.99726534375,91.641820925L352.58195134375,92.453078225ZM349.31724534375,94.80601862500001L350.94960034375,93.629548025L350.36491434375,92.818291625L348.73255934375,93.994762425L349.31724534375,94.80601862500001ZM346.05253634375,97.158961625L347.68489034375,95.982490625L347.10020434375,95.171234625L345.46784934375,96.347704625L346.05253634375,97.158961625ZM342.78783434375,99.51190162500001L344.42018534375,98.335430625L343.83549834375,97.524174625L342.20314834375,98.70064562499999L342.78783434375,99.51190162500001ZM339.52312834375,101.86484362499999L341.15547934375,100.688373625L340.57079334375,99.877116625L338.93843834375,101.05358662500001L339.52312834375,101.86484362499999ZM336.25842134375,104.21778462500001L337.89077534375,103.041314625L337.30608534375,102.230058625L335.67373134375,103.40652862499999L336.25842134375,104.21778462500001ZM332.99371734375,106.570724625L334.62607034375003,105.394254625L334.04138034375,104.582998625L332.40902734375,105.759467625L332.99371734375,106.570724625ZM329.72900934375,108.92366762500001L331.36136234375,107.747197625L330.77667234375,106.935941625L329.14431934375,108.112411625L329.72900934375,108.92366762500001ZM326.46430434375,111.27660962499999L328.09665834375,110.100137625L327.51196834375,109.288881625L325.87961434375,110.46535262500001L326.46430434375,111.27660962499999ZM323.19959834375,113.629548625L324.83194934375,112.453079625L324.24725934375,111.641822625L322.61490834375,112.818292625L323.19959834375,113.629548625ZM319.93489234375,115.98249262499999L321.56724734375,114.80601862500001L320.98255734375,113.99476262499999L319.35020234375,115.17123562500001L319.93489234375,115.98249262499999ZM316.67018704375,118.335430625L318.30253834375,117.158962625L317.71784734375,116.347705625L316.08549694375,117.524173625L316.67018704375,118.335430625ZM313.40548134375,120.688373625L315.03783224375,119.511905625L314.45314214375003,118.700649625L312.82079124375,119.877117625L313.40548134375,120.688373625ZM310.14077164375,123.04131662500001L311.77312664375,121.864845625L311.18843674375,121.053588625L309.55608204375,122.230060625L310.14077164375,123.04131662500001ZM307.69224721375,124.80601862500001L308.50842454375,124.21778462500001L307.92373484375,123.40652862499999L307.10755747375,123.99476262499999L307.69224721375,124.80601862500001Z" fill-rule="evenodd" fill="#67E4F4" fill-opacity="1"/><path d="M311.73714587375,240.373159405L312.69027471375,240.701284405L313.01578764375,239.755746845L312.06265881375,239.427621845L311.73714587375,240.373159405ZM314.59652094375,241.357534425L316.50277094375,242.013784425L316.82828374375,241.068246825L314.92203374375,240.411996845L314.59652094375,241.357534425ZM318.40902094375,242.670034425L320.31527044375,243.326284425L320.64078424375,242.380746825L318.73453374375,241.724496825L318.40902094375,242.670034425ZM322.22152034375,243.982534425L324.12777034375,244.638784425L324.45328434375,243.693246825L322.54703434375,243.036996825L322.22152034375,243.982534425ZM326.03402034375,245.295032525L327.94027534375,245.951284425L328.26578734375,245.005746825L326.35953434375,244.349494925L326.03402034375,245.295032525ZM329.84652134375,246.607534425L331.75277134375,247.263784425L332.07828334375,246.318246825L330.17203334375,245.661996825L329.84652134375,246.607534425ZM333.65902134375,247.920034425L335.56527134375,248.576284425L335.89078334375,247.630746825L333.98453334375,246.974496825L333.65902134375,247.920034425ZM337.47152134375,249.23253342499999L339.37777134375,249.88878342499999L339.70328334375,248.94324592499999L337.79703334375,248.28699592499999L337.47152134375,249.23253342499999ZM341.28401934375,250.545033625L343.19027134375,251.201284625L343.51578334375,250.255746625L341.60953134375,249.59949592499999L341.28401934375,250.545033625ZM345.09651934375,251.857533625L347.00276934375,252.513784625L347.32828534375,251.568246625L345.42203534375,250.911995625L345.09651934375,251.857533625ZM348.90901934375,253.170034625L350.81526934375,253.826284625L351.14078534375,252.880746625L349.23453534375,252.224496625L348.90901934375,253.170034625ZM352.72151934375,254.482534625L354.62776934375,255.138784625L354.95328534375,254.193246625L353.04703534375,253.536996625L352.72151934375,254.482534625ZM356.53401934375,255.795034625L358.44026934375,256.45128462499997L358.76578534375,255.505746625L356.85953534375,254.849496625L356.53401934375,255.795034625ZM360.34651934375,257.10753462499997L362.25276934375,257.76378462499997L362.57828534375,256.81824662500003L360.67203534375,256.16199662500003L360.34651934375,257.10753462499997ZM364.15901934375,258.42003462499997L366.06526934375,259.07628462499997L366.39078534375,258.13074662500003L364.48453534375,257.47449662500003L364.15901934375,258.42003462499997ZM372.89990234375,263.800392625Q373.04237334375,263.800392625,373.18415034375,263.786428625Q373.32593534375,263.772464625,373.46566034375,263.744666625Q373.60539234375,263.716874625,373.74172634375,263.675517625Q373.87805934375,263.634162625,374.00967434375,263.579639625Q374.14130434375,263.525121625,374.26694834375,263.457963625Q374.39259334375,263.390804625,374.51104734375,263.311653625Q374.62950934375,263.232501625,374.73963934375,263.142124625Q374.84976934375,263.051742625,374.95050834375,262.951000625Q375.05125034375,262.85026162500003,375.14163234375,262.740131625Q375.23201034375,262.630000625,375.31115734375,262.511543625Q375.39031234375,262.393085625,375.45747034375,262.267438625Q375.52462734375,262.141794625,375.57914334375,262.010169625Q375.63366734375,261.878547625,375.67502634375,261.742214625Q375.71638534375,261.605882625,375.74417534375,261.466150625Q375.77197234375,261.326421625,375.78594234375,261.184640625Q375.79990434375,261.042857625,375.79990434375,260.900390625Q375.79990434375,260.757923625,375.78593434375,260.616142625Q375.77197234375,260.474359625,375.74417534375,260.334627625Q375.71638534375,260.194896625,375.67502634375,260.058563625Q375.63367034375,259.922229625,375.57914734375,259.790605625Q375.52462734375,259.658985625,375.45747034375,259.533338625Q375.39031234375,259.407691625,375.31115734375,259.289234625Q375.23201034375,259.170778625,375.14162834374997,259.060649625Q375.05125034375,258.95051962499997,374.95050834375,258.849779625Q374.84976934375,258.749040625,374.73963934375,258.658658625Q374.62950934375,258.568279625,374.51104734375,258.489126625Q374.39259334375,258.409976625,374.26694834375,258.342817625Q374.14130434375,258.275657625,374.00967834375,258.221137625Q373.87805934375,258.166620625,373.74172934375,258.125263625Q373.60539234375,258.083907625,373.46566034375,258.056110625Q373.32593534375,258.028318625,373.18415034375,258.014354625Q373.04237334375,258.000388625,372.89990234375,258.000388625Q372.75743834375,258.000388625,372.61565434375,258.014354625Q372.47386934375,258.028320625,372.33413734375,258.056112625Q372.19440434375,258.083907625,372.05807534375,258.125264625Q371.92173734375,258.166620625,371.79010734375,258.221137625Q371.65848534375,258.275657625,371.53284434375,258.342817625Q371.40719634375,258.409976625,371.28873834374997,258.489126625Q371.17028834375003,258.568279625,371.06015734375,258.658660625Q370.95002734375,258.749040625,370.84928534375,258.849779625Q370.74855034375,258.95051962499997,370.65816534375,259.060649625Q370.56778734375,259.170778625,370.48863234375,259.289234625Q370.40948534375,259.407691625,370.34232734375,259.533338625Q370.27516934375,259.658985625,370.22064234375,259.790605625Q370.16612234375,259.922229625,370.12477134375,260.058563625Q370.08341234375,260.194896625,370.05561834375,260.334627625Q370.02782834375,260.474359625,370.01386634375,260.616142625Q369.99990034375,260.757923625,369.99990034375,260.900390625Q369.99990034375,261.042857625,370.01386234375,261.184638625Q370.02782834375,261.326421625,370.05561834375,261.466150625Q370.08341234375,261.605882625,370.12476734375,261.742215625Q370.16612234375,261.878547625,370.22064234375,262.010169625Q370.27516934375,262.141794625,370.34232734375,262.267440625Q370.40948534375,262.393085625,370.48863234375,262.511543625Q370.56778734375,262.630000625,370.65816834375,262.740131625Q370.74855034375,262.85026162500003,370.84928934375,262.951000625Q370.95003534375,263.051742625,371.06016134375,263.142122625Q371.17028834375003,263.232501625,371.28874234375,263.311651625Q371.40720334375,263.390804625,371.53284834375,263.457961625Q371.65849334375,263.525121625,371.79011134375,263.579639625Q371.92173734375,263.634162625,372.05807534375,263.675517625Q372.19440834375,263.716876625,372.33413734375,263.744668625Q372.47386934375,263.772464625,372.61565434375,263.786430625Q372.75743834375,263.800392625,372.89990234375,263.800392625ZM367.97151934375,259.73253462499997L369.87776934375,260.38878462499997L370.20328534375,259.44324662500003L368.29703534375,258.78699662500003L367.97151934375,259.73253462499997Z" fill-rule="evenodd" fill="#67E4F4" fill-opacity="1"/><path d="M191.39990234375,320.900390625L191.39990234375,321.921222725L192.39990234375,321.921222725L192.39990234375,320.900390625L191.39990234375,320.900390625ZM191.39990234375,323.962890625L191.39990234375,326.004554725L192.39990234375,326.004554725L192.39990234375,323.962890625L191.39990234375,323.962890625ZM191.39990234375,328.046222725L191.39990234375,330.087890625L192.39990234375,330.087890625L192.39990234375,328.046222725L191.39990234375,328.046222725ZM191.39990234375,332.129554625L191.39990234375,334.171222625L192.39990234375,334.171222625L192.39990234375,332.129554625L191.39990234375,332.129554625ZM191.39990234375,336.212890625L191.39990234375,338.254556625L192.39990234375,338.254556625L192.39990234375,336.212890625L191.39990234375,336.212890625ZM191.39990234375,340.296222625L191.39990234375,342.337888625L192.39990234375,342.337888625L192.39990234375,340.296222625L191.39990234375,340.296222625ZM191.39990234375,344.379556625L191.39990234375,346.421222625L192.39990234375,346.421222625L192.39990234375,344.379556625L191.39990234375,344.379556625ZM191.39990234375,348.462890625L191.39990234375,350.504556625L192.39990234375,350.504556625L192.39990234375,348.462890625L191.39990234375,348.462890625ZM191.39990234375,352.546222625L191.39990234375,354.587890625L192.39990234375,354.587890625L192.39990234375,352.546222625L191.39990234375,352.546222625ZM191.39990234375,356.62955462499997L191.39990234375,358.671222625L192.39990234375,358.671222625L192.39990234375,356.62955462499997L191.39990234375,356.62955462499997ZM191.39990234375,360.712890625L191.39990234375,362.754558625L192.39990234375,362.754558625L192.39990234375,360.712890625L191.39990234375,360.712890625ZM191.39990234375,364.796222625L191.39990234375,366.837890625L192.39990234375,366.837890625L192.39990234375,364.796222625L191.39990234375,364.796222625ZM191.89990234375,372.800392625Q192.04237027375,372.800392625,192.18415218375,372.786430625Q192.32593411375,372.772464625,192.46566450375,372.744666625Q192.60539496375,372.716873625,192.74172830375,372.675521625Q192.87806165375,372.634162625,193.00968484375,372.579639625Q193.14130804375,372.525123625,193.26695344375,372.457961625Q193.39259884375,372.390800625,193.51105664375,372.311649625Q193.62951444375,372.232498625,193.73964364375001,372.14211662499997Q193.84977294375,372.051738625,193.95051284375,371.950996625Q194.05125284375,371.850257625,194.14163374375,371.740127625Q194.23201444375,371.629997625,194.31116534375,371.511539625Q194.39031644375,371.393081625,194.45747514375,371.267436625Q194.52463414375,371.141792625,194.57915404375,371.010166625Q194.63367434375,370.878547625,194.67503074375,370.742217625Q194.71638704375,370.605880625,194.74418094375,370.466148625Q194.77197554375,370.326423625,194.78593964375,370.184638625Q194.79990414375,370.042861625,194.79990384375,369.900390625Q194.79990414375,369.757926625,194.78593994375,369.616142625Q194.77197554375,369.474357625,194.74418114375,369.334625625Q194.71638724375,369.194900625,194.67503094375,369.058563625Q194.63367464375,368.922233625,194.57915424375,368.790603625Q194.52463434375,368.658981625,194.45747544375,368.533332625Q194.39031644375,368.407691625,194.31116534375,368.289230625Q194.23201444375,368.17077662500003,194.14163354375,368.060645625Q194.05125284375,367.950515625,193.95051284375,367.849777625Q193.84977294375,367.749038625,193.73964354375,367.658656625Q193.62951444375,367.568275625,193.51105654375,367.489124625Q193.39259884375,367.409973625,193.26695344375,367.342815625Q193.14130804375,367.275657625,193.00968484375,367.221134625Q192.87806165375,367.166618625,192.74172830375,367.125263625Q192.60539496375,367.083907625,192.46566450375,367.056110625Q192.32593411375,367.028320625,192.18415218375,367.014354625Q192.04237027375,367.000392625,191.89990234375,367.000388625Q191.75743441375,367.000392625,191.61565250375,367.014358625Q191.47387057375,367.028320625,191.33414018375,367.056110625Q191.19440972375,367.083907625,191.05807638375,367.125263625Q190.92174303375,367.166618625,190.79011984375,367.221134625Q190.65849664375,367.275657625,190.53285124375,367.342815625Q190.40720584375,367.409973625,190.28874804375,367.489120625Q190.17029024375,367.568275625,190.06016104374999,367.658656625Q189.95003174375,367.749038625,189.84929184375,367.849777625Q189.74855184375,367.950515625,189.65817094375,368.060645625Q189.56779024375,368.17077662500003,189.48863934375,368.289230625Q189.40948824375,368.407691625,189.34232954375,368.533336625Q189.27517054375,368.658981625,189.22065064375,368.790603625Q189.16613034375,368.922233625,189.12477394375,369.058566625Q189.08341764375,369.194900625,189.05562374375,369.334625625Q189.02782914375,369.474357625,189.01386504375,369.616142625Q188.99990054375,369.757926625,188.99990084375,369.900390625Q188.99990054375,370.042861625,189.01386474375,370.184642625Q189.02782914375,370.326423625,189.05562354375,370.466152625Q189.08341744375,370.605884625,189.12477374375,370.742214625Q189.16613004375,370.878547625,189.22065044375,371.010166625Q189.27517034375,371.141792625,189.34232924375,371.267440625Q189.40948824375,371.393081625,189.48863934375,371.511539625Q189.56779024375,371.629997625,189.65817114375,371.740127625Q189.74855184375,371.850257625,189.84929184375,371.950996625Q189.95003174375,372.051738625,190.06016114375,372.142120625Q190.17029024375,372.232498625,190.28874814375,372.311645625Q190.40720584375,372.390800625,190.53285124375,372.457958625Q190.65849664375,372.525115625,190.79011984375,372.579635625Q190.92174303375,372.634162625,191.05807638375,372.675517625Q191.19440972375,372.716873625,191.33414018375,372.744666625Q191.47387057375,372.772464625,191.61565250375,372.786430625Q191.75743441375,372.800392625,191.89990234375,372.800392625Z" fill-rule="evenodd" fill="#67E4F4" fill-opacity="1"/><path d="M59.78063934375,231.414822455L58.83064634375,231.648154705L59.06916834375,232.619291065L60.01916134375,232.385958795L59.78063934375,231.414822455ZM56.93064134375,232.114821735L55.03064334375,232.581487895L55.26916934375,233.552624225L57.16916634375,233.085958125L56.93064134375,232.114821735ZM53.13064534375,233.048154125L51.23064034375,233.514821125L51.46916534375,234.485957425L53.36916734375,234.019290425L53.13064534375,233.048154125ZM49.33064234375,233.981488225L47.43064534375,234.448154425L47.66916634375,235.419290525L49.56916434375,234.952624325L49.33064234375,233.981488225ZM45.53064334375,234.914820625L43.63064234375,235.381488525L43.86916334375,236.352624425L45.76916934375,235.885956725L45.53064334375,234.914820625ZM41.73064034375,235.848154825L39.83064634375,236.314820725L40.06916834375,237.285956825L41.96916534375,236.819290625L41.73064034375,235.848154825ZM37.93064134375,236.781487925L36.03064334375,237.248155125L36.26916534375,238.219291225L38.16916634375,237.752624025L37.93064134375,236.781487925ZM34.130645343750004,237.714821325L32.23064234375,238.181488025L32.46916534375,239.152624625L34.36916934375,238.685957425L34.130645343750004,237.714821325ZM30.33064234375,238.648155225L28.43064134375,239.114820925L28.66916434375,240.085957525L30.56916634375,239.619291825L30.33064234375,238.648155225ZM26.53064134375,239.581487225L24.63064234375,240.048154825L24.86916534375,241.019291925L26.76916534375,240.552623725L26.53064134375,239.581487225ZM22.73064234375,240.514821025L20.83064234375,240.981487225L21.06916634375,241.952624625L22.96916534375,241.485958125L22.73064234375,240.514821025ZM18.93064134375,241.448154425L17.03064134375,241.914821625L17.26916534375,242.885958625L19.16916434375,242.419291625L18.93064134375,241.448154425ZM15.13064034375,242.381487625L13.23064034375,242.848154625L13.46916534375,243.819291625L15.36916534375,243.352624625L15.13064034375,242.381487625ZM2.89990234375,248.800392625Q3.04237029375,248.800392625,3.18415218375,248.78642862499999Q3.32593411375,248.772464625,3.4656645037500002,248.744668625Q3.60539502375,248.716874625,3.74172830375,248.675517625Q3.87806165375,248.634162625,4.00968484375,248.579639625Q4.14130804375,248.525121625,4.26695344375,248.457961625Q4.39259914375,248.390804625,4.51105674375,248.311651625Q4.62951454375,248.232499625,4.73964384375,248.142120625Q4.84977304375,248.05173862499998,4.950513143749999,247.951000625Q5.05125334375,247.850260625,5.14163394375,247.740131625Q5.23201464375,247.630001625,5.31116534375,247.511543625Q5.390316443750001,247.393087625,5.45747544375,247.267440625Q5.52463434375,247.141795625,5.579154243750001,247.010171625Q5.63367464375,246.878549625,5.67503094375,246.742215625Q5.71638724375,246.605882625,5.74418114375,246.466152625Q5.77197554375,246.326421625,5.78593964375,246.184640625Q5.799904343750001,246.042857625,5.79990414375,245.900390625Q5.799904343750001,245.757923625,5.78593994375,245.616140625Q5.77197554375,245.474359625,5.74418114375,245.334627625Q5.71638724375,245.194898625,5.67503094375,245.058565625Q5.63367464375,244.922231625,5.579154243750001,244.790607625Q5.52463434375,244.658985625,5.45747544375,244.533338625Q5.390316443750001,244.407693625,5.31116534375,244.289234625Q5.23201464375,244.170778625,5.14163394375,244.060648625Q5.05125334375,243.950519625,4.950513143749999,243.849779625Q4.84977304375,243.749040625,4.73964384375,243.658658625Q4.62951454375,243.568278625,4.51105674375,243.489126625Q4.39259914375,243.409975625,4.26695354375,243.342817625Q4.14130804375,243.275657625,4.00968484375,243.22113662499999Q3.87806165375,243.166618625,3.74172836375,243.125261625Q3.60539502375,243.083906625,3.4656645037500002,243.056112625Q3.32593411375,243.028318625,3.18415218375,243.014353625Q3.04237029375,243.000388625,2.89990234375,243.000388625Q2.75743439375,243.000388625,2.61565250375,243.014354625Q2.47387057375,243.028318625,2.3341401837499998,243.056112625Q2.19440966375,243.083906625,2.05807638375,243.125263625Q1.9217430337499999,243.166618625,1.79011984375,243.221137625Q1.65849664375,243.275659625,1.53285124375,243.342818625Q1.40720554375,243.409976625,1.28874794375,243.489127625Q1.17029014375,243.568279625,1.06016084375,243.658659625Q0.95003164375,243.749040625,0.8492915437500002,243.849779625Q0.74855134375,243.950519625,0.65817074375,244.060647625Q0.5677900437500001,244.170778625,0.48863934375000007,244.289234625Q0.4094882437499998,244.407693625,0.34232924375000007,244.533338625Q0.27517034375000016,244.658985625,0.22065044374999987,244.790607625Q0.16613004374999996,244.922231625,0.12477374375000005,245.058565625Q0.08341744375000015,245.194898625,0.05562354374999989,245.334627625Q0.027829143750000007,245.474359625,0.013865043750000083,245.616142625Q-0.00009965625000019074,245.757923625,-0.00009945625000007396,245.900390625Q-0.00009965625000019074,246.042857625,0.01386474375000013,246.184640625Q0.027829143750000007,246.326421625,0.05562354374999989,246.466152625Q0.08341744375000015,246.605882625,0.12477374375000005,246.742215625Q0.16613004374999996,246.878549625,0.22065044374999987,247.010171625Q0.27517034375000016,247.141795625,0.34232924375000007,247.267441625Q0.4094882437499998,247.393087625,0.48863934375000007,247.511543625Q0.5677900437500001,247.630001625,0.65817074375,247.740131625Q0.74855134375,247.850260625,0.8492915437500002,247.951000625Q0.95003164375,248.05173862499998,1.06016084375,248.142120625Q1.17029014375,248.232499625,1.28874794375,248.311651625Q1.40720554375,248.390804625,1.53285114375,248.457961625Q1.65849664375,248.525121625,1.79011984375,248.579639625Q1.9217430337499999,248.634162625,2.05807632375,248.675517625Q2.19440966375,248.716874625,2.3341401837499998,248.744668625Q2.47387057375,248.772464625,2.61565250375,248.78642862499999Q2.75743439375,248.800392625,2.89990234375,248.800392625ZM11.33064074375,243.314821625L9.43064074375,243.781487625L9.669164643750001,244.752624625L11.56916524375,244.285957625L11.33064074375,243.314821625ZM7.53064064375,244.248154625L5.630640243749999,244.714820625L5.86916514375,245.68595762500001L7.76916504375,245.219291625L7.53064064375,244.248154625Z" fill-rule="evenodd" fill="#67E4F4" fill-opacity="1"/><path d="M3.89990234375,104.300392125Q4.04237027375,104.300392125,4.18415218375,104.286428225Q4.32593411375,104.272463825,4.46566450375,104.244669425Q4.60539496375,104.216875525,4.7417283037499995,104.175519225Q4.87806165375,104.134162925,5.00968484375,104.079642525Q5.14130804375,104.025122625,5.26695344375,103.957963725Q5.39259884375,103.890804725,5.51105664375,103.811653625Q5.62951444375,103.732502725,5.73964364375,103.642121825Q5.84977294375,103.551741125,5.9505128437499994,103.451001125Q6.0512528437499995,103.350261225,6.14163374375,103.240131825Q6.23201444375,103.130002725,6.31116534375,103.011544825Q6.390316443750001,102.893087125,6.45747514375,102.767441725Q6.52463414375,102.641796325,6.57915404375,102.510173125Q6.63367434375,102.378549935,6.67503074375,102.242216585Q6.71638704375,102.105883245,6.74418094375,101.966152785Q6.77197554375,101.826422395,6.78593964375,101.684640465Q6.79990414375,101.542858555,6.79990384375,101.400390625Q6.79990414375,101.257922695,6.78593994375,101.116140785Q6.77197554375,100.974358855,6.74418114375,100.834628465Q6.71638724375,100.694898005,6.67503094375,100.558564665Q6.63367464375,100.422231315,6.579154243750001,100.290608125Q6.52463434375,100.158984925,6.45747544375,100.033339525Q6.390316443750001,99.907694125,6.31116534375,99.789236325Q6.23201444375,99.670778525,6.14163354375,99.560649325Q6.0512528437499995,99.450520025,5.9505128437499994,99.349780125Q5.84977294375,99.249040125,5.73964354375,99.158659225Q5.62951444375,99.068278525,5.51105654375,98.989127625Q5.39259884375,98.909976525,5.26695344375,98.842817825Q5.14130804375,98.775658825,5.00968484375,98.721138925Q4.87806165375,98.666618625,4.7417283037499995,98.625262225Q4.60539496375,98.583905925,4.46566450375,98.556112025Q4.32593411375,98.528317425,4.18415218375,98.514353325Q4.04237027375,98.500389125,3.89990234375,98.500389125Q3.75743441375,98.500389125,3.61565250375,98.514353025Q3.47387057375,98.528317425,3.3341401837499998,98.556111825Q3.1944097237499998,98.583905725,3.05807638375,98.625262025Q2.92174303375,98.666618325,2.7901198437500003,98.721138725Q2.65849664375,98.775658625,2.5328512437499997,98.842817525Q2.40720584375,98.909976525,2.28874804375,98.989127625Q2.1702902437500002,99.068278525,2.06016104375,99.158659425Q1.95003174375,99.249040125,1.84929184375,99.349780125Q1.74855184375,99.450520025,1.65817094375,99.560649425Q1.5677902437500002,99.670778525,1.48863934375,99.789236425Q1.4094882437499998,99.907694125,1.34232954375,100.033339525Q1.2751705437499998,100.158984925,1.22065064375,100.290608125Q1.16613034375,100.422231315,1.1247739437500002,100.558564665Q1.0834176437499998,100.694898005,1.05562374375,100.834628465Q1.02782914375,100.974358855,1.01386504375,101.116140785Q0.9999005437499999,101.257922695,0.9999008437499999,101.400390625Q0.9999005437499999,101.542858555,1.0138647437500001,101.684640465Q1.02782914375,101.826422395,1.05562354375,101.966152785Q1.0834174437500002,102.105883245,1.12477374375,102.242216585Q1.16613004375,102.378549935,1.2206504437499999,102.510173125Q1.2751703437500002,102.641796325,1.34232924375,102.767441725Q1.4094882437499998,102.893087125,1.48863934375,103.011544925Q1.5677902437500002,103.130002725,1.6581711437500002,103.240131925Q1.74855184375,103.350261225,1.84929184375,103.451001125Q1.95003174375,103.551741125,2.0601611437500003,103.642122025Q2.1702902437500002,103.732502725,2.28874814375,103.811653625Q2.40720584375,103.890804725,2.5328512437499997,103.957963425Q2.65849664375,104.025122425,2.7901198437500003,104.079642325Q2.92174303375,104.134162625,3.05807638375,104.175519025Q3.1944097237499998,104.216875325,3.3341401837499998,104.244669225Q3.47387057375,104.272463825,3.61565250375,104.286427925Q3.75743441375,104.300392125,3.89990234375,104.300392125ZM8.76500944375,102.507302925L6.882655843749999,101.874949965L6.564207343750001,102.822890025L8.44656034375,103.455242825L8.76500944375,102.507302925ZM12.52971454375,103.772008625L10.647362243749999,103.139655725L10.32891324375,104.087595725L12.21126554375,104.719948525L12.52971454375,103.772008625ZM16.294421343750003,105.036714525L14.41206734375,104.404361725L14.09361834375,105.352301625L15.97597234375,105.984654425L16.294421343750003,105.036714525ZM20.05912634375,106.301420225L18.176774343749997,105.669067825L17.85832534375,106.617007725L19.74067734375,107.249360125L20.05912634375,106.301420225ZM23.82383134375,107.566126325L21.94147834375,106.933773525L21.62303134375,107.881713425L23.50538434375,108.514066225L23.82383134375,107.566126325ZM27.58853734375,108.830832525L25.70618434375,108.198479225L25.38773734375,109.146419025L27.27009034375,109.778772325L27.58853734375,108.830832525ZM31.35324334375,110.095537225L29.47089034375,109.463184325L29.15244334375,110.411125225L31.03479534375,111.043478025L31.35324334375,110.095537225ZM35.117950343749996,111.360243825L33.23559734375,110.727890925L32.917150343749995,111.675831625L34.799503343750004,112.308184625L35.117950343749996,111.360243825ZM38.88265634375,112.624949625L37.00030534375,111.992596625L36.68185434375,112.940537625L38.56420534375,113.572890625L38.88265634375,112.624949625ZM42.64736134375,113.889654625L40.76501034375,113.257302625L40.44656034375,114.205242625L42.32891034375,114.837595625L42.64736134375,113.889654625ZM46.41207134375,115.154361625L44.52971634375,114.522008625L44.21126534375,115.469949625L46.09362034375,116.102302625L46.41207134375,115.154361625ZM50.17677334375,116.419066625L48.29442234375,115.786713625L47.97597134375,116.734654625L49.85832234375,117.367007625L50.17677334375,116.419066625ZM53.94147834375,117.683772625L52.05912734375,117.051420625L51.74067734375,117.99936062500001L53.62302734375,118.631713625L53.94147834375,117.683772625ZM57.70618834375,118.948478625L55.82383334375,118.316125625L55.50538234375,119.264066625L57.38773734375,119.896419625L57.70618834375,118.948478625ZM61.47089034375,120.213184625L59.58853934375,119.580831625L59.27008834375,120.528772625L61.15243934375,121.161125625L61.47089034375,120.213184625ZM65.23559934375001,121.477889625L63.35324434375,120.84553762499999L63.03479434375,121.79347762500001L64.91714834375,122.425830625L65.23559934375001,121.477889625ZM68.05912734375,122.426418625L67.11794634374999,122.110242625L66.79949534375001,123.058183625L67.74067734375001,123.374359625L68.05912734375,122.426418625Z" fill-rule="evenodd" fill="#67E4F4" fill-opacity="1"/><path d="M188.39990234375,5.800392125Q188.54237027375,5.800392125,188.68415218375,5.786428225Q188.82593411375,5.772463825,188.96566450375,5.744669425Q189.10539496375,5.716875525,189.24172830375,5.675519225Q189.37806165375,5.634162925,189.50968484375,5.579642525000001Q189.64130804375,5.525122625,189.76695344375,5.457963725Q189.89259884375,5.390804725000001,190.01105664375,5.311653625Q190.12951444375,5.232502725,190.23964364375001,5.142121825Q190.34977294375,5.0517411249999995,190.45051284375,4.9510011249999994Q190.55125284375,4.850261225,190.64163374375,4.740131825Q190.73201444375,4.630002725,190.81116534375,4.511544825Q190.89031644375,4.393087125,190.95747514375,4.267441725Q191.02463414375,4.141796325,191.07915404375,4.010173125Q191.13367434375,3.878549935,191.17503074375,3.742216585Q191.21638704375,3.6058832450000002,191.24418094375,3.4661527850000002Q191.27197554375,3.326422395,191.28593964375,3.184640465Q191.29990414375,3.042858555,191.29990384375,2.900390625Q191.29990414375,2.757922695,191.28593994375,2.616140785Q191.27197554375,2.474358855,191.24418114375,2.3346284649999998Q191.21638724375,2.1948980049999998,191.17503094375,2.058564665Q191.13367464375,1.9222313149999999,191.07915424375,1.790608125Q191.02463434375,1.658984925,190.95747544375,1.533339525Q190.89031644375,1.407694125,190.81116534375,1.289236325Q190.73201444375,1.170778525,190.64163354375,1.060649325Q190.55125284375,0.9505200250000001,190.45051284375,0.8497801250000001Q190.34977294375,0.7490401250000001,190.23964354375,0.6586592250000001Q190.12951444375,0.5682785250000002,190.01105654375,0.48912762500000007Q189.89259884375,0.4099765249999998,189.76695344375,0.342817825Q189.64130804375,0.27565882499999983,189.50968484375,0.221138925Q189.37806165375,0.1666186249999999,189.24172830375,0.12526222500000017Q189.10539496375,0.08390592499999983,188.96566450375,0.05611202500000001Q188.82593411375,0.028317425000000007,188.68415218375,0.014353325000000083Q188.54237027375,0.00038912499999987915,188.39990234375,0.00038912499999987915Q188.25743441375,0.00038912499999987915,188.11565250375,0.01435302500000013Q187.97387057375,0.028317425000000007,187.83414018375,0.05611182499999989Q187.69440972375,0.08390572500000015,187.55807638375,0.12526202500000005Q187.42174303375,0.16661832499999996,187.29011984375,0.22113872499999987Q187.15849664375,0.27565862500000016,187.03285124375,0.34281752500000007Q186.90720584375,0.4099765249999998,186.78874804375,0.48912762500000007Q186.67029024375,0.5682785250000002,186.56016104374999,0.6586594250000002Q186.45003174375,0.7490401250000001,186.34929184375,0.8497801250000001Q186.24855184375,0.9505200250000001,186.15817094375,1.060649425Q186.06779024375,1.170778525,185.98863934375,1.289236425Q185.90948824375,1.407694125,185.84232954375,1.533339525Q185.77517054375,1.658984925,185.72065064375,1.790608125Q185.66613034375,1.9222313149999999,185.62477394375,2.058564665Q185.58341764375,2.1948980049999998,185.55562374375,2.3346284649999998Q185.52782914375,2.474358855,185.51386504375,2.616140785Q185.49990054375,2.757922695,185.49990084375,2.900390625Q185.49990054375,3.042858555,185.51386474375,3.184640465Q185.52782914375,3.326422395,185.55562354375,3.4661527850000002Q185.58341744375,3.6058832450000002,185.62477374375,3.742216585Q185.66613004375,3.878549935,185.72065044375,4.010173125Q185.77517034375,4.141796325,185.84232924375,4.267441725Q185.90948824375,4.393087125,185.98863934375,4.511544925Q186.06779024375,4.630002725,186.15817114375,4.740131925Q186.24855184375,4.850261225,186.34929184375,4.9510011249999994Q186.45003174375,5.0517411249999995,186.56016114375,5.142122025Q186.67029024375,5.232502725,186.78874814375,5.311653625Q186.90720584375,5.390804725000001,187.03285124375,5.457963425Q187.15849664375,5.525122425,187.29011984375,5.579642325Q187.42174303375,5.634162625,187.55807638375,5.675519025Q187.69440972375,5.716875325,187.83414018375,5.744669225Q187.97387057375,5.772463825,188.11565250375,5.786427925Q188.25743441375,5.800392125,188.39990234375,5.800392125ZM188.89990234375,8.004557625L188.89990234375,5.962890625L187.89990234375,5.962890625L187.89990234375,8.004557625L188.89990234375,8.004557625ZM188.89990234375,12.087890625L188.89990234375,10.046224125L187.89990234375,10.046224125L187.89990234375,12.087890625L188.89990234375,12.087890625ZM188.89990234375,16.171224625L188.89990234375,14.129557625L187.89990234375,14.129557625L187.89990234375,16.171224625L188.89990234375,16.171224625ZM188.89990234375,20.254558625L188.89990234375,18.212891624999997L187.89990234375,18.212891624999997L187.89990234375,20.254558625L188.89990234375,20.254558625ZM188.89990234375,24.337890625L188.89990234375,22.296224625L187.89990234375,22.296224625L187.89990234375,24.337890625L188.89990234375,24.337890625ZM188.89990234375,28.421224625L188.89990234375,26.379558625L187.89990234375,26.379558625L187.89990234375,28.421224625L188.89990234375,28.421224625ZM188.89990234375,32.504558625L188.89990234375,30.462892625L187.89990234375,30.462892625L187.89990234375,32.504558625L188.89990234375,32.504558625ZM188.89990234375,36.587890625L188.89990234375,34.546224625L187.89990234375,34.546224625L187.89990234375,36.587890625L188.89990234375,36.587890625ZM188.89990234375,40.671226625L188.89990234375,38.629558625L187.89990234375,38.629558625L187.89990234375,40.671226625L188.89990234375,40.671226625ZM188.89990234375,44.754558625L188.89990234375,42.712890625L187.89990234375,42.712890625L187.89990234375,44.754558625L188.89990234375,44.754558625ZM188.89990234375,48.837890625L188.89990234375,46.796226625L187.89990234375,46.796226625L187.89990234375,48.837890625L188.89990234375,48.837890625ZM188.89990234375,51.900390625L188.89990234375,50.879558625L187.89990234375,50.879558625L187.89990234375,51.900390625L188.89990234375,51.900390625Z" fill-rule="evenodd" fill="#67E4F4" fill-opacity="1"/></svg>
\ No newline at end of file
diff --git a/src/assets/system/baogong.svg b/src/assets/system/baogong.svg
new file mode 100644
index 0000000..daddd9c
--- /dev/null
+++ b/src/assets/system/baogong.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="62.90625" viewBox="0 0 59.5 62.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35815"><rect x="14" y="0" width="30" height="30" rx="0"/></clipPath></defs><path d="M1.204819142818451,26.096379L28.69276814281845,42.307213000000004L58.29516614281845,26.096379L28.69276814281845,12L1.204819142818451,26.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,26.065809L28.43877614281845,42.737896L28.68361514281845,42.882288L59.39288714281845,26.065309L28.684561142818453,11.44229615L0.16870074281845082,26.065809ZM28.70192014281845,41.732138L2.240937742818451,26.12695L28.70097614281845,12.55770427L57.19744514281845,26.127449L28.70192014281845,41.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,26.80120277404785L0.5,45.83131777404785L28.692764,62.042150774047855L28.692764,43.01204077404785L0.5,26.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,62.906411774047854L29.192764,42.722777774047856L28.941999,42.578587774047854L-5.999999996841865e-8,25.93693918404785L0,46.12058077404785L29.192764,62.906411774047854ZM28.192764,43.30130377404785L28.192764,61.17788877404785L1,45.542054774047855L1,27.66546636404785L28.192764,43.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,26.80120277404785L59,45.12649377404785L88.5,62.00042177404785L88.5,42.26604677404785L59,26.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,62.86243677404785L89,41.96362277404785L88.732151,41.82320777404785L58.50000003,25.97454732404785L58.50000003,45.41651177404785L89,62.86243677404785ZM88,42.568469774047855L88,61.13840677404785L59.5,44.836475774047855L59.5,27.627858224047852L88,42.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35815)"><path d="M17.76220696875,10.375L20.44873021875,10.375C20.92626951875,10.375,21.35986331875,9.985351999999999,21.35986331875,9.5078125C21.35986331875,9.030272499999999,20.97021481875,8.640624500000001,20.44873021875,8.640624500000001L17.76220696875,8.640624500000001C17.28466784875,8.640624500000001,16.85107421875,9.030272499999999,16.85107421875,9.5078125C16.85400386155,9.985351099999999,17.24365239875,10.375,17.76220696875,10.375ZM21.31591801875,20.51465C21.31591801875,20.037111,20.92626951875,19.647463,20.40478491875,19.647463L17.76220696875,19.647463C17.28466784875,19.647463,16.85107421875,20.037111,16.85107421875,20.51465C16.85107421875,20.992188,17.24072274875,21.381836,17.76220696875,21.381836L20.44873021875,21.381836C20.92626951875,21.381836,21.31591801875,20.992188,21.31591801875,20.51465ZM33.66748021875,20.643555L33.27783221875,20.382812C32.88818321875,20.078123,32.80029421875,19.515625,33.10498021875,19.125975L35.31396521875,16.395505L34.61962921875,15.833005L32.36669821875,18.563475C32.062010218750004,18.953123,31.49951221875,18.997068,31.109861218749998,18.692382000000002L30.764159218750002,18.431639L40.90380821875,6.2119140999999996C40.90380821875,4.2607421,39.30126921875,2.875,37.35009721875,2.875L21.74951221875,2.875C20.27587891875,2.875,18.97509791875,3.69824213,18.97509791875,5.171875L18.97509791875,6.90625L20.31982471875,6.90625C21.48877001875,6.90625,22.66064501875,7.9023438,22.66064501875,9.203125C22.66064501875,10.5039062,21.74951221875,11.5,20.45166061875,11.5L18.97802781875,11.5L18.97802781875,17.87207L20.32275461875,17.87207C21.49170021875,17.87207,22.66357521875,18.868164999999998,22.66357521875,20.168947C22.66357521875,21.469728,21.75244191875,22.465822,20.45459051875,22.465822L18.98095731875,22.465822L18.98095731875,24.5459C18.98095731875,26.019533,20.28173831875,27.103518,21.755372018750002,27.103518L38.22314421875,27.103518C39.69677721875,27.103518,41.12646521875,26.019533,41.12646521875,24.5459L41.12646521875,11.6728516L33.66748021875,20.643555ZM30.37451121875,22.333984C30.069822218749998,22.462893,29.29052621875,22.205078,29.33447121875,21.856445L30.24560321875,19.125977L33.02001721875,21.334963L30.37451121875,22.333984Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/baogongtaizhang.svg b/src/assets/system/baogongtaizhang.svg
new file mode 100644
index 0000000..d668f96
--- /dev/null
+++ b/src/assets/system/baogongtaizhang.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35824"><rect x="12.5" y="0" width="34.000003814697266" height="34" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35824)"><path d="M30.364756937499997,25.1723935L31.5122539375,23.7415655C30.7614219375,22.8348995,30.3080869375,21.6732315,30.3080869375,20.4265635C30.3080869375,17.5648925,32.6455929375,15.2273945,35.5214249375,15.2273945C37.0655859375,15.2273945,38.4680879375,15.9073935,39.417254937500005,16.969893499999998L39.417254937500005,4.4890625C39.417254937500005,3.55406219,38.652257937499996,2.7890625,37.7172569375,2.7890625L19.3005861375,2.7890625C18.3655856875,2.7890625,17.6005859375,3.55406243,17.6005859375,4.4890625L17.6005859375,26.5890575C17.6005859375,27.5240575,18.3655862175,28.2890625,19.3005861375,28.2890625L27.4747514375,28.2890625C27.8289189375,26.8015635,28.933920937499998,25.6115625,30.364756937499997,25.1723935ZM23.706419037499998,6.7982302L33.339754937500004,6.7982302C33.963088937500004,6.7982302,34.4730839375,7.3082304,34.4730839375,7.9315624C34.4730839375,8.5548954,33.963088937500004,9.0648956,33.339754937500004,9.0648956L23.7064218375,9.0648956C23.083087437499998,9.0648956,22.5730877375,8.5548968,22.5730877375,7.9315639C22.5730877375,7.3082304,23.0689206375,6.7982302,23.706419037499998,6.7982302ZM23.706419037499998,11.8982306L33.339754937500004,11.8982306C33.963088937500004,11.8982306,34.4730839375,12.4082298,34.4730839375,13.0315625C34.4730839375,13.6548975,33.963088937500004,14.1648965,33.339754937500004,14.1648965L23.7064218375,14.1648965C23.083087437499998,14.1648965,22.5730877375,13.6548975,22.5730877375,13.0315625C22.5730877375,12.4082298,23.0689206375,11.8982306,23.706419037499998,11.8982306ZM22.5730872375,18.1315635C22.5730872375,17.5082325,23.0830865375,16.9982295,23.706419037499998,16.9982295L27.6730879375,16.9982295C28.296420937500002,16.9982295,28.8064209375,17.5082265,28.8064209375,18.1315575C28.8064209375,18.754890500000002,28.296420937500002,19.2648965,27.6730879375,19.2648965L23.706419037499998,19.2648965C23.0689187375,19.2648965,22.5730872375,18.7548925,22.5730872375,18.1315635Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M39.43028168125,31.168883875L31.596113881249998,31.168883875C30.51944607125,31.168883875,29.64111328125,30.290549875,29.64111328125,29.213882875C29.64111328125,28.137214874999998,30.51944607125,27.258882475,31.596113881249998,27.258882475L34.81194638125,23.263881675C33.52277968125,22.952215175,32.57361388125,21.804714675,32.57361388125,20.430547675C32.57361388125,18.815546775,33.89111378125,17.498046875,35.52027988125,17.498046875C37.14944748125,17.498046875,38.46694468125,18.815546775,38.46694468125,20.430547675C38.46694468125,21.804714675,37.51777978125,22.952215175,36.22861288125,23.263881675L39.44444748125,27.258882475C40.52111428125,27.258882475,41.39944628125,28.137214874999998,41.39944628125,29.213882875C41.39944628125,30.290549875,40.52111428125,31.168883875,39.43028168125,31.168883875Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/caigoubaobiao.svg b/src/assets/system/caigoubaobiao.svg
new file mode 100644
index 0000000..164efe4
--- /dev/null
+++ b/src/assets/system/caigoubaobiao.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="60.90625" viewBox="0 0 59.5 60.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35725"><rect x="16.5" y="0" width="27" height="27" rx="0"/></clipPath></defs><path d="M1.204819142818451,24.096379L28.69276814281845,40.307213000000004L58.29516614281845,24.096379L28.69276814281845,10L1.204819142818451,24.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,24.065809L28.43877614281845,40.737896L28.68361514281845,40.882288L59.39288714281845,24.065309L28.684561142818453,9.44229615L0.16870074281845082,24.065809ZM28.70192014281845,39.732138L2.240937742818451,24.12695L28.70097614281845,10.55770427L57.19744514281845,24.127449L28.70192014281845,39.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,24.80120277404785L0.5,43.83131777404785L28.692764,60.042150774047855L28.692764,41.01204077404785L0.5,24.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,60.906411774047854L29.192764,40.722777774047856L28.941999,40.578587774047854L-5.999999996841865e-8,23.93693918404785L0,44.12058077404785L29.192764,60.906411774047854ZM28.192764,41.30130377404785L28.192764,59.17788877404785L1,43.542054774047855L1,25.66546636404785L28.192764,41.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,24.80120277404785L59,43.12649377404785L88.5,60.00042177404785L88.5,40.26604677404785L59,24.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,60.86243677404785L89,39.96362277404785L88.732151,39.82320777404785L58.50000003,23.97454732404785L58.50000003,43.41651177404785L89,60.86243677404785ZM88,40.568469774047855L88,59.13840677404785L59.5,42.836475774047855L59.5,25.627858224047852L88,40.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35725)"><path d="M18.2960399,19.401375L41.644558,19.401375C42.208858,19.348387,42.619251,18.840738,42.553804,18.276657L42.553804,1.1247175C42.619251,0.56063783,42.208858,0.052988321,41.644558,0L18.2960399,0C17.7349908,0.059026856,17.3279168,0.56256902,17.38679475,1.1247175L17.38679475,18.321648C17.34767658,18.868998,17.7509638,19.347902,18.2960399,19.401375ZM34.628777,3.554107L39.46686,3.554107C39.800758,3.5838315,40.052634999999995,3.8708398,40.039347,4.2064433C40.05232,4.5398693,39.799026,4.8235159,39.46686,4.8475323L34.628777,4.8475323C34.305557,4.8122854,34.065008,4.5319171,34.078739,4.2064433C34.064516,3.8786428,34.303837,3.5948114,34.628777,3.554107ZM34.628777,6.7145634L39.46686,6.7145634C39.799026,6.7385783,40.05232,7.0222259,40.039347,7.3556523C40.052634999999995,7.6912551,39.800758,7.9782634,39.46686,8.007988L34.628777,8.007988C34.303835,7.9672842,34.064516,7.6834526,34.078739,7.3556523C34.082552,7.0430193,34.31859,6.7823882,34.628777,6.7483044L34.628777,6.7145634ZM34.628777,9.976243L39.46686,9.976243C39.797287,9.9944181,40.052101,10.27476,40.039347,10.606086C40.052634999999995,10.941689,39.800758,11.228697,39.46686,11.258421L34.628777,11.258421C34.303835,11.217718,34.064516,10.933886,34.078739,10.606086C34.06542,10.282872,34.307281,10.005919,34.628777,9.976243ZM26.322093000000002,3.554107C26.4553547,3.4100845,26.682741,3.4100845,26.816004,3.554107L29.061052,6.4333839L28.163033,6.4333839L26.277193099999998,4.0827241C26.1536102,3.9213576,26.1730719,3.6922283,26.322093000000002,3.554107ZM21.5176859,6.4783726L23.7627358,3.610343C23.8959985,3.4663203,24.123385,3.4663203,24.2566466,3.610343C24.392286300000002,3.7603433,24.392286300000002,3.9889596,24.2566466,4.1389604L22.382030999999998,6.5346084L21.4840107,6.5346084L21.5176859,6.4783726ZM20.0247281,7.2769227L30.542787,7.2769227C30.746521,7.2944822,30.902746,7.4656887,30.901995,7.6705737C30.908293999999998,7.8693829,30.75244,8.0356159,30.554012,8.0417309L30.206031,8.0417309L29.083506,14.126452C29.001704,14.465208,28.701669000000003,14.705706,28.353864,14.711306L22.191201200000002,14.711306C21.841960399999998,14.709742,21.5397458,14.467498,21.4615598,14.126452L20.3390353,8.0304842L20.0247283,8.0304842C19.8418345,7.9987054,19.7082825,7.8396893,19.7082825,7.6537042C19.7082825,7.4677172,19.8418345,7.308702,20.0247281,7.2769227ZM23.482104800000002,12.833026C23.6805339,12.826911,23.8363857,12.660679,23.8300877,12.461868L23.8300877,9.4588737C23.8300886,9.2663126,23.6742921,9.1102114,23.482105699999998,9.1102114C23.2899208,9.1102114,23.1341233,9.2663126,23.1341228,9.4588737L23.1341228,12.461869C23.1278248,12.660679,23.2836776,12.826911,23.482104800000002,12.833026ZM42.991589000000005,20.919744L16.94900998,20.919744C16.69082925,20.919744,16.5,21.549585,16.5,21.853258C16.5,22.156931,16.71327975,22.798021,16.94900998,22.798021L23.897439,22.798021L22.1126246,25.868502C21.9664502,26.204638,22.0473838,26.596581,22.3146791,26.847004C22.6148901,27.015087,22.9897623,26.964701,23.2351494,26.723286L25.480198899999998,22.798023L34.898184,22.798023L36.952404,26.71204C37.138704000000004,26.994795,37.512434,27.083481,37.805521,26.91449C38.056238,26.717609,38.147625000000005,26.378819,38.030027000000004,26.082197L36.335014,22.775528L43.070164,22.775528C43.328342,22.775528,43.519172999999995,22.134438,43.519172999999995,21.830765C43.519172999999995,21.527092,43.249767,20.919744,42.991589000000005,20.919744ZM27.141537,12.833026C27.339965,12.826911,27.495817000000002,12.660679,27.489519,12.461868L27.489519,9.4588737C27.48952,9.2663126,27.333722,9.1102114,27.141537,9.1102114C26.949351,9.1102114,26.793554,9.2663126,26.793553,9.4588737L26.793553,12.461869C26.787255000000002,12.66068,26.943109,12.826912,27.141537,12.833026ZM25.311821000000002,12.833026C25.5102491,12.826911,25.6661024,12.660679,25.6598034,12.461868L25.6598034,9.4588737C25.6598034,9.2663126,25.5040064,9.1102114,25.311821899999998,9.1102114C25.119635600000002,9.1102114,24.963838600000003,9.2663126,24.963838600000003,9.4588737L24.963838600000003,12.461869C24.957541499999998,12.660679,25.1133938,12.826911,25.311821000000002,12.833026Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/caigoupeizhi.svg b/src/assets/system/caigoupeizhi.svg
new file mode 100644
index 0000000..56cbd1b
--- /dev/null
+++ b/src/assets/system/caigoupeizhi.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="89.5693359375" height="96" viewBox="0 0 89.5693359375 96"><defs><filter id="master_svg0_143_35986" filterUnits="objectBoundingBox" color-interpolation-filters="sRGB" x="-0.6666666666666666" y="-0.6666666666666666" width="2.3333333333333335" height="2.3333333333333335"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="0" dx="0"/><feGaussianBlur stdDeviation="8"/><feColorMatrix type="matrix" values="0 0 0 0 0.12895070016384125 0 0 0 0 0.29307788610458374 0 0 0 0 0.9107142686843872 0 0 0 0.5299999713897705 0"/><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><clipPath id="master_svg1_143_35990"><rect x="29.78466796875" y="36" width="27" height="26" rx="0"/></clipPath></defs><path d="M44.78466796875,24.000000000000004L65.56927496875,36L65.56927496875,60L44.78466796875,72L24.00005836875,60L24.00005796875,36.000001L44.78466796875,24.000000000000004Z" fill="#496FFE" fill-opacity="1" filter="url(#master_svg0_143_35986)"/><g clip-path="url(#master_svg1_143_35990)"><path d="M38.936583953125,55.370050875000004C37.733132853125,55.370050875000004,36.759445153125,56.318214874999995,36.759445153125,57.477092875C36.759445153125,58.635968875,37.733132853125,59.584132875,38.936583953125,59.584132875C40.140035153125,59.584132875,41.124664353125,58.635968875,41.124664353125,57.477092875C41.124664353125,56.318214874999995,40.140035153125,55.370050875000004,38.936583953125,55.370050875000004ZM32.372314453125,38.513671875L32.372314453125,40.620712975000004L34.560395253125,40.620712975000004L38.498940453125,48.616956875L37.021982153125,51.198091875C36.846930553125,51.493078875,36.748476053125,51.840727875,36.748476053125,52.209476875C36.748476053125,53.368354875,37.733105153125,54.316516875,38.936556353125,54.316516875L52.065095453125,54.316516875L52.065095453125,52.209476875L39.396084753125,52.209476875C39.242918053125,52.209476875,39.122578653125004,52.093592875,39.122578653125004,51.946099875L39.155405553125,51.819678875L40.140035153125,50.102435875L48.290658453125,50.102435875C49.111179453125004,50.102435875,49.833271453125,49.670489875,50.205232453125,49.017316875L54.121921453125,42.179950975C54.209434453125,42.032456875,54.253204453125,41.853351075,54.253204453125,41.674271375000004C54.253204453125,41.094831975,53.760877453125005,40.620738275,53.159151453125,40.620738275L36.978240053125,40.620738275L35.949840753125,38.513697251678L32.372314453125,38.513671875ZM49.877016453125,55.370050875000004C48.673564453124996,55.370050875000004,47.699876453125,56.318214874999995,47.699876453125,57.477092875C47.699876453125,58.635968875,48.673564453124996,59.584132875,49.877016453125,59.584132875C51.080467453124996,59.584132875,52.065095453125,58.635968875,52.065095453125,57.477092875C52.065095453125,56.318214874999995,51.080467453124996,55.370050875000004,49.877016453125,55.370050875000004Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/caigoutaizhang.svg b/src/assets/system/caigoutaizhang.svg
new file mode 100644
index 0000000..a6cf257
--- /dev/null
+++ b/src/assets/system/caigoutaizhang.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="58.90625" viewBox="0 0 59.5 58.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35697"><rect x="18" y="0" width="23" height="23" rx="0"/></clipPath></defs><path d="M1.204819142818451,22.096379L28.69276814281845,38.307213000000004L58.29516614281845,22.096379L28.69276814281845,8L1.204819142818451,22.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,22.065809L28.43877614281845,38.737896L28.68361514281845,38.882288L59.39288714281845,22.065309L28.684561142818453,7.44229615L0.16870074281845082,22.065809ZM28.70192014281845,37.732138L2.240937742818451,22.12695L28.70097614281845,8.55770427L57.19744514281845,22.127449L28.70192014281845,37.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,22.80120277404785L0.5,41.83131777404785L28.692764,58.042150774047855L28.692764,39.01204077404785L0.5,22.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,58.906411774047854L29.192764,38.722777774047856L28.941999,38.578587774047854L-5.999999996841865e-8,21.93693918404785L0,42.12058077404785L29.192764,58.906411774047854ZM28.192764,39.30130377404785L28.192764,57.17788877404785L1,41.542054774047855L1,23.66546636404785L28.192764,39.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,22.80120277404785L59,41.12649377404785L88.5,58.00042177404785L88.5,38.26604677404785L59,22.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,58.86243677404785L89,37.96362277404785L88.732151,37.82320777404785L58.50000003,21.97454732404785L58.50000003,41.41651177404785L89,58.86243677404785ZM88,38.568469774047855L88,57.13840677404785L59.5,40.836475774047855L59.5,23.627858224047852L88,38.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35697)"><path d="M40.99999,13.416666C41.00259,13.384775,41.00259,13.352724,40.99999,13.320833C41.002308,13.287331,41.002308,13.253709,40.99999,13.220208L38.278324,1.4375043C38.059512999999995,0.59004736,37.294407,-0.0015301184,36.419159,0.0000048838556L22.580831500000002,0.0000048838556C21.6940155,0.000090580994,20.9230585,0.60853219,20.7168739,1.4710461L18.023958318,13.20104C18.021638496,13.234541,18.021638496,13.268163,18.023958318,13.301665C18.012162563,13.339105,18.0041366061,13.377629,18,13.416666L18,21.08333C18,22.141876,18.8581205,22.999996,19.916665899999998,22.999996L39.083326,22.999996C40.14187,22.999996,40.99999,22.141876,40.999992,21.08333L40.99999,13.416666ZM22.580831500000002,1.9166707L36.414367999999996,1.9166707L38.838949,12.458332L34.770826,12.458332C34.241554,12.458332,33.812493,12.887392,33.812494,13.416665L33.812494,15.812497L25.1874971,15.812497L25.1874971,13.416665C25.1874971,12.887392,24.7584372,12.458332,24.2291641,12.458332L20.1610408,12.458332L22.580831500000002,1.9166707ZM22.7916646,9.583334C22.7916641,9.0540609,23.2207246,8.625001,23.7499971,8.625001L35.249992,8.625001C35.778898,8.6255178,36.207388,9.0544252,36.207388,9.583333C36.207388,10.112241,35.778898,10.541149,35.249992,10.541666L23.7499976,10.541666C23.2207251,10.541666,22.7916651,10.112606,22.7916646,9.583334ZM24.2291641,5.2708354C24.2291651,4.7415628,24.6582246,4.3125029,25.1874971,4.3125029L33.812494,4.3125029C34.341768,4.3125029,34.770828,4.7415628,34.770828,5.2708359C34.770828,5.8001089,34.341768,6.2291689,33.812494,6.2291689L25.1874971,6.2291689C24.6582246,6.2291689,24.2291641,5.8001089,24.2291641,5.2708354Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/caigoutuihuo.svg b/src/assets/system/caigoutuihuo.svg
new file mode 100644
index 0000000..6b2df38
--- /dev/null
+++ b/src/assets/system/caigoutuihuo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35706"><rect x="12.5" y="0" width="34.000003814697266" height="34" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35706)"><path d="M16.5,17.5C16.5,10.0479994,22.547999400000002,4,30,4C37.452,4,43.5,10.0479999,43.5,17.5C43.5,24.952,37.452,31,30,31C22.547999400000002,31,16.5,24.952,16.5,17.5ZM25.2099543,11.2335448C25.056938199999998,11.1590481,24.889226,11.1196365,24.719044699999998,11.118180800000001C24.4576359,11.118180800000001,24.2600451,11.2102261,24.1250448,11.3930907C23.965045,11.566371,23.8772473,11.7942061,23.8795905,12.0300455C23.8795905,12.2902269,23.9716368,12.4779997,24.1545,12.5945911C24.1946921,12.6422205,24.2439098,12.681428,24.2993183,12.7099552C24.812317800000002,13.0376368,25.309363400000002,13.3910923,25.789226499999998,13.7654095C25.875135399999998,13.8550005,25.9782276,13.9249544,26.0935907,13.9691372C26.1991358,14.026819,26.3292265,14.055046,26.4838629,14.055046C26.763681,14.046455,26.98459,13.9114552,27.149045,13.6500463C27.266969,13.4697084,27.327709,13.2579737,27.323318,13.0425463C27.319848,12.8229294,27.219160000000002,12.6161594,27.048409,12.4780006C26.492164600000002,11.9851937,25.874157,11.5668635,25.2099543,11.2335448ZM34.731136,11.2924538L29.478408,11.2924538C28.610726,11.2924538,28.177500000000002,11.648363100000001,28.177500000000002,12.3626356L28.177500000000002,19.944725C28.176272,20.042908,28.18609,20.138634,28.205727,20.234362C28.070726,20.349726,28.003225999999998,20.553452,28.003225999999998,20.841862C28.013044999999998,21.11186,28.070726,21.309452,28.176271,21.434633C28.292863,21.569633,28.500272000000002,21.637133,28.798499,21.637133C29.464908,21.637133,30.404999,21.487406,31.621223999999998,21.189178C31.861770999999997,21.140087,32.026225,21.059088,32.112133,20.943726C32.243669,20.79126,32.310976,20.593739,32.299906,20.392679C32.299906,20.17177,32.252043,19.997498,32.156316000000004,19.872315999999998C32.033533,19.737026,31.860347,19.658427,31.677678999999998,19.655088C31.157317,19.732407000000002,30.632043,19.804814999999998,30.100635,19.872315999999998L30.100635,17.455816L34.673452,17.455816C34.759361,17.455816,34.837906000000004,17.450908,34.905409,17.441088999999998C34.893873,17.462564,34.878954,17.482039999999998,34.861225000000005,17.498772000000002C34.521141,17.859035,34.158792,18.197606999999998,33.776316,18.512499C33.275587,18.193407999999998,32.749089999999995,17.895182,32.199270999999996,17.615363000000002C32.035903,17.54202,31.857872,17.507168,31.678905999999998,17.513499C31.417496999999997,17.513499,31.21009,17.590817,31.056679000000003,17.745455C30.902043,17.880454999999998,30.823497,18.082954,30.823497,18.352954C30.828218,18.505149,30.890499,18.649888,30.997768,18.757953999999998C31.051738999999998,18.809553,31.116205,18.848902000000002,31.186768999999998,18.873317999999998C32.266769,19.520091999999998,33.48177,20.369364,34.831768,21.419909C35.01586,21.604,35.246586,21.694817,35.527632,21.694817C35.766716,21.695692,35.992073000000005,21.583241,36.135132,21.39168C36.289076,21.227486,36.371994,21.009171,36.36586,20.78418C36.362389,20.564564,36.261703,20.357796,36.090952,20.219635C35.802544,19.968045,35.503088000000005,19.728727,35.195042,19.495544000000002C35.705585,19.197316999999998,36.115498,18.873317,36.424769999999995,18.526C36.533014,18.357194,36.592867,18.161924,36.597816,17.961454C36.611883,17.651581,36.456854,17.358437000000002,36.192816,17.195636999999998C36.041086,17.073852000000002,35.852282,17.007597,35.657722,17.007863999999998C35.753451999999996,16.853229,35.802544,16.645819,35.802544,16.385637L35.802544,12.3626366C35.802544,11.6483641,35.445406,11.2924547,34.731136,11.2924538ZM33.878181,13.664773L30.100636,13.664773L30.100636,13.1002264C30.100636,12.9468174,30.192681999999998,12.8695002,30.375545000000002,12.8695002L33.603271,12.8695002C33.786135,12.8695002,33.87818,12.9468174,33.87818,13.1002264L33.878181,13.664773ZM33.603271,15.864044L30.100636,15.864044L30.100636,15.02459L33.878181,15.02459L33.878181,15.646818C33.878181,15.791636,33.786135,15.864044,33.603271,15.864044ZM25.6296825,15.473772L24.2698641,15.473772C23.9507732,15.473772,23.724954099999998,15.546182,23.5899544,15.690999C23.4451365,15.844409,23.3727274,16.076363999999998,23.3727274,16.385635C23.3727274,16.723136,23.4402275,16.950181999999998,23.5752277,17.065544C23.6905913,17.248407,23.9225464,17.340454,24.2698641,17.340454L24.834409700000002,17.340454C24.9497738,17.340454,25.0074549,17.398136,25.0074549,17.513499L25.0074549,21.637135C25.0074549,21.714453,24.9546824,21.786861,24.847909899999998,21.854361C24.4527292,22.139252,24.0315437,22.386209,23.5899544,22.591951C23.4451365,22.698725,23.3727274,22.814087,23.3727274,22.939268C23.3771172,23.0942,23.4117188,23.246782,23.4745903,23.388451C23.6476359,23.697725,23.835408700000002,23.943178,24.0391369,24.126041C24.1385455,24.200905,24.260046,24.242634,24.3852277,24.242634C24.5752201,24.217705,24.7579012,24.153328,24.921546,24.053633C25.228364,23.907587,25.5204544,23.728407,25.7892284,23.518543C25.8849545,23.432632,25.9622736,23.388451,26.0211821,23.388451C26.132575,23.390455,26.2397785,23.431255,26.3243189,23.503817C27.153955,23.842543,28.572682999999998,24.010681,30.578046,24.010681L35.97559,24.010681C36.313091,23.991043,36.545048,23.900225,36.670227,23.735771C36.805227,23.61059,36.872726,23.363907,36.872726,22.99818C36.872726,22.67909,36.790501,22.453272,36.627274,22.317045C36.492271,22.201681,36.294682,22.144001,36.033272,22.144001C35.368092000000004,22.144001,34.566679,22.163635,33.6315,22.201681C32.551497999999995,22.239727,31.379454000000003,22.259361,30.115364,22.259361C28.996092,22.259361,28.114910000000002,22.191862,27.468138,22.056862C27.071728999999998,21.979544,26.874138000000002,21.8605,26.874138000000002,21.694817L26.874138000000002,16.601637C26.874138000000002,15.849319,26.4593201,15.472546,25.6296825,15.472546L25.6296825,15.473772Z" fill="#F56127" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/cangchuwuliu.svg b/src/assets/system/cangchuwuliu.svg
new file mode 100644
index 0000000..47ee791
--- /dev/null
+++ b/src/assets/system/cangchuwuliu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="89.5693359375" height="96" viewBox="0 0 89.5693359375 96"><defs><filter id="master_svg0_143_36004" filterUnits="objectBoundingBox" color-interpolation-filters="sRGB" x="-0.6666666666666666" y="-0.6666666666666666" width="2.3333333333333335" height="2.3333333333333335"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="0" dx="0"/><feGaussianBlur stdDeviation="8"/><feColorMatrix type="matrix" values="0 0 0 0 0.12895070016384125 0 0 0 0 0.29307788610458374 0 0 0 0 0.9107142686843872 0 0 0 0.5299999713897705 0"/><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><clipPath id="master_svg1_143_36007"><rect x="31.78466796875" y="34" width="27" height="27" rx="0"/></clipPath></defs><path d="M44.78466796875,24.000000000000004L65.56927496875,36L65.56927496875,60L44.78466796875,72L24.00005836875,60L24.00005796875,36.000001L44.78466796875,24.000000000000004Z" fill="#496FFE" fill-opacity="1" filter="url(#master_svg0_143_36004)"/><g clip-path="url(#master_svg1_143_36007)"><path d="M34.169981631875,44.43093495L43.148853271875,36.70824987C44.265293171875,35.80037498,45.805637171875,35.80037498,46.867416171875,36.70824987L55.794290171875005,44.43024925C56.165541171875,44.71509455,56.272196171874995,45.22674945,56.112884171874995,45.68034455C55.964262171875,46.11337375,55.561472171874996,46.40785775,55.103762171875005,46.41812375L34.914511871875,46.41812375C34.456565501875,46.40813475,34.053443121875,46.113611750000004,33.904727786875,45.680370350000004C33.711113065875,45.25040145,33.818446621275,44.74454685,34.169981631875,44.43024925L34.169981631875,44.43093495ZM50.745292171875,58.40140375L39.270291771875,58.40140375C38.554649371875,58.40620775,37.873565171875,58.094085750000005,37.409981271875,57.54887375C36.908443671875,57.007405750000004,36.624764471875,56.29965575,36.613481471875,55.561685749999995L36.613481471875,47.15793475C36.613481471875,46.41880975,36.878761971875,45.68103025,37.409981771875,45.17006015C37.889231671875,44.65906425,38.579761471875,44.31818965,39.270291771875,44.31818965L50.745292171875,44.31818965C52.233667171875,44.31818965,53.403446171875004,45.56693935,53.403446171875004,47.15724975L53.403446171875004,55.61905875C53.403446171875004,56.35615575,53.136822171875,57.09527975,52.606947171875,57.60625075C52.121288171875,58.11399275,51.449251171875005,58.40124275,50.746635171875,58.40140375L50.745292171875,58.40140375ZM42.511636771875004,49.99765575L47.505977171875,49.99765575L47.505977171875,46.02190495L42.511636771875004,46.02190495L42.511636771875004,49.99765575ZM38.739731771875,55.73175275L43.734072671875,55.73175275L43.734072671875,51.75668875L38.739072771875,51.75668875L38.739072771875,55.73243875L38.739731771875,55.73175275ZM46.231571171875004,55.73175275L51.224542171875,55.73175275L51.224542171875,51.75668875L46.229543171875,51.75668875L46.229543171875,55.73243875L46.230887171875,55.73243875L46.231571171875004,55.73175275Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/chanpinweihu.svg b/src/assets/system/chanpinweihu.svg
new file mode 100644
index 0000000..eeb2bd4
--- /dev/null
+++ b/src/assets/system/chanpinweihu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35548"><rect x="12.5" y="0" width="34.000003814697266" height="34" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35548)"><path d="M28.938397000000002,16.1305256C34.699799999999996,16.1305256,39.376799,14.6742373,39.376799,12.8673234L39.376799,10.2632031C39.376799,8.4562893,34.699799999999996,7,28.938399,7C23.1769919,7,18.5,8.4562893,18.5,10.2632031L18.5,12.8673234C18.50033210215,14.663614299999999,23.1666999,16.1305256,28.938397000000002,16.1305256ZM44.276911,18.339863C44.276911,18.339863,42.873419,19.757969,42.724998,19.899082C42.583887000000004,20.040194,42.093805,20.025255,42.093805,20.025255C42.093805,20.025255,40.809180999999995,19.884143,40.623573,19.698537C40.437962999999996,19.512926,40.311792,18.168866,40.311792,18.168866C40.311792,18.168866,40.274605,17.797656,40.460212999999996,17.612047L42.019760000000005,16.0528297C42.019760000000005,16.0528297,42.198063000000005,15.8522825,41.93808,15.8522825C41.113977,15.8522825,39.443195,15.8668947,38.633708999999996,16.6763859L38.470348,16.8397465C37.512438,17.804958,37.453001,19.25295,38.225306,20.292540000000002L33.487556,24.584711C33.108707,24.963554,32.611326,25.988201,33.487556,26.8568C34.363788,27.73303,35.388435,27.235645,35.759642,26.8568C35.759642,26.8568,40.04418,22.119374999999998,40.036541,22.127012L40.043846,22.119711000000002C41.083433,22.899317,42.524117000000004,22.839883,43.489328,21.874662999999998L43.652695,21.711306999999998C44.439938999999995,20.924397,44.462188999999995,19.268223,44.462188999999995,18.429181C44.477458999999996,18.169201,44.276911,18.339866,44.276911,18.339863ZM34.749937,26.381657C34.311662,26.381657,33.955388,26.025389,33.955388,25.587109C33.955388,25.148829,34.311662,24.792561,34.749937,24.792561C35.187893,24.792561,35.544493,25.148829,35.544493,25.587109C35.544493,26.025389,35.188225,26.381657,34.749937,26.381657ZM32.203593,23.791481C31.17629,23.897064,30.078592999999998,23.954185,28.93873,23.954185C23.1773219,23.954185,18.510955571,22.497892999999998,18.510955571,20.690977L18.510955571,24.602633C18.510955571,26.398924,23.1773219,27.865839,28.949353000000002,27.865839C29.741912,27.865839,30.513885000000002,27.838282,31.256308,27.78582C31.17955,27.72587,31.106586,27.661226,31.037833,27.592243C29.979317,26.542686,30.580291000000003,25.304878,31.037833,24.847677L32.203593,23.791481Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M28.938066,21.99160385C31.111878,21.99160385,33.131288,21.78408435,34.803736,21.42881015L36.760063,19.65642495C36.216522,18.92463065,36.013987,18.02515645,36.169043,17.171171649999998C34.29274,17.73429655,31.744064,18.079609650000002,28.9384,18.079609650000002C23.176991,18.079609650000002,18.5,16.62332105,18.5,14.81640625L18.5,18.72806505C18.5,20.524686850000002,23.1663661,21.99159955,28.938066,21.99160385Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/chukuguanli.svg b/src/assets/system/chukuguanli.svg
new file mode 100644
index 0000000..6c8f0e9
--- /dev/null
+++ b/src/assets/system/chukuguanli.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="61.40625" viewBox="0 0 59.5 61.40625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35869"><rect x="17" y="0" width="27" height="27" rx="0"/></clipPath></defs><path d="M1.204819142818451,24.596379L28.69276814281845,40.807213000000004L58.29516614281845,24.596379L28.69276814281845,10.5L1.204819142818451,24.596379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,24.565809L28.43877614281845,41.237896L28.68361514281845,41.382288L59.39288714281845,24.565309L28.684561142818453,9.94229615L0.16870074281845082,24.565809ZM28.70192014281845,40.232138L2.240937742818451,24.62695L28.70097614281845,11.05770427L57.19744514281845,24.627449L28.70192014281845,40.232138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,25.30120277404785L0.5,44.33131777404785L28.692764,60.542150774047855L28.692764,41.51204077404785L0.5,25.30120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,61.406411774047854L29.192764,41.222777774047856L28.941999,41.078587774047854L-5.999999996841865e-8,24.43693918404785L0,44.62058077404785L29.192764,61.406411774047854ZM28.192764,41.80130377404785L28.192764,59.67788877404785L1,44.042054774047855L1,26.16546636404785L28.192764,41.80130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,25.30120277404785L59,43.62649377404785L88.5,60.50042177404785L88.5,40.76604677404785L59,25.30120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,61.36243677404785L89,40.46362277404785L88.732151,40.32320777404785L58.50000003,24.47454732404785L58.50000003,43.91651177404785L89,61.36243677404785ZM88,41.068469774047855L88,59.63840677404785L59.5,43.336475774047855L59.5,26.127858224047852L88,41.068469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35869)"><path d="M28.74531903125,25.271427375C28.39732503125,25.271427375,28.17831903125,25.178430375,28.058323031249998,24.938408375C27.938325031250002,24.695407375,27.99530403125,23.342403375,27.99530403125,22.961397375L27.99530403125,16.871421375C27.99530403125,15.995425375,28.04933103125,15.815414375,28.95533503125,15.815414375L35.04831503125,15.815414375C37.12731503125,15.815414375,36.860321031249995,15.620430375,36.860321031249995,18.101423375L36.860321031249995,24.161420375C36.860321031249995,25.046407375,36.84832403125,25.271427375,35.91832703125,25.268419375L33.90832903125,25.268419375C33.50032403125,22.874410375,30.17331203125,22.874410375,29.76530603125,25.268419375C29.42633003125,25.268419375,29.08432103125,25.268419375,28.74531903125,25.271427375ZM38.03632503125,25.259428375C38.50434303125,23.054420375,41.04832603125,22.958418375,41.93331303125,24.536441375C42.08932903125,24.812425375,42.07432703125,24.992408375,42.18831203125,25.259428375C43.53232603125,25.415416375,43.35833203125,24.809417375,43.35833203125,23.711410375C43.35833203125,23.033430375,43.448324031249996,21.659410375,43.22032403125,21.119409375C42.98331103125,20.549430375,42.24231303125,19.649438375,41.837313031250005,19.109439375C41.738329031250004,18.977419375,41.633308031249996,18.824411375,41.54031203125,18.716411375C41.32731803125,18.470407375,41.177315031250004,18.197424375,40.68830903125,18.179416375L37.958332031249995,18.188406375C37.52933703125,18.248417375,37.44831103125,18.575422375,37.44831103125,19.031419375L37.44831103125,24.494434375C37.44831103125,24.926408375,37.589323031250004,25.259428375,38.03632503125,25.259428375L38.03632503125,25.259428375ZM42.359327031250004,21.722429375C41.498308031250005,21.731420375,40.046320031250005,21.746423375,39.221317031249995,21.719423375L39.221317031249995,19.364412375C39.60532903125,19.340421375,40.21731003125,19.337413375,40.59831803125,19.358427375C40.88632403125,19.373432375,41.07529803125,19.751432375,41.28831903125,20.048429375C41.53730603125,20.396423375,42.34730703125,21.407421375,42.359327031250004,21.722429375ZM33.16432603125,25.262409375C33.35630803125,26.084432375,32.77432803125,26.753416375,32.13832803125,26.894432375C31.31329903125,27.080427375,30.66532503125,26.516433375,30.512316031250002,25.877424375C30.30831203125,25.031431375,30.88730903125,24.401416375,31.532330031249998,24.236408375C32.35730503125,24.029426375,33.01432103125,24.629411375,33.16432603125,25.262409375ZM41.46231803125,25.427415375C41.53730803125,26.228426375,40.94931803125,26.855409375,40.23832703125,26.918424375C39.44032503125,26.993415375,38.81331203125,26.399412375,38.75332603125,25.676426375C38.68432403125,24.866426375,39.28133003125,24.281417375,40.00431803125,24.209406375C40.77231603125,24.134418375,41.39632203125,24.737409375,41.46231803125,25.427415375ZM17.46533203125,6.377412775L17.46533203125,26.912439375L22.15431403125,26.912439375L22.15431403125,10.127406375L38.62431103125,10.127406375L38.62431103125,13.475406375L43.313322031249996,13.475406375L43.313322031249996,6.437397475C43.313322031249996,6.185406175,42.82431403125,6.050406475,42.62632403125,5.960415375L41.40832103125,5.384397475C41.23732903125,5.309382875,41.153320031250004,5.246392275,40.98233003125,5.171403375C40.667322031249995,5.030391675,40.46331803125,4.928403875,40.14833603125,4.790397675C38.65732603125,4.127394675,36.75232103125,3.122409775,35.270327031250005,2.486407075L30.752310031249998,0.347395155C29.94832203125,-0.042601921,30.131310031250003,-0.04560776800000001,29.51632203125,0.230403925L25.47230913125,2.273412675C25.19932893125,2.411418875,24.938320131250002,2.519418475,24.650311931250002,2.669421475L18.20933491125,5.903409975C18.03531152125,5.993427275,17.465332081541415,6.149415475,17.46533203125,6.377412775ZM23.54033093125,19.565410375L26.42632583125,19.565410375L26.42632583125,22.061432375L23.54033093125,22.061432375L23.54033093125,19.565410375ZM23.54033093125,16.235420375L26.42632583125,16.235420375L26.42632583125,18.677415375L23.54033093125,18.677415375L23.54033093125,16.235420375L23.54033093125,16.235420375Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/fahuotaizhang.svg b/src/assets/system/fahuotaizhang.svg
new file mode 100644
index 0000000..67e1b3f
--- /dev/null
+++ b/src/assets/system/fahuotaizhang.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="62.90625" viewBox="0 0 59.5 62.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35626"><rect x="15" y="0" width="29" height="29" rx="0"/></clipPath></defs><path d="M1.204819142818451,26.096379L28.69276814281845,42.307213000000004L58.29516614281845,26.096379L28.69276814281845,12L1.204819142818451,26.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,26.065809L28.43877614281845,42.737896L28.68361514281845,42.882288L59.39288714281845,26.065309L28.684561142818453,11.44229615L0.16870074281845082,26.065809ZM28.70192014281845,41.732138L2.240937742818451,26.12695L28.70097614281845,12.55770427L57.19744514281845,26.127449L28.70192014281845,41.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,26.80120277404785L0.5,45.83131777404785L28.692764,62.042150774047855L28.692764,43.01204077404785L0.5,26.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,62.906411774047854L29.192764,42.722777774047856L28.941999,42.578587774047854L-5.999999996841865e-8,25.93693918404785L0,46.12058077404785L29.192764,62.906411774047854ZM28.192764,43.30130377404785L28.192764,61.17788877404785L1,45.542054774047855L1,27.66546636404785L28.192764,43.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,26.80120277404785L59,45.12649377404785L88.5,62.00042177404785L88.5,42.26604677404785L59,26.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,62.86243677404785L89,41.96362277404785L88.732151,41.82320777404785L58.50000003,25.97454732404785L58.50000003,45.41651177404785L89,62.86243677404785ZM88,42.568469774047855L88,61.13840677404785L59.5,44.836475774047855L59.5,27.627858224047852L88,42.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35626)"><path d="M21.7729373375,2.85937521606684C24.1931619375,2.85937521606684,26.1551654375,4.8214065999999995,26.1551654375,7.2416601C26.1551654375,9.661829000000001,24.1932191375,11.6238594,21.772936837499998,11.6238594C19.3526830375,11.6238594,17.3906514375,9.661829000000001,17.3906514375,7.2416601C17.3906514375,4.8214065999999995,19.3526828375,2.859375,21.7729373375,2.85937521606684ZM23.9577636375,6.8265414L23.9577636375,5.148194800000001L21.1443958375,7.656723L19.5880813375,6.2642415L19.5880813375,7.9425879L21.1443958375,9.3350978L23.9577636375,6.8265414ZM41.402877437499995,17.636632C41.2427544375,17.645353,41.056009437499995,17.649941,40.8424184375,17.649941L35.9334904375,17.649941C35.8089674375,17.649941,35.684467437500004,17.587636,35.5599444375,17.463169999999998C35.4354194375,17.338504,35.3731444375,17.231906000000002,35.3731444375,17.142895L35.3731444375,17.116274C35.3555274375,16.244659,35.346351437500005,15.453276,35.346351437500005,14.741616L35.346351437500005,12.3138857C35.346351437500005,12.1182203,35.426643437500005,11.9358091,35.5866224375,11.7669363C35.7466604375,11.5979471,35.942268437500005,11.5136681,36.1735594375,11.5136681C36.1913474375,11.5136681,36.3247334375,11.504632,36.5738674375,11.4868193C36.8228604375,11.4689779,37.1295984375,11.4822874,37.4943634375,11.5267506C37.8590144375,11.5711298,38.245813437500004,11.6691179,38.654903437499996,11.8202353C39.0638734375,11.9714098,39.4463124375,12.216011,39.802125437499996,12.5540142C40.3535234375,13.123253,40.744798437499995,13.812456,40.9760074375,14.62168C41.2072714375,15.430904,41.4208314375,16.271338,41.6162684375,17.142867000000003C41.6162684375,17.178466999999998,41.620914437500005,17.205171999999997,41.6296654375,17.223014C41.6385594375,17.240686,41.6428904375,17.267477,41.6428904375,17.303047C41.6429194375,17.516666999999998,41.5630284375,17.627825,41.4029064375,17.636662L41.402877437499995,17.636632ZM43.497190437499995,17.956765C43.363775437499996,16.951846,43.1280674375,15.924557,42.7901764375,14.875204C42.4520304375,13.825795,41.9452974375,12.8384638,41.2692894375,11.9139185C40.8958854375,11.3980083,40.4334694375,10.9533787,39.881877437499995,10.5799184C39.3335594375,10.207787,38.7423764375,9.9031267,38.1211204375,9.672535400000001C37.4985274375,9.4413004,36.867096437499995,9.2769294,36.226747437499995,9.1791105C35.5864524375,9.0812922,34.9727214375,9.0591178,34.3857844375,9.1124163C34.1902884375,9.1300879,34.0256614375,9.2681208,33.8922464375,9.5259213C33.7588554375,9.783721,33.6922204375,10.0105953,33.6922204375,10.2061749L33.6922204375,23.066061C33.6922204375,23.386337,33.8077664375,23.661949,34.0390304375,23.893127C34.2703224375,24.124277,34.5457364375,24.239882,34.8660414375,24.239882L35.346351437500005,24.239882C35.346351437500005,23.795309,35.4308054375,23.372713,35.5998214375,22.972548C35.7688924375,22.572323,36.0000154375,22.221321,36.2935274375,21.918835C36.5870934375,21.616402,36.9293154375,21.376131,37.320705437499996,21.198366C37.7119484375,21.020514,38.1387654375,20.931503,38.6011794375,20.931503C39.0637624375,20.931503,39.503860437499995,20.993809,39.9219534375,21.118334C40.3399624375,21.242828,40.7047254375,21.438437,41.015682437500004,21.705215C41.3270644375,21.972076,41.576114437499996,22.318859,41.762888437499996,22.74576C41.949518437500004,23.172689,42.042860437499996,23.688599,42.042860437499996,24.29324L43.136875437499995,24.29324C43.243528437500004,24.29324,43.3281514375,24.239828,43.3904534375,24.13323C43.4524744375,24.026405,43.5060004375,23.897461,43.5502914375,23.746147C43.5943754375,23.597946,43.625642437500005,23.446234,43.6437514375,23.292681C43.661592437500005,23.141449,43.670429437500005,23.012423,43.670429437500005,22.905769C43.670429437500005,22.354401,43.6749284375,21.642796,43.6837384375,20.771267C43.6928004375,19.900023,43.6303824375,18.961798,43.496967437500004,17.956794000000002L43.497190437499995,17.956794000000002L43.497190437499995,17.956765ZM38.734764437500004,22.612343C39.197120437500004,22.612343,39.5974274375,22.7813,39.935373437500004,23.11936C40.273376437500005,23.457363,40.4423064375,23.857302,40.4423064375,24.319971C40.4423064375,24.800087,40.2733214375,25.2092,39.935373437500004,25.547007C39.5974274375,25.885094,39.1972884375,26.054052,38.734764437500004,26.054052C38.254676437499995,26.054052,37.8498684375,25.885094,37.5207824375,25.547007C37.1917614375,25.209059,37.0272444375,24.800114,37.0272444375,24.319971C37.0272444375,23.857357,37.1917874375,23.457333,37.5207824375,23.11936C37.8498654375,22.7813,38.2544764375,22.61237,38.734764437500004,22.61237L38.734764437500004,22.612343ZM20.3789520375,22.585806C19.8986959375,22.585806,19.4939141375,22.754765,19.1649737375,23.092569C18.835835437500002,23.430544,18.6714358375,23.839544,18.6714358375,24.320026C18.6714358375,24.782158,18.8358349375,25.182467,19.1649737375,25.520525C19.4939708375,25.858528,19.8986964375,26.027487,20.3789520375,26.027487C20.8592076375,26.027487,21.263820137499998,25.858501,21.5929026375,25.520554C21.9220414375,25.182493,22.0863556375,24.782356,22.0863556375,24.320026C22.0863556375,23.839769,21.9218993375,23.430487,21.5929026375,23.092569C21.2638206375,22.754765,20.8591795375,22.585806,20.3789520375,22.585806ZM27.1951694375,5.8573070000000005C27.3276244375,6.328613799999999,27.3944584375,6.8189235,27.3944584375,7.3281794C27.3944584375,8.1049209,27.2467114375,8.8312807,26.9510474375,9.506775900000001C26.6555254375,10.182272399999999,26.2544524375,10.773316900000001,25.7479444375,11.279911C25.2412948375,11.7864199,24.6504764375,12.1834698,23.9748087375,12.470439C23.2992000375,12.7574387,22.5816755375,12.900966,21.8216157375,12.900966C21.044789837499998,12.900966,20.318656937500002,12.7574387,19.6431894375,12.470439C18.9676652375,12.1834126,18.3767049375,11.7864199,17.8700547375,11.279911C17.3634042375,10.773316900000001,16.9623885375,10.182272399999999,16.6669227375,9.506775900000001C16.3713153575,8.8312807,16.2235116975,8.105062499999999,16.2235116975,7.3281794C16.2235116975,7.2433033,16.2257206475,7.1590791,16.2292040575,7.075222C15.9749726075,7.3872271,15.7748329975,7.7281752,15.6298046675,8.098464C15.4696249815,8.507465400000001,15.3896484375,8.9255304,15.3896484375,9.3524303L15.3896484375,23.226042C15.3896484375,23.564045,15.4874668125,23.821957,15.6831034975,23.999754C15.8789101275,24.177435,16.1278457075,24.266588,16.4301931375,24.266588L17.0438944375,24.266588C17.0438944375,23.128279,17.3330446375,22.278898,17.9110057375,21.718695C18.4888818375,21.158379,19.3205645375,20.87809,20.405487037500002,20.87809C21.5439634375,20.87809,22.3800354375,21.180494,22.9134769375,21.785387C23.4470596375,22.390112,23.7139778375,23.217234,23.7139778375,24.266588L30.9974874375,24.266588C31.282276437500002,24.266588,31.5265384375,24.16877,31.7312084375,23.973017C31.9357104375,23.777466,32.038061437500005,23.52853,32.038061437500005,23.226097L31.9846784375,5.8573070000000005L27.1951694375,5.8573070000000005Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/gongxu.svg b/src/assets/system/gongxu.svg
new file mode 100644
index 0000000..8d313c1
--- /dev/null
+++ b/src/assets/system/gongxu.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="61.90625" viewBox="0 0 59.5 61.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35769"><rect x="15.5" y="0" width="28" height="28" rx="0"/></clipPath></defs><path d="M1.204819142818451,25.096379L28.69276814281845,41.307213000000004L58.29516614281845,25.096379L28.69276814281845,11L1.204819142818451,25.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,25.065809L28.43877614281845,41.737896L28.68361514281845,41.882288L59.39288714281845,25.065309L28.684561142818453,10.44229615L0.16870074281845082,25.065809ZM28.70192014281845,40.732138L2.240937742818451,25.12695L28.70097614281845,11.55770427L57.19744514281845,25.127449L28.70192014281845,40.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,25.80120277404785L0.5,44.83131777404785L28.692764,61.042150774047855L28.692764,42.01204077404785L0.5,25.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,61.906411774047854L29.192764,41.722777774047856L28.941999,41.578587774047854L-5.999999996841865e-8,24.93693918404785L0,45.12058077404785L29.192764,61.906411774047854ZM28.192764,42.30130377404785L28.192764,60.17788877404785L1,44.542054774047855L1,26.66546636404785L28.192764,42.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,25.80120277404785L59,44.12649377404785L88.5,61.00042177404785L88.5,41.26604677404785L59,25.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,61.86243677404785L89,40.96362277404785L88.732151,40.82320777404785L58.50000003,24.97454732404785L58.50000003,44.41651177404785L89,61.86243677404785ZM88,41.568469774047855L88,60.13840677404785L59.5,43.836475774047855L59.5,26.627858224047852L88,41.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35769)"><path d="M31.250000999999997,28.000002L18.6675,28.000002C17.88696605,28.000973,17.2528940293,27.370031,17.25,26.589502L17.25,5.7189999C17.253842816,4.9388809,17.88735223,4.3084998,18.6675,4.3084998L20.7045,4.3084998L20.7045,10.0485L35.901500999999996,10.0485L35.901500999999996,4.3084998L37.938501,4.3084998C38.720751,4.3084998,39.356003,4.9419994,39.356003,5.7189994L39.356003,12.726C35.280792000000005,11.314371,30.784750000000003,13.082648,28.763004000000002,16.892187C26.7412615,20.701725,27.79683,25.416275,31.250000999999997,28.000002ZM34.519001,8.6047497L22.0852499,8.6047497L22.0852499,3.9567502C22.0852499,2.9697499,22.7835007,2.1682501,23.6410003,2.1682501L25.24575,2.1682501C25.8197508,0.85225004,26.9975014,0,28.303,0C29.606749999999998,0,30.784501,0.85225004,31.3585,2.1682501L32.965001,2.1682501C33.822500000000005,2.1682501,34.519001,2.9697502,34.519001,3.9567502L34.519001,8.6047497ZM20.7045,15.058749C20.7045,15.44375,21.0195003,15.77625,21.4045005,15.77625L27.60125,15.77625C27.997515999999997,15.77625,28.318751,15.455014,28.318751,15.058749C28.318751,14.662486,27.997515999999997,14.34125,27.60125,14.34125L21.40625,14.34125C21.016150500000002,14.349814,20.7044065,14.668555,20.7045,15.058749ZM20.7045,20.79525C20.7045,21.18025,21.023,21.512751,21.416750399999998,21.512751L26.21,21.512751C26.602631600000002,21.50613,26.9185143,21.187918,26.9222498,20.79525C26.9222498,20.41025,26.6037502,20.077749,26.21,20.077749L21.4150004,20.077749C21.0223703,20.084372,20.7064886,20.402582,20.7027502,20.79525L20.7045,20.79525ZM25.1932502,6.454C25.7770567,6.4434648,26.241730699999998,5.9615531,26.231000899999998,5.3777499C26.2407513,4.7946243,25.7763624,4.3137889,25.1932502,4.3032494C24.610827,4.3147411,24.1474919,4.7952948,24.1572504,5.3777499C24.1572504,5.9727497,24.6209998,6.454,25.1932502,6.454ZM31.411,6.454C31.994120000000002,6.442512,32.457737,5.9608827,32.447001,5.3777499C32.456759,4.7952943,31.993423999999997,4.3147411,31.411,4.3032494C30.828578,4.3147411,30.365243,4.7952948,30.375000999999997,5.3777499C30.375000999999997,5.9727497,30.838752,6.454,31.411,6.454ZM36.500001999999995,28.000002C32.634008,28.000002,29.500000999999997,24.865995,29.500000999999997,21.000002C29.500000999999997,17.134008,32.634008,14.000001,36.500001999999995,14.000001C40.365995,14.000001,43.500001999999995,17.134008,43.500001999999995,21.000002C43.500001999999995,24.865995,40.365995,28.000002,36.500001999999995,28.000002ZM40.73675,18.669003L40.73675,15.795501L36.8675,15.795501L36.8675,16.747499L35.17,16.747499L35.17,24.4055L36.858749,24.4055L36.858749,25.845751L40.721001,25.845751L40.721001,22.902248L36.858749,22.902248L36.858749,23.44475L36.0275,23.44475L36.0275,17.678499L36.867498,17.678499L36.867498,18.669001L40.738501,18.669001L40.73675,18.669003ZM34.720248999999995,20.02C34.421001000000004,19.460003,34.424501,19.451252,34.424501,19.451252C34.424501,19.451252,33.392002000000005,19.775002,33.199501,18.667252L32.601001,18.667252C32.601001,18.667252,32.436502000000004,19.776752,31.381251,19.447752L31.066252,20.035753C31.066252,20.035753,31.892252,20.683254,31.066252,21.558254L31.386499999999998,22.149754C31.386499999999998,22.149754,32.298251,21.708754,32.597502,22.912754L33.203001,22.912754C33.203001,22.912754,33.392002000000005,21.806753,34.417501,22.134005C34.709751,21.574005,34.715002,21.570503,34.715002,21.570503C34.715002,21.570503,33.939752999999996,20.821505,34.720248999999995,20.02Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/gongyingshangdangan.svg b/src/assets/system/gongyingshangdangan.svg
new file mode 100644
index 0000000..39e4859
--- /dev/null
+++ b/src/assets/system/gongyingshangdangan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="62.90625" viewBox="0 0 59.5 62.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35686"><rect x="13.5" y="0" width="32" height="32" rx="0"/></clipPath></defs><path d="M1.204819142818451,26.096379L28.69276814281845,42.307213000000004L58.29516614281845,26.096379L28.69276814281845,12L1.204819142818451,26.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,26.065809L28.43877614281845,42.737896L28.68361514281845,42.882288L59.39288714281845,26.065309L28.684561142818453,11.44229615L0.16870074281845082,26.065809ZM28.70192014281845,41.732138L2.240937742818451,26.12695L28.70097614281845,12.55770427L57.19744514281845,26.127449L28.70192014281845,41.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,26.80120277404785L0.5,45.83131777404785L28.692764,62.042150774047855L28.692764,43.01204077404785L0.5,26.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,62.906411774047854L29.192764,42.722777774047856L28.941999,42.578587774047854L-5.999999996841865e-8,25.93693918404785L0,46.12058077404785L29.192764,62.906411774047854ZM28.192764,43.30130377404785L28.192764,61.17788877404785L1,45.542054774047855L1,27.66546636404785L28.192764,43.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,26.80120277404785L59,45.12649377404785L88.5,62.00042177404785L88.5,42.26604677404785L59,26.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,62.86243677404785L89,41.96362277404785L88.732151,41.82320777404785L58.50000003,25.97454732404785L58.50000003,45.41651177404785L89,62.86243677404785ZM88,42.568469774047855L88,61.13840677404785L59.5,44.836475774047855L59.5,27.627858224047852L88,42.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35686)"><path d="M25.178437199999998,6.013828425L25.162812199999998,6.013828425C25.055625900000003,5.080703315,24.4187503,4.361328125,23.6415634,4.361328125L18.1996875,4.361328125C17.42218757,4.361328125,16.78562498,5.081015645,16.678437473,6.013828425L16.65625,6.013828425L16.65625,12.286016925L16.6628124719,12.286016925L16.6628124719,14.008203525C16.66187501,14.038516025,16.65625,14.066953625,16.65625,14.097578025L16.65625,25.993517125L16.678437473,25.993517125C16.78562498,26.926641125,17.42218739,27.646017125,18.1996875,27.646332125L23.6415634,27.646332125C24.4190626,27.646332125,25.055625900000003,26.926641125,25.162812199999998,25.993517125L25.178437199999998,25.993517125L25.178437199999998,25.849769125L25.1849995,25.849769125L25.1849995,12.170390625L25.178437199999998,12.170390625L25.178437199999998,10.595078025C25.1793747,10.566952725,25.1849995,10.540703324999999,25.1849995,10.512577024999999L25.1849995,6.287265825C25.1849995,6.259141125,25.1790628,6.232890725,25.178437199999998,6.204765825L25.178437199999998,6.013828425ZM33.707815,6.013828425L33.692503,6.013828425C33.585001,5.080703315,32.948439,4.361328125,32.170938,4.361328125L26.729062,4.361328125C25.951875700000002,4.361328125,25.3149996,5.081015645,25.207813299999998,6.013828425L25.185626,6.013828425L25.185626,12.286016925L25.192188299999998,12.286016925L25.192188299999998,14.008203525C25.1912498,14.038516025,25.185626,14.066953625,25.185626,14.097578025L25.185626,25.993517125L25.207813299999998,25.993517125C25.3149996,26.926641125,25.951875700000002,27.646017125,26.729062,27.646332125L32.170937,27.646332125C32.948437,27.646332125,33.584998999999996,26.926641125,33.692501,25.993517125L33.707813,25.993517125L33.707813,25.849769125L33.714376,25.849769125L33.714376,12.170390625L33.707813,12.170390625L33.707813,10.595078025C33.708752000000004,10.566952725,33.714376,10.540703324999999,33.714376,10.512577024999999L33.714376,6.287265825C33.714376,6.259141125,33.708752000000004,6.232890725,33.707813,6.204765825L33.707815,6.013828425ZM27.336877,7.185703525L31.563439000000002,7.185703525L31.563439000000002,17.928203125L27.336876,17.928203125L27.336877,7.185703525ZM30.604689999999998,24.859453125C29.890158,25.271952125,29.009847,25.271952125,28.295313999999998,24.859453125C27.580841,24.446863125,27.140695,23.684502125,27.140627000000002,22.859453125C27.140627000000002,22.034452125,27.580939,21.271953125,28.295313999999998,20.859453125C29.009847,20.446957125,29.890158,20.446957125,30.604689999999998,20.859453125C31.319263,21.271946125,31.759445,22.034368125,31.759377,22.859453125C31.759377,23.684454125,31.319376,24.446953125,30.604689999999998,24.859453125Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M29.988359475,21.926954125C29.655124775,21.734768125,29.244719565,21.734768125,28.911484715,21.926954125C28.578357715,22.119329125,28.373121261604,22.474771125,28.373046875,22.859453125C28.373046875,23.454141125,28.854922355,23.936329125,29.449609775,23.936329125C30.044298175,23.936329125,30.526485475,23.454141125,30.526485475,22.859453125C30.526752475,22.474760125,30.321571475,22.119207125,29.988359475,21.926954125ZM42.343673875,10.512578525L42.343673875,6.287265825C42.343673875,6.259141125,42.338047875,6.232890725,42.337108875,6.204765825L42.337108875,6.013828425L42.321483875,6.013828425C42.214296875,5.080703315,41.577421875,4.361328125,40.800234875,4.361328125L35.358357875,4.361328125C34.580858675,4.361328125,33.944295875,5.081015645,33.836795775,6.013828425L33.814920875,6.013828425L33.814920875,12.286016925L33.821484075,12.286016925L33.821484075,14.008203525C33.820545175,14.038516025,33.814920875,14.066953625,33.814920875,14.097578025L33.814920875,25.993517125L33.836795775,25.993517125C33.944295875,26.926641125,34.580858675,27.646017125,35.358045575,27.646332125L40.799920875,27.646332125C41.577108875,27.646332125,42.213984875,26.926641125,42.321170875,25.993517125L42.336795875,25.993517125L42.336795875,25.849769125L42.343668875,25.849769125L42.343668875,12.170390625L42.337107875,12.170390625L42.337107875,10.595078025C42.338045875,10.566952725,42.343668875,10.541015125000001,42.343673875,10.512578525ZM35.965858475,7.185703525L40.192420874999996,7.185703525L40.192420874999996,17.928203125L35.965858475,17.928203125L35.965858475,7.185703525ZM39.233984875000004,24.859453125C38.519452875,25.271953125,37.639140175,25.271953125,36.924609175,24.859453125C36.210134475000004,24.446863125,35.769990875,23.684502125,35.769920875,22.859453125C35.769920875,22.034452125,36.210234675,21.271953125,36.924609175,20.859453125C37.639140175,20.446957125,38.519452875,20.446957125,39.233984875000004,20.859453125C39.948458875,21.272045125,40.388600875,22.034408125,40.388672875,22.859453125C40.388672875,23.684454125,39.948671875,24.446953125,39.233984875000004,24.859453125Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M38.6179295875,21.92148398C38.2848072875,21.729297638,37.8744889475,21.729297638,37.5413666975,21.92148398C37.2082405375,22.11385918,37.003004074111,22.46930122,37.0029296875,22.85398475C37.0029296875,23.23867225,37.2082424475,23.59398465,37.5413666975,23.78648545C38.0563736875,24.08376885,38.7148630875,23.90727045,39.0121481875,23.39226535C39.3094334875,22.87726005,39.1329366875,22.21876907,38.6179295875,21.92148398Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/gongyingshangwanglai.svg b/src/assets/system/gongyingshangwanglai.svg
new file mode 100644
index 0000000..69de69e
--- /dev/null
+++ b/src/assets/system/gongyingshangwanglai.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="62.90625" viewBox="0 0 59.5 62.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35715"><rect x="16" y="0" width="30" height="30" rx="0"/></clipPath></defs><path d="M1.204819142818451,26.096379L28.69276814281845,42.307213000000004L58.29516614281845,26.096379L28.69276814281845,12L1.204819142818451,26.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,26.065809L28.43877614281845,42.737896L28.68361514281845,42.882288L59.39288714281845,26.065309L28.684561142818453,11.44229615L0.16870074281845082,26.065809ZM28.70192014281845,41.732138L2.240937742818451,26.12695L28.70097614281845,12.55770427L57.19744514281845,26.127449L28.70192014281845,41.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,26.80120277404785L0.5,45.83131777404785L28.692764,62.042150774047855L28.692764,43.01204077404785L0.5,26.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,62.906411774047854L29.192764,42.722777774047856L28.941999,42.578587774047854L-5.999999996841865e-8,25.93693918404785L0,46.12058077404785L29.192764,62.906411774047854ZM28.192764,43.30130377404785L28.192764,61.17788877404785L1,45.542054774047855L1,27.66546636404785L28.192764,43.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,26.80120277404785L59,45.12649377404785L88.5,62.00042177404785L88.5,42.26604677404785L59,26.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,62.86243677404785L89,41.96362277404785L88.732151,41.82320777404785L58.50000003,25.97454732404785L58.50000003,45.41651177404785L89,62.86243677404785ZM88,42.568469774047855L88,61.13840677404785L59.5,44.836475774047855L59.5,27.627858224047852L88,42.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35715)"><path d="M41.17005759375,2.549529299C38.500059593749995,2.549529299,36.340057593750004,3.98952925,36.340057593750004,5.75952935C36.340057593750004,6.95952895,37.30005659375,7.97952935,38.74005659375,8.549529549999999C38.68005559375,8.93952985,38.53005559375,9.35952945,38.290056593749995,9.80952885C38.290056593749995,9.80952885,39.64005659375,9.689528450000001,40.33005759375,8.939528450000001C40.60005759375,8.96952815,40.870057593750005,8.99952885,41.17005759375,8.99952885C43.84005559375,8.99952885,46.000059593749995,7.55952835,46.000059593749995,5.78952885C46.000059593749995,3.98952875,43.84005559375,2.549528852,41.17005759375,2.549529299ZM39.22005659375,6.20952915C38.83005759375,6.20952915,38.53005759375,5.90952925,38.53005759375,5.51952915C38.53005759375,5.12952945,38.83005759375,4.82952905,39.22005659375,4.82952905C39.61005759375,4.82952905,39.91005659375,5.12952945,39.91005659375,5.51952915C39.91005659375,5.87952925,39.58005759375,6.20952915,39.22005659375,6.20952915ZM41.290058593750004,6.20952915C40.90005659375,6.20952915,40.60005759375,5.90952925,40.60005759375,5.51952915C40.60005759375,5.12952945,40.90005659375,4.82952905,41.290058593750004,4.82952905C41.68005759375,4.82952905,41.98005659375,5.12952945,41.98005659375,5.51952915C41.98005659375,5.87952925,41.65005859375,6.20952915,41.290058593750004,6.20952915ZM43.36005759375,6.20952915C42.97005659375,6.20952915,42.67005759375,5.90952925,42.67005759375,5.51952915C42.67005759375,5.12952945,42.97005659375,4.82952905,43.36005759375,4.82952905C43.75005759375,4.82952905,44.050058593749995,5.12952945,44.050058593749995,5.51952915C44.050058593749995,5.87952925,43.72005859375,6.20952915,43.36005759375,6.20952915ZM25.66005799375,19.67952925C26.14005859375,19.52953125,26.620058593750002,19.28953025,27.04005859375,19.04953025C27.22005859375,18.92952925,27.34005859375,18.83952725,27.40005859375,18.71953025C27.46005859375,18.62952825,27.490057593750002,18.47952925,27.52005859375,18.29953025C27.49005859375,18.149530249999998,27.46005859375,17.99953125,27.46005859375,17.90952925C27.46005859375,17.819529250000002,27.430058593749997,17.72953025,27.40005859375,17.66952925C27.370058593750002,17.60953025,27.31005859375,17.54952925,27.250058593749998,17.48952925C27.190058593750003,17.42953025,27.10005859375,17.33953025,26.98005859375,17.24953025C26.86005759375,17.15952825,26.74005859375,17.009528250000002,26.65005759375,16.79952925C26.560057593750003,16.58952925,26.47005759375,16.379530250000002,26.41005859375,16.199529249999998C26.35005859375,15.95953025,26.29005859375,15.71953125,26.20005859375,15.44953025C26.11005859375,15.41953225,25.99005889375,15.35953025,25.90005879375,15.23953025C25.810059593749997,15.14952925,25.72005839375,15.02952825,25.63005919375,14.87952925C25.54005909375,14.72952925,25.45005799375,14.51953025,25.36005879375,14.24953125C25.27005859375,13.97953025,25.24005799375,13.73952925,25.27005859375,13.52953025C25.30005929375,13.31952925,25.330059093750002,13.10953025,25.39005949375,12.95953125C25.45005889375,12.77952925,25.54005909375,12.59953025,25.69005969375,12.47953035C25.69005969375,11.90952965,25.72005839375,11.33952995,25.810059593749997,10.73953055C25.90005879375,10.25952955,26.02005859375,9.74953035,26.17005859375,9.17953105C26.320058593749998,8.60953095,26.59005959375,8.09953065,26.92005959375,7.64953085C27.19005959375,7.28953125,27.49005859375,6.92953065,27.85005859375,6.56953095C28.21005859375,6.20953105,28.54005859375,5.939530850000001,28.84005859375,5.75953105C28.750059593750002,5.45953105,28.66005859375,5.15953135,28.60005859375,4.91953115C28.54005859375,4.67953105,28.42005859375,4.40953095,28.180059593750002,4.13953135C27.85005859375,3.74953155,27.370058593750002,3.38953143,26.71005959375,3.02953136C26.05005929375,2.66953133,25.30005929375,2.51953125,24.49005889375,2.51953125C24.16005899375,2.51953125,23.83005949375,2.549531311,23.47005889375,2.579531148C23.11005929375,2.63953105,22.78005889375,2.72953123,22.45005849375,2.84953102C22.12005849375,2.99953112,21.79005859375,3.17953098,21.46005869375,3.41953105C21.13005879375,3.6595311500000003,20.830059093750002,3.98953115,20.56005859375,4.3795309499999995C20.26005889375,4.799530949999999,20.050058593750002,5.24953075,19.90005879375,5.75953105C19.75005889375,6.26953125,19.66005849375,6.74953075,19.570058593749998,7.19953155C19.51005859375,7.73953155,19.48005869375,8.279531949999999,19.51005859375,8.78953125C19.39005879375,8.90953205,19.300058593750002,9.05953165,19.24005869375,9.23953105C19.18005849375,9.389530650000001,19.12005849375,9.56953145,19.12005849375,9.77953105C19.09005859375,9.98953105,19.15005849375,10.19953065,19.21005869375,10.43953085C19.30005839375,10.67952915,19.39005849375,10.85953045,19.48005869375,10.97953125C19.570058593749998,11.09953115,19.66005849375,11.21953015,19.75005889375,11.27953145C19.84005899375,11.33952995,19.96005889375,11.39953135,20.05005909375,11.42953105C20.11005929375,11.66952895,20.17005869375,11.90952965,20.26005889375,12.08953185C20.32005879375,12.26953125,20.41005899375,12.44952965,20.50005909375,12.62953125C20.590059293750002,12.80953125,20.71005869375,12.92953125,20.830059093750002,13.01953125C21.07005929375,13.22953025,21.28005889375,13.40953125,21.46005869375,13.58953225C21.64005849375,13.76953125,21.760059393749998,14.03953125,21.79005909375,14.36953025C21.82005929375,14.57953125,21.82005929375,14.78953125,21.82005929375,14.96953225C21.82005929375,15.14953125,21.79005909375,15.32953225,21.73005919375,15.50953225C21.67005869375,15.68953125,21.55005879375,15.83953225,21.40005879375,16.019532249999997C21.25005869375,16.16953225,21.01005839375,16.37953425,20.71005869375,16.55953225C20.32005879375,16.79953325,19.90005879375,16.97953325,19.42005849375,17.09953325C18.94005849375,17.219533249999998,18.46005819375,17.36953325,18.01005849375,17.519533250000002C17.56005849375,17.66953325,17.14005849375,17.87953425,16.81005853375,18.149534250000002C16.48005858375,18.41953325,16.27005860375,18.80953425,16.18005858375,19.31953225C16.06005857978016,20.24953225,16.03005858875,20.99953225,16.09005858575,21.56953425C16.15005857475,22.13953225,16.27005860375,22.49953225,16.42005857375,22.61953325C16.51005858375,22.70953325,16.75005853375,22.76953325,17.14005849375,22.85953325C17.53005849375,22.94953525,17.98005869375,23.00953525,18.52005859375,23.09953325C19.06005839375,23.18953525,19.66005849375,23.24953425,20.32005879375,23.30953625C20.98005869375,23.36953525,21.64005849375,23.42953525,22.27005859375,23.48953425C22.30005839375,23.45953325,22.30005839375,23.42953525,22.33005859375,23.39953425C22.36005879375,23.36953525,22.36005879375,23.33953525,22.39005849375,23.30953225C22.42005869375,23.15953425,22.42005869375,23.00953525,22.42005869375,22.82953425C22.42005869375,22.64953425,22.45005939375,22.46953425,22.48005919375,22.28953325C22.57005929375,21.68953325,22.78005889375,21.23953425,23.11005929375,20.90953425C23.44005919375,20.57953425,23.83005859375,20.33953325,24.280058893750002,20.12953425C24.70005889375,20.03953125,25.18005849375,19.85953325,25.66005799375,19.67952925Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M40.660066375,22.83203525C40.540063375,22.47203625,40.300064375000005,22.172036249999998,40.000063374999996,21.93203725C39.700063375,21.69203625,39.340065375,21.51203625,38.950064375,21.36203725C38.560063375,21.21203825,38.170063375,21.09203725,37.750064375,20.972036250000002C37.330062375,20.852036249999998,36.940062375,20.73203525,36.610063374999996,20.58203625C36.220063375,20.43203725,35.920063375,20.25203725,35.680063375,20.07203625C35.470061375,19.86203625,35.290063375,19.68203525,35.200063375,19.44203425C35.110063374999996,19.23203325,35.050063375,19.02203425,35.050063375,18.78203525C35.050063375,18.57203325,35.050063375,18.33203525,35.080063375,18.12203425C35.110063374999996,17.972033250000003,35.170065375,17.82203525,35.230063375,17.70203525C35.290063375,17.582033250000002,35.350063375,17.49203325,35.440063375,17.40203525L35.710064375,17.13203425C35.830063375,17.04203425,35.950065375,16.95203525,36.070065375,16.83203525C36.190065375,16.74203425,36.280065375,16.62203425,36.400064375,16.44203425C36.490064375,16.26203445,36.580063375,16.08203315,36.640065375,15.90203385C36.700063375,15.69203285,36.760065374999996,15.48203375,36.850066375,15.24203395C37.000064375,15.21203325,37.120066375,15.12203315,37.240066375,15.03203205C37.330066375,14.94203185,37.450066375,14.79203225,37.540065375,14.61203285C37.630066375,14.43203165,37.690065375,14.19203285,37.750066375,13.89203315C37.780065375,13.652032850000001,37.780065375,13.47203255,37.750066375,13.32203245C37.720067375,13.17203285,37.660065375,13.05203195,37.600067375,12.932032150000001C37.540067375,12.81203175,37.450066375,12.72203205,37.390065375,12.66203165C37.420066375,12.092031949999999,37.360066375,11.55203195,37.300065375,10.98203185C37.210064375,10.50203185,37.090064375,9.99203225,36.910064375,9.45203205C36.730066375,8.91203185,36.460065375,8.40203185,36.100064375,7.95203165C35.800065375,7.56203105,35.290064375,7.20203125,34.660065375,6.84203142C34.000064375,6.48203157,33.250064875,6.33203125,32.410065675,6.33203125C32.110065475,6.33203125,31.780065075,6.362031313,31.420065875,6.392031375C31.060065275,6.4520315,30.730065775,6.54203124,30.370065675,6.6620315C30.010066075,6.81203136,29.680065675,6.99203128,29.350065675,7.23203135C29.020065775,7.47203145,28.720065575,7.80203175,28.450065575,8.19203105C28.150065875,8.61203095,27.940065875000002,9.09203125,27.790065975,9.57203125C27.640066175,10.08203105,27.550066675,10.56203125,27.490066275,11.01203155C27.430066375,11.55203195,27.400066575,12.092031949999999,27.430066375,12.60203175C27.310065975,12.72203205,27.220066275,12.872032149999999,27.160066375,13.052031549999999C27.100065975,13.17203185,27.040065975,13.35203125,27.040065975,13.56203075C27.010066275,13.772031349999999,27.070065775,13.98203185,27.130065675,14.22203205C27.220065375,14.46203045,27.310065975,14.64203165,27.400065675,14.76203255C27.490065575,14.88203235,27.580066175,15.00203135,27.670065875,15.06203265C27.760065775,15.12203125,27.880065875,15.18203255,27.970065575,15.21203235C28.030065575000002,15.45203305,28.090065975,15.69203095,28.180065675,15.87203315C28.240065575,16.05203245,28.330065275,16.23203375,28.420065375,16.41203225C28.510064575,16.592032250000003,28.600065275,16.74203125,28.720065075,16.832033250000002C28.960064375,17.01203125,29.170064475,17.19203325,29.350064775,17.37203325C29.560064775,17.55203225,29.650064475,17.82203325,29.710064875,18.15203425C29.740064175,18.392034250000002,29.740064175,18.602033249999998,29.740064175,18.78203525C29.740064175,18.962033249999998,29.710064875,19.142034250000002,29.620064275,19.292035249999998C29.560063875,19.44203425,29.440063975,19.62203425,29.260064175,19.77203425C29.110064075,19.92203425,28.870063775,20.13203525,28.570064075,20.31203425C28.180063675,20.55203425,27.760063875,20.732034249999998,27.280063675,20.85203525C26.800063875,20.972035249999998,26.320063575,21.12203525,25.870064175,21.27203425C25.420064575,21.42203425,25.000064475,21.662035250000002,24.670064335,21.90203525C24.340064105,22.172036249999998,24.130064125,22.56203625,24.040064395,23.07203525C23.9200641513,23.97203425,23.890064538,24.72203425,23.980064273,25.29203425C24.070064005,25.86203425,24.160064635,26.22203625,24.310064495,26.34203725C24.400064225,26.40203525,24.580064595,26.49203525,24.910064815,26.55203825C25.240064975,26.61203625,25.630064475,26.70203825,26.080064775,26.76203725C26.560065075,26.82203625,27.070064575,26.91203925,27.610064775,26.97203825C28.180064675,27.03203725,28.750064375,27.09203925,29.320064975,27.12204025C29.890064674999998,27.18203725,30.460064875,27.21203825,30.970065075,27.24203825C31.510064575,27.27203925,31.990065575,27.30203825,32.380065875,27.30203825C32.770066275,27.30203825,33.280065575,27.27203925,33.820065475,27.24203825C34.360066375,27.21203825,34.930065375,27.18203925,35.500064375,27.12204025C36.070065375,27.06204025,36.670065375,27.00203925,37.240066375,26.94203925C37.810065375,26.88204025,38.350066375,26.79203825,38.830065375000004,26.70203925C39.310063375,26.61203725,39.700063375,26.52203925,40.030063375,26.43203725C40.360065375000005,26.34203925,40.540067375,26.22203625,40.600065375,26.13204025C40.690067375,25.98203825,40.780063375,25.77203725,40.810064374999996,25.47203825C40.870065374999996,25.17203925,40.870065374999996,24.87203825,40.870065374999996,24.51203925C40.870065374999996,24.18203925,40.840067375000004,23.85203725,40.810064374999996,23.52203925C40.780067375,23.34203925,40.720064375,23.07203825,40.660066375,22.83203525Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/jichupeizhi.svg b/src/assets/system/jichupeizhi.svg
new file mode 100644
index 0000000..bdaf618
--- /dev/null
+++ b/src/assets/system/jichupeizhi.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="89.5693359375" height="96" viewBox="0 0 89.5693359375 96"><defs><filter id="master_svg0_143_35968" filterUnits="objectBoundingBox" color-interpolation-filters="sRGB" x="-0.6666666666666666" y="-0.6666666666666666" width="2.3333333333333335" height="2.3333333333333335"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="0" dx="0"/><feGaussianBlur stdDeviation="8"/><feColorMatrix type="matrix" values="0 0 0 0 0.12895070016384125 0 0 0 0 0.29307788610458374 0 0 0 0 0.9107142686843872 0 0 0 0.5299999713897705 0"/><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><clipPath id="master_svg1_143_34356"><rect x="32.78466796875" y="36" width="25" height="24" rx="0"/></clipPath></defs><path d="M44.78466796875,24.000000000000004L65.56927496875,36L65.56927496875,60L44.78466796875,72L24.00005836875,60L24.00005796875,36.000001L44.78466796875,24.000000000000004Z" fill="#496FFE" fill-opacity="1" filter="url(#master_svg0_143_35968)"/><g clip-path="url(#master_svg1_143_34356)"><path d="M45.88867596875,51.096690875C45.959985968750004,50.688688875,46.48291996875,50.544689875,46.76815496875,50.832688875L49.24020996875,53.304690875C49.59675396875,53.664689875,50.167228968749995,53.664689875,50.523774968750004,53.304690875L51.783568968750004,52.032688875C52.140115968749996,51.672690875,52.140115968749996,51.096691875,51.783568968750004,50.760690875L49.311517968749996,48.240688875000004C49.00251196875,47.952689875000004,49.14512796875,47.424689875,49.54921496875,47.352690875C50.99916796875,47.064690875,52.544200968750005,47.496689875,53.68514596875,48.648689875C54.92117496875,49.896689875,55.32525796875,51.672689875,54.84986496875,53.280688874999996C54.77855496875,53.544686875,54.826095968749996,53.832688875,55.016252968749995,54.024690875000005L57.53584496875,56.544690875C57.89239096875,56.904691875,57.89239096875,57.480689874999996,57.53584496875,57.816690875L56.252276968749996,59.088687875000005C55.89573496875,59.448688875,55.32525796875,59.448688875,54.96871196875,59.088687875000005L52.44912196875,56.520688875000005C52.25896296875,56.328687875,51.97372796875,56.280688874999996,51.71225896875,56.352689874999996C50.14345896875,56.808686875,48.38449896875,56.424689875,47.148470968750004,55.176688874999996C46.05506396875,54.072690875,45.65097696875,52.536686875,45.88867596875,51.096690875ZM55.77688396875,57.936690875C56.15719796875,57.936690875,56.46620596875,57.624688875000004,56.46620596875,57.240689875C56.46620596875,56.856690875,56.15719796875,56.544690875,55.77688396875,56.544690875C55.39656996875,56.544690875,55.08756296875,56.856690875,55.08756296875,57.240689875C55.08756296875,57.624688875000004,55.39656996875,57.936690875,55.77688396875,57.936690875ZM55.46787796875,46.224690475L54.944943968749996,44.304689875C54.89740596875,44.112690975,54.75478596875,43.944690675000004,54.58839796875,43.848690475C54.39823896875,43.728690575,54.18431096875,43.656691574999996,53.99415796875,43.704690975L52.87697996875,43.992689575C52.37781296875,44.088689775,51.854880968749995,43.896689375,51.54587396875,43.464689775L50.571313968750005,42.192689875C50.23853896875,41.832690675,50.14345896875,41.208689675,50.38115696875,40.776689975L50.97539896875,39.720690275C51.16555996875,39.360690075,51.02293796875,38.904690475,50.66639296875,38.736690275L48.88366296875,37.680690775C48.71727296875,37.584690805,48.50334796875,37.560690705,48.31318996875,37.608690795C48.12303096875,37.656690675,47.98041296875,37.776690675,47.88533296875,37.944690675L47.29109096875,38.952690575C47.02962396875,39.384690475,46.50668996875,39.600690375,46.00752496875,39.504690375L44.43872296875,39.288690575C43.96332796875,39.240690475,43.41662296875,38.856690675,43.32154696875,38.376690475L43.01253896875,37.248690605C42.96499896875,37.056690575,42.82238196875,36.888690625,42.65599346875,36.792690615C42.46583656875,36.672690658,42.22813796875,36.624690654,42.01420976875,36.672690658L40.08886286875,37.152690615C39.68477726875,37.248690605,39.44707866875,37.680690575,39.56592896875,38.064690575L39.87493566875,39.192690175C39.99378396875,39.696690075,39.80362656875,40.200690075,39.37577106875,40.488690575L38.11597536875,41.448689975C37.73566006875,41.784690875,37.16518686875,41.832690675,36.68979166875,41.592690475L35.66769336875,40.992690575C35.33491706875,40.776689975,34.90706206875,40.872690675,34.66936476875,41.208690175L33.59972607875,43.056691175C33.52841686875,43.224689975000004,33.50464713875,43.392690675,33.55218660875,43.560690375C33.62349575875,43.752690775,33.74234455875,43.920689575,33.90873256875,44.016689775L34.93083096875,44.616690675C35.38245586875,44.904689775,35.59638306875,45.432690575,35.50130466875,45.936691275L35.33491656875,47.520690875C35.31114646875,48.000689875,34.93083096875,48.528689875,34.45543626875,48.600688875L33.33825903875,48.888688875C32.93417358875,49.008689875,32.69647629575,49.416688875,32.81532496375,49.824688875L33.33825903875,51.744687875C33.45710759875,52.152688875,33.86119316875,52.392688875,34.28904826875,52.296688875L35.42999556875,52.008687875C35.92916006875,51.912686875,36.45209356875,52.104688875,36.76110026875,52.536686875L37.73565956875,53.808686875C38.06843516875,54.192685874999995,38.13974476875,54.768687875,37.90204766875,55.200687875L37.30780406875,56.208688875C37.21272566875,56.376687875,37.18895526875,56.592689875000005,37.236494568750004,56.784686875C37.28403426875,56.976686875,37.42665286875,57.144685875,37.59304046875,57.240686875L39.37577056875,58.272687875C39.54215856875,58.368686875,39.75608586875,58.392684875,39.94624326875,58.344688875C40.13640066875,58.296685875,40.30278966875,58.176684875,40.39786866875,58.008685875L40.99211216875,57.000686875C41.27734946875,56.568687874999995,41.800283468749996,56.352687875,42.29944706875,56.448686875L43.89201796875,56.664687875C44.36741496875,56.688687875,44.89034796875,57.096688875,45.00919596875,57.576686875L45.31820196875,58.704686875C45.43705196875,59.112687875,45.84113596875,59.352685875,46.26899196875,59.256686875L48.19433996875,58.776687875C48.47957896875,58.704686875,48.66973396875,58.464685875,48.74104396875,58.200687875C47.67140596875,57.888686875000005,46.69684696875,57.336686875,45.88867596875,56.520688875000005C45.128044968750004,55.752687875,44.581341968749996,54.816690875,44.29610396875,53.808688875L44.224794968750004,53.808688875C41.01588246875,53.808688875,38.42498156875,51.192689875,38.42498156875,47.952688875C38.42498156875,44.712689375,41.01588246875,42.096689175,44.224794968750004,42.096689175C46.53045896875,42.096689175,48.52711696875,43.440689075,49.454135968749995,45.408688575C49.81068196875,45.360690075,50.16722696875,45.312688875,50.523772968749995,45.312688875C52.21142196875,45.312688875,53.78022796875,45.960688575,54.99248496875,47.112687875L55.016252968749995,47.112687875C55.32525796875,47.040688875,55.56295796875,46.632689475,55.46787796875,46.224690475ZM44.03463696875,50.760689875C44.05840696875,50.544689875,44.12971596875,50.352689874999996,44.224794968750004,50.184689875000004C43.84447996875,50.208690875,43.440392968750004,50.112688875,43.08384796875,49.896689875C42.06174946875,49.296689875,41.70520396875,47.976688875,42.29944706875,46.944690875C42.89368896875,45.912689175,44.20102496875,45.552690475,45.22312496875,46.152690875C46.15014196875,46.680688875,46.50668996875,47.808690874999996,46.15014196875,48.792688874999996C46.245220968750004,48.792688874999996,46.31653096875,48.768689875,46.41161096875,48.768689875C46.81569496875,48.768689875,47.19600996875,48.864689874999996,47.52878596875,49.032688875C47.24354896875,48.480689874999996,47.148470968750004,47.808688875,47.338627968750004,47.184689875000004C47.48124796875,46.680688875,47.79025396875,46.248689675,48.170568968750004,45.960690475C47.43370796875,44.448689975,45.888674968749996,43.416690875,44.10594596875,43.416690875C41.61012456875,43.416690875,39.56592796875,45.456689875,39.56592796875,48.000689875C39.56592796875,50.472690875,41.49127676875,52.464687874999996,43.91578896875,52.560689875C43.89201796875,51.960688875,43.91578896875,51.360688875,44.03463696875,50.760689875Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/kehudangan.svg b/src/assets/system/kehudangan.svg
new file mode 100644
index 0000000..a0dd2ec
--- /dev/null
+++ b/src/assets/system/kehudangan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="63.90625" viewBox="0 0 59.5 63.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35599"><rect x="14.5" y="0" width="32" height="32" rx="0"/></clipPath></defs><path d="M1.204819142818451,27.096379L28.69276814281845,43.307213000000004L58.29516614281845,27.096379L28.69276814281845,13L1.204819142818451,27.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,27.065809L28.43877614281845,43.737896L28.68361514281845,43.882288L59.39288714281845,27.065309L28.684561142818453,12.44229615L0.16870074281845082,27.065809ZM28.70192014281845,42.732138L2.240937742818451,27.12695L28.70097614281845,13.55770427L57.19744514281845,27.127449L28.70192014281845,42.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,27.80120277404785L0.5,46.83131777404785L28.692764,63.042150774047855L28.692764,44.01204077404785L0.5,27.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,63.906411774047854L29.192764,43.722777774047856L28.941999,43.578587774047854L-5.999999996841865e-8,26.93693918404785L0,47.12058077404785L29.192764,63.906411774047854ZM28.192764,44.30130377404785L28.192764,62.17788877404785L1,46.542054774047855L1,28.66546636404785L28.192764,44.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,27.80120277404785L59,46.12649377404785L88.5,63.00042177404785L88.5,43.26604677404785L59,27.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,63.86243677404785L89,42.96362277404785L88.732151,42.82320777404785L58.50000003,26.97454732404785L58.50000003,46.41651177404785L89,63.86243677404785ZM88,43.568469774047855L88,62.13840677404785L59.5,45.836475774047855L59.5,28.627858224047852L88,43.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35599)"><path d="M33.0601790625,19.699661125C33.0601790625,19.699661125,31.9935130625,19.415219125,31.7090700625,19.272997125C31.1401790625,18.917440125,30.7846260625,18.206329125,30.7846260625,17.495218125C30.7846260625,16.712996125,31.4246250625,16.072997125,31.8512940625,15.575218125C32.9179590625,14.295218125,33.4868490625,12.588551525,33.4868490625,10.668550925C33.4868490625,6.6863279250000005,30.6424040625,3.486328125,27.1579580625,3.486328125C23.6735133625,3.486328125,20.8290676625,6.757439625,20.8290676625,10.739662124999999C20.8290676625,12.659662225,21.4690680625,14.366329125,22.4646234625,15.646329125C23.104623762499997,16.001884125,23.5312900625,16.784108125,23.5312900625,17.566329125C23.5312900625,18.277439125,23.1757345625,18.917439125,22.606845862500002,19.344107125L22.606845862500002,19.415219125C22.5357351625,19.415219125,22.4646234625,19.486330125,22.4646234625,19.486330125C22.1090674625,19.699661125,21.753511862499998,19.841886125,21.3268451625,19.841886125C18.6246223625,20.766333125,16.3490668625,22.188555125,14.9979558625,23.966331125L15.0690669725,23.966331125C14.7135113625,24.464107125,14.571289122104652,25.033001125,14.571289151906974,25.601889125C14.571289151906974,27.308554125,15.9935116625,28.730776125,17.7712893625,28.730776125L36.5446260625,28.730776125C38.3935150625,28.730776125,39.8157370625,27.379669125,39.8157370625,25.601889125C39.8157370625,24.890777125,39.5312920625,24.250776125,39.175737062500005,23.681887125C37.8246270625,21.904108125,35.6912940625,20.552999125,33.0601790625,19.699661125ZM45.7890700625,22.828550125C44.5801810625,21.335218125,42.731295062499996,20.055218125,40.384626062500004,19.344107125C40.384626062500004,19.344107125,39.460182062499996,19.130773124999997,39.175737062500005,18.988552125C38.6779590625,18.632995125,38.3935150625,18.064107125,38.3935150625,17.424106125C38.3935150625,16.712996125,38.9624040625,16.144107124999998,39.3179570625,15.788550125C40.2424050625,14.650774125,40.8112940625,13.157439225,40.8112940625,11.450773224999999C40.8112940625,7.966328125,38.3224010625,5.050772425,35.1935140625,5.050772425C34.6957340625,5.050772425,34.1979580625,5.121883625,33.771292062499995,5.264105525C34.837957062499996,6.828550125,35.4068470625,8.677439725,35.4068470625,10.739662124999999C35.4068470625,12.944106125,34.624624062500004,15.077440125,33.2735140625,16.855217125L33.131292062499995,16.997439125L33.131292062499995,17.068550125C32.9890690625,17.210771125,32.7757360625,17.424106125,32.7046260625,17.637441125000002C32.7046260625,17.708552125,32.7046260625,17.708552125,32.7757380625,17.708552125C32.9179590625,17.779663125,33.131294062500004,17.850773125,33.344627062499995,17.850773125L33.415739062499995,17.850773125L33.7001840625,17.992995125C36.615740062499995,18.917439125,39.1046280625,20.552994125,40.7401830625,22.615217125C41.4512940625,23.539663125,41.8779620625,24.606331125,41.8779620625,25.744110125C41.8779620625,26.384107125,41.7357390625,26.881883125,41.522406062499996,27.450775125L43.584627062500005,27.450775125C45.2201820625,27.450775125,46.500181062500005,26.241884125,46.500181062500005,24.677442125C46.4290710625,23.895220125,46.2157380625,23.326330125,45.7890700625,22.828550125Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/kehuwanglai.svg b/src/assets/system/kehuwanglai.svg
new file mode 100644
index 0000000..41a62c0
--- /dev/null
+++ b/src/assets/system/kehuwanglai.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35644"><rect x="15" y="0" width="31" height="31" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35644)"><path d="M41.009501,2.634513605C38.250498,2.634513605,36.018496999999996,4.122513425,36.018496999999996,5.9515133250000005C36.018496999999996,7.191513025,37.0105,8.245513425,38.498497,8.834512725C38.436499,9.237513025,38.281496000000004,9.671513125,38.033497,10.136512725C38.033497,10.136512725,39.428499,10.012512725,40.141498999999996,9.237512625C40.420497999999995,9.268512225,40.699499,9.299512825,41.009501,9.299512825C43.768496999999996,9.299512825,46.0005,7.811512425,46.0005,5.982512724999999C46.0005,4.122513025,43.768496999999996,2.634513143,41.009501,2.634513605ZM38.994499000000005,6.416513425C38.591497000000004,6.416513425,38.281498,6.106513525,38.281498,5.703513425000001C38.281498,5.3005135249999995,38.591497000000004,4.990513325,38.994499000000005,4.990513325C39.397501,4.990513325,39.707499,5.3005135249999995,39.707499,5.703513425000001C39.707499,6.0755131250000005,39.366499000000005,6.416513425,38.994499000000005,6.416513425ZM41.133500999999995,6.416513425C40.730498999999995,6.416513425,40.420497999999995,6.106513525,40.420497999999995,5.703513425000001C40.420497999999995,5.3005135249999995,40.730498999999995,4.990513325,41.133500999999995,4.990513325C41.536499,4.990513325,41.846498,5.3005135249999995,41.846498,5.703513425000001C41.846498,6.0755131250000005,41.505499,6.416513425,41.133500999999995,6.416513425ZM43.272501,6.416513425C42.869499000000005,6.416513425,42.5595,6.106513525,42.5595,5.703513425000001C42.5595,5.3005135249999995,42.869499000000005,4.990513325,43.272501,4.990513325C43.675499,4.990513325,43.985498,5.3005135249999995,43.985498,5.703513425000001C43.985498,6.0755131250000005,43.644498999999996,6.416513425,43.272501,6.416513425ZM24.9825001,20.335512625C25.478499,20.180513625,25.9745,19.932514625,26.408499,19.684512625C26.594501,19.560512625,26.718499,19.467510625,26.780499,19.343513625C26.842501,19.250511625,26.8735,19.095512625,26.904501,18.909513625C26.873500999999997,18.754514625,26.842501,18.599513625,26.842501,18.506513625C26.842501,18.413512625,26.811501,18.320512625,26.780499,18.258511625C26.749499999999998,18.196513625,26.6875,18.134511625000002,26.625500000000002,18.072512625C26.563499,18.010514625,26.4705,17.917513624999998,26.346499,17.824512625C26.2225,17.731510625,26.098501,17.576512625,26.005499999999998,17.359513624999998C25.912499,17.142513625,25.819499999999998,16.925514624999998,25.7575,16.739514624999998C25.695498999999998,16.491513625,25.633499,16.243514625,25.5405,15.964514625C25.447499999999998,15.933514625,25.3235,15.871514625,25.2305,15.747513625C25.1375,15.654513625,25.0445004,15.530512625,24.9514999,15.375512625C24.8585014,15.220513625,24.7655001,15.003514625,24.6724997,14.724513625C24.5795012,14.445513625,24.5485001,14.197513625,24.5795012,13.980514625C24.610499400000002,13.763513625,24.6415005,13.546514625,24.7035007,13.391513625C24.765501,13.205513625,24.8585014,13.019513625,25.0135012,12.895513625C25.0135012,12.306513825,25.0445004,11.717514025,25.1375,11.097515125C25.2305,10.601513825,25.354501,10.074514425,25.509501,9.485514625C25.6645,8.896514925,25.943500999999998,8.369514425,26.2845,7.904515225C26.563499999999998,7.532515525,26.873500999999997,7.160514825,27.2455,6.788515125C27.6175,6.416515125,27.958501,6.137515325,28.268499,5.951514925C28.1755,5.641515025,28.0825,5.331515325,28.020501,5.083515425C27.958501,4.835515525,27.8345,4.5565153249999995,27.5865,4.2775156249999995C27.2455,3.874515725,26.749499999999998,3.5025157350000002,26.067500000000003,3.130515695C25.385502000000002,2.758515705,24.6105013,2.603515625,23.7735014,2.603515625C23.4325008,2.603515625,23.0915012,2.634515684,22.7195001,2.665515512C22.3475003,2.727515395,22.0065007,2.820515585,21.665499699999998,2.944515375C21.3244996,3.099515435,20.983500499999998,3.285515305,20.6424999,3.533515335C20.301499800000002,3.781515525,19.9915004,4.122515325,19.7125006,4.525515325C19.4025002,4.959515325,19.1855001,5.424515025,19.0304999,5.951514925C18.8755002,6.4785151249999995,18.7825,6.974514925,18.6894999,7.439515125C18.6275001,7.997515725,18.5964999,8.555515725,18.6275001,9.082515225C18.5035,9.206515325,18.4105,9.361515525,18.3485,9.547515425C18.2865002,9.702514625,18.2245002,9.888515025,18.2245002,10.105515024999999C18.1934998,10.322515025000001,18.2555001,10.539514525000001,18.3175001,10.787514725C18.4104998,11.035512925,18.5034997,11.221514725,18.5964999,11.345515225C18.6894999,11.469514825,18.7825,11.593514425,18.8755002,11.655514725C18.9685004,11.717514025,19.0925002,11.779515225,19.1855001,11.810515425C19.2475004,12.058512725,19.3095007,12.306513825,19.4025006,12.492514625C19.4645004,12.678514625,19.557500400000002,12.864513625,19.6505008,13.050515625C19.7435007,13.236513625,19.8675003,13.360515625,19.9915004,13.453514625C20.2395,13.670513625,20.4565005,13.856515625,20.6424999,14.042514625C20.8285003,14.228514625,20.9525008,14.507514625,20.983501,14.848514625C21.0145001,15.065514625,21.0145001,15.282513625,21.0145001,15.468516625C21.0145001,15.654515625,20.983501,15.840515625,20.9215002,16.026515625000002C20.8595004,16.212514625,20.7355008,16.367514625,20.5805006,16.553516625C20.4255004,16.708515625,20.1775002,16.925516625,19.8674998,17.111516625C19.4645,17.359516624999998,19.0304999,17.545515625,18.5345001,17.669515625000003C18.0384998,17.793516625000002,17.5424998,17.948516625,17.0775001,18.103517625000002C16.6124998,18.258515625,16.1785001,18.475516624999997,15.837499919999999,18.754516625C15.49650002,19.033515625,15.27950002,19.436517625,15.18649999,19.963516625C15.062499985564498,20.924516625,15.031499993,21.699516625,15.09349999,22.288516625C15.155499987,22.877515625,15.27950002,23.249513625,15.43449998,23.373515625C15.5275,23.466518625,15.77549994,23.528514625,16.1785001,23.621517625C16.5814999,23.714517625,17.0465,23.776515625,17.6045001,23.869516625C18.1624997,23.962518625,18.7825,24.024518625,19.4645,24.086519625C20.1465001,24.148517625,20.8285003,24.210517625,21.479500299999998,24.272518625C21.510500399999998,24.241519625,21.510500399999998,24.210517625,21.5415006,24.179517625C21.5725002,24.148517625,21.5725002,24.117519625,21.6035004,24.086515625C21.6345005,23.931516625,21.6345005,23.776515625,21.6345005,23.590516625C21.6345005,23.404518625,21.6655006,23.218515625,21.6965003,23.032514625C21.7895012,22.412517625,22.0065007,21.947515625,22.3475003,21.606515625C22.6885009,21.265518625,23.091499300000002,21.017516625,23.556501400000002,20.800517625C23.9905005,20.707515625,24.4865007,20.521516625,24.9825001,20.335512625Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M40.48196953125,23.59297175C40.35796753125,23.22097175,40.109968531250004,22.91097275,39.79996853125,22.66297375C39.48996953125,22.41497375,39.11797053125,22.22897475,38.71496853125,22.07397375C38.31196653125,21.91897375,37.90896753125,21.79497375,37.47496753125,21.67097375C37.040967531250004,21.546972750000002,36.63796753125,21.42297375,36.296967531250004,21.26797275C35.893968531249996,21.11297275,35.58396853125,20.92697475,35.33596753125,20.74097175C35.11896653125,20.52397175,34.93296753125,20.33797275,34.83996853125,20.08997175C34.74696753125,19.87297075,34.68496753125,19.65597175,34.68496753125,19.40797175C34.68496753125,19.19097175,34.68496753125,18.94297075,34.71596853125,18.725970750000002C34.74696753125,18.57097075,34.80896953125,18.41597075,34.87096853125,18.29197075C34.93296753125,18.16797175,34.99496853125,18.07496975,35.08796853125,17.98197175L35.36696953125,17.70297175C35.49096853125,17.609969749999998,35.61497053125,17.51697175,35.73896953125,17.39297075C35.86296953125,17.29997075,35.95597053125,17.175970749999998,36.07996853125,16.989970749999998C36.172969531250004,16.803971750000002,36.265968531249996,16.617970749999998,36.32796953125,16.43197155C36.38996853125,16.21496965,36.45197053125,15.99797055,36.54497053125,15.74997045C36.69997053125,15.71897025,36.82397053125,15.62597085,36.94797053125,15.53296945C37.04097153125,15.43996905,37.16497253125,15.28496935,37.25797053125,15.09896945C37.350971531249996,14.91296765,37.41297053125,14.66496945,37.47497053125,14.35496955C37.50597053125,14.106969849999999,37.50597053125,13.920969450000001,37.47497053125,13.76596975C37.44397153125,13.61096955,37.38197153125,13.48696945,37.31997153125,13.36296935C37.25797153125,13.23896885,37.16497253125,13.14596935,37.10297153125,13.08396915C37.13397053125,12.49496885,37.07197153125,11.93696925,37.00996953125,11.34796905C36.916969531250004,10.85196925,36.79296953125,10.324969750000001,36.60696953125,9.766969249999999C36.42097053125,9.20896915,36.14196953125,8.68196945,35.76996953125,8.21696885C35.45997053125,7.81396865,34.93296853125,7.44196874,34.28196953125,7.06996888C33.59996953125,6.69796906,32.82497023125,6.54296875,31.95697023125,6.54296875C31.64697073125,6.54296875,31.30596923125,6.573968815,30.93396993125,6.604968879C30.56197023125,6.66696901,30.22097063125,6.75996874,29.84897093125,6.88396898C29.47697063125,7.03896886,29.13597063125,7.22496873,28.79497053125,7.47296876C28.45396993125,7.72096885,28.143970531249998,8.06196915,27.86497073125,8.46496855C27.554970731250002,8.89896845,27.33797073125,9.39496855,27.182970731250002,9.89096855C27.02797103125,10.417968049999999,26.934971131250002,10.913968050000001,26.87297103125,11.37896875C26.81097103125,11.93696925,26.77997133125,12.49496885,26.81097103125,13.021969349999999C26.68697073125,13.14596935,26.59397073125,13.30096915,26.53197073125,13.48696895C26.46997073125,13.610968549999999,26.40797063125,13.796967949999999,26.40797063125,14.01396845C26.37697103125,14.23096795,26.43897013125,14.447968450000001,26.50097013125,14.69596955C26.59397003125,14.94396785,26.68697073125,15.12996865,26.77997043125,15.25397015C26.87297033125,15.37796975,26.96597103125,15.50196835,27.05897043125,15.56396865C27.15197013125,15.62596795,27.275970431250002,15.68797015,27.36897043125,15.71896835C27.43097023125,15.96696945,27.49297043125,16.21496775,27.58596993125,16.40096955C27.64797063125,16.58696975,27.740970131250002,16.77296975,27.83396963125,16.95896875C27.926969531250002,17.14496875,28.01996993125,17.299969750000002,28.14396953125,17.39296975C28.39196873125,17.57896975,28.60896923125,17.76496975,28.79496913125,17.95096975C29.01196953125,18.13696975,29.10496953125,18.415969750000002,29.16696933125,18.75697075C29.19796893125,19.00497175,29.19796893125,19.22197075,29.19796893125,19.40797175C29.19796893125,19.59397075,29.16696933125,19.77997275,29.07396843125,19.934971750000003C29.01196813125,20.08997175,28.88796803125,20.27597175,28.70196913125,20.43097075C28.54696893125,20.58597175,28.29896883125,20.802972750000002,27.98896883125,20.98897075C27.58596853125,21.236971750000002,27.15196823125,21.422970749999998,26.65596843125,21.54697175C26.15996813125,21.67097275,25.66396833125,21.82597275,25.19896863125,21.980970749999997C24.73396893125,22.13597075,24.29996903125,22.38397175,23.95896882125,22.63197375C23.61796862125,22.91097275,23.40096862125,23.31397475,23.30796889125,23.84097075C23.18396863345,24.77097175,23.15296903225,25.54597075,23.24596876325,26.13497175C23.33896848125,26.72397075,23.43196915125,27.09597175,23.58696902125,27.21997275C23.67996874125,27.28197075,23.86596912125,27.37497175,24.20696933125,27.43697375C24.54796953125,27.49897175,24.950969131249998,27.59197475,25.41596933125,27.65397275C25.91196943125,27.71597275,26.43896933125,27.80897375,26.99696943125,27.87097575C27.58596943125,27.93297175,28.17496923125,27.99497375,28.76396993125,28.02597475C29.352969631249998,28.08797475,29.94196983125,28.11897475,30.46897033125,28.14997675C31.026969431250002,28.18097475,31.52297023125,28.21197275,31.92597003125,28.21197275C32.32897093125,28.21197275,32.85596943125,28.18097475,33.41397053125,28.14997675C33.97196953125,28.11897475,34.56097053125,28.08797675,35.14997053125,28.02597475C35.73896953125,27.96397575,36.35896853125,27.90197375,36.94797053125,27.83997575C37.53697053125,27.77797475,38.094971531249996,27.68497475,38.59097053125,27.59197475C39.08696753125,27.49897375,39.48996953125,27.40597575,39.83096853125,27.31297675C40.17197253125,27.21997475,40.35797153125,27.09597575,40.41997153125,27.00297575C40.51297153125,26.84797475,40.60597053125,26.63097375,40.63697053125,26.32097675C40.69896853125,26.01097475,40.69896853125,25.70097575,40.69896853125,25.32897375C40.69896853125,24.98797375,40.66797253125,24.64697475,40.63697053125,24.30597475C40.60597453125,24.11997375,40.54397053125,23.84097675,40.48196953125,23.59297175Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/kucunguanli.svg b/src/assets/system/kucunguanli.svg
new file mode 100644
index 0000000..85b1219
--- /dev/null
+++ b/src/assets/system/kucunguanli.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="61.90625" viewBox="0 0 59.5 61.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35878"><rect x="13.5" y="0" width="32" height="32" rx="0"/></clipPath></defs><path d="M1.204819142818451,25.096379L28.69276814281845,41.307213000000004L58.29516614281845,25.096379L28.69276814281845,11L1.204819142818451,25.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,25.065809L28.43877614281845,41.737896L28.68361514281845,41.882288L59.39288714281845,25.065309L28.684561142818453,10.44229615L0.16870074281845082,25.065809ZM28.70192014281845,40.732138L2.240937742818451,25.12695L28.70097614281845,11.55770427L57.19744514281845,25.127449L28.70192014281845,40.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,25.80120277404785L0.5,44.83131777404785L28.692764,61.042150774047855L28.692764,42.01204077404785L0.5,25.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,61.906411774047854L29.192764,41.722777774047856L28.941999,41.578587774047854L-5.999999996841865e-8,24.93693918404785L0,45.12058077404785L29.192764,61.906411774047854ZM28.192764,42.30130377404785L28.192764,60.17788877404785L1,44.542054774047855L1,26.66546636404785L28.192764,42.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,25.80120277404785L59,44.12649377404785L88.5,61.00042177404785L88.5,41.26604677404785L59,25.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,61.86243677404785L89,40.96362277404785L88.732151,40.82320777404785L58.50000003,24.97454732404785L58.50000003,44.41651177404785L89,61.86243677404785ZM88,41.568469774047855L88,60.13840677404785L59.5,43.836475774047855L59.5,26.627858224047852L88,41.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35878)"><path d="M38.91415821875,30.224147875L20.39949081875,30.224147875C19.11332841875,30.269017875,18.02515268375,29.281917875,17.94482421875,27.997483875L17.94482421875,7.246370275C17.94482421875,6.032147875,19.124379618749998,5.126370475,20.45460221875,5.126370475L22.12793541875,5.126370475L22.138602218750002,5.526370575C22.14790441875,7.155996275,23.47563411875,8.470001674999999,25.10526891875,8.462370875000001L34.10704621875,8.462370875000001C35.73682621875,8.469508675,37.064411218749996,7.155250075,37.07371321875,5.525481975L37.18660121875,5.125481375L38.85993421875,5.125481375C40.29237921875,5.125481375,41.36971321875,6.030815075,41.36971321875,7.245481475L41.36971321875,28.401923875C41.07641021875,29.507728875,40.05750421875,30.263835875,38.91415821875,30.224147875ZM27.76704601875,13.421037875C27.37507341875,13.016912875,26.72657391875,13.016912875,26.334601418749997,13.421037875L23.87904551875,15.850370875L22.44748971875,14.433926875C22.05595681875,14.028763875,21.40657731875,14.028763875,21.01504511875,14.433926875C20.60587361875,14.817425875,20.60587361875,15.466869875,21.01504511875,15.850370875L23.05949071875,17.874814875C23.05845741875,17.932044875,23.10538391875,17.978566875,23.16260101875,17.977037875C23.219183918749998,17.974988875,23.26588101875,18.020871875,23.26482391875,18.077481875C23.65635581875,18.482642875,24.30573561875,18.482642875,24.69726801875,18.077481875L27.97015721875,14.838815875C28.42443721875,14.427935875,28.31841421875,13.687848875,27.76704601875,13.421037875ZM27.76704601875,19.998815875C27.37551311875,19.593654875,26.72613331875,19.593654875,26.334601418749997,19.998815875L23.87904551875,22.428149875L22.44748971875,21.010369875C22.05595591875,20.605210875,21.40657731875,20.605210875,21.01504511875,21.010369875C20.60547471875,21.394223875,20.60547471875,22.044293875,21.01504511875,22.428149875L23.05949071875,24.452592875C23.05896661875,24.509471875,23.10573861875,24.555438875,23.16260101875,24.553926875C23.21932891875,24.552398875,23.26585721875,24.598519875,23.26482391875,24.655261875C23.65635581875,25.060420875,24.30573561875,25.060420875,24.69726801875,24.655261875L27.97015721875,21.517037875C28.378157218749998,21.113927875,28.378157218749998,20.507260875,27.767046918749998,20.002814875L27.767046918749998,19.998370875L27.76704601875,19.998815875ZM38.12260221875,13.496593875L32.89415821875,13.496593875C32.01108221875,13.554154875,31.324264218750002,14.287201875,31.324264218750002,15.172150875C31.324264218750002,16.057098875,32.01108421875,16.790143875,32.89415821875,16.847704874999998L38.12260221875,16.847704874999998C39.02518621875,16.833244875,39.73615621875,16.073720875,39.69104721875,15.172148875C39.69799421875,14.285016875,39.008262218750005,13.548182875,38.12260221875,13.496593875ZM38.12260221875,20.183259875L32.89415821875,20.183259875C32.01249721875,20.242505875,31.32762721875,20.974942875,31.32762721875,21.858592875C31.327630218750002,22.742242875,32.01249921875,23.474679875,32.89415821875,23.533925875L38.12260221875,23.533925875C39.04142221875,23.558019875,39.77521321875,22.774531875,39.69104721875,21.859258875C39.69778221875,20.972129875,39.00823021875,20.235292875,38.12260221875,20.183259875ZM33.328378218750004,6.798814775L25.98926831875,6.798814775C24.81208661875,6.811668375,23.84158901875,5.879128975,23.80749031875,4.702370175C23.84182641875,3.525786675,24.81225441875,2.593511875,25.98926831875,2.6063706250000003L26.461268418750002,2.6063706250000003C26.68953801875,2.092310075,27.20474621875,1.766140461,27.76704601875,1.77970385551L31.55193621875,1.77970385551C32.113917218750004,1.766503215,32.62868121875,2.092614055,32.856825218750004,2.6063706250000003L33.33015921875,2.6063706250000003C34.50716021875,2.593498345,35.47777521875,3.5253773749999997,35.512825218749995,4.701925975C35.60971421875,5.845481375,34.61815821875,6.798369875,33.32837921875,6.798369875L33.328378218750004,6.798814775Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/rukuguanli.svg b/src/assets/system/rukuguanli.svg
new file mode 100644
index 0000000..bb7df01
--- /dev/null
+++ b/src/assets/system/rukuguanli.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35860"><rect x="12.5" y="0" width="34" height="34.000003814697266" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35860)"><path d="M35.790823312499995,24.330472625L40.532228312499996,24.330472625C41.1830103125,24.330472625,41.7109393125,24.858400625,41.7109393125,25.509183625L41.7109393125,27.893161625C41.7109393125,28.543947625,41.1830103125,29.071872625,40.532228312499996,29.071872625L35.790823312499995,29.071872625L35.790823312499995,30.483011625C35.790823312499995,31.469142625,34.6519513125,32.020316625,33.8783243125,31.409372625L29.0505873125,27.587697625C28.4496093125,27.112893625,28.4529313125,26.203126625,29.0572293125,25.731641625L33.8849623125,21.963086625C34.658598312500004,21.358793625,35.790823312499995,21.909965625,35.790823312499995,22.892774625L35.790823312499995,24.330472625ZM27.028485312500003,5.268882525L17.2591470125,13.108853325C16.3131175025,13.868049625,15.7626953125,15.015515625,15.7626953125,16.228507625L15.7626953125,26.801757625C15.7626953125,29.010896625,17.5535564125,30.801757625,19.7626958125,30.801757625L29.9935593125,30.801757625L27.8519553125,29.105081625C27.0982433125,28.507425625,26.6666033125,27.614259625,26.6699233125,26.651368625C26.6732453125,25.688478625,27.1115253125,24.798635625,27.868557312500002,24.207618625L32.6962933125,20.439060625C33.250789312500004,20.004102625,33.9115273125,19.778322625,34.6087933125,19.778322625C36.159378312499996,19.778322625,37.4476603125,20.913871625,37.6867213125,22.394727625L42.5277363125,22.394727625C42.796682312499996,22.394727625,43.052347312500004,22.441215625,43.3013703125,22.504300625L43.3013703125,16.228507625C43.3013703125,15.015515625,42.7509483125,13.868049625,41.8049183125,13.108853325L32.0355813125,5.268882725C30.5729253125,4.095089075,28.4911423125,4.095089075,27.028485312500003,5.268882525M23.8476562125,28.381252625L18.4587896125,28.381252625L18.4587896125,22.989066625L23.8509788125,22.989066625L23.8509788125,28.381252625L23.8476562125,28.381252625ZM23.8476562125,20.422456625L18.4587896125,20.422456625L18.4587896125,15.030274625L23.8509788125,15.030274625L23.8509788125,20.422456625L23.8476562125,20.422456625Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/shengchandingdan.svg b/src/assets/system/shengchandingdan.svg
new file mode 100644
index 0000000..8e3ff7f
--- /dev/null
+++ b/src/assets/system/shengchandingdan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="62.90625" viewBox="0 0 59.5 62.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35796"><rect x="15" y="0" width="31" height="31" rx="0"/></clipPath></defs><path d="M1.204819142818451,26.096379L28.69276814281845,42.307213000000004L58.29516614281845,26.096379L28.69276814281845,12L1.204819142818451,26.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,26.065809L28.43877614281845,42.737896L28.68361514281845,42.882288L59.39288714281845,26.065309L28.684561142818453,11.44229615L0.16870074281845082,26.065809ZM28.70192014281845,41.732138L2.240937742818451,26.12695L28.70097614281845,12.55770427L57.19744514281845,26.127449L28.70192014281845,41.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,26.80120277404785L0.5,45.83131777404785L28.692764,62.042150774047855L28.692764,43.01204077404785L0.5,26.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,62.906411774047854L29.192764,42.722777774047856L28.941999,42.578587774047854L-5.999999996841865e-8,25.93693918404785L0,46.12058077404785L29.192764,62.906411774047854ZM28.192764,43.30130377404785L28.192764,61.17788877404785L1,45.542054774047855L1,27.66546636404785L28.192764,43.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,26.80120277404785L59,45.12649377404785L88.5,62.00042177404785L88.5,42.26604677404785L59,26.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,62.86243677404785L89,41.96362277404785L88.732151,41.82320777404785L58.50000003,25.97454732404785L58.50000003,45.41651177404785L89,62.86243677404785ZM88,42.568469774047855L88,61.13840677404785L59.5,44.836475774047855L59.5,27.627858224047852L88,42.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35796)"><path d="M41.009501,2.634513605C38.250498,2.634513605,36.018496999999996,4.122513425,36.018496999999996,5.9515133250000005C36.018496999999996,7.191513025,37.0105,8.245513425,38.498497,8.834512725C38.436499,9.237513025,38.281496000000004,9.671513125,38.033497,10.136512725C38.033497,10.136512725,39.428499,10.012512725,40.141498999999996,9.237512625C40.420497999999995,9.268512225,40.699499,9.299512825,41.009501,9.299512825C43.768496999999996,9.299512825,46.0005,7.811512425,46.0005,5.982512724999999C46.0005,4.122513025,43.768496999999996,2.634513143,41.009501,2.634513605ZM38.994499000000005,6.416513425C38.591497000000004,6.416513425,38.281498,6.106513525,38.281498,5.703513425000001C38.281498,5.3005135249999995,38.591497000000004,4.990513325,38.994499000000005,4.990513325C39.397501,4.990513325,39.707499,5.3005135249999995,39.707499,5.703513425000001C39.707499,6.0755131250000005,39.366499000000005,6.416513425,38.994499000000005,6.416513425ZM41.133500999999995,6.416513425C40.730498999999995,6.416513425,40.420497999999995,6.106513525,40.420497999999995,5.703513425000001C40.420497999999995,5.3005135249999995,40.730498999999995,4.990513325,41.133500999999995,4.990513325C41.536499,4.990513325,41.846498,5.3005135249999995,41.846498,5.703513425000001C41.846498,6.0755131250000005,41.505499,6.416513425,41.133500999999995,6.416513425ZM43.272501,6.416513425C42.869499000000005,6.416513425,42.5595,6.106513525,42.5595,5.703513425000001C42.5595,5.3005135249999995,42.869499000000005,4.990513325,43.272501,4.990513325C43.675499,4.990513325,43.985498,5.3005135249999995,43.985498,5.703513425000001C43.985498,6.0755131250000005,43.644498999999996,6.416513425,43.272501,6.416513425ZM24.9825001,20.335512625C25.478499,20.180513625,25.9745,19.932514625,26.408499,19.684512625C26.594501,19.560512625,26.718499,19.467510625,26.780499,19.343513625C26.842501,19.250511625,26.8735,19.095512625,26.904501,18.909513625C26.873500999999997,18.754514625,26.842501,18.599513625,26.842501,18.506513625C26.842501,18.413512625,26.811501,18.320512625,26.780499,18.258511625C26.749499999999998,18.196513625,26.6875,18.134511625000002,26.625500000000002,18.072512625C26.563499,18.010514625,26.4705,17.917513624999998,26.346499,17.824512625C26.2225,17.731510625,26.098501,17.576512625,26.005499999999998,17.359513624999998C25.912499,17.142513625,25.819499999999998,16.925514624999998,25.7575,16.739514624999998C25.695498999999998,16.491513625,25.633499,16.243514625,25.5405,15.964514625C25.447499999999998,15.933514625,25.3235,15.871514625,25.2305,15.747513625C25.1375,15.654513625,25.0445004,15.530512625,24.9514999,15.375512625C24.8585014,15.220513625,24.7655001,15.003514625,24.6724997,14.724513625C24.5795012,14.445513625,24.5485001,14.197513625,24.5795012,13.980514625C24.610499400000002,13.763513625,24.6415005,13.546514625,24.7035007,13.391513625C24.765501,13.205513625,24.8585014,13.019513625,25.0135012,12.895513625C25.0135012,12.306513825,25.0445004,11.717514025,25.1375,11.097515125C25.2305,10.601513825,25.354501,10.074514425,25.509501,9.485514625C25.6645,8.896514925,25.943500999999998,8.369514425,26.2845,7.904515225C26.563499999999998,7.532515525,26.873500999999997,7.160514825,27.2455,6.788515125C27.6175,6.416515125,27.958501,6.137515325,28.268499,5.951514925C28.1755,5.641515025,28.0825,5.331515325,28.020501,5.083515425C27.958501,4.835515525,27.8345,4.5565153249999995,27.5865,4.2775156249999995C27.2455,3.874515725,26.749499999999998,3.5025157350000002,26.067500000000003,3.130515695C25.385502000000002,2.758515705,24.6105013,2.603515625,23.7735014,2.603515625C23.4325008,2.603515625,23.0915012,2.634515684,22.7195001,2.665515512C22.3475003,2.727515395,22.0065007,2.820515585,21.665499699999998,2.944515375C21.3244996,3.099515435,20.983500499999998,3.285515305,20.6424999,3.533515335C20.301499800000002,3.781515525,19.9915004,4.122515325,19.7125006,4.525515325C19.4025002,4.959515325,19.1855001,5.424515025,19.0304999,5.951514925C18.8755002,6.4785151249999995,18.7825,6.974514925,18.6894999,7.439515125C18.6275001,7.997515725,18.5964999,8.555515725,18.6275001,9.082515225C18.5035,9.206515325,18.4105,9.361515525,18.3485,9.547515425C18.2865002,9.702514625,18.2245002,9.888515025,18.2245002,10.105515024999999C18.1934998,10.322515025000001,18.2555001,10.539514525000001,18.3175001,10.787514725C18.4104998,11.035512925,18.5034997,11.221514725,18.5964999,11.345515225C18.6894999,11.469514825,18.7825,11.593514425,18.8755002,11.655514725C18.9685004,11.717514025,19.0925002,11.779515225,19.1855001,11.810515425C19.2475004,12.058512725,19.3095007,12.306513825,19.4025006,12.492514625C19.4645004,12.678514625,19.557500400000002,12.864513625,19.6505008,13.050515625C19.7435007,13.236513625,19.8675003,13.360515625,19.9915004,13.453514625C20.2395,13.670513625,20.4565005,13.856515625,20.6424999,14.042514625C20.8285003,14.228514625,20.9525008,14.507514625,20.983501,14.848514625C21.0145001,15.065514625,21.0145001,15.282513625,21.0145001,15.468516625C21.0145001,15.654515625,20.983501,15.840515625,20.9215002,16.026515625000002C20.8595004,16.212514625,20.7355008,16.367514625,20.5805006,16.553516625C20.4255004,16.708515625,20.1775002,16.925516625,19.8674998,17.111516625C19.4645,17.359516624999998,19.0304999,17.545515625,18.5345001,17.669515625000003C18.0384998,17.793516625000002,17.5424998,17.948516625,17.0775001,18.103517625000002C16.6124998,18.258515625,16.1785001,18.475516624999997,15.837499919999999,18.754516625C15.49650002,19.033515625,15.27950002,19.436517625,15.18649999,19.963516625C15.062499985564498,20.924516625,15.031499993,21.699516625,15.09349999,22.288516625C15.155499987,22.877515625,15.27950002,23.249513625,15.43449998,23.373515625C15.5275,23.466518625,15.77549994,23.528514625,16.1785001,23.621517625C16.5814999,23.714517625,17.0465,23.776515625,17.6045001,23.869516625C18.1624997,23.962518625,18.7825,24.024518625,19.4645,24.086519625C20.1465001,24.148517625,20.8285003,24.210517625,21.479500299999998,24.272518625C21.510500399999998,24.241519625,21.510500399999998,24.210517625,21.5415006,24.179517625C21.5725002,24.148517625,21.5725002,24.117519625,21.6035004,24.086515625C21.6345005,23.931516625,21.6345005,23.776515625,21.6345005,23.590516625C21.6345005,23.404518625,21.6655006,23.218515625,21.6965003,23.032514625C21.7895012,22.412517625,22.0065007,21.947515625,22.3475003,21.606515625C22.6885009,21.265518625,23.091499300000002,21.017516625,23.556501400000002,20.800517625C23.9905005,20.707515625,24.4865007,20.521516625,24.9825001,20.335512625Z" fill="#3BB078" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M40.48196953125,23.59297175C40.35796753125,23.22097175,40.109968531250004,22.91097275,39.79996853125,22.66297375C39.48996953125,22.41497375,39.11797053125,22.22897475,38.71496853125,22.07397375C38.31196653125,21.91897375,37.90896753125,21.79497375,37.47496753125,21.67097375C37.040967531250004,21.546972750000002,36.63796753125,21.42297375,36.296967531250004,21.26797275C35.893968531249996,21.11297275,35.58396853125,20.92697475,35.33596753125,20.74097175C35.11896653125,20.52397175,34.93296753125,20.33797275,34.83996853125,20.08997175C34.74696753125,19.87297075,34.68496753125,19.65597175,34.68496753125,19.40797175C34.68496753125,19.19097175,34.68496753125,18.94297075,34.71596853125,18.725970750000002C34.74696753125,18.57097075,34.80896953125,18.41597075,34.87096853125,18.29197075C34.93296753125,18.16797175,34.99496853125,18.07496975,35.08796853125,17.98197175L35.36696953125,17.70297175C35.49096853125,17.609969749999998,35.61497053125,17.51697175,35.73896953125,17.39297075C35.86296953125,17.29997075,35.95597053125,17.175970749999998,36.07996853125,16.989970749999998C36.172969531250004,16.803971750000002,36.265968531249996,16.617970749999998,36.32796953125,16.43197155C36.38996853125,16.21496965,36.45197053125,15.99797055,36.54497053125,15.74997045C36.69997053125,15.71897025,36.82397053125,15.62597085,36.94797053125,15.53296945C37.04097153125,15.43996905,37.16497253125,15.28496935,37.25797053125,15.09896945C37.350971531249996,14.91296765,37.41297053125,14.66496945,37.47497053125,14.35496955C37.50597053125,14.106969849999999,37.50597053125,13.920969450000001,37.47497053125,13.76596975C37.44397153125,13.61096955,37.38197153125,13.48696945,37.31997153125,13.36296935C37.25797153125,13.23896885,37.16497253125,13.14596935,37.10297153125,13.08396915C37.13397053125,12.49496885,37.07197153125,11.93696925,37.00996953125,11.34796905C36.916969531250004,10.85196925,36.79296953125,10.324969750000001,36.60696953125,9.766969249999999C36.42097053125,9.20896915,36.14196953125,8.68196945,35.76996953125,8.21696885C35.45997053125,7.81396865,34.93296853125,7.44196874,34.28196953125,7.06996888C33.59996953125,6.69796906,32.82497023125,6.54296875,31.95697023125,6.54296875C31.64697073125,6.54296875,31.30596923125,6.573968815,30.93396993125,6.604968879C30.56197023125,6.66696901,30.22097063125,6.75996874,29.84897093125,6.88396898C29.47697063125,7.03896886,29.13597063125,7.22496873,28.79497053125,7.47296876C28.45396993125,7.72096885,28.143970531249998,8.06196915,27.86497073125,8.46496855C27.554970731250002,8.89896845,27.33797073125,9.39496855,27.182970731250002,9.89096855C27.02797103125,10.417968049999999,26.934971131250002,10.913968050000001,26.87297103125,11.37896875C26.81097103125,11.93696925,26.77997133125,12.49496885,26.81097103125,13.021969349999999C26.68697073125,13.14596935,26.59397073125,13.30096915,26.53197073125,13.48696895C26.46997073125,13.610968549999999,26.40797063125,13.796967949999999,26.40797063125,14.01396845C26.37697103125,14.23096795,26.43897013125,14.447968450000001,26.50097013125,14.69596955C26.59397003125,14.94396785,26.68697073125,15.12996865,26.77997043125,15.25397015C26.87297033125,15.37796975,26.96597103125,15.50196835,27.05897043125,15.56396865C27.15197013125,15.62596795,27.275970431250002,15.68797015,27.36897043125,15.71896835C27.43097023125,15.96696945,27.49297043125,16.21496775,27.58596993125,16.40096955C27.64797063125,16.58696975,27.740970131250002,16.77296975,27.83396963125,16.95896875C27.926969531250002,17.14496875,28.01996993125,17.299969750000002,28.14396953125,17.39296975C28.39196873125,17.57896975,28.60896923125,17.76496975,28.79496913125,17.95096975C29.01196953125,18.13696975,29.10496953125,18.415969750000002,29.16696933125,18.75697075C29.19796893125,19.00497175,29.19796893125,19.22197075,29.19796893125,19.40797175C29.19796893125,19.59397075,29.16696933125,19.77997275,29.07396843125,19.934971750000003C29.01196813125,20.08997175,28.88796803125,20.27597175,28.70196913125,20.43097075C28.54696893125,20.58597175,28.29896883125,20.802972750000002,27.98896883125,20.98897075C27.58596853125,21.236971750000002,27.15196823125,21.422970749999998,26.65596843125,21.54697175C26.15996813125,21.67097275,25.66396833125,21.82597275,25.19896863125,21.980970749999997C24.73396893125,22.13597075,24.29996903125,22.38397175,23.95896882125,22.63197375C23.61796862125,22.91097275,23.40096862125,23.31397475,23.30796889125,23.84097075C23.18396863345,24.77097175,23.15296903225,25.54597075,23.24596876325,26.13497175C23.33896848125,26.72397075,23.43196915125,27.09597175,23.58696902125,27.21997275C23.67996874125,27.28197075,23.86596912125,27.37497175,24.20696933125,27.43697375C24.54796953125,27.49897175,24.950969131249998,27.59197475,25.41596933125,27.65397275C25.91196943125,27.71597275,26.43896933125,27.80897375,26.99696943125,27.87097575C27.58596943125,27.93297175,28.17496923125,27.99497375,28.76396993125,28.02597475C29.352969631249998,28.08797475,29.94196983125,28.11897475,30.46897033125,28.14997675C31.026969431250002,28.18097475,31.52297023125,28.21197275,31.92597003125,28.21197275C32.32897093125,28.21197275,32.85596943125,28.18097475,33.41397053125,28.14997675C33.97196953125,28.11897475,34.56097053125,28.08797675,35.14997053125,28.02597475C35.73896953125,27.96397575,36.35896853125,27.90197375,36.94797053125,27.83997575C37.53697053125,27.77797475,38.094971531249996,27.68497475,38.59097053125,27.59197475C39.08696753125,27.49897375,39.48996953125,27.40597575,39.83096853125,27.31297675C40.17197253125,27.21997475,40.35797153125,27.09597575,40.41997153125,27.00297575C40.51297153125,26.84797475,40.60597053125,26.63097375,40.63697053125,26.32097675C40.69896853125,26.01097475,40.69896853125,25.70097575,40.69896853125,25.32897375C40.69896853125,24.98797375,40.66797253125,24.64697475,40.63697053125,24.30597475C40.60597453125,24.11997375,40.54397053125,23.84097675,40.48196953125,23.59297175Z" fill="#3BB078" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/shengchanheduan.svg b/src/assets/system/shengchanheduan.svg
new file mode 100644
index 0000000..55d6f9e
--- /dev/null
+++ b/src/assets/system/shengchanheduan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="63.90625" viewBox="0 0 59.5 63.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35834"><rect x="14" y="0" width="31" height="31" rx="0"/></clipPath></defs><path d="M1.204819142818451,27.096379L28.69276814281845,43.307213000000004L58.29516614281845,27.096379L28.69276814281845,13L1.204819142818451,27.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,27.065809L28.43877614281845,43.737896L28.68361514281845,43.882288L59.39288714281845,27.065309L28.684561142818453,12.44229615L0.16870074281845082,27.065809ZM28.70192014281845,42.732138L2.240937742818451,27.12695L28.70097614281845,13.55770427L57.19744514281845,27.127449L28.70192014281845,42.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,27.80120277404785L0.5,46.83131777404785L28.692764,63.042150774047855L28.692764,44.01204077404785L0.5,27.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,63.906411774047854L29.192764,43.722777774047856L28.941999,43.578587774047854L-5.999999996841865e-8,26.93693918404785L0,47.12058077404785L29.192764,63.906411774047854ZM28.192764,44.30130377404785L28.192764,62.17788877404785L1,46.542054774047855L1,28.66546636404785L28.192764,44.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,27.80120277404785L59,46.12649377404785L88.5,63.00042177404785L88.5,43.26604677404785L59,27.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,63.86243677404785L89,42.96362277404785L88.732151,42.82320777404785L58.50000003,26.97454732404785L58.50000003,46.41651177404785L89,63.86243677404785ZM88,43.568469774047855L88,62.13840677404785L59.5,45.836475774047855L59.5,28.627858224047852L88,43.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35834)"><path d="M38.21875,2.90625L20.78125,2.90625C19.71562505,2.90625,18.84375,3.77812505,18.84375,4.84375L18.84375,6.78125L20.78125,6.78125C21.8468752,6.78125,22.71875,7.6531253,22.71875,8.71875C22.71875,9.7843752,21.8468752,10.65625,20.78125,10.65625L18.84375,10.65625L18.84375,13.5625L20.78125,13.5625C21.8468752,13.5625,22.71875,14.434375,22.71875,15.5C22.71875,16.565625,21.8468752,17.4375,20.78125,17.4375L18.84375,17.4375L18.84375,20.34375L20.78125,20.34375C21.8468752,20.34375,22.71875,21.215626,22.71875,22.28125C22.71875,23.346876,21.8468752,24.21875,20.78125,24.21875L18.84375,24.21875L18.84375,26.15625C18.84375,27.221876,19.71562505,28.09375,20.78125,28.09375L38.21875,28.09375C39.284376,28.09375,40.15625,27.221876,40.15625,26.15625L40.15625,4.84375C40.15625,3.77812505,39.284376,2.90625,38.21875,2.90625Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M33.12253475625,13.77256255L34.31409645625,13.77256255L34.31409645625,15.38262465L32.30103305625,15.38262465L31.88446995625,16.18668745L31.88446995625,17.00818685L34.31409455625,17.00818685L34.31409455625,18.616312049999998L31.88350005625,18.616312049999998L31.88350005625,20.88803025L29.66215705625,20.88803025L29.66215705625,18.617280049999998L27.21509462625,18.617280049999998L27.21509462625,17.00818685L29.66215705625,17.00818685L29.66215705625,16.18668745L29.24268885625,15.38262465L27.21509557625,15.38262465L27.21509557625,13.77256255L28.42215825625,13.77256255L26.21728515625,9.42578125L28.64884805625,9.42578125L30.77234835625,14.06124975L32.89778615625,9.42578125L35.31190965625,9.42578125L33.12253475625,13.77256255Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M20.78125,9.6875L16.90625,9.6875C16.37343752,9.6875,15.9375,9.2515626,15.9375,8.71875C15.9375,8.18593732,16.37343752,7.75,16.90625,7.75L20.78125,7.75C21.3140626,7.75,21.75,8.18593732,21.75,8.71875C21.75,9.2515626,21.3140626,9.6875,20.78125,9.6875ZM20.78125,16.46875L16.90625,16.46875C16.37343752,16.46875,15.9375,16.0328121,15.9375,15.5C15.9375,14.967187899999999,16.37343752,14.53125,16.90625,14.53125L20.78125,14.53125C21.3140626,14.53125,21.75,14.9671874,21.75,15.5C21.75,16.0328121,21.3140626,16.46875,20.78125,16.46875ZM20.78125,23.25L16.90625,23.25C16.37343752,23.25,15.9375,22.814062,15.9375,22.28125C15.9375,21.748438,16.37343752,21.3125,16.90625,21.3125L20.78125,21.3125C21.3140626,21.3125,21.75,21.748438,21.75,22.28125C21.75,22.814062,21.3140626,23.25,20.78125,23.25Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/shengchanpaichan.svg b/src/assets/system/shengchanpaichan.svg
new file mode 100644
index 0000000..0165a7b
--- /dev/null
+++ b/src/assets/system/shengchanpaichan.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35806"><rect x="14.5" y="0" width="32" height="32" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35806)"><path d="M25.2873048875,20.756252C25.2873048875,19.415625,25.5498046875,18.115626,26.0685548875,16.890625999999997C26.5685548875,15.706251,27.284178687500003,14.646876,28.1966796875,13.734375C29.1091806875,12.8218746,30.1685546875,12.1062508,31.3529306875,11.6062508C32.5779306875,11.0875001,33.8779296875,10.8250008,35.2185556875,10.8250008C36.5591816875,10.8250008,37.8591806875,11.0875001,39.0841806875,11.6062508C39.4623066875,11.765625,39.8248066875,11.9468746,40.1779306875,12.1499996L40.1779306875,3.90000015C40.1779306875,3.47187522,39.831058687500004,3.125,39.4029316875,3.125L31.9779286875,3.125C31.9935536875,3.196875095,31.9998046875,3.26874995,31.9998046875,3.34375001L31.9998046875,7.5062499C31.9998046875,8.1062503,31.5123046875,8.593749500000001,30.9123046875,8.593749500000001L26.846678687500003,8.593749500000001C26.2466792875,8.593749500000001,25.7591800875,8.1062493,25.7591800875,7.5062499L25.7591800875,3.34687544C25.7591800875,3.27187538,25.765430487499998,3.200000525,25.781055487499998,3.1281254294L18.3560545475,3.1281254294C17.9279296075,3.1281254294,17.5810546875,3.47500041,17.5810546875,3.90312535L17.5810546875,25.046877C17.5810546875,25.475,17.9279296075,25.821877,18.3560547275,25.821877L26.674805687499997,25.821877C26.446680987500002,25.437502,26.243555987500002,25.0375,26.0685557875,24.625002C25.5498056875,23.396875,25.2873057875,22.096874,25.2873048875,20.756252ZM35.2185556875,12.7000017C30.768552687499998,12.7000017,27.1623048875,16.30625,27.1623048875,20.756248C27.1623048875,25.206247,30.768552687499998,28.812502,35.2185556875,28.812502C39.6685566875,28.812502,43.2748026875,25.206249,43.2748026875,20.756252C43.2748026875,16.306252,39.6685566875,12.7000017,35.2185556875,12.7000017ZM38.7248036875,21.868752C37.8185536875,22.775002,36.5091776875,23.0375,35.3716816875,22.65625L33.2310546875,24.796877C32.6654306875,25.362499,31.7435556875,25.362499,31.1779296875,24.796877C30.612302687499998,24.231253,30.612305687499997,23.309378,31.1779296875,22.743752L33.3185536875,20.603125C32.9373046875,19.465628,33.1966786875,18.156250999999997,34.1060566875,17.250000999999997C35.0841786875,16.271876,36.5248066875,16.043751999999998,37.7185556875,16.562500999999997L35.6216816875,18.659374L35.9623066875,20L37.315427687500005,20.353125L39.4123036875,18.256251C39.9310526875,19.453125,39.6998066875,20.893751,38.7248036875,21.868752ZM31.846677687499998,23.475C31.846179687499998,23.836065,32.1387386875,24.129026,32.4998026875,24.129026C32.8608656875,24.129026,33.153425687500004,23.836065,33.1529276875,23.475C33.153425687500004,23.113937,32.8608656875,22.820976,32.4998026875,22.820976C32.1387386875,22.820976,31.8461786875,23.113937,31.846677687499998,23.475Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/shengchanpeizhi.svg b/src/assets/system/shengchanpeizhi.svg
new file mode 100644
index 0000000..ea6690b
--- /dev/null
+++ b/src/assets/system/shengchanpeizhi.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="89.5693359375" height="96" viewBox="0 0 89.5693359375 96"><defs><filter id="master_svg0_143_35998" filterUnits="objectBoundingBox" color-interpolation-filters="sRGB" x="-0.6666666666666666" y="-0.6666666666666666" width="2.3333333333333335" height="2.3333333333333335"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="0" dx="0"/><feGaussianBlur stdDeviation="8"/><feColorMatrix type="matrix" values="0 0 0 0 0.12895070016384125 0 0 0 0 0.29307788610458374 0 0 0 0 0.9107142686843872 0 0 0 0.5299999713897705 0"/><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><clipPath id="master_svg1_143_36001"><rect x="33.78466796875" y="35" width="24" height="24" rx="0"/></clipPath></defs><path d="M44.78466796875,24.000000000000004L65.56927496875,36L65.56927496875,60L44.78466796875,72L24.00005836875,60L24.00005796875,36.000001L44.78466796875,24.000000000000004Z" fill="#496FFE" fill-opacity="1" filter="url(#master_svg0_143_35998)"/><g clip-path="url(#master_svg1_143_36001)"><path d="M47.28466896875,59.000001999999995L36.49966816875,59.000001999999995C35.83063894875,59.000834999999995,35.28714856525,58.46003,35.28466796875,57.791002L35.28466796875,39.9020004C35.28796181125,39.2333269,35.83096992875,38.6930003,36.49966816875,38.6930003L38.24566816875,38.6930003L38.24566816875,43.6130009L51.271669968750004,43.6130009L51.271669968750004,38.6930003L53.01767196875,38.6930003C53.68817096875,38.6930003,54.23267196875,39.2359996,54.23267196875,39.902L54.23267196875,45.908001C50.739634968749996,44.6980333,46.885882968749996,46.213699,45.15295886875,49.479017999999996C43.420036268749996,52.744337,44.32480906875,56.785379,47.28466896875,59.000001999999995ZM50.08666996875,42.3755007L39.42916826875,42.3755007L39.42916826875,38.3915005C39.42916826875,37.5455,40.02766896875,36.8585002,40.76266856875,36.8585002L42.13816836875,36.8585002C42.63016936875,35.73050004,43.63966936875,35,44.75866886875,35C45.87616896875,35,46.88566996875,35.73050004,47.37766896875,36.8585002L48.75466996875,36.8585002C49.489668968749996,36.8585002,50.08666996875,37.5455003,50.08666996875,38.3915005L50.08666996875,42.3755007ZM38.24566816875,47.9075C38.24566816875,48.237501,38.51566836875,48.522501,38.84566856875,48.522501L44.15716836875,48.522501C44.49682516875,48.522501,44.77217006875,48.247156000000004,44.77217006875,47.9075C44.77217006875,47.567845,44.49682516875,47.292500000000004,44.15716836875,47.292500000000004L38.84716846875,47.292500000000004C38.51279706875,47.299842,38.24558806875,47.573049,38.24566816875,47.9075ZM38.24566816875,52.824503C38.24566816875,53.154503,38.51866816875,53.439501,38.85616846875,53.439501L42.96466876875,53.439501C43.30120946875,53.433825999999996,43.57196616875,53.161074,43.57516856875,52.824503C43.57516856875,52.494501,43.30216886875,52.209501,42.96466876875,52.209501L38.85466856875,52.209501C38.51812866875,52.215178,38.24737286875,52.48793,38.24416856875,52.824503L38.24566816875,52.824503ZM42.09316876875,40.5320001C42.59357406875,40.5229702,42.99186656875,40.1099033,42.98266936875,39.6095004C42.99102686875,39.1096783,42.59297896875,38.6975336,42.09316876875,38.6884999C41.59394886875,38.6983497,41.19680456875,39.1102533,41.20516876875,39.6095004C41.20516876875,40.1195002,41.60266826875,40.5320001,42.09316876875,40.5320001ZM47.42266896875,40.5320001C47.922485968749996,40.5221539,48.31987196875,40.1093283,48.31066996875,39.6095004C48.31903496875,39.1102524,47.92188996875,38.6983497,47.42266896875,38.6884999C46.923450968750004,38.6983497,46.52630596875,39.1102533,46.53466896875,39.6095004C46.53466896875,40.1195002,46.93217096875,40.5320001,47.42266896875,40.5320001ZM51.784669968749995,59.000001999999995C48.470961968750004,59.000001999999995,45.78466896875,56.313713,45.78466896875,53.000001999999995C45.78466896875,49.686294000000004,48.470961968750004,47.000001,51.784669968749995,47.000001C55.09838096875,47.000001,57.784669968749995,49.686294000000004,57.784669968749995,53.000001999999995C57.784669968749995,56.313713,55.09838096875,59.000001999999995,51.784669968749995,59.000001999999995ZM55.41616996875,51.002003L55.41616996875,48.539001L52.099668968749995,48.539001L52.099668968749995,49.355001L50.64466996875,49.355001L50.64466996875,55.919001L52.09216896875,55.919001L52.09216896875,57.153503L55.402669968750004,57.153503L55.402669968750004,54.630500999999995L52.09216896875,54.630500999999995L52.09216896875,55.095501L51.379668968749996,55.095501L51.379668968749996,50.153001L52.099668968749995,50.153001L52.099668968749995,51.002001L55.41767096875,51.002001L55.41616996875,51.002003ZM50.259168968750004,52.160002C50.00266996875,51.680004,50.005670968749996,51.672504,50.005670968749996,51.672504C50.005670968749996,51.672504,49.12067096875,51.950002999999995,48.95566996875,51.000502L48.442669968749996,51.000502C48.442669968749996,51.000502,48.30166996875,51.951504,47.39716996875,51.669502L47.12717096875,52.173504C47.12717096875,52.173504,47.83517096875,52.728504,47.12717096875,53.478504L47.40166996875,53.985506C47.40166996875,53.985506,48.18316996875,53.607506,48.43967096875,54.639505L48.95866996875,54.639505C48.95866996875,54.639505,49.12067096875,53.691505,49.99966996875,53.972006C50.25016996875,53.492004,50.25467096875,53.489004,50.25467096875,53.489004C50.25467096875,53.489004,49.59017096875,52.847006,50.259168968750004,52.160002Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/shenpiguanli.svg b/src/assets/system/shenpiguanli.svg
new file mode 100644
index 0000000..16ba84b
--- /dev/null
+++ b/src/assets/system/shenpiguanli.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35558"><rect x="12.5" y="0" width="34.000003814697266" height="34" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35558)"><path d="M30.364756937499997,25.1723935L31.5122539375,23.7415655C30.7614219375,22.8348995,30.3080869375,21.6732315,30.3080869375,20.4265635C30.3080869375,17.5648925,32.6455929375,15.2273945,35.5214249375,15.2273945C37.0655859375,15.2273945,38.4680879375,15.9073935,39.417254937500005,16.969893499999998L39.417254937500005,4.4890625C39.417254937500005,3.55406219,38.652257937499996,2.7890625,37.7172569375,2.7890625L19.3005861375,2.7890625C18.3655856875,2.7890625,17.6005859375,3.55406243,17.6005859375,4.4890625L17.6005859375,26.5890575C17.6005859375,27.5240575,18.3655862175,28.2890625,19.3005861375,28.2890625L27.4747514375,28.2890625C27.8289189375,26.8015635,28.933920937499998,25.6115625,30.364756937499997,25.1723935ZM23.706419037499998,6.7982302L33.339754937500004,6.7982302C33.963088937500004,6.7982302,34.4730839375,7.3082304,34.4730839375,7.9315624C34.4730839375,8.5548954,33.963088937500004,9.0648956,33.339754937500004,9.0648956L23.7064218375,9.0648956C23.083087437499998,9.0648956,22.5730877375,8.5548968,22.5730877375,7.9315639C22.5730877375,7.3082304,23.0689206375,6.7982302,23.706419037499998,6.7982302ZM23.706419037499998,11.8982306L33.339754937500004,11.8982306C33.963088937500004,11.8982306,34.4730839375,12.4082298,34.4730839375,13.0315625C34.4730839375,13.6548975,33.963088937500004,14.1648965,33.339754937500004,14.1648965L23.7064218375,14.1648965C23.083087437499998,14.1648965,22.5730877375,13.6548975,22.5730877375,13.0315625C22.5730877375,12.4082298,23.0689206375,11.8982306,23.706419037499998,11.8982306ZM22.5730872375,18.1315635C22.5730872375,17.5082325,23.0830865375,16.9982295,23.706419037499998,16.9982295L27.6730879375,16.9982295C28.296420937500002,16.9982295,28.8064209375,17.5082265,28.8064209375,18.1315575C28.8064209375,18.754890500000002,28.296420937500002,19.2648965,27.6730879375,19.2648965L23.706419037499998,19.2648965C23.0689187375,19.2648965,22.5730872375,18.7548925,22.5730872375,18.1315635Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/><path d="M39.43028168125,31.168883875L31.596113881249998,31.168883875C30.51944607125,31.168883875,29.64111328125,30.290549875,29.64111328125,29.213882875C29.64111328125,28.137214874999998,30.51944607125,27.258882475,31.596113881249998,27.258882475L34.81194638125,23.263881675C33.52277968125,22.952215175,32.57361388125,21.804714675,32.57361388125,20.430547675C32.57361388125,18.815546775,33.89111378125,17.498046875,35.52027988125,17.498046875C37.14944748125,17.498046875,38.46694468125,18.815546775,38.46694468125,20.430547675C38.46694468125,21.804714675,37.51777978125,22.952215175,36.22861288125,23.263881675L39.44444748125,27.258882475C40.52111428125,27.258882475,41.39944628125,28.137214874999998,41.39944628125,29.213882875C41.39944628125,30.290549875,40.52111428125,31.168883875,39.43028168125,31.168883875Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/xiaoshoubaojia.svg b/src/assets/system/xiaoshoubaojia.svg
new file mode 100644
index 0000000..13a84dd
--- /dev/null
+++ b/src/assets/system/xiaoshoubaojia.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="59.90625" viewBox="0 0 59.5 59.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35608"><rect x="18" y="0" width="24" height="24" rx="0"/></clipPath></defs><path d="M1.204819142818451,23.096379L28.69276814281845,39.307213000000004L58.29516614281845,23.096379L28.69276814281845,9L1.204819142818451,23.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,23.065809L28.43877614281845,39.737896L28.68361514281845,39.882288L59.39288714281845,23.065309L28.684561142818453,8.44229615L0.16870074281845082,23.065809ZM28.70192014281845,38.732138L2.240937742818451,23.12695L28.70097614281845,9.55770427L57.19744514281845,23.127449L28.70192014281845,38.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,23.80120277404785L0.5,42.83131777404785L28.692764,59.042150774047855L28.692764,40.01204077404785L0.5,23.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,59.906411774047854L29.192764,39.722777774047856L28.941999,39.578587774047854L-5.999999996841865e-8,22.93693918404785L0,43.12058077404785L29.192764,59.906411774047854ZM28.192764,40.30130377404785L28.192764,58.17788877404785L1,42.542054774047855L1,24.66546636404785L28.192764,40.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,23.80120277404785L59,42.12649377404785L88.5,59.00042177404785L88.5,39.26604677404785L59,23.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,59.86243677404785L89,38.96362277404785L88.732151,38.82320777404785L58.50000003,22.97454732404785L58.50000003,42.41651177404785L89,59.86243677404785ZM88,39.568469774047855L88,58.13840677404785L59.5,41.836475774047855L59.5,24.627858224047852L88,39.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35608)"><path d="M20.17195003125,2.664L37.21195053125,2.664C37.94394853125,2.664,38.531948531249995,3.2640002,38.51994853125,3.9960001L38.51994853125,22.667999C38.531948531249995,23.4,37.94394853125,24,37.21195053125,24L20.17195003125,24C19.43994993125,24,18.85195001925,23.400002,18.86395004392,22.667999L18.86395004392,3.9960001C18.85195001925,3.2640002,19.43995005125,2.6759999,20.17195003125,2.664ZM22.78794983125,15.324L22.78794983125,19.320002C22.78794983125,19.691999,23.087950231249998,19.991999,23.45994993125,19.991999C23.80795003125,19.991999,24.09594963125,19.704,24.09594963125,19.356001L24.09594963125,15.312C24.09594963125,14.952001,23.80795003125,14.664,23.44794993125,14.664C23.087950231249998,14.664,22.78794983125,14.964,22.78794983125,15.324ZM28.03195003125,16.68L28.03195003125,19.308001C28.03195003125,19.68,28.331950231249998,19.991999,28.71595003125,19.991999C29.06395053125,19.991999,29.33994953125,19.716,29.33994953125,19.368L29.33994953125,16.620001C29.33994953125,16.271999,29.06395053125,15.995999,28.71595003125,15.995999C28.34395023125,15.995999,28.03195003125,16.308001,28.03195003125,16.68ZM33.275950531250004,12.683999L33.275950531250004,19.296C33.275950531250004,19.679998,33.587949531250004,20.004,33.98394953125,20.004C34.31995053125,20.004,34.58394953125,19.727999,34.58394953125,19.404001L34.58394953125,12.624001C34.58394953125,12.276,34.30794953125,12,33.959949531250004,12C33.58794853125,12,33.27594853125,12.312,33.275950531250004,12.683999ZM28.96795053125,12.084L34.58394953125,8.6999998C34.883949531249996,8.5200005,34.97994853125,8.1359997,34.799948531249996,7.8360004C34.60794953125,7.5120001,34.17594953125,7.4040003,33.85194853125,7.5959997L29.14795053125,10.44C29.02795153125,10.512,28.85995103125,10.5,28.76395033125,10.392L26.25594993125,7.7879996C26.12395053125,7.6560001,25.91995093125,7.6319995,25.76395033125,7.7399998L22.60794993125,9.8640003C22.283950131250002,10.08,22.19995023125,10.512,22.41595003125,10.836C22.60794993125,11.124,23.00395063125,11.208,23.29194973125,11.016L25.61995033125,9.4320002C25.751949831250002,9.3479996,25.91995003125,9.3600006,26.03994993125,9.4800005L28.51195143125,12.048001C28.63195033125,12.156,28.82395073125,12.180001,28.96795053125,12.084ZM40.487951531250005,21.323999C40.12795053125,21.323999,39.82795153125,21.024,39.82795153125,20.664L39.82795153125,2.664C39.83995253125,1.932,39.25195153125,1.332,38.51995253125,1.332L22.10395023125,1.332C21.75595043125,1.332,21.47995043125,1.056,21.47995043125,0.708C21.47995023125,0.31200001,21.79195043125,0,22.18795033125,0L38.519950531250004,0C39.983949531250005,0.012,41.159950531250004,1.2,41.14794953125,2.664L41.14794953125,20.676001C41.14794953125,21.036001,40.847949531249995,21.336,40.487951531250005,21.323999Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/xiaoshoupeizhi.svg b/src/assets/system/xiaoshoupeizhi.svg
new file mode 100644
index 0000000..7191560
--- /dev/null
+++ b/src/assets/system/xiaoshoupeizhi.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="89.5693359375" height="96" viewBox="0 0 89.5693359375 96"><defs><filter id="master_svg0_143_35978" filterUnits="objectBoundingBox" color-interpolation-filters="sRGB" x="-0.6666666666666666" y="-0.6666666666666666" width="2.3333333333333335" height="2.3333333333333335"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset dy="0" dx="0"/><feGaussianBlur stdDeviation="8"/><feColorMatrix type="matrix" values="0 0 0 0 0.12895070016384125 0 0 0 0 0.29307788610458374 0 0 0 0 0.9107142686843872 0 0 0 0.5299999713897705 0"/><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter><clipPath id="master_svg1_143_35983"><rect x="32.78466796875" y="36" width="23" height="23" rx="0"/></clipPath></defs><path d="M44.78466796875,24.000000000000004L65.56927496875,36L65.56927496875,60L44.78466796875,72L24.00005836875,60L24.00005796875,36.000001L44.78466796875,24.000000000000004Z" fill="#496FFE" fill-opacity="1" filter="url(#master_svg0_143_35978)"/><g clip-path="url(#master_svg1_143_35983)"><path d="M44.950077965625,36Q45.615746465625,36,46.269937765625,36.14920157Q46.924129765625,36.29840314,47.497981765625,36.55089813Q48.071835765624996,36.80339313,48.519438765625,37.124750399999996Q48.967043765625,37.4461077,49.219538765625,37.7674649Q49.816345765625,38.5019956,50.091794765624996,39.3857281Q50.367244765625,40.269460699999996,50.482013765625,41.0728536Q50.596784765625,41.9910169,50.596784765625,42.9321346Q50.757463765625,43.046905,50.849280765625,43.2305379Q50.941096765625,43.391216299999996,50.998480765625004,43.6437116Q51.055866765625,43.8962064,51.009958765625,44.2634716Q50.964050765625004,44.7455082,50.814847765625004,45.032433499999996Q50.665645765625,45.3193598,50.482013765625,45.4800377Q50.275426765625,45.6636715,50.045885765625,45.7325335Q49.931114765625,46.122753,49.816345765625,46.467065Q49.701575765625,46.765468,49.552372765625,47.052393Q49.403172765625,47.339319,49.219538765625,47.499997Q48.783411765625004,47.844309,48.507961765625,48.085328000000004Q48.232512765625,48.326347,48.140695765625,48.900198Q48.071834765625,49.244509,48.094788765625,49.600298Q48.117743765625,49.956086,48.289897765625,50.311875Q48.462053765625,50.667664,48.817842765625,50.989021Q49.173630765625,51.310377,49.816345765625,51.562872999999996Q50.390198765625,51.792412999999996,51.067343765625,51.976046Q51.744489765625005,52.159679,52.387202765625,52.400695999999996Q53.029916765625,52.641714,53.523429765624996,52.997502999999995Q54.016944765625,53.353291,54.223531765625,53.973051Q54.338302765625,54.340319,54.395686765625,54.879736Q54.453071765625,55.419159,54.453071765625,55.970057Q54.453071765625,56.520954,54.372731765625005,57.002989Q54.292394765625005,57.485027,54.131713765625,57.73752Q54.016944765625,57.898201,53.477523765625,58.058878Q52.938104765625,58.219556999999995,52.146186765625,58.368757Q51.354269765625,58.51796,50.378720765625,58.632729999999995Q49.403173765625,58.747501,48.416146765625,58.827839Q47.429119765625,58.908178,46.522433765624996,58.954088Q45.615747465625,58.999995999999996,44.950078965624996,58.999994Q44.284410465625,58.999994,43.400678165625,58.954088Q42.516944865625,58.908182,41.575827565625,58.827841Q40.634709865625,58.747501,39.705069065625,58.644209000000004Q38.775428565625,58.540915999999996,38.017943365625,58.426144Q37.260458465625,58.311375,36.732514365625,58.196604Q36.204570295625,58.081837,36.066845565625,57.967064Q35.837304725625,57.783429999999996,35.711057230625,56.842314Q35.584809735625,55.901196,35.814350605625,54.40918Q35.929121195625,53.559877,36.491496265625,53.112272000000004Q37.053871265625,52.664669,37.799879565625,52.400695999999996Q38.545887665625,52.136724,39.349280365625,51.930138Q40.152672765625,51.723553,40.772433265625,51.333332Q41.254469865625,51.03493,41.506964665625,50.759479999999996Q41.759460465625,50.484030000000004,41.874229865625,50.208582Q41.989000365625,49.933134,41.989000365625,49.63473Q41.989000365625,49.336327,41.966046765625,48.969062Q41.920138365625,48.441118,41.598781565625,48.131239Q41.277424365625,47.821359,40.887204665625,47.5Q40.703571765625,47.339321,40.565847865625,47.052395000000004Q40.428123965625,46.765468999999996,40.313353065625,46.467067Q40.198582165625,46.122755,40.083811765625,45.7325354Q39.923133365625,45.6866264,39.762454465625,45.5718565Q39.624729865625,45.4570866,39.464051465625,45.2504997Q39.303372865625,45.0439129,39.188602665625,44.653694200000004Q39.050877765625,44.2634735,39.085309065625,43.9421172Q39.119739965625,43.62076,39.211556165625,43.391218699999996Q39.303372865625,43.1387234,39.509959465625,42.9091825Q39.487005965625,42.0369272,39.601776165625,41.1646714Q39.693592565624996,40.4301405,39.934610365625,39.5808394Q40.175627665625,38.7315381,40.657664265625,38.0658693Q41.093792465625,37.423155,41.633213065625,37.0214585Q42.172633665625,36.61976188,42.735009665625,36.390221Q43.297384765625,36.16068012,43.859759365625,36.080340803Q44.422134365625,36.0000014994291,44.950077965625,36Z" fill="#FFFFFF" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/xiaoshoutaizhang.svg b/src/assets/system/xiaoshoutaizhang.svg
new file mode 100644
index 0000000..9880541
--- /dev/null
+++ b/src/assets/system/xiaoshoutaizhang.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="63.90625" viewBox="0 0 59.5 63.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35617"><rect x="13.5" y="0" width="32" height="32" rx="0"/></clipPath></defs><path d="M1.204819142818451,27.096379L28.69276814281845,43.307213000000004L58.29516614281845,27.096379L28.69276814281845,13L1.204819142818451,27.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,27.065809L28.43877614281845,43.737896L28.68361514281845,43.882288L59.39288714281845,27.065309L28.684561142818453,12.44229615L0.16870074281845082,27.065809ZM28.70192014281845,42.732138L2.240937742818451,27.12695L28.70097614281845,13.55770427L57.19744514281845,27.127449L28.70192014281845,42.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,27.80120277404785L0.5,46.83131777404785L28.692764,63.042150774047855L28.692764,44.01204077404785L0.5,27.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,63.906411774047854L29.192764,43.722777774047856L28.941999,43.578587774047854L-5.999999996841865e-8,26.93693918404785L0,47.12058077404785L29.192764,63.906411774047854ZM28.192764,44.30130377404785L28.192764,62.17788877404785L1,46.542054774047855L1,28.66546636404785L28.192764,44.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,27.80120277404785L59,46.12649377404785L88.5,63.00042177404785L88.5,43.26604677404785L59,27.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,63.86243677404785L89,42.96362277404785L88.732151,42.82320777404785L58.50000003,26.97454732404785L58.50000003,46.41651177404785L89,63.86243677404785ZM88,43.568469774047855L88,62.13840677404785L59.5,45.836475774047855L59.5,28.627858224047852L88,43.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35617)"><path d="M36.903126,3.015625L22.0968752,3.015625C20.6062503,3.015625,19.4000001,4.2249999,19.4000001,5.7125001L19.4000001,17.44375L39.6,17.44375L39.6,5.7125001C39.6,4.2218751999999995,38.390625,3.015625,36.903126,3.015625ZM26.615625,14.556249L22.2843752,14.556249C21.4875002,14.556249,20.8406253,13.909374,20.8406253,13.112499C20.8406253,12.3156242,21.4875002,11.6687489,22.2843752,11.6687489L26.6125,11.6687489C27.409375,11.6687489,28.056251,12.3156242,28.056251,13.112499C28.056251,13.912499,27.4125,14.556249,26.615625,14.556249ZM32.384377,7.4312496C32.384377,8.181249600000001,31.778126,8.7874994,31.028126,8.7874994L22.1999993,8.7874994C21.4499993,8.7874994,20.84375,8.181249600000001,20.84375,7.4312496L20.84375,7.2593746C20.84375,6.5093746,21.4499993,5.9031248000000005,22.1999993,5.9031248000000005L31.03125,5.9031248000000005C31.78125,5.9031248000000005,32.387501,6.5093746,32.387501,7.2593746L32.387501,7.4312496L32.384377,7.4312496ZM39.600002,20.328125L19.4000001,20.328125C17.8062501,20.328125,16.515625,21.61875,16.515625,23.2125L16.515625,26.096872C16.515625,26.862497,16.81874993,27.596872,17.359375,28.137499C17.9000001,28.678123,18.6343751,28.981249,19.4000001,28.981249L39.6,28.981249C40.365625,28.981249,41.1,28.678123,41.640625,28.137499C42.18125,27.596872,42.484375,26.862497,42.484375,26.096872L42.484375,23.2125C42.484375,21.621874,41.19375,20.328125,39.600002,20.328125ZM20.84375,26.1C20.2593751,26.1,19.734375,25.746876,19.5093751,25.209375C19.2874997,24.668751,19.4093752,24.050001,19.8218751,23.637501C20.234375,23.225,20.8562498,23.1,21.3937502,23.325001C21.9343743,23.546875,22.2843752,24.075001,22.2843752,24.659376C22.2843752,25.453127,21.640625,26.1,20.84375,26.1ZM26.615625,26.1C26.03125,26.1,25.5062504,25.746876,25.28125,25.209375C25.0593748,24.668751,25.1812496,24.050001,25.59375,23.637501C26.0062504,23.225,26.628125,23.1,27.165624,23.325001C27.706249,23.546875,28.056249,24.075001,28.056249,24.659376C28.056249,25.453127,27.412499,26.1,26.615625,26.1Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/xiaoshoutuihuo.svg b/src/assets/system/xiaoshoutuihuo.svg
new file mode 100644
index 0000000..bcacee2
--- /dev/null
+++ b/src/assets/system/xiaoshoutuihuo.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35635"><rect x="12.5" y="0" width="34.000003814697266" height="34" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35635)"><path d="M16.5,17.5C16.5,10.0479994,22.547999400000002,4,30,4C37.452,4,43.5,10.0479999,43.5,17.5C43.5,24.952,37.452,31,30,31C22.547999400000002,31,16.5,24.952,16.5,17.5ZM25.2099543,11.2335448C25.056938199999998,11.1590481,24.889226,11.1196365,24.719044699999998,11.118180800000001C24.4576359,11.118180800000001,24.2600451,11.2102261,24.1250448,11.3930907C23.965045,11.566371,23.8772473,11.7942061,23.8795905,12.0300455C23.8795905,12.2902269,23.9716368,12.4779997,24.1545,12.5945911C24.1946921,12.6422205,24.2439098,12.681428,24.2993183,12.7099552C24.812317800000002,13.0376368,25.309363400000002,13.3910923,25.789226499999998,13.7654095C25.875135399999998,13.8550005,25.9782276,13.9249544,26.0935907,13.9691372C26.1991358,14.026819,26.3292265,14.055046,26.4838629,14.055046C26.763681,14.046455,26.98459,13.9114552,27.149045,13.6500463C27.266969,13.4697084,27.327709,13.2579737,27.323318,13.0425463C27.319848,12.8229294,27.219160000000002,12.6161594,27.048409,12.4780006C26.492164600000002,11.9851937,25.874157,11.5668635,25.2099543,11.2335448ZM34.731136,11.2924538L29.478408,11.2924538C28.610726,11.2924538,28.177500000000002,11.648363100000001,28.177500000000002,12.3626356L28.177500000000002,19.944725C28.176272,20.042908,28.18609,20.138634,28.205727,20.234362C28.070726,20.349726,28.003225999999998,20.553452,28.003225999999998,20.841862C28.013044999999998,21.11186,28.070726,21.309452,28.176271,21.434633C28.292863,21.569633,28.500272000000002,21.637133,28.798499,21.637133C29.464908,21.637133,30.404999,21.487406,31.621223999999998,21.189178C31.861770999999997,21.140087,32.026225,21.059088,32.112133,20.943726C32.243669,20.79126,32.310976,20.593739,32.299906,20.392679C32.299906,20.17177,32.252043,19.997498,32.156316000000004,19.872315999999998C32.033533,19.737026,31.860347,19.658427,31.677678999999998,19.655088C31.157317,19.732407000000002,30.632043,19.804814999999998,30.100635,19.872315999999998L30.100635,17.455816L34.673452,17.455816C34.759361,17.455816,34.837906000000004,17.450908,34.905409,17.441088999999998C34.893873,17.462564,34.878954,17.482039999999998,34.861225000000005,17.498772000000002C34.521141,17.859035,34.158792,18.197606999999998,33.776316,18.512499C33.275587,18.193407999999998,32.749089999999995,17.895182,32.199270999999996,17.615363000000002C32.035903,17.54202,31.857872,17.507168,31.678905999999998,17.513499C31.417496999999997,17.513499,31.21009,17.590817,31.056679000000003,17.745455C30.902043,17.880454999999998,30.823497,18.082954,30.823497,18.352954C30.828218,18.505149,30.890499,18.649888,30.997768,18.757953999999998C31.051738999999998,18.809553,31.116205,18.848902000000002,31.186768999999998,18.873317999999998C32.266769,19.520091999999998,33.48177,20.369364,34.831768,21.419909C35.01586,21.604,35.246586,21.694817,35.527632,21.694817C35.766716,21.695692,35.992073000000005,21.583241,36.135132,21.39168C36.289076,21.227486,36.371994,21.009171,36.36586,20.78418C36.362389,20.564564,36.261703,20.357796,36.090952,20.219635C35.802544,19.968045,35.503088000000005,19.728727,35.195042,19.495544000000002C35.705585,19.197316999999998,36.115498,18.873317,36.424769999999995,18.526C36.533014,18.357194,36.592867,18.161924,36.597816,17.961454C36.611883,17.651581,36.456854,17.358437000000002,36.192816,17.195636999999998C36.041086,17.073852000000002,35.852282,17.007597,35.657722,17.007863999999998C35.753451999999996,16.853229,35.802544,16.645819,35.802544,16.385637L35.802544,12.3626366C35.802544,11.6483641,35.445406,11.2924547,34.731136,11.2924538ZM33.878181,13.664773L30.100636,13.664773L30.100636,13.1002264C30.100636,12.9468174,30.192681999999998,12.8695002,30.375545000000002,12.8695002L33.603271,12.8695002C33.786135,12.8695002,33.87818,12.9468174,33.87818,13.1002264L33.878181,13.664773ZM33.603271,15.864044L30.100636,15.864044L30.100636,15.02459L33.878181,15.02459L33.878181,15.646818C33.878181,15.791636,33.786135,15.864044,33.603271,15.864044ZM25.6296825,15.473772L24.2698641,15.473772C23.9507732,15.473772,23.724954099999998,15.546182,23.5899544,15.690999C23.4451365,15.844409,23.3727274,16.076363999999998,23.3727274,16.385635C23.3727274,16.723136,23.4402275,16.950181999999998,23.5752277,17.065544C23.6905913,17.248407,23.9225464,17.340454,24.2698641,17.340454L24.834409700000002,17.340454C24.9497738,17.340454,25.0074549,17.398136,25.0074549,17.513499L25.0074549,21.637135C25.0074549,21.714453,24.9546824,21.786861,24.847909899999998,21.854361C24.4527292,22.139252,24.0315437,22.386209,23.5899544,22.591951C23.4451365,22.698725,23.3727274,22.814087,23.3727274,22.939268C23.3771172,23.0942,23.4117188,23.246782,23.4745903,23.388451C23.6476359,23.697725,23.835408700000002,23.943178,24.0391369,24.126041C24.1385455,24.200905,24.260046,24.242634,24.3852277,24.242634C24.5752201,24.217705,24.7579012,24.153328,24.921546,24.053633C25.228364,23.907587,25.5204544,23.728407,25.7892284,23.518543C25.8849545,23.432632,25.9622736,23.388451,26.0211821,23.388451C26.132575,23.390455,26.2397785,23.431255,26.3243189,23.503817C27.153955,23.842543,28.572682999999998,24.010681,30.578046,24.010681L35.97559,24.010681C36.313091,23.991043,36.545048,23.900225,36.670227,23.735771C36.805227,23.61059,36.872726,23.363907,36.872726,22.99818C36.872726,22.67909,36.790501,22.453272,36.627274,22.317045C36.492271,22.201681,36.294682,22.144001,36.033272,22.144001C35.368092000000004,22.144001,34.566679,22.163635,33.6315,22.201681C32.551497999999995,22.239727,31.379454000000003,22.259361,30.115364,22.259361C28.996092,22.259361,28.114910000000002,22.191862,27.468138,22.056862C27.071728999999998,21.979544,26.874138000000002,21.8605,26.874138000000002,21.694817L26.874138000000002,16.601637C26.874138000000002,15.849319,26.4593201,15.472546,25.6296825,15.472546L25.6296825,15.473772Z" fill="#BC49FE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/yonghuguanli.svg b/src/assets/system/yonghuguanli.svg
new file mode 100644
index 0000000..a8365a7
--- /dev/null
+++ b/src/assets/system/yonghuguanli.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35539"><rect x="12.5" y="0" width="34.000003814697266" height="34" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35539)"><path d="M31.6272783125,19.833336L31.6272783125,19.125002C31.6272783125,18.735416,31.3085343125,18.416668,30.9189493125,18.416668L28.0856153125,18.416668C27.696029312500002,18.416668,27.3772793125,18.735416,27.3772793125,19.125002L27.3772793125,19.833336C27.3772793125,20.222919,27.696029312500002,20.541668,28.0856153125,20.541668L28.5106133125,20.541668L27.4056153125,24.664169C27.3914463125,24.742083,27.4056153125,24.827087,27.455197312499998,24.890835L29.162277312500002,27.171669C29.3322813125,27.398335,29.672281312499997,27.398335,29.8422813125,27.171669L31.5493633125,24.890835C31.598944312500002,24.827087,31.613113312499998,24.742083,31.598944312500002,24.664169L30.493948312500002,20.541674L30.918943312499998,20.541674C31.3085323125,20.541674,31.6272763125,20.222921,31.6272783125,19.833336ZM29.5022783125,17.708334999999998C25.9960270125,17.708334999999998,23.1272793125,14.839586,23.1272793125,11.3333368L23.1272793125,8.5C23.1272793125,4.9937501,25.9960270125,2.125,29.5022783125,2.125C33.008526312499995,2.125,35.8772793125,4.9937501,35.8772793125,8.5L35.8772793125,11.3333368C35.8772793125,14.839586,33.008526312499995,17.708334999999998,29.5022783125,17.708334999999998ZM38.172279312499995,19.125002L36.557281312499995,18.770834C36.2597753125,18.707085,35.9481063125,18.841665,35.7993643125,19.110832L29.9414463125,29.601257C29.7501973125,29.941248,29.2543643125,29.941248,29.0631123125,29.601257L23.0493646125,18.834585C22.9010763125,18.564627,22.5916772125,18.425833,22.2914457125,18.494585L19.4156120125,19.125002C17.4497964125,19.53912,16.04301562917,21.273962,16.0439453125,23.282919L16.0439453125,28.333334C16.0439453125,29.898754,17.3118622125,31.166664,18.877278812500002,31.166664L38.7106133125,31.166664C40.2760293125,31.166664,41.543947312499995,29.898754,41.543947312499995,28.333334L41.543947312499995,23.282919C41.543947312499995,21.27125,40.134359312499996,19.535837,38.172279312499995,19.125002Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/zhibiaotongji.svg b/src/assets/system/zhibiaotongji.svg
new file mode 100644
index 0000000..00e97bb
--- /dev/null
+++ b/src/assets/system/zhibiaotongji.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35654"><rect x="14.5" y="0" width="30" height="30" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35654)"><path d="M27.329689375,4.0806096499999995C21.384180075,4.72396945,16.755859375,9.759809449999999,16.755859375,15.87641125C16.755859375,22.42924525,22.068262575,27.74164625,28.621094375,27.74164625C34.737697374999996,27.74164625,39.773244375000004,23.11332725,40.416896375,17.16781625C40.491603375,16.47728925,39.945509375,15.87641025,39.251175375,15.87641025L29.792969375,15.87641025C29.145801374999998,15.87641025,28.621094375,15.35170425,28.621094375,14.70453425L28.621094375,5.246332649999999C28.621094375,4.55199675,28.020215375,4.00590305,27.329689375,4.0806096499999995ZM30.378907375,3.48881295L30.378907375,12.94672425C30.378907375,13.59389225,30.903614375,14.11859925,31.550782375,14.11859925L41.008691375,14.11859925C41.705665374999995,14.11859925,42.249416374999996,13.51391025,42.174123375,12.82104025C41.573829375,7.30473135,37.192775375,2.92367607,31.676466375,2.3233831231C30.983595375,2.248090155,30.378907375,2.79184017,30.378907375,3.48881295Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/assets/system/zhushengchanjihua.svg b/src/assets/system/zhushengchanjihua.svg
new file mode 100644
index 0000000..8573eac
--- /dev/null
+++ b/src/assets/system/zhushengchanjihua.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_36341"><rect x="14.5" y="0" width="32" height="32" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_36341)"><path d="M32.56859821875,27.56376125L32.42002721875,27.42661825C32.09464421875,27.10965125,31.90252121875,26.68043925,31.882883218750003,26.22661825L30.74002621875,26.22661825L30.74002621875,3.88376141C30.73742821875,3.43834615,30.27823221875,3.14228559,29.87145421875,3.323761463L19.80288311875,7.52947525C19.35276551875,7.73022935,19.06522271875,8.17951485,19.07145451875,8.672332749999999L19.07145451875,26.21518925L18.10002588875,26.21518925C17.62617706875,26.21453525,17.24169921875,26.59848025,17.24169921875,27.07232825C17.24169921875,27.54617925,17.62617683875,27.93012825,18.10002588875,27.92947425L33.17431221875,27.92947425L32.78574221875,27.66661625L32.56859821875,27.56376125ZM37.35717021875,16.84376125C37.73498121875,16.60642825,38.168478218749996,16.47243925,38.61431321875,16.45519025C38.94686521875,16.46270525,39.27405121875,16.54060725,39.574312218749995,16.68376125L41.860027218750005,17.66661825L41.860027218750005,12.44376185C41.86428221875,11.94822695,41.57176021875,11.49819085,41.117170218750005,11.30090525L32.47716921875,7.99804685L32.47716921875,19.083761250000002C32.64974421875,18.88615425,32.86457421875,18.72991325,33.10573921875,18.626617250000002L37.35717021875,16.84376125ZM43.551454218749996,20.45519025C43.59511021875,20.43103825,43.62220421875,20.38508225,43.62220421875,20.33519125C43.62220421875,20.28530125,43.59511021875,20.23934525,43.551454218749996,20.21519125L41.92859621875,19.52947625L38.91145521875,18.283762250000002C38.82957221875,18.240982250000002,38.740429218749995,18.213852250000002,38.64859721875,18.20376225C38.464931218749996,18.25888025,38.28845821875,18.335607250000002,38.122883218750005,18.43233325L33.74573921875,20.20376225C33.70786821875,20.20376225,33.67716821875,20.23446225,33.67716821875,20.27233325C33.67716821875,20.31020325,33.70786821875,20.34090425,33.74573921875,20.34090425L38.591453218750004,23.06090325L38.86574021875,23.06090325L41.92859621875,21.34661825L43.551454218749996,20.45519025ZM43.82574121875,21.01519025L41.92859821875,22.04376225L39.06002621875,23.62090525C39.04922821875,23.64647825,39.04922821875,23.67533125,39.06002621875,23.70090525L39.06002621875,28.92376125C39.053015218750005,28.97232025,39.09096521875,29.01569325,39.140026218749995,29.01519025L40.945740218750004,27.94090425L43.72288321875,26.28376225L43.87145421875,26.19233325L43.87145421875,21.10661925C43.88021121875,21.06911125,43.86100021875,21.03068925,43.82574121875,21.01519025ZM33.72288321875,21.04947425C33.673822218750004,21.04897125,33.635872218749995,21.09234425,33.64288321875,21.14090325L33.64288321875,26.13518925C33.632086218750004,26.16076325,33.632086218750004,26.18961525,33.64288321875,26.21518925L36.52288421875,27.94090225L38.351455218750004,29.08375925C38.40051621875,29.08426325,38.43846721875,29.04089125,38.43145521875,28.99233025L38.43145521875,23.72375825C38.44225121875,23.69818325,38.44225121875,23.66933225,38.43145521875,23.64375925L33.72288321875,21.04947425ZM35.15145521875,23.52947425C34.991022218750004,23.45744525,34.94520021875,23.25124325,35.06002621875,23.11804625C35.11695121875,23.02876325,35.217087218749995,22.97651625,35.32288321875,22.98090325C35.37916521875,22.96686925,35.43803021875,22.96686925,35.49431221875,22.98090325L37.78002721875,24.23804625C37.919319218750005,24.32610925,37.960340218750005,24.51070625,37.87145621875,24.64947525C37.81297721875,24.73704525,37.71387821875,24.78874825,37.608598218750004,24.78661725C37.55167421875,24.79367025,37.49409721875,24.79367025,37.43717021875,24.78661725L35.15145521875,23.52947425Z" fill="#496FFE" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git "a/src/assets/system/\347\273\204 210683.svg" "b/src/assets/system/\347\273\204 210683.svg"
new file mode 100644
index 0000000..bea3701
--- /dev/null
+++ "b/src/assets/system/\347\273\204 210683.svg"
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="59.5" height="64.90625" viewBox="0 0 59.5 64.90625"><defs><linearGradient x1="0" y1="0.16914062201976776" x2="0.8045321262753937" y2="1.0460527890370677" id="master_svg0_141_37616"><stop offset="48.549023270606995%" stop-color="#F7F7F7" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><linearGradient x1="-0.03565644100308418" y1="0" x2="0.8226521556701855" y2="0.9991461352834076" id="master_svg1_141_37562"><stop offset="46.036821603775024%" stop-color="#F3F3F3" stop-opacity="1"/><stop offset="100%" stop-color="#FFFFFF" stop-opacity="1"/></linearGradient><clipPath id="master_svg2_143_35787"><rect x="12.5" y="0" width="34.000003814697266" height="34" rx="0"/></clipPath></defs><path d="M1.204819142818451,28.096379L28.69276814281845,44.307213000000004L58.29516614281845,28.096379L28.69276814281845,14L1.204819142818451,28.096379Z" fill="#FFFFFF" fill-opacity="1"/><path d="M0.16870074281845082,28.065809L28.43877614281845,44.737896L28.68361514281845,44.882288L59.39288714281845,28.065309L28.684561142818453,13.44229615L0.16870074281845082,28.065809ZM28.70192014281845,43.732138L2.240937742818451,28.12695L28.70097614281845,14.55770427L57.19744514281845,28.127449L28.70192014281845,43.732138Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><path d="M0.5,28.80120277404785L0.5,47.83131777404785L28.692764,64.04215077404785L28.692764,45.01204077404785L0.5,28.80120277404785Z" fill="url(#master_svg0_141_37616)" fill-opacity="1"/><path d="M29.192764,64.90641177404785L29.192764,44.722777774047856L28.941999,44.578587774047854L-5.999999996841865e-8,27.93693918404785L0,48.12058077404785L29.192764,64.90641177404785ZM28.192764,45.30130377404785L28.192764,63.17788877404785L1,47.542054774047855L1,29.66546636404785L28.192764,45.30130377404785Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/><g transform="matrix(-1,0,0,1,118,0)"><path d="M59,28.80120277404785L59,47.12649377404785L88.5,64.00042177404785L88.5,44.26604677404785L59,28.80120277404785Z" fill="url(#master_svg1_141_37562)" fill-opacity="1"/><path d="M89,64.86243677404785L89,43.96362277404785L88.732151,43.82320777404785L58.50000003,27.97454732404785L58.50000003,47.41651177404785L89,64.86243677404785ZM88,44.568469774047855L88,63.13840677404785L59.5,46.836475774047855L59.5,29.627858224047852L88,44.568469774047855Z" fill-rule="evenodd" fill="#ECECEC" fill-opacity="1"/></g><g clip-path="url(#master_svg2_143_35787)"><path d="M16.5,17.5C16.5,10.0479994,22.547999400000002,4,30,4C37.452,4,43.5,10.0479999,43.5,17.5C43.5,24.952,37.452,31,30,31C22.547999400000002,31,16.5,24.952,16.5,17.5ZM25.2099543,11.2335448C25.056938199999998,11.1590481,24.889226,11.1196365,24.719044699999998,11.118180800000001C24.4576359,11.118180800000001,24.2600451,11.2102261,24.1250448,11.3930907C23.965045,11.566371,23.8772473,11.7942061,23.8795905,12.0300455C23.8795905,12.2902269,23.9716368,12.4779997,24.1545,12.5945911C24.1946921,12.6422205,24.2439098,12.681428,24.2993183,12.7099552C24.812317800000002,13.0376368,25.309363400000002,13.3910923,25.789226499999998,13.7654095C25.875135399999998,13.8550005,25.9782276,13.9249544,26.0935907,13.9691372C26.1991358,14.026819,26.3292265,14.055046,26.4838629,14.055046C26.763681,14.046455,26.98459,13.9114552,27.149045,13.6500463C27.266969,13.4697084,27.327709,13.2579737,27.323318,13.0425463C27.319848,12.8229294,27.219160000000002,12.6161594,27.048409,12.4780006C26.492164600000002,11.9851937,25.874157,11.5668635,25.2099543,11.2335448ZM34.731136,11.2924538L29.478408,11.2924538C28.610726,11.2924538,28.177500000000002,11.648363100000001,28.177500000000002,12.3626356L28.177500000000002,19.944725C28.176272,20.042908,28.18609,20.138634,28.205727,20.234362C28.070726,20.349726,28.003225999999998,20.553452,28.003225999999998,20.841862C28.013044999999998,21.11186,28.070726,21.309452,28.176271,21.434633C28.292863,21.569633,28.500272000000002,21.637133,28.798499,21.637133C29.464908,21.637133,30.404999,21.487406,31.621223999999998,21.189178C31.861770999999997,21.140087,32.026225,21.059088,32.112133,20.943726C32.243669,20.79126,32.310976,20.593739,32.299906,20.392679C32.299906,20.17177,32.252043,19.997498,32.156316000000004,19.872315999999998C32.033533,19.737026,31.860347,19.658427,31.677678999999998,19.655088C31.157317,19.732407000000002,30.632043,19.804814999999998,30.100635,19.872315999999998L30.100635,17.455816L34.673452,17.455816C34.759361,17.455816,34.837906000000004,17.450908,34.905409,17.441088999999998C34.893873,17.462564,34.878954,17.482039999999998,34.861225000000005,17.498772000000002C34.521141,17.859035,34.158792,18.197606999999998,33.776316,18.512499C33.275587,18.193407999999998,32.749089999999995,17.895182,32.199270999999996,17.615363000000002C32.035903,17.54202,31.857872,17.507168,31.678905999999998,17.513499C31.417496999999997,17.513499,31.21009,17.590817,31.056679000000003,17.745455C30.902043,17.880454999999998,30.823497,18.082954,30.823497,18.352954C30.828218,18.505149,30.890499,18.649888,30.997768,18.757953999999998C31.051738999999998,18.809553,31.116205,18.848902000000002,31.186768999999998,18.873317999999998C32.266769,19.520091999999998,33.48177,20.369364,34.831768,21.419909C35.01586,21.604,35.246586,21.694817,35.527632,21.694817C35.766716,21.695692,35.992073000000005,21.583241,36.135132,21.39168C36.289076,21.227486,36.371994,21.009171,36.36586,20.78418C36.362389,20.564564,36.261703,20.357796,36.090952,20.219635C35.802544,19.968045,35.503088000000005,19.728727,35.195042,19.495544000000002C35.705585,19.197316999999998,36.115498,18.873317,36.424769999999995,18.526C36.533014,18.357194,36.592867,18.161924,36.597816,17.961454C36.611883,17.651581,36.456854,17.358437000000002,36.192816,17.195636999999998C36.041086,17.073852000000002,35.852282,17.007597,35.657722,17.007863999999998C35.753451999999996,16.853229,35.802544,16.645819,35.802544,16.385637L35.802544,12.3626366C35.802544,11.6483641,35.445406,11.2924547,34.731136,11.2924538ZM33.878181,13.664773L30.100636,13.664773L30.100636,13.1002264C30.100636,12.9468174,30.192681999999998,12.8695002,30.375545000000002,12.8695002L33.603271,12.8695002C33.786135,12.8695002,33.87818,12.9468174,33.87818,13.1002264L33.878181,13.664773ZM33.603271,15.864044L30.100636,15.864044L30.100636,15.02459L33.878181,15.02459L33.878181,15.646818C33.878181,15.791636,33.786135,15.864044,33.603271,15.864044ZM25.6296825,15.473772L24.2698641,15.473772C23.9507732,15.473772,23.724954099999998,15.546182,23.5899544,15.690999C23.4451365,15.844409,23.3727274,16.076363999999998,23.3727274,16.385635C23.3727274,16.723136,23.4402275,16.950181999999998,23.5752277,17.065544C23.6905913,17.248407,23.9225464,17.340454,24.2698641,17.340454L24.834409700000002,17.340454C24.9497738,17.340454,25.0074549,17.398136,25.0074549,17.513499L25.0074549,21.637135C25.0074549,21.714453,24.9546824,21.786861,24.847909899999998,21.854361C24.4527292,22.139252,24.0315437,22.386209,23.5899544,22.591951C23.4451365,22.698725,23.3727274,22.814087,23.3727274,22.939268C23.3771172,23.0942,23.4117188,23.246782,23.4745903,23.388451C23.6476359,23.697725,23.835408700000002,23.943178,24.0391369,24.126041C24.1385455,24.200905,24.260046,24.242634,24.3852277,24.242634C24.5752201,24.217705,24.7579012,24.153328,24.921546,24.053633C25.228364,23.907587,25.5204544,23.728407,25.7892284,23.518543C25.8849545,23.432632,25.9622736,23.388451,26.0211821,23.388451C26.132575,23.390455,26.2397785,23.431255,26.3243189,23.503817C27.153955,23.842543,28.572682999999998,24.010681,30.578046,24.010681L35.97559,24.010681C36.313091,23.991043,36.545048,23.900225,36.670227,23.735771C36.805227,23.61059,36.872726,23.363907,36.872726,22.99818C36.872726,22.67909,36.790501,22.453272,36.627274,22.317045C36.492271,22.201681,36.294682,22.144001,36.033272,22.144001C35.368092000000004,22.144001,34.566679,22.163635,33.6315,22.201681C32.551497999999995,22.239727,31.379454000000003,22.259361,30.115364,22.259361C28.996092,22.259361,28.114910000000002,22.191862,27.468138,22.056862C27.071728999999998,21.979544,26.874138000000002,21.8605,26.874138000000002,21.694817L26.874138000000002,16.601637C26.874138000000002,15.849319,26.4593201,15.472546,25.6296825,15.472546L25.6296825,15.473772Z" fill="#3BB078" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></svg>
\ No newline at end of file
diff --git a/src/components/AIChatSidebar/assistants/financeAssistant.js b/src/components/AIChatSidebar/assistants/financeAssistant.js
new file mode 100644
index 0000000..f5dd7c6
--- /dev/null
+++ b/src/components/AIChatSidebar/assistants/financeAssistant.js
@@ -0,0 +1,28 @@
+import { Money } from '@element-plus/icons-vue'
+
+export const financeAssistant = {
+ key: 'finance',
+ label: '璐㈠姟鍔╃悊',
+ title: '璐㈠姟鏅鸿兘鍔╃悊',
+ tooltip: '璐㈠姟鏅鸿兘鍔╃悊',
+ icon: Money,
+ apiBase: '/financial-ai',
+ storageKey: 'financial_ai_chat_uuid',
+ placeholder: '璇疯緭鍏ヨ储鍔¢棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ崗鍔╀綘瀹屾垚鎴愭湰鏍哥畻銆佸埄娑﹀垎鏋愩�佸簱瀛樿祫閲戝垎鏋愩�佺幇閲戞祦棰勬祴銆佸紓甯搁璀﹀拰缁忚惀鍛ㄦ姤瑙h銆�',
+ allowFileUpload: false,
+ emptySessionText: '鏆傛棤璐㈠姟浼氳瘽',
+ quickPrompts: [
+ '鏌ョ湅缁忚惀椹鹃┒鑸�',
+ '鏌ョ湅鏈湀缁忚惀椹鹃┒鑸�',
+ '鐢熸垚鏈懆缁忚惀鍛ㄦ姤锛堝埄娑︿笌鐜伴噾娴侊級',
+ '鍒嗘瀽鏈湀鍒╂鼎涓嬮檷鍘熷洜',
+ '鏌ヨ杩�30澶╀簭鎹熻鍗�',
+ '杩�30澶╁摢涓鎴峰埄娑﹁础鐚渶楂�',
+ '鍒嗘瀽杩�30澶╁簱瀛樿祫閲戝崰鐢�',
+ '棰勬祴鏈潵3涓湀鐜伴噾娴�',
+ '鏌ヨ搴旀敹鍥炴椋庨櫓',
+ '鍝釜宸ュ簭鎴愭湰鏈�楂�'
+ ]
+}
diff --git a/src/components/AIChatSidebar/assistants/generalAssistant.js b/src/components/AIChatSidebar/assistants/generalAssistant.js
new file mode 100644
index 0000000..b5610e3
--- /dev/null
+++ b/src/components/AIChatSidebar/assistants/generalAssistant.js
@@ -0,0 +1,33 @@
+import { Cpu } from '@element-plus/icons-vue'
+
+export const generalAssistant = {
+ key: 'general',
+ label: '寰呭姙鍔╃悊',
+ title: '寰呭姙鏅鸿兘鍔╃悊',
+ tooltip: '寰呭姙鍔╂墜',
+ icon: Cpu,
+ apiBase: '/xiaozhi',
+ storageKey: 'ai_chat_uuid',
+ placeholder: '璇疯緭鍏ユ偍鐨勯棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ洖绛斾綘鐨勯棶棰橈紝涓轰綘鎻愪緵涓氬姟鏁版嵁瑙h淇℃伅銆佸鐞嗗缓璁拰杈呭姪鍐崇瓥鏀寔銆�',
+ allowFileUpload: true,
+ emptySessionText: '鏆傛棤鍘嗗彶浼氳瘽',
+ quickPrompts: [
+ '鎴戝綋鍓嶆湁鍝簺瀹℃壒寰呭姙闇�瑕佸鐞嗭紵',
+ '甯垜鍒楀嚭浠婂ぉ鏂板鐨勫鎵瑰緟鍔炪��',
+ '褰撳墠寰呮垜瀹℃壒鐨勫崟鎹紝鎸夋椂闂村�掑簭鍒楀嚭鏉ャ��',
+ '鎴戝彂璧风殑瀹℃壒閲岋紝鍝簺杩樺湪澶勭悊涓紵',
+ '鏌ヨ娴佺▼缂栧彿 XXX 鐨勫鎵硅鎯呫��',
+ '娴佺▼缂栧彿 XXX 鐜板湪鍗″湪鍝釜瀹℃壒鑺傜偣锛熷綋鍓嶅鎵逛汉鏄皝锛�',
+ '甯垜鏌ョ湅娴佺▼缂栧彿 XXX 鐨勫鎵规祦杞褰曘��',
+ '杩�7澶╂垜鐨勫鎵瑰緟鍔炵粺璁℃儏鍐垫�庝箞鏍凤紵',
+ '鏈湀鎴戠殑瀹℃壒涓紝閫氳繃銆侀┏鍥炪�佸鐞嗕腑鍚勬湁澶氬皯锛�',
+ '杩�30澶╁悇绫诲瀷瀹℃壒鏁伴噺鍒嗗竷鏄粈涔堬紵',
+ '甯垜瀹℃壒閫氳繃娴佺▼缂栧彿 XXX锛屽娉ㄢ�滃悓鎰忊�濄��',
+ '甯垜椹冲洖娴佺▼缂栧彿 XXX锛屽娉ㄢ�滆琛ュ厖璇存槑鈥濄��',
+ '鎾ら攢鎴戝垰鍒氬娴佺▼缂栧彿 XXX 鐨勫鎵规搷浣溿��',
+ '甯垜淇敼娴佺▼缂栧彿 XXX 鐨勫娉ㄤ负鈥滃凡琛ュ厖闄勪欢鈥濄��',
+ '鍒犻櫎鎴戝彂璧风殑娴佺▼缂栧彿 XXX銆�'
+ ]
+}
diff --git a/src/components/AIChatSidebar/assistants/index.js b/src/components/AIChatSidebar/assistants/index.js
new file mode 100644
index 0000000..6e2a35d
--- /dev/null
+++ b/src/components/AIChatSidebar/assistants/index.js
@@ -0,0 +1,17 @@
+import { generalAssistant } from './generalAssistant'
+import { purchaseAssistant } from './purchaseAssistant'
+import { productionAssistant } from './productionAssistant'
+import { salesAssistant } from './salesAssistant'
+import { financeAssistant } from './financeAssistant'
+
+export { generalAssistant, purchaseAssistant, productionAssistant, salesAssistant, financeAssistant }
+
+export const assistantRegistry = {
+ general: generalAssistant,
+ sales: salesAssistant,
+ purchase: purchaseAssistant,
+ production: productionAssistant,
+ finance: financeAssistant
+}
+
+export const builtInAssistants = [generalAssistant, salesAssistant, purchaseAssistant, productionAssistant, financeAssistant]
diff --git a/src/components/AIChatSidebar/assistants/productionAssistant.js b/src/components/AIChatSidebar/assistants/productionAssistant.js
new file mode 100644
index 0000000..d5b4924
--- /dev/null
+++ b/src/components/AIChatSidebar/assistants/productionAssistant.js
@@ -0,0 +1,28 @@
+import { Operation } from '@element-plus/icons-vue'
+
+export const productionAssistant = {
+ key: 'production',
+ label: '鐢熶骇鍔╃悊',
+ title: '鐢熶骇鏅鸿兘鍔╃悊',
+ tooltip: '鐢熶骇鏅鸿兘鍔╂墜',
+ icon: Operation,
+ apiBase: '/manufacturing-ai',
+ storageKey: 'production_ai_chat_uuid',
+ placeholder: '璇疯緭鍏ョ敓浜х浉鍏抽棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ洿缁曠敓浜х幇鍦恒�佽鍒掋�佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞嗘彁渚涙煡璇€�侀璀︺�佸垎鏋愬拰鍔炵悊寤鸿銆�',
+ allowFileUpload: false,
+ emptySessionText: '鏆傛棤鐢熶骇浼氳瘽',
+ quickPrompts: [
+ '鏌ヨ鐢熶骇鐜板満鏁版嵁',
+ '鏌ヨ鐢熶骇璁″垝鎵ц鎯呭喌',
+ '鏌ヨ宸ュ崟鎵ц鎯呭喌',
+ '鏌ヨ璁惧杩愯鎯呭喌',
+ '鏌ヨ璐ㄩ噺寮傚父鏁版嵁',
+ '鏌ヨ鐗╂枡鐩稿叧鏁版嵁',
+ '鏌ョ湅鍒堕�犻璀︾湅鏉�',
+ '鍒嗘瀽鏈湀鍒堕�犵粡钀ユ儏鍐�',
+ '鍒嗘瀽杩�7澶╃敓浜у紓甯�',
+ '鐢熸垚鍒堕�犲姙鐞嗗缓璁�'
+ ]
+}
diff --git a/src/components/AIChatSidebar/assistants/purchaseAssistant.js b/src/components/AIChatSidebar/assistants/purchaseAssistant.js
new file mode 100644
index 0000000..5dd85e7
--- /dev/null
+++ b/src/components/AIChatSidebar/assistants/purchaseAssistant.js
@@ -0,0 +1,30 @@
+import { ShoppingCart } from '@element-plus/icons-vue'
+
+export const purchaseAssistant = {
+ key: 'purchase',
+ label: '閲囪喘鍔╃悊',
+ title: '閲囪喘鏅鸿兘鍔╃悊',
+ tooltip: '閲囪喘鏅鸿兘鍔╃悊',
+ icon: ShoppingCart,
+ apiBase: '/purchase-ai',
+ storageKey: 'purchase_ai_chat_uuid',
+ placeholder: '璇疯緭鍏ラ噰璐棶棰�... (Enter 鍙戦��, Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ崗鍔╀綘鍒嗘瀽閲囪喘璁㈠崟銆佸埌璐ц繘搴︺�佷緵搴斿晢琛ㄧ幇鍜屼粯娆炬儏鍐碉紝甯姪浣犲揩閫熷畾浣嶉噰璐紓甯搞��',
+ allowFileUpload: true,
+ allowMultipleFileUpload: true,
+ fileAnalyzeUrl: '/purchase-ai/analyze-files',
+ emptySessionText: '鏆傛棤閲囪喘浼氳瘽',
+ quickPrompts: [
+ '鏌ヨ閲囪喘鍙拌处鍒楄〃',
+ '鏌ヨ鏈�杩�10鏉¢噰璐彴璐�',
+ '缁熻鏈湀閲囪喘鏁版嵁',
+ '鏌ヨ鏈湀閲囪喘鐗╂枡閲戦鎺掕',
+ '鏌ヨ鏈叆搴撻噰璐鍗�',
+ '鏌ヨ閲囪喘鍒拌揣寮傚父',
+ '鏌ヨ寰呬粯娆鹃噰璐崟',
+ '鏌ヨ鏈湀閲囪喘閫�璐ф儏鍐�',
+ '鏌ヨ鏌愪釜閲囪喘鍙拌处璇︽儏',
+ '鍒嗘瀽渚涘簲鍟嗛噰璐噾棰濇帓鍚�'
+ ]
+}
diff --git a/src/components/AIChatSidebar/assistants/salesAssistant.js b/src/components/AIChatSidebar/assistants/salesAssistant.js
new file mode 100644
index 0000000..03cb102
--- /dev/null
+++ b/src/components/AIChatSidebar/assistants/salesAssistant.js
@@ -0,0 +1,28 @@
+import { TrendCharts } from '@element-plus/icons-vue'
+
+export const salesAssistant = {
+ key: 'sales',
+ label: '閿�鍞姪鎵�',
+ title: '閿�鍞櫤鑳藉姪鎵�',
+ tooltip: '閿�鍞櫤鑳藉姪鎵�',
+ icon: TrendCharts,
+ apiBase: '/sales-ai',
+ storageKey: 'sales_ai_chat_uuid',
+ placeholder: '璇疯緭鍏ラ攢鍞浉鍏抽棶棰�... (Enter 鍙戦�� / Shift+Enter 鎹㈣)',
+ welcomeMessage: '浣犲ソ',
+ description: '鎴戝彲浠ュ崗鍔╀綘鏌ヨ瀹㈡埛妗f銆侀攢鍞姤浠枫�侀攢鍞彴璐︺�侀攢鍞��璐с�佸鎴峰線鏉ャ�佸彂璐у彴璐︼紝骞堕噸鐐瑰垎鏋愬鎴锋祦澶遍闄╁強鍥炴/鎶ヤ环绛栫暐銆�',
+ allowFileUpload: false,
+ emptySessionText: '鏆傛棤閿�鍞細璇�',
+ quickPrompts: [
+ '鏌ヨ绉佹捣瀹㈡埛妗f鍓�10鏉�',
+ '鏌ヨ鍏捣瀹㈡埛妗f',
+ '鏌ヨ鏈湀閿�鍞姤浠�',
+ '鏌ヨ鏈湀閿�鍞彴璐�',
+ '鏌ヨ杩�30澶╅攢鍞��璐�',
+ '鏌ヨ杩�30澶╁鎴峰洖娆惧線鏉�',
+ '鏌ヨ鏈湀鍙戣揣鍙拌处',
+ '鏌ョ湅閿�鍞寚鏍囩粺璁�',
+ '甯垜鍋氬鎴锋祦澶遍闄╁垎鏋愶紝杩�30澶╋紝鍓�20鏉�',
+ '鐢熸垚鍥炴涓庢姤浠风瓥鐣ュ缓璁紝浼樺厛楂橀闄╁鎴�'
+ ]
+}
diff --git a/src/components/AIChatSidebar/index.vue b/src/components/AIChatSidebar/index.vue
new file mode 100644
index 0000000..7b22d7b
--- /dev/null
+++ b/src/components/AIChatSidebar/index.vue
@@ -0,0 +1,6513 @@
+<template>
+ <div class="ai-chat-sidebar-wrapper">
+ <!-- 鎮诞鍥炬爣 -->
+ <div v-if="!hideTrigger" class="ai-chat-trigger" @click="toggleSidebar" v-show="!visible">
+ <el-tooltip :content="currentAssistant.tooltip" placement="left">
+ <div class="trigger-icon">
+ <el-icon :size="22" color="#fff"><component :is="currentAssistant.icon" /></el-icon>
+ </div>
+ </el-tooltip>
+ </div>
+
+ <!-- 渚ц竟鏍忓璇濇 -->
+ <el-drawer
+ v-model="visible"
+ :size="computedDrawerSize"
+ :direction="drawerDirection"
+ :with-header="true"
+ class="ai-chat-drawer"
+ :modal="false"
+ modal-class="ai-chat-overlay"
+ :show-close="false"
+ :append-to-body="false"
+ :close-on-press-escape="!hideTrigger"
+ :close-on-click-modal="!hideTrigger"
+ @close="handleClose"
+ >
+ <template #header>
+ <div class="drawer-header">
+ <div class="header-left">
+ <el-icon :size="20" class="header-icon"><component :is="currentAssistant.icon" /></el-icon>
+ <span class="title">{{ currentAssistant.title }}</span>
+ </div>
+ <div v-if="showAssistantSwitch" class="assistant-switcher">
+ <el-radio-group v-model="selectedAssistantKey" size="small">
+ <el-radio-button
+ v-for="assistant in assistants"
+ :key="assistant.key"
+ :label="assistant.key"
+ >
+ {{ assistant.label }}
+ </el-radio-button>
+ </el-radio-group>
+ </div>
+ <div class="header-actions">
+ <el-tooltip content="浼氳瘽鍘嗗彶" placement="bottom">
+ <el-button link class="header-action-btn" @click="handleToggleHistory">
+ <el-icon :size="18"><Timer /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="寮�鍚柊浼氳瘽" placement="bottom">
+ <el-button link class="header-action-btn" @click="handleNewChat">
+ <el-icon :size="18"><Plus /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <el-button
+ v-if="headerExtraActionText"
+ link
+ class="header-action-btn header-action-btn--text"
+ @click="handleHeaderExtraAction"
+ >
+ {{ headerExtraActionText }}
+ </el-button>
+ <div v-if="!hideTrigger" class="action-divider"></div>
+ <el-tooltip v-if="!hideTrigger" content="鍏抽棴" placement="bottom">
+ <el-button link class="header-action-btn close-btn" @click="handleManualClose">
+ <el-icon :size="18"><Close /></el-icon>
+ </el-button>
+ </el-tooltip>
+ </div>
+ </div>
+ </template>
+
+ <div class="chat-container">
+ <!-- 鍘嗗彶浼氳瘽鍒楄〃 -->
+ <div v-if="showHistory" class="history-panel">
+ <div class="history-header">
+ <span>鏈�杩戜細璇�</span>
+ <el-button link type="primary" @click="showHistory = false">杩斿洖瀵硅瘽</el-button>
+ </div>
+ <el-skeleton :loading="loadingSessions" animated>
+ <template #template>
+ <div v-for="i in 5" :key="i" style="padding: 10px">
+ <el-skeleton-item variant="p" style="width: 80%" />
+ </div>
+ </template>
+ <div class="session-list">
+ <div
+ v-for="session in sessions"
+ :key="session.memoryId"
+ :class="['session-item', { active: uuid === session.memoryId }]"
+ @click="selectSession(session)"
+ >
+ <el-icon><ChatDotSquare /></el-icon>
+ <span class="session-name" :title="session.lastMessage || '鏂颁細璇�'">
+ {{ session.lastMessage || '鏂颁細璇�' }}
+ </span>
+ <el-button
+ link
+ type="danger"
+ class="delete-btn"
+ @click.stop="handleDeleteSession(session.memoryId)"
+ >
+ <el-icon><Delete /></el-icon>
+ </el-button>
+ </div>
+ <el-empty v-if="sessions.length === 0" :description="currentAssistant.emptySessionText" />
+ </div>
+ </el-skeleton>
+ </div>
+
+ <div v-else class="chat-main">
+ <div :class="['chat-hero', { compact: hasMessages }]">
+ <div :class="['assistant-stand', { thinking: isSending, compact: hasMessages }]">
+ <div class="assistant-halo"></div>
+ <div class="assistant-scan-ring"></div>
+ <div class="assistant-orbit assistant-orbit-a"></div>
+ <div class="assistant-orbit assistant-orbit-b"></div>
+ <div class="assistant-model-shell">
+ <div class="assistant-model-cut">
+ <img
+ v-if="currentAssistantAvatar"
+ class="assistant-model-img"
+ :src="currentAssistantAvatar"
+ :alt="currentAssistant.label"
+ />
+ <div v-else class="assistant-model-fallback">
+ <el-icon :size="30"><component :is="currentAssistant.icon" /></el-icon>
+ </div>
+ </div>
+ </div>
+ <div class="assistant-status">
+ <span class="assistant-status-dot"></span>
+ {{ isSending ? '鎬濊�冧腑...' : currentAssistant.label }}
+ </div>
+ <div class="assistant-base assistant-base-lg"></div>
+ <div class="assistant-base assistant-base-md"></div>
+ <div class="assistant-base assistant-base-sm"></div>
+ </div>
+
+ <div :class="['welcome-card', { compact: hasMessages }]">
+ <div class="welcome-eyebrow">鏅鸿兘鍔╂墜</div>
+ <h3 class="welcome-title">
+ 鎮ㄥソ
+ <br />
+ 鎴戞槸{{ currentAssistant.label }}鍒嗘瀽瑙h鍔╂墜
+ </h3>
+ <p class="welcome-desc">
+ {{ currentAssistant.description || '鎴戝彲浠ュ洿缁曚笟鍔¢棶棰樻彁渚涜В璇汇�佹煡璇㈠缓璁拰鍒嗘瀽鏀寔锛屽府鍔╀綘鏇村揩瀹屾垚鍒ゆ柇涓庡鐞嗐��' }}
+ </p>
+
+ <div class="quick-prompt-list">
+ <button
+ v-for="prompt in displayedQuickPrompts"
+ :key="prompt"
+ type="button"
+ class="quick-prompt-btn"
+ :disabled="isSending"
+ @click="sendQuickPrompt(prompt)"
+ >
+ {{ prompt }}
+ </button>
+ </div>
+
+ <button
+ v-if="quickPrompts.length > quickPromptLimit"
+ type="button"
+ class="more-prompts-btn"
+ @click="refreshQuickPrompts"
+ >
+ <el-icon><RefreshRight /></el-icon>
+ <span>鎹竴鎹�</span>
+ </button>
+ </div>
+ </div>
+
+ <div class="message-list" ref="messageListRef">
+ <div
+ v-for="(message, index) in messages"
+ :key="index"
+ :class="['message-item', message.isUser ? 'user-message' : 'bot-message']"
+ >
+ <div class="avatar">
+ <el-icon v-if="message.isUser"><User /></el-icon>
+ <el-icon v-else><Cpu /></el-icon>
+ </div>
+ <div class="message-content">
+ <!-- 鏂囨湰鍐呭 -->
+ <div class="text-box" v-html="message.htmlContent"></div>
+
+ <div v-if="message.localUploadFiles?.length" class="message-local-file-list">
+ <div
+ v-for="(file, fileIndex) in message.localUploadFiles"
+ :key="`${file.previewId || file.name}-${fileIndex}`"
+ :class="['message-local-file-item', { clickable: !!file.accessUrl && !file.isImage }]"
+ @click="handleMessageFileClick(file)"
+ >
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(message.localUploadFiles)"
+ :initial-index="getImagePreviewInitialIndex(message.localUploadFiles, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="message-local-file-thumb"
+ />
+ <el-icon v-else class="message-local-file-icon"><Document /></el-icon>
+ <div class="message-local-file-meta">
+ <span
+ :class="['message-local-file-name', { clickable: !!file.accessUrl }]"
+ :title="file.name"
+ @click.stop="openMessageAttachment(file)"
+ >
+ {{ file.name }}
+ </span>
+ <small v-if="Number(file.size) > 0" class="message-local-file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍥捐〃鍐呭 -->
+ <div v-if="message.chartOptions && message.chartRenderReady" class="charts-wrapper">
+ <div
+ v-for="(option, key) in message.chartOptions"
+ :key="key"
+ class="chart-item"
+ :id="`ai-chart-${index}-${key}`"
+ ></div>
+ </div>
+ <div
+ v-else-if="message.chartMarkdownParseFailed"
+ class="chart-empty-state"
+ >
+ 鍥捐〃瑙f瀽澶辫触锛岃绋嶅悗閲嶈瘯銆�
+ </div>
+
+ <!-- 琛ㄦ牸鍐呭 -->
+ <div v-if="message.type === 'todo_list' && message.tableData" class="table-wrapper">
+ <el-table :data="message.tableData.items" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.tableData.columns"
+ :key="col"
+ :prop="col"
+ :label="columnLabelMap[col] || col"
+ min-width="100"
+ show-overflow-tooltip
+ />
+ </el-table>
+ </div>
+
+ <div v-if="message.manufacturingData" class="manufacturing-card">
+ <div class="manufacturing-card__title">{{ getManufacturingTypeLabel(message.type) }}</div>
+
+ <div
+ v-if="message.manufacturingData.summaryEntries?.length || message.manufacturingData.coreMetrics?.length"
+ class="manufacturing-summary-grid"
+ >
+ <div
+ v-for="(entry, entryIndex) in message.manufacturingData.summaryEntries"
+ :key="`summary-${entry.key}-${entryIndex}`"
+ class="manufacturing-summary-item"
+ >
+ <span class="manufacturing-summary-label">{{ entry.label }}</span>
+ <strong class="manufacturing-summary-value">{{ entry.value }}</strong>
+ </div>
+ <div
+ v-for="(metric, metricIndex) in message.manufacturingData.coreMetrics"
+ :key="`core-${metric.key}-${metricIndex}`"
+ class="manufacturing-summary-item manufacturing-summary-item--core"
+ >
+ <span class="manufacturing-summary-label">{{ metric.label }}</span>
+ <strong class="manufacturing-summary-value">{{ metric.value }}</strong>
+ </div>
+ </div>
+
+ <div v-if="message.manufacturingData.warningItems?.length" class="manufacturing-warning-list">
+ <div
+ v-for="(warning, warningIndex) in message.manufacturingData.warningItems"
+ :key="`warning-${warning.title || warningIndex}`"
+ class="manufacturing-warning-item"
+ >
+ <div class="manufacturing-warning-item__head">
+ <el-tag size="small" :type="getManufacturingWarningLevelType(warning.level)">
+ {{ getManufacturingWarningLevelLabel(warning.level) }}
+ </el-tag>
+ <strong>{{ warning.title || `棰勮 ${warningIndex + 1}` }}</strong>
+ <span v-if="warning.count !== '' && warning.count !== null && warning.count !== undefined" class="manufacturing-warning-count">
+ {{ warning.count }}
+ </span>
+ </div>
+ <p v-if="warning.detail" class="manufacturing-warning-detail">{{ warning.detail }}</p>
+ </div>
+ </div>
+
+ <div
+ v-if="message.manufacturingData.listItems?.length && message.manufacturingData.columns?.length"
+ class="table-wrapper manufacturing-table-wrapper"
+ >
+ <el-table :data="message.manufacturingData.listItems" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.manufacturingData.columns"
+ :key="col"
+ :label="getStructuredFieldLabel(col)"
+ min-width="140"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <div v-if="message.manufacturingData.actionCards?.length" class="manufacturing-action-list">
+ <div
+ v-for="(card, cardIndex) in message.manufacturingData.actionCards"
+ :key="card.runtimeKey || `${card.code}-${card.targetApi}-${cardIndex}`"
+ class="manufacturing-action-card"
+ >
+ <div class="manufacturing-action-card__head">
+ <strong>{{ card.name || `鍔ㄤ綔 ${cardIndex + 1}` }}</strong>
+ <el-tag size="small" type="info">{{ getNormalizedRequestMethod(card.method) }}</el-tag>
+ </div>
+ <div class="manufacturing-action-card__meta">
+ <span>{{ card.code || '--' }}</span>
+ <span>{{ card.targetApi || '--' }}</span>
+ </div>
+ <p v-if="card.description" class="manufacturing-action-card__desc">{{ card.description }}</p>
+ <div v-if="card.requiredFields?.length" class="manufacturing-required-fields">
+ <span>蹇呭~瀛楁</span>
+ <el-tag
+ v-for="field in card.requiredFields"
+ :key="field"
+ size="small"
+ type="warning"
+ >
+ {{ getStructuredPathLabel(field) }}
+ </el-tag>
+ </div>
+ <el-input
+ v-model="card.payloadText"
+ type="textarea"
+ :rows="6"
+ resize="vertical"
+ :disabled="card.executing"
+ placeholder="璇疯緭鍏� JSON 璇锋眰鍙傛暟"
+ />
+ <div class="manufacturing-action-footer">
+ <span
+ v-if="card.executeResult"
+ :class="['manufacturing-action-result', card.executeError ? 'error' : 'success']"
+ >
+ {{ card.executeResult }}
+ </span>
+ <el-button
+ type="primary"
+ size="small"
+ :loading="card.executing"
+ @click="executeManufacturingAction(message, card, cardIndex)"
+ >
+ 纭骞舵墽琛�
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="message.salesData" class="sales-structured-card">
+ <div class="sales-structured-card__title">{{ getSalesTypeLabel(message.type) }}</div>
+
+ <div v-if="message.salesData.summaryEntries?.length" class="sales-summary-grid">
+ <div
+ v-for="(entry, entryIndex) in message.salesData.summaryEntries"
+ :key="`sales-summary-${entry.key}-${entryIndex}`"
+ class="sales-summary-item"
+ >
+ <span class="sales-summary-label">{{ entry.label }}</span>
+ <strong class="sales-summary-value">{{ entry.value }}</strong>
+ </div>
+ </div>
+
+ <div v-if="message.type === 'sales_customer_churn_risk' && message.salesData.listItems?.length" class="sales-focus-list">
+ <div
+ v-for="(item, itemIndex) in message.salesData.listItems"
+ :key="`risk-${item.customerName || itemIndex}`"
+ class="sales-focus-item"
+ >
+ <div class="sales-focus-item__head">
+ <strong>{{ formatStructuredValue(item.customerName) }}</strong>
+ <div class="sales-focus-tags">
+ <el-tag size="small" :type="getSalesLevelTagType(item.riskLevel)">
+ {{ getSalesLevelLabel(item.riskLevel, 'risk') }}
+ </el-tag>
+ <el-tag size="small" type="warning">椋庨櫓鍒� {{ formatStructuredValue(item.riskScore) }}</el-tag>
+ </div>
+ </div>
+ <div class="sales-focus-metrics">
+ <span>寰呭洖娆撅細{{ formatStructuredValue(item.pendingAmount) }}</span>
+ <span>寰呭洖娆惧崰姣旓細{{ formatStructuredValue(item.pendingRate) }}</span>
+ <span>璺濅笂娆′笅鍗曪細{{ formatStructuredValue(item.daysSinceLastOrder) }}</span>
+ </div>
+ <div v-if="toStructuredStringArray(item.riskReasons).length" class="sales-focus-reasons">
+ <el-tag
+ v-for="(reason, reasonIndex) in toStructuredStringArray(item.riskReasons)"
+ :key="`${item.customerName || itemIndex}-reason-${reasonIndex}`"
+ size="small"
+ type="danger"
+ effect="plain"
+ >
+ {{ reason }}
+ </el-tag>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="message.type === 'sales_collection_quote_strategy' && message.salesData.listItems?.length" class="sales-focus-list">
+ <div
+ v-for="(item, itemIndex) in message.salesData.listItems"
+ :key="`strategy-${item.customerName || itemIndex}`"
+ class="sales-focus-item sales-focus-item--strategy"
+ >
+ <div class="sales-focus-item__head">
+ <strong>{{ formatStructuredValue(item.customerName) }}</strong>
+ <div class="sales-focus-tags">
+ <el-tag size="small" :type="getSalesLevelTagType(item.priority)">
+ {{ getSalesLevelLabel(item.priority, 'priority') }}
+ </el-tag>
+ <el-tag size="small" type="success">杞寲鐜� {{ formatStructuredValue(item.quoteConversionRate) }}</el-tag>
+ </div>
+ </div>
+ <div class="sales-focus-metrics">
+ <span>寰呭洖娆撅細{{ formatStructuredValue(item.pendingAmount) }}</span>
+ <span v-if="item.nextAction">涓嬩竴姝ワ細{{ formatStructuredValue(item.nextAction) }}</span>
+ </div>
+ <p v-if="item.collectionStrategy" class="sales-strategy-line">
+ <strong>鍥炴绛栫暐锛�</strong>{{ formatStructuredValue(item.collectionStrategy) }}
+ </p>
+ <p v-if="item.quotationStrategy" class="sales-strategy-line">
+ <strong>鎶ヤ环绛栫暐锛�</strong>{{ formatStructuredValue(item.quotationStrategy) }}
+ </p>
+ </div>
+ </div>
+
+ <div
+ v-if="message.salesData.listItems?.length && message.salesData.columns?.length && !isSalesFocusType(message.type)"
+ class="table-wrapper manufacturing-table-wrapper"
+ >
+ <el-table :data="message.salesData.listItems" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.salesData.columns"
+ :key="col"
+ :label="getStructuredFieldLabel(col)"
+ min-width="140"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <div v-if="message.salesData.topCustomers?.length && message.salesData.topCustomerColumns?.length" class="table-wrapper manufacturing-table-wrapper">
+ <div class="sales-section-title">閲嶇偣瀹㈡埛</div>
+ <el-table :data="message.salesData.topCustomers" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.salesData.topCustomerColumns"
+ :key="`top-customer-${col}`"
+ :label="getStructuredFieldLabel(col)"
+ min-width="120"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <div v-if="message.salesData.contractTrend?.length && message.salesData.contractTrendColumns?.length" class="table-wrapper manufacturing-table-wrapper">
+ <div class="sales-section-title">鍚堝悓瓒嬪娍</div>
+ <el-table :data="message.salesData.contractTrend" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.salesData.contractTrendColumns"
+ :key="`contract-trend-${col}`"
+ :label="getStructuredFieldLabel(col)"
+ min-width="120"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div v-if="message.purchaseData" class="sales-structured-card">
+ <div class="sales-structured-card__title">{{ getPurchaseTypeLabel(message.type) }}</div>
+
+ <div v-if="message.purchaseData.summaryEntries?.length" class="sales-summary-grid">
+ <div
+ v-for="(entry, entryIndex) in message.purchaseData.summaryEntries"
+ :key="`purchase-summary-${entry.key}-${entryIndex}`"
+ class="sales-summary-item"
+ >
+ <span class="sales-summary-label">{{ entry.label }}</span>
+ <strong class="sales-summary-value">{{ entry.value }}</strong>
+ </div>
+ </div>
+
+ <div
+ v-if="message.purchaseData.listItems?.length && message.purchaseData.columns?.length"
+ class="table-wrapper manufacturing-table-wrapper"
+ >
+ <el-table :data="message.purchaseData.listItems" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in message.purchaseData.columns"
+ :key="col"
+ :label="getStructuredFieldLabel(col)"
+ min-width="140"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div v-if="message.financeData" class="sales-structured-card">
+ <div class="sales-structured-card__title">{{ getFinancialTypeLabel(message.type) }}</div>
+
+ <div v-if="message.financeData.summaryEntries?.length" class="sales-summary-grid">
+ <div
+ v-for="(entry, entryIndex) in message.financeData.summaryEntries"
+ :key="`finance-summary-${entry.key}-${entryIndex}`"
+ class="sales-summary-item"
+ >
+ <span class="sales-summary-label">{{ entry.label }}</span>
+ <strong class="sales-summary-value">{{ entry.value }}</strong>
+ </div>
+ </div>
+
+ <div v-if="message.financeData.headline" class="finance-headline">
+ {{ message.financeData.headline }}
+ </div>
+
+ <div v-if="message.financeData.conclusions?.length" class="finance-text-section">
+ <div class="sales-section-title">鏍稿績缁撹</div>
+ <ul>
+ <li v-for="(item, idx) in message.financeData.conclusions" :key="`finance-conclusion-${idx}`">
+ {{ item }}
+ </li>
+ </ul>
+ </div>
+
+ <div v-if="message.financeData.riskSuggestions?.length" class="finance-text-section">
+ <div class="sales-section-title">椋庨櫓寤鸿</div>
+ <ul>
+ <li v-for="(item, idx) in message.financeData.riskSuggestions" :key="`finance-risk-${idx}`">
+ {{ item }}
+ </li>
+ </ul>
+ </div>
+
+ <div
+ v-for="section in message.financeData.sections"
+ :key="`finance-section-${section.key}`"
+ class="table-wrapper manufacturing-table-wrapper"
+ >
+ <div class="sales-section-title">{{ section.title }}</div>
+ <el-table :data="section.items" border stripe size="small" style="width: 100%">
+ <el-table-column
+ v-for="col in section.columns"
+ :key="`finance-${section.key}-${col}`"
+ :label="getStructuredFieldLabel(col)"
+ min-width="120"
+ show-overflow-tooltip
+ >
+ <template #default="{ row }">
+ {{ formatStructuredValue(row[col]) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div v-if="message.purchaseIntentData?.quickPrompts?.length" class="purchase-intent-quick-prompt-wrap">
+ <div class="purchase-intent-quick-prompt-title">璇曡瘯浠ヤ笅鎻愰棶</div>
+ <div class="quick-prompt-list purchase-intent-quick-prompt-list">
+ <button
+ v-for="prompt in message.purchaseIntentData.quickPrompts"
+ :key="`purchase-intent-${prompt}`"
+ type="button"
+ class="quick-prompt-btn"
+ :disabled="isSending"
+ @click="sendQuickPrompt(prompt)"
+ >
+ {{ prompt }}
+ </button>
+ </div>
+ </div>
+
+ <div v-if="message.purchaseAnalysisData" class="purchase-confirm-card">
+ <div class="purchase-confirm-header">
+ <span>{{ businessTypeLabelMap[message.purchaseAnalysisData.businessType] || message.purchaseAnalysisData.businessType || '閲囪喘涓氬姟' }}</span>
+ <el-tag size="small" type="success" v-if="message.purchaseAnalysisData.confidence !== undefined">
+ 缃俊搴� {{ formatPercent(message.purchaseAnalysisData.confidence) }}
+ </el-tag>
+ </div>
+ <div class="purchase-confirm-desc">
+ {{ getPurchaseConfirmDescription(message.purchaseAnalysisData) }}
+ </div>
+ <div v-if="isPurchasePayloadEmpty(message.purchaseAnalysisData.payload)" class="purchase-empty-state">
+ <div class="empty-title">娌℃湁璇嗗埆鍒板彲鐩存帴鎻愪氦鐨勯噰璐彴璐︿俊鎭�</div>
+ <div class="empty-desc">褰撳墠鏂囦欢閲岀己灏戦噰璐悎鍚屽彿銆佷緵搴斿晢銆侀」鐩�佹棩鏈熴�佺墿鏂欐槑缁嗙瓑鍏抽敭鍐呭銆傝涓婁紶鏇村畬鏁寸殑鍚堝悓銆佽鍗曟垨鏄庣粏琛紝鎴栧湪涓嬫柟琛ュ厖鏁版嵁鍚庡啀纭銆�</div>
+ </div>
+ <div v-if="message.purchaseAnalysisData.warnings?.length" class="purchase-alert warning">
+ <strong>椋庨櫓鎻愮ず</strong>
+ <ul>
+ <li v-for="(warning, warningIndex) in message.purchaseAnalysisData.warnings" :key="warningIndex">
+ {{ formatPreviewItem(warning) }}
+ </li>
+ </ul>
+ </div>
+ <div v-if="getVisiblePurchaseMissingFields(message.purchaseAnalysisData).length" class="purchase-alert missing">
+ <strong>闇�瑕佽ˉ鍏� {{ getVisiblePurchaseMissingFields(message.purchaseAnalysisData).length }} 椤�</strong>
+ <el-tag
+ v-for="field in getVisiblePurchaseMissingFields(message.purchaseAnalysisData)"
+ :key="field"
+ size="small"
+ type="danger"
+ >
+ {{ field }}
+ </el-tag>
+ </div>
+ <div v-if="message.purchaseAnalysisData.preview?.length" class="purchase-preview">
+ <div class="purchase-section-title">纭鎽樿</div>
+ <ul>
+ <li v-for="(item, previewIndex) in message.purchaseAnalysisData.preview" :key="previewIndex">
+ {{ formatPreviewItem(item) }}
+ </li>
+ </ul>
+ </div>
+ <div class="purchase-section-title">琛ュ厖鎴栫‘璁ゆ暟鎹�</div>
+ <div class="payload-toolbar">
+ <el-button
+ size="small"
+ plain
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseRootField(message)"
+ >
+ <el-icon><Plus /></el-icon>
+ 鏂板椤跺眰瀛楁
+ </el-button>
+ </div>
+ <div class="payload-tree-table-wrapper">
+ <el-table
+ :data="message.payloadTreeData || []"
+ row-key="id"
+ border
+ stripe
+ size="small"
+ default-expand-all
+ :tree-props="{ children: 'children' }"
+ empty-text="鏆傛棤寰呯‘璁ゆ暟鎹�"
+ >
+ <el-table-column label="瀛楁" min-width="240">
+ <template #default="{ row }">
+ <div class="payload-key-cell">
+ <template v-if="row.parentType === 'object'">
+ <el-input
+ v-if="row.keyEditable"
+ v-model="row.key"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ placeholder="瀛楁鍚�"
+ />
+ <div v-else class="payload-fixed-key" :title="row.key">
+ <span>{{ getPurchaseFieldLabel(row.key) }}</span>
+ <small v-if="getPurchaseFieldLabel(row.key) !== row.key">{{ row.key }}</small>
+ </div>
+ </template>
+ <span v-else class="payload-array-index">{{ getPurchaseArrayItemLabel(row, message) }}</span>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="绫诲瀷" width="130" align="center">
+ <template #default="{ row }">
+ <el-select
+ v-model="row.valueType"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ @change="handlePurchaseNodeTypeChange(message, row)"
+ >
+ <el-option
+ v-for="option in purchaseValueTypeOptions"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍊�" min-width="250">
+ <template #default="{ row }">
+ <div v-if="row.valueType === 'object'" class="payload-container-cell">
+ 瀵硅薄锛坽{ row.children?.length || 0 }}锛�
+ </div>
+ <div v-else-if="row.valueType === 'array'" class="payload-container-cell">
+ 鏁扮粍锛坽{ row.children?.length || 0 }}锛�
+ </div>
+ <el-switch
+ v-else-if="row.valueType === 'boolean'"
+ v-model="row.value"
+ size="small"
+ :disabled="message.confirming || message.confirmed"
+ />
+ <span v-else-if="row.valueType === 'null'" class="payload-null-value">null</span>
+ <el-input
+ v-else
+ v-model="row.value"
+ size="small"
+ :placeholder="row.valueType === 'number' ? '璇疯緭鍏ユ暟瀛�' : '璇疯緭鍏ュ唴瀹�'"
+ :disabled="message.confirming || message.confirmed"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" align="center">
+ <template #default="{ row }">
+ <div class="payload-row-actions">
+ <el-tooltip v-if="row.valueType === 'object'" content="鏂板瀛楁" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseChildNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip v-else-if="row.valueType === 'array'" content="鏂板鏁扮粍椤�" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseChildNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip v-if="row.parentType === 'array'" content="鏂板鍚岀骇椤�" placement="top">
+ <el-button
+ :icon="Plus"
+ circle
+ size="small"
+ text
+ type="primary"
+ :disabled="message.confirming || message.confirmed"
+ @click="addPurchaseSiblingNode(message, row)"
+ />
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎褰撳墠椤�" placement="top">
+ <el-button
+ :icon="Delete"
+ circle
+ size="small"
+ text
+ type="danger"
+ :disabled="message.confirming || message.confirmed"
+ @click="removePurchaseNode(message, row)"
+ />
+ </el-tooltip>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <div class="payload-editor-tip">
+ 鏃ユ湡璇峰~鍐� yyyy-MM-dd锛屼緥濡� 2026-04-30銆備骇鍝佹槑缁嗗缓璁斁鍦ㄦ瘡鏉¢噰璐彴璐︾殑 productData 涓紝纭鏃朵細鑷姩鍏煎鏃ф牸寮忓苟娓呯悊瀹℃壒瀛楁銆�
+ </div>
+ <div class="purchase-confirm-actions">
+ <span v-if="message.confirmResult" :class="['confirm-result', message.confirmed ? 'success' : 'error']">
+ {{ message.confirmResult }}
+ </span>
+ <el-button
+ type="primary"
+ size="small"
+ :loading="message.confirming"
+ :disabled="message.confirmed || isSending"
+ @click="confirmPurchaseAnalysisFromTable(message)"
+ >
+ 纭骞舵墽琛�
+ </el-button>
+ </div>
+ </div>
+
+ <div v-if="message.isTyping" class="typing-indicator">
+ <span class="dot"></span>
+ <span class="dot"></span>
+ <span class="dot"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="input-area">
+ <div class="input-actions">
+ <el-button link class="utility-action-btn" type="primary" size="small" @click="handleNewChat">
+ <el-icon><Plus /></el-icon>鏂颁細璇�
+ </el-button>
+ <el-button v-if="isSending" link class="utility-action-btn stop-action-btn" type="danger" size="small" @click="stopGeneration">
+ <el-icon><VideoPause /></el-icon>鍋滄鐢熸垚
+ </el-button>
+ <el-upload
+ v-if="currentAssistant.allowFileUpload"
+ class="file-upload-trigger"
+ action="#"
+ :auto-upload="false"
+ :show-file-list="false"
+ v-model:file-list="uploadFileList"
+ :multiple="currentAssistant.allowMultipleFileUpload"
+ :on-change="handleFileChange"
+ :disabled="isSending"
+ >
+ <el-button link class="utility-action-btn upload-action-btn" type="primary" size="small" :disabled="isSending">
+ <el-icon><Upload /></el-icon>鍒嗘瀽鏂囦欢
+ </el-button>
+ </el-upload>
+ </div>
+ <div class="input-box">
+ <div v-if="selectedFiles.length" class="selected-file-list">
+ <div v-for="(file, fileIndex) in selectedFileSnapshots" :key="`${file.previewId || file.name}-${fileIndex}`" class="selected-file-tag">
+ <el-image
+ v-if="file.isImage && file.previewUrl"
+ :src="file.previewUrl"
+ :preview-src-list="getImagePreviewList(selectedFileSnapshots)"
+ :initial-index="getImagePreviewInitialIndex(selectedFileSnapshots, file.previewUrl)"
+ :z-index="4000"
+ preview-teleported
+ fit="cover"
+ class="selected-file-thumb"
+ />
+ <el-icon v-else><Document /></el-icon>
+ <div class="selected-file-meta">
+ <span class="file-name">{{ file.name }}</span>
+ <small class="file-size">{{ formatFileSize(file.size) }}</small>
+ </div>
+ <el-icon class="remove-file" @click="removeSelectedFile(fileIndex)"><Close /></el-icon>
+ </div>
+ </div>
+ <el-input
+ v-model="inputMessage"
+ type="textarea"
+ :rows="selectedFiles.length ? 2 : 3"
+ :placeholder="currentAssistant.placeholder"
+ resize="none"
+ @keydown.enter.exact.prevent="sendMessage"
+ />
+ <el-button
+ type="primary"
+ class="send-btn"
+ :disabled="isSending || (!inputMessage.trim() && !selectedFiles.length)"
+ @click="sendMessage"
+ aria-label="鍙戦��"
+ >
+ <el-icon><Promotion /></el-icon>
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-drawer>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, nextTick, watch, computed } from 'vue'
+import request from '@/utils/request'
+import * as echarts from 'echarts'
+import { Cpu, User, Plus, Timer, Delete, ChatDotSquare, VideoPause, Upload, Document, Close, Promotion, RefreshRight } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { builtInAssistants, generalAssistant } from './assistants'
+import todoAssistantAvatar from '@/assets/AI/寰呭姙鍔╂墜.png'
+import salesAssistantAvatar from '@/assets/AI/閿�鍞姪鎵�.png'
+import purchaseAssistantAvatar from '@/assets/AI/閲囪喘鍔╂墜.png'
+import productionAssistantAvatar from '@/assets/AI/鐢熶骇鍔╂墜.png'
+import financeAssistantAvatar from '@/assets/AI/璐㈠姟鍔╂墜.png'
+import bossAssistantAvatar from '@/assets/AI/寰呭姙鍔╂墜.png'
+
+const emit = defineEmits(['header-extra-action'])
+
+const props = defineProps({
+ assistants: {
+ type: Array,
+ default: () => []
+ },
+ defaultAssistant: {
+ type: String,
+ default: ''
+ },
+ hideTrigger: {
+ type: Boolean,
+ default: false
+ },
+ autoOpen: {
+ type: Boolean,
+ default: false
+ },
+ drawerSize: {
+ type: [String, Number],
+ default: ''
+ },
+ drawerDirection: {
+ type: String,
+ default: 'rtl'
+ },
+ headerExtraActionText: {
+ type: String,
+ default: ''
+ }
+})
+
+const hideTrigger = computed(() => props.hideTrigger)
+const headerExtraActionText = computed(() => String(props.headerExtraActionText || '').trim())
+const drawerDirection = computed(() => (props.drawerDirection === 'ttb' || props.drawerDirection === 'btt' || props.drawerDirection === 'ltr' || props.drawerDirection === 'rtl')
+ ? props.drawerDirection
+ : 'rtl')
+const assistants = computed(() => props.assistants?.length ? props.assistants : builtInAssistants)
+const selectedAssistantKey = ref(props.defaultAssistant || assistants.value[0]?.key || 'general')
+const currentAssistant = computed(() => assistants.value.find(item => item.key === selectedAssistantKey.value) || assistants.value[0] || builtInAssistants[0])
+const assistantAvatarByKey = {
+ general: todoAssistantAvatar,
+ todo: todoAssistantAvatar,
+ purchase: purchaseAssistantAvatar,
+ sales: salesAssistantAvatar,
+ production: productionAssistantAvatar,
+ finance: financeAssistantAvatar,
+ boss: bossAssistantAvatar
+}
+const currentAssistantAvatar = computed(() => {
+ const assistant = currentAssistant.value || {}
+ return assistant.avatar || assistantAvatarByKey[assistant.key] || ''
+})
+const showAssistantSwitch = computed(() => assistants.value.length > 1)
+const quickPromptLimit = 3
+const quickPromptStart = ref(0)
+const quickPrompts = computed(() => {
+ const assistant = currentAssistant.value || {}
+ if (Array.isArray(assistant.quickPrompts) && assistant.quickPrompts.length) {
+ return assistant.quickPrompts
+ }
+ return generalAssistant.quickPrompts || []
+})
+const displayedQuickPrompts = computed(() => {
+ const prompts = quickPrompts.value || []
+ if (prompts.length <= quickPromptLimit) return prompts
+
+ const result = []
+ for (let i = 0; i < quickPromptLimit; i++) {
+ result.push(prompts[(quickPromptStart.value + i) % prompts.length])
+ }
+ return result
+})
+const hasMessages = computed(() => messages.value.length > 0)
+
+const visible = ref(false)
+const windowWidth = ref(window.innerWidth)
+const responsiveDrawerSize = computed(() => {
+ if (windowWidth.value < 768) return '100%'
+ if (windowWidth.value < 1200) return '50%'
+ return '50%'
+})
+const computedDrawerSize = computed(() => props.drawerSize || responsiveDrawerSize.value)
+const messageListRef = ref(null)
+const isSending = ref(false)
+const currentAbortController = ref(null)
+const inputMessage = ref('')
+const selectedFiles = ref([])
+const uploadFileList = ref([])
+const selectedFileSnapshots = ref([])
+const messages = ref([])
+const uuid = ref('')
+const chartInstances = ref({})
+const resizeHandlers = ref([])
+const outputState = ref({})
+const businessTypeLabelMap = {
+ purchase_ledger: '閲囪喘鍙拌处',
+ payment_registration: '浠樻鐧昏',
+ purchase_return_order: '閲囪喘閫�璐у崟',
+ unknown: '鏈煡閲囪喘涓氬姟'
+}
+const salesStructuredTypeSet = new Set([
+ 'sales_customer_profile_list',
+ 'sales_quotation_list',
+ 'sales_ledger_list',
+ 'sales_return_list',
+ 'sales_customer_interaction_list',
+ 'sales_shipping_list',
+ 'sales_dashboard',
+ 'sales_customer_churn_risk',
+ 'sales_collection_quote_strategy'
+])
+const salesFocusTypeSet = new Set([
+ 'sales_customer_churn_risk',
+ 'sales_collection_quote_strategy'
+])
+const salesTypeLabelMap = {
+ sales_customer_profile_list: '瀹㈡埛妗f',
+ sales_quotation_list: '閿�鍞姤浠�',
+ sales_ledger_list: '閿�鍞彴璐�',
+ sales_return_list: '閿�鍞��娆�/鍥炴璁板綍',
+ sales_customer_interaction_list: '瀹㈡埛寰�鏉�',
+ sales_shipping_list: '鍙戣揣鍙拌处',
+ sales_dashboard: '閿�鍞寚鏍囩粺璁�',
+ sales_customer_churn_risk: '瀹㈡埛娴佸け椋庨櫓鍒嗘瀽',
+ sales_collection_quote_strategy: '鍥炴涓庢姤浠风瓥鐣ュ缓璁�'
+}
+const purchaseTypeLabelMap = {
+ purchase_material_rank: '閲囪喘鐗╂枡閲戦鎺掕',
+ purchase_stats: '閲囪喘缁熻',
+ purchase_pending_payment_list: '寰呬粯娆鹃噰璐崟'
+}
+const financialStructuredTypeSet = new Set([
+ 'financial_cost_accounting',
+ 'financial_order_profit_analysis',
+ 'financial_inventory_capital_analysis',
+ 'financial_cashflow_forecast',
+ 'financial_business_anomaly_warning',
+ 'financial_business_cockpit',
+ 'financial_operation_report',
+ 'financial_rag_knowledge'
+])
+const financialTypeLabelMap = {
+ financial_cost_accounting: '鎴愭湰鏍哥畻',
+ financial_order_profit_analysis: '璁㈠崟鍒╂鼎鍒嗘瀽',
+ financial_inventory_capital_analysis: '搴撳瓨璧勯噾鍒嗘瀽',
+ financial_cashflow_forecast: '鐜伴噾娴侀娴�',
+ financial_business_anomaly_warning: '缁忚惀寮傚父棰勮',
+ financial_business_cockpit: '缁忚惀椹鹃┒鑸�',
+ financial_operation_report: '缁忚惀鎶ュ憡',
+ financial_rag_knowledge: '璐㈠姟鐭ヨ瘑妫�绱�'
+}
+const manufacturingStructuredTypeSet = new Set([
+ 'manufacturing_site_snapshot',
+ 'manufacturing_plan_list',
+ 'manufacturing_workorder_list',
+ 'manufacturing_device_list',
+ 'manufacturing_device_repair_list',
+ 'manufacturing_quality_list',
+ 'manufacturing_material_list',
+ 'manufacturing_exception_list',
+ 'manufacturing_warning',
+ 'manufacturing_analysis',
+ 'manufacturing_action_plan'
+])
+const manufacturingListTypeSet = new Set([
+ 'manufacturing_plan_list',
+ 'manufacturing_workorder_list',
+ 'manufacturing_device_list',
+ 'manufacturing_device_repair_list',
+ 'manufacturing_quality_list',
+ 'manufacturing_material_list',
+ 'manufacturing_exception_list'
+])
+const manufacturingTypeLabelMap = {
+ manufacturing_site_snapshot: '鐢熶骇鐜板満姒傝',
+ manufacturing_plan_list: '璁″垝鏌ヨ',
+ manufacturing_workorder_list: '宸ュ崟鏌ヨ',
+ manufacturing_device_list: '璁惧鏌ヨ',
+ manufacturing_device_repair_list: '璁惧缁翠慨璁板綍鏌ヨ',
+ manufacturing_quality_list: '璐ㄩ噺鏌ヨ',
+ manufacturing_material_list: '鐗╂枡鏌ヨ',
+ manufacturing_exception_list: '寮傚父鏌ヨ',
+ manufacturing_warning: '棰勮鐪嬫澘',
+ manufacturing_analysis: '缁忚惀鍒嗘瀽',
+ manufacturing_action_plan: '鍔炵悊寤鸿'
+}
+const structuredFieldLabelMap = {
+ workOrderNo: '宸ュ崟鍙�',
+ planEndTime: '璁″垝缁撴潫鏃堕棿',
+ planStartTime: '璁″垝寮�濮嬫椂闂�',
+ timeRange: '鏃堕棿鑼冨洿',
+ startDate: '寮�濮嬫棩鏈�',
+ endDate: '缁撴潫鏃ユ湡',
+ warningCount: '棰勮鏁伴噺',
+ overduePlanCount: '閫炬湡璁″垝鏁�',
+ overdueWorkOrderCount: '閫炬湡宸ュ崟鏁�',
+ actionCount: '寤鸿鍔ㄤ綔鏁�',
+ qualityOpenCount: '璐ㄩ噺寰呭鐞嗘暟',
+ lowStockCount: '浣庡簱瀛樻暟',
+ exceptionCount: '寮傚父鏁�',
+ userId: '鐢ㄦ埛ID',
+ tenantId: '绉熸埛ID',
+ status: '鐘舵��',
+ deviceName: '璁惧鍚嶇О',
+ deviceModel: '璁惧鍨嬪彿',
+ pendingRepairCount: '寰呯淮淇暟',
+ repairTime: '缁翠慨鏃堕棿',
+ repairName: '鎶ヤ慨浜�',
+ maintenanceName: '缁翠慨浜哄憳',
+ level: '棰勮绛夌骇',
+ title: '鏍囬',
+ count: '鏁伴噺',
+ detail: '璇︽儏',
+ remark: '澶囨敞',
+ createTime: '鍒涘缓鏃堕棿',
+ updateTime: '鏇存柊鏃堕棿',
+ exceptionType: '寮傚父绫诲瀷',
+ materialName: '鐗╂枡鍚嶇О',
+ stockQty: '搴撳瓨閲�'
+}
+Object.assign(structuredFieldLabelMap, {
+ refundId: '閫�娆�/鍥炴鍗曞彿',
+ collectionNumber: '鏀舵鍗曞彿',
+ collectionAmount: '鏀舵閲戦',
+ actualAmount: '鏀舵閲戦(鍏煎)',
+ customerId: '瀹㈡埛ID',
+ salesLedgerId: '閿�鍞彴璐D',
+ projectName: '椤圭洰鍚嶇О',
+ receiptPaymentDate: '鏀朵粯娆炬棩鏈�',
+ receiptPaymentAmount: '鏀朵粯娆鹃噾棰�',
+ receiptPaymentType: '鏀朵粯娆剧被鍨�',
+ registrant: '鐧昏浜�',
+ returnAmount: '鍥炴閲戦姹囨��',
+ totalReceiptAmount: '鏀舵鎬婚',
+ customerCount: '瀹㈡埛鏁�',
+ paidAmount: '宸蹭粯娆鹃噾棰�',
+ contractAmount: '鍚堝悓閲戦',
+ paymentCount: '浠樻绗旀暟',
+ invoiceCount: '鍙戠エ绗旀暟',
+ paymentAmount: '浠樻閲戦',
+ invoiceAmount: '鍙戠エ閲戦',
+ customerName: '瀹㈡埛鍚嶇О',
+ riskLevel: '椋庨櫓绛夌骇',
+ riskScore: '椋庨櫓璇勫垎',
+ riskReasons: '椋庨櫓鍘熷洜',
+ pendingAmount: '寰呭洖娆鹃噾棰�',
+ pendingRate: '寰呭洖娆惧崰姣�',
+ daysSinceLastOrder: '璺濅笂娆′笅鍗曞ぉ鏁�',
+ priority: '浼樺厛绾�',
+ quoteConversionRate: '鎶ヤ环杞寲鐜�',
+ collectionStrategy: '鍥炴绛栫暐',
+ quotationStrategy: '鎶ヤ环绛栫暐',
+ nextAction: '涓嬩竴姝ュ姩浣�',
+ salesContractNo: '閿�鍞悎鍚屽彿',
+ revenue: '鏀跺叆',
+ materialCost: '鏉愭枡鎴愭湰',
+ laborCost: '浜哄伐鎴愭湰',
+ depreciationCost: '鎶樻棫鎴愭湰',
+ scrapCost: '鎶ュ簾鎴愭湰',
+ totalCost: '鎬绘垚鏈�',
+ profit: '鍒╂鼎',
+ profitRate: '鍒╂鼎鐜�',
+ reasons: '鍘熷洜',
+ suggestion: '寤鸿',
+ productName: '浜у搧鍚嶇О',
+ model: '鍨嬪彿',
+ quantity: '鏁伴噺',
+ inventoryValue: '搴撳瓨璧勯噾',
+ stagnantDays: '鍛嗘粸澶╂暟',
+ overstock: '鏄惁瓒呭偍',
+ month: '鏈堜唤',
+ income: '鏀跺叆',
+ expense: '鏀嚭',
+ netFlow: '鍑�鐜伴噾娴�',
+ message: '棰勮淇℃伅',
+ headline: '鎶ュ憡鏍囬',
+ conclusions: '鏍稿績缁撹',
+ riskSuggestions: '椋庨櫓寤鸿',
+ orderProfitTop: '鍒╂鼎Top',
+ contractAmountTotal: '鍚堝悓鎬婚',
+ receivedAmountTotal: '宸插洖娆鹃噾棰�',
+ pendingAmountTotal: '寰呭洖娆炬�婚',
+ shipRate: '鍙戣揣鐜�',
+ pendingOrderCount: '寰呬粯娆捐鍗曟暟',
+ totalContractAmount: '寰呬粯娆惧悎鍚屾�婚',
+ totalPaidAmount: '宸蹭粯娆炬�婚',
+ totalPendingAmount: '寰呬粯娆炬�婚'
+})
+const purchasePayloadFieldLabelMap = {
+ purchaseLedgers: '閲囪喘鍙拌处',
+ productData: '浜у搧鏄庣粏',
+ purchaseContractNumber: '閲囪喘鍚堝悓鍙�',
+ purchaseContractNo: '閲囪喘鍚堝悓鍙�',
+ purchaseOrderNumber: '閲囪喘鍚堝悓鍙�',
+ salesContractNo: '閿�鍞悎鍚屽彿',
+ salesContractNumber: '閿�鍞悎鍚屽彿',
+ salesOrderNumber: '閿�鍞悎鍚屽彿',
+ salesContractNoId: '閿�鍞悎鍚孖D',
+ approveUserIds: '瀹℃壒鐢ㄦ埛ID鍒楄〃',
+ entryDateStart: '褰曞叆寮�濮嬫棩鏈�',
+ entryDateEnd: '褰曞叆缁撴潫鏃ユ湡',
+ id: 'ID',
+ supplierId: '渚涘簲鍟咺D',
+ projectName: '椤圭洰鍚嶇О',
+ supplierName: '渚涘簲鍟嗗悕绉�',
+ isWhite: '鏄惁鐧藉悕鍗�',
+ recorderId: '褰曞叆浜篒D',
+ recorderName: '褰曞叆浜�',
+ contractDate: '鎵ц鏃ユ湡',
+ executionDate: '鎵ц鏃ユ湡',
+ inputPerson: '褰曞叆浜�',
+ inputDate: '褰曞叆鏃ユ湡',
+ entryDate: '褰曞叆鏃ユ湡',
+ paymentMethod: '浠樻鏂瑰紡',
+ auditors: '瀹℃壒浜�',
+ approverId: '瀹℃壒浜篒D',
+ approvalStatus: '瀹℃壒鐘舵��',
+ remark: '澶囨敞',
+ remarks: '澶囨敞',
+ attachmentMaterials: '闄勪欢鏉愭枡',
+ createdAt: '鍒涘缓鏃堕棿',
+ updatedAt: '鏇存柊鏃堕棿',
+ salesLedgerId: '閿�鍞彴璐D',
+ hasChildren: '鏄惁鏈夊瓙椤�',
+ Type: '绫诲瀷',
+ type: '绫诲瀷',
+ tempFileIds: '涓存椂鏂囦欢ID',
+ SalesLedgerFiles: '閿�鍞彴璐﹂檮浠�',
+ phoneNumber: '鑱旂郴鐢佃瘽',
+ businessPersonId: '涓氬姟鍛業D',
+ productId: '浜у搧ID',
+ productModelId: '浜у搧鍨嬪彿ID',
+ invoiceNumber: '鍙戠エ鍙风爜',
+ invoiceAmount: '鍙戠エ閲戦',
+ ticketRegistrationId: '寮�绁ㄧ櫥璁癐D',
+ contractAmount: '鍚堝悓閲戦',
+ receiptPaymentAmount: '宸叉敹浠樻閲戦',
+ unReceiptPaymentAmount: '鏈敹浠樻閲戦',
+ templateName: '妯℃澘鍚嶇О',
+ productCategory: '浜у搧绫诲埆',
+ specificationModel: '瑙勬牸鍨嬪彿',
+ unit: '鍗曚綅',
+ taxRate: '绋庣巼',
+ taxInclusiveUnitPrice: '鍚◣鍗曚环',
+ priceWithTax: '鍚◣鍗曚环',
+ quantity: '鏁伴噺',
+ taxInclusiveTotalPrice: '鍚◣鎬讳环',
+ totalPriceWithTax: '鍚◣鎬讳环',
+ invoiceType: '鍙戠エ绫诲瀷',
+ inventoryWarningQuantity: '搴撳瓨棰勮鏁伴噺',
+ isInspected: '鏄惁璐ㄦ',
+ isChecked: '鏄惁璐ㄦ'
+}
+const purchasePayloadFieldKeyMap = {
+ 閲囪喘鍙拌处: 'purchaseLedgers',
+ 浜у搧鏄庣粏: 'productData',
+ 閲囪喘鍚堝悓鍙�: 'purchaseContractNumber',
+ 閲囪喘鍗曞彿: 'purchaseContractNumber',
+ 閲囪喘璁㈠崟鍙�: 'purchaseContractNumber',
+ 閿�鍞悎鍚屽彿: 'salesContractNo',
+ 閿�鍞崟鍙�: 'salesContractNo',
+ 閿�鍞鍗曞彿: 'salesContractNo',
+ 閿�鍞悎鍚孖D: 'salesContractNoId',
+ 瀹℃壒鐢ㄦ埛ID鍒楄〃: 'approveUserIds',
+ 褰曞叆寮�濮嬫棩鏈�: 'entryDateStart',
+ 褰曞叆缁撴潫鏃ユ湡: 'entryDateEnd',
+ ID: 'id',
+ 椤圭洰鍚嶇О: 'projectName',
+ 渚涘簲鍟咺D: 'supplierId',
+ 渚涘簲鍟嗗悕绉�: 'supplierName',
+ 鏄惁鐧藉悕鍗�: 'isWhite',
+ 褰曞叆浜篒D: 'recorderId',
+ 褰曞叆浜�: 'recorderName',
+ 绛捐鏃ユ湡: 'executionDate',
+ 鎵ц鏃ユ湡: 'executionDate',
+ 褰曞叆鏃ユ湡: 'entryDate',
+ 浠樻鏂瑰紡: 'paymentMethod',
+ 瀹℃牳浜�: 'approverId',
+ 瀹℃壒浜�: 'approverId',
+ 瀹℃壒浜篒D: 'approverId',
+ 瀹℃壒鐘舵��: 'approvalStatus',
+ 澶囨敞: 'remarks',
+ 闄勪欢鏉愭枡: 'attachmentMaterials',
+ 鍒涘缓鏃堕棿: 'createdAt',
+ 鏇存柊鏃堕棿: 'updatedAt',
+ 閿�鍞彴璐D: 'salesLedgerId',
+ 鏄惁鏈夊瓙椤�: 'hasChildren',
+ 绫诲瀷: 'type',
+ 涓存椂鏂囦欢ID: 'tempFileIds',
+ 閿�鍞彴璐﹂檮浠�: 'SalesLedgerFiles',
+ 鑱旂郴鐢佃瘽: 'phoneNumber',
+ 涓氬姟鍛業D: 'businessPersonId',
+ 浜у搧ID: 'productId',
+ 浜у搧鍨嬪彿ID: 'productModelId',
+ 鍙戠エ鍙风爜: 'invoiceNumber',
+ 鍙戠エ閲戦: 'invoiceAmount',
+ 寮�绁ㄧ櫥璁癐D: 'ticketRegistrationId',
+ 鍚堝悓閲戦: 'contractAmount',
+ 宸叉敹浠樻閲戦: 'receiptPaymentAmount',
+ 鏈敹浠樻閲戦: 'unReceiptPaymentAmount',
+ 妯℃澘鍚嶇О: 'templateName',
+ 浜у搧绫诲埆: 'productCategory',
+ 浜у搧鍚嶇О: 'productCategory',
+ 瑙勬牸鍨嬪彿: 'specificationModel',
+ 鍗曚綅: 'unit',
+ 绋庣巼: 'taxRate',
+ 鍚◣鍗曚环: 'taxInclusiveUnitPrice',
+ 鏁伴噺: 'quantity',
+ 鍚◣鎬讳环: 'taxInclusiveTotalPrice',
+ 鍙戠エ绫诲瀷: 'invoiceType',
+ 搴撳瓨棰勮鏁伴噺: 'inventoryWarningQuantity',
+ 鏄惁璐ㄦ: 'isInspected',
+ purchaseLedgers: 'purchaseLedgers',
+ productData: 'productData',
+ purchaseContractNumber: 'purchaseContractNumber',
+ purchaseContractNo: 'purchaseContractNumber',
+ purchaseOrderNumber: 'purchaseContractNumber',
+ salesContractNo: 'salesContractNo',
+ salesContractNumber: 'salesContractNo',
+ salesOrderNumber: 'salesContractNo',
+ contractDate: 'executionDate',
+ inputPerson: 'recorderName',
+ inputDate: 'entryDate',
+ auditors: 'approverId',
+ remark: 'remarks',
+ productCategory: 'productCategory',
+ productName: 'productCategory',
+ specificationModel: 'specificationModel',
+ unit: 'unit',
+ taxRate: 'taxRate',
+ priceWithTax: 'taxInclusiveUnitPrice',
+ taxInclusiveUnitPrice: 'taxInclusiveUnitPrice',
+ quantity: 'quantity',
+ totalPriceWithTax: 'taxInclusiveTotalPrice',
+ taxInclusiveTotalPrice: 'taxInclusiveTotalPrice',
+ invoiceType: 'invoiceType',
+ inventoryWarningQuantity: 'inventoryWarningQuantity',
+ isInspected: 'isInspected',
+ isChecked: 'isInspected'
+}
+const isPlainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
+
+const stringifyStructuredPayload = (value, spaces = 2) => {
+ if (typeof value === 'string') return value
+ try {
+ return JSON.stringify(value ?? {}, null, spaces)
+ } catch (err) {
+ return '{}'
+ }
+}
+
+const structuredFieldTokenLabelMap = {
+ time: '鏃堕棿',
+ range: '鑼冨洿',
+ start: '寮�濮�',
+ end: '缁撴潫',
+ date: '鏃ユ湡',
+ warning: '棰勮',
+ overdue: '閫炬湡',
+ plan: '璁″垝',
+ work: '宸�',
+ order: '鍗�',
+ workorder: '宸ュ崟',
+ count: '鏁伴噺',
+ quality: '璐ㄩ噺',
+ low: '浣�',
+ stock: '搴撳瓨',
+ exception: '寮傚父',
+ action: '鍔ㄤ綔',
+ user: '鐢ㄦ埛',
+ tenant: '绉熸埛',
+ id: 'ID',
+ no: '缂栧彿',
+ number: '缂栧彿',
+ code: '缂栫爜',
+ name: '鍚嶇О',
+ status: '鐘舵��',
+ level: '绛夌骇',
+ title: '鏍囬',
+ detail: '璇︽儏',
+ total: '鎬绘暟',
+ rate: '姣旂巼',
+ type: '绫诲瀷',
+ pending: '寰�',
+ repair: '缁翠慨',
+ device: '璁惧',
+ material: '鐗╂枡'
+}
+
+const convertStructuredFieldKeyToChinese = (fieldKey = '') => {
+ const key = String(fieldKey || '').trim()
+ if (!key) return '-'
+ if (structuredFieldLabelMap[key]) return structuredFieldLabelMap[key]
+ if (/[\u4e00-\u9fa5]/.test(key)) return key
+
+ const rawTokens = key
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
+ .replace(/[_-]+/g, ' ')
+ .trim()
+ .split(/\s+/)
+ .filter(Boolean)
+ .map(token => token.toLowerCase())
+
+ if (!rawTokens.length) return '瀛楁'
+
+ const mappedTokens = rawTokens
+ .map(token => structuredFieldTokenLabelMap[token] || '')
+ .filter(Boolean)
+
+ if (mappedTokens.length) {
+ return mappedTokens.join('')
+ }
+
+ return '瀛楁'
+}
+
+const getStructuredFieldLabel = (fieldKey = '') => {
+ return convertStructuredFieldKeyToChinese(fieldKey)
+}
+
+const getStructuredPathLabel = (fieldPath = '') => {
+ const path = String(fieldPath || '').trim()
+ if (!path) return '-'
+
+ const segments = path
+ .replace(/\[(\d+)]/g, '.$1')
+ .split('.')
+ .filter(Boolean)
+
+ if (!segments.length) return getStructuredFieldLabel(path)
+
+ return segments.map((segment) => {
+ if (/^\d+$/.test(segment)) {
+ return `绗�${Number(segment) + 1}椤筦
+ }
+ return getStructuredFieldLabel(segment)
+ }).join(' / ')
+}
+
+const formatStructuredValue = (value) => {
+ if (value === null || value === undefined || value === '') return '-'
+ if (Array.isArray(value)) {
+ const preview = value.slice(0, 3).map(item => formatStructuredValue(item)).join('銆�')
+ return value.length > 3 ? `${preview} 绛�${value.length}椤筦 : preview
+ }
+ if (isPlainObject(value)) return stringifyStructuredPayload(value, 0)
+ return String(value)
+}
+
+const normalizeManufacturingSummaryEntries = (summary) => {
+ if (!isPlainObject(summary)) return []
+ return Object.entries(summary)
+ .filter(([, value]) => value !== undefined && value !== null && `${value}`.trim() !== '')
+ .map(([key, value]) => ({
+ key,
+ label: getStructuredFieldLabel(key),
+ value: formatStructuredValue(value)
+ }))
+}
+
+const normalizeManufacturingCoreMetrics = (coreMetrics) => {
+ if (Array.isArray(coreMetrics)) {
+ return coreMetrics.map((item, index) => {
+ if (isPlainObject(item)) {
+ const label = item.label || item.name || item.key || `鎸囨爣${index + 1}`
+ const metricValue = item.value ?? item.metricValue ?? item.data ?? '-'
+ const unit = item.unit ? ` ${item.unit}` : ''
+ return {
+ key: String(item.key || item.name || index),
+ label,
+ value: `${formatStructuredValue(metricValue)}${unit}`.trim()
+ }
+ }
+ return {
+ key: String(index),
+ label: `鎸囨爣${index + 1}`,
+ value: formatStructuredValue(item)
+ }
+ })
+ }
+
+ if (isPlainObject(coreMetrics)) {
+ return Object.entries(coreMetrics).map(([key, value]) => ({
+ key,
+ label: getStructuredFieldLabel(key),
+ value: formatStructuredValue(value)
+ }))
+ }
+
+ return []
+}
+
+const normalizeManufacturingWarningItems = (items = []) => {
+ if (!Array.isArray(items)) return []
+ return items
+ .filter(item => isPlainObject(item))
+ .map(item => ({
+ level: String(item.level || '').toLowerCase(),
+ title: item.title || '',
+ count: item.count ?? '',
+ detail: item.detail ?? ''
+ }))
+}
+
+const inferManufacturingColumns = (items = []) => {
+ if (!Array.isArray(items) || !items.length) return []
+ const fieldSet = new Set()
+ items.forEach((item) => {
+ if (!isPlainObject(item)) return
+ Object.keys(item).forEach((key) => fieldSet.add(key))
+ })
+ return Array.from(fieldSet)
+}
+
+const getManufacturingActionCardRuntimeKey = (card = {}, index = 0) => {
+ const code = String(card?.code || '').trim()
+ const api = String(card?.targetApi || '').trim()
+ const name = String(card?.name || '').trim()
+ return `${code}::${api}::${name}::${index}`
+}
+
+const normalizeManufacturingActionCards = (actionCards = [], previousCards = []) => {
+ const previousMap = new Map()
+ if (Array.isArray(previousCards)) {
+ previousCards.forEach((card, index) => {
+ previousMap.set(getManufacturingActionCardRuntimeKey(card, index), card)
+ })
+ }
+
+ return (Array.isArray(actionCards) ? actionCards : [])
+ .filter(card => isPlainObject(card))
+ .map((card, index) => {
+ const runtimeKey = getManufacturingActionCardRuntimeKey(card, index)
+ const previousCard = previousMap.get(runtimeKey)
+ const fallbackPayloadText = stringifyStructuredPayload(card.examplePayload, 2)
+ return {
+ ...card,
+ runtimeKey,
+ payloadText: previousCard?.payloadText ?? fallbackPayloadText,
+ executing: Boolean(previousCard?.executing),
+ executed: Boolean(previousCard?.executed),
+ executeResult: previousCard?.executeResult || '',
+ executeError: Boolean(previousCard?.executeError)
+ }
+ })
+}
+
+const buildManufacturingStructuredData = (parsedData, previousData = null) => {
+ const type = String(parsedData?.type || '')
+ if (!manufacturingStructuredTypeSet.has(type)) return null
+
+ const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
+ const items = Array.isArray(rawData.items) ? rawData.items.filter(item => isPlainObject(item)) : []
+ const warningItems = type === 'manufacturing_warning' ? normalizeManufacturingWarningItems(items) : []
+ const listItems = manufacturingListTypeSet.has(type) ? items : []
+ const actionCards = type === 'manufacturing_action_plan'
+ ? normalizeManufacturingActionCards(rawData.actionCards, previousData?.actionCards)
+ : []
+
+ return {
+ type,
+ summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
+ coreMetrics: normalizeManufacturingCoreMetrics(rawData.coreMetrics),
+ listItems,
+ columns: inferManufacturingColumns(listItems),
+ warningItems,
+ actionCards
+ }
+}
+
+const getManufacturingTypeLabel = (type = '') => manufacturingTypeLabelMap[String(type || '')] || '鍒堕�犵粨鏋�'
+
+const inferSalesColumns = (items = []) => {
+ if (!Array.isArray(items) || !items.length) return []
+ const fieldSet = new Set()
+ items.forEach((item) => {
+ if (!isPlainObject(item)) return
+ Object.keys(item).forEach((key) => fieldSet.add(key))
+ })
+ return Array.from(fieldSet)
+}
+
+const normalizeSalesListItems = (items) => {
+ if (!Array.isArray(items)) return []
+ return items.filter(item => isPlainObject(item))
+}
+
+const normalizeSalesReturnListItems = (items = []) => {
+ return normalizeSalesListItems(items).map((item) => {
+ const collectionAmount = item.collectionAmount ?? item.actualAmount
+ const collectionNumber = item.collectionNumber || item.refundId || ''
+ return {
+ ...item,
+ refundId: item.refundId || collectionNumber,
+ collectionNumber,
+ collectionAmount: collectionAmount ?? 0,
+ actualAmount: item.actualAmount ?? collectionAmount ?? 0
+ }
+ })
+}
+
+const normalizeSalesTypeListItems = (type = '', items = []) => {
+ if (type === 'sales_return_list') {
+ return normalizeSalesReturnListItems(items)
+ }
+ return normalizeSalesListItems(items)
+}
+
+const inferSalesColumnsByType = (type = '', listItems = []) => {
+ const columns = inferSalesColumns(listItems)
+ if (type === 'sales_return_list') {
+ return columns.filter(column => column !== 'actualAmount')
+ }
+ return columns
+}
+
+const buildSalesStructuredData = (parsedData) => {
+ const type = String(parsedData?.type || '')
+ if (!salesStructuredTypeSet.has(type)) return null
+
+ const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
+ const listItems = normalizeSalesTypeListItems(type, rawData.items)
+ const topCustomers = normalizeSalesListItems(rawData.topCustomers)
+ const contractTrend = normalizeSalesListItems(rawData.contractTrend)
+
+ return {
+ type,
+ summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
+ listItems,
+ columns: inferSalesColumnsByType(type, listItems),
+ topCustomers,
+ topCustomerColumns: inferSalesColumns(topCustomers),
+ contractTrend,
+ contractTrendColumns: inferSalesColumns(contractTrend)
+ }
+}
+
+const getSalesTypeLabel = (type = '') => salesTypeLabelMap[String(type || '')] || '閿�鍞煡璇㈢粨鏋�'
+
+const buildPurchaseStructuredData = (parsedData) => {
+ if (parsedData?.success !== true) return null
+
+ const type = String(parsedData?.type || '')
+ if (!type.startsWith('purchase_')) return null
+
+ const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
+ const listItems = normalizeSalesListItems(rawData.items)
+
+ return {
+ type,
+ summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
+ listItems,
+ columns: inferSalesColumns(listItems)
+ }
+}
+
+const getPurchaseTypeLabel = (type = '') => purchaseTypeLabelMap[String(type || '')] || '閲囪喘鏌ヨ缁撴灉'
+
+const financialDataSectionConfig = [
+ { key: 'orders', title: '璁㈠崟鏄庣粏' },
+ { key: 'items', title: '鏄庣粏鍒楄〃' },
+ { key: 'actualMonthly', title: '鍘嗗彶鐜伴噾娴�' },
+ { key: 'forecastMonthly', title: '棰勬祴鐜伴噾娴�' },
+ { key: 'receivableRiskTop', title: '搴旀敹椋庨櫓Top' },
+ { key: 'payablePressureTop', title: '搴斾粯鍘嬪姏Top' },
+ { key: 'orderProfitTop', title: '鍒╂鼎Top' }
+]
+
+const buildFinancialStructuredSections = (rawData = {}) => {
+ const sections = []
+ financialDataSectionConfig.forEach((section) => {
+ const items = normalizeSalesListItems(rawData?.[section.key])
+ if (!items.length) return
+ sections.push({
+ key: section.key,
+ title: section.title,
+ items,
+ columns: inferSalesColumns(items)
+ })
+ })
+ return sections
+}
+
+const buildFinancialStructuredData = (parsedData) => {
+ const type = String(parsedData?.type || '')
+ if (!financialStructuredTypeSet.has(type) || parsedData?.success !== true) return null
+
+ const rawData = isPlainObject(parsedData?.data) ? parsedData.data : {}
+ const headline = String(rawData?.headline || '').trim()
+
+ return {
+ type,
+ summaryEntries: normalizeManufacturingSummaryEntries(parsedData?.summary),
+ headline,
+ conclusions: toStructuredStringArray(rawData?.conclusions),
+ riskSuggestions: toStructuredStringArray(rawData?.riskSuggestions),
+ sections: buildFinancialStructuredSections(rawData)
+ }
+}
+
+const getFinancialTypeLabel = (type = '') => financialTypeLabelMap[String(type || '')] || '璐㈠姟鏌ヨ缁撴灉'
+
+const isSalesFocusType = (type = '') => salesFocusTypeSet.has(String(type || ''))
+
+const getSalesLevelTagType = (level = '') => {
+ const normalizedLevel = String(level || '').toLowerCase()
+ if (normalizedLevel === 'high') return 'danger'
+ if (normalizedLevel === 'medium') return 'warning'
+ if (normalizedLevel === 'low') return 'success'
+ return 'info'
+}
+
+const getSalesLevelLabel = (level = '', mode = 'risk') => {
+ const normalizedLevel = String(level || '').toLowerCase()
+ const suffix = mode === 'priority' ? '浼樺厛绾�' : '椋庨櫓'
+ if (normalizedLevel === 'high') return `楂�${suffix}`
+ if (normalizedLevel === 'medium') return `涓�${suffix}`
+ if (normalizedLevel === 'low') return `浣�${suffix}`
+ if (!normalizedLevel) return mode === 'priority' ? '鏈垎绾�' : '鏈瘎浼�'
+ return normalizedLevel.toUpperCase()
+}
+
+const toStructuredStringArray = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => String(item || '').trim()).filter(Boolean)
+ }
+ if (typeof value === 'string') {
+ return value
+ .split(/[,\uFF0C\u3001;\uFF1B\n]/)
+ .map(item => item.trim())
+ .filter(Boolean)
+ }
+ return []
+}
+
+const normalizePurchaseIntentNotRecognizedData = (parsedData) => {
+ if (String(parsedData?.type || '') !== 'purchase_intent_not_recognized') return null
+ const quickPrompts = toStructuredStringArray(parsedData?.data?.quickPrompts)
+ if (!quickPrompts.length) return null
+ return { quickPrompts }
+}
+
+const getManufacturingWarningLevelType = (level = '') => {
+ const normalizedLevel = String(level || '').toLowerCase()
+ if (normalizedLevel === 'high') return 'danger'
+ if (normalizedLevel === 'medium') return 'warning'
+ return 'info'
+}
+
+const getManufacturingWarningLevelLabel = (level = '') => {
+ const normalizedLevel = String(level || '').toLowerCase()
+ if (normalizedLevel === 'high') return '楂�'
+ if (normalizedLevel === 'medium') return '涓�'
+ return normalizedLevel ? normalizedLevel.toUpperCase() : '涓�鑸�'
+}
+
+const normalizeRequestMethod = (method = 'POST') => {
+ const normalized = String(method || 'POST').trim().toUpperCase()
+ if (['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].includes(normalized)) return normalized
+ return 'POST'
+}
+
+const getNormalizedRequestMethod = (method) => normalizeRequestMethod(method)
+
+const getPayloadValueByPath = (payload, fieldPath = '') => {
+ const normalizedPath = String(fieldPath || '').trim()
+ if (!normalizedPath || !isPlainObject(payload)) return undefined
+
+ const pathSegments = normalizedPath
+ .replace(/\[(\d+)]/g, '.$1')
+ .split('.')
+ .filter(Boolean)
+
+ return pathSegments.reduce((current, segment) => {
+ if (current === null || current === undefined) return undefined
+ if (!['object', 'function'].includes(typeof current)) return undefined
+ return current[segment]
+ }, payload)
+}
+
+const getMissingRequiredFields = (requiredFields = [], payload = {}) => {
+ if (!Array.isArray(requiredFields) || !requiredFields.length) return []
+ return requiredFields.filter((fieldPath) => {
+ const value = getPayloadValueByPath(payload, fieldPath)
+ return !hasMeaningfulPayloadValue(value)
+ })
+}
+
+const parseManufacturingActionPayload = (payloadText = '') => {
+ const text = String(payloadText ?? '').trim()
+ if (!text) return {}
+ return JSON.parse(text)
+}
+
+const executeManufacturingAction = async (message, actionCard, cardIndex = 0) => {
+ if (!message?.manufacturingData || !actionCard || actionCard.executing) return
+
+ const actionName = actionCard.name || `鍔ㄤ綔 ${cardIndex + 1}`
+ const targetApi = String(actionCard.targetApi || '').trim()
+ if (!targetApi) {
+ actionCard.executeError = true
+ actionCard.executeResult = '缂哄皯 targetApi锛屾棤娉曟墽琛屽姩浣�'
+ return
+ }
+
+ let payload = {}
+ try {
+ payload = parseManufacturingActionPayload(actionCard.payloadText)
+ } catch (err) {
+ actionCard.executeError = true
+ actionCard.executeResult = '璇锋眰鍙傛暟涓嶆槸鍚堟硶 JSON锛岃妫�鏌ュ悗閲嶈瘯'
+ return
+ }
+
+ const requiredFields = Array.isArray(actionCard.requiredFields) ? actionCard.requiredFields : []
+ if (requiredFields.length && !isPlainObject(payload)) {
+ actionCard.executeError = true
+ actionCard.executeResult = '蹇呭~瀛楁鏍¢獙澶辫触锛氳姹傚弬鏁板繀椤绘槸 JSON 瀵硅薄'
+ return
+ }
+
+ const missingFields = getMissingRequiredFields(requiredFields, payload)
+ if (missingFields.length) {
+ actionCard.executeError = true
+ const missingFieldLabels = missingFields.map(field => getStructuredPathLabel(field))
+ actionCard.executeResult = `缂哄皯蹇呭~瀛楁锛�${missingFieldLabels.join('銆�')}`
+ return
+ }
+
+ try {
+ await ElMessageBox.confirm(`纭鎵ц銆�${actionName}銆嶅悧锛焋, '鎵ц纭', {
+ confirmButtonText: '纭鎵ц',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ } catch (err) {
+ return
+ }
+
+ actionCard.executing = true
+ actionCard.executeError = false
+ actionCard.executeResult = ''
+
+ const method = normalizeRequestMethod(actionCard.method)
+ const requestConfig = {
+ url: targetApi,
+ method: method.toLowerCase()
+ }
+
+ if (method === 'GET') {
+ requestConfig.params = payload
+ } else {
+ requestConfig.data = payload
+ }
+
+ try {
+ const res = await request(requestConfig)
+ const successMsg = res?.msg || `${actionName}鎵ц鎴愬姛`
+ actionCard.executed = true
+ actionCard.executeError = false
+ actionCard.executeResult = successMsg
+ ElMessage.success(successMsg)
+ } catch (err) {
+ actionCard.executed = false
+ actionCard.executeError = true
+ actionCard.executeResult = err?.message || `${actionName}鎵ц澶辫触锛岃绋嶅悗閲嶈瘯`
+ } finally {
+ actionCard.executing = false
+ }
+}
+
+// 鍘嗗彶浼氳瘽鐩稿叧
+const purchaseValueTypeOptions = [
+ { label: '鏂囨湰', value: 'string' },
+ { label: '鏁板瓧', value: 'number' },
+ { label: '甯冨皵', value: 'boolean' },
+ { label: '绌哄��', value: 'null' },
+ { label: '瀵硅薄', value: 'object' },
+ { label: '鏁扮粍', value: 'array' }
+]
+const purchaseContainerValueTypes = new Set(['object', 'array'])
+const purchaseHiddenFieldKeySet = new Set(['templatename', 'approvalstatus', 'phonenumber', 'type'])
+const purchaseHiddenKeyWordList = [
+ 'attachment',
+ 'file',
+ 'invoice',
+ 'ticketregistration',
+ 'receiptpayment',
+ 'payment'
+]
+const purchaseHiddenChineseKeywordList = ['闄勪欢', '寮�绁�', '鏉ョエ', '鍥炴', '浠樻']
+let purchasePayloadTreeNodeSeed = 0
+
+const shouldHidePurchaseField = (fieldKey = '') => {
+ const rawKey = String(fieldKey || '')
+ if (!rawKey) return false
+ const normalizedFieldKey = purchasePayloadFieldKeyMap[rawKey] || rawKey
+ const lowerKey = String(normalizedFieldKey).toLowerCase()
+
+ if (lowerKey.endsWith('id') || lowerKey.endsWith('ids')) return true
+ if (purchaseHiddenFieldKeySet.has(lowerKey)) return true
+ if (purchaseHiddenKeyWordList.some(keyword => lowerKey.includes(keyword))) return true
+ if (purchaseHiddenChineseKeywordList.some(keyword => rawKey.includes(keyword))) return true
+ return false
+}
+
+const showHistory = ref(false)
+const sessions = ref([])
+const loadingSessions = ref(false)
+
+const isImageFileType = (fileType = '') => String(fileType || '').toLowerCase().startsWith('image/')
+const imageFilePathPattern = /\.(png|jpe?g|gif|webp|bmp|svg)$/i
+
+const getPathnameFromFilePath = (filePath = '') => {
+ const rawPath = String(filePath || '').trim()
+ if (!rawPath) return ''
+ try {
+ const baseOrigin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'
+ return new URL(rawPath, baseOrigin).pathname || ''
+ } catch (err) {
+ return rawPath.split('?')[0]
+ }
+}
+
+const isImageFilePath = (filePath = '') => {
+ const pathname = getPathnameFromFilePath(filePath).toLowerCase()
+ return imageFilePathPattern.test(pathname)
+}
+
+const getHistoryFileName = (filePath = '', index = 0) => {
+ const pathname = getPathnameFromFilePath(filePath)
+ const fileName = pathname.split('/').filter(Boolean).pop()
+ if (!fileName) return `file-${index + 1}`
+ try {
+ return decodeURIComponent(fileName)
+ } catch (err) {
+ return fileName
+ }
+}
+
+const getImagePreviewList = (files = []) => {
+ if (!Array.isArray(files)) return []
+ return files
+ .filter(item => item?.isImage && item?.previewUrl)
+ .map(item => item.previewUrl)
+}
+
+const getImagePreviewInitialIndex = (files = [], previewUrl = '') => {
+ const list = getImagePreviewList(files)
+ const index = list.indexOf(previewUrl)
+ return index >= 0 ? index : 0
+}
+
+const formatFileSize = (size) => {
+ const bytes = Number(size)
+ if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
+ if (bytes < 1024) return `${bytes} B`
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1).replace(/\.0$/, '')} KB`
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(/\.0$/, '')} MB`
+}
+
+const createLocalFileSnapshot = (file, index = 0) => {
+ const rawFile = typeof File !== 'undefined' && file instanceof File ? file : null
+ const fileType = rawFile?.type || ''
+ const isImage = isImageFileType(fileType)
+ const canCreateObjectURL = typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function'
+ const previewUrl = isImage && rawFile && canCreateObjectURL ? URL.createObjectURL(rawFile) : ''
+ return {
+ previewId: `${rawFile?.name || 'file'}-${rawFile?.size || 0}-${rawFile?.lastModified || Date.now()}-${index}`,
+ name: rawFile?.name || `file-${index + 1}`,
+ size: rawFile?.size || 0,
+ type: fileType,
+ isImage,
+ previewUrl,
+ accessUrl: '',
+ rawFile,
+ isObjectUrl: !!previewUrl
+ }
+}
+
+const createHistoryFileSnapshot = (filePath, memoryId = '', messageIndex = 0, fileIndex = 0) => {
+ const normalizedPath = String(filePath || '').trim()
+ if (!normalizedPath) return null
+ const isImage = isImageFilePath(normalizedPath)
+ return {
+ previewId: `${memoryId || 'history'}-${messageIndex}-${fileIndex}`,
+ name: getHistoryFileName(normalizedPath, fileIndex),
+ size: 0,
+ type: '',
+ isImage,
+ previewUrl: isImage ? normalizedPath : '',
+ accessUrl: normalizedPath,
+ rawFile: null,
+ isObjectUrl: false
+ }
+}
+
+const revokeLocalFileSnapshots = (snapshots = []) => {
+ const canRevokeObjectURL = typeof URL !== 'undefined' && typeof URL.revokeObjectURL === 'function'
+ if (!canRevokeObjectURL) return
+ if (!Array.isArray(snapshots)) return
+ snapshots.forEach((snapshot) => {
+ if (snapshot?.isObjectUrl && snapshot?.previewUrl) {
+ URL.revokeObjectURL(snapshot.previewUrl)
+ }
+ })
+}
+
+const mapHistoryFilePathsToSnapshots = (filePaths = [], memoryId = '', messageIndex = 0) => {
+ if (!Array.isArray(filePaths)) return []
+ return filePaths
+ .map((filePath, fileIndex) => createHistoryFileSnapshot(filePath, memoryId, messageIndex, fileIndex))
+ .filter(Boolean)
+}
+
+const openMessageAttachment = (file) => {
+ const accessUrl = String(file?.accessUrl || '').trim()
+ if (!accessUrl) return
+ if (typeof window === 'undefined' || typeof window.open !== 'function') return
+ window.open(accessUrl, '_blank', 'noopener,noreferrer')
+}
+
+const handleMessageFileClick = (file) => {
+ if (!file?.accessUrl || file?.isImage) return
+ openMessageAttachment(file)
+}
+
+const revokeMessageLocalFileSnapshots = (messageList = []) => {
+ if (!Array.isArray(messageList)) return
+ messageList.forEach((msg) => {
+ if (Array.isArray(msg?.localUploadFiles)) {
+ revokeLocalFileSnapshots(msg.localUploadFiles)
+ msg.localUploadFiles = []
+ }
+ })
+}
+
+const clearSelectedFiles = ({ releaseSnapshots = true } = {}) => {
+ if (releaseSnapshots) {
+ revokeLocalFileSnapshots(selectedFileSnapshots.value)
+ }
+ selectedFiles.value = []
+ uploadFileList.value = []
+ selectedFileSnapshots.value = []
+}
+
+const abortCurrentRequest = () => {
+ if (!currentAbortController.value) return
+
+ currentAbortController.value.abort()
+ currentAbortController.value = null
+ isSending.value = false
+
+ const lastMsg = messages.value[messages.value.length - 1]
+ if (lastMsg && !lastMsg.isUser) {
+ lastMsg.isTyping = false
+ }
+}
+
+const toggleHistory = () => {
+ showHistory.value = !showHistory.value
+ if (showHistory.value) {
+ loadSessions()
+ }
+}
+
+const handleToggleHistory = () => {
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ toggleHistory()
+}
+
+const loadSessions = async () => {
+ loadingSessions.value = true
+ try {
+ const res = await request.get(`${currentAssistant.value.apiBase}/history/sessions`)
+ if (res.code === 200) {
+ sessions.value = res.data || []
+ }
+ } catch (err) {
+ console.error('Failed to load sessions', err)
+ } finally {
+ loadingSessions.value = false
+ }
+}
+
+const selectSession = async (session) => {
+ showHistory.value = false
+ clearSelectedFiles()
+ uuid.value = session.memoryId
+ localStorage.setItem(currentAssistant.value.storageKey, uuid.value)
+
+ // 鍔犺浇浼氳瘽娑堟伅
+ try {
+ const res = await request.get(`${currentAssistant.value.apiBase}/history/messages/${uuid.value}`)
+ if (res.code === 200) {
+ revokeMessageLocalFileSnapshots(messages.value)
+ disposeCharts()
+ messages.value = []
+ const historyMsgs = res.data || []
+
+ // 閲嶆柊鏋勯�犳秷鎭垪琛ㄥ苟瑙f瀽
+ historyMsgs.forEach((msg, idx) => {
+ const isUser = msg.role === 'user'
+ const botMsgIndex = messages.value.length
+
+ const messageObj = {
+ isUser,
+ content: msg.content || '',
+ htmlContent: '',
+ isTyping: false,
+ chartOptions: null,
+ chartRenderReady: false,
+ type: '',
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null,
+ purchaseAnalysisData: null,
+ manufacturingData: null,
+ salesData: null,
+ purchaseData: null,
+ purchaseIntentData: null,
+ financeData: null,
+ chartMarkdownParseFailed: false,
+ localUploadFiles: isUser ? mapHistoryFilePathsToSnapshots(msg.filePaths, uuid.value, idx) : []
+ }
+
+ messages.value.push(messageObj)
+
+ if (!isUser) {
+ outputState.value[botMsgIndex] = {
+ isPaused: false,
+ jsonBlockStartPos: -1,
+ jsBlockStartPos: -1,
+ blockEndPos: -1,
+ hasRenderedChart: false
+ }
+
+ // 瑙f瀽鍘嗗彶娑堟伅涓殑 JSON
+ const extracted = extractEmbeddedSuccessJson(msg.content || '')
+ if (extracted) {
+ applyStructuredMessageData(messageObj, extracted.data, botMsgIndex)
+ }
+
+ updateOutputState(msg.content || '', botMsgIndex)
+ messageObj.htmlContent = convertStreamOutput(msg.content || '', botMsgIndex)
+ } else {
+ messageObj.htmlContent = convertTextToHtml(msg.content || '')
+ }
+ })
+ scrollToBottom()
+ }
+ } catch (err) {
+ console.error('Failed to load messages', err)
+ }
+}
+
+const handleDeleteSession = async (memoryId) => {
+ try {
+ const res = await request.delete(`${currentAssistant.value.apiBase}/history/${memoryId}`)
+ if (res.code === 200) {
+ loadSessions()
+ if (uuid.value === memoryId) {
+ newChat()
+ }
+ }
+ } catch (err) {
+ console.error('Failed to delete session', err)
+ }
+}
+
+const columnLabelMap = {
+ approveId: '瀹℃壒缂栧彿',
+ approveType: '瀹℃壒绫诲瀷',
+ approveUserName: '瀹℃壒浜�',
+ approveUserCurrentName: '褰撳墠澶勭悊浜�',
+ approveReason: '瀹℃壒鍘熷洜',
+ approveStatus: '瀹℃壒鐘舵��',
+ createTime: '鍒涘缓鏃堕棿'
+}
+
+onMounted(() => {
+ if (props.autoOpen) {
+ visible.value = true
+ }
+ initUUID()
+ window.addEventListener('resize', handleWindowResize)
+})
+
+onUnmounted(() => {
+ revokeMessageLocalFileSnapshots(messages.value)
+ clearSelectedFiles()
+ disposeCharts()
+ window.removeEventListener('resize', handleWindowResize)
+})
+
+watch(selectedAssistantKey, (nextKey, prevKey) => {
+ if (!prevKey || nextKey === prevKey) return
+
+ abortCurrentRequest()
+ revokeMessageLocalFileSnapshots(messages.value)
+ disposeCharts()
+ messages.value = []
+ outputState.value = {}
+ sessions.value = []
+ showHistory.value = false
+ clearSelectedFiles()
+ inputMessage.value = ''
+ quickPromptStart.value = 0
+ initUUID()
+})
+
+watch(() => props.defaultAssistant, (nextKey) => {
+ if (!nextKey || nextKey === selectedAssistantKey.value) return
+ if (!assistants.value.some(item => item.key === nextKey)) return
+ selectedAssistantKey.value = nextKey
+})
+
+watch(() => props.autoOpen, (nextValue) => {
+ if (nextValue) {
+ visible.value = true
+ }
+})
+
+const handleWindowResize = () => {
+ windowWidth.value = window.innerWidth
+}
+
+const handleHeaderExtraAction = () => {
+ emit('header-extra-action')
+}
+
+const toggleSidebar = () => {
+ visible.value = !visible.value
+ if (visible.value) {
+ scrollToBottom()
+ }
+}
+
+const handleClose = () => {
+ if (hideTrigger.value) return
+ visible.value = false
+}
+
+const handleManualClose = () => {
+ if (hideTrigger.value) return
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ handleClose()
+}
+
+const initUUID = () => {
+ let storedUUID = localStorage.getItem(currentAssistant.value.storageKey)
+ if (!storedUUID) {
+ storedUUID = Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(4)
+ localStorage.setItem(currentAssistant.value.storageKey, storedUUID)
+ }
+ uuid.value = storedUUID
+}
+
+const newChat = () => {
+ revokeMessageLocalFileSnapshots(messages.value)
+ disposeCharts()
+ messages.value = []
+ outputState.value = {}
+ sessions.value = []
+ showHistory.value = false
+ clearSelectedFiles()
+ quickPromptStart.value = 0
+ localStorage.removeItem(currentAssistant.value.storageKey)
+ initUUID()
+}
+
+const handleNewChat = () => {
+ if (isSending.value) {
+ abortCurrentRequest()
+ }
+ newChat()
+}
+
+const sendQuickPrompt = (prompt) => {
+ if (!prompt || isSending.value) return
+ inputMessage.value = prompt
+ sendMessage()
+}
+
+const refreshQuickPrompts = () => {
+ const prompts = quickPrompts.value || []
+ if (prompts.length <= quickPromptLimit) return
+ quickPromptStart.value = (quickPromptStart.value + quickPromptLimit) % prompts.length
+}
+
+const disposeCharts = () => {
+ Object.values(chartInstances.value).forEach(chart => chart.dispose())
+ resizeHandlers.value.forEach(handler => window.removeEventListener('resize', handler))
+ chartInstances.value = {}
+ resizeHandlers.value = []
+}
+
+const extractEmbeddedSuccessJson = (text) => {
+ if (!text || typeof text !== 'string') return null
+
+ const startMatch = text.match(/\{\s*"success"\s*:/)
+ if (!startMatch) return null
+ const startIdx = startMatch.index ?? -1
+ if (startIdx < 0) return null
+
+ for (let i = startIdx; i < text.length; i++) {
+ if (text[i] !== '{') continue
+
+ let depth = 0
+ let inString = false
+ let escaped = false
+
+ for (let j = i; j < text.length; j++) {
+ const char = text[j]
+
+ if (inString) {
+ if (escaped) {
+ escaped = false
+ } else if (char === '\\') {
+ escaped = true
+ } else if (char === '"') {
+ inString = false
+ }
+ continue
+ }
+
+ if (char === '"') {
+ inString = true
+ continue
+ }
+
+ if (char === '{') {
+ depth++
+ } else if (char === '}') {
+ depth--
+ if (depth === 0) {
+ const candidate = text.slice(i, j + 1)
+ try {
+ const parsed = JSON.parse(candidate)
+ if (typeof parsed?.success === 'boolean') {
+ return {
+ data: parsed,
+ startIdx: i,
+ endIdx: j + 1
+ }
+ }
+ } catch (err) {
+ continue
+ }
+ }
+ }
+ }
+ }
+
+ return null
+}
+
+const applyStructuredMessageData = (messageObj, parsedData, msgIndex, shouldRenderCharts = true) => {
+ const isPurchaseIntentNotRecognized = String(parsedData?.type || '') === 'purchase_intent_not_recognized'
+ if (!messageObj || (parsedData?.success !== true && !isPurchaseIntentNotRecognized)) return
+
+ const previousManufacturingData = messageObj.manufacturingData
+ messageObj.type = parsedData.type || ''
+ messageObj.tableData = null
+ messageObj.purchaseAnalysisData = null
+ messageObj.manufacturingData = null
+ messageObj.salesData = null
+ messageObj.purchaseData = null
+ messageObj.purchaseIntentData = null
+ messageObj.financeData = null
+ messageObj.chartMarkdownParseFailed = false
+
+ if (isPurchaseIntentNotRecognized) {
+ messageObj.purchaseIntentData = normalizePurchaseIntentNotRecognizedData(parsedData)
+ messageObj.chartOptions = null
+ messageObj.chartRenderReady = false
+ return
+ }
+
+ if (messageObj.type === 'todo_list' && parsedData.data) {
+ messageObj.tableData = parsedData.data
+ }
+
+ const salesData = buildSalesStructuredData(parsedData)
+ if (salesData) {
+ messageObj.salesData = salesData
+ }
+
+ const manufacturingData = buildManufacturingStructuredData(parsedData, previousManufacturingData)
+ if (manufacturingData) {
+ messageObj.manufacturingData = manufacturingData
+ }
+
+ const purchaseData = buildPurchaseStructuredData(parsedData)
+ if (purchaseData) {
+ messageObj.purchaseData = purchaseData
+ }
+
+ const financeData = buildFinancialStructuredData(parsedData)
+ if (financeData) {
+ messageObj.financeData = financeData
+ }
+
+ if (parsedData.action === 'confirm_required' && parsedData.businessType) {
+ messageObj.type = 'purchase_analysis_confirm'
+ messageObj.purchaseAnalysisData = parsedData
+ messageObj.manufacturingData = null
+ messageObj.salesData = null
+ messageObj.purchaseData = null
+ messageObj.financeData = null
+ if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
+ initializePurchasePayloadTree(messageObj, parsedData.payload || {})
+ }
+ if (!messageObj.payloadText) {
+ const payloadFromTree = buildPurchasePayloadFromNodes(messageObj.payloadTreeData, 'object')
+ const payloadWithHidden = mergePurchasePayloadWithHidden(payloadFromTree, messageObj.payloadHiddenData)
+ messageObj.payloadText = JSON.stringify(localizePurchasePayload(payloadWithHidden), null, 2)
+ }
+ messageObj.confirmResult = ''
+ messageObj.confirmed = false
+ messageObj.confirming = false
+ }
+
+ const chartOptions = getStructuredChartOptions(parsedData)
+ if (chartOptions && Object.keys(chartOptions).length > 0) {
+ messageObj.chartOptions = chartOptions
+ messageObj.chartRenderReady = true
+
+ if (shouldRenderCharts) {
+ renderCharts(msgIndex, messageObj.chartOptions)
+ if (outputState.value[msgIndex]) {
+ outputState.value[msgIndex].hasRenderedChart = true
+ }
+ }
+ }
+}
+
+const getStructuredChartOptions = (parsedData) => {
+ if (!parsedData?.success) return null
+
+ if (parsedData.charts && Object.keys(parsedData.charts).length > 0) {
+ return parsedData.charts
+ }
+
+ if (parsedData.type === 'purchase_material_rank') {
+ return buildPurchaseMaterialRankCharts(parsedData)
+ }
+
+ return null
+}
+
+const getStructuredFallbackText = (parsedData) => {
+ if (!parsedData) return '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+ if (parsedData.type === 'todo_list') return '宸蹭负鎮ㄦ暣鐞嗗ソ鐩稿叧鏁版嵁銆�'
+ if (parsedData.type === 'purchase_intent_not_recognized') return '鏈瘑鍒埌鍙墽琛岀殑閲囪喘鏌ヨ鏉′欢锛岃琛ュ厖鏌ヨ鏉′欢鍚庡啀璇曘��'
+ if (salesStructuredTypeSet.has(parsedData.type)) {
+ if (parsedData.type === 'sales_customer_churn_risk') return '宸蹭负鎮ㄧ敓鎴愬鎴锋祦澶遍闄╁垎鏋愩��'
+ if (parsedData.type === 'sales_collection_quote_strategy') return '宸蹭负鎮ㄧ敓鎴愬洖娆句笌鎶ヤ环绛栫暐寤鸿銆�'
+ if (parsedData.type === 'sales_dashboard') return '宸蹭负鎮ㄧ敓鎴愰攢鍞寚鏍囩粺璁°��'
+ if (parsedData.type === 'sales_return_list') return '宸茶繑鍥為攢鍞��娆�/鍥炴璁板綍銆�'
+ if (parsedData.type === 'sales_customer_interaction_list') return '宸茶繑鍥炲鎴峰線鏉ユ暟鎹��'
+ return '宸茶繑鍥為攢鍞煡璇㈢粨鏋溿��'
+ }
+ if (manufacturingStructuredTypeSet.has(parsedData.type)) {
+ if (parsedData.type === 'manufacturing_action_plan') return '宸蹭负鎮ㄧ敓鎴愬姙鐞嗗缓璁紝璇风‘璁ゅ姩浣滃悗鎵ц銆�'
+ if (parsedData.type === 'manufacturing_warning') return '宸蹭负鎮ㄧ敓鎴愬埗閫犻璀︾湅鏉裤��'
+ if (parsedData.type === 'manufacturing_analysis') return '宸蹭负鎮ㄧ敓鎴愬埗閫犲垎鏋愮粨鏋溿��'
+ return '宸茶繑鍥炲埗閫犳煡璇㈢粨鏋溿��'
+ }
+ if (financialStructuredTypeSet.has(parsedData.type)) return '宸茶繑鍥炶储鍔″垎鏋愮粨鏋溿��'
+ if (String(parsedData.type || '').startsWith('purchase_')) return '宸茶繑鍥為噰璐煡璇㈢粨鏋溿��'
+ if (parsedData.charts && Object.keys(parsedData.charts).length > 0) return '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
+ return '姝e湪涓烘偍灞曠ず鍒嗘瀽缁撴灉...'
+}
+
+const resolveStructuredDescription = (parsedData) => {
+ if (!parsedData) return ''
+ if (parsedData.action === 'confirm_required') {
+ return getPurchaseConfirmDescription(parsedData)
+ }
+
+ const description = String(parsedData.description || '').trim()
+ if (!description) return ''
+
+ if (parsedData.type === 'sales_customer_interaction_list') {
+ if (description === 'no_customer_interactions') return '璇ユ椂闂磋寖鍥存殏鏃犲鎴峰線鏉ヨ褰曘��'
+ if (description === 'ok') return '宸茶繑鍥炲鎴峰線鏉ユ暟鎹��'
+ }
+
+ if (description === 'ok' || description === 'success') {
+ return getStructuredFallbackText(parsedData)
+ }
+
+ return description
+}
+
+const buildPurchaseMaterialRankCharts = (parsedData) => {
+ const items = Array.isArray(parsedData?.data?.items) ? parsedData.data.items : []
+ if (!items.length) return null
+
+ const names = items.map(item => item.productCategory || '-')
+ const amounts = items.map(item => Number(item.amount) || 0)
+
+ return {
+ purchaseMaterialAmountRank: {
+ title: {
+ text: '\u91c7\u8d2d\u7269\u6599\u91d1\u989d\u6392\u884c',
+ left: 'center',
+ textStyle: {
+ fontSize: 14,
+ fontWeight: 600,
+ color: '#1a1a2e'
+ }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ },
+ formatter(params) {
+ const dataIndex = params?.[0]?.dataIndex ?? 0
+ const item = items[dataIndex] || {}
+ const amount = Number(item.amount) || 0
+ const quantity = Number(item.quantity) || 0
+ return [
+ `${item.productCategory || '-'}`,
+ `${params?.[0]?.marker || ''} \u91d1\u989d\uff1a${formatCurrency(amount)}`,
+ `\u89c4\u683c\u578b\u53f7\uff1a${item.specificationModel || '-'}`,
+ `\u6570\u91cf\uff1a${quantity}${item.unit || ''}`
+ ].join('<br/>')
+ }
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: names.some(name => String(name).length > 6) ? 72 : 48,
+ top: 48,
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: names,
+ axisLabel: {
+ interval: 0,
+ rotate: names.some(name => String(name).length > 6) ? 28 : 0,
+ color: '#4b5563'
+ }
+ },
+ yAxis: {
+ type: 'value',
+ name: '\u91d1\u989d(\u5143)',
+ axisLabel: {
+ color: '#4b5563',
+ formatter: value => formatCompactNumber(value)
+ }
+ },
+ series: [{
+ name: '\u91c7\u8d2d\u91d1\u989d',
+ type: 'bar',
+ barMaxWidth: 36,
+ data: amounts,
+ itemStyle: {
+ borderRadius: [6, 6, 0, 0],
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#2f7cf6' },
+ { offset: 1, color: '#55d7ff' }
+ ])
+ },
+ label: {
+ show: true,
+ position: 'top',
+ color: '#1f2937',
+ formatter: params => formatCompactNumber(params.value)
+ }
+ }]
+ }
+ }
+}
+
+const formatCurrency = (value) => {
+ const amount = Number(value) || 0
+ return `\u00a5${amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`
+}
+
+const formatCompactNumber = (value) => {
+ const amount = Number(value) || 0
+ if (Math.abs(amount) >= 10000) {
+ return `${(amount / 10000).toFixed(2).replace(/\.?0+$/, '')}\u4e07`
+ }
+ return amount.toLocaleString('zh-CN', { maximumFractionDigits: 2 })
+}
+
+const formatPercent = (value) => {
+ const number = Number(value)
+ if (Number.isNaN(number)) return '-'
+ return `${Math.round(number * 100)}%`
+}
+
+const formatPreviewItem = (item) => {
+ if (item === null || item === undefined) return '-'
+ if (typeof item === 'string') return item
+ try {
+ return JSON.stringify(item)
+ } catch (err) {
+ return String(item)
+ }
+}
+
+const hasMeaningfulPayloadValue = (value) => {
+ if (value === null || value === undefined) return false
+ if (typeof value === 'string') return value.trim() !== ''
+ if (Array.isArray(value)) return value.some(item => hasMeaningfulPayloadValue(item))
+ if (typeof value === 'object') return Object.values(value).some(item => hasMeaningfulPayloadValue(item))
+ return true
+}
+
+const mergeMappedPayloadValue = (existingValue, incomingValue) => {
+ if (existingValue === undefined) return incomingValue
+
+ const existingHasValue = hasMeaningfulPayloadValue(existingValue)
+ const incomingHasValue = hasMeaningfulPayloadValue(incomingValue)
+
+ if (existingHasValue && !incomingHasValue) return existingValue
+ if (!existingHasValue && incomingHasValue) return incomingValue
+
+ if (
+ existingValue &&
+ incomingValue &&
+ typeof existingValue === 'object' &&
+ typeof incomingValue === 'object' &&
+ !Array.isArray(existingValue) &&
+ !Array.isArray(incomingValue)
+ ) {
+ return { ...existingValue, ...incomingValue }
+ }
+
+ return incomingValue
+}
+
+const mapPayloadKeys = (value, keyMap) => {
+ if (Array.isArray(value)) {
+ return value.map(item => mapPayloadKeys(item, keyMap))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ const mappedKey = keyMap[key] || key
+ const mappedValue = mapPayloadKeys(item, keyMap)
+ result[mappedKey] = mergeMappedPayloadValue(result[mappedKey], mappedValue)
+ return result
+ }, {})
+ }
+ return value
+}
+
+const clonePurchasePayloadValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => clonePurchasePayloadValue(item))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ result[key] = clonePurchasePayloadValue(item)
+ return result
+ }, {})
+ }
+ return value
+}
+
+const splitPurchasePayloadByVisibility = (value) => {
+ if (Array.isArray(value)) {
+ const splitItems = value.map(item => splitPurchasePayloadByVisibility(item))
+ const visible = splitItems.map(item => item.visible)
+ const hidden = splitItems.map(item => item.hidden)
+ return { visible, hidden }
+ }
+
+ if (value && typeof value === 'object') {
+ const visible = {}
+ const hidden = {}
+
+ Object.entries(value).forEach(([key, item]) => {
+ if (shouldHidePurchaseField(key)) {
+ hidden[key] = clonePurchasePayloadValue(item)
+ return
+ }
+ const child = splitPurchasePayloadByVisibility(item)
+ visible[key] = child.visible
+ if (hasMeaningfulPayloadValue(child.hidden)) {
+ hidden[key] = child.hidden
+ }
+ })
+
+ return { visible, hidden }
+ }
+
+ return { visible: value, hidden: undefined }
+}
+
+const mergePurchasePayloadWithHidden = (visibleValue, hiddenValue) => {
+ if (hiddenValue === undefined || hiddenValue === null) return visibleValue
+ if (visibleValue === undefined || visibleValue === null) return clonePurchasePayloadValue(hiddenValue)
+
+ if (Array.isArray(visibleValue) && Array.isArray(hiddenValue)) {
+ return visibleValue.map((item, index) => mergePurchasePayloadWithHidden(item, hiddenValue[index]))
+ }
+
+ if (
+ visibleValue &&
+ hiddenValue &&
+ typeof visibleValue === 'object' &&
+ typeof hiddenValue === 'object' &&
+ !Array.isArray(visibleValue) &&
+ !Array.isArray(hiddenValue)
+ ) {
+ const merged = { ...clonePurchasePayloadValue(hiddenValue) }
+ Object.entries(visibleValue).forEach(([key, item]) => {
+ merged[key] = mergePurchasePayloadWithHidden(item, merged[key])
+ })
+ return merged
+ }
+
+ return visibleValue
+}
+
+const localizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldLabelMap)
+
+const normalizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldKeyMap)
+
+const createPurchasePayloadNodeId = () => `purchase-node-${Date.now()}-${purchasePayloadTreeNodeSeed++}`
+
+const detectPurchaseValueType = (value) => {
+ if (Array.isArray(value)) return 'array'
+ if (value === null) return 'null'
+ const valueType = typeof value
+ if (valueType === 'number') return 'number'
+ if (valueType === 'boolean') return 'boolean'
+ if (valueType === 'object') return 'object'
+ return 'string'
+}
+
+const normalizePurchaseNodeValueForEdit = (value, valueType) => {
+ if (valueType === 'number') return value === null || value === undefined ? '' : String(value)
+ if (valueType === 'boolean') return Boolean(value)
+ if (valueType === 'null') return ''
+ return value === null || value === undefined ? '' : String(value)
+}
+
+const createPurchaseTreeNode = ({
+ key = '',
+ parentType = 'object',
+ keyEditable = false,
+ valueType = 'string',
+ value = '',
+ children = []
+} = {}) => ({
+ id: createPurchasePayloadNodeId(),
+ key,
+ parentType,
+ keyEditable,
+ valueType,
+ value,
+ children
+})
+
+const reorderPurchaseObjectEntries = (value) => {
+ const entries = Object.entries(value || {})
+ const productDataIndex = entries.findIndex(([key]) => key === 'productData')
+ if (productDataIndex <= -1 || productDataIndex === entries.length - 1) {
+ return entries
+ }
+ const [productDataEntry] = entries.splice(productDataIndex, 1)
+ entries.push(productDataEntry)
+ return entries
+}
+
+const buildPurchasePayloadTreeNodes = (value, parentType = 'object') => {
+ if (Array.isArray(value)) {
+ return value.map(item => {
+ const itemType = detectPurchaseValueType(item)
+ const node = createPurchaseTreeNode({
+ key: '',
+ parentType: 'array',
+ keyEditable: false,
+ valueType: itemType,
+ value: normalizePurchaseNodeValueForEdit(item, itemType)
+ })
+ if (purchaseContainerValueTypes.has(itemType)) {
+ node.children = buildPurchasePayloadTreeNodes(item, itemType)
+ }
+ return node
+ })
+ }
+
+ if (value && typeof value === 'object') {
+ return reorderPurchaseObjectEntries(value).map(([key, item]) => {
+ const itemType = detectPurchaseValueType(item)
+ const node = createPurchaseTreeNode({
+ key,
+ parentType,
+ keyEditable: false,
+ valueType: itemType,
+ value: normalizePurchaseNodeValueForEdit(item, itemType)
+ })
+ if (purchaseContainerValueTypes.has(itemType)) {
+ node.children = buildPurchasePayloadTreeNodes(item, itemType)
+ }
+ return node
+ })
+ }
+
+ return []
+}
+
+const initializePurchasePayloadTree = (messageObj, payload = {}) => {
+ const sourcePayload = payload && typeof payload === 'object' && !Array.isArray(payload)
+ ? payload
+ : {}
+ const { visible, hidden } = splitPurchasePayloadByVisibility(sourcePayload)
+ const visiblePayload = visible && typeof visible === 'object' && !Array.isArray(visible) ? visible : {}
+ messageObj.payloadTreeData = buildPurchasePayloadTreeNodes(visiblePayload, 'object')
+ messageObj.payloadHiddenData = hidden && typeof hidden === 'object' ? hidden : {}
+}
+
+const getPurchaseFieldLabel = (fieldKey) => purchasePayloadFieldLabelMap[fieldKey] || fieldKey || '瀛楁'
+
+const createPurchaseDefaultNode = (parentType = 'object') => createPurchaseTreeNode({
+ key: parentType === 'object' ? 'newField' : '',
+ parentType,
+ keyEditable: parentType === 'object',
+ valueType: 'string',
+ value: ''
+})
+
+const getPurchaseScalarNodeValue = (node) => {
+ if (node.valueType === 'null') return null
+ if (node.valueType === 'boolean') return Boolean(node.value)
+ if (node.valueType === 'number') {
+ const text = String(node.value ?? '').trim()
+ if (!text) return null
+ const numberValue = Number(text)
+ return Number.isFinite(numberValue) ? numberValue : text
+ }
+ return node.value === null || node.value === undefined ? '' : String(node.value)
+}
+
+const buildPurchasePayloadFromNodes = (nodes, parentType = 'object') => {
+ if (!Array.isArray(nodes)) {
+ return parentType === 'array' ? [] : {}
+ }
+
+ if (parentType === 'array') {
+ return nodes.map(node => {
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ return buildPurchasePayloadFromNodes(node.children, node.valueType)
+ }
+ return getPurchaseScalarNodeValue(node)
+ })
+ }
+
+ return nodes.reduce((result, node, index) => {
+ const rawKey = String(node.key ?? '').trim()
+ const key = rawKey || `field_${index + 1}`
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ result[key] = buildPurchasePayloadFromNodes(node.children, node.valueType)
+ } else {
+ result[key] = getPurchaseScalarNodeValue(node)
+ }
+ return result
+ }, {})
+}
+
+const findPurchaseNodeLocation = (nodes, targetId, parentNode = null) => {
+ if (!Array.isArray(nodes)) return null
+ for (let index = 0; index < nodes.length; index++) {
+ const node = nodes[index]
+ if (node.id === targetId) {
+ return {
+ siblings: nodes,
+ index,
+ node,
+ parentNode
+ }
+ }
+ const next = findPurchaseNodeLocation(node.children, targetId, node)
+ if (next) return next
+ }
+ return null
+}
+
+const getPurchaseArrayItemLabel = (row, message) => {
+ const location = findPurchaseNodeLocation(message?.payloadTreeData, row.id)
+ return `[${(location?.index ?? 0) + 1}]`
+}
+
+const handlePurchaseNodeTypeChange = (message, row) => {
+ if (!message || !row) return
+ if (purchaseContainerValueTypes.has(row.valueType)) {
+ row.children = []
+ row.value = ''
+ return
+ }
+ row.children = []
+ if (row.valueType === 'boolean') {
+ row.value = false
+ } else if (row.valueType === 'null') {
+ row.value = ''
+ } else {
+ row.value = ''
+ }
+}
+
+const addPurchaseRootField = (message) => {
+ if (!message) return
+ if (!Array.isArray(message.payloadTreeData)) {
+ message.payloadTreeData = []
+ }
+ message.payloadTreeData.push(createPurchaseDefaultNode('object'))
+}
+
+const addPurchaseChildNode = (message, row) => {
+ if (!message || !row || !purchaseContainerValueTypes.has(row.valueType)) return
+ if (!Array.isArray(row.children)) {
+ row.children = []
+ }
+ row.children.push(createPurchaseDefaultNode(row.valueType))
+}
+
+const addPurchaseSiblingNode = (message, row) => {
+ if (!message || !row) return
+ const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
+ if (!location || location.node.parentType !== 'array') return
+ location.siblings.splice(location.index + 1, 0, createPurchaseDefaultNode('array'))
+}
+
+const removePurchaseNode = (message, row) => {
+ if (!message || !row) return
+ const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
+ if (!location) return
+ location.siblings.splice(location.index, 1)
+}
+
+const hasPurchaseNodeValidationError = (nodes, parentType = 'object') => {
+ if (!Array.isArray(nodes)) return false
+ return nodes.some((node) => {
+ if (parentType === 'object' && !String(node.key ?? '').trim()) {
+ return true
+ }
+ if (node.valueType === 'number') {
+ const text = String(node.value ?? '').trim()
+ if (text && !Number.isFinite(Number(text))) {
+ return true
+ }
+ }
+ if (purchaseContainerValueTypes.has(node.valueType)) {
+ return hasPurchaseNodeValidationError(node.children, node.valueType)
+ }
+ return false
+ })
+}
+
+const purchaseDateFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'entryDate',
+ 'executionDate',
+ 'contractDate',
+ 'inputDate',
+ 'createdAt',
+ 'updatedAt'
+])
+
+const purchaseLedgerAllowedFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'id',
+ 'purchaseContractNumber',
+ 'supplierId',
+ 'supplierName',
+ 'isWhite',
+ 'recorderId',
+ 'recorderName',
+ 'salesContractNo',
+ 'salesContractNoId',
+ 'projectName',
+ 'entryDate',
+ 'executionDate',
+ 'remarks',
+ 'attachmentMaterials',
+ 'createdAt',
+ 'updatedAt',
+ 'salesLedgerId',
+ 'hasChildren',
+ 'Type',
+ 'productData',
+ 'tempFileIds',
+ 'SalesLedgerFiles',
+ 'phoneNumber',
+ 'businessPersonId',
+ 'productId',
+ 'productModelId',
+ 'invoiceNumber',
+ 'invoiceAmount',
+ 'ticketRegistrationId',
+ 'contractAmount',
+ 'receiptPaymentAmount',
+ 'unReceiptPaymentAmount',
+ 'type',
+ 'paymentMethod',
+ 'approvalStatus',
+ 'templateName'
+])
+
+const purchaseApprovalFieldKeys = new Set([
+ 'approveUserIds',
+ 'approverId',
+ 'auditors',
+ '瀹℃牳浜�',
+ '瀹℃壒浜�',
+ '瀹℃壒浜篒D',
+ '瀹℃壒鐢ㄦ埛ID鍒楄〃'
+])
+
+const purchaseIntegerFieldKeys = new Set([
+ 'id',
+ 'supplierId',
+ 'recorderId',
+ 'salesContractNoId',
+ 'salesLedgerId',
+ 'Type',
+ 'businessPersonId',
+ 'productId',
+ 'productModelId',
+ 'ticketRegistrationId',
+ 'type',
+ 'approvalStatus',
+ 'inventoryWarningQuantity'
+])
+
+const purchaseDecimalFieldKeys = new Set([
+ 'invoiceAmount',
+ 'contractAmount',
+ 'receiptPaymentAmount',
+ 'unReceiptPaymentAmount',
+ 'quantity',
+ 'taxRate',
+ 'taxInclusiveUnitPrice',
+ 'taxInclusiveTotalPrice',
+ 'taxExclusiveTotalPrice',
+ 'priceWithTax',
+ 'totalPriceWithTax'
+])
+
+const purchaseBooleanFieldKeys = new Set([
+ 'hasChildren',
+ 'isWhite',
+ 'isInspected',
+ 'isChecked'
+])
+
+const purchaseStringFieldKeys = new Set([
+ 'entryDateStart',
+ 'entryDateEnd',
+ 'purchaseContractNumber',
+ 'supplierName',
+ 'recorderName',
+ 'salesContractNo',
+ 'projectName',
+ 'entryDate',
+ 'executionDate',
+ 'remarks',
+ 'attachmentMaterials',
+ 'createdAt',
+ 'updatedAt',
+ 'phoneNumber',
+ 'invoiceNumber',
+ 'paymentMethod',
+ 'templateName',
+ 'productCategory',
+ 'specificationModel',
+ 'unit',
+ 'invoiceType'
+])
+
+const purchaseGenericArrayFieldKeys = new Set([
+ 'purchaseLedgers',
+ 'productData'
+])
+
+const purchaseStringArrayFieldKeys = new Set(['tempFileIds'])
+
+const purchaseObjectArrayFieldKeys = new Set(['SalesLedgerFiles'])
+
+const normalizePurchaseProductRecord = (record) => {
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return record
+ return mapPayloadKeys(record, purchasePayloadFieldKeyMap)
+}
+
+const getPurchaseProductMatchKey = (record) => {
+ if (!record || typeof record !== 'object') return ''
+ return record.purchaseContractNumber ||
+ record.purchaseContractNo ||
+ record.salesContractNo ||
+ record.salesContractNumber ||
+ ''
+}
+
+const prunePurchaseProductRecord = (record) => {
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return null
+ const normalizedRecord = normalizePurchaseProductRecord(record)
+ const hasVisibleFieldValue = Object.entries(normalizedRecord).some(([key, value]) => {
+ if (shouldHidePurchaseField(key)) return false
+ return hasMeaningfulPayloadValue(value)
+ })
+ return hasVisibleFieldValue ? normalizedRecord : null
+}
+
+const normalizeAndFilterPurchaseProductData = (value) => {
+ if (!Array.isArray(value)) return value
+ return value
+ .map(item => prunePurchaseProductRecord(item))
+ .filter(Boolean)
+}
+
+const mergeLegacyProductDataIntoLedgers = (payload) => {
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return payload
+ if (!Array.isArray(payload.purchaseLedgers) || !Array.isArray(payload.productData) || !payload.productData.length) {
+ return payload
+ }
+
+ const ledgers = payload.purchaseLedgers.map(ledger => ({
+ ...ledger,
+ productData: normalizeAndFilterPurchaseProductData(ledger.productData) || []
+ }))
+ const unmatchedProducts = []
+
+ normalizeAndFilterPurchaseProductData(payload.productData).forEach(product => {
+ const productMatchKey = getPurchaseProductMatchKey(product)
+ const matchedLedger = ledgers.find(ledger => {
+ const ledgerKeys = [
+ ledger.purchaseContractNumber,
+ ledger.purchaseContractNo,
+ ledger.salesContractNo,
+ ledger.salesContractNumber
+ ].filter(Boolean)
+ return productMatchKey && ledgerKeys.includes(productMatchKey)
+ })
+
+ if (matchedLedger) {
+ matchedLedger.productData.push(product)
+ } else if (ledgers.length === 1) {
+ ledgers[0].productData.push(product)
+ } else {
+ unmatchedProducts.push(product)
+ }
+ })
+
+ const nextPayload = {
+ ...payload,
+ purchaseLedgers: ledgers
+ }
+
+ if (unmatchedProducts.length) {
+ nextPayload.productData = unmatchedProducts
+ } else {
+ delete nextPayload.productData
+ }
+
+ return nextPayload
+}
+
+const filterPurchaseLedgerRecord = (record) => {
+ if (!record || typeof record !== 'object' || Array.isArray(record)) return record
+ const normalizedRecord = {
+ ...record,
+ productData: normalizeAndFilterPurchaseProductData(record.productData)
+ }
+ return Object.entries(normalizedRecord).reduce((result, [key, value]) => {
+ if (purchaseLedgerAllowedFieldKeys.has(key)) {
+ result[key] = value
+ }
+ return result
+ }, {})
+}
+
+const normalizeAttachmentMaterialsValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseAttachmentMaterialsField = (value) => {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchaseAttachmentMaterialsField(item))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ if (key === 'attachmentMaterials') {
+ result[key] = normalizeAttachmentMaterialsValue(item)
+ } else {
+ result[key] = normalizePurchaseAttachmentMaterialsField(item)
+ }
+ return result
+ }, {})
+ }
+ return value
+}
+
+const normalizePurchaseNumericText = (text = '') => {
+ return String(text)
+ .replace(/[,\s锛宂/g, '')
+ .replace(/[楼锟ュ厓%]/g, '')
+}
+
+const parsePurchaseNumberValue = (value) => {
+ if (typeof value === 'number') {
+ return Number.isFinite(value) ? value : null
+ }
+ if (typeof value === 'boolean') {
+ return value ? 1 : 0
+ }
+ if (typeof value !== 'string') {
+ return null
+ }
+ const text = normalizePurchaseNumericText(value.trim())
+ if (!text) return null
+ if (!/^[-+]?\d+(\.\d+)?$/.test(text)) return null
+ const numberValue = Number(text)
+ return Number.isFinite(numberValue) ? numberValue : null
+}
+
+const normalizePurchaseIntegerFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ if (numberValue === null) return null
+ return Math.trunc(numberValue)
+}
+
+const normalizePurchaseDecimalFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ const numberValue = parsePurchaseNumberValue(value)
+ return numberValue === null ? null : numberValue
+}
+
+const normalizePurchaseBooleanFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (value === '') return null
+ if (typeof value === 'boolean') return value
+ if (typeof value === 'number') return value !== 0
+ if (typeof value !== 'string') return null
+
+ const text = value.trim().toLowerCase()
+ if (!text) return null
+ if (['true', '1', 'yes', 'y', '鏄�', '宸�', 'checked'].includes(text)) return true
+ if (['false', '0', 'no', 'n', '鍚�', '鏈�', 'unchecked'].includes(text)) return false
+ return null
+}
+
+const normalizePurchaseStringFieldValue = (value) => {
+ if (value === null || value === undefined) return value
+ if (typeof value === 'string') return value
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
+ try {
+ return JSON.stringify(value)
+ } catch (err) {
+ return String(value)
+ }
+}
+
+const normalizePurchaseStringArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ if (value === null || value === undefined || value === '') return []
+ if (typeof value === 'string') {
+ const text = value.trim()
+ if (!text) return []
+ if (/^\[.*\]$/.test(text)) {
+ try {
+ const parsedValue = JSON.parse(text)
+ if (Array.isArray(parsedValue)) {
+ return parsedValue
+ .map(item => normalizePurchaseStringFieldValue(item))
+ .filter(item => item !== null && item !== undefined && item !== '')
+ }
+ } catch (err) {
+ // Keep as plain text when not valid JSON array.
+ }
+ }
+ const splitValues = text
+ .split(/[,\n锛宂/)
+ .map(item => item.trim())
+ .filter(Boolean)
+ return splitValues.length > 1 ? splitValues : [text]
+ }
+ const normalizedValue = normalizePurchaseStringFieldValue(value)
+ return normalizedValue === null || normalizedValue === undefined || normalizedValue === ''
+ ? []
+ : [normalizedValue]
+}
+
+const normalizePurchaseObjectArrayFieldValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.filter(item => item && typeof item === 'object')
+ }
+ if (value && typeof value === 'object') {
+ return [value]
+ }
+ return []
+}
+
+const normalizePurchaseValueByFieldKey = (fieldKey, value) => {
+ if (fieldKey === 'attachmentMaterials') return normalizeAttachmentMaterialsValue(value)
+ if (purchaseIntegerFieldKeys.has(fieldKey)) return normalizePurchaseIntegerFieldValue(value)
+ if (purchaseDecimalFieldKeys.has(fieldKey)) return normalizePurchaseDecimalFieldValue(value)
+ if (purchaseBooleanFieldKeys.has(fieldKey)) return normalizePurchaseBooleanFieldValue(value)
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) return normalizePurchaseStringArrayFieldValue(value)
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) return normalizePurchaseObjectArrayFieldValue(value)
+ if (purchaseStringFieldKeys.has(fieldKey)) return normalizePurchaseStringFieldValue(value)
+ return value
+}
+
+const normalizePurchasePayloadFieldTypes = (value, fieldKey = '') => {
+ if (purchaseGenericArrayFieldKeys.has(fieldKey)) {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+ if (value && typeof value === 'object') {
+ return [normalizePurchasePayloadFieldTypes(value)]
+ }
+ return []
+ }
+
+ if (purchaseStringArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseStringArrayFieldValue(value)
+ }
+
+ if (purchaseObjectArrayFieldKeys.has(fieldKey)) {
+ return normalizePurchaseObjectArrayFieldValue(value)
+ }
+
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadFieldTypes(item))
+ }
+
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [key, item]) => {
+ result[key] = normalizePurchasePayloadFieldTypes(item, key)
+ return result
+ }, {})
+ }
+
+ return normalizePurchaseValueByFieldKey(fieldKey, value)
+}
+
+const sanitizePurchasePayloadForSubmit = (payload, businessType) => {
+ if (businessType !== 'purchase_ledger' || !payload || typeof payload !== 'object') return payload
+
+ let sanitized = mergeLegacyProductDataIntoLedgers(Array.isArray(payload) ? [...payload] : { ...payload })
+ sanitized = normalizePurchaseAttachmentMaterialsField(sanitized)
+ sanitized = normalizePurchasePayloadFieldTypes(sanitized)
+ if (Array.isArray(sanitized.purchaseLedgers)) {
+ sanitized.purchaseLedgers = sanitized.purchaseLedgers.map(filterPurchaseLedgerRecord)
+ }
+ if (Array.isArray(sanitized.productData)) {
+ sanitized.productData = normalizeAndFilterPurchaseProductData(sanitized.productData)
+ }
+ if (Array.isArray(sanitized.productData) && !sanitized.productData.length) {
+ delete sanitized.productData
+ }
+
+ purchaseApprovalFieldKeys.forEach(key => {
+ if (!Array.isArray(sanitized)) {
+ delete sanitized[key]
+ }
+ })
+
+ return sanitized
+}
+
+const getVisiblePurchaseMissingFields = (analysisData) => {
+ const fields = Array.isArray(analysisData?.missingFields) ? analysisData.missingFields : []
+ const visibleFields = analysisData?.businessType === 'purchase_ledger'
+ ? fields.filter(field => {
+ if (purchaseApprovalFieldKeys.has(field)) return false
+ const normalizedField = purchasePayloadFieldKeyMap[field] || field
+ return !shouldHidePurchaseField(normalizedField) && !shouldHidePurchaseField(field)
+ })
+ : fields
+ return visibleFields.map(field => purchasePayloadFieldLabelMap[field] || field)
+}
+
+const formatDateParts = (year, month, day) => {
+ const normalizedYear = Number(year)
+ const normalizedMonth = Number(month)
+ const normalizedDay = Number(day)
+ if (!normalizedYear || !normalizedMonth || !normalizedDay) return ''
+
+ const date = new Date(normalizedYear, normalizedMonth - 1, normalizedDay)
+ if (
+ date.getFullYear() !== normalizedYear ||
+ date.getMonth() !== normalizedMonth - 1 ||
+ date.getDate() !== normalizedDay
+ ) {
+ return ''
+ }
+
+ return [
+ String(normalizedYear).padStart(4, '0'),
+ String(normalizedMonth).padStart(2, '0'),
+ String(normalizedDay).padStart(2, '0')
+ ].join('-')
+}
+
+const normalizeDateString = (value) => {
+ if (typeof value !== 'string') return value
+ const text = value.trim()
+ if (!text) return value
+
+ let match = text.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[T\s].*)?$/)
+ if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+ match = text.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})(?:\s.*)?$/)
+ if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+ match = text.match(/^(\d{4})骞�(\d{1,2})鏈�(\d{1,2})鏃�?(?:\s.*)?$/)
+ if (match) return formatDateParts(match[1], match[2], match[3]) || value
+
+ match = text.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})(?:\s.*)?$/)
+ if (match) {
+ const year = match[3].length === 2 ? Number(`20${match[3]}`) : Number(match[3])
+ return formatDateParts(year, match[1], match[2]) || value
+ }
+
+ return value
+}
+
+const normalizePurchasePayloadDates = (value, key = '') => {
+ if (Array.isArray(value)) {
+ return value.map(item => normalizePurchasePayloadDates(item, key))
+ }
+ if (value && typeof value === 'object') {
+ return Object.entries(value).reduce((result, [itemKey, item]) => {
+ result[itemKey] = normalizePurchasePayloadDates(item, itemKey)
+ return result
+ }, {})
+ }
+ return purchaseDateFieldKeys.has(key) ? normalizeDateString(value) : value
+}
+
+const isEmptyValue = (value) => {
+ if (value === null || value === undefined || value === '') return true
+ if (Array.isArray(value)) return value.every(item => isEmptyValue(item))
+ if (typeof value === 'object') return Object.values(value).every(item => isEmptyValue(item))
+ return false
+}
+
+const isPurchasePayloadEmpty = (payload) => isEmptyValue(payload)
+
+const getPurchaseConfirmDescription = (analysisData) => {
+ if (!analysisData) return ''
+ if (isPurchasePayloadEmpty(analysisData.payload)) {
+ return '鎴戞病鏈変粠鏂囦欢涓彁鍙栧埌瀹屾暣鐨勯噰璐笟鍔℃暟鎹紝鏆傛椂涓嶈兘鐩存帴鐢熸垚閲囪喘鍙拌处銆�'
+ }
+ return analysisData.description || '宸叉暣鐞嗗嚭寰呯‘璁ょ殑閲囪喘涓氬姟鏁版嵁锛岃鏍稿鍚庢彁浜ゃ��'
+}
+
+const confirmPurchaseAnalysis = async (message) => {
+ if (!message?.purchaseAnalysisData || message.confirming || message.confirmed) return
+
+ let payload
+ try {
+ const parsedPayload = message.payloadText?.trim() ? JSON.parse(message.payloadText) : {}
+ payload = sanitizePurchasePayloadForSubmit(
+ normalizePurchasePayloadDates(normalizePurchasePayload(parsedPayload)),
+ message.purchaseAnalysisData.businessType
+ )
+ } catch (err) {
+ message.confirmResult = '寰呮彁浜ゆ暟鎹笉鏄悎娉� JSON锛岃淇敼鍚庡啀纭'
+ message.confirmed = false
+ return
+ }
+
+ message.confirming = true
+ message.confirmResult = ''
+
+ try {
+ const res = await request.post(`${currentAssistant.value.apiBase}/analyze-files/confirm`, {
+ businessType: message.purchaseAnalysisData.businessType,
+ payload
+ })
+ message.confirmed = true
+ message.confirmResult = res?.msg || '纭鎴愬姛锛屼笟鍔″鐞嗗凡鎻愪氦'
+ ElMessage.success(message.confirmResult)
+ } catch (err) {
+ message.confirmed = false
+ message.confirmResult = err?.message || '纭澶辫触锛岃妫�鏌ユ暟鎹悗閲嶈瘯'
+ } finally {
+ message.confirming = false
+ }
+}
+
+const confirmPurchaseAnalysisFromTable = async (message) => {
+ if (!message?.purchaseAnalysisData || message.confirming || message.confirmed) return
+
+ if (!Array.isArray(message.payloadTreeData)) {
+ initializePurchasePayloadTree(message, message.purchaseAnalysisData.payload || {})
+ }
+ if (hasPurchaseNodeValidationError(message.payloadTreeData, 'object')) {
+ message.confirmResult = '璇峰厛琛ュ叏瀛楁鍚嶏紝骞剁‘淇濇暟瀛楀瓧娈靛~鍐欏悎娉曟暟瀛�'
+ message.confirmed = false
+ return
+ }
+
+ let payload
+ try {
+ const draftPayload = buildPurchasePayloadFromNodes(message.payloadTreeData, 'object')
+ const mergedPayload = mergePurchasePayloadWithHidden(draftPayload, message.payloadHiddenData)
+ const normalizedPayload = normalizePurchasePayload(mergedPayload)
+ payload = sanitizePurchasePayloadForSubmit(
+ normalizePurchasePayloadDates(normalizedPayload),
+ message.purchaseAnalysisData.businessType
+ )
+ message.payloadText = JSON.stringify(localizePurchasePayload(normalizedPayload), null, 2)
+ } catch (err) {
+ message.confirmResult = '寰呮彁浜ゆ暟鎹牸寮忔湁璇紝璇锋鏌ュ悗鍐嶇‘璁�'
+ message.confirmed = false
+ return
+ }
+
+ message.confirming = true
+ message.confirmResult = ''
+
+ try {
+ const res = await request.post(`${currentAssistant.value.apiBase}/analyze-files/confirm`, {
+ businessType: message.purchaseAnalysisData.businessType,
+ payload
+ })
+ message.confirmed = true
+ message.confirmResult = res?.msg || '纭鎴愬姛锛屼笟鍔″鐞嗗凡鎻愪氦'
+ ElMessage.success(message.confirmResult)
+ } catch (err) {
+ message.confirmed = false
+ message.confirmResult = err?.message || '纭澶辫触锛岃妫�鏌ユ暟鎹悗閲嶈瘯'
+ } finally {
+ message.confirming = false
+ }
+}
+
+const scrollToBottom = () => {
+ nextTick(() => {
+ if (messageListRef.value) {
+ messageListRef.value.scrollTop = messageListRef.value.scrollHeight
+ }
+ })
+}
+
+const handleFileChange = (file, fileList = []) => {
+ if (!file) return
+ const nextFiles = currentAssistant.value.allowMultipleFileUpload
+ ? fileList.map(item => item.raw).filter(Boolean)
+ : [file.raw].filter(Boolean)
+
+ const validFiles = nextFiles.filter(rawFile => {
+ const isLt10M = rawFile.size / 1024 / 1024 < 10
+ if (!isLt10M) {
+ ElMessage.error(`${rawFile.name} 鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!`)
+ }
+ return isLt10M
+ })
+
+ clearSelectedFiles()
+ selectedFiles.value = validFiles
+ uploadFileList.value = fileList.filter(item => item.raw && validFiles.includes(item.raw))
+ selectedFileSnapshots.value = validFiles.map((rawFile, index) => createLocalFileSnapshot(rawFile, index))
+}
+
+const removeSelectedFile = (index) => {
+ const [removedSnapshot] = selectedFileSnapshots.value.splice(index, 1)
+ revokeLocalFileSnapshots(removedSnapshot ? [removedSnapshot] : [])
+ selectedFiles.value.splice(index, 1)
+ uploadFileList.value.splice(index, 1)
+}
+
+const analyzeFiles = async (files, message = '', localFileSnapshots = []) => {
+ const uploadFiles = Array.isArray(files) ? files : [files].filter(Boolean)
+ if (!uploadFiles.length) return
+ if (isSending.value) return
+ isSending.value = true
+ currentAbortController.value = new AbortController()
+
+ const fileNames = uploadFiles.map(file => file.name).join('銆�')
+ const userMsg = message ? `${message}\n[涓婁紶鏂囦欢鍒嗘瀽] ${fileNames}` : `[涓婁紶鏂囦欢鍒嗘瀽] ${fileNames}`
+ messages.value.push({
+ isUser: true,
+ content: userMsg,
+ htmlContent: convertTextToHtml(userMsg),
+ isTyping: false,
+ localUploadFiles: Array.isArray(localFileSnapshots) ? localFileSnapshots : []
+ })
+
+ const botMsgIndex = messages.value.length
+ messages.value.push({
+ isUser: false,
+ content: '',
+ htmlContent: '',
+ isTyping: true,
+ chartOptions: null,
+ chartRenderReady: false,
+ type: '',
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null,
+ purchaseAnalysisData: null,
+ manufacturingData: null,
+ salesData: null,
+ purchaseData: null,
+ purchaseIntentData: null,
+ financeData: null,
+ chartMarkdownParseFailed: false
+ })
+
+ outputState.value[botMsgIndex] = {
+ isPaused: false,
+ jsonBlockStartPos: -1,
+ jsBlockStartPos: -1,
+ blockEndPos: -1,
+ hasRenderedChart: false
+ }
+
+ scrollToBottom()
+
+ const formData = new FormData()
+ const fileFieldName = currentAssistant.value.allowMultipleFileUpload ? 'files' : 'file'
+ uploadFiles.forEach(file => formData.append(fileFieldName, file))
+ formData.append('memoryId', uuid.value)
+ if (message.trim()) {
+ formData.append('message', message.trim())
+ }
+
+ const analyzeUrl = currentAssistant.value.fileAnalyzeUrl || `${currentAssistant.value.apiBase}/analyze-file`
+ request.post(analyzeUrl, formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data'
+ },
+ signal: currentAbortController.value.signal,
+ onDownloadProgress: (e) => {
+ const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
+ if (!fullText) return
+
+ const currentMsg = messages.value[botMsgIndex]
+ if (!currentMsg) return
+
+ currentMsg.content = fullText
+
+ // 瑙f瀽 JSON 鏁版嵁锛堥拡瀵瑰祵鍏ュ紡 JSON锛�
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
+ }
+
+ updateOutputState(fullText, botMsgIndex)
+ currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ scrollToBottom()
+ }
+ }).then(() => {
+ const currentMsg = messages.value[botMsgIndex]
+ currentMsg.isTyping = false
+ isSending.value = false
+ currentAbortController.value = null
+
+ const extracted = extractEmbeddedSuccessJson(currentMsg.content)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex, !outputState.value[botMsgIndex].hasRenderedChart)
+ }
+ currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
+
+ // 鏈�缁堣В鏋愮‘淇濆浘琛ㄦ覆鏌�
+ if (currentMsg.chartOptions && !outputState.value[botMsgIndex].hasRenderedChart) {
+ renderCharts(botMsgIndex, currentMsg.chartOptions)
+ outputState.value[botMsgIndex].hasRenderedChart = true
+ }
+ }).catch(err => {
+ if (err.name === 'CanceledError' || err.name === 'AbortError') {
+ console.log('Analysis aborted by user')
+ isSending.value = false
+ currentAbortController.value = null
+ return
+ }
+ console.error('File analysis error:', err)
+ const errorMsg = '鎶辨瓑锛屾枃浠跺垎鏋愯繃绋嬩腑閬囧埌浜嗕竴鐐归棶棰橈紝璇风◢鍚庡啀璇曘��'
+ if (messages.value[botMsgIndex]) {
+ messages.value[botMsgIndex].content = errorMsg
+ messages.value[botMsgIndex].htmlContent = convertTextToHtml(errorMsg)
+ messages.value[botMsgIndex].isTyping = false
+ }
+ isSending.value = false
+ currentAbortController.value = null
+ })
+}
+
+const sendMessage = () => {
+ const msg = inputMessage.value?.trim() || ''
+ if ((msg || selectedFiles.value.length) && !isSending.value) {
+ if (selectedFiles.value.length) {
+ const localFileSnapshots = selectedFileSnapshots.value
+ analyzeFiles([...selectedFiles.value], msg, localFileSnapshots)
+ clearSelectedFiles({ releaseSnapshots: false })
+ } else {
+ sendRequest(msg)
+ }
+ inputMessage.value = ''
+ }
+}
+
+const stopGeneration = () => {
+ abortCurrentRequest()
+}
+
+const sendRequest = (message) => {
+ isSending.value = true
+ currentAbortController.value = new AbortController()
+
+ // 鐢ㄦ埛娑堟伅
+ messages.value.push({
+ isUser: true,
+ content: message,
+ htmlContent: convertTextToHtml(message),
+ isTyping: false
+ })
+
+ // 鏈哄櫒浜哄崰浣�
+ const botMsgIndex = messages.value.length
+ const botMsg = {
+ isUser: false,
+ content: '',
+ htmlContent: '',
+ isTyping: true,
+ chartOptions: null,
+ chartRenderReady: false,
+ type: '',
+ tableData: null,
+ payloadTreeData: null,
+ payloadHiddenData: null,
+ purchaseAnalysisData: null,
+ manufacturingData: null,
+ salesData: null,
+ purchaseData: null,
+ purchaseIntentData: null,
+ financeData: null,
+ chartMarkdownParseFailed: false
+ }
+ messages.value.push(botMsg)
+
+ outputState.value[botMsgIndex] = {
+ isPaused: false,
+ jsonBlockStartPos: -1,
+ jsBlockStartPos: -1,
+ blockEndPos: -1,
+ hasRenderedChart: false
+ }
+
+ scrollToBottom()
+
+ request.post(`${currentAssistant.value.apiBase}/chat`,
+ { memoryId: uuid.value, message },
+ {
+ signal: currentAbortController.value.signal,
+ onDownloadProgress: (e) => {
+ // 鍏煎涓嶅悓鐗堟湰鐨� axios 鑾峰彇鍝嶅簲鏂囨湰鐨勬柟寮�
+ const fullText = e.target ? e.target.responseText : (e.event ? e.event.target.responseText : '')
+ if (!fullText) return
+
+ const currentMsg = messages.value[botMsgIndex]
+ if (!currentMsg) return
+
+ currentMsg.content = fullText
+
+ // 灏濊瘯鎻愬彇骞惰В鏋� JSON
+ const extracted = extractEmbeddedSuccessJson(fullText)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ }
+
+ updateOutputState(fullText, botMsgIndex)
+ currentMsg.htmlContent = convertStreamOutput(fullText, botMsgIndex)
+ scrollToBottom()
+ }
+ }
+ ).then(() => {
+ const currentMsg = messages.value[botMsgIndex]
+ currentMsg.isTyping = false
+ isSending.value = false
+ currentAbortController.value = null
+
+ // 鏈�缁堣В鏋�
+ const extracted = extractEmbeddedSuccessJson(currentMsg.content)
+ if (extracted) {
+ applyStructuredMessageData(currentMsg, extracted.data, botMsgIndex)
+ }
+ currentMsg.htmlContent = convertStreamOutput(currentMsg.content || '', botMsgIndex)
+ }).catch(err => {
+ if (err.name === 'CanceledError' || err.name === 'AbortError') {
+ console.log('Request aborted by user')
+ return
+ }
+ console.error('AI Chat Error:', err)
+ const errorMsg = '鎶辨瓑锛屾垜鐜板湪閬囧埌浜嗕竴鐐归棶棰橈紝璇风◢鍚庡啀璇曘��'
+ if (messages.value[botMsgIndex]) {
+ messages.value[botMsgIndex].content = errorMsg
+ messages.value[botMsgIndex].htmlContent = convertTextToHtml(errorMsg)
+ messages.value[botMsgIndex].isTyping = false
+ }
+ isSending.value = false
+ currentAbortController.value = null
+ })
+}
+
+const updateOutputState = (text, msgIndex) => {
+ const state = outputState.value[msgIndex]
+ if (state.jsonBlockStartPos === -1) {
+ const pos = text.indexOf('```json')
+ if (pos !== -1) { state.jsonBlockStartPos = pos; state.isPaused = true }
+ }
+ if (state.jsBlockStartPos === -1) {
+ const pos = text.indexOf('```javascript') !== -1 ? text.indexOf('```javascript') : text.indexOf('```js')
+ if (pos !== -1) { state.jsBlockStartPos = pos; state.isPaused = true }
+ }
+ if ((state.jsonBlockStartPos !== -1 || state.jsBlockStartPos !== -1) && state.blockEndPos === -1) {
+ const startCheck = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos + 7 : state.jsBlockStartPos + (text.includes('javascript') ? 13 : 5)
+ const endPos = text.indexOf('```', startCheck)
+ if (endPos !== -1) { state.blockEndPos = endPos + 3; state.isPaused = false }
+ }
+}
+
+const convertTextToHtml = (text) => {
+ if (!text) return ''
+ return text
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/\n/g, '<br>')
+}
+
+const localChartMarkdownImagePattern = /!\[[^\]]*]\((https?:\/\/local\/generate_chart\?[^)\s]+)\)/gi
+
+const parseLocalChartOptionText = (optionText = '') => {
+ const text = String(optionText || '').trim()
+ if (!text) return null
+
+ const parseCandidates = [text]
+ try {
+ const decoded = decodeURIComponent(text)
+ if (decoded && decoded !== text) {
+ parseCandidates.push(decoded)
+ }
+ } catch (err) {
+ // Keep original text candidate.
+ }
+
+ for (const candidate of parseCandidates) {
+ try {
+ const parsed = JSON.parse(candidate)
+ if (isPlainObject(parsed)) {
+ return parsed
+ }
+ } catch (err) {
+ continue
+ }
+ }
+
+ return null
+}
+
+const parseLocalChartOptionFromUrl = (urlText = '') => {
+ try {
+ const url = new URL(String(urlText || '').trim())
+ if (String(url.hostname || '').toLowerCase() !== 'local' || !String(url.pathname || '').includes('/generate_chart')) {
+ return null
+ }
+ const optionText = url.searchParams.get('options')
+ return parseLocalChartOptionText(optionText)
+ } catch (err) {
+ return null
+ }
+}
+
+const extractLocalChartMarkdown = (text = '') => {
+ const sourceText = String(text || '')
+ if (!sourceText) {
+ return {
+ cleanedText: '',
+ hasLocalChartMarkdown: false,
+ chartOptions: null,
+ parseFailed: false
+ }
+ }
+
+ let hasLocalChartMarkdown = false
+ let chartIndex = 0
+ const chartOptions = {}
+
+ const cleanedText = sourceText.replace(localChartMarkdownImagePattern, (fullMatch, chartUrl) => {
+ hasLocalChartMarkdown = true
+ const option = parseLocalChartOptionFromUrl(chartUrl)
+ if (option) {
+ chartOptions[`markdownChart_${chartIndex++}`] = option
+ }
+ return ''
+ })
+
+ const normalizedText = cleanedText
+ .replace(/\n[ \t]*\n[ \t]*\n+/g, '\n\n')
+ .trim()
+ const hasParsedCharts = Object.keys(chartOptions).length > 0
+
+ return {
+ cleanedText: normalizedText,
+ hasLocalChartMarkdown,
+ chartOptions: hasParsedCharts ? chartOptions : null,
+ parseFailed: hasLocalChartMarkdown && !hasParsedCharts
+ }
+}
+
+const applyLocalChartMarkdownFallback = (displayText, msgIndex) => {
+ const messageObj = messages.value[msgIndex]
+ if (!messageObj || messageObj.isUser) return displayText
+
+ const {
+ cleanedText,
+ hasLocalChartMarkdown,
+ chartOptions,
+ parseFailed
+ } = extractLocalChartMarkdown(displayText)
+
+ if (!hasLocalChartMarkdown) {
+ return displayText
+ }
+
+ if (chartOptions) {
+ messageObj.chartOptions = chartOptions
+ messageObj.chartRenderReady = true
+ messageObj.chartMarkdownParseFailed = false
+
+ const streamState = outputState.value[msgIndex]
+ if (!streamState || !streamState.hasRenderedChart) {
+ renderCharts(msgIndex, chartOptions)
+ if (streamState) {
+ streamState.hasRenderedChart = true
+ }
+ }
+
+ return cleanedText || '宸蹭负鎮ㄧ敓鎴愬垎鏋愬浘琛ㄣ��'
+ }
+
+ if (!messageObj.chartOptions || !messageObj.chartRenderReady) {
+ messageObj.chartOptions = null
+ messageObj.chartRenderReady = false
+ messageObj.chartMarkdownParseFailed = parseFailed
+ }
+
+ return cleanedText || '鍥捐〃瑙f瀽澶辫触锛岃绋嶅悗閲嶈瘯銆�'
+}
+
+const convertStreamOutput = (output, msgIndex) => {
+ if (!output) return ''
+ const state = outputState.value[msgIndex]
+ let display = output
+
+ // 灏濊瘯鎻愬彇 JSON 閮ㄥ垎
+ const extracted = extractEmbeddedSuccessJson(output)
+ const startMatch = output.match(/\{\s*"success"\s*:/)
+ const startIdx = extracted ? extracted.startIdx : (startMatch?.index ?? -1)
+
+ // 濡傛灉杩樺湪浠g爜鍧椾腑涓旀湭缁撴潫锛屾樉绀烘彁绀烘枃瀛�
+ if (state && ((state.jsonBlockStartPos !== -1) || (state.jsBlockStartPos !== -1)) && state.blockEndPos === -1) {
+ const startPos = state.jsonBlockStartPos !== -1 ? state.jsonBlockStartPos : state.jsBlockStartPos
+ const textBeforeBlock = display.substring(0, startPos).trim()
+ display = textBeforeBlock || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ return convertTextToHtml(display)
+ }
+
+ if (extracted) {
+ const parsed = extracted.data
+ display = (output.substring(0, extracted.startIdx) + output.substring(extracted.endIdx)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ const resolvedDescription = resolveStructuredDescription(parsed)
+ if (resolvedDescription) {
+ display = resolvedDescription
+ }
+
+ if (!display) {
+ display = getStructuredFallbackText(parsed)
+ }
+ } else if (startIdx !== -1) {
+ const lastBraceIdx = output.lastIndexOf('}')
+ if (lastBraceIdx !== -1 && lastBraceIdx > startIdx) {
+ const potentialJson = output.substring(startIdx, lastBraceIdx + 1)
+ try {
+ const parsed = JSON.parse(potentialJson)
+ // 鎴愬姛瑙f瀽锛岀Щ闄� JSON 閮ㄥ垎鏄剧ず鏂囧瓧
+ display = (output.substring(0, startIdx) + output.substring(lastBraceIdx + 1)).trim()
+
+ if (/^[\s}\],锛屻��.:锛�;锛沒+$/.test(display)) {
+ display = ''
+ }
+
+ const resolvedDescription = resolveStructuredDescription(parsed)
+ if (resolvedDescription) {
+ display = resolvedDescription
+ }
+
+ if (!display) {
+ display = getStructuredFallbackText(parsed)
+ }
+ } catch (e) {
+ // 瑙f瀽澶辫触锛岃鏄� JSON 杩樺湪浼犺緭涓垨鏍煎紡涓嶆纭�
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ }
+ } else {
+ // 鎵惧埌浜嗗紑濮嬩絾杩樻病鎵惧埌缁撴潫
+ display = output.substring(0, startIdx).trim() || '姝e湪鍒嗘瀽鏁版嵁骞剁敓鎴愬浘琛�...'
+ }
+ }
+
+ display = applyLocalChartMarkdownFallback(display, msgIndex)
+ let html = convertTextToHtml(display)
+
+ // 杩樺師浠g爜鍧�
+ html = html.replace(/```(javascript|js)([\s\S]*?)```/g, '<pre class="code-block js-code">$2</pre>')
+ html = html.replace(/```([\s\S]*?)```/g, '<pre class="code-block">$1</pre>')
+
+ return html || '...'
+}
+
+const renderCharts = (msgIndex, chartOptions) => {
+ nextTick(() => {
+ Object.keys(chartOptions).forEach(key => {
+ const id = `ai-chart-${msgIndex}-${key}`
+ const tryInit = (count = 0) => {
+ const dom = document.getElementById(id)
+ if (dom) {
+ if (chartInstances.value[id]) {
+ // 濡傛灉宸茬粡鍒濆鍖栬繃锛岀洿鎺ユ洿鏂版暟鎹�
+ const chart = chartInstances.value[id]
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) chart.setOption(option)
+ return
+ }
+
+ const chart = echarts.init(dom)
+ chartInstances.value[id] = chart
+ const option = normalizeAiChartOption(chartOptions[key])
+ if (option) {
+ chart.setOption(option)
+ } else {
+ console.warn('Invalid chart option for:', id, chartOptions[key])
+ }
+
+ const handler = () => chart.resize()
+ resizeHandlers.value.push(handler)
+ window.addEventListener('resize', handler)
+ } else if (count < 15) { // 绋嶅井澧炲姞閲嶈瘯娆℃暟
+ setTimeout(() => tryInit(count + 1), 200)
+ }
+ }
+ tryInit()
+ })
+ })
+}
+
+// 鏍煎紡鍖� AI 杩斿洖鐨勫浘琛ㄩ厤缃紝灏嗗叾杞崲涓烘爣鍑嗙殑 ECharts 閰嶇疆
+const formatChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ // 濡傛灉宸茬粡鏄爣鍑� ECharts 閰嶇疆锛堝寘鍚� series锛夛紝鍒欑洿鎺ヨ繑鍥�
+ const hasSeries = rawOption.series && Array.isArray(rawOption.series)
+
+ // 灏濊瘯杞崲绠�鏄撴牸寮�
+ try {
+ const isPie = rawOption.type === 'pie' || (rawOption.title && rawOption.title.includes('鍗犳瘮'))
+
+ const option = {
+ title: {
+ text: rawOption.title || '',
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: {
+ bottom: '0'
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ type: 'category',
+ data: rawOption.xAxisData || (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : []),
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : ''
+ },
+ yAxis: isPie ? undefined : {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ },
+ series: rawOption.series || [{
+ name: rawOption.title || '鏁板��',
+ type: rawOption.type || 'line',
+ data: rawOption.seriesData || (Array.isArray(rawOption.data) ? rawOption.data : []),
+ smooth: true,
+ radius: isPie ? '50%' : undefined
+ }]
+ }
+
+ // 閽堝楗煎浘鐨勭壒娈婂鐞�
+ if (isPie && !option.series[0].data && Array.isArray(rawOption.data)) {
+ option.series[0].data = rawOption.data
+ }
+
+ return option
+ } catch (err) {
+ console.error('Chart option conversion failed:', err)
+ return null
+ }
+}
+
+const normalizeAiChartOption = (rawOption) => {
+ if (!rawOption) return null
+
+ try {
+ const hasSeries = Array.isArray(rawOption.series) && rawOption.series.length > 0
+ const firstSeriesType = hasSeries ? rawOption.series[0]?.type : rawOption.type
+ const titleConfig = rawOption.title && typeof rawOption.title === 'object'
+ ? rawOption.title
+ : null
+ const tooltipConfig = rawOption.tooltip && typeof rawOption.tooltip === 'object'
+ ? rawOption.tooltip
+ : null
+ const legendConfig = rawOption.legend && typeof rawOption.legend === 'object'
+ ? rawOption.legend
+ : null
+ const rawXAxisConfig = rawOption.xAxis && typeof rawOption.xAxis === 'object' && !Array.isArray(rawOption.xAxis)
+ ? rawOption.xAxis
+ : null
+ const rawYAxisConfig = rawOption.yAxis && typeof rawOption.yAxis === 'object' && !Array.isArray(rawOption.yAxis)
+ ? rawOption.yAxis
+ : null
+ const titleText = typeof rawOption.title === 'string' ? rawOption.title : rawOption.title?.text || ''
+ const isPie = firstSeriesType === 'pie' || titleText.includes('鍗犳瘮')
+ const baseXAxisData = Array.isArray(rawOption.xAxisData)
+ ? rawOption.xAxisData
+ : (Array.isArray(rawOption.xAxis) ? rawOption.xAxis : (Array.isArray(rawXAxisConfig?.data) ? rawXAxisConfig.data : []))
+ const fallbackSeries = [{
+ name: titleText || '鏁版嵁',
+ type: rawOption.type || 'line',
+ data: Array.isArray(rawOption.seriesData) ? rawOption.seriesData : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ }]
+ const normalizedSeries = (hasSeries ? rawOption.series : fallbackSeries).map((seriesItem, index) => {
+ const seriesType = seriesItem?.type || rawOption.type || 'line'
+ const nextSeries = {
+ ...seriesItem,
+ name: seriesItem?.name || titleText || `绯诲垪${index + 1}`,
+ type: seriesType
+ }
+
+ if (isPie) {
+ nextSeries.radius = nextSeries.radius || '55%'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : (Array.isArray(rawOption.data) ? rawOption.data : [])
+ } else {
+ nextSeries.smooth = typeof nextSeries.smooth === 'boolean' ? nextSeries.smooth : seriesType === 'line'
+ nextSeries.data = Array.isArray(nextSeries.data) ? nextSeries.data : []
+ }
+
+ return nextSeries
+ })
+ const categorySource = !isPie
+ ? normalizedSeries.find(seriesItem => Array.isArray(seriesItem.data) && seriesItem.data.every(item => item && typeof item === 'object' && 'name' in item && 'value' in item))
+ : null
+ const xAxisData = categorySource
+ ? categorySource.data.map(item => item.name)
+ : baseXAxisData
+ const finalSeries = !isPie && categorySource
+ ? normalizedSeries.map(seriesItem => ({
+ ...seriesItem,
+ data: Array.isArray(seriesItem.data)
+ ? seriesItem.data.map(item => (item && typeof item === 'object' && 'value' in item ? item.value : item))
+ : []
+ }))
+ : normalizedSeries
+
+ return {
+ title: titleConfig || {
+ text: titleText,
+ left: 'center',
+ textStyle: { fontSize: 14 }
+ },
+ tooltip: tooltipConfig || {
+ trigger: isPie ? 'item' : 'axis'
+ },
+ legend: legendConfig || {
+ bottom: '0'
+ },
+ grid: isPie ? undefined : {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ containLabel: true
+ },
+ xAxis: isPie ? undefined : {
+ ...(rawXAxisConfig || {}),
+ type: 'category',
+ data: xAxisData,
+ name: typeof rawOption.xAxis === 'string' ? rawOption.xAxis : (rawXAxisConfig?.name || '')
+ },
+ yAxis: isPie ? undefined : (rawYAxisConfig || {
+ type: 'value',
+ name: typeof rawOption.yAxis === 'string' ? rawOption.yAxis : ''
+ }),
+ series: finalSeries
+ }
+ } catch (err) {
+ console.error('AI chart normalization failed:', err, rawOption)
+ return formatChartOption(rawOption)
+ }
+}
+
+watch(messages, () => scrollToBottom(), { deep: true })
+</script>
+
+<style lang="scss">
+.ai-chat-overlay {
+ pointer-events: none !important;
+ background: transparent !important;
+}
+
+.ai-chat-overlay .el-drawer {
+ pointer-events: auto;
+}
+</style>
+
+<style scoped lang="scss">
+$primary-blue: #0055d4;
+$secondary-blue: #2e8ce0;
+$light-blue: #7ab8ff;
+$pale-blue: #c5dcff;
+$ice-white: #e8f2ff;
+$deep-blue: #003b8e;
+$deepest-blue: #002b66;
+$gradient-blue: linear-gradient(145deg, #004fc7 0%, #0066e0 40%, #2580e8 70%, #5a9fe0 100%);
+$gradient-dark: linear-gradient(145deg, #003b8e 0%, #0055d4 50%, #0077e8 100%);
+$gradient-ice: linear-gradient(180deg, #e0ecff 0%, #d4e5ff 50%, #e8f0ff 100%);
+$shadow-blue: 0 8px 40px rgba(0, 85, 212, 0.35);
+$shadow-deep: 0 12px 48px rgba(0, 40, 120, 0.4);
+$shadow-card: 0 6px 24px rgba(0, 51, 136, 0.12);
+
+.ai-chat-sidebar-wrapper {
+ position: static;
+ z-index: 2000;
+ pointer-events: auto;
+
+ :deep(.el-drawer) {
+ pointer-events: auto;
+ }
+}
+
+.ai-chat-trigger {
+ pointer-events: auto;
+ position: fixed;
+ right: 10px;
+ bottom: 12px;
+ width: 40px;
+ height: 40px;
+ background: $gradient-dark;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: 0 8px 18px rgba(0, 68, 170, 0.32), 0 0 0 1px rgba(0, 136, 232, 0.32) inset;
+ transition: transform 0.22s ease, box-shadow 0.22s ease, opacity 0.22s ease;
+ z-index: 1020;
+ opacity: 0.9;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: -4px;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.4), rgba(0, 136, 232, 0.3), rgba(90, 159, 224, 0.2));
+ border-radius: 50%;
+ z-index: -1;
+ filter: blur(10px);
+ opacity: 0.35;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.3) 0%, transparent 50%);
+ pointer-events: none;
+ }
+
+ &:hover {
+ transform: scale(1.05) translateY(-1px);
+ box-shadow: 0 10px 22px rgba(0, 68, 170, 0.38), 0 0 0 1px rgba(0, 136, 232, 0.42) inset;
+
+ .trigger-icon {
+ transform: scale(1.03);
+ filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.42));
+ }
+ }
+
+ .trigger-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ color: #fff;
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2));
+ }
+}
+
+@keyframes triggerPulse {
+ 0%, 100% {
+ box-shadow: $shadow-blue, 0 0 0 2px rgba(0, 85, 212, 0.25) inset, 0 0 20px rgba(0, 119, 232, 0.15);
+ }
+ 50% {
+ box-shadow: $shadow-deep, 0 0 0 3px rgba(0, 136, 232, 0.35) inset, 0 0 40px rgba(0, 136, 232, 0.25);
+ }
+}
+
+@keyframes glowPulse {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.1);
+ }
+}
+
+.ai-chat-drawer {
+ :deep(.el-drawer__body) {
+ padding: 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ }
+ :deep(.el-drawer__header) {
+ margin-bottom: 0 !important;
+ padding: 0 !important;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.12);
+ background: $gradient-dark;
+ color: #fff;
+ }
+}
+
+.drawer-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ padding: 12px 18px;
+ background: $gradient-dark;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: -60%;
+ right: -25%;
+ width: 250px;
+ height: 250px;
+ background: radial-gradient(circle, rgba(0, 136, 232, 0.4) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 4s ease-in-out infinite alternate;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: -40%;
+ left: -15%;
+ width: 200px;
+ height: 200px;
+ background: radial-gradient(circle, rgba(0, 85, 212, 0.3) 0%, transparent 70%);
+ pointer-events: none;
+ animation: headerGlow 5s ease-in-out infinite alternate-reverse;
+ }
+
+ .header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ position: relative;
+ z-index: 1;
+
+ .header-icon {
+ color: rgba(255, 255, 255, 0.95);
+ filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
+ animation: iconFloat 3s ease-in-out infinite;
+ }
+
+ .title {
+ font-size: 17px;
+ font-weight: 600;
+ color: #fff;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
+ letter-spacing: 0.5px;
+ }
+ }
+
+ .header-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ position: relative;
+ z-index: 1;
+
+ .action-divider {
+ width: 1px;
+ height: 16px;
+ background: rgba(255, 255, 255, 0.2);
+ margin: 0 2px;
+ }
+
+ :deep(.el-button) {
+ color: rgba(255, 255, 255, 0.85);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ background: rgba(255, 255, 255, 0.12);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 8px;
+ padding: 8px;
+ height: 32px;
+ width: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ color: #fff;
+ background: rgba(255, 255, 255, 0.25);
+ border-color: rgba(255, 255, 255, 0.3);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &.close-btn {
+ background: rgba(255, 255, 255, 0.1);
+ &:hover {
+ background: rgba(245, 108, 108, 0.8);
+ border-color: rgba(245, 108, 108, 0.5);
+ }
+ }
+ }
+
+ :deep(.header-action-btn) {
+ position: relative;
+ overflow: hidden;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.08));
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.12);
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 55%);
+ pointer-events: none;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -120%;
+ left: -40%;
+ width: 60%;
+ height: 260%;
+ background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.28), transparent);
+ transform: rotate(24deg);
+ opacity: 0;
+ transition: all 0.35s ease;
+ }
+
+ &:hover::after {
+ left: 100%;
+ opacity: 1;
+ }
+ }
+
+ :deep(.header-action-btn--text) {
+ width: auto !important;
+ min-width: 104px;
+ padding: 8px 14px !important;
+ font-size: 14px;
+ font-weight: 600;
+ white-space: nowrap;
+ }
+ }
+
+ .assistant-switcher {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ padding: 0 12px;
+ position: relative;
+ z-index: 1;
+
+ :deep(.el-radio-group) {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ justify-content: center;
+ padding: 4px;
+ border-radius: 999px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.08));
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 10px 18px rgba(0, 0, 0, 0.1);
+ }
+
+ :deep(.el-radio-button__inner) {
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(255, 255, 255, 0.12);
+ color: rgba(255, 255, 255, 0.86);
+ box-shadow: none;
+ padding: 7px 14px;
+ font-weight: 500;
+ }
+
+ :deep(.el-radio-button:first-child .el-radio-button__inner),
+ :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-radius: 999px;
+ }
+
+ :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: #fff;
+ color: $primary-blue;
+ border-color: #fff;
+ box-shadow: 0 6px 14px rgba(0, 40, 120, 0.16);
+ }
+ }
+}
+
+@keyframes headerGlow {
+ 0% {
+ opacity: 0.6;
+ transform: scale(1);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1.15);
+ }
+}
+
+@keyframes iconFloat {
+ 0%, 100% {
+ transform: translateY(0) rotate(0);
+ }
+ 50% {
+ transform: translateY(-2px) rotate(3deg);
+ }
+}
+
+.chat-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ width: 100%;
+ background: $ice-white;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 128px;
+ background: linear-gradient(180deg, rgba(0, 85, 212, 0.06) 0%, transparent 100%);
+ pointer-events: none;
+ }
+}
+
+.history-panel {
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(180deg, #fff 0%, $ice-white 100%);
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ box-shadow: -8px 0 32px rgba(0, 85, 212, 0.15);
+
+ .history-header {
+ padding: 18px 20px;
+ border-bottom: 1px solid rgba(0, 85, 212, 0.12);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ font-size: 14px;
+ color: $deep-blue;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.08) 0%, rgba(0, 136, 232, 0.05) 100%);
+ position: relative;
+
+ &::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.2), transparent);
+ }
+ }
+
+ .session-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 12px 16px;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 6px rgba(0, 85, 212, 0.25);
+ }
+
+ .session-item {
+ display: flex;
+ align-items: center;
+ padding: 14px 16px;
+ margin-bottom: 6px;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ gap: 12px;
+ position: relative;
+ border: 1px solid transparent;
+ background: #fff;
+ animation: sessionSlideIn 0.35s ease;
+
+ @keyframes sessionSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-15px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ &:hover {
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.06) 0%, rgba(0, 136, 232, 0.08) 100%);
+ border-color: rgba(0, 85, 212, 0.12);
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.1);
+ transform: translateX(4px);
+
+ .delete-btn {
+ opacity: 1;
+ transform: scale(1);
+ }
+ }
+
+ &.active {
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.12) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border-color: rgba(0, 85, 212, 0.25);
+ color: $primary-blue;
+ box-shadow: 0 4px 16px rgba(0, 85, 212, 0.15);
+
+ .el-icon {
+ color: $primary-blue;
+ }
+ }
+
+ .el-icon {
+ font-size: 18px;
+ flex-shrink: 0;
+ color: $secondary-blue;
+ transition: color 0.2s;
+ }
+
+ .session-name {
+ flex: 1;
+ font-size: 13px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #1a1a2e;
+ font-weight: 500;
+ }
+
+ .delete-btn {
+ opacity: 0;
+ transform: scale(0.8);
+ transition: all 0.25s ease;
+ padding: 6px;
+ border-radius: 6px;
+ color: #c0c4cc;
+
+ &:hover {
+ color: #fff;
+ background: rgba(245, 108, 108, 0.85);
+ transform: scale(1.1) rotate(8deg);
+ }
+ }
+ }
+ }
+}
+
+.chat-main {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ flex: 1;
+ overflow: hidden;
+}
+
+.message-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 24px 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ background: linear-gradient(180deg, transparent 0%, rgba(0, 85, 212, 0.02) 100%);
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, $secondary-blue, $primary-blue);
+ border-radius: 4px;
+ box-shadow: 0 0 8px rgba(0, 85, 212, 0.3);
+ }
+}
+
+.message-item {
+ display: flex;
+ gap: 14px;
+ width: 100%;
+ animation: messageSlideIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+ @keyframes messageSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+ }
+
+ .avatar {
+ width: 42px;
+ height: 42px;
+ border-radius: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ font-size: 24px;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: inherit;
+ filter: blur(10px);
+ opacity: 0.5;
+ z-index: -1;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -50%;
+ left: -50%;
+ width: 200%;
+ height: 200%;
+ background: linear-gradient(45deg, transparent 40%, rgba(255, 255, 255, 0.2) 50%, transparent 60%);
+ animation: shimmer 3s infinite;
+ }
+ }
+
+ .message-content {
+ flex: 1;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ max-width: calc(100% - 56px);
+
+ .text-box {
+ padding: 14px 20px;
+ border-radius: 18px;
+ font-size: 14px;
+ line-height: 1.7;
+ word-break: break-word;
+ max-width: 100%;
+ width: fit-content;
+ overflow-x: auto;
+ transition: all 0.3s ease;
+ position: relative;
+
+ &::-webkit-scrollbar {
+ height: 4px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: rgba(0, 85, 212, 0.25);
+ border-radius: 2px;
+ }
+ }
+
+ .message-local-file-list {
+ margin-top: 8px;
+ display: grid;
+ gap: 8px;
+ max-width: 100%;
+ }
+
+ .message-local-file-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ border-radius: 10px;
+ border: 1px solid rgba(88, 117, 255, 0.2);
+ background: rgba(255, 255, 255, 0.9);
+ max-width: 100%;
+
+ &.clickable {
+ cursor: pointer;
+ transition: all 0.2s ease;
+
+ &:hover {
+ border-color: rgba(44, 109, 255, 0.38);
+ background: rgba(243, 247, 255, 0.96);
+ }
+ }
+ }
+
+ .message-local-file-thumb {
+ width: 40px;
+ height: 40px;
+ border-radius: 6px;
+ overflow: hidden;
+ flex-shrink: 0;
+ border: 1px solid rgba(124, 148, 255, 0.26);
+ background: #f4f7ff;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .message-local-file-icon {
+ font-size: 20px;
+ color: $primary-blue;
+ flex-shrink: 0;
+ }
+
+ .message-local-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .message-local-file-name {
+ font-size: 12px;
+ color: #1f2a44;
+ font-weight: 600;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ &.clickable {
+ color: $primary-blue;
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .message-local-file-size {
+ font-size: 11px;
+ color: #7f8ba1;
+ line-height: 1.2;
+ }
+ }
+
+ &.bot-message {
+ .message-content {
+ align-items: flex-start;
+ }
+ .avatar {
+ background: $gradient-dark;
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.35);
+ }
+ .text-box {
+ background: #fff;
+ color: #1a1a2e;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 85, 212, 0.08);
+ border-top-left-radius: 6px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, rgba(0, 85, 212, 0.15), transparent);
+ }
+ }
+ }
+
+ &.user-message {
+ flex-direction: row-reverse;
+ .message-content {
+ align-items: flex-end;
+ }
+ .avatar {
+ background: linear-gradient(145deg, #5a9fe0, #3d8bd4);
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
+ }
+ .text-box {
+ background: $gradient-dark;
+ color: #fff;
+ border-top-right-radius: 6px;
+ box-shadow: 0 6px 24px rgba(0, 85, 212, 0.3);
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3));
+ }
+ }
+ }
+}
+
+@keyframes shimmer {
+ 0% {
+ transform: translateX(-100%) rotate(45deg);
+ }
+ 100% {
+ transform: translateX(100%) rotate(45deg);
+ }
+}
+
+.charts-wrapper {
+ margin-top: 12px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ overflow-x: auto;
+ width: 100%;
+ padding-bottom: 8px;
+
+ &::-webkit-scrollbar {
+ height: 6px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
+ border-radius: 3px;
+ }
+}
+
+.chart-item {
+ width: 100%;
+ min-width: 300px;
+ height: 300px;
+ border-radius: 12px;
+ padding: 12px;
+ margin-bottom: 12px;
+}
+
+.chart-empty-state {
+ margin-top: 12px;
+ width: 100%;
+ border-radius: 10px;
+ border: 1px dashed rgba(148, 163, 184, 0.6);
+ background: #f8fafc;
+ color: #64748b;
+ font-size: 13px;
+ line-height: 1.6;
+ padding: 12px;
+}
+
+.table-wrapper {
+ margin-top: 12px;
+ background: #fff;
+ border-radius: 12px;
+ overflow: hidden;
+ overflow-x: auto;
+ width: 100%;
+ box-shadow: $shadow-card;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+
+ &::-webkit-scrollbar {
+ height: 6px;
+ }
+ &::-webkit-scrollbar-thumb {
+ background: linear-gradient(90deg, $light-blue, $secondary-blue);
+ border-radius: 3px;
+ }
+
+ .el-table {
+ min-width: 300px;
+ --el-table-border-color: rgba(0, 122, 255, 0.08);
+ --el-table-header-bg-color: $ice-white;
+ }
+}
+
+.manufacturing-card {
+ margin-top: 12px;
+ width: 100%;
+ background: #fff;
+ border: 1px solid rgba(0, 85, 212, 0.12);
+ border-radius: 12px;
+ box-shadow: $shadow-card;
+ padding: 14px;
+}
+
+.manufacturing-card__title {
+ font-size: 14px;
+ font-weight: 700;
+ color: $deep-blue;
+ margin-bottom: 10px;
+}
+
+.manufacturing-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.manufacturing-summary-item {
+ border-radius: 10px;
+ padding: 10px 12px;
+ border: 1px solid rgba(0, 85, 212, 0.08);
+ background: linear-gradient(180deg, #f8fbff, #f1f7ff);
+ min-height: 66px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 6px;
+}
+
+.manufacturing-summary-item--core {
+ border-color: rgba(30, 91, 255, 0.24);
+}
+
+.manufacturing-summary-label {
+ font-size: 12px;
+ color: #4b5563;
+}
+
+.manufacturing-summary-value {
+ font-size: 15px;
+ color: #1f2937;
+ line-height: 1.4;
+ word-break: break-all;
+}
+
+.manufacturing-warning-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.manufacturing-warning-item {
+ border-radius: 10px;
+ border: 1px solid rgba(245, 158, 11, 0.22);
+ background: linear-gradient(135deg, rgba(255, 247, 237, 0.9), rgba(255, 255, 255, 0.98));
+ padding: 10px 12px;
+}
+
+.manufacturing-warning-item__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: #92400e;
+ font-size: 13px;
+}
+
+.manufacturing-warning-count {
+ margin-left: auto;
+ font-weight: 700;
+ color: #c2410c;
+}
+
+.manufacturing-warning-detail {
+ margin: 8px 0 0;
+ font-size: 12px;
+ line-height: 1.6;
+ color: #7c2d12;
+ word-break: break-all;
+}
+
+.manufacturing-table-wrapper {
+ margin-top: 10px;
+}
+
+.manufacturing-action-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ margin-top: 12px;
+}
+
+.manufacturing-action-card {
+ border: 1px solid rgba(0, 85, 212, 0.1);
+ border-radius: 10px;
+ padding: 10px 12px;
+ background: #f8fbff;
+}
+
+.manufacturing-action-card__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 13px;
+ color: #1f2937;
+ margin-bottom: 8px;
+}
+
+.manufacturing-action-card__meta {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ margin-bottom: 8px;
+ font-size: 12px;
+ color: #64748b;
+ word-break: break-all;
+}
+
+.manufacturing-action-card__desc {
+ margin: 0 0 8px;
+ font-size: 12px;
+ line-height: 1.6;
+ color: #475467;
+}
+
+.manufacturing-required-fields {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+ margin-bottom: 8px;
+ font-size: 12px;
+ color: #7c2d12;
+}
+
+.manufacturing-action-footer {
+ margin-top: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 12px;
+}
+
+.manufacturing-action-result {
+ flex: 1;
+ font-size: 12px;
+ line-height: 1.5;
+
+ &.success {
+ color: #1f9d55;
+ }
+
+ &.error {
+ color: #d93025;
+ }
+}
+
+.sales-structured-card {
+ margin-top: 12px;
+ width: 100%;
+ background: #fff;
+ border: 1px solid rgba(31, 122, 114, 0.2);
+ border-radius: 12px;
+ box-shadow: $shadow-card;
+ padding: 14px;
+}
+
+.sales-structured-card__title {
+ font-size: 14px;
+ font-weight: 700;
+ color: #1f5ddf;
+ margin-bottom: 10px;
+}
+
+.sales-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
+ gap: 8px;
+ margin-bottom: 12px;
+}
+
+.sales-summary-item {
+ border-radius: 10px;
+ padding: 10px 12px;
+ border: 1px solid rgba(30, 91, 255, 0.12);
+ background: linear-gradient(180deg, #f7fbff, #edf6ff);
+ min-height: 66px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 6px;
+}
+
+.sales-summary-label {
+ font-size: 12px;
+ color: #4b5563;
+}
+
+.sales-summary-value {
+ font-size: 15px;
+ color: #1f2937;
+ line-height: 1.4;
+ word-break: break-all;
+}
+
+.sales-focus-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.sales-focus-item {
+ border-radius: 10px;
+ border: 1px solid rgba(30, 91, 255, 0.14);
+ background: #f8fbff;
+ padding: 10px 12px;
+}
+
+.sales-focus-item--strategy {
+ border-color: rgba(31, 122, 114, 0.22);
+ background: linear-gradient(180deg, #f7fcfb, #edf9f6);
+}
+
+.sales-focus-item__head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ font-size: 13px;
+ color: #1f2937;
+}
+
+.sales-focus-tags {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+.sales-focus-metrics {
+ margin-top: 8px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ font-size: 12px;
+ color: #475467;
+}
+
+.sales-focus-reasons {
+ margin-top: 8px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.sales-strategy-line {
+ margin: 8px 0 0;
+ font-size: 12px;
+ line-height: 1.6;
+ color: #334155;
+}
+
+.sales-section-title {
+ margin: 4px 0 8px;
+ font-size: 13px;
+ font-weight: 700;
+ color: $deep-blue;
+}
+
+.finance-headline {
+ margin-top: 4px;
+ font-size: 13px;
+ line-height: 1.7;
+ color: #344054;
+}
+
+.finance-text-section {
+ margin-top: 10px;
+
+ ul {
+ margin: 6px 0 0;
+ padding-left: 18px;
+ font-size: 13px;
+ line-height: 1.7;
+ color: #344054;
+ }
+}
+
+.purchase-intent-quick-prompt-wrap {
+ margin-top: 12px;
+}
+
+.purchase-intent-quick-prompt-title {
+ font-size: 12px;
+ color: #4b5563;
+}
+
+.purchase-intent-quick-prompt-list {
+ margin-top: 8px;
+}
+
+.purchase-confirm-card {
+ margin-top: 12px;
+ width: 100%;
+ background: #fff;
+ border: 1px solid rgba(0, 85, 212, 0.12);
+ border-radius: 12px;
+ box-shadow: $shadow-card;
+ padding: 14px;
+ color: #1a1a2e;
+}
+
+.purchase-confirm-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 15px;
+ font-weight: 700;
+ margin-bottom: 12px;
+}
+
+.purchase-confirm-desc {
+ margin-bottom: 12px;
+ color: #374151;
+ font-size: 13px;
+ line-height: 1.6;
+}
+
+.purchase-empty-state {
+ margin-bottom: 12px;
+ padding: 12px;
+ border-radius: 10px;
+ background: linear-gradient(135deg, rgba(255, 247, 237, 0.96), rgba(255, 255, 255, 0.98));
+ border: 1px solid rgba(245, 158, 11, 0.25);
+
+ .empty-title {
+ font-size: 14px;
+ font-weight: 700;
+ color: #92400e;
+ margin-bottom: 6px;
+ }
+
+ .empty-desc {
+ color: #78350f;
+ font-size: 13px;
+ line-height: 1.6;
+ }
+}
+
+.purchase-alert {
+ border-radius: 8px;
+ padding: 10px 12px;
+ margin-bottom: 10px;
+ font-size: 13px;
+
+ ul {
+ margin: 6px 0 0;
+ padding-left: 18px;
+ }
+
+ &.warning {
+ background: rgba(230, 162, 60, 0.12);
+ color: #9a5b00;
+ }
+
+ &.missing {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ background: rgba(245, 108, 108, 0.1);
+ color: #b42318;
+ }
+}
+
+.purchase-preview {
+ margin-bottom: 12px;
+
+ ul {
+ margin: 6px 0 0;
+ padding-left: 18px;
+ font-size: 13px;
+ line-height: 1.7;
+ }
+}
+
+.purchase-section-title {
+ margin: 10px 0 6px;
+ font-size: 13px;
+ font-weight: 700;
+ color: $deep-blue;
+}
+
+.payload-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 8px;
+}
+
+.payload-tree-table-wrapper {
+ border: 1px solid rgba(0, 85, 212, 0.1);
+ border-radius: 10px;
+ overflow: auto;
+
+ :deep(.el-table) {
+ --el-table-header-bg-color: #f5f8ff;
+ --el-table-border-color: rgba(0, 85, 212, 0.08);
+ }
+}
+
+.payload-key-cell {
+ display: flex;
+ align-items: center;
+ min-height: 28px;
+}
+
+.payload-fixed-key {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ line-height: 1.3;
+ color: #1f2937;
+
+ small {
+ font-size: 11px;
+ color: #6b7280;
+ }
+}
+
+.payload-array-index {
+ font-size: 12px;
+ color: #475467;
+}
+
+.payload-container-cell {
+ color: #344054;
+ font-size: 12px;
+}
+
+.payload-null-value {
+ color: #6b7280;
+ font-size: 12px;
+}
+
+.payload-row-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+}
+
+.payload-editor-tip {
+ margin-top: 6px;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #6b7280;
+}
+
+.purchase-confirm-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 12px;
+ margin-top: 12px;
+
+ .confirm-result {
+ flex: 1;
+ font-size: 13px;
+
+ &.success {
+ color: #1f9d55;
+ }
+
+ &.error {
+ color: #d93025;
+ }
+ }
+}
+
+.input-area {
+ padding: 18px 20px;
+ background: linear-gradient(180deg, rgba(232, 242, 255, 0.95) 0%, #fff 100%);
+ border-top: 1px solid rgba(0, 85, 212, 0.1);
+ position: relative;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 20px;
+ right: 20px;
+ height: 1px;
+ background: linear-gradient(90deg, transparent, rgba(0, 85, 212, 0.15), transparent);
+ }
+
+ .input-actions {
+ display: flex;
+ gap: 14px;
+ margin-bottom: 12px;
+ align-items: center;
+
+ .file-upload-trigger {
+ display: inline-flex;
+ align-items: center;
+ }
+
+ :deep(.utility-action-btn) {
+ position: relative;
+ height: 34px;
+ padding: 0 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(92, 119, 255, 0.18);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(236, 243, 255, 0.98));
+ color: $primary-blue;
+ font-weight: 600;
+ box-shadow: 0 10px 20px rgba(0, 85, 212, 0.08);
+ transition: all 0.25s ease;
+
+ .el-icon {
+ margin-right: 5px;
+ }
+
+ &:hover:not(.is-disabled) {
+ color: #fff;
+ border-color: transparent;
+ background: linear-gradient(135deg, #1f6dff 0%, #6b38ef 100%);
+ box-shadow: 0 14px 24px rgba(64, 90, 255, 0.2);
+ transform: translateY(-1px);
+ }
+ }
+
+ :deep(.stop-action-btn) {
+ border-color: rgba(255, 99, 123, 0.18);
+ color: #d33e5e;
+
+ &:hover:not(.is-disabled) {
+ background: linear-gradient(135deg, #f5536e 0%, #a33cff 100%);
+ }
+ }
+ }
+
+ .input-box {
+ padding: 16px;
+ position: relative;
+ background: #fff;
+ border: 2px solid rgba(0, 85, 212, 0.12);
+ border-radius: 16px;
+ margin: 0 4px;
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+
+ &:focus-within {
+ border-color: $primary-blue;
+ box-shadow: 0 0 0 4px rgba(0, 85, 212, 0.12), $shadow-deep;
+ transform: translateY(-2px);
+ background: #fff;
+ }
+
+ .selected-file-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 12px;
+ }
+
+ .selected-file-tag {
+ display: flex;
+ align-items: center;
+ background: linear-gradient(135deg, rgba(0, 85, 212, 0.1) 0%, rgba(0, 136, 232, 0.15) 100%);
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ border-radius: 10px;
+ padding: 8px 12px;
+ gap: 10px;
+ width: fit-content;
+ max-width: 100%;
+ animation: tagSlideIn 0.3s ease;
+
+ @keyframes tagSlideIn {
+ from {
+ opacity: 0;
+ transform: translateX(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(0);
+ }
+ }
+
+ .el-icon {
+ color: $primary-blue;
+ font-size: 18px;
+ }
+
+ .selected-file-thumb {
+ width: 30px;
+ height: 30px;
+ border-radius: 6px;
+ overflow: hidden;
+ border: 1px solid rgba(0, 85, 212, 0.2);
+ flex-shrink: 0;
+ cursor: zoom-in;
+
+ :deep(.el-image__inner) {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .selected-file-meta {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .file-name {
+ font-size: 13px;
+ color: $deep-blue;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-weight: 600;
+ }
+
+ .file-size {
+ font-size: 11px;
+ color: #5f86b4;
+ line-height: 1.1;
+ }
+
+ .remove-file {
+ cursor: pointer;
+ color: $secondary-blue;
+ transition: all 0.2s;
+ padding: 4px;
+ border-radius: 50%;
+
+ &:hover {
+ color: #fff;
+ background: rgba(245, 108, 108, 0.8);
+ transform: scale(1.1) rotate(90deg);
+ }
+ }
+ }
+
+ :deep(.el-textarea__inner) {
+ padding: 0;
+ padding-bottom: 35px;
+ border: none;
+ box-shadow: none;
+ background: transparent;
+ font-family: inherit;
+ font-size: 14px;
+ line-height: 1.6;
+ color: #1a1a2e;
+
+ &::placeholder {
+ color: #7ab8ff;
+ }
+
+ &:focus {
+ box-shadow: none;
+ }
+ }
+
+ .send-btn {
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ padding: 10px 22px;
+ background: $gradient-dark;
+ border: none;
+ border-radius: 10px;
+ font-weight: 600;
+ font-size: 14px;
+ color: #fff;
+ box-shadow: 0 6px 20px rgba(0, 85, 212, 0.4);
+ transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+ overflow: hidden;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ letter-spacing: 0.3px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transition: left 0.5s;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-3px) scale(1.02);
+ box-shadow: 0 10px 30px rgba(0, 85, 212, 0.5);
+
+ &::before {
+ left: 100%;
+ }
+ }
+
+ &:active:not(:disabled) {
+ transform: translateY(-1px) scale(0.98);
+ }
+
+ &:disabled {
+ background: linear-gradient(145deg, #b0b0b0, #c5c5c5);
+ box-shadow: none;
+ cursor: not-allowed;
+ }
+
+ .el-icon {
+ font-size: 15px;
+ transform: translateY(-1px);
+ }
+ }
+ }
+}
+
+.typing-indicator {
+ display: flex;
+ gap: 5px;
+ padding: 10px 14px;
+ background: #fff;
+ border-radius: 14px;
+ width: fit-content;
+ box-shadow: $shadow-card;
+ margin-top: 6px;
+ border: 1px solid rgba(0, 122, 255, 0.06);
+ border-top-left-radius: 4px;
+
+ .dot {
+ width: 7px;
+ height: 7px;
+ background: $secondary-blue;
+ border-radius: 50%;
+ animation: typing 1.4s infinite ease-in-out;
+
+ &:nth-child(2) {
+ animation-delay: 0.2s;
+ background: $primary-blue;
+ }
+ &:nth-child(3) {
+ animation-delay: 0.4s;
+ background: $deep-blue;
+ }
+ }
+}
+
+@keyframes typing {
+ 0%, 80%, 100% {
+ transform: scale(0.6);
+ opacity: 0.4;
+ }
+ 40% {
+ transform: scale(1);
+ opacity: 1;
+ }
+}
+
+.code-block {
+ background: linear-gradient(145deg, #1a1a2e, #16213e);
+ color: #a8d8ff;
+ padding: 14px;
+ border-radius: 10px;
+ font-family: 'Fira Code', 'Consolas', monospace;
+ margin: 10px 0;
+ overflow-x: auto;
+ border: 1px solid rgba(90, 200, 250, 0.15);
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
+
+ &.js-code {
+ color: #5ac8fa;
+ }
+}
+
+.chat-main {
+ background:
+ radial-gradient(circle at top left, rgba(46, 140, 224, 0.12) 0%, transparent 34%),
+ linear-gradient(180deg, #fff 0%, #f7fbff 46%, #fff 100%);
+}
+
+.chat-hero {
+ display: grid;
+ grid-template-columns: 176px minmax(0, 1fr);
+ gap: 14px;
+ align-items: stretch;
+ padding: 8px 18px 4px;
+
+ &.compact {
+ grid-template-columns: 132px minmax(0, 1fr);
+ gap: 10px;
+ padding: 4px 18px 2px;
+ }
+}
+
+.assistant-stand {
+ position: relative;
+ min-height: 206px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ padding-top: 8px;
+ overflow: hidden;
+
+ &.compact {
+ min-height: 160px;
+ padding-top: 4px;
+ }
+
+ &.thinking {
+ .assistant-halo {
+ opacity: 1;
+ transform: scale(1.12);
+ filter: blur(9px);
+ }
+
+ .assistant-scan-ring {
+ opacity: 0.95;
+ animation-duration: 1.5s;
+ }
+
+ .assistant-orbit {
+ opacity: 0.76;
+ }
+
+ .assistant-model-shell {
+ transform: translateY(-5px) scale(1.02);
+ }
+
+ .assistant-model-cut {
+ animation-duration: 2.2s;
+ }
+
+ .assistant-model-img {
+ filter: saturate(1.06) drop-shadow(0 18px 20px rgba(22, 48, 80, 0.22));
+ }
+
+ .assistant-status {
+ color: #6a3bee;
+ box-shadow: 0 10px 22px rgba(106, 59, 238, 0.14);
+ }
+
+ .assistant-status-dot {
+ background: #6a3bee;
+ box-shadow: 0 0 12px rgba(106, 59, 238, 0.9);
+ animation: thinkingDot 1s ease-in-out infinite;
+ }
+
+ .assistant-base-lg {
+ animation-duration: 1.8s;
+ }
+
+ .assistant-base-md {
+ animation-duration: 1.5s;
+ }
+
+ .assistant-base-sm {
+ box-shadow: 0 0 24px rgba(30, 91, 255, 0.36);
+ animation-duration: 1.25s;
+ }
+ }
+}
+
+.assistant-halo {
+ position: absolute;
+ top: 24px;
+ width: 146px;
+ height: 146px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(31, 122, 114, 0.26) 0%, rgba(30, 91, 255, 0.2) 42%, rgba(109, 65, 237, 0.12) 66%, transparent 80%);
+ filter: blur(6px);
+ opacity: 0.78;
+ transition: all 0.35s ease;
+}
+
+.assistant-scan-ring {
+ position: absolute;
+ top: 44px;
+ width: 136px;
+ height: 136px;
+ border-radius: 50%;
+ border: 1px solid rgba(67, 145, 223, 0.24);
+ box-shadow: inset 0 0 16px rgba(255, 255, 255, 0.25);
+ opacity: 0.52;
+ animation: scanRing 4s linear infinite;
+}
+
+.assistant-orbit {
+ position: absolute;
+ top: 52px;
+ width: 156px;
+ height: 156px;
+ border-radius: 50%;
+ border: 1px dashed rgba(92, 135, 255, 0.24);
+ opacity: 0.42;
+}
+
+.assistant-orbit-a {
+ animation: orbitRotate 8.6s linear infinite;
+}
+
+.assistant-orbit-b {
+ width: 124px;
+ height: 124px;
+ top: 68px;
+ border-color: rgba(31, 122, 114, 0.24);
+ animation: orbitRotateReverse 6.2s linear infinite;
+}
+
+.assistant-model-shell {
+ position: relative;
+ z-index: 1;
+ width: 148px;
+ height: 178px;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ margin-top: 4px;
+ transition: transform 0.35s ease;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ bottom: 2px;
+ width: 164px;
+ height: 42px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ background: radial-gradient(
+ ellipse at center,
+ rgba(43, 126, 211, 0.32) 0%,
+ rgba(43, 126, 211, 0.14) 46%,
+ rgba(43, 126, 211, 0) 74%
+ );
+ filter: blur(2.6px);
+ animation: baseGlow 4.6s ease-in-out infinite;
+ z-index: 1;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ left: 50%;
+ bottom: 10px;
+ width: 138px;
+ height: 28px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ border: 1px solid rgba(36, 116, 198, 0.6);
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.58),
+ 0 0 22px rgba(42, 116, 196, 0.24);
+ animation: basePulse 3.2s ease-in-out infinite;
+ z-index: 4;
+ }
+}
+
+.assistant-model-cut {
+ position: relative;
+ width: 132px;
+ height: 178px;
+ z-index: 6;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ transform-origin: center 84%;
+ animation: avatarFloat 3.2s ease-in-out infinite;
+}
+
+.assistant-model-img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ object-position: center bottom;
+ display: block;
+ filter: saturate(1.03) drop-shadow(0 14px 18px rgba(22, 49, 79, 0.2));
+ transition: filter 0.35s ease;
+}
+
+.assistant-model-fallback {
+ width: 92px;
+ height: 92px;
+ border-radius: 24px;
+ color: #fff;
+ background: linear-gradient(145deg, rgba(31, 122, 114, 0.9), rgba(30, 91, 255, 0.9));
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ box-shadow: 0 12px 24px rgba(31, 85, 173, 0.22);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.assistant-base {
+ position: absolute;
+ left: 50%;
+ bottom: 8px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ border: 1px solid rgba(36, 116, 198, 0.28);
+ background: radial-gradient(
+ ellipse at center,
+ rgba(255, 255, 255, 0.94) 0%,
+ rgba(81, 164, 233, 0.16) 58%,
+ rgba(30, 91, 255, 0.06) 100%
+ );
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
+}
+
+.assistant-status {
+ position: relative;
+ z-index: 1;
+ margin-top: 7px;
+ padding: 5px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 600;
+ color: $deep-blue;
+ background: rgba(255, 255, 255, 0.95);
+ border: 1px solid rgba(0, 85, 212, 0.12);
+ box-shadow: 0 8px 20px rgba(0, 85, 212, 0.08);
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.assistant-status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: #2e8ce0;
+ box-shadow: 0 0 10px rgba(46, 140, 224, 0.72);
+}
+
+.assistant-base {
+ pointer-events: none;
+}
+
+.assistant-base-md {
+ bottom: 15px;
+ width: 104px;
+ height: 22px;
+ border-color: rgba(36, 116, 198, 0.48);
+ animation: basePulse 2.8s ease-in-out infinite;
+}
+
+.assistant-base-sm {
+ bottom: 20px;
+ width: 68px;
+ height: 14px;
+ background: linear-gradient(90deg, rgba(31, 122, 114, 0.82), rgba(45, 124, 255, 0.9));
+ border: none;
+ box-shadow: 0 0 18px rgba(45, 124, 255, 0.34);
+ animation: basePulse 2.2s ease-in-out infinite;
+}
+
+@keyframes orbitRotate {
+ from {
+ transform: rotate(0deg);
+ }
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes orbitRotateReverse {
+ from {
+ transform: rotate(360deg);
+ }
+ to {
+ transform: rotate(0deg);
+ }
+}
+
+@keyframes scanRing {
+ 0%, 100% {
+ transform: scale(0.96);
+ opacity: 0.42;
+ }
+ 50% {
+ transform: scale(1.04);
+ opacity: 0.86;
+ }
+}
+
+@keyframes thinkingDot {
+ 0%, 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.35);
+ }
+}
+
+.assistant-base-lg {
+ width: 142px;
+ height: 32px;
+ animation: basePulse 3.4s ease-in-out infinite;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 130px;
+ height: 130px;
+ transform: translate(-50%, -50%);
+ border-radius: 50%;
+ background: conic-gradient(
+ from 180deg,
+ transparent 0deg,
+ rgba(36, 116, 198, 0.65) 48deg,
+ transparent 114deg,
+ rgba(36, 116, 198, 0.55) 212deg,
+ transparent 286deg,
+ rgba(31, 122, 114, 0.45) 334deg,
+ transparent 360deg
+ );
+ -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ opacity: 0.62;
+ animation: baseSpin 9s linear infinite;
+ }
+}
+
+@keyframes avatarFloat {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-7px);
+ }
+}
+
+@keyframes basePulse {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scale(1);
+ opacity: 0.88;
+ }
+ 50% {
+ transform: translateX(-50%) scale(1.05);
+ opacity: 0.98;
+ }
+}
+
+@keyframes baseSpin {
+ from {
+ transform: translate(-50%, -50%) rotate(0deg);
+ }
+ to {
+ transform: translate(-50%, -50%) rotate(360deg);
+ }
+}
+
+@keyframes baseGlow {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scaleX(1);
+ opacity: 0.82;
+ }
+ 50% {
+ transform: translateX(-50%) scaleX(1.06);
+ opacity: 0.96;
+ }
+}
+
+.welcome-card {
+ position: relative;
+ align-self: stretch;
+ min-height: 206px;
+ padding: 9px 10px 8px;
+ border-radius: 16px;
+ background:
+ linear-gradient(#fff, #fff) padding-box,
+ linear-gradient(135deg, rgba(255, 64, 96, 0.85), rgba(117, 65, 255, 0.9)) border-box;
+ border: 1px solid transparent;
+ box-shadow: 0 16px 36px rgba(0, 85, 212, 0.12);
+
+ &.compact {
+ min-height: 160px;
+ padding: 8px 9px 7px;
+ border-radius: 12px;
+ box-shadow: 0 8px 16px rgba(0, 85, 212, 0.07);
+
+ .welcome-eyebrow {
+ margin-bottom: 4px;
+ }
+
+ .welcome-title {
+ font-size: 16px;
+ line-height: 1.25;
+
+ br {
+ display: none;
+ }
+ }
+
+ .welcome-desc {
+ margin-top: 4px;
+ font-size: 11px;
+ line-height: 1.5;
+ }
+
+ .quick-prompt-list {
+ margin-top: 8px;
+ gap: 5px;
+ }
+
+ .quick-prompt-btn {
+ padding: 7px 9px;
+ font-size: 11px;
+ border-radius: 7px;
+ }
+
+ .more-prompts-btn {
+ margin-top: 6px;
+ font-size: 11px;
+ }
+ }
+}
+
+.welcome-eyebrow {
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 2px;
+ color: rgba(0, 85, 212, 0.58);
+ margin-bottom: 5px;
+}
+
+.welcome-title {
+ margin: 0;
+ font-size: 20px;
+ line-height: 1.15;
+ font-weight: 800;
+ color: #172033;
+
+ br {
+ display: none;
+ }
+}
+
+.welcome-desc {
+ margin: 5px 0 0;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #5f6980;
+}
+
+.quick-prompt-list {
+ display: grid;
+ gap: 6px;
+ margin-top: 8px;
+}
+
+.quick-prompt-btn {
+ width: 100%;
+ border: none;
+ border-radius: 9px;
+ padding: 7px 10px;
+ text-align: left;
+ font-size: 12px;
+ font-weight: 600;
+ color: #fff;
+ cursor: pointer;
+ background: linear-gradient(90deg, #ff4c55 0%, #7c38ef 100%);
+ box-shadow: 0 12px 22px rgba(124, 56, 239, 0.18);
+ transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.2s ease;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.22), transparent 56%);
+ pointer-events: none;
+ }
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: -120%;
+ left: -30%;
+ width: 45%;
+ height: 260%;
+ background: linear-gradient(180deg, transparent, rgba(255, 255, 255, 0.3), transparent);
+ transform: rotate(22deg);
+ opacity: 0;
+ transition: all 0.35s ease;
+ }
+
+ &:hover:not(:disabled) {
+ transform: translateY(-2px) scale(1.01);
+ box-shadow: 0 16px 28px rgba(124, 56, 239, 0.24);
+
+ &::after {
+ left: 100%;
+ opacity: 1;
+ }
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.65;
+ }
+}
+
+.more-prompts-btn {
+ margin-top: 6px;
+ padding: 0 10px;
+ height: 26px;
+ border: 1px solid rgba(208, 65, 81, 0.12);
+ border-radius: 999px;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 241, 245, 0.96));
+ color: #d04151;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ box-shadow: 0 10px 18px rgba(208, 65, 81, 0.08);
+ transition: all 0.25s ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ background: linear-gradient(135deg, #ff5570 0%, #8a3df6 100%);
+ border-color: transparent;
+ color: #fff;
+ box-shadow: 0 14px 24px rgba(138, 61, 246, 0.18);
+ }
+}
+
+.message-list {
+ padding: 8px 18px 18px;
+ gap: 16px;
+ background: transparent;
+}
+
+.input-area {
+ padding: 12px 18px 16px;
+ background: #fff;
+ border-top: none;
+
+ &::before {
+ display: none;
+ }
+
+ .input-box {
+ padding: 14px 16px 16px;
+ border: 1px solid rgba(123, 56, 239, 0.9);
+ border-radius: 22px;
+ margin: 0;
+ transition: all 0.25s ease;
+ box-shadow: 0 14px 34px rgba(0, 85, 212, 0.08);
+
+ &:focus-within {
+ border-color: #7c38ef;
+ box-shadow: 0 0 0 3px rgba(124, 56, 239, 0.1), 0 18px 40px rgba(0, 85, 212, 0.12);
+ transform: none;
+ }
+
+ :deep(.el-textarea__inner) {
+ padding-right: 58px;
+ padding-bottom: 0;
+ min-height: 104px;
+
+ &::placeholder {
+ color: #a0a9bc;
+ }
+ }
+
+ .send-btn {
+ right: 25px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 36px;
+ min-width: 36px;
+ height: 36px;
+ padding: 0;
+ background: linear-gradient(135deg, #ff5570 0%, #7a36f2 58%, #2d79ff 100%);
+ border-radius: 50%;
+ box-shadow: 0 12px 24px rgba(109, 50, 236, 0.24);
+ transition: all 0.25s ease;
+ gap: 0;
+
+ &:hover:not(:disabled) {
+ transform: translateY(calc(-50% - 1px)) scale(1.04);
+ box-shadow: 0 16px 28px rgba(109, 50, 236, 0.3);
+ }
+
+ &:active:not(:disabled) {
+ transform: translateY(-50%) scale(0.96);
+ }
+
+ .el-icon {
+ margin: 0;
+ font-size: 16px;
+ transform: translate(0, -1px);
+ }
+ }
+ }
+}
+
+@media (max-width: 767px) {
+ .chat-hero {
+ grid-template-columns: 1fr;
+ gap: 10px;
+ padding: 14px 14px 6px;
+
+ &.compact {
+ padding: 8px 14px 4px;
+ }
+ }
+
+ .assistant-stand {
+ min-height: 184px;
+ }
+
+ .welcome-card {
+ padding: 12px 12px 10px;
+ }
+
+ .welcome-title {
+ font-size: 21px;
+ }
+
+ .message-list {
+ padding: 8px 14px 14px;
+ }
+
+ .input-area {
+ padding: 10px 14px 14px;
+ }
+
+ .input-area .input-actions {
+ gap: 10px;
+ flex-wrap: wrap;
+ }
+}
+</style>
diff --git a/src/components/AttachmentPreview/image/index.vue b/src/components/AttachmentPreview/image/index.vue
new file mode 100644
index 0000000..1c6039b
--- /dev/null
+++ b/src/components/AttachmentPreview/image/index.vue
@@ -0,0 +1,76 @@
+<script setup>
+const props = defineProps({
+ fileList: {
+ type: Array,
+ default: () => [],
+ },
+ thumbSize: {
+ type: Number,
+ default: 72,
+ },
+ gap: {
+ type: Number,
+ default: 10,
+ },
+})
+
+const normalizedList = computed(() => {
+ return (props.fileList || [])
+ .filter((item) => item && item.previewURL)
+ .map((item, index) => ({
+ id: item.id ?? index,
+ name: item.originalFilename || `image-${index + 1}`,
+ url: item.previewURL,
+ }))
+})
+const previewUrls = computed(() => normalizedList.value.map((item) => item.url))
+</script>
+
+<template>
+ <div class="attachment-image-preview">
+ <div v-if="!normalizedList.length" class="empty">鏆傛棤鍥剧墖</div>
+
+ <div v-else class="thumbs" :style="{ gap: `${gap}px` }">
+ <el-image
+ v-for="(item, index) in normalizedList"
+ :key="item.id"
+ class="thumb"
+ :style="{ width: `${thumbSize}px`, height: `${thumbSize}px` }"
+ :src="item.url"
+ :preview-src-list="previewUrls"
+ :initial-index="index"
+ fit="cover"
+ preview-teleported
+ />
+ </div>
+ </div>
+</template>
+
+<style scoped lang="scss">
+.attachment-image-preview {
+ width: 100%;
+}
+
+.empty {
+ height: 120px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--el-text-color-secondary);
+ border: 1px dashed var(--el-border-color);
+ border-radius: 8px;
+}
+
+.thumbs {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.thumb {
+ border: 1px solid var(--el-border-color);
+ border-radius: 6px;
+ overflow: hidden;
+ cursor: pointer;
+ background: #fff;
+}
+</style>
diff --git a/src/components/AttachmentUpload/file/index.vue b/src/components/AttachmentUpload/file/index.vue
new file mode 100644
index 0000000..1e4508c
--- /dev/null
+++ b/src/components/AttachmentUpload/file/index.vue
@@ -0,0 +1,309 @@
+<script setup>
+import { UploadFilled } from '@element-plus/icons-vue'
+import { uploadFile } from '@/api/basicData/common'
+
+const props = defineProps({
+ fileList: {
+ type: Array,
+ default: () => [],
+ },
+ index: {
+ type: Number,
+ default: -1,
+ },
+ childrenKey: {
+ type: String,
+ default: 'files',
+ },
+ limit: {
+ type: Number,
+ default: 10,
+ },
+ fileSize: {
+ type: Number,
+ default: 50,
+ },
+ fileType: {
+ type: Array,
+ default: () => [],
+ },
+ buttonText: {
+ type: String,
+ default: '鍗曞嚮閫夋嫨鏂囦欢',
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ uploadFieldName: {
+ type: String,
+ default: 'files',
+ },
+})
+
+const emit = defineEmits(['update:fileList', 'change'])
+const { proxy } = getCurrentInstance()
+
+const uploadRef = ref()
+const uploadQueueTimer = ref(null)
+const uploading = ref(false)
+const queuedUidSet = ref(new Set())
+const innerList = ref([])
+
+function readListFromProps() {
+ if (props.index > -1) {
+ const row = props.fileList?.[props.index]
+ return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : []
+ }
+ return Array.isArray(props.fileList) ? props.fileList : []
+}
+
+watch(
+ () => props.fileList,
+ () => {
+ innerList.value = [...readListFromProps()]
+ },
+ { deep: true, immediate: true },
+)
+
+const currentList = computed({
+ get() {
+ return innerList.value
+ },
+ set(value) {
+ const nextList = Array.isArray(value) ? value : []
+ innerList.value = nextList
+
+ if (props.index > -1) {
+ const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : []
+ const currentRow = nextModelValue[props.index] || {}
+ nextModelValue[props.index] = {
+ ...currentRow,
+ [props.childrenKey]: nextList,
+ }
+ emit('update:fileList', nextModelValue)
+ emit('change', nextList, nextModelValue)
+ return
+ }
+
+ emit('update:fileList', nextList)
+ emit('change', nextList, nextList)
+ },
+})
+
+const displayFileList = computed(() => {
+ return currentList.value.map((item, index) => ({
+ uid: getItemUid(item, index),
+ name: getItemName(item, index),
+ url: getItemUrl(item),
+ status: 'success',
+ rawData: item,
+ }))
+})
+
+const uploadTip = computed(() => {
+ if (!props.fileType.length) return `鍗曚釜鏂囦欢涓嶈秴杩� ${props.fileSize}MB`
+ return `鏀寔 ${props.fileType.join('/')}锛屽崟涓枃浠朵笉瓒呰繃 ${props.fileSize}MB`
+})
+
+function getItemUid(item, index) {
+ if (item?.id !== undefined && item?.id !== null) return `${item.id}`
+ return `${getItemName(item, index)}-${getItemUrl(item) || index}`
+}
+
+function getItemUrl(item) {
+ if (!item) return ''
+ if (typeof item === 'string') return item
+ return item.url || item.downloadURL || item.previewURL || item.previewUrl || ''
+}
+
+function getItemName(item, index = 0) {
+ if (!item) return `file-${index + 1}`
+ if (typeof item === 'string') return `file-${index + 1}`
+ return item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${index + 1}`
+}
+
+function normalizeResponseItem(item, index) {
+ if (typeof item === 'string') {
+ return {
+ name: `file-${currentList.value.length + index + 1}`,
+ url: item,
+ }
+ }
+ return Object.assign({}, item, {
+ url: item.url || item.downloadURL || item.previewURL || item.previewUrl || '',
+ name: item.name || item.originalFilename || item.fileName || item.uidFilename || `file-${currentList.value.length + index + 1}`,
+ })
+}
+
+function extractResponseArray(response) {
+ if (Array.isArray(response)) return response
+ if (Array.isArray(response?.data)) return response.data
+ if (Array.isArray(response?.data?.data)) return response.data.data
+ if (Array.isArray(response?.payload)) return response.payload
+ if (Array.isArray(response?.payload?.data)) return response.payload.data
+ if (Array.isArray(response?.rows)) return response.rows
+ if (Array.isArray(response?.result)) return response.result
+ return []
+}
+
+function validateFile(rawFile) {
+ const extension = rawFile.name.includes('.')
+ ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase()
+ : ''
+
+ if (props.fileType.length) {
+ const isValidType = props.fileType.some((type) => {
+ const normalizedType = String(type).toLowerCase()
+ return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType
+ })
+ if (!isValidType) {
+ proxy.$modal.msgError(`璇蜂笂浼� ${props.fileType.join('/')} 鏍煎紡鐨勬枃浠禶)
+ return false
+ }
+ }
+
+ const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize
+ if (!isWithinSize) {
+ proxy.$modal.msgError(`鏂囦欢澶у皬涓嶈兘瓒呰繃 ${props.fileSize}MB`)
+ return false
+ }
+
+ return true
+}
+
+function scheduleUpload(uploadFiles) {
+ clearTimeout(uploadQueueTimer.value)
+ uploadQueueTimer.value = setTimeout(() => {
+ const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid))
+ if (!readyFiles.length) return
+
+ const remainCount = props.limit - currentList.value.length
+ if (remainCount <= 0) {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 涓枃浠禶)
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const selectedFiles = readyFiles.slice(0, remainCount)
+ if (selectedFiles.length < readyFiles.length) {
+ proxy.$modal.msgWarning(`鏈�澶氫笂浼� ${props.limit} 涓枃浠讹紝瓒呭嚭閮ㄥ垎宸插拷鐣)
+ }
+
+ selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid))
+ uploadSelectedFiles(selectedFiles)
+ }, 0)
+}
+
+async function uploadSelectedFiles(files) {
+ const validFiles = files.filter((file) => validateFile(file.raw))
+ const invalidFiles = files.filter((file) => !validFiles.includes(file))
+
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+
+ if (!validFiles.length) {
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const formData = new FormData()
+ validFiles.forEach((file) => {
+ formData.append(props.uploadFieldName, file.raw)
+ })
+
+ uploading.value = true
+ proxy.$modal.loading('鏂囦欢涓婁紶涓紝璇风◢鍊�...')
+
+ try {
+ const response = await uploadFile(formData)
+ const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index))
+
+ if (!responseList.length) {
+ proxy.$modal.msgError('涓婁紶鎺ュ彛鏈繑鍥炴暟缁勬暟鎹�')
+ return
+ }
+
+ currentList.value = [...currentList.value, ...responseList]
+ proxy.$modal.msgSuccess('涓婁紶鎴愬姛')
+ } catch (error) {
+ proxy.$modal.msgError(error?.message || '涓婁紶澶辫触')
+ } finally {
+ validFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ uploadRef.value?.clearFiles()
+ uploading.value = false
+ proxy.$modal.closeLoading()
+ }
+}
+
+function handleChange(file, uploadFiles) {
+ if (props.disabled || uploading.value) return
+ scheduleUpload(uploadFiles)
+}
+
+function handleRemove(file) {
+ const targetUrl = file.url || getItemUrl(file.rawData)
+ const nextList = currentList.value.filter((item, index) => {
+ const itemUrl = getItemUrl(item)
+ const itemName = getItemName(item, index)
+ return !(itemUrl === targetUrl && itemName === file.name)
+ })
+ currentList.value = nextList
+}
+
+function handleExceed() {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 涓枃浠禶)
+}
+
+function openFile(file) {
+ const fileUrl = file.url || getItemUrl(file.rawData)
+ if (!fileUrl) return
+ window.open(fileUrl, '_blank')
+}
+
+onBeforeUnmount(() => {
+ clearTimeout(uploadQueueTimer.value)
+})
+</script>
+
+<template>
+ <div class="attachment-upload-file">
+ <el-upload
+ ref="uploadRef"
+ drag
+ :auto-upload="false"
+ :multiple="true"
+ :show-file-list="true"
+ :file-list="displayFileList"
+ :disabled="disabled || uploading"
+ :limit="limit"
+ :on-change="handleChange"
+ :on-remove="handleRemove"
+ :on-exceed="handleExceed"
+ :on-preview="openFile"
+ >
+ <el-icon class="upload-drag-icon"><UploadFilled /></el-icon>
+ <div class="el-upload__text">
+ 灏嗘枃浠舵嫋鍒版澶勶紝鎴� <em>{{ buttonText }}</em>
+ </div>
+ <div class="upload-tip">{{ uploadTip }}</div>
+ </el-upload>
+ </div>
+</template>
+
+<style scoped lang="scss">
+.attachment-upload-file {
+ width: 100%;
+}
+
+.upload-drag-icon {
+ font-size: 40px;
+ color: var(--el-text-color-secondary);
+}
+
+.upload-tip {
+ margin-top: 8px;
+ color: var(--el-text-color-secondary);
+ font-size: 12px;
+}
+</style>
diff --git a/src/components/AttachmentUpload/image/index.vue b/src/components/AttachmentUpload/image/index.vue
new file mode 100644
index 0000000..8243f9c
--- /dev/null
+++ b/src/components/AttachmentUpload/image/index.vue
@@ -0,0 +1,335 @@
+<script setup>
+import {Plus} from '@element-plus/icons-vue'
+import {uploadFile} from '@/api/basicData/common'
+
+const props = defineProps({
+ fileList: {
+ type: Array,
+ default: () => [],
+ },
+ index: {
+ type: Number,
+ default: -1,
+ },
+ childrenKey: {
+ type: String,
+ default: 'images',
+ },
+ limit: {
+ type: Number,
+ default: 10,
+ },
+ fileSize: {
+ type: Number,
+ default: 10,
+ },
+ fileType: {
+ type: Array,
+ default: () => ['png', 'jpg', 'jpeg', 'webp'],
+ },
+ buttonText: {
+ type: String,
+ default: '涓婁紶鍥剧墖',
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ uploadFieldName: {
+ type: String,
+ default: 'files',
+ },
+})
+
+const emit = defineEmits(['update:fileList', 'change'])
+const {proxy} = getCurrentInstance()
+
+const uploadRef = ref()
+const previewVisible = ref(false)
+const previewUrl = ref('')
+const uploadQueueTimer = ref(null)
+const uploading = ref(false)
+const queuedUidSet = ref(new Set())
+
+const currentList = computed({
+ get() {
+ if (props.index > -1) {
+ const row = props.fileList?.[props.index]
+ return Array.isArray(row?.[props.childrenKey]) ? row[props.childrenKey] : []
+ }
+ return Array.isArray(props.fileList) ? props.fileList : []
+ },
+ set(value) {
+ const nextList = Array.isArray(value) ? value : []
+ if (props.index > -1) {
+ const nextModelValue = Array.isArray(props.fileList) ? [...props.fileList] : []
+ const currentRow = nextModelValue[props.index] || {}
+ nextModelValue[props.index] = {
+ ...currentRow,
+ [props.childrenKey]: nextList,
+ }
+ emit('update:fileList', nextModelValue)
+ emit('change', nextList, nextModelValue)
+ return
+ }
+ emit('update:fileList', nextList)
+ emit('change', nextList, nextList)
+ },
+})
+
+const displayFileList = computed(() => {
+ return currentList.value.map((item, index) => ({
+ uid: getItemUid(item, index),
+ name: getItemName(item, index),
+ url: getItemUrl(item),
+ status: 'success',
+ rawData: item,
+ }))
+})
+
+const uploadTip = computed(() => {
+ return `鏀寔 ${props.fileType.join('/')}锛屽崟寮犱笉瓒呰繃 ${props.fileSize}MB锛屾渶澶氫笂浼� ${props.limit} 寮犲浘鐗嘸
+})
+
+function getItemUid(item, index) {
+ if (item?.id !== undefined && item?.id !== null) return `${item.id}`
+ return `${getItemName(item, index)}-${getItemUrl(item) || index}`
+}
+
+function getItemUrl(item) {
+ if (!item) return ''
+ if (typeof item === 'string') return item
+ return item.url || item.previewURL || ''
+}
+
+function getItemName(item, index = 0) {
+ if (!item) return `image-${index + 1}`
+ if (typeof item === 'string') return `image-${index + 1}`
+ return item.name || item.fileName || item.originalFilename || `image-${index + 1}`
+}
+
+function normalizeResponseItem(item, index) {
+ if (typeof item === 'string') {
+ return {
+ name: `image-${currentList.value.length + index + 1}`,
+ url: item,
+ }
+ }
+ return Object.assign({}, item, {
+ url: item.url || item.previewURL || item.previewUrl || '',
+ name: item.name || item.originalFilename || item.fileName || `image-${currentList.value.length + index + 1}`,
+ })
+}
+
+function extractResponseArray(response) {
+ if (Array.isArray(response)) return response
+ if (Array.isArray(response?.data)) return response.data
+ if (Array.isArray(response?.data?.data)) return response.data.data
+ if (Array.isArray(response?.payload)) return response.payload
+ if (Array.isArray(response?.payload?.data)) return response.payload.data
+ if (Array.isArray(response?.rows)) return response.rows
+ if (Array.isArray(response?.result)) return response.result
+ return []
+}
+
+function validateFile(rawFile) {
+ let isValidType = false
+ const extension = rawFile.name.includes('.')
+ ? rawFile.name.slice(rawFile.name.lastIndexOf('.') + 1).toLowerCase()
+ : ''
+
+ if (props.fileType.length) {
+ isValidType = props.fileType.some((type) => {
+ const normalizedType = String(type).toLowerCase()
+ return rawFile.type.toLowerCase().includes(normalizedType) || extension === normalizedType
+ })
+ } else {
+ isValidType = rawFile.type.includes('image')
+ }
+
+ if (!isValidType) {
+ proxy.$modal.msgError(`璇蜂笂浼� ${props.fileType.join('/')} 鏍煎紡鐨勫浘鐗嘸)
+ return false
+ }
+
+ const isWithinSize = rawFile.size / 1024 / 1024 <= props.fileSize
+ if (!isWithinSize) {
+ proxy.$modal.msgError(`鍥剧墖澶у皬涓嶈兘瓒呰繃 ${props.fileSize}MB`)
+ return false
+ }
+
+ return true
+}
+
+function scheduleUpload(uploadFiles) {
+ clearTimeout(uploadQueueTimer.value)
+ uploadQueueTimer.value = setTimeout(() => {
+ const readyFiles = uploadFiles.filter((file) => file.raw && !queuedUidSet.value.has(file.uid))
+ if (!readyFiles.length) return
+
+ const remainCount = props.limit - currentList.value.length
+ if (remainCount <= 0) {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗嘸)
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const selectedFiles = readyFiles.slice(0, remainCount)
+ if (selectedFiles.length < readyFiles.length) {
+ proxy.$modal.msgWarning(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗囷紝瓒呭嚭閮ㄥ垎宸插拷鐣)
+ }
+
+ selectedFiles.forEach((file) => queuedUidSet.value.add(file.uid))
+ uploadSelectedFiles(selectedFiles)
+ }, 0)
+}
+
+async function uploadSelectedFiles(files) {
+ const validFiles = files.filter((file) => validateFile(file.raw))
+ const invalidFiles = files.filter((file) => !validFiles.includes(file))
+
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+
+ if (!validFiles.length) {
+ uploadRef.value?.clearFiles()
+ return
+ }
+
+ const formData = new FormData()
+ validFiles.forEach((file) => {
+ formData.append(props.uploadFieldName, file.raw)
+ })
+
+ uploading.value = true
+ proxy.$modal.loading('鍥剧墖涓婁紶涓紝璇风◢鍊�...')
+
+ try {
+ const response = await uploadFile(formData)
+ const responseList = extractResponseArray(response).map((item, index) => normalizeResponseItem(item, index))
+
+ if (!responseList.length) {
+ proxy.$modal.msgError('涓婁紶鎺ュ彛鏈繑鍥炴暟缁勬暟鎹�')
+ return
+ }
+ console.log('responseList', responseList)
+
+ currentList.value = [...currentList.value, ...responseList]
+ console.log('currentList.value', currentList.value)
+ proxy.$modal.msgSuccess('涓婁紶鎴愬姛')
+ } catch (error) {
+ proxy.$modal.msgError(error?.message || '涓婁紶澶辫触')
+ } finally {
+ validFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ invalidFiles.forEach((file) => queuedUidSet.value.delete(file.uid))
+ uploadRef.value?.clearFiles()
+ uploading.value = false
+ proxy.$modal.closeLoading()
+ }
+}
+
+function handleChange(file, uploadFiles) {
+ if (props.disabled || uploading.value) return
+ scheduleUpload(uploadFiles)
+}
+
+function handleRemove(file) {
+ const targetUrl = file.url || getItemUrl(file.rawData)
+ const nextList = currentList.value.filter((item, index) => {
+ const itemUrl = getItemUrl(item)
+ const itemName = getItemName(item, index)
+ return !(itemUrl === targetUrl && itemName === file.name)
+ })
+ currentList.value = nextList
+}
+
+function handlePreview(file) {
+ previewUrl.value = file.url || getItemUrl(file.rawData)
+ previewVisible.value = true
+}
+
+function handleExceed() {
+ proxy.$modal.msgError(`鏈�澶氫笂浼� ${props.limit} 寮犲浘鐗嘸)
+}
+
+onBeforeUnmount(() => {
+ clearTimeout(uploadQueueTimer.value)
+})
+</script>
+
+<template>
+ <div class="attachment-upload-image">
+ <el-upload
+ ref="uploadRef"
+ :auto-upload="false"
+ :multiple="true"
+ :show-file-list="true"
+ :file-list="displayFileList"
+ list-type="picture-card"
+ accept="image/*"
+ :disabled="disabled || uploading"
+ :limit="limit"
+ :on-change="handleChange"
+ :on-remove="handleRemove"
+ :on-preview="handlePreview"
+ :on-exceed="handleExceed"
+ >
+ <div class="upload-trigger">
+ <el-icon>
+ <Plus/>
+ </el-icon>
+ <span>{{ buttonText }}</span>
+ </div>
+ </el-upload>
+
+ <div class="upload-tip">
+ {{ uploadTip }}
+ </div>
+
+ <el-dialog v-model="previewVisible" title="鍥剧墖棰勮" width="720px" append-to-body>
+ <img class="preview-image" :src="previewUrl" alt="preview"/>
+ </el-dialog>
+ </div>
+</template>
+
+<style scoped lang="scss">
+.attachment-upload-image {
+ width: 100%;
+}
+
+.upload-trigger {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ color: var(--el-text-color-secondary);
+ font-size: 12px;
+ line-height: 1.2;
+}
+
+.upload-tip {
+ margin-top: 8px;
+ color: var(--el-text-color-secondary);
+ font-size: 12px;
+}
+
+.preview-image {
+ display: block;
+ max-width: 100%;
+ margin: 0 auto;
+}
+
+:deep(.el-upload-list--picture-card) {
+ margin: 0;
+}
+
+:deep(.el-upload--picture-card) {
+ width: 132px;
+ height: 132px;
+}
+
+:deep(.el-upload-list--picture-card .el-upload-list__item) {
+ width: 132px;
+ height: 132px;
+}
+</style>
diff --git a/src/components/Breadcrumb/index.vue b/src/components/Breadcrumb/index.vue
new file mode 100644
index 0000000..3ea63b5
--- /dev/null
+++ b/src/components/Breadcrumb/index.vue
@@ -0,0 +1,119 @@
+<template>
+ <el-breadcrumb class="app-breadcrumb" separator="/">
+ <transition-group name="breadcrumb">
+ <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
+ <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{
+ item.meta.title }}</span>
+ <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+ </el-breadcrumb-item>
+ </transition-group>
+ </el-breadcrumb>
+</template>
+
+<script setup>
+import usePermissionStore from '@/store/modules/permission'
+
+const route = useRoute()
+const router = useRouter()
+const permissionStore = usePermissionStore()
+const levelList = ref([])
+
+function getBreadcrumb() {
+ // only show routes with meta.title
+ let matched = []
+ const pathNum = findPathNum(route.path)
+ // multi-level menu
+ if (pathNum > 2) {
+ const reg = /\/\w+/gi
+ const pathList = route.path.match(reg).map((item, index) => {
+ if (index !== 0) item = item.slice(1)
+ return item
+ })
+ getMatched(pathList, permissionStore.defaultRoutes, matched)
+ } else {
+ matched = route.matched.filter((item) => item.meta && item.meta.title)
+ }
+ // 鍒ゆ柇鏄惁涓洪椤�
+ if (!isDashboard(matched[0])) {
+ matched = [{ path: "/index", meta: { title: "棣栭〉" } }].concat(matched)
+ }
+ levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+}
+function findPathNum(str, char = "/") {
+ let index = str.indexOf(char)
+ let num = 0
+ while (index !== -1) {
+ num++
+ index = str.indexOf(char, index + 1)
+ }
+ return num
+}
+function getMatched(pathList, routeList, matched) {
+ let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
+ if (data) {
+ matched.push(data)
+ if (data.children && pathList.length) {
+ pathList.shift()
+ getMatched(pathList, data.children, matched)
+ }
+ }
+}
+function isDashboard(route) {
+ const name = route && route.name
+ if (!name) {
+ return false
+ }
+ return name.trim() === 'Index'
+}
+function handleLink(item) {
+ const { redirect, path } = item
+ if (redirect) {
+ router.push(redirect)
+ return
+ }
+ router.push(path)
+}
+
+watchEffect(() => {
+ // if you go to the redirect page, do not update the breadcrumbs
+ if (route.path.startsWith('/redirect/')) {
+ return
+ }
+ getBreadcrumb()
+})
+getBreadcrumb()
+</script>
+
+<style lang='scss' scoped>
+.app-breadcrumb.el-breadcrumb {
+ display: inline-flex;
+ align-items: center;
+ font-size: 14px;
+ line-height: 1;
+ margin-left: 8px;
+
+ :deep(.el-breadcrumb__inner) {
+ color: var(--text-secondary);
+ font-weight: 500;
+ transition: color 0.2s ease;
+ }
+
+ :deep(.el-breadcrumb__separator) {
+ color: var(--text-tertiary);
+ }
+
+ a {
+ color: var(--text-secondary);
+
+ &:hover {
+ color: var(--current-color);
+ }
+ }
+
+ .no-redirect {
+ color: var(--current-color);
+ font-weight: 600;
+ cursor: text;
+ }
+}
+</style>
diff --git a/src/components/Crontab/day.vue b/src/components/Crontab/day.vue
new file mode 100644
index 0000000..456686f
--- /dev/null
+++ b/src/components/Crontab/day.vue
@@ -0,0 +1,174 @@
+<template>
+ <el-form>
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="1">
+ 鏃ワ紝鍏佽鐨勯�氶厤绗, - * ? / L W]
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="2">
+ 涓嶆寚瀹�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="3">
+ 鍛ㄦ湡浠�
+ <el-input-number v-model='cycle01' :min="1" :max="30" /> -
+ <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" /> 鏃�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="4">
+ 浠�
+ <el-input-number v-model='average01' :min="1" :max="30" /> 鍙峰紑濮嬶紝姣�
+ <el-input-number v-model='average02' :min="1" :max="31 - average01" /> 鏃ユ墽琛屼竴娆�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="5">
+ 姣忔湀
+ <el-input-number v-model='workday' :min="1" :max="31" /> 鍙锋渶杩戠殑閭d釜宸ヤ綔鏃�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="6">
+ 鏈湀鏈�鍚庝竴澶�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="7">
+ 鎸囧畾
+ <el-select clearable v-model="checkboxList" placeholder="鍙閫�" multiple :multiple-limit="10">
+ <el-option v-for="item in 31" :key="item" :label="item" :value="item" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+ </el-form>
+</template>
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+ cron: {
+ type: Object,
+ default: {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: "",
+ }
+ },
+ check: {
+ type: Function,
+ default: () => {
+ }
+ }
+})
+const radioValue = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(1)
+const average02 = ref(1)
+const workday = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([1])
+const cycleTotal = computed(() => {
+ cycle01.value = props.check(cycle01.value, 1, 30)
+ cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
+ return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+ average01.value = props.check(average01.value, 1, 30)
+ average02.value = props.check(average02.value, 1, 31 - average01.value)
+ return average01.value + '/' + average02.value
+})
+const workdayTotal = computed(() => {
+ workday.value = props.check(workday.value, 1, 31)
+ return workday.value + 'W'
+})
+const checkboxString = computed(() => {
+ return checkboxList.value.join(',')
+})
+watch(() => props.cron.day, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+ if (value === "*") {
+ radioValue.value = 1
+ } else if (value === "?") {
+ radioValue.value = 2
+ } else if (value.indexOf("-") > -1) {
+ const indexArr = value.split('-')
+ cycle01.value = Number(indexArr[0])
+ cycle02.value = Number(indexArr[1])
+ radioValue.value = 3
+ } else if (value.indexOf("/") > -1) {
+ const indexArr = value.split('/')
+ average01.value = Number(indexArr[0])
+ average02.value = Number(indexArr[1])
+ radioValue.value = 4
+ } else if (value.indexOf("W") > -1) {
+ const indexArr = value.split("W")
+ workday.value = Number(indexArr[0])
+ radioValue.value = 5
+ } else if (value === "L") {
+ radioValue.value = 6
+ } else {
+ checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+ radioValue.value = 7
+ }
+}
+// 鍗曢�夋寜閽�煎彉鍖栨椂
+function onRadioChange() {
+ if (radioValue.value === 2 && props.cron.week === '?') {
+ emit('update', 'week', '*', 'day')
+ }
+ if (radioValue.value !== 2 && props.cron.week !== '?') {
+ emit('update', 'week', '?', 'day')
+ }
+ switch (radioValue.value) {
+ case 1:
+ emit('update', 'day', '*', 'day')
+ break
+ case 2:
+ emit('update', 'day', '?', 'day')
+ break
+ case 3:
+ emit('update', 'day', cycleTotal.value, 'day')
+ break
+ case 4:
+ emit('update', 'day', averageTotal.value, 'day')
+ break
+ case 5:
+ emit('update', 'day', workdayTotal.value, 'day')
+ break
+ case 6:
+ emit('update', 'day', 'L', 'day')
+ break
+ case 7:
+ if (checkboxList.value.length === 0) {
+ checkboxList.value.push(checkCopy.value[0])
+ } else {
+ checkCopy.value = checkboxList.value
+ }
+ emit('update', 'day', checkboxString.value, 'day')
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+ margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+ width: 18.8rem;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Crontab/hour.vue b/src/components/Crontab/hour.vue
new file mode 100644
index 0000000..d05779e
--- /dev/null
+++ b/src/components/Crontab/hour.vue
@@ -0,0 +1,133 @@
+<template>
+ <el-form>
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="1">
+ 灏忔椂锛屽厑璁哥殑閫氶厤绗, - * /]
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="2">
+ 鍛ㄦ湡浠�
+ <el-input-number v-model='cycle01' :min="0" :max="22" /> -
+ <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" /> 鏃�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="3">
+ 浠�
+ <el-input-number v-model='average01' :min="0" :max="22" /> 鏃跺紑濮嬶紝姣�
+ <el-input-number v-model='average02' :min="1" :max="23 - average01" /> 灏忔椂鎵ц涓�娆�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="4">
+ 鎸囧畾
+ <el-select clearable v-model="checkboxList" placeholder="鍙閫�" multiple :multiple-limit="10">
+ <el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+ cron: {
+ type: Object,
+ default: {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: "",
+ }
+ },
+ check: {
+ type: Function,
+ default: () => {
+ }
+ }
+})
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(1)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([0])
+const cycleTotal = computed(() => {
+ cycle01.value = props.check(cycle01.value, 0, 22)
+ cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
+ return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+ average01.value = props.check(average01.value, 0, 22)
+ average02.value = props.check(average02.value, 1, 23 - average01.value)
+ return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+ return checkboxList.value.join(',')
+})
+watch(() => props.cron.hour, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+ if (props.cron.min === '*') {
+ emit('update', 'min', '0', 'hour')
+ }
+ if (props.cron.second === '*') {
+ emit('update', 'second', '0', 'hour')
+ }
+ if (value === '*') {
+ radioValue.value = 1
+ } else if (value.indexOf('-') > -1) {
+ const indexArr = value.split('-')
+ cycle01.value = Number(indexArr[0])
+ cycle02.value = Number(indexArr[1])
+ radioValue.value = 2
+ } else if (value.indexOf('/') > -1) {
+ const indexArr = value.split('/')
+ average01.value = Number(indexArr[0])
+ average02.value = Number(indexArr[1])
+ radioValue.value = 3
+ } else {
+ checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+ radioValue.value = 4
+ }
+}
+function onRadioChange() {
+ switch (radioValue.value) {
+ case 1:
+ emit('update', 'hour', '*', 'hour')
+ break
+ case 2:
+ emit('update', 'hour', cycleTotal.value, 'hour')
+ break
+ case 3:
+ emit('update', 'hour', averageTotal.value, 'hour')
+ break
+ case 4:
+ if (checkboxList.value.length === 0) {
+ checkboxList.value.push(checkCopy.value[0])
+ } else {
+ checkCopy.value = checkboxList.value
+ }
+ emit('update', 'hour', checkboxString.value, 'hour')
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+ margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+ width: 18.8rem;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Crontab/index.vue b/src/components/Crontab/index.vue
new file mode 100644
index 0000000..71f3824
--- /dev/null
+++ b/src/components/Crontab/index.vue
@@ -0,0 +1,309 @@
+<template>
+ <div>
+ <el-tabs type="border-card">
+ <el-tab-pane label="绉�" v-if="shouldHide('second')">
+ <CrontabSecond
+ @update="updateCrontabValue"
+ :check="checkNumber"
+ :cron="crontabValueObj"
+ ref="cronsecond"
+ />
+ </el-tab-pane>
+
+ <el-tab-pane label="鍒嗛挓" v-if="shouldHide('min')">
+ <CrontabMin
+ @update="updateCrontabValue"
+ :check="checkNumber"
+ :cron="crontabValueObj"
+ ref="cronmin"
+ />
+ </el-tab-pane>
+
+ <el-tab-pane label="灏忔椂" v-if="shouldHide('hour')">
+ <CrontabHour
+ @update="updateCrontabValue"
+ :check="checkNumber"
+ :cron="crontabValueObj"
+ ref="cronhour"
+ />
+ </el-tab-pane>
+
+ <el-tab-pane label="鏃�" v-if="shouldHide('day')">
+ <CrontabDay
+ @update="updateCrontabValue"
+ :check="checkNumber"
+ :cron="crontabValueObj"
+ ref="cronday"
+ />
+ </el-tab-pane>
+
+ <el-tab-pane label="鏈�" v-if="shouldHide('month')">
+ <CrontabMonth
+ @update="updateCrontabValue"
+ :check="checkNumber"
+ :cron="crontabValueObj"
+ ref="cronmonth"
+ />
+ </el-tab-pane>
+
+ <el-tab-pane label="鍛�" v-if="shouldHide('week')">
+ <CrontabWeek
+ @update="updateCrontabValue"
+ :check="checkNumber"
+ :cron="crontabValueObj"
+ ref="cronweek"
+ />
+ </el-tab-pane>
+
+ <el-tab-pane label="骞�" v-if="shouldHide('year')">
+ <CrontabYear
+ @update="updateCrontabValue"
+ :check="checkNumber"
+ :cron="crontabValueObj"
+ ref="cronyear"
+ />
+ </el-tab-pane>
+ </el-tabs>
+
+ <div class="popup-main">
+ <div class="popup-result">
+ <p class="title">鏃堕棿琛ㄨ揪寮�</p>
+ <table>
+ <thead>
+ <th v-for="item of tabTitles" :key="item">{{item}}</th>
+ <th>Cron 琛ㄨ揪寮�</th>
+ </thead>
+ <tbody>
+ <td>
+ <span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
+ <el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
+ </td>
+ <td>
+ <span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
+ <el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
+ </td>
+ <td>
+ <span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
+ <el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
+ </td>
+ <td>
+ <span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
+ <el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
+ </td>
+ <td>
+ <span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
+ <el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
+ </td>
+ <td>
+ <span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
+ <el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
+ </td>
+ <td>
+ <span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
+ <el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
+ </td>
+ <td class="result">
+ <span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
+ <el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
+ </td>
+ </tbody>
+ </table>
+ </div>
+ <CrontabResult :ex="crontabValueString"></CrontabResult>
+
+ <div class="pop_btn">
+ <el-button type="primary" @click="submitFill">纭畾</el-button>
+ <el-button type="warning" @click="clearCron">閲嶇疆</el-button>
+ <el-button @click="hidePopup">鍙栨秷</el-button>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import CrontabSecond from "./second.vue"
+import CrontabMin from "./min.vue"
+import CrontabHour from "./hour.vue"
+import CrontabDay from "./day.vue"
+import CrontabMonth from "./month.vue"
+import CrontabWeek from "./week.vue"
+import CrontabYear from "./year.vue"
+import CrontabResult from "./result.vue"
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['hide', 'fill'])
+const props = defineProps({
+ hideComponent: {
+ type: Array,
+ default: () => [],
+ },
+ expression: {
+ type: String,
+ default: ""
+ }
+})
+const tabTitles = ref(["绉�", "鍒嗛挓", "灏忔椂", "鏃�", "鏈�", "鍛�", "骞�"])
+const tabActive = ref(0)
+const hideComponent = ref([])
+const expression = ref('')
+const crontabValueObj = ref({
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: "",
+})
+const crontabValueString = computed(() => {
+ const obj = crontabValueObj.value
+ return obj.second
+ + " "
+ + obj.min
+ + " "
+ + obj.hour
+ + " "
+ + obj.day
+ + " "
+ + obj.month
+ + " "
+ + obj.week
+ + (obj.year === "" ? "" : " " + obj.year)
+})
+watch(expression, () => resolveExp())
+function shouldHide(key) {
+ return !(hideComponent.value && hideComponent.value.includes(key))
+}
+function resolveExp() {
+ // 鍙嶈В鏋� 琛ㄨ揪寮�
+ if (expression.value) {
+ const arr = expression.value.split(/\s+/)
+ if (arr.length >= 6) {
+ //6 浣嶄互涓婃槸鍚堟硶琛ㄨ揪寮�
+ let obj = {
+ second: arr[0],
+ min: arr[1],
+ hour: arr[2],
+ day: arr[3],
+ month: arr[4],
+ week: arr[5],
+ year: arr[6] ? arr[6] : ""
+ }
+ crontabValueObj.value = {
+ ...obj,
+ }
+ }
+ } else {
+ // 娌℃湁浼犲叆鐨勮〃杈惧紡 鍒欒繕鍘�
+ clearCron()
+ }
+}
+// tab鍒囨崲鍊�
+function tabCheck(index) {
+ tabActive.value = index
+}
+// 鐢卞瓙缁勪欢瑙﹀彂锛屾洿鏀硅〃杈惧紡缁勬垚鐨勫瓧娈靛��
+function updateCrontabValue(name, value, from) {
+ crontabValueObj.value[name] = value
+}
+// 琛ㄥ崟閫夐」鐨勫瓙缁勪欢鏍¢獙鏁板瓧鏍煎紡锛堥�氳繃-props浼犻�掞級
+function checkNumber(value, minLimit, maxLimit) {
+ // 妫�鏌ュ繀椤讳负鏁存暟
+ value = Math.floor(value)
+ if (value < minLimit) {
+ value = minLimit
+ } else if (value > maxLimit) {
+ value = maxLimit
+ }
+ return value
+}
+// 闅愯棌寮圭獥
+function hidePopup() {
+ emit("hide")
+}
+// 濉厖琛ㄨ揪寮�
+function submitFill() {
+ emit("fill", crontabValueString.value)
+ hidePopup()
+}
+function clearCron() {
+ // 杩樺師閫夋嫨椤�
+ crontabValueObj.value = {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: "",
+ }
+}
+onMounted(() => {
+ expression.value = props.expression
+ hideComponent.value = props.hideComponent
+})
+</script>
+
+<style lang="scss" scoped>
+.pop_btn {
+ text-align: center;
+ margin-top: 20px;
+}
+.popup-main {
+ position: relative;
+ margin: 10px auto;
+ border-radius: 5px;
+ font-size: 12px;
+ overflow: hidden;
+}
+.popup-title {
+ overflow: hidden;
+ line-height: 34px;
+ padding-top: 6px;
+ background: #f2f2f2;
+}
+.popup-result {
+ box-sizing: border-box;
+ line-height: 24px;
+ margin: 25px auto;
+ padding: 15px 10px 10px;
+ border: 1px solid #ccc;
+ position: relative;
+}
+.popup-result .title {
+ position: absolute;
+ top: -28px;
+ left: 50%;
+ width: 140px;
+ font-size: 14px;
+ margin-left: -70px;
+ text-align: center;
+ line-height: 30px;
+ background: #fff;
+}
+.popup-result table {
+ text-align: center;
+ width: 100%;
+ margin: 0 auto;
+}
+.popup-result table td:not(.result) {
+ width: 3.5rem;
+ min-width: 3.5rem;
+ max-width: 3.5rem;
+}
+.popup-result table span {
+ display: block;
+ width: 100%;
+ font-family: arial;
+ line-height: 30px;
+ height: 30px;
+ white-space: nowrap;
+ overflow: hidden;
+ border: 1px solid #e8e8e8;
+}
+.popup-result-scroll {
+ font-size: 12px;
+ line-height: 24px;
+ height: 10em;
+ overflow-y: auto;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Crontab/min.vue b/src/components/Crontab/min.vue
new file mode 100644
index 0000000..33e9bec
--- /dev/null
+++ b/src/components/Crontab/min.vue
@@ -0,0 +1,126 @@
+<template>
+ <el-form>
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="1">
+ 鍒嗛挓锛屽厑璁哥殑閫氶厤绗, - * /]
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="2">
+ 鍛ㄦ湡浠�
+ <el-input-number v-model='cycle01' :min="0" :max="58" /> -
+ <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 鍒嗛挓
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="3">
+ 浠�
+ <el-input-number v-model='average01' :min="0" :max="58" /> 鍒嗛挓寮�濮嬶紝 姣�
+ <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 鍒嗛挓鎵ц涓�娆�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="4">
+ 鎸囧畾
+ <el-select clearable v-model="checkboxList" placeholder="鍙閫�" multiple :multiple-limit="10">
+ <el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+ </el-form>
+</template>
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+ cron: {
+ type: Object,
+ default: {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: "",
+ }
+ },
+ check: {
+ type: Function,
+ default: () => {
+ }
+ }
+})
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(1)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([0])
+const cycleTotal = computed(() => {
+ cycle01.value = props.check(cycle01.value, 0, 58)
+ cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
+ return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+ average01.value = props.check(average01.value, 0, 58)
+ average02.value = props.check(average02.value, 1, 59 - average01.value)
+ return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+ return checkboxList.value.join(',')
+})
+watch(() => props.cron.min, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+ if (value === '*') {
+ radioValue.value = 1
+ } else if (value.indexOf('-') > -1) {
+ const indexArr = value.split('-')
+ cycle01.value = Number(indexArr[0])
+ cycle02.value = Number(indexArr[1])
+ radioValue.value = 2
+ } else if (value.indexOf('/') > -1) {
+ const indexArr = value.split('/')
+ average01.value = Number(indexArr[0])
+ average02.value = Number(indexArr[1])
+ radioValue.value = 3
+ } else {
+ checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+ radioValue.value = 4
+ }
+}
+function onRadioChange() {
+ switch (radioValue.value) {
+ case 1:
+ emit('update', 'min', '*', 'min')
+ break
+ case 2:
+ emit('update', 'min', cycleTotal.value, 'min')
+ break
+ case 3:
+ emit('update', 'min', averageTotal.value, 'min')
+ break
+ case 4:
+ if (checkboxList.value.length === 0) {
+ checkboxList.value.push(checkCopy.value[0])
+ } else {
+ checkCopy.value = checkboxList.value
+ }
+ emit('update', 'min', checkboxString.value, 'min')
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+ margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+ width: 19.8rem;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Crontab/month.vue b/src/components/Crontab/month.vue
new file mode 100644
index 0000000..542dd6f
--- /dev/null
+++ b/src/components/Crontab/month.vue
@@ -0,0 +1,141 @@
+<template>
+ <el-form>
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="1">
+ 鏈堬紝鍏佽鐨勯�氶厤绗, - * /]
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="2">
+ 鍛ㄦ湡浠�
+ <el-input-number v-model='cycle01' :min="1" :max="11" /> -
+ <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" /> 鏈�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="3">
+ 浠�
+ <el-input-number v-model='average01' :min="1" :max="11" /> 鏈堝紑濮嬶紝姣�
+ <el-input-number v-model='average02' :min="1" :max="12 - average01" /> 鏈堟湀鎵ц涓�娆�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="4">
+ 鎸囧畾
+ <el-select clearable v-model="checkboxList" placeholder="鍙閫�" multiple :multiple-limit="8">
+ <el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+ cron: {
+ type: Object,
+ default: {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: "",
+ }
+ },
+ check: {
+ type: Function,
+ default: () => {
+ }
+ }
+})
+const radioValue = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(1)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([1])
+const monthList = ref([
+ {key: 1, value: '涓�鏈�'},
+ {key: 2, value: '浜屾湀'},
+ {key: 3, value: '涓夋湀'},
+ {key: 4, value: '鍥涙湀'},
+ {key: 5, value: '浜旀湀'},
+ {key: 6, value: '鍏湀'},
+ {key: 7, value: '涓冩湀'},
+ {key: 8, value: '鍏湀'},
+ {key: 9, value: '涔濇湀'},
+ {key: 10, value: '鍗佹湀'},
+ {key: 11, value: '鍗佷竴鏈�'},
+ {key: 12, value: '鍗佷簩鏈�'}
+])
+const cycleTotal = computed(() => {
+ cycle01.value = props.check(cycle01.value, 1, 11)
+ cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
+ return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+ average01.value = props.check(average01.value, 1, 11)
+ average02.value = props.check(average02.value, 1, 12 - average01.value)
+ return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+ return checkboxList.value.join(',')
+})
+watch(() => props.cron.month, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+ if (value === '*') {
+ radioValue.value = 1
+ } else if (value.indexOf('-') > -1) {
+ const indexArr = value.split('-')
+ cycle01.value = Number(indexArr[0])
+ cycle02.value = Number(indexArr[1])
+ radioValue.value = 2
+ } else if (value.indexOf('/') > -1) {
+ const indexArr = value.split('/')
+ average01.value = Number(indexArr[0])
+ average02.value = Number(indexArr[1])
+ radioValue.value = 3
+ } else {
+ checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+ radioValue.value = 4
+ }
+}
+function onRadioChange() {
+ switch (radioValue.value) {
+ case 1:
+ emit('update', 'month', '*', 'month')
+ break
+ case 2:
+ emit('update', 'month', cycleTotal.value, 'month')
+ break
+ case 3:
+ emit('update', 'month', averageTotal.value, 'month')
+ break
+ case 4:
+ if (checkboxList.value.length === 0) {
+ checkboxList.value.push(checkCopy.value[0])
+ } else {
+ checkCopy.value = checkboxList.value
+ }
+ emit('update', 'month', checkboxString.value, 'month')
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+ margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+ width: 18.8rem;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Crontab/result.vue b/src/components/Crontab/result.vue
new file mode 100644
index 0000000..b260d59
--- /dev/null
+++ b/src/components/Crontab/result.vue
@@ -0,0 +1,540 @@
+<template>
+ <div class="popup-result">
+ <p class="title">鏈�杩�5娆¤繍琛屾椂闂�</p>
+ <ul class="popup-result-scroll">
+ <template v-if='isShow'>
+ <li v-for='item in resultList' :key="item">{{item}}</li>
+ </template>
+ <li v-else>璁$畻缁撴灉涓�...</li>
+ </ul>
+ </div>
+</template>
+
+<script setup>
+const props = defineProps({
+ ex: {
+ type: String,
+ default: ''
+ }
+})
+const dayRule = ref('')
+const dayRuleSup = ref('')
+const dateArr = ref([])
+const resultList = ref([])
+const isShow = ref(false)
+watch(() => props.ex, () => expressionChange())
+// 琛ㄨ揪寮忓�煎彉鍖栨椂锛屽紑濮嬪幓璁$畻缁撴灉
+function expressionChange() {
+ // 璁$畻寮�濮�-闅愯棌缁撴灉
+ isShow.value = false
+ // 鑾峰彇瑙勫垯鏁扮粍[0绉掋��1鍒嗐��2鏃躲��3鏃ャ��4鏈堛��5鏄熸湡銆�6骞碷
+ let ruleArr = props.ex.split(' ')
+ // 鐢ㄤ簬璁板綍杩涘叆寰幆鐨勬鏁�
+ let nums = 0
+ // 鐢ㄤ簬鏆傛椂瀛樼鍙锋椂闂磋鍒欑粨鏋滅殑鏁扮粍
+ let resultArr = []
+ // 鑾峰彇褰撳墠鏃堕棿绮剧‘鑷砙骞淬�佹湀銆佹棩銆佹椂銆佸垎銆佺]
+ let nTime = new Date()
+ let nYear = nTime.getFullYear()
+ let nMonth = nTime.getMonth() + 1
+ let nDay = nTime.getDate()
+ let nHour = nTime.getHours()
+ let nMin = nTime.getMinutes()
+ let nSecond = nTime.getSeconds()
+ // 鏍规嵁瑙勫垯鑾峰彇鍒拌繎100骞村彲鑳藉勾鏁扮粍銆佹湀鏁扮粍绛夌瓑
+ getSecondArr(ruleArr[0])
+ getMinArr(ruleArr[1])
+ getHourArr(ruleArr[2])
+ getDayArr(ruleArr[3])
+ getMonthArr(ruleArr[4])
+ getWeekArr(ruleArr[5])
+ getYearArr(ruleArr[6], nYear)
+ // 灏嗚幏鍙栧埌鐨勬暟缁勮祴鍊�-鏂逛究浣跨敤
+ let sDate = dateArr.value[0]
+ let mDate = dateArr.value[1]
+ let hDate = dateArr.value[2]
+ let DDate = dateArr.value[3]
+ let MDate = dateArr.value[4]
+ let YDate = dateArr.value[5]
+ // 鑾峰彇褰撳墠鏃堕棿鍦ㄦ暟缁勪腑鐨勭储寮�
+ let sIdx = getIndex(sDate, nSecond)
+ let mIdx = getIndex(mDate, nMin)
+ let hIdx = getIndex(hDate, nHour)
+ let DIdx = getIndex(DDate, nDay)
+ let MIdx = getIndex(MDate, nMonth)
+ let YIdx = getIndex(YDate, nYear)
+ // 閲嶇疆鏈堟棩鏃跺垎绉掔殑鍑芥暟(鍚庨潰鐢ㄧ殑姣旇緝澶�)
+ const resetSecond = function () {
+ sIdx = 0
+ nSecond = sDate[sIdx]
+ }
+ const resetMin = function () {
+ mIdx = 0
+ nMin = mDate[mIdx]
+ resetSecond()
+ }
+ const resetHour = function () {
+ hIdx = 0
+ nHour = hDate[hIdx]
+ resetMin()
+ }
+ const resetDay = function () {
+ DIdx = 0
+ nDay = DDate[DIdx]
+ resetHour()
+ }
+ const resetMonth = function () {
+ MIdx = 0
+ nMonth = MDate[MIdx]
+ resetDay()
+ }
+ // 濡傛灉褰撳墠骞翠唤涓嶄负鏁扮粍涓綋鍓嶅��
+ if (nYear !== YDate[YIdx]) {
+ resetMonth()
+ }
+ // 濡傛灉褰撳墠鏈堜唤涓嶄负鏁扮粍涓綋鍓嶅��
+ if (nMonth !== MDate[MIdx]) {
+ resetDay()
+ }
+ // 濡傛灉褰撳墠鈥滄棩鈥濅笉涓烘暟缁勪腑褰撳墠鍊�
+ if (nDay !== DDate[DIdx]) {
+ resetHour()
+ }
+ // 濡傛灉褰撳墠鈥滄椂鈥濅笉涓烘暟缁勪腑褰撳墠鍊�
+ if (nHour !== hDate[hIdx]) {
+ resetMin()
+ }
+ // 濡傛灉褰撳墠鈥滃垎鈥濅笉涓烘暟缁勪腑褰撳墠鍊�
+ if (nMin !== mDate[mIdx]) {
+ resetSecond()
+ }
+ // 寰幆骞翠唤鏁扮粍
+ goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
+ let YY = YDate[Yi]
+ // 濡傛灉鍒拌揪鏈�澶у�兼椂
+ if (nMonth > MDate[MDate.length - 1]) {
+ resetMonth()
+ continue
+ }
+ // 寰幆鏈堜唤鏁扮粍
+ goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
+ // 璧嬪�笺�佹柟渚垮悗闈㈣繍绠�
+ let MM = MDate[Mi];
+ MM = MM < 10 ? '0' + MM : MM
+ // 濡傛灉鍒拌揪鏈�澶у�兼椂
+ if (nDay > DDate[DDate.length - 1]) {
+ resetDay()
+ if (Mi === MDate.length - 1) {
+ resetMonth()
+ continue goYear
+ }
+ continue
+ }
+ // 寰幆鏃ユ湡鏁扮粍
+ goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
+ // 璧嬪�笺�佹柟渚垮悗闈㈣繍绠�
+ let DD = DDate[Di]
+ let thisDD = DD < 10 ? '0' + DD : DD
+ // 濡傛灉鍒拌揪鏈�澶у�兼椂
+ if (nHour > hDate[hDate.length - 1]) {
+ resetHour()
+ if (Di === DDate.length - 1) {
+ resetDay()
+ if (Mi === MDate.length - 1) {
+ resetMonth()
+ continue goYear
+ }
+ continue goMonth
+ }
+ continue
+ }
+ // 鍒ゆ柇鏃ユ湡鐨勫悎娉曟�э紝涓嶅悎娉曠殑璇濅篃鏄烦鍑哄綋鍓嶅惊鐜�
+ if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
+ resetDay()
+ continue goMonth
+ }
+ // 濡傛灉鏃ユ湡瑙勫垯涓湁鍊兼椂
+ if (dayRule.value === 'lastDay') {
+ // 濡傛灉涓嶆槸鍚堟硶鏃ユ湡鍒欓渶瑕佸皢鍓嶅皢鏃ユ湡璋冨埌鍚堟硶鏃ユ湡鍗虫湀鏈渶鍚庝竴澶�
+ if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+ while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+ DD--
+ thisDD = DD < 10 ? '0' + DD : DD
+ }
+ }
+ } else if (dayRule.value === 'workDay') {
+ // 鏍¢獙骞惰皟鏁村鏋滄槸2鏈�30鍙疯繖绉嶆棩鏈熶紶杩涙潵鏃堕渶璋冩暣鑷虫甯告湀搴�
+ if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+ while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+ DD--
+ thisDD = DD < 10 ? '0' + DD : DD
+ }
+ }
+ // 鑾峰彇杈惧埌鏉′欢鐨勬棩鏈熸槸鏄熸湡X
+ let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
+ // 褰撴槦鏈熸棩鏃�
+ if (thisWeek === 1) {
+ // 鍏堟壘涓嬩竴涓棩锛屽苟鍒ゆ柇鏄惁涓烘湀搴�
+ DD++
+ thisDD = DD < 10 ? '0' + DD : DD
+ // 鍒ゆ柇涓嬩竴鏃ュ凡缁忎笉鏄悎娉曟棩鏈�
+ if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+ DD -= 3
+ }
+ } else if (thisWeek === 7) {
+ // 褰撴槦鏈�6鏃跺彧闇�鍒ゆ柇涓嶆槸1鍙峰氨鍙繘琛屾搷浣�
+ if (dayRuleSup.value !== 1) {
+ DD--
+ } else {
+ DD += 2
+ }
+ }
+ } else if (dayRule.value === 'weekDay') {
+ // 濡傛灉鎸囧畾浜嗘槸鏄熸湡鍑�
+ // 鑾峰彇褰撳墠鏃ユ湡鏄睘浜庢槦鏈熷嚑
+ let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
+ // 鏍¢獙褰撳墠鏄熸湡鏄惁鍦ㄦ槦鏈熸睜锛坉ayRuleSup锛変腑
+ if (dayRuleSup.value.indexOf(thisWeek) < 0) {
+ // 濡傛灉鍒拌揪鏈�澶у�兼椂
+ if (Di === DDate.length - 1) {
+ resetDay()
+ if (Mi === MDate.length - 1) {
+ resetMonth()
+ continue goYear
+ }
+ continue goMonth
+ }
+ continue
+ }
+ } else if (dayRule.value === 'assWeek') {
+ // 濡傛灉鎸囧畾浜嗘槸绗嚑鍛ㄧ殑鏄熸湡鍑�
+ // 鑾峰彇姣忔湀1鍙锋槸灞炰簬鏄熸湡鍑�
+ let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week')
+ if (dayRuleSup.value[1] >= thisWeek) {
+ DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1
+ } else {
+ DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1
+ }
+ } else if (dayRule.value === 'lastWeek') {
+ // 濡傛灉鎸囧畾浜嗘瘡鏈堟渶鍚庝竴涓槦鏈熷嚑
+ // 鏍¢獙骞惰皟鏁村鏋滄槸2鏈�30鍙疯繖绉嶆棩鏈熶紶杩涙潵鏃堕渶璋冩暣鑷虫甯告湀搴�
+ if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+ while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+ DD--
+ thisDD = DD < 10 ? '0' + DD : DD
+ }
+ }
+ // 鑾峰彇鏈堟湯鏈�鍚庝竴澶╂槸鏄熸湡鍑�
+ let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week')
+ // 鎵惧埌瑕佹眰涓渶杩戠殑閭d釜鏄熸湡鍑�
+ if (dayRuleSup.value < thisWeek) {
+ DD -= thisWeek - dayRuleSup.value
+ } else if (dayRuleSup.value > thisWeek) {
+ DD -= 7 - (dayRuleSup.value - thisWeek)
+ }
+ }
+ // 鍒ゆ柇鏃堕棿鍊兼槸鍚﹀皬浜�10缃崲鎴愨��05鈥濊繖绉嶆牸寮�
+ DD = DD < 10 ? '0' + DD : DD
+ // 寰幆鈥滄椂鈥濇暟缁�
+ goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
+ let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
+ // 濡傛灉鍒拌揪鏈�澶у�兼椂
+ if (nMin > mDate[mDate.length - 1]) {
+ resetMin()
+ if (hi === hDate.length - 1) {
+ resetHour()
+ if (Di === DDate.length - 1) {
+ resetDay()
+ if (Mi === MDate.length - 1) {
+ resetMonth()
+ continue goYear
+ }
+ continue goMonth
+ }
+ continue goDay
+ }
+ continue
+ }
+ // 寰幆"鍒�"鏁扮粍
+ goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
+ let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi]
+ // 濡傛灉鍒拌揪鏈�澶у�兼椂
+ if (nSecond > sDate[sDate.length - 1]) {
+ resetSecond()
+ if (mi === mDate.length - 1) {
+ resetMin()
+ if (hi === hDate.length - 1) {
+ resetHour()
+ if (Di === DDate.length - 1) {
+ resetDay()
+ if (Mi === MDate.length - 1) {
+ resetMonth()
+ continue goYear
+ }
+ continue goMonth
+ }
+ continue goDay
+ }
+ continue goHour
+ }
+ continue
+ }
+ // 寰幆"绉�"鏁扮粍
+ goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
+ let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si]
+ // 娣诲姞褰撳墠鏃堕棿锛堟椂闂村悎娉曟�у湪鏃ユ湡寰幆鏃跺凡缁忓垽鏂級
+ if (MM !== '00' && DD !== '00') {
+ resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
+ nums++
+ }
+ // 濡傛灉鏉℃暟婊′簡灏遍��鍑哄惊鐜�
+ if (nums === 5) break goYear
+ // 濡傛灉鍒拌揪鏈�澶у�兼椂
+ if (si === sDate.length - 1) {
+ resetSecond()
+ if (mi === mDate.length - 1) {
+ resetMin()
+ if (hi === hDate.length - 1) {
+ resetHour()
+ if (Di === DDate.length - 1) {
+ resetDay()
+ if (Mi === MDate.length - 1) {
+ resetMonth()
+ continue goYear
+ }
+ continue goMonth
+ }
+ continue goDay
+ }
+ continue goHour
+ }
+ continue goMin
+ }
+ } //goSecond
+ } //goMin
+ }//goHour
+ }//goDay
+ }//goMonth
+ }
+ // 鍒ゆ柇100骞村唴鐨勭粨鏋滄潯鏁�
+ if (resultArr.length === 0) {
+ resultList.value = ['娌℃湁杈惧埌鏉′欢鐨勭粨鏋滐紒']
+ } else {
+ resultList.value = resultArr
+ if (resultArr.length !== 5) {
+ resultList.value.push('鏈�杩�100骞村唴鍙湁涓婇潰' + resultArr.length + '鏉$粨鏋滐紒')
+ }
+ }
+ // 璁$畻瀹屾垚-鏄剧ず缁撴灉
+ isShow.value = true
+}
+// 鐢ㄤ簬璁$畻鏌愪綅鏁板瓧鍦ㄦ暟缁勪腑鐨勭储寮�
+function getIndex(arr, value) {
+ if (value <= arr[0] || value > arr[arr.length - 1]) {
+ return 0
+ } else {
+ for (let i = 0; i < arr.length - 1; i++) {
+ if (value > arr[i] && value <= arr[i + 1]) {
+ return i + 1
+ }
+ }
+ }
+}
+// 鑾峰彇"骞�"鏁扮粍
+function getYearArr(rule, year) {
+ dateArr.value[5] = getOrderArr(year, year + 100)
+ if (rule !== undefined) {
+ if (rule.indexOf('-') >= 0) {
+ dateArr.value[5] = getCycleArr(rule, year + 100, false)
+ } else if (rule.indexOf('/') >= 0) {
+ dateArr.value[5] = getAverageArr(rule, year + 100)
+ } else if (rule !== '*') {
+ dateArr.value[5] = getAssignArr(rule)
+ }
+ }
+}
+// 鑾峰彇"鏈�"鏁扮粍
+function getMonthArr(rule) {
+ dateArr.value[4] = getOrderArr(1, 12)
+ if (rule.indexOf('-') >= 0) {
+ dateArr.value[4] = getCycleArr(rule, 12, false)
+ } else if (rule.indexOf('/') >= 0) {
+ dateArr.value[4] = getAverageArr(rule, 12)
+ } else if (rule !== '*') {
+ dateArr.value[4] = getAssignArr(rule)
+ }
+}
+// 鑾峰彇"鏃�"鏁扮粍-涓昏涓烘棩鏈熻鍒�
+function getWeekArr(rule) {
+ // 鍙湁褰撴棩鏈熻鍒欑殑涓や釜鍊煎潎涓衡�溾�濇椂鍒欒〃杈炬棩鏈熸槸鏈夐�夐」鐨�
+ if (dayRule.value === '' && dayRuleSup.value === '') {
+ if (rule.indexOf('-') >= 0) {
+ dayRule.value = 'weekDay'
+ dayRuleSup.value = getCycleArr(rule, 7, false)
+ } else if (rule.indexOf('#') >= 0) {
+ dayRule.value = 'assWeek'
+ let matchRule = rule.match(/[0-9]{1}/g)
+ dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])]
+ dateArr.value[3] = [1]
+ if (dayRuleSup.value[1] === 7) {
+ dayRuleSup.value[1] = 0
+ }
+ } else if (rule.indexOf('L') >= 0) {
+ dayRule.value = 'lastWeek'
+ dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
+ dateArr.value[3] = [31]
+ if (dayRuleSup.value === 7) {
+ dayRuleSup.value = 0
+ }
+ } else if (rule !== '*' && rule !== '?') {
+ dayRule.value = 'weekDay'
+ dayRuleSup.value = getAssignArr(rule)
+ }
+ }
+}
+// 鑾峰彇"鏃�"鏁扮粍-灏戦噺涓烘棩鏈熻鍒�
+function getDayArr(rule) {
+ dateArr.value[3] = getOrderArr(1, 31)
+ dayRule.value = ''
+ dayRuleSup.value = ''
+ if (rule.indexOf('-') >= 0) {
+ dateArr.value[3] = getCycleArr(rule, 31, false)
+ dayRuleSup.value = 'null'
+ } else if (rule.indexOf('/') >= 0) {
+ dateArr.value[3] = getAverageArr(rule, 31)
+ dayRuleSup.value = 'null'
+ } else if (rule.indexOf('W') >= 0) {
+ dayRule.value = 'workDay'
+ dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0])
+ dateArr.value[3] = [dayRuleSup.value]
+ } else if (rule.indexOf('L') >= 0) {
+ dayRule.value = 'lastDay'
+ dayRuleSup.value = 'null'
+ dateArr.value[3] = [31]
+ } else if (rule !== '*' && rule !== '?') {
+ dateArr.value[3] = getAssignArr(rule)
+ dayRuleSup.value = 'null'
+ } else if (rule === '*') {
+ dayRuleSup.value = 'null'
+ }
+}
+// 鑾峰彇"鏃�"鏁扮粍
+function getHourArr(rule) {
+ dateArr.value[2] = getOrderArr(0, 23)
+ if (rule.indexOf('-') >= 0) {
+ dateArr.value[2] = getCycleArr(rule, 24, true)
+ } else if (rule.indexOf('/') >= 0) {
+ dateArr.value[2] = getAverageArr(rule, 23)
+ } else if (rule !== '*') {
+ dateArr.value[2] = getAssignArr(rule)
+ }
+}
+// 鑾峰彇"鍒�"鏁扮粍
+function getMinArr(rule) {
+ dateArr.value[1] = getOrderArr(0, 59)
+ if (rule.indexOf('-') >= 0) {
+ dateArr.value[1] = getCycleArr(rule, 60, true)
+ } else if (rule.indexOf('/') >= 0) {
+ dateArr.value[1] = getAverageArr(rule, 59)
+ } else if (rule !== '*') {
+ dateArr.value[1] = getAssignArr(rule)
+ }
+}
+// 鑾峰彇"绉�"鏁扮粍
+function getSecondArr(rule) {
+ dateArr.value[0] = getOrderArr(0, 59)
+ if (rule.indexOf('-') >= 0) {
+ dateArr.value[0] = getCycleArr(rule, 60, true)
+ } else if (rule.indexOf('/') >= 0) {
+ dateArr.value[0] = getAverageArr(rule, 59)
+ } else if (rule !== '*') {
+ dateArr.value[0] = getAssignArr(rule)
+ }
+}
+// 鏍规嵁浼犺繘鏉ョ殑min-max杩斿洖涓�涓『搴忕殑鏁扮粍
+function getOrderArr(min, max) {
+ let arr = []
+ for (let i = min; i <= max; i++) {
+ arr.push(i)
+ }
+ return arr
+}
+// 鏍规嵁瑙勫垯涓寚瀹氱殑闆舵暎鍊艰繑鍥炰竴涓暟缁�
+function getAssignArr(rule) {
+ let arr = []
+ let assiginArr = rule.split(',')
+ for (let i = 0; i < assiginArr.length; i++) {
+ arr[i] = Number(assiginArr[i])
+ }
+ arr.sort(compare)
+ return arr
+}
+// 鏍规嵁涓�瀹氱畻鏈鍒欒绠楄繑鍥炰竴涓暟缁�
+function getAverageArr(rule, limit) {
+ let arr = []
+ let agArr = rule.split('/')
+ let min = Number(agArr[0])
+ let step = Number(agArr[1])
+ while (min <= limit) {
+ arr.push(min)
+ min += step
+ }
+ return arr
+}
+// 鏍规嵁瑙勫垯杩斿洖涓�涓叿鏈夊懆鏈熸�х殑鏁扮粍
+function getCycleArr(rule, limit, status) {
+ // status--琛ㄧず鏄惁浠�0寮�濮嬶紙鍒欎粠1寮�濮嬶級
+ let arr = []
+ let cycleArr = rule.split('-')
+ let min = Number(cycleArr[0])
+ let max = Number(cycleArr[1])
+ if (min > max) {
+ max += limit
+ }
+ for (let i = min; i <= max; i++) {
+ let add = 0
+ if (status === false && i % limit === 0) {
+ add = limit
+ }
+ arr.push(Math.round(i % limit + add))
+ }
+ arr.sort(compare)
+ return arr
+}
+// 姣旇緝鏁板瓧澶у皬锛堢敤浜嶢rray.sort锛�
+function compare(value1, value2) {
+ if (value2 - value1 > 0) {
+ return -1
+ } else {
+ return 1
+ }
+}
+// 鏍煎紡鍖栨棩鏈熸牸寮忓锛�2017-9-19 18:04:33
+function formatDate(value, type) {
+ // 璁$畻鏃ユ湡鐩稿叧鍊�
+ let time = typeof value == 'number' ? new Date(value) : value
+ let Y = time.getFullYear()
+ let M = time.getMonth() + 1
+ let D = time.getDate()
+ let h = time.getHours()
+ let m = time.getMinutes()
+ let s = time.getSeconds()
+ let week = time.getDay()
+ // 濡傛灉浼犻�掍簡type鐨勮瘽
+ if (type === undefined) {
+ return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s)
+ } else if (type === 'week') {
+ // 鍦╭uartz涓� 1涓烘槦鏈熸棩
+ return week + 1
+ }
+}
+// 妫�鏌ユ棩鏈熸槸鍚﹀瓨鍦�
+function checkDate(value) {
+ let time = new Date(value)
+ let format = formatDate(time)
+ return value === format
+}
+onMounted(() => {
+ expressionChange()
+})
+</script>
\ No newline at end of file
diff --git a/src/components/Crontab/second.vue b/src/components/Crontab/second.vue
new file mode 100644
index 0000000..05e8855
--- /dev/null
+++ b/src/components/Crontab/second.vue
@@ -0,0 +1,128 @@
+<template>
+ <el-form>
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="1">
+ 绉掞紝鍏佽鐨勯�氶厤绗, - * /]
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="2">
+ 鍛ㄦ湡浠�
+ <el-input-number v-model='cycle01' :min="0" :max="58" /> -
+ <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 绉�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="3">
+ 浠�
+ <el-input-number v-model='average01' :min="0" :max="58" /> 绉掑紑濮嬶紝姣�
+ <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 绉掓墽琛屼竴娆�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="4">
+ 鎸囧畾
+ <el-select clearable v-model="checkboxList" placeholder="鍙閫�" multiple :multiple-limit="10">
+ <el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+ cron: {
+ type: Object,
+ default: {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: "",
+ }
+ },
+ check: {
+ type: Function,
+ default: () => {
+ }
+ }
+})
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(1)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([0])
+const cycleTotal = computed(() => {
+ cycle01.value = props.check(cycle01.value, 0, 58)
+ cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
+ return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+ average01.value = props.check(average01.value, 0, 58)
+ average02.value = props.check(average02.value, 1, 59 - average01.value)
+ return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+ return checkboxList.value.join(',')
+})
+watch(() => props.cron.second, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+ if (value === '*') {
+ radioValue.value = 1
+ } else if (value.indexOf('-') > -1) {
+ const indexArr = value.split('-')
+ cycle01.value = Number(indexArr[0])
+ cycle02.value = Number(indexArr[1])
+ radioValue.value = 2
+ } else if (value.indexOf('/') > -1) {
+ const indexArr = value.split('/')
+ average01.value = Number(indexArr[0])
+ average02.value = Number(indexArr[1])
+ radioValue.value = 3
+ } else {
+ checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+ radioValue.value = 4
+ }
+}
+// 鍗曢�夋寜閽�煎彉鍖栨椂
+function onRadioChange() {
+ switch (radioValue.value) {
+ case 1:
+ emit('update', 'second', '*', 'second')
+ break
+ case 2:
+ emit('update', 'second', cycleTotal.value, 'second')
+ break
+ case 3:
+ emit('update', 'second', averageTotal.value, 'second')
+ break
+ case 4:
+ if (checkboxList.value.length === 0) {
+ checkboxList.value.push(checkCopy.value[0])
+ } else {
+ checkCopy.value = checkboxList.value
+ }
+ emit('update', 'second', checkboxString.value, 'second')
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+ margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+ width: 18.8rem;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Crontab/week.vue b/src/components/Crontab/week.vue
new file mode 100644
index 0000000..69a03b0
--- /dev/null
+++ b/src/components/Crontab/week.vue
@@ -0,0 +1,197 @@
+<template>
+ <el-form>
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="1">
+ 鍛紝鍏佽鐨勯�氶厤绗, - * ? / L #]
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="2">
+ 涓嶆寚瀹�
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="3">
+ 鍛ㄦ湡浠�
+ <el-select clearable v-model="cycle01">
+ <el-option
+ v-for="(item,index) of weekList"
+ :key="index"
+ :label="item.value"
+ :value="item.key"
+ :disabled="item.key === 7"
+ >{{item.value}}</el-option>
+ </el-select>
+ -
+ <el-select clearable v-model="cycle02">
+ <el-option
+ v-for="(item,index) of weekList"
+ :key="index"
+ :label="item.value"
+ :value="item.key"
+ :disabled="item.key <= cycle01"
+ >{{item.value}}</el-option>
+ </el-select>
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="4">
+ 绗�
+ <el-input-number v-model='average01' :min="1" :max="4" /> 鍛ㄧ殑
+ <el-select clearable v-model="average02">
+ <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="5">
+ 鏈湀鏈�鍚庝竴涓�
+ <el-select clearable v-model="weekday">
+ <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio v-model='radioValue' :value="6">
+ 鎸囧畾
+ <el-select class="multiselect" clearable v-model="checkboxList" placeholder="鍙閫�" multiple :multiple-limit="6">
+ <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+
+ </el-form>
+</template>
+
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+ cron: {
+ type: Object,
+ default: {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: ""
+ }
+ },
+ check: {
+ type: Function,
+ default: () => {
+ }
+ }
+})
+const radioValue = ref(2)
+const cycle01 = ref(2)
+const cycle02 = ref(3)
+const average01 = ref(1)
+const average02 = ref(2)
+const weekday = ref(2)
+const checkboxList = ref([])
+const checkCopy = ref([2])
+const weekList = ref([
+ {key: 1, value: '鏄熸湡鏃�'},
+ {key: 2, value: '鏄熸湡涓�'},
+ {key: 3, value: '鏄熸湡浜�'},
+ {key: 4, value: '鏄熸湡涓�'},
+ {key: 5, value: '鏄熸湡鍥�'},
+ {key: 6, value: '鏄熸湡浜�'},
+ {key: 7, value: '鏄熸湡鍏�'}
+])
+const cycleTotal = computed(() => {
+ cycle01.value = props.check(cycle01.value, 1, 6)
+ cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
+ return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+ average01.value = props.check(average01.value, 1, 4)
+ average02.value = props.check(average02.value, 1, 7)
+ return average02.value + '#' + average01.value
+})
+const weekdayTotal = computed(() => {
+ weekday.value = props.check(weekday.value, 1, 7)
+ return weekday.value + 'L'
+})
+const checkboxString = computed(() => {
+ return checkboxList.value.join(',')
+})
+watch(() => props.cron.week, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+ if (value === "*") {
+ radioValue.value = 1
+ } else if (value === "?") {
+ radioValue.value = 2
+ } else if (value.indexOf("-") > -1) {
+ const indexArr = value.split('-')
+ cycle01.value = Number(indexArr[0])
+ cycle02.value = Number(indexArr[1])
+ radioValue.value = 3
+ } else if (value.indexOf("#") > -1) {
+ const indexArr = value.split('#')
+ average01.value = Number(indexArr[1])
+ average02.value = Number(indexArr[0])
+ radioValue.value = 4
+ } else if (value.indexOf("L") > -1) {
+ const indexArr = value.split("L")
+ weekday.value = Number(indexArr[0])
+ radioValue.value = 5
+ } else {
+ checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+ radioValue.value = 6
+ }
+}
+function onRadioChange() {
+ if (radioValue.value === 2 && props.cron.day === '?') {
+ emit('update', 'day', '*', 'week')
+ }
+ if (radioValue.value !== 2 && props.cron.day !== '?') {
+ emit('update', 'day', '?', 'week')
+ }
+ switch (radioValue.value) {
+ case 1:
+ emit('update', 'week', '*', 'week')
+ break
+ case 2:
+ emit('update', 'week', '?', 'week')
+ break
+ case 3:
+ emit('update', 'week', cycleTotal.value, 'week')
+ break
+ case 4:
+ emit('update', 'week', averageTotal.value, 'week')
+ break
+ case 5:
+ emit('update', 'week', weekdayTotal.value, 'week')
+ break
+ case 6:
+ if (checkboxList.value.length === 0) {
+ checkboxList.value.push(checkCopy.value[0])
+ } else {
+ checkCopy.value = checkboxList.value
+ }
+ emit('update', 'week', checkboxString.value, 'week')
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+ margin: 0 0.5rem;
+}
+.el-select, .el-select--small {
+ width: 8rem;
+}
+.el-select.multiselect, .el-select--small.multiselect {
+ width: 17.8rem;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Crontab/year.vue b/src/components/Crontab/year.vue
new file mode 100644
index 0000000..01b58f3
--- /dev/null
+++ b/src/components/Crontab/year.vue
@@ -0,0 +1,143 @@
+<template>
+ <el-form>
+ <el-form-item>
+ <el-radio :value="1" v-model='radioValue'>
+ 涓嶅~锛屽厑璁哥殑閫氶厤绗, - * /]
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio :value="2" v-model='radioValue'>
+ 姣忓勾
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio :value="3" v-model='radioValue'>
+ 鍛ㄦ湡浠�
+ <el-input-number v-model='cycle01' :min='fullYear' :max="2098"/> -
+ <el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099"/>
+ </el-radio>
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio :value="4" v-model='radioValue'>
+ 浠�
+ <el-input-number v-model='average01' :min='fullYear' :max="2098"/> 骞村紑濮嬶紝姣�
+ <el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 骞存墽琛屼竴娆�
+ </el-radio>
+
+ </el-form-item>
+
+ <el-form-item>
+ <el-radio :value="5" v-model='radioValue'>
+ 鎸囧畾
+ <el-select clearable v-model="checkboxList" placeholder="鍙閫�" multiple :multiple-limit="8">
+ <el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
+ </el-select>
+ </el-radio>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+ cron: {
+ type: Object,
+ default: {
+ second: "*",
+ min: "*",
+ hour: "*",
+ day: "*",
+ month: "*",
+ week: "?",
+ year: ""
+ }
+ },
+ check: {
+ type: Function,
+ default: () => {
+ }
+ }
+})
+
+const fullYear = Number(new Date().getFullYear())
+const maxFullYear = fullYear + 10
+const radioValue = ref(1)
+const cycle01 = ref(fullYear)
+const cycle02 = ref(fullYear + 1)
+const average01 = ref(fullYear)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([fullYear])
+
+const cycleTotal = computed(() => {
+ cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
+ cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
+ return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+ average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
+ average02.value = props.check(average02.value, 1, 10)
+ return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+ return checkboxList.value.join(',')
+})
+watch(() => props.cron.year, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+ if (value === '') {
+ radioValue.value = 1
+ } else if (value === "*") {
+ radioValue.value = 2
+ } else if (value.indexOf("-") > -1) {
+ const indexArr = value.split('-')
+ cycle01.value = Number(indexArr[0])
+ cycle02.value = Number(indexArr[1])
+ radioValue.value = 3
+ } else if (value.indexOf("/") > -1) {
+ const indexArr = value.split('/')
+ average01.value = Number(indexArr[0])
+ average02.value = Number(indexArr[1])
+ radioValue.value = 4
+ } else {
+ checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+ radioValue.value = 5
+ }
+}
+function onRadioChange() {
+ switch (radioValue.value) {
+ case 1:
+ emit('update', 'year', '', 'year')
+ break
+ case 2:
+ emit('update', 'year', '*', 'year')
+ break
+ case 3:
+ emit('update', 'year', cycleTotal.value, 'year')
+ break
+ case 4:
+ emit('update', 'year', averageTotal.value, 'year')
+ break
+ case 5:
+ if (checkboxList.value.length === 0) {
+ checkboxList.value.push(checkCopy.value[0])
+ } else {
+ checkCopy.value = checkboxList.value
+ }
+ emit('update', 'year', checkboxString.value, 'year')
+ break
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+ margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+ width: 18.8rem;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/Dialog/FileList.vue b/src/components/Dialog/FileList.vue
new file mode 100644
index 0000000..b0e78cf
--- /dev/null
+++ b/src/components/Dialog/FileList.vue
@@ -0,0 +1,263 @@
+<template>
+ <el-dialog v-model="isShow"
+ :title="title"
+ :width="width"
+ @close="handleClose"
+ class="attachment-dialog">
+ <!-- 宸ュ叿鏍� -->
+ <div v-if="editable"
+ class="toolbar">
+ <el-button type="primary"
+ size="small"
+ @click="handleUpload">
+ 涓婁紶闄勪欢
+ </el-button>
+ </div>
+ <!-- 涓婁紶缁勪欢寮圭獥 -->
+ <el-dialog v-model="uploadDialogVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeUpload">
+ <AttachmentUpload v-model:file-list="newFileList" />
+ <template #footer>
+ <el-button @click="saveUpload">淇濆瓨</el-button>
+ <el-button @click="closeUpload">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ <!-- 鏂囦欢鍒楄〃琛ㄦ牸 -->
+ <div class="table-container">
+ <el-table :data="tableData"
+ border
+ class="attachment-table"
+ :height="tableData.length > 0 ? 'auto' : '120px'">
+ <el-table-column label="闄勪欢鍚嶇О"
+ prop="originalFilename"
+ show-overflow-tooltip />
+ <el-table-column v-if="showActions"
+ fixed="right"
+ label="鎿嶄綔"
+ :width="150"
+ align="center">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ size="small"
+ class="download-link"
+ @click="previewFile(scope.row.previewURL)">
+ 棰勮
+ </el-button>
+ <el-button link
+ type="primary"
+ size="small"
+ class="download-link"
+ @click="downloadFile(scope.row.downloadURL)">
+ 涓嬭浇
+ </el-button>
+ <el-button v-if="editable"
+ link
+ type="danger"
+ size="small"
+ @click="handleDelete(scope.row)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ElMessage } from 'element-plus'
+ import { ref, computed, getCurrentInstance, onMounted, watch } from "vue";
+ import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue";
+ import {
+ attachmentList,
+ deleteAttachment,
+ createAttachment,
+ } from "@/api/basicData/storageAttachment.js";
+ import filePreview from '@/components/filePreview/index.vue'
+ const filePreviewRef = ref()
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ recordType: {
+ type: String,
+ default: "",
+ required: true,
+ },
+ recordId: {
+ type: Number,
+ default: 0,
+ required: true,
+ },
+ title: {
+ type: String,
+ default: "闄勪欢",
+ },
+ width: {
+ type: String,
+ default: "50%",
+ },
+ showActions: {
+ type: Boolean,
+ default: true,
+ },
+ editable: {
+ type: Boolean,
+ default: true,
+ },
+ });
+
+ const emit = defineEmits(["close", "download", "upload", "delete"]);
+
+ const { proxy } = getCurrentInstance();
+ const tableData = ref([]);
+ const uploadDialogVisible = ref(false);
+ const newFileList = ref([]);
+
+ const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+ });
+
+ const handleClose = () => {
+ isShow.value = false;
+ };
+
+ // 棰勮鏂囦欢
+ const previewFile = (url) => {
+ if (url) {
+ filePreviewRef.value.open(url)
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�')
+ }
+ }
+
+ const handleUpload = () => {
+ uploadDialogVisible.value = true;
+ };
+
+ const saveUpload = async () => {
+ // 妫�鏌ユ槸鍚︽湁鏂颁笂浼犵殑鏂囦欢
+ if (newFileList.value.length > 0) {
+ createAttachment({
+ application: "file",
+ recordType: props.recordType,
+ recordId: props.recordId,
+ storageBlobDTOs: [...newFileList.value, ...tableData.value],
+ }).then((res) => {
+ if (res && res.code === 200) {
+ proxy?.$modal?.msgSuccess("涓婁紶鎴愬姛");
+ newFileList.value = [];
+ // 鍒锋柊鍒楄〃
+ setList();
+ }
+ }).finally(() => {
+ uploadDialogVisible.value = false;
+ })
+ }
+ }
+
+ const closeUpload = () => {
+ newFileList.value = [];
+ uploadDialogVisible.value = false;
+ };
+
+ const handleDelete = async (row, index) => {
+ deleteAttachment([row.storageAttachmentId]).then((res) => {
+ if (res && res.code === 200) {
+ proxy?.$modal?.msgSuccess("鍒犻櫎鎴愬姛");
+ setList();
+ }
+ })
+ };
+
+ const setList = () => {
+ attachmentList({
+ recordType: props.recordType,
+ recordId: props.recordId,
+ }).then(res => {
+ tableData.value = (res && res.data) || [];
+ });
+ };
+
+ const downloadFile = url => {
+ window.open(url, "_blank");
+ };
+ onMounted(() => {
+ setList();
+ });
+</script>
+
+<style scoped>
+ .attachment-dialog {
+ border-radius: 12px;
+ }
+
+ .toolbar {
+ margin-bottom: 16px;
+ text-align: right;
+ }
+
+ .table-container {
+ max-height: 40vh;
+ overflow-y: auto;
+ min-height: 120px;
+ padding-bottom: 16px;
+ box-sizing: border-box;
+ will-change: scroll-position;
+ transform: translateZ(0);
+ -webkit-overflow-scrolling: touch;
+ }
+
+ :deep(.el-table) {
+ margin-bottom: 0;
+ }
+
+ :deep(.el-table__body-wrapper) {
+ overflow-y: auto;
+ will-change: transform;
+ transform: translateZ(0);
+ }
+
+ :deep(.el-table__body tr) {
+ transition: none;
+ }
+
+ :deep(.el-dialog__footer) {
+ padding-top: 12px;
+ border-top: 1px solid #e9ecef;
+ }
+
+ .attachment-table {
+ border-radius: 8px;
+ }
+
+ :deep(.el-dialog__header) {
+ background-color: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ padding: 16px 20px;
+ }
+
+ :deep(.el-dialog__title) {
+ font-size: 16px;
+ font-weight: 600;
+ }
+
+ :deep(.el-dialog__body) {
+ padding: 16px 20px;
+ }
+
+ :deep(.el-table__empty-text) {
+ color: #999;
+ }
+</style>
\ No newline at end of file
diff --git a/src/components/Dialog/FileListDialog.vue b/src/components/Dialog/FileListDialog.vue
new file mode 100644
index 0000000..6fea795
--- /dev/null
+++ b/src/components/Dialog/FileListDialog.vue
@@ -0,0 +1,329 @@
+<template>
+ <el-dialog v-model="dialogVisible"
+ :title="title"
+ :width="width"
+ :before-close="handleClose">
+ <div class="file-list-toolbar"
+ v-if="showToolbar">
+ <template v-if="useBuiltInUpload">
+ <el-upload v-model:file-list="uploadFileList"
+ class="upload-demo"
+ :action="uploadAction"
+ :headers="uploadHeaders"
+ :show-file-list="false"
+ :on-success="handleDefaultUploadSuccess"
+ :on-error="handleDefaultUploadError">
+ <el-button v-if="showUploadButton"
+ type="primary"
+ size="small">
+ 涓婁紶闄勪欢
+ </el-button>
+ </el-upload>
+ </template>
+ <template v-else>
+ <el-button v-if="showUploadButton"
+ type="primary"
+ size="small"
+ @click="handleUpload">
+ 鏂板闄勪欢
+ </el-button>
+ </template>
+ </div>
+ <el-table :data="tableData"
+ border
+ :height="tableHeight">
+ <el-table-column :label="nameColumnLabel"
+ :prop="nameColumnProp"
+ :min-width="nameColumnMinWidth"
+ show-overflow-tooltip />
+ <el-table-column v-if="showActions"
+ fixed="right"
+ label="鎿嶄綔"
+ :width="actionColumnWidth"
+ align="center">
+ <template #default="scope">
+ <el-button v-if="showDownload"
+ link
+ type="primary"
+ size="small"
+ @click="handleDownload(scope.row)">
+ 涓嬭浇
+ </el-button>
+ <el-button v-if="showPreview"
+ link
+ type="primary"
+ size="small"
+ @click="handlePreview(scope.row)">
+ 棰勮
+ </el-button>
+ <el-button v-if="showDeleteButton"
+ link
+ type="danger"
+ size="small"
+ @click="handleDelete(scope.row, scope.$index)">
+ 鍒犻櫎
+ </el-button>
+ <slot name="actions"
+ :row="scope.row"></slot>
+ </template>
+ </el-table-column>
+ <slot name="columns"></slot>
+ </el-table>
+ <pagination v-if="isShowPagination"
+ style="margin-bottom: 20px;"
+ :total="page.total"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationSearch"
+ @change="handleChange" />
+ </el-dialog>
+<!-- // todo 闄勪欢棰勮鐩稿叧 -->
+ <filePreview v-if="showPreview"
+ ref="filePreviewRef" />
+</template>
+
+<script setup>
+ import { ref, computed, getCurrentInstance } from "vue";
+ import pagination from "@/components/Pagination/index.vue";
+ import { ElMessage } from "element-plus";
+ import filePreview from "@/components/filePreview/index.vue";
+ import { getToken } from "@/utils/auth";
+
+ const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false,
+ },
+ title: {
+ type: String,
+ default: "闄勪欢",
+ },
+ width: {
+ type: String,
+ default: "40%",
+ },
+ tableHeight: {
+ type: String,
+ default: "40vh",
+ },
+ nameColumnLabel: {
+ type: String,
+ default: "闄勪欢鍚嶇О",
+ },
+ nameColumnProp: {
+ type: String,
+ default: "name",
+ },
+ nameColumnMinWidth: {
+ type: [String, Number],
+ default: 400,
+ },
+ actionColumnWidth: {
+ type: [String, Number],
+ default: 160,
+ },
+ showActions: {
+ type: Boolean,
+ default: true,
+ },
+ showDownload: {
+ type: Boolean,
+ default: true,
+ },
+ showPreview: {
+ type: Boolean,
+ default: true,
+ },
+ showUploadButton: {
+ type: Boolean,
+ default: false,
+ },
+ showDeleteButton: {
+ type: Boolean,
+ default: false,
+ },
+ urlField: {
+ type: String,
+ default: "url",
+ },
+ downloadMethod: {
+ type: Function,
+ default: null,
+ },
+ previewMethod: {
+ type: Function,
+ default: null,
+ },
+ uploadMethod: {
+ type: Function,
+ default: null,
+ },
+ deleteMethod: {
+ type: Function,
+ default: null,
+ },
+ rulesRegulationsManagementId: {
+ type: [String, Number],
+ default: "",
+ },
+ uploadUrl: {
+ type: String,
+ default: `${import.meta.env.VITE_APP_BASE_API}/file/upload`,
+ },
+ isShowPagination: {
+ type: Boolean,
+ default: false,
+ },
+ page: {
+ type: Object,
+ default: () => ({
+ current: 1,
+ size: 10,
+ total: 0,
+ }),
+ },
+ });
+
+ const emit = defineEmits([
+ "update:modelValue",
+ "close",
+ "download",
+ "preview",
+ "upload",
+ "delete",
+ ]);
+
+ const { proxy } = getCurrentInstance();
+ const filePreviewRef = ref(null);
+ const uploadFileList = ref([]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const tableData = ref([]);
+ const showToolbar = computed(() => props.showUploadButton);
+ const useBuiltInUpload = computed(() => !props.uploadMethod);
+ const uploadAction = computed(() => props.uploadUrl);
+ const uploadHeaders = computed(() => ({
+ Authorization: `Bearer ${getToken()}`,
+ }));
+
+ const handleClose = () => {
+ emit("close");
+ dialogVisible.value = false;
+ };
+
+ const handleDownload = row => {
+ if (props.downloadMethod) {
+ props.downloadMethod(row);
+ } else {
+ // 榛樿涓嬭浇鏂规硶
+ proxy.$download.name(row[props.urlField]);
+ }
+ emit("download", row);
+ };
+
+ const handlePreview = row => {
+ if (props.previewMethod) {
+ props.previewMethod(row);
+ } else {
+ // 榛樿棰勮鏂规硶
+ if (filePreviewRef.value) {
+ filePreviewRef.value.open(row[props.urlField]);
+ }
+ }
+ emit("preview", row);
+ };
+ const paginationSearch = page => {
+ props.page.current = page.page;
+ props.page.size = page.limit;
+ emit("pagination", page.page, page.limit);
+ };
+
+ const open = list => {
+ dialogVisible.value = true;
+ tableData.value = list || [];
+ };
+
+ const handleUpload = async () => {
+ if (props.uploadMethod) {
+ // 濡傛灉鎻愪緵浜嗚嚜瀹氫箟涓婁紶鏂规硶锛岀敱鐖剁粍浠惰礋璐f洿鏂板垪琛紙閫氳繃 setList锛�
+ // 杩欓噷涓嶅啀鑷姩娣诲姞锛岄伩鍏嶄笌鐖剁粍浠剁殑 setList 閲嶅
+ await props.uploadMethod();
+ }
+ emit("upload");
+ };
+
+ const handleDelete = async (row, index) => {
+ if (props.deleteMethod) {
+ const result = await props.deleteMethod(row, index);
+ if (result === false) {
+ return;
+ }
+ // 濡傛灉鎻愪緵浜� deleteMethod锛岀敱鐖剁粍浠惰礋璐e埛鏂板垪琛紝涓嶅湪杩欓噷鍒犻櫎
+ } else {
+ // 濡傛灉娌℃湁鎻愪緵 deleteMethod锛屾墠鍦ㄧ粍浠跺唴閮ㄥ垹闄�
+ removeAttachment(index);
+ }
+ emit("delete", row);
+ };
+
+ const addAttachment = item => {
+ tableData.value = [...tableData.value, item];
+ };
+
+ const handleDefaultUploadSuccess = async (res, file) => {
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || "鏂囦欢涓婁紶澶辫触");
+ return;
+ }
+ if (!props.rulesRegulationsManagementId) {
+ ElMessage.error("缂哄皯瑙勭珷鍒跺害ID锛屾棤娉曚繚瀛橀檮浠�");
+ return;
+ }
+ const fileName = res?.data?.originalName || file?.name;
+ const fileUrl = res?.data?.tempPath || res?.data?.url;
+ const payload = {
+ fileName,
+ fileUrl,
+ rulesRegulationsManagementId: props.rulesRegulationsManagementId,
+ raw: res?.data || {},
+ };
+ emit("upload", payload);
+ };
+
+ const handleDefaultUploadError = () => {
+ ElMessage.error("鏂囦欢涓婁紶澶辫触");
+ };
+
+ const removeAttachment = index => {
+ if (index > -1 && index < tableData.value.length) {
+ const newList = [...tableData.value];
+ newList.splice(index, 1);
+ tableData.value = newList;
+ }
+ };
+
+ const setList = list => {
+ tableData.value = list || [];
+ };
+
+ defineExpose({
+ open,
+ addAttachment,
+ removeAttachment,
+ setList,
+ handleUpload,
+ handleDelete,
+ });
+</script>
+
+<style scoped>
+ .file-list-toolbar {
+ margin-bottom: 8px;
+ text-align: right;
+ }
+</style>
+
diff --git a/src/components/Dialog/FormDialog.vue b/src/components/Dialog/FormDialog.vue
new file mode 100644
index 0000000..b60bfb4
--- /dev/null
+++ b/src/components/Dialog/FormDialog.vue
@@ -0,0 +1,86 @@
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ :title="computedTitle"
+ :width="width"
+ @close="handleClose"
+ >
+ <slot></slot>
+ <template #footer>
+ <div class="dialog-footer">
+ <!-- 鑷畾涔夋寜閽彃妲� -->
+ <slot name="footer">
+ <!-- 榛樿鎸夐挳 -->
+ <el-button
+ v-if="showConfirm"
+ type="primary"
+ @click="handleConfirm"
+ >
+ 纭
+ </el-button>
+ <el-button @click="handleCancel">鍙栨秷</el-button>
+ </slot>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false
+ },
+ title: {
+ type: [String, Function],
+ default: ''
+ },
+ operationType: {
+ type: String,
+ default: ''
+ },
+ width: {
+ type: String,
+ default: '70%'
+ }
+})
+
+const emit = defineEmits(['update:modelValue', 'close', 'confirm', 'cancel'])
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+// 璇︽儏妯″紡涓嶅睍绀衡�滅‘璁も�濇寜閽紝鍏跺畠绫诲瀷姝e父鏄剧ず
+const showConfirm = computed(() => props.operationType !== 'detail' && props.operationType !== 'view')
+
+const computedTitle = computed(() => {
+ if (typeof props.title === 'function') {
+ return props.title(props.operationType)
+ }
+ return props.title
+})
+
+const handleClose = () => {
+ emit('close')
+}
+
+const handleConfirm = () => {
+ emit('confirm')
+}
+
+const handleCancel = () => {
+ emit('cancel')
+ dialogVisible.value = false
+}
+</script>
+
+<style scoped>
+.dialog-footer {
+ text-align: center;
+}
+</style>
+
diff --git a/src/components/Dialog/ImportDialog.vue b/src/components/Dialog/ImportDialog.vue
new file mode 100644
index 0000000..5b126dc
--- /dev/null
+++ b/src/components/Dialog/ImportDialog.vue
@@ -0,0 +1,172 @@
+<template>
+ <el-dialog
+ :title="title"
+ v-model="dialogVisible"
+ :width="width"
+ :append-to-body="appendToBody"
+ @close="handleClose"
+ >
+ <el-upload
+ ref="uploadRef"
+ :limit="limit"
+ :accept="accept"
+ :headers="headers"
+ :action="action"
+ :disabled="disabled"
+ :before-upload="beforeUpload"
+ :on-progress="onProgress"
+ :on-success="onSuccess"
+ :on-error="onError"
+ :on-change="onChange"
+ :auto-upload="autoUpload"
+ drag
+ >
+ <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>{{ tipText }}</span>
+ <el-link
+ v-if="showDownloadTemplate"
+ type="primary"
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline; margin-left: 5px;"
+ @click="handleDownloadTemplate"
+ >涓嬭浇妯℃澘</el-link
+ >
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleConfirm">纭� 瀹�</el-button>
+ <el-button @click="handleCancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import { UploadFilled } from '@element-plus/icons-vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false
+ },
+ title: {
+ type: String,
+ default: '瀵煎叆'
+ },
+ width: {
+ type: String,
+ default: '400px'
+ },
+ appendToBody: {
+ type: Boolean,
+ default: true
+ },
+ limit: {
+ type: Number,
+ default: 1
+ },
+ accept: {
+ type: String,
+ default: '.xlsx, .xls'
+ },
+ headers: {
+ type: Object,
+ default: () => ({})
+ },
+ action: {
+ type: String,
+ required: true
+ },
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+ autoUpload: {
+ type: Boolean,
+ default: false
+ },
+ tipText: {
+ type: String,
+ default: '浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�'
+ },
+ showDownloadTemplate: {
+ type: Boolean,
+ default: true
+ },
+ beforeUpload: {
+ type: Function,
+ default: null
+ },
+ onProgress: {
+ type: Function,
+ default: null
+ },
+ onSuccess: {
+ type: Function,
+ default: null
+ },
+ onError: {
+ type: Function,
+ default: null
+ },
+ onChange: {
+ type: Function,
+ default: null
+ }
+})
+
+const emit = defineEmits(['update:modelValue', 'close', 'confirm', 'cancel', 'download-template'])
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const uploadRef = ref(null)
+
+const handleClose = () => {
+ emit('close')
+}
+
+const handleConfirm = () => {
+ emit('confirm')
+}
+
+const submit = () => {
+ if (uploadRef.value) {
+ uploadRef.value.submit()
+ }
+}
+
+const handleCancel = () => {
+ emit('cancel')
+ dialogVisible.value = false
+}
+
+const handleDownloadTemplate = () => {
+ emit('download-template')
+}
+
+defineExpose({
+ uploadRef,
+ submit,
+ clearFiles: () => {
+ if (uploadRef.value) {
+ uploadRef.value.clearFiles()
+ }
+ }
+})
+</script>
+
+<style scoped>
+.dialog-footer {
+ text-align: center;
+}
+</style>
+
diff --git a/src/components/DictTag/index.vue b/src/components/DictTag/index.vue
new file mode 100644
index 0000000..5e70502
--- /dev/null
+++ b/src/components/DictTag/index.vue
@@ -0,0 +1,82 @@
+<template>
+ <div>
+ <template v-for="(item, index) in options">
+ <template v-if="values.includes(item.value)">
+ <span
+ v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
+ :key="item.value"
+ :index="index"
+ :class="item.elTagClass"
+ >{{ item.label + " " }}</span>
+ <el-tag
+ v-else
+ :disable-transitions="true"
+ :key="item.value + ''"
+ :index="index"
+ :type="item.elTagType"
+ :class="item.elTagClass"
+ >{{ item.label + " " }}</el-tag>
+ </template>
+ </template>
+ <template v-if="unmatch && showValue">
+ {{ unmatchArray | handleArray }}
+ </template>
+ </div>
+</template>
+
+<script setup>
+// 璁板綍鏈尮閰嶇殑椤�
+const unmatchArray = ref([])
+
+const props = defineProps({
+ // 鏁版嵁
+ options: {
+ type: Array,
+ default: null,
+ },
+ // 褰撳墠鐨勫��
+ value: [Number, String, Array],
+ // 褰撴湭鎵惧埌鍖归厤鐨勬暟鎹椂锛屾樉绀簐alue
+ showValue: {
+ type: Boolean,
+ default: true,
+ },
+ separator: {
+ type: String,
+ default: ",",
+ }
+})
+
+const values = computed(() => {
+ if (props.value === null || typeof props.value === 'undefined' || props.value === '') return []
+ return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator)
+})
+
+const unmatch = computed(() => {
+ unmatchArray.value = []
+ // 娌℃湁value涓嶆樉绀�
+ if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
+ // 浼犲叆鍊间负鏁扮粍
+ let unmatch = false // 娣诲姞涓�涓爣蹇楁潵鍒ゆ柇鏄惁鏈夋湭鍖归厤椤�
+ values.value.forEach(item => {
+ if (!props.options.some(v => v.value === item)) {
+ unmatchArray.value.push(item)
+ unmatch = true // 濡傛灉鏈夋湭鍖归厤椤癸紝灏嗘爣蹇楄缃负true
+ }
+ })
+ return unmatch // 杩斿洖鏍囧織鐨勫��
+})
+
+function handleArray(array) {
+ if (array.length === 0) return ""
+ return array.reduce((pre, cur) => {
+ return pre + " " + cur
+ })
+}
+</script>
+
+<style scoped>
+.el-tag + .el-tag {
+ margin-left: 10px;
+}
+</style>
diff --git a/src/components/DynamicTable/index.vue b/src/components/DynamicTable/index.vue
new file mode 100644
index 0000000..9da9a3c
--- /dev/null
+++ b/src/components/DynamicTable/index.vue
@@ -0,0 +1,402 @@
+<template>
+ <div class="dynamic-table-container">
+ <el-table
+ ref="tableRef"
+ v-loading="loading"
+ :data="tableData"
+ :border="border"
+ :height="height"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ style="width: 100%"
+ @selection-change="handleSelectionChange"
+ @row-click="handleRowClick"
+ >
+ <!-- 閫夋嫨鍒� -->
+ <el-table-column
+ v-if="showSelection"
+ align="center"
+ type="selection"
+ width="55"
+ />
+
+ <!-- 搴忓彿鍒� -->
+ <el-table-column
+ v-if="showIndex"
+ align="center"
+ label="搴忓彿"
+ type="index"
+ width="60"
+ />
+
+ <!-- 鍥哄畾鍒楋細閮ㄩ棬 -->
+ <el-table-column
+ label="閮ㄩ棬"
+ prop="department"
+ width="120"
+ show-overflow-tooltip
+ align="center"
+ />
+
+ <!-- 鍥哄畾鍒楋細濮撳悕 -->
+ <el-table-column
+ label="濮撳悕"
+ prop="name"
+ width="100"
+ show-overflow-tooltip
+ align="center"
+ />
+
+ <!-- 鍥哄畾鍒楋細宸ュ彿 -->
+ <el-table-column
+ label="宸ュ彿"
+ prop="employeeId"
+ width="100"
+ show-overflow-tooltip
+ align="center"
+ />
+
+ <!-- 鍔ㄦ�佸垪锛氭牴鎹瓧鍏告覆鏌� -->
+ <el-table-column
+ v-for="(dictItem, index) in dynamicColumns"
+ :key="dictItem.value"
+ :label="dictItem.label"
+ :prop="dictItem.value"
+ :width="dictItem.width || 120"
+ show-overflow-tooltip
+ align="center"
+ >
+ <template #default="scope">
+ <!-- 鏍规嵁瀛楀吀绫诲瀷娓叉煋涓嶅悓鐨勬樉绀烘柟寮� -->
+ <template v-if="dictItem.renderType === 'tag'">
+ <el-tag
+ :type="getTagType(scope.row[dictItem.value])"
+ size="small"
+ >
+ {{ getDictValueLabel(dictItem.dictType, scope.row[dictItem.value]) }}
+ </el-tag>
+ </template>
+ <template v-else-if="dictItem.renderType === 'select'">
+ <el-select
+ v-model="scope.row[dictItem.value]"
+ placeholder="璇烽�夋嫨"
+ size="small"
+ @change="handleSelectChange(scope.row, dictItem.value, $event)"
+ >
+ <el-option
+ v-for="option in dictItem.options"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </template>
+ <template v-else-if="dictItem.renderType === 'input'">
+ <el-input
+ v-model="scope.row[dictItem.value]"
+ size="small"
+ placeholder="璇疯緭鍏�"
+ @blur="handleInputChange(scope.row, dictItem.value, $event)"
+ />
+ </template>
+ <template v-else>
+ <span>{{ getDictValueLabel(dictItem.dictType, scope.row[dictItem.value]) }}</span>
+ </template>
+ </template>
+ </el-table-column>
+
+ <!-- 鎿嶄綔鍒� -->
+ <el-table-column
+ v-if="showActions"
+ label="鎿嶄綔"
+ width="150"
+ align="center"
+ fixed="right"
+ >
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ size="small"
+ @click="handleEdit(scope.row, scope.$index)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="danger"
+ link
+ size="small"
+ @click="handleDelete(scope.row, scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉缁勪欢 -->
+ <div v-if="showPagination" class="pagination-container">
+ <el-pagination
+ v-model:current-page="pagination.current"
+ v-model:page-size="pagination.size"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import { useDict } from '@/utils/dict'
+
+// 瀹氫箟缁勪欢灞炴��
+const props = defineProps({
+ // 琛ㄦ牸鏁版嵁
+ data: {
+ type: Array,
+ default: () => []
+ },
+ // 瀛楀吀绫诲瀷鏁扮粍锛岀敤浜庡姩鎬佺敓鎴愬垪
+ dictTypes: {
+ type: Array,
+ default: () => []
+ },
+ // 鏄惁鏄剧ず閫夋嫨鍒�
+ showSelection: {
+ type: Boolean,
+ default: false
+ },
+ // 鏄惁鏄剧ず搴忓彿鍒�
+ showIndex: {
+ type: Boolean,
+ default: true
+ },
+ // 鏄惁鏄剧ず鎿嶄綔鍒�
+ showActions: {
+ type: Boolean,
+ default: false
+ },
+ // 鏄惁鏄剧ず鍒嗛〉
+ showPagination: {
+ type: Boolean,
+ default: false
+ },
+ // 琛ㄦ牸楂樺害
+ height: {
+ type: [String, Number],
+ default: 'auto'
+ },
+ // 鏄惁鏄剧ず杈规
+ border: {
+ type: Boolean,
+ default: true
+ },
+ // 鍔犺浇鐘舵��
+ loading: {
+ type: Boolean,
+ default: false
+ },
+ // 鍒嗛〉閰嶇疆
+ pagination: {
+ type: Object,
+ default: () => ({
+ current: 1,
+ size: 10,
+ total: 0
+ })
+ }
+})
+
+// 瀹氫箟浜嬩欢
+const emit = defineEmits([
+ 'selection-change',
+ 'row-click',
+ 'edit',
+ 'delete',
+ 'select-change',
+ 'input-change',
+ 'size-change',
+ 'current-change'
+])
+
+// 鍝嶅簲寮忔暟鎹�
+const tableRef = ref(null)
+const tableData = ref([])
+
+// 鑾峰彇瀛楀吀鏁版嵁
+const dictData = ref({})
+
+// 鍔ㄦ�佸垪閰嶇疆
+const dynamicColumns = computed(() => {
+ const columns = []
+
+ props.dictTypes.forEach(dictType => {
+ const dictItems = dictData.value[dictType] || []
+ // 涓烘瘡涓瓧鍏哥被鍨嬪垱寤轰竴涓垪锛岃�屼笉鏄负姣忎釜瀛楀吀椤瑰垱寤哄垪
+ if (dictItems.length > 0) {
+ columns.push({
+ label: getDictLabel(dictType), // 鑾峰彇瀛楀吀绫诲瀷鐨勬樉绀哄悕绉�
+ value: dictType, // 浣跨敤瀛楀吀绫诲瀷浣滀负瀛楁鍚�
+ width: 120,
+ renderType: 'tag', // 榛樿浣跨敤鏍囩鏄剧ず
+ options: dictItems, // 鎻愪緵閫夐」
+ dictType: dictType
+ })
+ }
+ })
+
+ return columns
+})
+
+// 鑾峰彇瀛楀吀绫诲瀷鐨勬樉绀哄悕绉�
+const getDictLabel = (dictType) => {
+ const labelMap = {
+ 'sys_normal_disable': '鐘舵��',
+ 'sys_user_level': '绾у埆',
+ 'sys_user_position': '鑱屼綅',
+ 'sys_yes_no': '鏄惁',
+ 'sys_user_sex': '鎬у埆',
+ 'sys_lavor_issue': '鍔冲姟闂' // 娣诲姞鍔冲姟闂瀛楀吀
+ }
+ return labelMap[dictType] || dictType
+}
+
+// 鑾峰彇瀛楀吀鏁版嵁
+const loadDictData = async () => {
+ try {
+ const dictPromises = props.dictTypes.map(async (dictType) => {
+ const { getDicts } = await import('@/api/system/dict/data')
+ const response = await getDicts(dictType)
+ return {
+ type: dictType,
+ data: response.data.map(item => ({
+ label: item.dictLabel,
+ value: item.dictValue,
+ elTagType: item.listClass,
+ elTagClass: item.cssClass
+ }))
+ }
+ })
+
+ const results = await Promise.all(dictPromises)
+ results.forEach(result => {
+ dictData.value[result.type] = result.data
+ })
+ } catch (error) {
+ console.error('鍔犺浇瀛楀吀鏁版嵁澶辫触:', error)
+ // 濡傛灉瀛楀吀鍔犺浇澶辫触锛屼娇鐢ㄩ粯璁ゆ暟鎹�
+ props.dictTypes.forEach(dictType => {
+ if (!dictData.value[dictType]) {
+ dictData.value[dictType] = []
+ }
+ })
+ }
+}
+
+// 鑾峰彇鏍囩绫诲瀷
+const getTagType = (value) => {
+ // 鏍规嵁鍊艰繑鍥炰笉鍚岀殑鏍囩绫诲瀷
+ if (value === '1' || value === 'true' || value === '鏄�') return 'success'
+ if (value === '0' || value === 'false' || value === '鍚�') return 'danger'
+ if (value === '2' || value === 'warning') return 'warning'
+ return 'info'
+}
+
+// 鑾峰彇瀛楀吀鍊肩殑鏍囩
+const getDictValueLabel = (dictType, value) => {
+ if (!value) return '-'
+ const dictItems = dictData.value[dictType] || []
+ const item = dictItems.find(item => item.value === value)
+ return item ? item.label : value
+}
+
+// 浜嬩欢澶勭悊鍑芥暟
+const handleSelectionChange = (selection) => {
+ emit('selection-change', selection)
+}
+
+const handleRowClick = (row, column, event) => {
+ emit('row-click', row, column, event)
+}
+
+const handleEdit = (row, index) => {
+ emit('edit', row, index)
+}
+
+const handleDelete = (row, index) => {
+ emit('delete', row, index)
+}
+
+const handleSelectChange = (row, prop, value) => {
+ emit('select-change', row, prop, value)
+}
+
+const handleInputChange = (row, prop, event) => {
+ emit('input-change', row, prop, event.target.value)
+}
+
+const handleSizeChange = (size) => {
+ emit('size-change', size)
+}
+
+const handleCurrentChange = (current) => {
+ emit('current-change', current)
+}
+
+// 鐩戝惉鏁版嵁鍙樺寲
+watch(() => props.data, (newData) => {
+ tableData.value = newData
+}, { immediate: true })
+
+// 鐩戝惉瀛楀吀绫诲瀷鍙樺寲
+watch(() => props.dictTypes, () => {
+ loadDictData()
+}, { immediate: true })
+
+// 缁勪欢鎸傝浇鏃跺姞杞藉瓧鍏告暟鎹�
+onMounted(() => {
+ loadDictData()
+})
+
+// 鏆撮湶鏂规硶缁欑埗缁勪欢
+defineExpose({
+ tableRef,
+ getSelection: () => tableRef.value?.getSelectionRows() || [],
+ clearSelection: () => tableRef.value?.clearSelection(),
+ toggleRowSelection: (row, selected) => tableRef.value?.toggleRowSelection(row, selected),
+ setCurrentRow: (row) => tableRef.value?.setCurrentRow(row)
+})
+</script>
+
+<style scoped>
+.dynamic-table-container {
+ width: 100%;
+}
+
+.pagination-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+:deep(.el-table .el-table__header-wrapper th) {
+ background-color: #F0F1F5 !important;
+ color: #333333;
+ font-weight: 600;
+}
+
+:deep(.el-table .el-table__body-wrapper td) {
+ padding: 8px 0;
+}
+
+:deep(.el-select) {
+ width: 100%;
+}
+
+:deep(.el-input) {
+ width: 100%;
+}
+</style>
diff --git a/src/components/Echarts/echarts.vue b/src/components/Echarts/echarts.vue
new file mode 100644
index 0000000..54a6c39
--- /dev/null
+++ b/src/components/Echarts/echarts.vue
@@ -0,0 +1,223 @@
+<template>
+ <div style="position: relative;">
+ <div ref="chartRef" :style="chartStyle"></div>
+ <slot></slot>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import * as echarts from 'echarts'
+
+const emit = defineEmits(['finished', 'click'])
+
+// Props
+const props = defineProps({
+ options: {
+ type: Object,
+ default: () => ({})
+ },
+ chartStyle: {
+ type: Object,
+ default: () => ({
+ height: '80%',
+ width: '100%'
+ })
+ },
+ dataset: {
+ type: Object,
+ default: () => { }
+ },
+ xAxis: {
+ type: Array,
+ default: () => []
+ },
+ yAxis: {
+ type: Array,
+ default: () => []
+ },
+ series: {
+ type: Array,
+ default: () => []
+ },
+ grid: {
+ type: Object,
+ default: () => ({})
+ },
+ legend: {
+ type: Object,
+ default: () => ({})
+ },
+ tooltip: {
+ type: Object,
+ default: () => ({})
+ },
+ lineColors: {
+ type: Array,
+ default: () => []
+ },
+ barColors: {
+ type: Array,
+ default: () => []
+ },
+ pieColors: {
+ type: Array,
+ default: () => []
+ },
+ loadingOption: {
+ type: Object,
+ default: () => ({
+ text: '鏁版嵁鍔犺浇涓�...',
+ color: '#00BAFF',
+ textColor: '#000',
+ maskColor: 'rgba(255, 255, 255, 0.8)',
+ zlevel: 0
+ })
+ },
+ color: {
+ type: Array,
+ default: () => []
+ },
+ visualMap: {
+ type: Object,
+ default: () => ({})
+ },
+ option: {
+ type: Object,
+ default: () => ({})
+ },
+})
+
+import { watch } from 'vue'
+
+// Refs
+const chartRef = ref(null)
+let chartInstance = null
+let finishedHandler = null
+let initTimer = null
+let initAttempts = 0
+
+function clearInitTimer() {
+ if (initTimer) {
+ clearTimeout(initTimer)
+ initTimer = null
+ }
+}
+
+function isContainerReady() {
+ const el = chartRef.value
+ if (!el) return false
+ // offsetWidth/offsetHeight 鏇磋创杩戠湡瀹炲竷灞�锛堜负 0 寰�寰�浠h〃杩樻病甯冨眬/涓嶅彲瑙侊級
+ return el.offsetWidth > 0 && el.offsetHeight > 0
+}
+
+function initChartWhenReady() {
+ clearInitTimer()
+ initAttempts += 1
+
+ if (!isContainerReady()) {
+ // 绛夊鍣ㄧ湡姝f湁灏哄锛堥伩鍏嶉灞忓垵濮嬪寲鍋忕Щ/绌虹櫧锛岀儹鏇存柊鍚庢墠姝e父鐨勬儏鍐碉級
+ // 鏈�澶氶噸璇曠害 3 绉掞紝閬垮厤鏃犻檺寰幆
+ if (initAttempts < 60) {
+ initTimer = setTimeout(initChartWhenReady, 50)
+ }
+ return
+ }
+
+ if (chartInstance) return
+ chartInstance = echarts.init(chartRef.value)
+ finishedHandler = () => emit('finished')
+ chartInstance.on('finished', finishedHandler)
+ chartInstance.on('click', (params) => {
+ emit('click', params)
+ })
+ renderChart()
+ // setOption 鍚庤ˉ涓�娆� resize锛岀‘淇濋灞忓昂瀵告纭�
+ nextTick(() => {
+ if (chartInstance) chartInstance.resize()
+ })
+}
+
+// Methods
+function generateChart(option) {
+ const copiedOption = option
+
+ if (copiedOption.series && copiedOption.series.length > 0) {
+ copiedOption.series.forEach((s, index) => {
+ if (s.type === 'line' && props.lineColors.length) {
+ s.itemStyle = s.itemStyle || {}
+ s.lineStyle = s.lineStyle || {}
+ s.itemStyle.color = props.lineColors[index] || props.lineColors[0]
+ s.lineStyle.color = props.lineColors[index] || props.lineColors[0]
+ } else if (s.type === 'bar' && props.barColors.length) {
+ s.itemStyle = s.itemStyle || {}
+ s.itemStyle.color = props.barColors[index] || props.barColors[0]
+ }
+ })
+ }
+
+ chartInstance.setOption(copiedOption)
+}
+
+function renderChart() {
+ const option = {
+ color: props.color.length ? props.color : undefined,
+ backgroundColor: props.options.backgroundColor || '#fff',
+ textStyle: props.options.textStyle || { color: '#333' },
+ xAxis: props.xAxis,
+ yAxis: props.yAxis,
+ dataset: props.dataset,
+ series: props.series,
+ grid: props.grid,
+ legend: props.legend,
+ tooltip: props.tooltip,
+ visualMap: Object.keys(props.visualMap).length ? props.visualMap : undefined,
+ }
+
+ chartInstance.clear()
+ generateChart(option)
+}
+
+function windowResizeListener() {
+ if (!chartInstance) return
+ chartInstance.resize()
+}
+
+// Lifecycle hooks
+onMounted(() => {
+ initAttempts = 0
+ initChartWhenReady()
+ window.addEventListener('resize', windowResizeListener)
+})
+
+onBeforeUnmount(() => {
+ if (chartInstance) {
+ window.removeEventListener('resize', windowResizeListener)
+ if (finishedHandler) {
+ chartInstance.off('finished', finishedHandler)
+ finishedHandler = null
+ }
+ chartInstance.dispose()
+ chartInstance = null
+ }
+ clearInitTimer()
+})
+
+// Watch all reactive props that affect the chart
+watch(
+ () => [props.xAxis, props.yAxis, props.series, props.legend, props.tooltip, props.visualMap],
+ () => {
+ // 濡傛灉棣栧睆杩樻病鍒濆鍖栨垚鍔燂紝绛夊緟瀹瑰櫒 ready 鍚庡啀娓叉煋
+ if (!chartInstance) {
+ initChartWhenReady()
+ return
+ }
+ renderChart()
+ // 鏁版嵁鍙樺寲鍚庤ˉ涓�娆� resize锛岄伩鍏嶅竷灞�鍙樺寲瀵艰嚧鐨勫亸绉�
+ nextTick(() => {
+ if (chartInstance) chartInstance.resize()
+ })
+ },
+ { deep: true, immediate: true }
+)
+</script>
\ No newline at end of file
diff --git a/src/components/Editor/index.vue b/src/components/Editor/index.vue
new file mode 100644
index 0000000..8f0eebd
--- /dev/null
+++ b/src/components/Editor/index.vue
@@ -0,0 +1,304 @@
+<template>
+ <div>
+ <el-upload
+ :action="uploadUrl"
+ :before-upload="handleBeforeUpload"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="files"
+ :show-file-list="false"
+ :headers="headers"
+ class="editor-img-uploader"
+ v-if="type === 'url'"
+ >
+ <i ref="uploadRef" class="editor-img-uploader"></i>
+ </el-upload>
+ </div>
+ <div class="editor">
+ <quill-editor
+ ref="quillEditorRef"
+ v-model:content="content"
+ contentType="html"
+ @textChange="(e) => $emit('update:modelValue', content)"
+ :options="options"
+ :style="styles"
+ />
+ </div>
+</template>
+
+<script setup>
+import { QuillEditor } from "@vueup/vue-quill";
+import "@vueup/vue-quill/dist/vue-quill.snow.css";
+import { getToken } from "@/utils/auth";
+import {uploadPublicFile} from "@/api/basicData/common.js";
+
+const { proxy } = getCurrentInstance();
+
+const quillEditorRef = ref();
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/public/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+
+const props = defineProps({
+ /* 缂栬緫鍣ㄧ殑鍐呭 */
+ modelValue: {
+ type: String,
+ },
+ /* 楂樺害 */
+ height: {
+ type: Number,
+ default: null,
+ },
+ /* 鏈�灏忛珮搴� */
+ minHeight: {
+ type: Number,
+ default: null,
+ },
+ /* 鍙 */
+ readOnly: {
+ type: Boolean,
+ default: false,
+ },
+ /* 涓婁紶鏂囦欢澶у皬闄愬埗(MB) */
+ fileSize: {
+ type: Number,
+ default: 5,
+ },
+ /* 绫诲瀷锛坆ase64鏍煎紡銆乽rl鏍煎紡锛� */
+ type: {
+ type: String,
+ default: "url",
+ },
+});
+
+const options = ref({
+ theme: "snow",
+ bounds: document.body,
+ debug: "warn",
+ modules: {
+ // 宸ュ叿鏍忛厤缃�
+ toolbar: [
+ ["bold", "italic", "underline", "strike"], // 鍔犵矖 鏂滀綋 涓嬪垝绾� 鍒犻櫎绾�
+ ["blockquote", "code-block"], // 寮曠敤 浠g爜鍧�
+ [{ list: "ordered" }, { list: "bullet" }], // 鏈夊簭銆佹棤搴忓垪琛�
+ [{ indent: "-1" }, { indent: "+1" }], // 缂╄繘
+ [{ size: ["small", false, "large", "huge"] }], // 瀛椾綋澶у皬
+ [{ header: [1, 2, 3, 4, 5, 6, false] }], // 鏍囬
+ [{ color: [] }, { background: [] }], // 瀛椾綋棰滆壊銆佸瓧浣撹儗鏅鑹�
+ [{ align: [] }], // 瀵归綈鏂瑰紡
+ ["clean"], // 娓呴櫎鏂囨湰鏍煎紡
+ ["link", "image", "video"], // 閾炬帴銆佸浘鐗囥�佽棰�
+ ],
+ },
+ placeholder: "璇疯緭鍏ュ唴瀹�",
+ readOnly: props.readOnly,
+});
+
+const styles = computed(() => {
+ let style = {};
+ if (props.minHeight) {
+ style.minHeight = `${props.minHeight}px`;
+ }
+ if (props.height) {
+ style.height = `${props.height}px`;
+ }
+ return style;
+});
+
+const content = ref("");
+watch(
+ () => props.modelValue,
+ (v) => {
+ if (v !== content.value) {
+ content.value = v == undefined ? "<p></p>" : v;
+ }
+ },
+ { immediate: true }
+);
+
+// 濡傛灉璁剧疆浜嗕笂浼犲湴鍧�鍒欒嚜瀹氫箟鍥剧墖涓婁紶浜嬩欢
+onMounted(() => {
+ if (props.type == "url") {
+ let quill = quillEditorRef.value.getQuill();
+ let toolbar = quill.getModule("toolbar");
+ toolbar.addHandler("image", (value) => {
+ if (value) {
+ proxy.$refs.uploadRef.click();
+ } else {
+ quill.format("image", false);
+ }
+ });
+ quill.root.addEventListener("paste", handlePasteCapture, true);
+ }
+});
+
+// 涓婁紶鍓嶆牎妫�鏍煎紡鍜屽ぇ灏�
+function handleBeforeUpload(file) {
+ const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
+ const isJPG = type.includes(file.type);
+ //妫�楠屾枃浠舵牸寮�
+ if (!isJPG) {
+ proxy.$modal.msgError(`鍥剧墖鏍煎紡閿欒!`);
+ return false;
+ }
+ // 鏍℃鏂囦欢澶у皬
+ if (props.fileSize) {
+ const isLt = file.size / 1024 / 1024 < props.fileSize;
+ if (!isLt) {
+ proxy.$modal.msgError(`涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃 ${props.fileSize} MB!`);
+ return false;
+ }
+ }
+ return true;
+}
+
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const imageUrl = resolveImageUrl(res);
+ if (!imageUrl) {
+ proxy.$modal.msgError("鏈幏鍙栧埌鍥剧墖鍦板潃");
+ return;
+ }
+ // 鑾峰彇瀵屾枃鏈疄渚�
+ let quill = toRaw(quillEditorRef.value).getQuill();
+ // 鑾峰彇鍏夋爣浣嶇疆
+ const selection = quill.getSelection(true);
+ const length = selection ? selection.index : quill.getLength();
+ // 鎻掑叆鍥剧墖锛宺es.url涓烘湇鍔″櫒杩斿洖鐨勫浘鐗囬摼鎺ュ湴鍧�
+ quill.insertEmbed(length, "image", imageUrl);
+ // 璋冩暣鍏夋爣鍒版渶鍚�
+ quill.setSelection(length + 1);
+ } else {
+ proxy.$modal.msgError("鍥剧墖鎻掑叆澶辫触");
+ }
+}
+
+function resolveImageUrl(res) {
+ if (!res) return "";
+ // 鍏煎鏂版帴鍙�: data[0].previewURL
+ const previewURL = res?.data?.[0]?.previewURL;
+ if (previewURL) {
+ return previewURL;
+ }
+ // 鍏煎鏃ф帴鍙�
+ if (res.url) {
+ return res.url;
+ }
+ if (res.fileName) {
+ return `${import.meta.env.VITE_APP_BASE_API}${res.fileName}`;
+ }
+ return "";
+}
+
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鍥剧墖鎻掑叆澶辫触");
+}
+
+// 澶嶅埗绮樿创鍥剧墖澶勭悊
+function handlePasteCapture(e) {
+ const clipboard = e.clipboardData || window.clipboardData;
+ if (clipboard && clipboard.items) {
+ for (let i = 0; i < clipboard.items.length; i++) {
+ const item = clipboard.items[i];
+ if (item.type.indexOf("image") !== -1) {
+ e.preventDefault();
+ const file = item.getAsFile();
+ insertImage(file);
+ }
+ }
+ }
+}
+
+function insertImage(file) {
+ const formData = new FormData();
+ formData.append("files", file);
+ uploadPublicFile(formData).then((res) => {
+ handleUploadSuccess(res)
+ })
+}
+</script>
+
+<style>
+.editor-img-uploader {
+ display: none;
+}
+.editor,
+.ql-toolbar {
+ white-space: pre-wrap !important;
+ line-height: normal !important;
+}
+.quill-img {
+ display: none;
+}
+.ql-snow .ql-tooltip[data-mode="link"]::before {
+ content: "璇疯緭鍏ラ摼鎺ュ湴鍧�:";
+}
+.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
+ border-right: 0px;
+ content: "淇濆瓨";
+ padding-right: 0px;
+}
+.ql-snow .ql-tooltip[data-mode="video"]::before {
+ content: "璇疯緭鍏ヨ棰戝湴鍧�:";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item::before {
+ content: "14px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before {
+ content: "10px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before {
+ content: "18px";
+}
+.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,
+.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before {
+ content: "32px";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item::before {
+ content: "鏂囨湰";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before {
+ content: "鏍囬1";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before {
+ content: "鏍囬2";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before {
+ content: "鏍囬3";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before {
+ content: "鏍囬4";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before {
+ content: "鏍囬5";
+}
+.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,
+.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before {
+ content: "鏍囬6";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item::before {
+ content: "鏍囧噯瀛椾綋";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before {
+ content: "琛嚎瀛椾綋";
+}
+.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,
+.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
+ content: "绛夊瀛椾綋";
+}
+</style>
diff --git a/src/components/FileCard.vue b/src/components/FileCard.vue
new file mode 100644
index 0000000..d14ec48
--- /dev/null
+++ b/src/components/FileCard.vue
@@ -0,0 +1,81 @@
+<template>
+ <div class="file-card">
+ <img src="@/assets/img/fileImg/unknowfile.png" alt="" v-if="fileType == 0"/>
+ <img src="@/assets/img/fileImg/word.png" alt="" v-else-if="fileType == 1"/>
+ <img src="@/assets/img/fileImg/excel.png" alt="" v-else-if="fileType == 2"/>
+ <img src="@/assets/img/fileImg/ppt.png" alt="" v-else-if="fileType == 3"/>
+ <img src="@/assets/img/fileImg/pdf.png" alt="" v-else-if="fileType == 4"/>
+ <img src="@/assets/img/fileImg/zpi.png" alt="" v-else-if="fileType == 5"/>
+ <img src="@/assets/img/fileImg/txt.png" alt="" v-else/>
+ <div class="word">
+ <span
+ >{{file.name || '鏈煡'}}</span
+ >
+ <span>154kb</span>
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ // props: ["fileType", "file"],
+ props: {
+ fileType: Number,
+ file: File,
+ default() {
+ return {};
+ },
+ },
+ watch: {
+ file() {
+ console.log(this.file);
+ },
+ },
+ mounted() {
+ console.log(this.file);
+ console.log(this.fileType);
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+.file-card {
+ width: 250px;
+ height: 100px;
+ background-color: rgb(45, 48, 63);
+ border-radius: 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 10px;
+ box-sizing: border-box;
+ cursor: pointer;
+ &:hover {
+ background-color: rgb(33, 36, 54);
+ }
+ img {
+ width: 60px;
+ height: 60px;
+ }
+ .word {
+ width: 60%;
+ margin-left: 10px;
+ overflow: hidden;
+ span {
+ width: 90%;
+ display: inline-block;
+ color: #fff;
+ }
+ span:first-child {
+ font-size: 14px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+ span:last-child {
+ font-size: 12px;
+ color: rgb(180, 180, 180);
+ }
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/components/FileUpload/index.vue b/src/components/FileUpload/index.vue
new file mode 100644
index 0000000..57e62b7
--- /dev/null
+++ b/src/components/FileUpload/index.vue
@@ -0,0 +1,259 @@
+<template>
+ <div class="upload-file">
+ <el-upload multiple :action="uploadFileUrl" :before-upload="handleBeforeUpload" :file-list="fileList" :data="data"
+ :limit="limit" :on-error="handleUploadError" :on-exceed="handleExceed" :on-success="handleUploadSuccess"
+ :show-file-list="false" :headers="headers" class="upload-file-uploader" ref="fileUpload" v-if="!disabled">
+ <!-- 涓婁紶鎸夐挳 -->
+ <el-button type="primary">閫夊彇鏂囦欢</el-button>
+ </el-upload>
+ <!-- 涓婁紶鎻愮ず -->
+ <div class="el-upload__tip" v-if="showTip && !disabled">
+ 璇蜂笂浼�
+ <template v-if="fileSize">
+ 澶у皬涓嶈秴杩� <b style="color: #f56c6c">{{ fileSize }}MB</b>
+ </template>
+ <template v-if="fileType">
+ 鏍煎紡涓� <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
+ </template>
+ 鐨勬枃浠�
+ </div>
+ <!-- 鏂囦欢鍒楄〃 -->
+ <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
+ <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
+ <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
+ <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
+ </el-link>
+ <div class="ele-upload-list__item-content-action">
+ <el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled"> 鍒犻櫎</el-link>
+ </div>
+ </li>
+ </transition-group>
+ </div>
+</template>
+
+<script setup>
+import { getToken } from "@/utils/auth";
+import Sortable from "sortablejs";
+
+const props = defineProps({
+ modelValue: [String, Object, Array],
+ // 涓婁紶鎺ュ彛鍦板潃
+ action: {
+ type: String,
+ default: "/common/upload",
+ },
+ // 涓婁紶鎼哄甫鐨勫弬鏁�
+ data: {
+ type: Object,
+ },
+ // 鏁伴噺闄愬埗
+ limit: {
+ type: Number,
+ default: 5,
+ },
+ // 澶у皬闄愬埗(MB)
+ fileSize: {
+ type: Number,
+ default: 5,
+ },
+ // 鏂囦欢绫诲瀷, 渚嬪['png', 'jpg', 'jpeg']
+ fileType: {
+ type: Array,
+ default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"],
+ },
+ // 鏄惁鏄剧ず鎻愮ず
+ isShowTip: {
+ type: Boolean,
+ default: true,
+ },
+ // 绂佺敤缁勪欢锛堜粎鏌ョ湅鏂囦欢锛�
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ // 鎷栧姩鎺掑簭
+ drag: {
+ type: Boolean,
+ default: true,
+ },
+});
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits();
+const number = ref(0);
+const uploadList = ref([]);
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action); // 涓婁紶鏂囦欢鏈嶅姟鍣ㄥ湴鍧�
+const headers = ref({ Authorization: "Bearer " + getToken() });
+const fileList = ref([]);
+const showTip = computed(
+ () => props.isShowTip && (props.fileType || props.fileSize)
+);
+
+watch(
+ () => props.modelValue,
+ (val) => {
+ if (val) {
+ let temp = 1;
+ // 棣栧厛灏嗗�艰浆涓烘暟缁�
+ const list = Array.isArray(val) ? val : props.modelValue.split(",");
+ // 鐒跺悗灏嗘暟缁勮浆涓哄璞℃暟缁�
+ fileList.value = list.map((item) => {
+ if (typeof item === "string") {
+ item = { name: item, url: item };
+ }
+ item.uid = item.uid || new Date().getTime() + temp++;
+ return item;
+ });
+ } else {
+ fileList.value = [];
+ return [];
+ }
+ },
+ { deep: true, immediate: true }
+);
+
+// 涓婁紶鍓嶆牎妫�鏍煎紡鍜屽ぇ灏�
+function handleBeforeUpload(file) {
+ // 鏍℃鏂囦欢绫诲瀷
+ if (props.fileType.length) {
+ const fileName = file.name.split(".");
+ const fileExt = fileName[fileName.length - 1];
+ const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+ if (!isTypeOk) {
+ proxy.$modal.msgError(
+ `鏂囦欢鏍煎紡涓嶆纭紝璇蜂笂浼�${props.fileType.join("/")}鏍煎紡鏂囦欢!`
+ );
+ return false;
+ }
+ }
+ // 鏍℃鏂囦欢鍚嶆槸鍚﹀寘鍚壒娈婂瓧绗�
+ if (file.name.includes(",")) {
+ proxy.$modal.msgError("鏂囦欢鍚嶄笉姝g‘锛屼笉鑳藉寘鍚嫳鏂囬�楀彿!");
+ return false;
+ }
+ // 鏍℃鏂囦欢澶у皬
+ if (props.fileSize) {
+ const isLt = file.size / 1024 / 1024 < props.fileSize;
+ if (!isLt) {
+ proxy.$modal.msgError(`涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃 ${props.fileSize} MB!`);
+ return false;
+ }
+ }
+ proxy.$modal.loading("姝e湪涓婁紶鏂囦欢锛岃绋嶅��...");
+ number.value++;
+ return true;
+}
+
+// 鏂囦欢涓暟瓒呭嚭
+function handleExceed() {
+ proxy.$modal.msgError(`涓婁紶鏂囦欢鏁伴噺涓嶈兘瓒呰繃 ${props.limit} 涓�!`);
+}
+
+// 涓婁紶澶辫触
+function handleUploadError(err) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶辫触");
+ proxy.$modal.closeLoading();
+}
+
+// 涓婁紶鎴愬姛鍥炶皟
+function handleUploadSuccess(res, file) {
+ if (res.code === 200) {
+ uploadList.value.push({ name: res.fileName, url: res.fileName });
+ uploadedSuccessfully();
+ } else {
+ number.value--;
+ proxy.$modal.closeLoading();
+ proxy.$modal.msgError(res.msg);
+ proxy.$refs.fileUpload.handleRemove(file);
+ uploadedSuccessfully();
+ }
+}
+
+// 鍒犻櫎鏂囦欢
+function handleDelete(index) {
+ fileList.value.splice(index, 1);
+ emit("update:modelValue", listToString(fileList.value));
+}
+
+// 涓婁紶缁撴潫澶勭悊
+function uploadedSuccessfully() {
+ if (number.value > 0 && uploadList.value.length === number.value) {
+ fileList.value = fileList.value
+ .filter((f) => f.url !== undefined)
+ .concat(uploadList.value);
+ uploadList.value = [];
+ number.value = 0;
+ emit("update:modelValue", listToString(fileList.value));
+ proxy.$modal.closeLoading();
+ }
+}
+
+// 鑾峰彇鏂囦欢鍚嶇О
+function getFileName(name) {
+ // 濡傛灉鏄痷rl閭d箞鍙栨渶鍚庣殑鍚嶅瓧 濡傛灉涓嶆槸鐩存帴杩斿洖
+ if (name.lastIndexOf("/") > -1) {
+ return name.slice(name.lastIndexOf("/") + 1);
+ } else {
+ return name;
+ }
+}
+
+// 瀵硅薄杞垚鎸囧畾瀛楃涓插垎闅�
+function listToString(list, separator) {
+ let strs = "";
+ separator = separator || ",";
+ for (let i in list) {
+ if (list[i].url) {
+ strs += list[i].url + separator;
+ }
+ }
+ return strs != "" ? strs.substr(0, strs.length - 1) : "";
+}
+
+// 鍒濆鍖栨嫋鎷芥帓搴�
+onMounted(() => {
+ if (props.drag) {
+ nextTick(() => {
+ const element = document.querySelector(".upload-file-list");
+ Sortable.create(element, {
+ ghostClass: "file-upload-darg",
+ onEnd: (evt) => {
+ const movedItem = fileList.value.splice(evt.oldIndex, 1)[0];
+ fileList.value.splice(evt.newIndex, 0, movedItem);
+ emit("update:modelValue", listToString(fileList.value));
+ },
+ });
+ });
+ }
+});
+</script>
+<style scoped lang="scss">
+.file-upload-darg {
+ opacity: 0.5;
+ background: #c8ebfb;
+}
+
+.upload-file-uploader {
+ margin-bottom: 5px;
+}
+
+.upload-file-list .el-upload-list__item {
+ border: 1px solid #e4e7ed;
+ line-height: 2;
+ margin-bottom: 10px;
+ position: relative;
+ transition: none !important;
+}
+
+.upload-file-list .ele-upload-list__item-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ color: inherit;
+}
+
+.ele-upload-list__item-content-action .el-link {
+ margin-right: 10px;
+}
+</style>
diff --git a/src/components/Hamburger/index.vue b/src/components/Hamburger/index.vue
new file mode 100644
index 0000000..c35557f
--- /dev/null
+++ b/src/components/Hamburger/index.vue
@@ -0,0 +1,50 @@
+<template>
+ <div class="hamburger-wrap" @click="toggleClick">
+ <svg
+ :class="{'is-active':isActive}"
+ class="hamburger"
+ viewBox="0 0 1024 1024"
+ xmlns="http://www.w3.org/2000/svg"
+ width="64"
+ height="64"
+ fill="currentColor"
+ >
+ <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
+ </svg>
+ </div>
+</template>
+
+<script setup>
+defineProps({
+ isActive: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const emit = defineEmits()
+const toggleClick = () => {
+ emit('toggleClick')
+}
+</script>
+
+<style scoped>
+.hamburger-wrap {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.hamburger {
+ display: inline-block;
+ vertical-align: middle;
+ width: 22px;
+ height: 22px;
+}
+
+.hamburger.is-active {
+ transform: rotate(180deg);
+}
+</style>
diff --git a/src/components/HeaderSearch/index.vue b/src/components/HeaderSearch/index.vue
new file mode 100644
index 0000000..a81dd98
--- /dev/null
+++ b/src/components/HeaderSearch/index.vue
@@ -0,0 +1,245 @@
+<template>
+ <div class="header-search">
+ <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+ <el-dialog
+ v-model="show"
+ width="600"
+ @close="close"
+ :show-close="true"
+ append-to-body
+ >
+ <el-input
+ v-model="search"
+ ref="headerSearchSelectRef"
+ size="large"
+ @input="querySearch"
+ prefix-icon="Search"
+ placeholder="鑿滃崟鎼滅储锛屾敮鎸佹爣棰樸�乁RL妯$硦鏌ヨ"
+ clearable
+ >
+ </el-input>
+
+ <div class="result-wrap">
+ <el-scrollbar>
+ <div class="search-item" tabindex="1" v-for="item in options" :key="item.path">
+ <div class="left">
+ <svg-icon class="menu-icon" :icon-class="item.icon" />
+ </div>
+ <div class="search-info" @click="change(item)">
+ <div class="menu-title">
+ {{ item.title.join(" / ") }}
+ </div>
+ <div class="menu-path">
+ {{ item.path }}
+ </div>
+ </div>
+ </div>
+ </el-scrollbar>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import Fuse from 'fuse.js'
+import { getNormalPath } from '@/utils/ruoyi'
+import { isHttp } from '@/utils/validate'
+import usePermissionStore from '@/store/modules/permission'
+
+const props = defineProps({
+ keyword: {
+ type: String,
+ default: ''
+ }
+})
+
+const search = ref('')
+const options = ref([])
+const searchPool = ref([])
+const show = ref(false)
+const fuse = ref(undefined)
+const headerSearchSelectRef = ref(null)
+const router = useRouter()
+const routes = computed(() => usePermissionStore().defaultRoutes)
+
+function click() {
+ show.value = !show.value
+ if (show.value) {
+ syncSearchFromKeyword()
+ nextTick(() => {
+ headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
+ })
+ }
+}
+
+function syncSearchFromKeyword() {
+ search.value = props.keyword?.trim?.() ?? ''
+ querySearch(search.value)
+}
+
+function open(keyword = props.keyword) {
+ show.value = true
+ search.value = keyword?.trim?.() ?? ''
+ querySearch(search.value)
+ nextTick(() => {
+ headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
+ })
+}
+
+function close() {
+ headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
+ search.value = ''
+ options.value = []
+ show.value = false
+}
+
+function change(val) {
+ const path = val.path
+ const query = val.query
+ if (isHttp(path)) {
+ // http(s):// 璺緞鏂扮獥鍙f墦寮�
+ const pindex = path.indexOf("http")
+ window.open(path.substr(pindex, path.length), "_blank")
+ } else {
+ if (query) {
+ router.push({ path: path, query: JSON.parse(query) })
+ } else {
+ router.push(path)
+ }
+ }
+
+ search.value = ''
+ options.value = []
+ nextTick(() => {
+ show.value = false
+ })
+}
+
+function initFuse(list) {
+ fuse.value = new Fuse(list, {
+ shouldSort: true,
+ threshold: 0.4,
+ location: 0,
+ distance: 100,
+ minMatchCharLength: 1,
+ keys: [{
+ name: 'title',
+ weight: 0.7
+ }, {
+ name: 'path',
+ weight: 0.3
+ }]
+ })
+}
+
+// Filter out the routes that can be displayed in the sidebar
+// And generate the internationalized title
+function generateRoutes(routes, basePath = '', prefixTitle = []) {
+ let res = []
+
+ for (const r of routes) {
+ // skip hidden router
+ if (r.hidden) { continue }
+ const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
+ const data = {
+ path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
+ title: [...prefixTitle],
+ icon: ''
+ }
+
+ if (r.meta && r.meta.title) {
+ data.title = [...data.title, r.meta.title]
+ data.icon = r.meta.icon
+ if (r.redirect !== "noRedirect") {
+ // only push the routes with title
+ // special case: need to exclude parent router without redirect
+ res.push(data)
+ }
+ }
+ if (r.query) {
+ data.query = r.query
+ }
+
+ // recursive child routes
+ if (r.children) {
+ const tempRoutes = generateRoutes(r.children, data.path, data.title)
+ if (tempRoutes.length >= 1) {
+ res = [...res, ...tempRoutes]
+ }
+ }
+ }
+ return res
+}
+
+function querySearch(query) {
+ if (query !== '') {
+ options.value = fuse.value.search(query).map((item) => item.item) ?? searchPool.value
+ } else {
+ options.value = searchPool.value
+ }
+}
+
+onMounted(() => {
+ searchPool.value = generateRoutes(routes.value)
+})
+
+watch(searchPool, (list) => {
+ initFuse(list)
+})
+
+defineExpose({
+ open
+})
+</script>
+
+<style lang='scss' scoped>
+.header-search {
+ .search-icon {
+ cursor: pointer;
+ font-size: 18px;
+ vertical-align: middle;
+ }
+}
+
+.result-wrap {
+ height: 280px;
+ margin: 10px 0;
+
+ .search-item {
+ display: flex;
+ height: 48px;
+
+ .left {
+ width: 60px;
+ text-align: center;
+
+ .menu-icon {
+ width: 18px;
+ height: 18px;
+ margin-top: 5px;
+ }
+ }
+
+ .search-info {
+ padding-left: 5px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+
+ .menu-title,
+ .menu-path {
+ height: 20px;
+ }
+ .menu-path {
+ color: #ccc;
+ font-size: 10px;
+ }
+ }
+ }
+
+ .search-item:hover {
+ cursor: pointer;
+ }
+}
+</style>
diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue
new file mode 100644
index 0000000..3bb177f
--- /dev/null
+++ b/src/components/IconSelect/index.vue
@@ -0,0 +1,111 @@
+<template>
+ <div class="icon-body">
+ <el-input
+ v-model="iconName"
+ class="icon-search"
+ clearable
+ placeholder="璇疯緭鍏ュ浘鏍囧悕绉�"
+ @clear="filterIcons"
+ @input="filterIcons"
+ >
+ <template #suffix><i class="el-icon-search el-input__icon" /></template>
+ </el-input>
+ <div class="icon-list">
+ <div class="list-container">
+ <div v-for="(item, index) in iconList" class="icon-item-wrapper" :key="index" @click="selectedIcon(item)">
+ <div :class="['icon-item', { active: activeIcon === item }]">
+ <svg-icon :icon-class="item" class-name="icon" style="height: 25px;width: 16px;"/>
+ <span>{{ item }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import icons from './requireIcons'
+
+const props = defineProps({
+ activeIcon: {
+ type: String
+ }
+})
+
+const iconName = ref('')
+const iconList = ref(icons)
+const emit = defineEmits(['selected'])
+
+function filterIcons() {
+ iconList.value = icons
+ if (iconName.value) {
+ iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
+ }
+}
+
+function selectedIcon(name) {
+ emit('selected', name)
+ document.body.click()
+}
+
+function reset() {
+ iconName.value = ''
+ iconList.value = icons
+}
+
+defineExpose({
+ reset
+})
+</script>
+
+<style lang='scss' scoped>
+ .icon-body {
+ width: 100%;
+ padding: 10px;
+ .icon-search {
+ position: relative;
+ margin-bottom: 5px;
+ }
+ .icon-list {
+ height: 200px;
+ overflow: auto;
+ .list-container {
+ display: flex;
+ flex-wrap: wrap;
+ .icon-item-wrapper {
+ width: calc(100% / 3);
+ height: 25px;
+ line-height: 25px;
+ cursor: pointer;
+ display: flex;
+ .icon-item {
+ display: flex;
+ max-width: 100%;
+ height: 100%;
+ padding: 0 5px;
+ &:hover {
+ background: #ececec;
+ border-radius: 5px;
+ }
+ .icon {
+ flex-shrink: 0;
+ }
+ span {
+ display: inline-block;
+ vertical-align: -0.15em;
+ fill: currentColor;
+ padding-left: 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ .icon-item.active {
+ background: #ececec;
+ border-radius: 5px;
+ }
+ }
+ }
+ }
+ }
+</style>
\ No newline at end of file
diff --git a/src/components/IconSelect/requireIcons.js b/src/components/IconSelect/requireIcons.js
new file mode 100644
index 0000000..ccdfaa4
--- /dev/null
+++ b/src/components/IconSelect/requireIcons.js
@@ -0,0 +1,8 @@
+let icons = []
+const modules = import.meta.glob('./../../assets/icons/svg/*.svg')
+for (const path in modules) {
+ const p = path.split('assets/icons/svg/')[1].split('.svg')[0]
+ icons.push(p)
+}
+
+export default icons
\ No newline at end of file
diff --git a/src/components/PIMTable/PIMTable.vue b/src/components/PIMTable/PIMTable.vue
new file mode 100644
index 0000000..d63c197
--- /dev/null
+++ b/src/components/PIMTable/PIMTable.vue
@@ -0,0 +1,528 @@
+<template>
+ <el-table ref="multipleTable"
+ v-loading="tableLoading"
+ :border="border"
+ :data="tableData"
+ :header-cell-style="mergedHeaderCellStyle"
+ :height="height"
+ :highlight-current-row="highlightCurrentRow"
+ :row-class-name="rowClassName"
+ :row-style="rowStyle"
+ :row-key="rowKey"
+ :style="tableStyle"
+ tooltip-effect="dark"
+ :tooltip-options="{ appendTo: 'body' }"
+ :expand-row-keys="expandRowKeys"
+ :show-summary="isShowSummary"
+ :summary-method="summaryMethod"
+ @row-click="rowClick"
+ @current-change="currentChange"
+ @selection-change="handleSelectionChange"
+ @expand-change="expandChange"
+ class="lims-table">
+ <el-table-column align="center"
+ type="selection"
+ :selectable="selectable"
+ width="55"
+ v-if="isSelection" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column v-for="(item, index) in column"
+ :key="index"
+ :column-key="item.columnKey"
+ :filter-method="item.filterHandler"
+ :filter-multiple="item.filterMultiple"
+ :filtered-value="item.filteredValue"
+ :filters="item.filters"
+ :fixed="item.fixed"
+ :label="item.label"
+ :prop="item.prop"
+ :show-overflow-tooltip="item.dataType !== 'action' && item.dataType !== 'slot'"
+ :align="item.align"
+ :sortable="!!item.sortable"
+ :type="item.type"
+ :width="item.width"
+ :minWidth="item.minWidth">
+ <template #header="scope">
+ <div class="pim-table-header-cell"
+ :class="{ 'has-extra': item.headerSlot }">
+ <div class="pim-table-header-title">
+ {{ item.label }}
+ </div>
+ <div v-if="item.headerSlot"
+ class="pim-table-header-extra">
+ <slot :name="item.headerSlot"
+ :column="scope.column" />
+ </div>
+ </div>
+ </template>
+ <template v-if="item.hasOwnProperty('colunmTemplate')"
+ #[item.colunmTemplate]="scope">
+ <slot v-if="item.theadSlot"
+ :name="item.theadSlot"
+ :index="scope.$index"
+ :row="scope.row" />
+ </template>
+ <template #default="scope">
+ <!-- 鎻掓Ы -->
+ <div v-if="item.dataType == 'slot'">
+ <slot v-if="item.slot"
+ :index="scope.$index"
+ :name="item.slot"
+ :row="scope.row" />
+ </div>
+ <!-- 杩涘害鏉� -->
+ <div v-else-if="item.dataType == 'progress'">
+ <el-progress :percentage="Number(scope.row[item.prop])" />
+ </div>
+ <!-- 鍥剧墖 -->
+ <div v-else-if="item.dataType == 'image'">
+ <img :src="javaApi + '/img/' + scope.row[item.prop]"
+ alt=""
+ style="width: 40px; height: 40px; margin-top: 10px" />
+ </div>
+ <!-- tag -->
+ <div v-else-if="item.dataType == 'tag'">
+ <el-tag v-if="
+ typeof dataTypeFn(scope.row[item.prop], item.formatData) ===
+ 'string'
+ "
+ :title="formatters(scope.row[item.prop], item.formatData)"
+ :type="formatType(scope.row[item.prop], item.formatType)">
+ {{ formatters(scope.row[item.prop], item.formatData) }}
+ </el-tag>
+ <el-tag v-for="(tag, index) in dataTypeFn(
+ scope.row[item.prop],
+ item.formatData
+ )"
+ v-else-if="
+ typeof dataTypeFn(scope.row[item.prop], item.formatData) ===
+ 'object'
+ "
+ :key="index"
+ :title="formatters(scope.row[item.prop], item.formatData)"
+ :type="formatType(tag, item.formatType)">
+ {{ item.tagGroup ? tag[item.tagGroup.label] ?? tag : tag }}
+ </el-tag>
+ <el-tag v-else
+ :title="formatters(scope.row[item.prop], item.formatData)"
+ :type="formatType(scope.row[item.prop], item.formatType)">
+ {{ formatters(scope.row[item.prop], item.formatData) }}
+ </el-tag>
+ </div>
+ <!-- 鎸夐挳 -->
+ <div v-else-if="item.dataType == 'action'"
+ @click.stop>
+ <template v-for="(o, key) in item.operation"
+ :key="key">
+ <el-button v-show="o.type != 'upload'"
+ v-if="o.showHide ? o.showHide(scope.row) : true"
+ :disabled="isOperationDisabled(o, scope.row)"
+ :plain="o.plain"
+ type="primary"
+ :style="{
+ color: getOperationColor(o, scope.row),
+ fontWeight: 'bold',
+ }"
+ link
+ @click.stop="o.clickFun(scope.row)"
+ :key="key">
+ {{ o.name }}
+ </el-button>
+ <el-upload :action="
+ javaApi +
+ o.url +
+ '?id=' +
+ (o.uploadIdFun ? o.uploadIdFun(scope.row) : scope.row.id)
+ "
+ ref="uploadRef"
+ :multiple="o.multiple ? o.multiple : false"
+ :limit="1"
+ :disabled="isOperationDisabled(o, scope.row)"
+ :accept="
+ o.accept
+ ? o.accept
+ : '.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar'
+ "
+ v-if="o.type == 'upload'"
+ style="display: inline-block; width: 50px"
+ v-show="o.showHide ? o.showHide(scope.row) : true"
+ :headers="uploadHeader"
+ :before-upload="(file) => beforeUpload(file, scope.$index)"
+ :on-change="
+ (file, fileList) => handleChange(file, fileList, scope.$index)
+ "
+ :on-error="
+ (error, file, fileList) =>
+ onError(error, file, fileList, scope.$index)
+ "
+ :on-success="
+ (response, file, fileList) =>
+ handleSuccessUp(response, file, fileList, scope.$index)
+ "
+ :on-exceed="onExceed"
+ :show-file-list="false">
+ <el-button link
+ type="primary"
+ :disabled="isOperationDisabled(o, scope.row)"
+ :style="{
+ color: getOperationColor(o, scope.row),
+ }">{{ o.name }}</el-button>
+ </el-upload>
+ </template>
+ </div>
+ <!-- 鍙偣鍑荤殑鏂囧瓧 -->
+ <div v-else-if="item.dataType == 'link'"
+ class="cell link"
+ style="width: 100%"
+ @click="goLink(scope.row, item.linkMethod)">
+ <span v-if="!item.formatData">{{ 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-else>{{
+ formatters(scope.row[item.prop], item.formatData)
+ }}</span>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-if="isShowPagination"
+ :total="page.total"
+ :layout="page.layout"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationSearch" />
+</template>
+
+<script setup>
+ import pagination from "./Pagination.vue";
+ import { computed, ref, inject, getCurrentInstance } from "vue";
+ import { ElMessage } from "element-plus";
+
+ // 鑾峰彇鍏ㄥ眬鐨� uploadHeader
+ const { proxy } = getCurrentInstance();
+ const uploadHeader = proxy.uploadHeader;
+ const javaApi = proxy.javaApi;
+
+ const emit = defineEmits([
+ "pagination",
+ "expand-change",
+ "selection-change",
+ "row-click",
+ ]);
+
+ // Filters
+ const typeFn = (val, row) => {
+ return typeof val === "function" ? val(row) : val;
+ };
+
+ const formatters = (val, format) => {
+ return typeof format === "function" ? format(val) : val;
+ };
+
+ // Props锛堜娇鐢� defineProps 鐨勯潪 TS 褰㈠紡锛�
+ const props = defineProps({
+ tableLoading: {
+ type: Boolean,
+ default: false,
+ },
+ height: {
+ type: [Number, String],
+ default: "calc(100vh - 22em)",
+ },
+ expandRowKeys: {
+ type: Array,
+ default: () => [],
+ },
+ summaryMethod: {
+ type: Function,
+ default: () => {},
+ },
+ rowClick: {
+ type: Function,
+ default: () => {},
+ },
+ currentChange: {
+ type: Function,
+ default: () => {},
+ },
+ border: {
+ type: Boolean,
+ default: true,
+ },
+ isSelection: {
+ type: Boolean,
+ default: false,
+ },
+ selectable: {
+ type: Function,
+ default: () => true,
+ },
+ isShowPagination: {
+ type: Boolean,
+ default: true,
+ },
+ isShowSummary: {
+ type: Boolean,
+ default: false,
+ },
+ highlightCurrentRow: {
+ type: Boolean,
+ default: false,
+ },
+ headerCellStyle: {
+ type: Object,
+ default: () => ({}),
+ },
+ column: {
+ type: Array,
+ default: () => [],
+ },
+ rowClassName: {
+ type: Function,
+ default: () => "",
+ },
+ rowStyle: {
+ type: [Object, Function],
+ default: () => ({}),
+ },
+ tableData: {
+ type: Array,
+ default: () => [],
+ },
+ rowKey: {
+ type: String,
+ default: "id",
+ },
+ page: {
+ type: Object,
+ default: () => ({
+ total: 0,
+ current: 0,
+ size: 10,
+ layout: "total, sizes, prev, pager, next, jumper",
+ }),
+ },
+ total: {
+ type: Number,
+ default: 0,
+ },
+ tableStyle: {
+ type: [String, Object],
+ default: () => ({ width: "100%" }),
+ },
+ });
+
+ const mergedHeaderCellStyle = computed(() => ({
+ background: "var(--surface-soft)",
+ color: "var(--text-secondary)",
+ fontWeight: 600,
+ ...props.headerCellStyle,
+ }));
+
+ // Data
+ const uploadRefs = ref([]);
+ const currentFiles = ref({});
+ const uploadKeys = ref({});
+
+ const indexMethod = index => {
+ return (props.page.current - 1) * props.page.size + index + 1;
+ };
+
+ // 鐐瑰嚮 link 浜嬩欢
+ const goLink = (row, linkMethod) => {
+ if (!linkMethod) {
+ return ElMessage.warning("璇烽厤缃� link 浜嬩欢");
+ }
+ const parentMethod = getParentMethod(linkMethod);
+ if (typeof parentMethod === "function") {
+ parentMethod(row);
+ } else {
+ console.warn(`鐖剁粍浠朵腑鏈壘鍒版柟娉�: ${linkMethod}`);
+ }
+ };
+
+ // 鑾峰彇鐖剁粍浠舵柟娉曪紙绀轰緥瀹炵幇锛�
+ const getParentMethod = methodName => {
+ const parentMethods = inject("parentMethods", {});
+ return parentMethods[methodName];
+ };
+
+ const dataTypeFn = (val, format) => {
+ if (typeof format === "function") {
+ return format(val);
+ } else return val;
+ };
+ const validTagTypes = ["primary", "success", "info", "warning", "danger"];
+
+ const formatType = (val, format) => {
+ const type = typeof format === "function" ? format(val) : undefined;
+ return validTagTypes.includes(type) ? type : undefined;
+ };
+
+ const isOperationDisabled = (operation, row) => {
+ if (!operation?.disabled) return false;
+ return typeof operation.disabled === "function"
+ ? !!operation.disabled(row)
+ : !!operation.disabled;
+ };
+
+ const parseHexToRgb = hex => {
+ const normalized = String(hex || "")
+ .trim()
+ .replace("#", "");
+ if (normalized.length === 3) {
+ const r = parseInt(normalized[0] + normalized[0], 16);
+ const g = parseInt(normalized[1] + normalized[1], 16);
+ const b = parseInt(normalized[2] + normalized[2], 16);
+ if ([r, g, b].some(n => Number.isNaN(n))) return null;
+ return { r, g, b };
+ }
+ if (normalized.length === 6 || normalized.length === 8) {
+ const r = parseInt(normalized.slice(0, 2), 16);
+ const g = parseInt(normalized.slice(2, 4), 16);
+ const b = parseInt(normalized.slice(4, 6), 16);
+ if ([r, g, b].some(n => Number.isNaN(n))) return null;
+ return { r, g, b };
+ }
+ return null;
+ };
+
+ const fadeColor = (color, alpha = 0.35) => {
+ const c = String(color || "").trim();
+ if (!c) return undefined;
+ if (c.startsWith("#")) {
+ const rgb = parseHexToRgb(c);
+ if (!rgb) return c;
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
+ }
+ const rgbMatch = c.match(
+ /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)(?:\s*,\s*[\d.]+\s*)?\)$/i
+ );
+ if (rgbMatch) {
+ const r = Number(rgbMatch[1]);
+ const g = Number(rgbMatch[2]);
+ const b = Number(rgbMatch[3]);
+ if ([r, g, b].some(n => Number.isNaN(n))) return c;
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+ }
+ if (c.includes("--el-color-primary")) {
+ return "var(--el-color-primary-light-5)";
+ }
+ if (c.includes("--el-color-danger")) {
+ return "var(--el-color-danger-light-5)";
+ }
+ return "var(--el-text-color-disabled)";
+ };
+
+ const getOperationColor = (operation, row) => {
+ const baseColor =
+ operation?.name === "鍒犻櫎" || operation?.name === "delete"
+ ? "#D93025"
+ : operation?.name === "璇︽儏"
+ ? "#67C23A"
+ : operation?.color || "var(--el-color-primary)";
+
+ if (isOperationDisabled(operation, row)) {
+ return fadeColor(baseColor, 0.35);
+ }
+ return baseColor;
+ };
+
+ // 鏂囦欢鍙樺寲澶勭悊
+ const handleChange = (file, fileList, index) => {
+ if (fileList.length > 1) {
+ const earliestFile = fileList[0];
+ uploadRefs.value[index]?.handleRemove(earliestFile);
+ }
+ currentFiles.value[index] = file;
+ };
+
+ // 鏂囦欢涓婁紶鍓嶆牎楠�
+ const beforeUpload = (rawFile, index) => {
+ currentFiles.value[index] = {};
+ if (rawfile.size > 1024 * 1024 * 10 * 10) {
+ ElMessage.error("涓婁紶鏂囦欢涓嶈秴杩�10M");
+ return false;
+ }
+ return true;
+ };
+
+ // 涓婁紶鎴愬姛
+ const handleSuccessUp = (response, file, fileList, index) => {
+ if (response.code == 200) {
+ if (uploadRefs[index]) {
+ uploadRefs[index].clearFiles();
+ }
+ currentFiles[index] = file;
+ ElMessage.success("涓婁紶鎴愬姛");
+ resetUploadComponent(index);
+ } else {
+ ElMessage.error(response.message);
+ }
+ };
+
+ const resetUploadComponent = index => {
+ uploadKeys[index] = Date.now();
+ };
+
+ // 涓婁紶澶辫触
+ const onError = (error, file, fileList, index) => {
+ ElMessage.error("鏂囦欢涓婁紶澶辫触锛岃閲嶈瘯");
+ if (uploadRefs.value[index]) {
+ uploadRefs.value[index].clearFiles();
+ }
+ };
+
+ // 鏂囦欢鏁伴噺瓒呴檺鎻愮ず
+ const onExceed = () => {
+ ElMessage.warning("瓒呭嚭鏂囦欢涓暟");
+ };
+
+ const paginationSearch = ({ page, limit }) => {
+ emit("pagination", { page: page, limit: limit });
+ };
+
+ const rowClick = row => {
+ emit("row-click", row);
+ };
+
+ const expandChange = (row, expandedRows) => {
+ emit("expand-change", row, expandedRows);
+ };
+
+ const handleSelectionChange = newSelection => {
+ emit("selection-change", newSelection);
+ };
+</script>
+
+<style scoped lang="scss">
+ .lims-table {
+ border: 1px solid var(--surface-border);
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.9);
+ }
+
+ .cell {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .pim-table-header-extra :deep(.el-input),
+ .pim-table-header-extra :deep(.el-select) {
+ width: 100%;
+ }
+
+ .pim-table-header-title {
+ font-weight: 600;
+ }
+</style>
diff --git a/src/components/PIMTable/Pagination.vue b/src/components/PIMTable/Pagination.vue
new file mode 100644
index 0000000..001f19a
--- /dev/null
+++ b/src/components/PIMTable/Pagination.vue
@@ -0,0 +1,99 @@
+<template>
+ <div :class="{ hidden }" class="pagination-container">
+ <el-pagination
+ :background="background"
+ v-model:current-page="currentPage"
+ v-model:page-size="pageSize"
+ :layout="layout"
+ :page-sizes="pageSizes"
+ :pager-count="pagerCount"
+ :total="total"
+ v-bind="$attrs"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+import { computed } from 'vue'
+import { scrollTo } from '@/utils/scroll-to'
+
+const props = defineProps({
+ total: {
+ type: Number,
+ required: true
+ },
+ page: {
+ type: Number,
+ default: 1
+ },
+ limit: {
+ type: Number,
+ default: 20
+ },
+ pageSizes: {
+ type: Array,
+ default: () => [10, 20, 30, 50, 100]
+ },
+ pagerCount: {
+ type: Number,
+ default: () => (document.body.clientWidth < 992 ? 5 : 7)
+ },
+ layout: {
+ type: String,
+ default: 'total, sizes, prev, pager, next, jumper'
+ },
+ background: {
+ type: Boolean,
+ default: true
+ },
+ autoScroll: {
+ type: Boolean,
+ default: true
+ },
+ hidden: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const emit = defineEmits(['update:page', 'update:limit', 'pagination'])
+
+const currentPage = computed({
+ get: () => props.page,
+ set: (val) => emit('update:page', val)
+})
+
+const pageSize = computed({
+ get: () => props.limit,
+ set: (val) => emit('update:limit', val)
+})
+
+const handleSizeChange = (val) => {
+ if (currentPage.value * val > props.total) {
+ currentPage.value = 1
+ }
+ emit('pagination', { page: currentPage.value, limit: val })
+ if (props.autoScroll) {
+ scrollTo(0, 800)
+ }
+}
+
+const handleCurrentChange = (val) => {
+ emit('pagination', { page: val, limit: pageSize.value })
+ if (props.autoScroll) {
+ scrollTo(0, 800)
+ }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+ background: #fff;
+ margin-top: 0;
+}
+.pagination-container.hidden {
+ display: none;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/PageHeader/index.vue b/src/components/PageHeader/index.vue
new file mode 100644
index 0000000..60d3961
--- /dev/null
+++ b/src/components/PageHeader/index.vue
@@ -0,0 +1,63 @@
+<template>
+ <div class="page-header-wrapper">
+ <el-page-header @back="handleBack" :content="content">
+ <template #icon v-if="$slots.icon">
+ <slot name="icon"></slot>
+ </template>
+ <template #title v-if="$slots.title">
+ <slot name="title"></slot>
+ </template>
+ <template #content v-if="$slots.content">
+ <slot name="content"></slot>
+ </template>
+ <template #extra>
+ <slot name="extra">
+ <slot name="right-button"></slot>
+ </slot>
+ </template>
+ </el-page-header>
+ </div>
+</template>
+
+<script setup>
+import { useRouter } from 'vue-router'
+
+const props = defineProps({
+ content: {
+ type: String,
+ default: ''
+ }
+})
+
+const emit = defineEmits(['back'])
+
+const router = useRouter()
+
+const handleBack = () => {
+ emit('back')
+ // 榛樿杩斿洖鍒颁笂涓�绾�
+ router.back()
+}
+</script>
+
+<style scoped>
+.page-header-wrapper {
+ margin-bottom: 16px;
+ padding: 16px 18px;
+ border: 1px solid var(--surface-border);
+ border-radius: var(--radius-md);
+ background: rgba(255, 255, 255, 0.82);
+ box-shadow: var(--shadow-sm);
+}
+
+.page-header-wrapper :deep(.el-page-header__extra) {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.page-header-wrapper :deep(.el-page-header__content) {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+</style>
diff --git a/src/components/Pagination/index.vue b/src/components/Pagination/index.vue
new file mode 100644
index 0000000..53fbec2
--- /dev/null
+++ b/src/components/Pagination/index.vue
@@ -0,0 +1,105 @@
+<template>
+ <div :class="{ 'hidden': hidden }" class="pagination-container">
+ <el-pagination
+ :background="background"
+ v-model:current-page="currentPage"
+ v-model:page-size="pageSize"
+ :layout="layout"
+ :page-sizes="pageSizes"
+ :pager-count="pagerCount"
+ :total="total"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+</template>
+
+<script setup>
+import { scrollTo } from '@/utils/scroll-to'
+
+const props = defineProps({
+ total: {
+ required: true,
+ type: Number
+ },
+ page: {
+ type: Number,
+ default: 1
+ },
+ limit: {
+ type: Number,
+ default: 20
+ },
+ pageSizes: {
+ type: Array,
+ default() {
+ return [10, 20, 30, 50]
+ }
+ },
+ // 绉诲姩绔〉鐮佹寜閽殑鏁伴噺绔粯璁ゅ��5
+ pagerCount: {
+ type: Number,
+ default: document.body.clientWidth < 992 ? 5 : 7
+ },
+ layout: {
+ type: String,
+ default: 'total, sizes, prev, pager, next, jumper'
+ },
+ background: {
+ type: Boolean,
+ default: true
+ },
+ autoScroll: {
+ type: Boolean,
+ default: true
+ },
+ hidden: {
+ type: Boolean,
+ default: false
+ }
+})
+
+const emit = defineEmits()
+const currentPage = computed({
+ get() {
+ return props.page
+ },
+ set(val) {
+ emit('update:page', val)
+ }
+})
+const pageSize = computed({
+ get() {
+ return props.limit
+ },
+ set(val){
+ emit('update:limit', val)
+ }
+})
+
+function handleSizeChange(val) {
+ if (currentPage.value * val > props.total) {
+ currentPage.value = 1
+ }
+ emit('pagination', { page: currentPage.value, limit: val })
+ if (props.autoScroll) {
+ scrollTo(0, 800)
+ }
+}
+
+function handleCurrentChange(val) {
+ emit('pagination', { page: val, limit: pageSize.value })
+ if (props.autoScroll) {
+ scrollTo(0, 800)
+ }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+ background: #fff;
+}
+.pagination-container.hidden {
+ display: none;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/ParentView/index.vue b/src/components/ParentView/index.vue
new file mode 100644
index 0000000..ad6360c
--- /dev/null
+++ b/src/components/ParentView/index.vue
@@ -0,0 +1,3 @@
+<template >
+ <router-view />
+</template>
diff --git a/src/components/ProcessParamListDialog.vue b/src/components/ProcessParamListDialog.vue
new file mode 100644
index 0000000..deee249
--- /dev/null
+++ b/src/components/ProcessParamListDialog.vue
@@ -0,0 +1,670 @@
+<template>
+ <el-dialog v-model="visible"
+ :title="title"
+ width="800px"
+ destroy-on-close>
+ <div class="param-list-container">
+ <div class="params-header">
+ <span>鍙傛暟鍒楄〃</span>
+ <div>
+ <el-button v-if="editable"
+ type="primary"
+ link
+ size="small"
+ @click="handleAddParam">
+ <el-icon>
+ <Plus />
+ </el-icon>鏂板
+ </el-button>
+ <!-- <el-button v-if="editable"
+ type="primary"
+ link
+ size="small"
+ @click="getsyncProcessParamItem">
+ <el-icon>
+ <Refresh />
+ </el-icon>鍚屾宸ュ簭鍙傛暟
+ </el-button> -->
+ </div>
+ </div>
+ <div class="params-list">
+ <div v-for="param in paramList"
+ :key="param.id"
+ class="param-item">
+ <div class="param-info">
+ <span class="param-code">{{ param.paramName }}</span>
+ <span class="param-value">
+ 鏍囧噯鍊硷細{{ param.standardValue || "-" }} {{ param.unit }}
+ </span>
+ </div>
+ <div class="param-actions">
+ <el-button v-if="editable"
+ link
+ type="primary"
+ size="small"
+ @click="handleEditParam(param)">
+ 缂栬緫
+ </el-button>
+ <el-button v-if="editable"
+ link
+ type="danger"
+ size="small"
+ @click="handleDeleteParam(param)">
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ <el-empty v-if="!paramList || paramList.length === 0"
+ description="鏆傛棤鍙傛暟"
+ :image-size="50" />
+ </div>
+ </div>
+ <!-- 閫夋嫨鍙傛暟瀵硅瘽妗� -->
+ <el-dialog v-model="selectParamDialogVisible"
+ title="閫夋嫨鍙傛暟"
+ width="1000px">
+ <div class="param-select-container">
+ <!-- 宸︿晶鍙傛暟鍒楄〃 -->
+ <div class="param-list-area">
+ <div class="area-title">鍙�夊弬鏁�</div>
+ <div class="search-box">
+ <el-input v-model="paramSearchKeyword"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉版悳绱�"
+ clearable
+ size="small"
+ @input="getBaseParamListData">
+ <template #prefix>
+ <el-icon>
+ <Search />
+ </el-icon>
+ </template>
+ </el-input>
+ </div>
+ <el-table :data="filteredParamList"
+ height="400"
+ border
+ highlight-current-row
+ @current-change="handleSelectParam">
+ <el-table-column prop="paramName"
+ label="鍙傛暟鍚嶇О" />
+ <el-table-column prop="paramType"
+ label="鍙傛暟绫诲瀷">
+ <template #default="scope">
+ <el-tag size="small"
+ :type="getParamTypeTag(scope.row.paramType)">{{ getParamTypeText(scope.row.paramType) }}</el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉鎺т欢 -->
+ <div class="pagination-container"
+ style="margin-top: 10px;">
+ <el-pagination :current-page="paramPage.current"
+ :page-size="paramPage.size"
+ :page-sizes="[10, 20, 50, 100]"
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="paramPage.total"
+ @size-change="getBaseParamListData"
+ @current-change="getBaseParamListData"
+ size="small" />
+ </div>
+ </div>
+ <!-- 鍙充晶鍙傛暟璇︽儏 -->
+ <div class="param-detail-area">
+ <div class="area-title">鍙傛暟璇︽儏</div>
+ <el-form v-if="selectedParam"
+ :model="selectedParam"
+ label-width="100px"
+ class="param-detail-form">
+ <el-form-item label="鍙傛暟鍚嶇О">
+ <span class="detail-text">{{ selectedParam.paramName }}</span>
+ </el-form-item>
+ <el-form-item label="鍙傛暟绫诲瀷">
+ <el-tag size="small"
+ :type="getParamTypeTag(selectedParam.paramType)">{{ getParamTypeText(selectedParam.paramType) }}</el-tag>
+ </el-form-item>
+ <el-form-item label="鍙傛暟鏍煎紡">
+ <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
+ </el-form-item>
+ <el-form-item label="鍗曚綅">
+ <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
+ </el-form-item>
+ <el-form-item label="鏍囧噯鍊�">
+ <el-input v-model="selectedParam.standardValue"
+ @input="val => onStandardValueInput(val, selectedParam)"
+ placeholder="璇疯緭鍏ラ粯璁ゅ��" />
+ </el-form-item>
+ <el-form-item label="鏄惁蹇呭~">
+ <el-switch :active-value="1"
+ :inactive-value="0"
+ v-model="selectedParam.isRequired" />
+ </el-form-item>
+ </el-form>
+ <el-empty v-else
+ description="璇蜂粠宸︿晶閫夋嫨鍙傛暟"
+ :image-size="100" />
+ </div>
+ </div>
+ <template #footer>
+ <el-button type="primary"
+ @click="handleParamSelectSubmit">纭畾</el-button>
+ <el-button @click="selectParamDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ <!-- 缂栬緫鍙傛暟瀵硅瘽妗� -->
+ <el-dialog v-model="editParamDialogVisible"
+ title="缂栬緫鍙傛暟"
+ width="600px">
+ <el-form :model="editParamForm"
+ :rules="editParamRules"
+ ref="editParamFormRef"
+ label-width="120px">
+ <el-form-item label="鍙傛暟鍚嶇О">
+ <span class="detail-text">{{ editParamForm.paramName }}</span>
+ </el-form-item>
+ <el-form-item label="鍙傛暟绫诲瀷">
+ <el-tag size="small"
+ :type="getParamTypeTag(editParamForm.paramType)">
+ {{ getParamTypeText(editParamForm.paramType) }}
+ </el-tag>
+ </el-form-item>
+ <el-form-item label="鍙傛暟鏍煎紡">
+ <span class="detail-text">{{ editParamForm.paramFormat || '-' }}</span>
+ </el-form-item>
+ <el-form-item label="鍗曚綅">
+ <span class="detail-text">{{ editParamForm.unit || '-' }}</span>
+ </el-form-item>
+ <el-form-item label="鏍囧噯鍊�"
+ prop="standardValue">
+ <el-input v-model="editParamForm.standardValue"
+ @input="val => onStandardValueInput(val, editParamForm)"
+ placeholder="璇疯緭鍏ユ爣鍑嗗��" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary"
+ @click="handleEditParamSubmit">纭畾</el-button>
+ <el-button @click="editParamDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </el-dialog>
+</template>
+
+<script setup>
+ import { ref, computed, watch } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { Plus, Search } from "@element-plus/icons-vue";
+ import {
+ delProcessRouteItemParam,
+ editProcessRouteItemParam,
+ addProcessRouteItemParam,
+ } from "@/api/productionManagement/processRouteItem.js";
+ import {
+ addProcessRouteItemParamOrder,
+ delProcessRouteItemParamOrder,
+ editProcessRouteItemParamOrder,
+ } from "@/api/productionManagement/productProcessRoute.js";
+
+ import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
+
+ const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false,
+ },
+ title: {
+ type: String,
+ default: "鍙傛暟鍒楄〃",
+ },
+ routeId: {
+ type: Number,
+ default: 0,
+ },
+ process: {
+ type: Object,
+ default: () => ({}),
+ },
+ paramList: {
+ type: Array,
+ default: () => [],
+ },
+ editable: {
+ type: Boolean,
+ default: true,
+ },
+ orderId: {
+ type: Number,
+ default: 0,
+ },
+ pageType: {
+ type: String,
+ default: "route",
+ },
+ });
+
+ const emit = defineEmits(["update:modelValue", "refresh"]);
+
+ const visible = computed({
+ get: () => props.modelValue,
+ set: value => emit("update:modelValue", value),
+ });
+
+ // 鍝嶅簲寮忔暟鎹�
+ const selectParamDialogVisible = ref(false);
+ const editParamDialogVisible = ref(false);
+ const paramSearchKeyword = ref("");
+ const selectedParam = ref(null);
+ const filteredParamList = ref([]);
+ const paramPage = ref({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const editParamForm = ref({
+ id: null,
+ processId: null,
+ paramId: null,
+ paramName: "",
+ standardValue: null,
+ isRequired: 0,
+ paramType: null,
+ paramFormat: "",
+ unit: "",
+ });
+
+ const onStandardValueInput = (val, target) => {
+ const data = target.value || target;
+ const type = data.paramType || data.parameterType;
+ if (type === 1) {
+ // 鏁板�兼牸寮忥細涓嶈兘杈撳叆涓枃鎴栬嫳鏂囧瓧绗�
+ data.standardValue = val.replace(/[a-zA-Z\u4e00-\u9fa5]/g, "");
+ }
+ };
+
+ const editParamRules = ref({
+ standardValue: [
+ {
+ validator: (rule, value, callback) => {
+ const type =
+ editParamForm.value.paramType || editParamForm.value.parameterType;
+ if (type === 1 && value) {
+ if (/[a-zA-Z\u4e00-\u9fa5]/.test(value)) {
+ return callback(new Error("鏁板�兼牸寮忎笉鑳藉寘鍚腑鑻辨枃瀛楃"));
+ }
+ }
+ callback();
+ },
+ trigger: "blur",
+ },
+ ],
+ });
+ const editParamFormRef = ref(null);
+
+ // 鏂板鍙傛暟
+ const handleAddParam = () => {
+ selectedParam.value = null;
+ paramSearchKeyword.value = "";
+ paramPage.value.current = 1;
+ // 鑾峰彇鍙�夊弬鏁板垪琛�
+ getBaseParamListData();
+ selectParamDialogVisible.value = true;
+ };
+
+ // 缂栬緫鍙傛暟
+ const handleEditParam = param => {
+ editParamForm.value = {
+ id: param.id,
+ processId: props.process.id,
+ paramId: param.paramId,
+ paramName: param.parameterName || param.paramName,
+ standardValue: param.standardValue,
+ isRequired: param.isRequired || 0,
+ paramType: param.parameterType || param.paramType,
+ paramFormat: param.parameterFormat || param.paramFormat,
+ unit: param.unit || param.unit,
+ };
+ editParamDialogVisible.value = true;
+ };
+
+ // 鍒犻櫎鍙傛暟
+ const handleDeleteParam = param => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ鍙傛暟鍚楋紵", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 璋冪敤API鍒犻櫎鍙傛暟
+ if (props.pageType === "order") {
+ delProcessRouteItemParamOrder(param.id)
+ .then(res => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ emit("refresh");
+ })
+ .catch(err => {
+ ElMessage.error("鍒犻櫎鍙傛暟澶辫触");
+ console.error("鍒犻櫎鍙傛暟澶辫触锛�", err);
+ });
+ } else {
+ delProcessRouteItemParam(param.id)
+ .then(res => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ emit("refresh");
+ })
+ .catch(err => {
+ ElMessage.error("鍒犻櫎鍙傛暟澶辫触");
+ console.error("鍒犻櫎鍙傛暟澶辫触锛�", err);
+ });
+ }
+ })
+ .catch(() => {});
+ };
+ const getsyncProcessParamItem = () => {
+ emit("getsyncProcessParamItem");
+ };
+
+ // 鑾峰彇鍙�夊弬鏁板垪琛�
+ const getBaseParamListData = () => {
+ console.log(paramPage, "paramPage.size");
+
+ getBaseParamList({
+ paramName: paramSearchKeyword.value,
+ current: paramPage.value.current,
+ size: paramPage.value.size,
+ }).then(res => {
+ if (res.code === 200) {
+ filteredParamList.value = res.data?.records || [];
+ paramPage.value.total = res.data.total || 0;
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ });
+ };
+
+ // 閫夋嫨鍙傛暟
+ const handleSelectParam = param => {
+ selectedParam.value = param;
+ };
+
+ // 鎻愪氦閫夋嫨鍙傛暟
+ const handleParamSelectSubmit = () => {
+ if (!selectedParam.value) {
+ ElMessage.warning("璇峰厛閫夋嫨涓�涓弬鏁�");
+ return;
+ }
+
+ if (!props.process || !props.process.id) {
+ ElMessage.error("宸ヨ壓璺嚎椤圭洰淇℃伅涓嶅畬鏁�");
+ return;
+ }
+
+ // 璋冪敤API鏂板鍙傛暟
+ if (props.pageType === "order") {
+ addProcessRouteItemParamOrder({
+ productionOrderId: Number(props.orderId),
+ productionOrderRoutingOperationId: props.process.id,
+ technologyRoutingOperationParamId: props.process.id,
+ paramId: selectedParam.value.id,
+ standardValue: selectedParam.value.standardValue || "",
+ isRequired: selectedParam.value.isRequired || 0,
+ })
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("娣诲姞鍙傛暟鎴愬姛");
+ selectParamDialogVisible.value = false;
+ emit("refresh");
+ } else {
+ ElMessage.error(res.msg || "娣诲姞鍙傛暟澶辫触");
+ }
+ })
+ .catch(err => {
+ ElMessage.error("娣诲姞鍙傛暟澶辫触");
+ console.error("娣诲姞鍙傛暟澶辫触锛�", err);
+ });
+ } else {
+ console.log(selectedParam.value, "selectedParam");
+
+ addProcessRouteItemParam({
+ technologyRoutingOperationId: props.process.id,
+ paramId: selectedParam.value.id,
+ standardValue: selectedParam.value.standardValue || "",
+ isRequired: selectedParam.value.isRequired || 0,
+ })
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("娣诲姞鍙傛暟鎴愬姛");
+ selectParamDialogVisible.value = false;
+ emit("refresh");
+ } else {
+ ElMessage.error(res.msg || "娣诲姞鍙傛暟澶辫触");
+ }
+ })
+ .catch(err => {
+ ElMessage.error("娣诲姞鍙傛暟澶辫触");
+ console.error("娣诲姞鍙傛暟澶辫触锛�", err);
+ });
+ }
+ };
+
+ // 鎻愪氦缂栬緫鍙傛暟
+ const handleEditParamSubmit = () => {
+ if (!editParamFormRef.value) return;
+ editParamFormRef.value.validate(valid => {
+ if (valid) {
+ if (props.pageType === "order") {
+ editProcessRouteItemParamOrder({
+ id: editParamForm.value.id,
+ standardValue: editParamForm.value.standardValue || "",
+ isRequired: editParamForm.value.isRequired || 0,
+ // productionOrderRoutingOperationId: props.process.id,
+ })
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("缂栬緫鎴愬姛");
+ editParamDialogVisible.value = false;
+ emit("refresh");
+ } else {
+ ElMessage.error(res.msg || "缂栬緫澶辫触");
+ }
+ })
+ .catch(err => {
+ ElMessage.error("缂栬緫鍙傛暟澶辫触");
+ console.error("缂栬緫鍙傛暟澶辫触锛�", err);
+ });
+ } else {
+ // 璋冪敤API淇敼鍙傛暟
+ editProcessRouteItemParam({
+ id: editParamForm.value.id,
+ technologyRoutingOperationId: props.process.id,
+ paramId: editParamForm.value.paramId,
+ standardValue: editParamForm.value.standardValue || "",
+ isRequired: editParamForm.value.isRequired || 0,
+ })
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("缂栬緫鎴愬姛");
+ editParamDialogVisible.value = false;
+ emit("refresh");
+ } else {
+ ElMessage.error(res.msg || "缂栬緫澶辫触");
+ }
+ })
+ .catch(err => {
+ ElMessage.error("缂栬緫鍙傛暟澶辫触");
+ console.error("缂栬緫鍙傛暟澶辫触锛�", err);
+ });
+ }
+ }
+ });
+ };
+
+ // 鑾峰彇鍙傛暟绫诲瀷鏍囩
+ const getParamTypeTag = type => {
+ const typeMap = {
+ 1: "primary",
+ 2: "info",
+ 3: "warning",
+ 4: "success",
+ };
+ return typeMap[type] || "default";
+ };
+
+ // 鑾峰彇鍙傛暟绫诲瀷鏂囨湰
+ const getParamTypeText = type => {
+ const typeMap = {
+ 1: "鏁板�兼牸寮�",
+ 2: "鏂囨湰鏍煎紡",
+ 3: "涓嬫媺閫夐」",
+ 4: "鏃堕棿鏍煎紡",
+ };
+ return typeMap[type] || type;
+ };
+
+ watch(
+ () => props.modelValue,
+ newVal => {
+ if (!newVal) {
+ // 寮圭獥鍏抽棴鏃堕噸缃暟鎹�
+ selectParamDialogVisible.value = false;
+ editParamDialogVisible.value = false;
+ selectedParam.value = null;
+ paramSearchKeyword.value = "";
+ paramPage.value.current = 1;
+ filteredParamList.value = [];
+ editParamForm.value = {
+ id: null,
+ processId: null,
+ paramId: null,
+ paramName: "",
+ standardValue: null,
+ isRequired: 0,
+ paramType: null,
+ paramFormat: "",
+ unit: "",
+ };
+ }
+ }
+ );
+</script>
+
+<style scoped>
+ .param-list-container {
+ padding: 10px 0;
+ }
+
+ .params-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #e4e7ed;
+ }
+
+ .params-header span {
+ font-size: 16px;
+ font-weight: 500;
+ color: #303133;
+ }
+
+ .params-list {
+ max-height: 400px;
+ overflow-y: auto;
+ }
+
+ .param-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ margin-bottom: 8px;
+ background-color: #f9f9f9;
+ border-radius: 4px;
+ transition: all 0.3s ease;
+ }
+
+ .param-item:hover {
+ background-color: #ecf5ff;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .param-info {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ flex: 1;
+ }
+
+ .param-code {
+ font-weight: 500;
+ color: #303133;
+ min-width: 120px;
+ }
+
+ .param-value {
+ color: #606266;
+ font-size: 14px;
+ }
+
+ .param-actions {
+ display: flex;
+ gap: 10px;
+ }
+
+ /* 婊氬姩鏉℃牱寮� */
+ .params-list::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ .params-list::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 3px;
+ }
+
+ .params-list::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 3px;
+ }
+
+ .params-list::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+ }
+
+ /* 閫夋嫨鍙傛暟瀵硅瘽妗嗘牱寮� */
+ .param-select-container {
+ display: flex;
+ gap: 20px;
+ }
+
+ .param-list-area {
+ flex: 1;
+ min-width: 400px;
+ }
+
+ .param-detail-area {
+ flex: 1;
+ min-width: 300px;
+ }
+
+ .area-title {
+ font-size: 14px;
+ font-weight: 500;
+ margin-bottom: 10px;
+ color: #303133;
+ }
+
+ .search-box {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 10px;
+ }
+
+ .param-detail-form {
+ background: #f9f9f9;
+ padding: 15px;
+ border-radius: 4px;
+ }
+
+ .detail-text {
+ font-weight: 500;
+ }
+</style>
\ No newline at end of file
diff --git a/src/components/ProjectManagement/DiscussProgressDialog.vue b/src/components/ProjectManagement/DiscussProgressDialog.vue
new file mode 100644
index 0000000..e278a50
--- /dev/null
+++ b/src/components/ProjectManagement/DiscussProgressDialog.vue
@@ -0,0 +1,141 @@
+<template>
+ <el-dialog v-model="visible" title="娲借皥杩涘害" width="700px" top="10vh" append-to-body destroy-on-close @close="handleClose">
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px">
+ <el-form-item label="椤圭洰闃舵" prop="planNodeId">
+ <el-select v-model="form.planNodeId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="璇疯緭鍏�" />
+ </el-form-item>
+
+ <el-form-item label="闄勪欢" prop="attachmentIds">
+ <el-upload
+ v-model:file-list="fileList"
+ :action="upload.url"
+ :headers="upload.headers"
+ multiple
+ name="files"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :on-remove="handleRemove"
+ >
+ <el-button type="primary">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ <el-button type="danger" @click="submit">鎻愪氦</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup name="DiscussProgressDialog">
+import { computed, reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ projectId: { type: [Number, String], default: undefined },
+ planNodes: { type: Array, default: () => [] },
+ defaultPlanNodeId: { type: [Number, String], default: undefined }
+})
+
+const emit = defineEmits(['update:modelValue', 'submitted'])
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: v => emit('update:modelValue', v)
+})
+
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
+ headers: { Authorization: 'Bearer ' + getToken() }
+})
+
+const formRef = ref()
+const fileList = ref([])
+const form = ref({
+ planNodeId: undefined,
+ remark: '',
+ attachmentIds: []
+})
+
+const rules = {
+ planNodeId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ remark: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }]
+}
+
+const stageOptions = computed(() => {
+ const list = Array.isArray(props.planNodes) ? props.planNodes : []
+ const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ return sorted
+ .map(n => ({
+ label: n.name || n.workContent || n.title || String(n.id ?? ''),
+ value: n.id
+ }))
+ .filter(i => i.value !== undefined && i.value !== null && i.value !== '')
+})
+
+watch(
+ () => props.modelValue,
+ v => {
+ if (v) {
+ form.value = { planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value, remark: '', attachmentIds: [] }
+ fileList.value = []
+ }
+ }
+)
+
+function handleClose() {
+ formRef.value?.resetFields?.()
+}
+
+function handleUploadError() {
+ ElMessage.error('涓婁紶鏂囦欢澶辫触')
+}
+
+function handleUploadSuccess(res, file) {
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || '涓婁紶澶辫触')
+ return
+ }
+ const attachmentId = res?.data?.id ?? res?.data?.tempId ?? ''
+ if (!attachmentId) return
+ form.value.attachmentIds.push(attachmentId)
+ try {
+ file.attachmentId = attachmentId
+ } catch (e) {}
+ ElMessage.success('涓婁紶鎴愬姛')
+}
+
+function handleRemove(file) {
+ const attachmentId = file?.attachmentId
+ if (!attachmentId) return
+ form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
+}
+
+async function submit() {
+ await formRef.value?.validate?.()
+ emit('submitted', {
+ projectId: props.projectId,
+ ...form.value
+ })
+ visible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+</style>
diff --git a/src/components/ProjectManagement/ProgressReportDialog.vue b/src/components/ProjectManagement/ProgressReportDialog.vue
new file mode 100644
index 0000000..dc6afe7
--- /dev/null
+++ b/src/components/ProjectManagement/ProgressReportDialog.vue
@@ -0,0 +1,242 @@
+<template>
+ <el-dialog v-model="visible" title="杩涘害姹囨姤" width="900px" top="8vh" append-to-body destroy-on-close @close="handleClose">
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top" label-width="120px">
+ <el-form-item label="椤圭洰闃舵" prop="planNodeId">
+ <el-select v-model="form.planNodeId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in stageOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="璁″垝寮�濮嬫椂闂�" prop="planStartTime">
+ <el-date-picker
+ v-model="form.planStartTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璁″垝瀹屽伐鏃堕棿" prop="planEndTime">
+ <el-date-picker
+ v-model="form.planEndTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹為檯寮�宸ユ棩鏈�" prop="actualStartTime">
+ <el-date-picker
+ v-model="form.actualStartTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鏈杩涘害鏃ユ湡" prop="reportDate">
+ <el-date-picker
+ v-model="form.reportDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="涓婃杩涘害(%)" prop="lastProgress">
+ <el-input-number v-model="form.lastProgress" :min="0" :max="100" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹屾垚杩涘害(%)" prop="completionProgress">
+ <div style="display: flex; gap: 8px; width: 100%;">
+ <el-input-number v-model="form.completionProgress" :min="0" :max="100" controls-position="right" style="flex: 1" />
+ <el-button type="danger" @click="markDone">瀹屾垚</el-button>
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="绱杩涘害(%)" prop="totalProgress">
+ <el-input-number v-model="form.totalProgress" :min="0" :max="100" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀹為檯瀹屽伐鏃ユ湡" prop="actualEndTime">
+ <el-date-picker
+ v-model="form.actualEndTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="璐熻矗浜�" prop="managerName">
+ <el-input v-model="form.managerName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閮ㄩ棬" prop="departmentName">
+ <el-input v-model="form.departmentName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="16">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="闄勪欢" prop="attachmentIds">
+ <FileUpload v-model:file-list="form.storageBlobDTOs" />
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submit">纭畾</el-button>
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup name="ProgressReportDialog">
+import { computed, reactive, ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ projectId: { type: [Number, String], default: undefined },
+ projectInfo: { type: Object, default: () => ({}) },
+ planNodes: { type: Array, default: () => [] },
+ defaultPlanNodeId: { type: [Number, String], default: undefined }
+})
+
+const emit = defineEmits(['update:modelValue', 'submitted'])
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: v => emit('update:modelValue', v)
+})
+
+const formRef = ref()
+
+const form = ref({
+ planNodeId: undefined,
+ planStartTime: '',
+ planEndTime: '',
+ actualStartTime: '',
+ actualEndTime: '',
+ reportDate: '',
+ lastProgress: 0,
+ completionProgress: 0,
+ totalProgress: 0,
+ managerName: '',
+ departmentName: '',
+ remark: '',
+ storageBlobDTOs: []
+})
+
+const rules = {
+ planNodeId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ planStartTime: [{ required: true, message: '璇烽�夋嫨璁″垝寮�濮嬫椂闂�', trigger: 'change' }],
+ planEndTime: [{ required: true, message: '璇烽�夋嫨璁″垝瀹屽伐鏃堕棿', trigger: 'change' }],
+ reportDate: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ completionProgress: [{ required: true, message: '璇疯緭鍏�', trigger: 'change' }]
+}
+
+const stageOptions = computed(() => {
+ const list = Array.isArray(props.planNodes) ? props.planNodes : []
+ const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ return sorted
+ .map(n => ({
+ label: n.name || n.workContent || n.title || String(n.id ?? ''),
+ value: n.id
+ }))
+ .filter(i => i.value !== undefined && i.value !== null && i.value !== '')
+})
+
+function resetFromProject() {
+ const info = props.projectInfo || {}
+ form.value = {
+ planNodeId: props.defaultPlanNodeId ?? stageOptions.value[0]?.value,
+ planStartTime: info.planStartTime || '',
+ planEndTime: info.planEndTime || '',
+ actualStartTime: info.actualStartTime || '',
+ actualEndTime: info.actualEndTime || '',
+ reportDate: '',
+ lastProgress: Number(info.lastProgress ?? 0) || 0,
+ completionProgress: 0,
+ totalProgress: Number(info.totalProgress ?? info.progress ?? 0) || 0,
+ managerName: info.managerName || '',
+ departmentName: info.departmentName || '',
+ remark: '',
+ storageBlobDTOs: []
+ }
+}
+
+watch(
+ () => props.modelValue,
+ v => {
+ if (v) resetFromProject()
+ }
+)
+
+function handleClose() {
+ formRef.value?.resetFields?.()
+}
+
+function markDone() {
+ form.value.completionProgress = 100
+ form.value.totalProgress = 100
+ if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || ''
+}
+
+async function submit() {
+ await formRef.value?.validate?.()
+ emit('submitted', {
+ projectId: props.projectId,
+ ...form.value
+ })
+ visible.value = false
+}
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+</style>
diff --git a/src/components/PurchaseAIChatSidebar/index.vue b/src/components/PurchaseAIChatSidebar/index.vue
new file mode 100644
index 0000000..428f84f
--- /dev/null
+++ b/src/components/PurchaseAIChatSidebar/index.vue
@@ -0,0 +1,10 @@
+<template>
+ <AIChatSidebar :assistants="assistants" default-assistant="purchase" />
+</template>
+
+<script setup>
+import AIChatSidebar from '@/components/AIChatSidebar/index.vue'
+import { purchaseAssistant } from '@/components/AIChatSidebar/assistants'
+
+const assistants = [purchaseAssistant]
+</script>
diff --git a/src/components/QRCodeGenerator/index.vue b/src/components/QRCodeGenerator/index.vue
new file mode 100644
index 0000000..fd44f44
--- /dev/null
+++ b/src/components/QRCodeGenerator/index.vue
@@ -0,0 +1,566 @@
+<template>
+ <div class="qr-code-generator">
+ <!-- 浜岀淮鐮佺敓鎴愯〃鍗� -->
+ <el-form :model="form"
+ :rules="rules"
+ ref="formRef"
+ label-width="120px"
+ class="qr-form">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏍囪瘑绫诲瀷"
+ prop="type">
+ <el-select v-model="form.type"
+ placeholder="璇烽�夋嫨鏍囪瘑绫诲瀷"
+ style="width: 100%">
+ <el-option label="浜岀淮鐮�"
+ value="qrcode"></el-option>
+ <el-option label="闃蹭吉鐮�"
+ value="security"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍐呭"
+ prop="content">
+ <el-input v-model="form.content"
+ placeholder="璇疯緭鍏ヨ缂栫爜鐨勫唴瀹�"
+ :type="form.type === 'security' ? 'textarea' : 'text'"
+ :rows="form.type === 'security' ? 3 : 1"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="灏哄"
+ prop="size">
+ <el-input-number v-model="form.size"
+ :min="100"
+ :max="500"
+ :step="50"
+ style="width: 100%"></el-input-number>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杈硅窛"
+ prop="margin">
+ <el-input-number v-model="form.margin"
+ :min="0"
+ :max="10"
+ :step="1"
+ style="width: 100%"></el-input-number>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍓嶆櫙鑹�"
+ prop="foregroundColor">
+ <el-color-picker v-model="form.foregroundColor"
+ style="width: 100%"></el-color-picker>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑳屾櫙鑹�"
+ prop="backgroundColor">
+ <el-color-picker v-model="form.backgroundColor"
+ style="width: 100%"></el-color-picker>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item>
+ <el-button type="primary"
+ @click="generateCode"
+ :loading="generating">
+ 鐢熸垚{{ form.type === 'qrcode' ? '浜岀淮鐮�' : '闃蹭吉鐮�' }}
+ </el-button>
+ <el-button @click="resetForm">閲嶇疆</el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <!-- 鐢熸垚鐨勭爜鏄剧ず鍖哄煙 -->
+ <div v-if="generatedCodeUrl"
+ class="code-display">
+ <el-divider content-position="center">
+ {{ form.type === 'qrcode' ? '鐢熸垚鐨勪簩缁寸爜' : '鐢熸垚鐨勯槻浼爜' }}
+ </el-divider>
+ <div class="code-container">
+ <div class="code-image">
+ <img :src="generatedCodeUrl"
+ :alt="form.type === 'qrcode' ? '浜岀淮鐮�' : '闃蹭吉鐮�'" />
+ </div>
+ <div class="code-info">
+ <p><strong>鍐呭锛�</strong>{{ form.content }}</p>
+ <p><strong>绫诲瀷锛�</strong>{{ form.type === 'qrcode' ? '浜岀淮鐮�' : '闃蹭吉鐮�' }}</p>
+ <p><strong>灏哄锛�</strong>{{ form.size }}x{{ form.size }}px</p>
+ <p><strong>鐢熸垚鏃堕棿锛�</strong>{{ generateTime }}</p>
+ </div>
+ </div>
+ <div class="code-actions">
+ <el-button type="success"
+ @click="downloadCode"
+ icon="Download">
+ 涓嬭浇鍥剧墖
+ </el-button>
+ <el-button type="primary"
+ @click="copyToClipboard"
+ icon="CopyDocument">
+ 澶嶅埗鍐呭
+ </el-button>
+ <el-button @click="printCode"
+ icon="Printer">
+ 鎵撳嵃
+ </el-button>
+ </div>
+ </div>
+ <!-- 鎵归噺鐢熸垚瀵硅瘽妗� -->
+ <el-dialog v-model="batchDialogVisible"
+ title="鎵归噺鐢熸垚"
+ width="600px">
+ <el-form :model="batchForm"
+ label-width="120px">
+ <el-form-item label="鐢熸垚鏁伴噺">
+ <el-input-number v-model="batchForm.quantity"
+ :min="1"
+ :max="100"
+ style="width: 100%"></el-input-number>
+ </el-form-item>
+ <el-form-item label="鍓嶇紑">
+ <el-input v-model="batchForm.prefix"
+ placeholder="璇疯緭鍏ュ墠缂�锛屽锛歅ROD_"></el-input>
+ </el-form-item>
+ <el-form-item label="璧峰缂栧彿">
+ <el-input-number v-model="batchForm.startNumber"
+ :min="1"
+ style="width: 100%"></el-input-number>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="generateBatchCodes">寮�濮嬬敓鎴�</el-button>
+ <el-button @click="batchDialogVisible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 鎵归噺鐢熸垚缁撴灉 -->
+ <div v-if="batchCodes.length > 0"
+ class="batch-results">
+ <el-divider content-position="center">鎵归噺鐢熸垚缁撴灉</el-divider>
+ <div class="batch-grid">
+ <div v-for="(code, index) in batchCodes"
+ :key="index"
+ class="batch-item">
+ <img :src="code.url"
+ :alt="code.content" />
+ <p class="batch-content">{{ code.content }}</p>
+ <el-button size="small"
+ @click="downloadSingleCode(code)">涓嬭浇</el-button>
+ </div>
+ </div>
+ <div class="batch-actions">
+ <el-button type="success"
+ @click="downloadAllCodes">涓嬭浇鍏ㄩ儴</el-button>
+ <el-button @click="clearBatchCodes">娓呯┖缁撴灉</el-button>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, onMounted } from "vue";
+ import QRCode from "qrcode";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { Download, CopyDocument, Printer } from "@element-plus/icons-vue";
+
+ // 瀹氫箟缁勪欢鍚嶇О
+ defineOptions({
+ name: "QRCodeGenerator",
+ });
+
+ // 琛ㄥ崟鏁版嵁
+ const form = reactive({
+ type: "qrcode",
+ content: "",
+ size: 200,
+ margin: 2,
+ foregroundColor: "#000000",
+ backgroundColor: "#FFFFFF",
+ });
+
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ type: [{ required: true, message: "璇烽�夋嫨鏍囪瘑绫诲瀷", trigger: "change" }],
+ content: [{ required: true, message: "璇疯緭鍏ュ唴瀹�", trigger: "blur" }],
+ };
+
+ // 鍝嶅簲寮忔暟鎹�
+ const formRef = ref();
+ const generating = ref(false);
+ const generatedCodeUrl = ref("");
+ const generateTime = ref("");
+ const batchDialogVisible = ref(false);
+ const batchForm = reactive({
+ quantity: 10,
+ prefix: "",
+ startNumber: 1,
+ });
+ const batchCodes = ref([]);
+
+ // 鐢熸垚浜岀淮鐮佹垨闃蹭吉鐮�
+ const generateCode = async () => {
+ try {
+ await formRef.value.validate();
+
+ if (!form.content.trim()) {
+ ElMessage.warning("璇疯緭鍏ヨ缂栫爜鐨勫唴瀹�");
+ return;
+ }
+
+ generating.value = true;
+
+ if (form.type === "qrcode") {
+ // 鐢熸垚浜岀淮鐮�
+ generatedCodeUrl.value = await QRCode.toDataURL(form.content, {
+ width: form.size,
+ margin: form.margin,
+ color: {
+ dark: form.foregroundColor,
+ light: form.backgroundColor,
+ },
+ errorCorrectionLevel: "M",
+ });
+ } else {
+ // 鐢熸垚闃蹭吉鐮侊紙浣跨敤浜岀淮鐮佹妧鏈紝浣嗗唴瀹规牸寮忎笉鍚岋級
+ const securityContent = generateSecurityCode(form.content);
+ generatedCodeUrl.value = await QRCode.toDataURL(securityContent, {
+ width: form.size,
+ margin: form.margin,
+ color: {
+ dark: form.foregroundColor,
+ light: form.backgroundColor,
+ },
+ errorCorrectionLevel: "H", // 闃蹭吉鐮佷娇鐢ㄦ渶楂樼籂閿欑骇鍒�
+ });
+ }
+
+ generateTime.value = new Date().toLocaleString();
+ ElMessage.success("鐢熸垚鎴愬姛锛�");
+ } catch (error) {
+ console.error("鐢熸垚澶辫触:", error);
+ ElMessage.error("鐢熸垚澶辫触锛�" + error.message);
+ } finally {
+ generating.value = false;
+ }
+ };
+
+ // 鐢熸垚闃蹭吉鐮佸唴瀹�
+ const generateSecurityCode = content => {
+ const timestamp = Date.now();
+ const random = Math.random().toString(36).substr(2, 8);
+ return `SEC_${content}_${timestamp}_${random}`;
+ };
+
+ // 涓嬭浇鐢熸垚鐨勭爜
+ const downloadCode = () => {
+ if (!generatedCodeUrl.value) {
+ ElMessage.warning("璇峰厛鐢熸垚鐮�");
+ return;
+ }
+
+ const a = document.createElement("a");
+ a.href = generatedCodeUrl.value;
+ a.download = `${
+ form.type === "qrcode" ? "浜岀淮鐮�" : "闃蹭吉鐮�"
+ }_${new Date().getTime()}.png`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ ElMessage.success("涓嬭浇鎴愬姛锛�");
+ };
+
+ // 澶嶅埗鍐呭鍒板壀璐存澘
+ const copyToClipboard = async () => {
+ try {
+ await navigator.clipboard.writeText(form.content);
+ ElMessage.success("鍐呭宸插鍒跺埌鍓创鏉�");
+ } catch (error) {
+ // 闄嶇骇鏂规
+ const textArea = document.createElement("textarea");
+ textArea.value = form.content;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ ElMessage.success("鍐呭宸插鍒跺埌鍓创鏉�");
+ }
+ };
+
+ // 鎵撳嵃鐮�
+ const printCode = () => {
+ if (!generatedCodeUrl.value) {
+ ElMessage.warning("璇峰厛鐢熸垚鐮�");
+ return;
+ }
+
+ const printWindow = window.open("", "_blank");
+ printWindow.document.write(`
+ <html>
+ <head>
+ <title>鎵撳嵃${form.type === "qrcode" ? "浜岀淮鐮�" : "闃蹭吉鐮�"}</title>
+ <style>
+ body { text-align: center; padding: 20px; }
+ img { max-width: 100%; height: auto; }
+ .info { margin: 20px 0; }
+ </style>
+ </head>
+ <body>
+ <h2>${form.type === "qrcode" ? "浜岀淮鐮�" : "闃蹭吉鐮�"}</h2>
+ <img src="${generatedCodeUrl.value}" alt="${
+ form.type === "qrcode" ? "浜岀淮鐮�" : "闃蹭吉鐮�"
+ }" />
+ <div class="info">
+ <p><strong>鍐呭锛�</strong>${form.content}</p>
+ <p><strong>鐢熸垚鏃堕棿锛�</strong>${generateTime.value}</p>
+ </div>
+ </body>
+ </html>
+ `);
+ printWindow.document.close();
+ printWindow.print();
+ };
+
+ // 閲嶇疆琛ㄥ崟
+ const resetForm = () => {
+ formRef.value.resetFields();
+ generatedCodeUrl.value = "";
+ generateTime.value = "";
+ batchCodes.value = [];
+ };
+
+ // 鎵归噺鐢熸垚
+ const generateBatchCodes = async () => {
+ if (!batchForm.prefix.trim()) {
+ ElMessage.warning("璇疯緭鍏ュ墠缂�");
+ return;
+ }
+
+ batchCodes.value = [];
+ generating.value = true;
+
+ try {
+ for (let i = 0; i < batchForm.quantity; i++) {
+ const number = batchForm.startNumber + i;
+ const content = `${batchForm.prefix}${number
+ .toString()
+ .padStart(6, "0")}`;
+
+ let codeUrl;
+ if (form.type === "qrcode") {
+ codeUrl = await QRCode.toDataURL(content, {
+ width: form.size,
+ margin: form.margin,
+ color: {
+ dark: form.foregroundColor,
+ light: form.backgroundColor,
+ },
+ });
+ } else {
+ const securityContent = generateSecurityCode(content);
+ codeUrl = await QRCode.toDataURL(securityContent, {
+ width: form.size,
+ margin: form.margin,
+ color: {
+ dark: form.foregroundColor,
+ light: form.backgroundColor,
+ },
+ });
+ }
+
+ batchCodes.value.push({
+ content,
+ url: codeUrl,
+ });
+ }
+
+ ElMessage.success(`鎵归噺鐢熸垚瀹屾垚锛屽叡鐢熸垚 ${batchForm.quantity} 涓爜`);
+ batchDialogVisible.value = false;
+ } catch (error) {
+ console.error("鎵归噺鐢熸垚澶辫触:", error);
+ ElMessage.error("鎵归噺鐢熸垚澶辫触锛�" + error.message);
+ } finally {
+ generating.value = false;
+ }
+ };
+
+ // 涓嬭浇鍗曚釜鎵归噺鐢熸垚鐨勭爜
+ const downloadSingleCode = code => {
+ const a = document.createElement("a");
+ a.href = code.url;
+ a.download = `${code.content}.png`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ };
+
+ // 涓嬭浇鎵�鏈夋壒閲忕敓鎴愮殑鐮�
+ const downloadAllCodes = async () => {
+ if (batchCodes.value.length === 0) {
+ ElMessage.warning("娌℃湁鍙笅杞界殑鐮�");
+ return;
+ }
+
+ try {
+ // 浣跨敤JSZip鎵撳寘涓嬭浇
+ const JSZip = await import("jszip");
+ const zip = new JSZip.default();
+
+ batchCodes.value.forEach((code, index) => {
+ // 灏哹ase64杞崲涓篵lob
+ const base64Data = code.url.split(",")[1];
+ const byteCharacters = atob(base64Data);
+ const byteNumbers = new Array(byteCharacters.length);
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
+ }
+ const byteArray = new Uint8Array(byteNumbers);
+
+ zip.file(`${code.content}.png`, byteArray);
+ });
+
+ const content = await zip.generateAsync({ type: "blob" });
+ const a = document.createElement("a");
+ a.href = URL.createObjectURL(content);
+ a.download = `鎵归噺${
+ form.type === "qrcode" ? "浜岀淮鐮�" : "闃蹭吉鐮�"
+ }_${new Date().getTime()}.zip`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(a.href);
+
+ ElMessage.success("鎵归噺涓嬭浇瀹屾垚锛�");
+ } catch (error) {
+ console.error("鎵归噺涓嬭浇澶辫触:", error);
+ ElMessage.error("鎵归噺涓嬭浇澶辫触锛岃閫愪釜涓嬭浇");
+ }
+ };
+
+ // 娓呯┖鎵归噺鐢熸垚缁撴灉
+ const clearBatchCodes = () => {
+ batchCodes.value = [];
+ };
+
+ // 鏆撮湶鏂规硶缁欑埗缁勪欢
+ defineExpose({
+ generateCode,
+ downloadCode,
+ resetForm,
+ form,
+ });
+</script>
+
+<style scoped>
+ .qr-code-generator {
+ padding: 20px;
+ }
+
+ .qr-form {
+ background: #f8f9fa;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ }
+
+ .code-display {
+ margin-top: 30px;
+ }
+
+ .code-container {
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ gap: 40px;
+ margin: 20px 0;
+ }
+
+ .code-image img {
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ .code-info {
+ text-align: left;
+ min-width: 200px;
+ }
+
+ .code-info p {
+ margin: 8px 0;
+ color: #666;
+ }
+
+ .code-actions {
+ text-align: center;
+ margin: 20px 0;
+ }
+
+ .code-actions .el-button {
+ margin: 0 10px;
+ }
+
+ .batch-results {
+ margin-top: 30px;
+ }
+
+ .batch-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+ gap: 20px;
+ margin: 20px 0;
+ }
+
+ .batch-item {
+ text-align: center;
+ padding: 15px;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ background: #fff;
+ }
+
+ .batch-item img {
+ width: 100px;
+ height: 100px;
+ margin-bottom: 10px;
+ }
+
+ .batch-content {
+ font-size: 12px;
+ color: #666;
+ margin: 10px 0;
+ word-break: break-all;
+ }
+
+ .batch-actions {
+ text-align: center;
+ margin: 20px 0;
+ }
+
+ .batch-actions .el-button {
+ margin: 0 10px;
+ }
+
+ @media (max-width: 768px) {
+ .code-container {
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .batch-grid {
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
+ }
+ }
+</style>
diff --git a/src/components/RightToolbar/index.vue b/src/components/RightToolbar/index.vue
new file mode 100644
index 0000000..e5dae99
--- /dev/null
+++ b/src/components/RightToolbar/index.vue
@@ -0,0 +1,157 @@
+<template>
+ <div class="top-right-btn" :style="style">
+ <el-row>
+ <el-tooltip class="item" effect="dark" :content="showSearch ? '闅愯棌鎼滅储' : '鏄剧ず鎼滅储'" placement="top" v-if="search">
+ <el-button circle icon="Search" @click="toggleSearch()" />
+ </el-tooltip>
+ <el-tooltip class="item" effect="dark" content="鍒锋柊" placement="top">
+ <el-button circle icon="Refresh" @click="refresh()" />
+ </el-tooltip>
+ <el-tooltip class="item" effect="dark" content="鏄鹃殣鍒�" placement="top" v-if="columns">
+ <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
+ <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
+ <el-button circle icon="Menu" />
+ <template #dropdown>
+ <el-dropdown-menu>
+ <!-- 鍏ㄩ��/鍙嶉�� 鎸夐挳 -->
+ <el-dropdown-item>
+ <el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 鍒楀睍绀� </el-checkbox>
+ </el-dropdown-item>
+ <div class="check-line"></div>
+ <template v-for="item in columns" :key="item.key">
+ <el-dropdown-item>
+ <el-checkbox v-model="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" />
+ </el-dropdown-item>
+ </template>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </el-tooltip>
+ </el-row>
+ <el-dialog :title="title" v-model="open" append-to-body>
+ <el-transfer
+ :titles="['鏄剧ず', '闅愯棌']"
+ v-model="value"
+ :data="columns"
+ @change="dataChange"
+ ></el-transfer>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+const props = defineProps({
+ /* 鏄惁鏄剧ず妫�绱㈡潯浠� */
+ showSearch: {
+ type: Boolean,
+ default: true
+ },
+ /* 鏄鹃殣鍒椾俊鎭� */
+ columns: {
+ type: Array
+ },
+ /* 鏄惁鏄剧ず妫�绱㈠浘鏍� */
+ search: {
+ type: Boolean,
+ default: true
+ },
+ /* 鏄鹃殣鍒楃被鍨嬶紙transfer绌挎妗嗐�乧heckbox澶嶉�夋锛� */
+ showColumnsType: {
+ type: String,
+ default: "checkbox"
+ },
+ /* 鍙冲杈硅窛 */
+ gutter: {
+ type: Number,
+ default: 10
+ },
+})
+
+const emits = defineEmits(['update:showSearch', 'queryTable'])
+
+// 鏄鹃殣鏁版嵁
+const value = ref([])
+// 寮瑰嚭灞傛爣棰�
+const title = ref("鏄剧ず/闅愯棌")
+// 鏄惁鏄剧ず寮瑰嚭灞�
+const open = ref(false)
+
+const style = computed(() => {
+ const ret = {}
+ if (props.gutter) {
+ ret.marginRight = `${props.gutter / 2}px`
+ }
+ return ret
+})
+
+// 鏄惁鍏ㄩ��/鍗婇�� 鐘舵��
+const isChecked = computed({
+ get: () => props.columns.every(col => col.visible),
+ set: () => {}
+})
+const isIndeterminate = computed(() => props.columns.some((col) => col.visible) && !isChecked.value)
+
+// 鎼滅储
+function toggleSearch() {
+ emits("update:showSearch", !props.showSearch)
+}
+
+// 鍒锋柊
+function refresh() {
+ emits("queryTable")
+}
+
+// 鍙充晶鍒楄〃鍏冪礌鍙樺寲
+function dataChange(data) {
+ for (let item in props.columns) {
+ const key = props.columns[item].key
+ props.columns[item].visible = !data.includes(key)
+ }
+}
+
+// 鎵撳紑鏄鹃殣鍒梔ialog
+function showColumn() {
+ open.value = true
+}
+
+if (props.showColumnsType == 'transfer') {
+ // 鏄鹃殣鍒楀垵濮嬮粯璁ら殣钘忓垪
+ for (let item in props.columns) {
+ if (props.columns[item].visible === false) {
+ value.value.push(parseInt(item))
+ }
+ }
+}
+
+// 鍗曞嬀閫�
+function checkboxChange(event, label) {
+ props.columns.filter(item => item.label == label)[0].visible = event
+}
+
+// 鍒囨崲鍏ㄩ��/鍙嶉��
+function toggleCheckAll() {
+ const newValue = !isChecked.value
+ props.columns.forEach((col) => (col.visible = newValue))
+}
+</script>
+
+<style lang='scss' scoped>
+:deep(.el-transfer__button) {
+ border-radius: 50%;
+ display: block;
+ margin-left: 0px;
+}
+:deep(.el-transfer__button:first-child) {
+ margin-bottom: 10px;
+}
+:deep(.el-dropdown-menu__item) {
+ line-height: 30px;
+ padding: 0 17px;
+}
+.check-line {
+ width: 90%;
+ height: 1px;
+ background-color: #ccc;
+ margin: 3px auto;
+}
+</style>
diff --git a/src/components/RuoYi/Doc/index.vue b/src/components/RuoYi/Doc/index.vue
new file mode 100644
index 0000000..91aebe0
--- /dev/null
+++ b/src/components/RuoYi/Doc/index.vue
@@ -0,0 +1,13 @@
+<template>
+ <div>
+ <svg-icon icon-class="question" @click="goto" />
+ </div>
+</template>
+
+<script setup>
+const url = ref('http://doc.ruoyi.vip/ruoyi-vue')
+
+function goto() {
+ window.open(url.value)
+}
+</script>
\ No newline at end of file
diff --git a/src/components/RuoYi/Git/index.vue b/src/components/RuoYi/Git/index.vue
new file mode 100644
index 0000000..96fdea7
--- /dev/null
+++ b/src/components/RuoYi/Git/index.vue
@@ -0,0 +1,13 @@
+<template>
+ <div>
+ <svg-icon icon-class="github" @click="goto" />
+ </div>
+</template>
+
+<script setup>
+const url = ref('https://gitee.com/y_project/RuoYi-Vue')
+
+function goto() {
+ window.open(url.value)
+}
+</script>
diff --git a/src/components/Screenfull/index.vue b/src/components/Screenfull/index.vue
new file mode 100644
index 0000000..cf23223
--- /dev/null
+++ b/src/components/Screenfull/index.vue
@@ -0,0 +1,22 @@
+<template>
+ <div>
+ <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
+ </div>
+</template>
+
+<script setup>
+import { useFullscreen } from '@vueuse/core'
+
+const { isFullscreen, enter, exit, toggle } = useFullscreen()
+</script>
+
+<style lang='scss' scoped>
+.screenfull-svg {
+ display: inline-block;
+ cursor: pointer;
+ fill: #5a5e66;
+ width: 20px;
+ height: 20px;
+ vertical-align: 10px;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/SearchPanel/index.vue b/src/components/SearchPanel/index.vue
new file mode 100644
index 0000000..a1859d2
--- /dev/null
+++ b/src/components/SearchPanel/index.vue
@@ -0,0 +1,257 @@
+<template>
+ <div class="search-panel-container">
+ <el-form
+ ref="formRef"
+ :model="modelValue"
+ class="search-form"
+ label-width="0"
+ >
+ <el-row :gutter="10" class="form-row">
+ <!-- 娓叉煋琛ㄥ崟椤� -->
+ <el-col
+ v-for="(item, index) in visibleSchema"
+ :key="item.prop || index"
+ :xs="24"
+ :sm="12"
+ :md="8"
+ :lg="4"
+ :xl="4"
+ class="search-col"
+ >
+ <el-form-item :prop="item.prop" :rules="item.rules" class="search-form-item">
+ <!-- 鑷畾涔夋彃妲� -->
+ <slot v-if="item.slot" :name="item.slot" :item="item"></slot>
+ <!-- 榛樿娓叉煋绫诲瀷 -->
+ <template v-else>
+ <!-- 杈撳叆妗� -->
+ <el-input
+ v-if="item.type === 'input'"
+ v-model="modelValue[item.prop]"
+ :placeholder="item.placeholder || '璇疯緭鍏�'"
+ clearable
+ class="full-width"
+ v-bind="item.props"
+ @keyup.enter="handleSearch"
+ />
+
+ <!-- 涓嬫媺妗� -->
+ <el-select
+ v-else-if="item.type === 'select'"
+ v-model="modelValue[item.prop]"
+ :placeholder="item.placeholder || '璇烽�夋嫨'"
+ clearable
+ class="full-width"
+ v-bind="item.props"
+ >
+ {{ item || '璇烽�夋嫨' }}
+ <!-- <el-option
+ v-for="(opt,idx) in getOptions(item)"
+ :key="idx"
+ :label="opt.label"
+ :value="opt.value"
+ /> -->
+ </el-select>
+
+ <!-- 鏃ユ湡閫夋嫨鍣� -->
+ <el-date-picker
+ v-else-if="item.type === 'date'"
+ v-model="modelValue[item.prop]"
+ type="date"
+ :placeholder="item.placeholder || '閫夋嫨鏃ユ湡'"
+ style="width: 100%"
+ value-format="YYYY-MM-DD"
+ class="full-width"
+ v-bind="item.props"
+ />
+
+ <!-- 鏃ユ湡鑼冨洿閫夋嫨鍣� -->
+ <el-date-picker
+ v-else-if="item.type === 'daterange'"
+ v-model="modelValue[item.prop]"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ class="full-width"
+ v-bind="item.props"
+ />
+ </template>
+ </el-form-item>
+ </el-col>
+
+ <!-- 鎸夐挳鍖哄煙 -->
+ <el-col :xs="24" :sm="12" :md="8" :lg="4" :xl="4" class="search-actions-col">
+ <el-form-item class="search-actions">
+ <el-button style="background: #002FA7; color: white;" icon="Search" @click="handleSearch">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 灞曞紑/鏀惰捣鎸夐挳 -->
+ <div v-if="schema.length > 5" class="expand-toggle" @click="toggleExpand">
+ <span>{{ isExpanded ? '鏀惰捣' : '灞曞紑' }}</span>
+ <el-icon :class="{ 'is-reverse': isExpanded }">
+ <ArrowDown />
+ </el-icon>
+ </div>
+ </el-form>
+ </div>
+</template>
+
+<script setup name="SearchPanel">
+import { ref, reactive, computed, getCurrentInstance, onMounted } from 'vue';
+import { ArrowDown, Search, Refresh } from '@element-plus/icons-vue';
+
+const { proxy } = getCurrentInstance();
+
+const props = defineProps({
+ // 琛ㄥ崟鏁版嵁瀵硅薄
+ modelValue: {
+ type: Object,
+ required: true
+ },
+ // 琛ㄥ崟閰嶇疆椤�
+ schema: {
+ type: Array,
+ default: () => []
+ }
+});
+
+const emit = defineEmits(['update:modelValue', 'search', 'reset']);
+
+// 鏄惁灞曞紑
+const isExpanded = ref(false);
+const formRef = ref(null);
+const dictMap = reactive({});
+
+// 璁$畻鍙鐨� schema 椤�
+const visibleSchema = computed(() => {
+ if (isExpanded.value || props.schema.length <= 5) {
+ return props.schema;
+ }
+ return props.schema.slice(0, 5);
+});
+
+// 鍒濆鍖栧瓧鍏告暟鎹�
+onMounted(() => {
+ const dicts = props.schema.filter(item => item.dict).map(item => item.dict);
+ if (dicts.length > 0 && proxy.useDict) {
+ const dictData = proxy.useDict(...dicts);
+ Object.keys(dictData).forEach(key => {
+ dictMap[key] = dictData[key];
+ });
+ }
+});
+
+// 鑾峰彇涓嬫媺閫夐」 (鏀寔闈欐�� options 鍜� 瀛楀吀 dict)
+function getOptions(item) {
+ if (item.options) return item.options;
+ if (item.dict && dictMap[item.dict]) {
+ return dictMap[item.dict].value || [];
+ }
+ return [];
+}
+
+// 鎼滅储
+function handleSearch() {
+ emit('search', props.modelValue);
+}
+
+// 閲嶇疆
+function handleReset() {
+ if (formRef.value) {
+ formRef.value.resetFields();
+ }
+ const keys = props.schema.map(item => item.prop).filter(Boolean);
+ keys.forEach(key => {
+ props.modelValue[key] = undefined;
+ });
+ emit('update:modelValue', props.modelValue);
+ emit('reset');
+}
+
+// 鍒囨崲灞曞紑/鏀惰捣
+function toggleExpand() {
+ isExpanded.value = !isExpanded.value;
+}
+</script>
+
+<style scoped lang="scss">
+.search-panel-container {
+ background: #fff;
+ padding: 15px 15px 5px;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ margin-bottom: 15px;
+
+ .search-form {
+ .form-row {
+ width: 100%;
+ }
+
+ .search-col {
+ margin-bottom: 10px;
+ }
+
+ .search-form-item {
+ margin-right: 0;
+ margin-bottom: 0;
+ width: 100%;
+
+ :deep(.el-form-item__content) {
+ width: 100%;
+ }
+ }
+
+ .full-width {
+ width: 100% !important;
+ }
+
+ .search-actions-col {
+ margin-left: auto;
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+
+ .search-actions {
+ margin-bottom: 0;
+ margin-right: 0;
+
+ :deep(.el-button--primary) {
+ background-color: #409eff;
+ border-color: #409eff;
+ }
+ }
+
+ .expand-toggle {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 4px;
+ font-size: 13px;
+ color: #909399;
+ cursor: pointer;
+ padding: 5px 0;
+ user-select: none;
+ width: 100%;
+ border-top: 1px solid #f0f2f5;
+ margin-top: 5px;
+
+ &:hover {
+ color: #409eff;
+ }
+
+ .el-icon {
+ transition: transform 0.3s;
+ &.is-reverse {
+ transform: rotate(180deg);
+ }
+ }
+ }
+ }
+}
+</style>
+
diff --git a/src/components/SizeSelect/index.vue b/src/components/SizeSelect/index.vue
new file mode 100644
index 0000000..dddb58a
--- /dev/null
+++ b/src/components/SizeSelect/index.vue
@@ -0,0 +1,45 @@
+<template>
+ <div>
+ <el-dropdown trigger="click" @command="handleSetSize">
+ <div class="size-icon--style">
+ <svg-icon class-name="size-icon" icon-class="size" />
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
+ {{ item.label }}
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+</template>
+
+<script setup>
+import useAppStore from "@/store/modules/app"
+
+const appStore = useAppStore()
+const size = computed(() => appStore.size)
+const route = useRoute()
+const router = useRouter()
+const { proxy } = getCurrentInstance()
+const sizeOptions = ref([
+ { label: "杈冨ぇ", value: "large" },
+ { label: "榛樿", value: "default" },
+ { label: "绋嶅皬", value: "small" },
+])
+
+function handleSetSize(size) {
+ proxy.$modal.loading("姝e湪璁剧疆甯冨眬澶у皬锛岃绋嶅��...")
+ appStore.setSize(size)
+ setTimeout("window.location.reload()", 1000)
+}
+</script>
+
+<style lang='scss' scoped>
+.size-icon--style {
+ font-size: 18px;
+ line-height: 50px;
+ padding-right: 7px;
+}
+</style>
\ No newline at end of file
diff --git a/src/components/SvgIcon/index.vue b/src/components/SvgIcon/index.vue
new file mode 100644
index 0000000..2b8368f
--- /dev/null
+++ b/src/components/SvgIcon/index.vue
@@ -0,0 +1,53 @@
+<template>
+ <svg :class="svgClass" aria-hidden="true">
+ <use :xlink:href="iconName" :fill="color" />
+ </svg>
+</template>
+
+<script>
+export default defineComponent({
+ props: {
+ iconClass: {
+ type: String,
+ default: ''
+ },
+ className: {
+ type: String,
+ default: ''
+ },
+ color: {
+ type: String,
+ default: ''
+ },
+ },
+ setup(props) {
+ return {
+ iconName: computed(() => `#icon-${props.iconClass}`),
+ svgClass: computed(() => {
+ if (props.className) {
+ return `svg-icon ${props.className}`
+ }
+ return 'svg-icon'
+ })
+ }
+ }
+})
+</script>
+
+<style scope lang="scss">
+.sub-el-icon,
+.nav-icon {
+ display: inline-block;
+ font-size: 15px;
+ margin-right: 12px;
+ position: relative;
+}
+
+.svg-icon {
+ width: 1em;
+ height: 1em;
+ position: relative;
+ fill: currentColor;
+ vertical-align: -2px;
+}
+</style>
diff --git a/src/components/SvgIcon/svgicon.js b/src/components/SvgIcon/svgicon.js
new file mode 100644
index 0000000..05284e3
--- /dev/null
+++ b/src/components/SvgIcon/svgicon.js
@@ -0,0 +1,10 @@
+import * as components from '@element-plus/icons-vue'
+
+export default {
+ install: (app) => {
+ for (const key in components) {
+ const componentConfig = components[key]
+ app.component(componentConfig.name, componentConfig)
+ }
+ }
+}
diff --git a/src/components/TopNav/index.vue b/src/components/TopNav/index.vue
new file mode 100644
index 0000000..56e2803
--- /dev/null
+++ b/src/components/TopNav/index.vue
@@ -0,0 +1,217 @@
+<template>
+ <el-menu
+ :default-active="activeMenu"
+ mode="horizontal"
+ @select="handleSelect"
+ :ellipsis="false"
+ >
+ <template v-for="(item, index) in topMenus">
+ <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
+ <svg-icon
+ v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
+ :icon-class="item.meta.icon"/>
+ {{ item.meta.title }}
+ </el-menu-item>
+ </template>
+
+ <!-- 椤堕儴鑿滃崟瓒呭嚭鏁伴噺鎶樺彔 -->
+ <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
+ <template #title>鏇村鑿滃崟</template>
+ <template v-for="(item, index) in topMenus">
+ <el-menu-item
+ :index="item.path"
+ :key="index"
+ v-if="index >= visibleNumber">
+ <svg-icon
+ v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
+ :icon-class="item.meta.icon"/>
+ {{ item.meta.title }}
+ </el-menu-item>
+ </template>
+ </el-sub-menu>
+ </el-menu>
+</template>
+
+<script setup>
+import { constantRoutes } from "@/router"
+import { isHttp } from '@/utils/validate'
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+// 椤堕儴鏍忓垵濮嬫暟
+const visibleNumber = ref(null)
+// 褰撳墠婵�娲昏彍鍗曠殑 index
+const currentIndex = ref(null)
+// 闅愯棌渚ц竟鏍忚矾鐢�
+const hideList = ['/index', '/user/profile']
+
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+const route = useRoute()
+const router = useRouter()
+
+// 涓婚棰滆壊
+const theme = computed(() => settingsStore.theme)
+// 鎵�鏈夌殑璺敱淇℃伅
+const routers = computed(() => permissionStore.topbarRouters)
+
+// 椤堕儴鏄剧ず鑿滃崟
+const topMenus = computed(() => {
+ let topMenus = []
+ routers.value.map((menu) => {
+ if (menu.hidden !== true) {
+ // 鍏煎椤堕儴鏍忎竴绾ц彍鍗曞唴閮ㄨ烦杞�
+ if (menu.path === '/' && menu.children) {
+ topMenus.push(menu.children[0])
+ } else {
+ topMenus.push(menu)
+ }
+ }
+ })
+ return topMenus
+})
+
+// 璁剧疆瀛愯矾鐢�
+const childrenMenus = computed(() => {
+ let childrenMenus = []
+ routers.value.map((router) => {
+ for (let item in router.children) {
+ if (router.children[item].parentPath === undefined) {
+ if(router.path === "/") {
+ router.children[item].path = "/" + router.children[item].path
+ } else {
+ if(!isHttp(router.children[item].path)) {
+ router.children[item].path = router.path + "/" + router.children[item].path
+ }
+ }
+ router.children[item].parentPath = router.path
+ }
+ childrenMenus.push(router.children[item])
+ }
+ })
+ return constantRoutes.concat(childrenMenus)
+})
+
+// 榛樿婵�娲荤殑鑿滃崟
+const activeMenu = computed(() => {
+ const path = route.path
+ let activePath = path
+ if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
+ const tmpPath = path.substring(1, path.length)
+ if (!route.meta.link) {
+ activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
+ appStore.toggleSideBarHide(false)
+ }
+ } else if(!route.children) {
+ activePath = path
+ appStore.toggleSideBarHide(true)
+ }
+ activeRoutes(activePath)
+ return activePath
+})
+
+function setVisibleNumber() {
+ const width = document.body.getBoundingClientRect().width / 3
+ visibleNumber.value = parseInt(width / 85)
+}
+
+function handleSelect(key, keyPath) {
+ currentIndex.value = key
+ const route = routers.value.find(item => item.path === key)
+ if (isHttp(key)) {
+ // http(s):// 璺緞鏂扮獥鍙f墦寮�
+ window.open(key, "_blank")
+ } else if (!route || !route.children) {
+ // 娌℃湁瀛愯矾鐢辫矾寰勫唴閮ㄦ墦寮�
+ const routeMenu = childrenMenus.value.find(item => item.path === key)
+ if (routeMenu && routeMenu.query) {
+ let query = JSON.parse(routeMenu.query)
+ router.push({ path: key, query: query })
+ } else {
+ router.push({ path: key })
+ }
+ appStore.toggleSideBarHide(true)
+ } else {
+ // 鏄剧ず宸︿晶鑱斿姩鑿滃崟
+ activeRoutes(key)
+ appStore.toggleSideBarHide(false)
+ }
+}
+
+function activeRoutes(key) {
+ let routes = []
+ if (childrenMenus.value && childrenMenus.value.length > 0) {
+ childrenMenus.value.map((item) => {
+ if (key == item.parentPath || (key == "index" && "" == item.path)) {
+ routes.push(item)
+ }
+ })
+ }
+ if(routes.length > 0) {
+ permissionStore.setSidebarRouters(routes)
+ } else {
+ appStore.toggleSideBarHide(true)
+ }
+ return routes
+}
+
+onMounted(() => {
+ window.addEventListener('resize', setVisibleNumber)
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('resize', setVisibleNumber)
+})
+
+onMounted(() => {
+ setVisibleNumber()
+})
+</script>
+
+<style lang="scss">
+.topmenu-container.el-menu--horizontal > .el-menu-item {
+ float: left;
+ height: 50px !important;
+ line-height: 50px !important;
+ color: #999093 !important;
+ padding: 0 5px !important;
+ margin: 0 10px !important;
+}
+
+.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
+ border-bottom: 2px solid #{'var(--theme)'} !important;
+ color: #303133;
+}
+
+/* sub-menu item */
+.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
+ float: left;
+ height: 50px !important;
+ line-height: 50px !important;
+ color: #999093 !important;
+ padding: 0 5px !important;
+ margin: 0 10px !important;
+}
+
+/* 鑳屾櫙鑹查殣钘� */
+.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
+ background-color: #ffffff;
+}
+
+/* 鍥炬爣鍙抽棿璺� */
+.topmenu-container .svg-icon {
+ margin-right: 4px;
+}
+
+/* topmenu more arrow */
+.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
+ position: static;
+ vertical-align: middle;
+ margin-left: 8px;
+ margin-top: 0px;
+}
+
+
+</style>
diff --git a/src/components/Upload/FileUpload.vue b/src/components/Upload/FileUpload.vue
new file mode 100644
index 0000000..8658621
--- /dev/null
+++ b/src/components/Upload/FileUpload.vue
@@ -0,0 +1,100 @@
+<script setup>
+import { ref } from "vue";
+
+defineOptions({
+ name: "鏂囦欢涓婁紶缁勪欢",
+});
+
+const props = defineProps({
+ downloadTemplate: Function,
+ showTips: Boolean,
+ accept: {
+ type: String,
+ default: ".xls, .xlsx",
+ },
+ headers: Object,
+ action: String,
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ showTip: {
+ type: Boolean,
+ default: true,
+ },
+ autoUpload: {
+ type: Boolean,
+ default: false,
+ },
+ limit: {
+ type: Number,
+ default: 1,
+ },
+});
+const emits = defineEmits(["success", "remove"]);
+
+const uploadRef = ref();
+const fileList = ref([]);
+
+const uploadApi = () => {
+ uploadRef.value.submit();
+};
+
+const handleFileSuccess = (response, file, fileList) => {
+ // uploadRef.value.handleRemove(file);
+ emits("success", response, file, fileList);
+};
+
+const handleRemove = (file) => {
+ emits("remove", file);
+};
+
+const clearFiles = () => {
+ fileList.value = [];
+};
+
+defineExpose({
+ fileList,
+ uploadApi,
+ clearFiles,
+});
+</script>
+
+<template>
+ <el-upload
+ ref="uploadRef"
+ v-model:file-list="fileList"
+ drag
+ multiple
+ :action="action"
+ :accept="accept"
+ :headers="headers"
+ :disabled="disabled"
+ :auto-upload="autoUpload"
+ :limit="limit"
+ :drag="true"
+ :on-success="handleFileSuccess"
+ :on-remove="handleRemove"
+ >
+ <div class="el-upload__text">
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">
+ 灏嗘枃浠舵嫋鍒版澶勶紝鎴�
+ <em>鐐瑰嚮涓婁紶闄勪欢</em>
+ </div>
+ </div>
+ <template #tip>
+ <div v-if="showTip" class="el-upload__tip text-center">
+ 鍙兘涓婁紶xlsx/xls鏂囦欢锛屼笖涓嶈秴杩�10M
+ <el-button
+ type="primary"
+ link
+ class="reset-margin"
+ @click="props.downloadTemplate()"
+ >
+ <span style="font-size: 12px; font-weight: normal">涓嬭浇妯℃澘</span>
+ </el-button>
+ </div>
+ </template>
+ </el-upload>
+</template>
diff --git a/src/components/Upload/index.js b/src/components/Upload/index.js
new file mode 100644
index 0000000..0ea60da
--- /dev/null
+++ b/src/components/Upload/index.js
@@ -0,0 +1 @@
+export { default as FileUpload } from "./FileUpload.vue";
diff --git a/src/components/filePreview/index.vue b/src/components/filePreview/index.vue
new file mode 100644
index 0000000..d8720c3
--- /dev/null
+++ b/src/components/filePreview/index.vue
@@ -0,0 +1,202 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="棰勮" width="100%" fullscreen align-center :before-close="handleClose" append-to-body>
+ <div>
+ <!-- 鍥剧墖棰勮 -->
+ <div v-if="isImage">
+ <img :src="imgUrl" alt="Image Preview" />
+ </div>
+
+ <!-- PDF棰勮鎻愮ず -->
+ <div v-if="isPdf" style="height: 100vh; display: flex; align-items: center; justify-content: center;">
+ <p>姝e湪鍑嗗PDF棰勮...</p>
+ </div>
+
+ <!-- Word鏂囨。棰勮 -->
+ <div v-if="isDoc">
+ <p v-if="!isDocShow">鏂囨。鏃犳硶鐩存帴棰勮锛岃涓嬭浇鏌ョ湅銆�</p>
+ <a :href="fileUrl" v-if="!isDocShow">涓嬭浇鏂囦欢</a>
+ <vue-office-docx
+ v-else
+ :src="fileUrl"
+ style="height: 100vh;"
+ @rendered="renderedHandler"
+ @error="errorHandler"
+ />
+ </div>
+
+ <!-- Excel鏂囨。棰勮 -->
+ <div v-if="isXls">
+ <p v-if="!isDocShow">鏂囨。鏃犳硶鐩存帴棰勮锛岃涓嬭浇鏌ョ湅銆�</p>
+ <a :href="fileUrl" v-if="!isDocShow">涓嬭浇鏂囦欢</a>
+ <vue-office-excel
+ v-else
+ :src="fileUrl"
+ :options="options"
+ style="height: 100vh;"
+ @rendered="renderedHandler"
+ @error="errorHandler"
+ />
+ </div>
+
+ <!-- 鍘嬬缉鏂囦欢澶勭悊 -->
+ <div v-if="isZipOrRar">
+ <p>鍘嬬缉鏂囦欢鏃犳硶鐩存帴棰勮锛岃涓嬭浇鏌ョ湅銆�</p>
+ <a :href="fileUrl">涓嬭浇鏂囦欢</a>
+ </div>
+
+ <!-- 涓嶆敮鎸佺殑鏍煎紡 -->
+ <div v-if="!isSupported">
+ <p>涓嶆敮鎸佺殑鏂囦欢鏍煎紡</p>
+ </div>
+ </div>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance, watch } from 'vue';
+import VueOfficeDocx from '@vue-office/docx';
+import '@vue-office/docx/lib/index.css';
+import VueOfficeExcel from '@vue-office/excel';
+import '@vue-office/excel/lib/index.css';
+
+// 鍝嶅簲寮忓彉閲�
+const fileUrl = ref('')
+const dialogVisible = ref(false)
+const { proxy } = getCurrentInstance();
+const javaApi = proxy.javaApi;
+
+// 鏂囨。棰勮鐘舵��
+const isDocShow = ref(true);
+const imgUrl = ref('');
+const options = ref({
+ xls: false,
+ minColLength: 0,
+ minRowLength: 0,
+ widthOffset: 10,
+ heightOffset: 10,
+ beforeTransformData: (workbookData) => workbookData,
+ transformData: (workbookData) => workbookData,
+});
+
+// 璁$畻灞炴�� - 鍒ゆ柇鏂囦欢绫诲瀷锛堟敮鎸乁RL甯︽煡璇㈠弬鏁帮級
+const isImage = computed(() => {
+ const state = /\.(jpg|jpeg|png|gif)(\?.*)?$/i.test(fileUrl.value);
+ if (state) {
+ imgUrl.value = fileUrl.value.replaceAll('word', 'img');
+ }
+ return state;
+});
+
+const isPdf = computed(() => {
+ console.log(fileUrl.value)
+ return /\.pdf(\?.*)?$/i.test(fileUrl.value);
+});
+
+const isDoc = computed(() => {
+ return /\.(doc|docx)(\?.*)?$/i.test(fileUrl.value);
+});
+
+const isXls = computed(() => {
+ const state = /\.(xls|xlsx)(\?.*)?$/i.test(fileUrl.value);
+ if (state) {
+ options.value.xls = /\.(xls)(\?.*)?$/i.test(fileUrl.value);
+ }
+ return state;
+});
+
+const isZipOrRar = computed(() => {
+ return /\.(zip|rar)(\?.*)?$/i.test(fileUrl.value);
+});
+
+const isSupported = computed(() => {
+ return isImage.value || isPdf.value || isDoc.value || isXls.value || isZipOrRar.value;
+});
+
+// 鍔ㄦ�佸垱寤篴鏍囩骞惰烦杞瑙圥DF
+const previewPdf = (url) => {
+ // 鍒涘缓a鏍囩
+ const link = document.createElement('a');
+ // 璁剧疆PDF鏂囦欢URL
+ link.href = url;
+ // 鍦ㄦ柊鏍囩椤垫墦寮�
+ link.target = '_blank';
+ // 瀹夊叏灞炴�э紝闃叉鏂伴〉闈㈣闂師椤甸潰
+ link.rel = 'noopener noreferrer';
+ // 鍙�夛細璁剧疆閾炬帴鏂囨湰
+ link.textContent = '棰勮PDF';
+ // 灏哸鏍囩娣诲姞鍒伴〉闈紙閮ㄥ垎娴忚鍣ㄨ姹傚繀椤诲湪DOM涓級
+ document.body.appendChild(link);
+ // 瑙﹀彂鐐瑰嚮浜嬩欢
+ link.click();
+ // 绉婚櫎a鏍囩锛屾竻鐞咲OM
+ document.body.removeChild(link);
+};
+
+
+// 鐩戝惉PDF鐘舵�佸彉鍖栵紝鑷姩瑙﹀彂璺宠浆
+watch(
+ () => isPdf.value,
+ (newVal) => {
+
+ // 褰撶‘璁ゆ槸PDF涓旀枃浠禪RL鏈夋晥鏃�
+ if (newVal && fileUrl.value) {
+ // 鍏抽棴瀵硅瘽妗�
+ dialogVisible.value = false;
+ // 鍔犱釜灏忓欢杩熺‘淇濈姸鎬佹洿鏂板畬鎴�
+ setTimeout(() => {
+ previewPdf(fileUrl.value);
+ fileUrl.value = '';
+ }, 100);
+ }
+ }
+);
+
+// 鏂规硶瀹氫箟
+const renderedHandler = () => {
+ console.log("娓叉煋瀹屾垚");
+ isDocShow.value = true;
+ resetStyle();
+};
+
+const errorHandler = () => {
+ console.log("娓叉煋澶辫触");
+ isDocShow.value = false;
+};
+
+const open = (url) => {
+ fileUrl.value = url;
+ dialogVisible.value = true;
+};
+const handleClose = () => {
+ dialogVisible.value = false;
+};
+
+const resetStyle = () => {
+ const elements = document.querySelectorAll('[style*="pt"]');
+ for (const element of elements) {
+ const style = element.getAttribute('style');
+ if (style) {
+ element.setAttribute('style', style.replace(/pt/g, 'px'));
+ }
+ }
+};
+
+// 鏆撮湶open鏂规硶渚涘閮ㄨ皟鐢�
+defineExpose({
+ open
+})
+</script>
+
+<style scoped>
+img {
+ max-width: 100%;
+ display: block;
+ margin: 0 auto;
+}
+
+.oneLine {
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+</style>
diff --git a/src/components/iFrame/index.vue b/src/components/iFrame/index.vue
new file mode 100644
index 0000000..fe98b2d
--- /dev/null
+++ b/src/components/iFrame/index.vue
@@ -0,0 +1,31 @@
+<template>
+ <div v-loading="loading" :style="'height:' + height">
+ <iframe
+ :src="url"
+ frameborder="no"
+ style="width: 100%; height: 100%"
+ scrolling="auto" />
+ </div>
+</template>
+
+<script setup>
+const props = defineProps({
+ src: {
+ type: String,
+ required: true
+ }
+})
+
+const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
+const loading = ref(true)
+const url = computed(() => props.src)
+
+onMounted(() => {
+ setTimeout(() => {
+ loading.value = false
+ }, 300)
+ window.onresize = function temp() {
+ height.value = document.documentElement.clientHeight - 94.5 + "px;"
+ }
+})
+</script>
diff --git a/src/directive/common/copyText.js b/src/directive/common/copyText.js
new file mode 100644
index 0000000..169c068
--- /dev/null
+++ b/src/directive/common/copyText.js
@@ -0,0 +1,66 @@
+/**
+* v-copyText 澶嶅埗鏂囨湰鍐呭
+* Copyright (c) 2022 ruoyi
+*/
+
+export default {
+ beforeMount(el, { value, arg }) {
+ if (arg === "callback") {
+ el.$copyCallback = value
+ } else {
+ el.$copyValue = value
+ const handler = () => {
+ copyTextToClipboard(el.$copyValue)
+ if (el.$copyCallback) {
+ el.$copyCallback(el.$copyValue)
+ }
+ }
+ el.addEventListener("click", handler)
+ el.$destroyCopy = () => el.removeEventListener("click", handler)
+ }
+ }
+}
+
+function copyTextToClipboard(input, { target = document.body } = {}) {
+ const element = document.createElement('textarea')
+ const previouslyFocusedElement = document.activeElement
+
+ element.value = input
+
+ // Prevent keyboard from showing on mobile
+ element.setAttribute('readonly', '')
+
+ element.style.contain = 'strict'
+ element.style.position = 'absolute'
+ element.style.left = '-9999px'
+ element.style.fontSize = '12pt' // Prevent zooming on iOS
+
+ const selection = document.getSelection()
+ const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0)
+
+ target.append(element)
+ element.select()
+
+ // Explicit selection workaround for iOS
+ element.selectionStart = 0
+ element.selectionEnd = input.length
+
+ let isSuccess = false
+ try {
+ isSuccess = document.execCommand('copy')
+ } catch { }
+
+ element.remove()
+
+ if (originalRange) {
+ selection.removeAllRanges()
+ selection.addRange(originalRange)
+ }
+
+ // Get the focus back on the previously focused element, if any
+ if (previouslyFocusedElement) {
+ previouslyFocusedElement.focus()
+ }
+
+ return isSuccess
+}
diff --git a/src/directive/index.js b/src/directive/index.js
new file mode 100644
index 0000000..a86e00b
--- /dev/null
+++ b/src/directive/index.js
@@ -0,0 +1,9 @@
+import hasRole from './permission/hasRole'
+import hasPermi from './permission/hasPermi'
+import copyText from './common/copyText'
+
+export default function directive(app){
+ app.directive('hasRole', hasRole)
+ app.directive('hasPermi', hasPermi)
+ app.directive('copyText', copyText)
+}
\ No newline at end of file
diff --git a/src/directive/permission/hasPermi.js b/src/directive/permission/hasPermi.js
new file mode 100644
index 0000000..e381ea9
--- /dev/null
+++ b/src/directive/permission/hasPermi.js
@@ -0,0 +1,28 @@
+ /**
+ * v-hasPermi 鎿嶄綔鏉冮檺澶勭悊
+ * Copyright (c) 2019 ruoyi
+ */
+
+import useUserStore from '@/store/modules/user'
+
+export default {
+ mounted(el, binding, vnode) {
+ const { value } = binding
+ const all_permission = "*:*:*"
+ const permissions = useUserStore().permissions
+
+ if (value && value instanceof Array && value.length > 0) {
+ const permissionFlag = value
+
+ const hasPermissions = permissions.some(permission => {
+ return all_permission === permission || permissionFlag.includes(permission)
+ })
+
+ if (!hasPermissions) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(`璇疯缃搷浣滄潈闄愭爣绛惧�糮)
+ }
+ }
+}
diff --git a/src/directive/permission/hasRole.js b/src/directive/permission/hasRole.js
new file mode 100644
index 0000000..2f1bb90
--- /dev/null
+++ b/src/directive/permission/hasRole.js
@@ -0,0 +1,28 @@
+ /**
+ * v-hasRole 瑙掕壊鏉冮檺澶勭悊
+ * Copyright (c) 2019 ruoyi
+ */
+
+import useUserStore from '@/store/modules/user'
+
+export default {
+ mounted(el, binding, vnode) {
+ const { value } = binding
+ const super_admin = "admin"
+ const roles = useUserStore().roles
+
+ if (value && value instanceof Array && value.length > 0) {
+ const roleFlag = value
+
+ const hasRole = roles.some(role => {
+ return super_admin === role || roleFlag.includes(role)
+ })
+
+ if (!hasRole) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(`璇疯缃鑹叉潈闄愭爣绛惧�糮)
+ }
+ }
+}
diff --git a/src/hooks/useChartBackground.js b/src/hooks/useChartBackground.js
new file mode 100644
index 0000000..d69a1fb
--- /dev/null
+++ b/src/hooks/useChartBackground.js
@@ -0,0 +1,133 @@
+import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'
+
+/**
+ * 鍥捐〃鑳屾櫙浣嶇疆璋冩暣 composable
+ * @param {Object} options 閰嶇疆閫夐」
+ * @param {Ref} options.wrapperRef - 鍥捐〃瀹瑰櫒鐨� ref
+ * @param {Ref} options.backgroundRef - 鑳屾櫙鍏冪礌鐨� ref
+ * @param {String} options.left - 鑳屾櫙 left 浣嶇疆锛屽 '25%' 鎴� '50%'锛岄粯璁� '50%'
+ * @param {String} options.top - 鑳屾櫙 top 浣嶇疆锛屽 '50%'锛岄粯璁� '50%'
+ * @param {String} options.offsetX - X 杞村亸绉诲�硷紝濡� '-51.5%' 鎴� '-50%'锛岄粯璁� '-50%'
+ * @param {String} options.offsetY - Y 杞村亸绉诲�硷紝濡� '-39%' 鎴� '-50%'锛岄粯璁� '-50%'
+ * @param {Ref} options.watchData - 鍙�夛紝鐩戝惉鐨勬暟鎹彉鍖栵紝鏁版嵁鍙樺寲鏃堕噸鏂拌皟鏁翠綅缃�
+ * @returns {Function} adjustBackgroundPosition - 鎵嬪姩璋冩暣鑳屾櫙浣嶇疆鐨勬柟娉�
+ */
+export function useChartBackground(options = {}) {
+ const {
+ wrapperRef,
+ backgroundRef,
+ left = '50%',
+ top = '50%',
+ offsetX = '-50%',
+ offsetY = '-50%',
+ watchData = null
+ } = options
+
+ let resizeObserver = null
+ let intersectionObserver = null
+ let retryTimers = []
+
+ const clearRetryTimers = () => {
+ if (!retryTimers.length) return
+ retryTimers.forEach((t) => clearTimeout(t))
+ retryTimers = []
+ }
+
+ // 璋冩暣鑳屾櫙浣嶇疆
+ const adjustBackgroundPosition = () => {
+ nextTick(() => {
+ if (!wrapperRef?.value || !backgroundRef?.value) {
+ return
+ }
+
+ // 鍒濆鍖栭樁娈电粡甯稿嚭鐜帮細瀹瑰櫒灏氭湭鍙/灏哄涓� 0锛堥潪鍏ㄥ睆銆乼ab銆佸姩鐢荤瓑锛�
+ // 杩欑鎯呭喌涓嬪厛涓嶅榻愶紝绛� ResizeObserver / IntersectionObserver 鍐嶈Е鍙�
+ const rect = wrapperRef.value.getBoundingClientRect()
+ if (!rect.width || !rect.height) return
+
+ const background = backgroundRef.value
+
+ // 浣跨敤鐧惧垎姣斿畾浣� + transform 寰皟锛堣繖鏄渶鍙潬鐨勬柟寮忥級
+ background.style.left = left
+ background.style.top = top
+ background.style.transform = `translate(${offsetX}, ${offsetY})`
+ })
+ }
+
+ // 鍒濆鍖栭樁娈靛娆♀�滆ˉ鍋垮榻愨�濓紝瑕嗙洊 Echarts 棣栨娓叉煋/鍔ㄧ敾閫犳垚鐨勫欢杩熷竷灞�
+ const scheduleKickAlign = () => {
+ clearRetryTimers()
+ ;[0, 60, 180, 360, 800].forEach((ms) => {
+ retryTimers.push(
+ setTimeout(() => {
+ adjustBackgroundPosition()
+ }, ms)
+ )
+ })
+ }
+
+ // 绐楀彛 resize 澶勭悊
+ const resizeHandler = () => {
+ adjustBackgroundPosition()
+ }
+
+ // 濡傛灉鎻愪緵浜� watchData锛岀洃鍚暟鎹彉鍖栵紙闇�瑕佸湪 setup 闃舵鍒涘缓锛�
+ if (watchData) {
+ watch(watchData, () => {
+ adjustBackgroundPosition()
+ }, { deep: true })
+ }
+
+ // 鍒濆鍖�
+ const init = () => {
+ // 鐩戝惉绐楀彛 resize
+ window.addEventListener('resize', resizeHandler)
+
+ // 浣跨敤 ResizeObserver 鐩戝惉瀹瑰櫒灏哄鍙樺寲
+ nextTick(() => {
+ if (wrapperRef?.value && window.ResizeObserver) {
+ resizeObserver = new ResizeObserver(() => {
+ adjustBackgroundPosition()
+ })
+ resizeObserver.observe(wrapperRef.value)
+ }
+
+ // 鐩戝惉鈥滀粠涓嶅彲瑙佸埌鍙鈥濓紝瑙e喅鍒濆鍖栨椂鏈榻愪絾鐑洿鏂板張姝e父鐨勯棶棰�
+ if (wrapperRef?.value && window.IntersectionObserver) {
+ intersectionObserver = new IntersectionObserver(
+ (entries) => {
+ const entry = entries?.[0]
+ if (entry?.isIntersecting) {
+ scheduleKickAlign()
+ }
+ },
+ { threshold: 0.01 }
+ )
+ intersectionObserver.observe(wrapperRef.value)
+ }
+
+ // 鍒濆鍖栧娆¤ˉ鍋垮榻愶紝纭繚鍥捐〃娓叉煋瀹屾垚
+ scheduleKickAlign()
+ })
+ }
+
+ // 娓呯悊
+ const cleanup = () => {
+ window.removeEventListener('resize', resizeHandler)
+ clearRetryTimers()
+ if (resizeObserver) {
+ resizeObserver.disconnect()
+ resizeObserver = null
+ }
+ if (intersectionObserver) {
+ intersectionObserver.disconnect()
+ intersectionObserver = null
+ }
+ }
+
+ return {
+ adjustBackgroundPosition,
+ init,
+ cleanup
+ }
+}
diff --git a/src/hooks/useFormData.js b/src/hooks/useFormData.js
new file mode 100644
index 0000000..d22204b
--- /dev/null
+++ b/src/hooks/useFormData.js
@@ -0,0 +1,15 @@
+import { reactive } from "vue";
+import { deepClone } from "@/utils/index.js"
+
+export default function useFormData(initData) {
+ const form = reactive(deepClone(initData, true));
+
+ function resetForm() {
+ const initData2 = JSON.parse(JSON.stringify(initData));
+ Object.keys(initData).forEach(key => {
+ form[key] = initData2[key];
+ });
+ }
+
+ return { form, resetForm };
+}
diff --git a/src/hooks/useModal.js b/src/hooks/useModal.js
new file mode 100644
index 0000000..0d443a1
--- /dev/null
+++ b/src/hooks/useModal.js
@@ -0,0 +1,41 @@
+import { ref } from "vue";
+export function useModal(options) {
+ const id = ref();
+ const visible = ref(false);
+ const loading = ref(false);
+ const modalOptions = ref({});
+
+ const openModal = (e) => {
+ id.value = e;
+ modalOptions.value = {
+ title: e ? `淇敼${options.title}` : `鏂板${options.title}`,
+ content: "纭畾鎵ц姝ゆ搷浣滃悧锛�",
+ confirmText: "纭畾",
+ cancelText: "鍙栨秷",
+ };
+ visible.value = true;
+ };
+
+ // 鍏抽棴妯℃�佹
+ const closeModal = () => {
+ visible.value = false;
+ loading.value = false;
+ };
+
+ // 纭鎿嶄綔
+ const handleConfirm = async (callback) => {
+ loading.value = true;
+ callback();
+ closeModal();
+ };
+
+ return {
+ id,
+ visible,
+ loading,
+ modalOptions,
+ openModal,
+ closeModal,
+ handleConfirm,
+ };
+}
diff --git a/src/hooks/usePaginationApi.jsx b/src/hooks/usePaginationApi.jsx
new file mode 100644
index 0000000..f1e8967
--- /dev/null
+++ b/src/hooks/usePaginationApi.jsx
@@ -0,0 +1,145 @@
+import { ref, reactive, watchEffect, unref } from "vue";
+import useFormData from "@/hooks/useFormData";
+import { deepClone, isEqual } from "@/utils/index.js"
+import { ElMessage } from 'element-plus'
+
+/**
+ * 鍒嗛〉api
+ * @param api 鎺ュ彛
+ * @param initalFilters 鍒濆鍖栫瓫閫夋潯浠�
+ * @param sorters
+ * @param filterTransformer
+ */
+export function usePaginationApi(
+ api,
+ initalFilters,
+ columns,
+ sorters,
+ filterTransformer,
+ cb
+) {
+ const dataList = ref([]);
+ const { form: filters, resetForm } = useFormData(initalFilters);
+ let lastFilters = deepClone(initalFilters);
+ const sorter = reactive(sorters || {});
+ const others = ref({});
+ const loading = ref(true);
+ const paginationAlign = ref("right");
+
+ /** 鍒嗛〉閰嶇疆 */
+ const pagination = reactive({
+ pageSize: 100,
+ currentPage: 1,
+ pageSizes: [10, 15, 20],
+ total: 0,
+ align: "right",
+ background: true
+ });
+
+ /** 鍔犺浇鍔ㄧ敾閰嶇疆 */
+ const loadingConfig = reactive({
+ text: "姝e湪鍔犺浇绗竴椤�...",
+ viewBox: "-10, -10, 50, 50",
+ spinner: `
+ <path class="path" d="
+ M 30 15
+ L 28 17
+ M 25.61 25.61
+ A 15 15, 0, 0, 1, 15 30
+ A 15 15, 0, 1, 1, 27.99 7.5
+ L 15 15
+ " style="stroke-width: 4px; fill: rgba(0, 0, 0, 0)"/>
+ `
+ // svg: "",
+ // background: rgba()
+ });
+
+ function getFinalParams() {
+ const finalFilters = {};
+ const beforeParams = unref(filters);
+ if (filterTransformer) {
+ Object.keys(beforeParams).forEach(key => {
+ if (filterTransformer[key]) {
+ Object.assign(
+ finalFilters,
+ filterTransformer[key](beforeParams[key], beforeParams)
+ );
+ } else {
+ finalFilters[key] = beforeParams[key];
+ }
+ });
+ }
+
+ return filterTransformer
+ ? { ...finalFilters, ...sorter }
+ : { ...beforeParams, ...sorter };
+ }
+
+ async function getTableData() {
+ // 濡傛灉杩欐鍜屼笂娆$殑filter涓嶅悓锛岄偅涔堝氨閲嶇疆椤电爜
+ if (!isEqual(unref(filters), lastFilters)) {
+ pagination.currentPage = 1;
+ lastFilters = deepClone(unref(filters));
+ }
+ loading.value = true;
+ api({
+ ...getFinalParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize
+ }).then(({ code, data, msg, ...rest }) => {
+ if (code == 200) {
+ // pagination.currentPage = meta.current_page;
+ // pagination.pageSize = meta.per_page;
+ pagination.total = data.total;
+ others.value = rest;
+ dataList.value = data.records;
+ cb && cb(data);
+ loading.value = false;
+ } else {
+ loading.value = false;
+ ElMessage({ message: msg, type: "error" });
+ }
+ });
+ }
+
+ function onSizeChange(val) {
+ pagination.pageSize = val;
+ pagination.currentPage = 1;
+ getTableData();
+ }
+
+ function onCurrentChange(val) {
+ loadingConfig.text = `姝e湪鍔犺浇绗�${val}椤�...`;
+ loading.value = true;
+ getTableData();
+ }
+ function resetFilters() {
+ resetForm();
+ pagination.currentPage = 1;
+ getTableData();
+ }
+
+ watchEffect(() => {
+ pagination.align = paginationAlign.value
+ });
+
+ // onMounted(() => {
+ // getTableData();
+ // });
+
+ return {
+ loading,
+ columns,
+ dataList,
+ pagination,
+ loadingConfig,
+ paginationAlign,
+ filters,
+ sorter,
+ others,
+ onSizeChange,
+ onCurrentChange,
+ getTableData,
+ resetFilters
+ };
+}
diff --git a/src/layout/components/AppMain.vue b/src/layout/components/AppMain.vue
new file mode 100644
index 0000000..a511014
--- /dev/null
+++ b/src/layout/components/AppMain.vue
@@ -0,0 +1,92 @@
+<template>
+ <section class="app-main">
+ <router-view v-slot="{ Component, route }">
+ <transition name="fade-transform" mode="out-in">
+ <div v-if="!route.meta.link" class="route-view-wrapper">
+ <keep-alive :include="tagsViewStore.cachedViews">
+ <component :is="Component" :key="route.path"/>
+ </keep-alive>
+ </div>
+ <div v-else class="route-view-wrapper"></div>
+ </transition>
+ </router-view>
+ <iframe-toggle />
+ </section>
+</template>
+
+<script setup>
+import iframeToggle from "./IframeToggle/index"
+import useTagsViewStore from '@/store/modules/tagsView'
+
+const route = useRoute()
+const tagsViewStore = useTagsViewStore()
+
+onMounted(() => {
+ addIframe()
+})
+
+watchEffect(() => {
+ addIframe()
+})
+
+function addIframe() {
+ if (route.meta.link) {
+ useTagsViewStore().addIframeView(route)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.app-main {
+ min-height: calc(100vh - var(--topbar-height));
+ width: 100%;
+ position: relative;
+ overflow: visible;
+ background: transparent;
+}
+
+.route-view-wrapper {
+ width: 100%;
+ height: 100%;
+ padding: var(--content-gap);
+ padding-top: 0;
+}
+
+.fixed-header + .app-main {
+ padding-top: 0;
+}
+
+.hasTagsView {
+ .app-main {
+ min-height: calc(100vh - var(--topbar-height) - var(--tagsbar-height));
+ }
+
+ .fixed-header + .app-main {
+ padding-top: 0;
+ }
+}
+</style>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+ .fixed-header {
+ padding-right: 6px;
+ }
+}
+
+::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+::-webkit-scrollbar-track {
+ background-color: rgba(218, 225, 220, 0.8);
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: #b2bdb5;
+ border-radius: 3px;
+}
+</style>
+
diff --git a/src/layout/components/IframeToggle/index.vue b/src/layout/components/IframeToggle/index.vue
new file mode 100644
index 0000000..992b59d
--- /dev/null
+++ b/src/layout/components/IframeToggle/index.vue
@@ -0,0 +1,25 @@
+<template>
+ <inner-link
+ v-for="(item, index) in tagsViewStore.iframeViews"
+ :key="item.path"
+ :iframeId="'iframe' + index"
+ v-show="route.path === item.path"
+ :src="iframeUrl(item.meta.link, item.query)"
+ ></inner-link>
+</template>
+
+<script setup>
+import InnerLink from "../InnerLink/index"
+import useTagsViewStore from "@/store/modules/tagsView"
+
+const route = useRoute()
+const tagsViewStore = useTagsViewStore()
+
+function iframeUrl(url, query) {
+ if (Object.keys(query).length > 0) {
+ let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&")
+ return url + "?" + params
+ }
+ return url
+}
+</script>
diff --git a/src/layout/components/InnerLink/index.vue b/src/layout/components/InnerLink/index.vue
new file mode 100644
index 0000000..2634830
--- /dev/null
+++ b/src/layout/components/InnerLink/index.vue
@@ -0,0 +1,35 @@
+<template>
+ <div :style="'height:' + height" v-loading="loading" element-loading-text="姝e湪鍔犺浇椤甸潰锛岃绋嶅�欙紒">
+ <iframe
+ :id="iframeId"
+ style="width: 100%; height: 100%"
+ :src="src"
+ ref="iframeRef"
+ frameborder="no"
+ ></iframe>
+ </div>
+</template>
+
+<script setup>
+const props = defineProps({
+ src: {
+ type: String,
+ default: "/"
+ },
+ iframeId: {
+ type: String
+ }
+})
+
+const loading = ref(true)
+const height = ref(document.documentElement.clientHeight - 94.5 + 'px')
+const iframeRef = ref(null)
+
+onMounted(() => {
+ if (iframeRef.value) {
+ iframeRef.value.onload = () => {
+ loading.value = false
+ }
+ }
+})
+</script>
diff --git a/src/layout/components/Navbar.vue b/src/layout/components/Navbar.vue
new file mode 100644
index 0000000..aae5330
--- /dev/null
+++ b/src/layout/components/Navbar.vue
@@ -0,0 +1,382 @@
+<template>
+ <div class="navbar">
+ <div class="left-zone">
+ <hamburger
+ id="hamburger-container"
+ :is-active="appStore.sidebar.opened"
+ class="hamburger-container"
+ @toggleClick="toggleSideBar"
+ />
+ <breadcrumb
+ v-if="!settingsStore.topNav"
+ id="breadcrumb-container"
+ class="breadcrumb-container"
+ />
+ </div>
+
+ <div class="center-zone">
+ <el-icon class="search-icon" @click="openHeaderSearch"><Search /></el-icon>
+ <el-input
+ v-model="topSearchKeyword"
+ placeholder="鎼滅储鑿滃崟 / 鍔熻兘 / 鏁版嵁"
+ clearable
+ @keyup.enter="openHeaderSearch"
+ />
+ <header-search
+ ref="headerSearchRef"
+ :keyword="topSearchKeyword"
+ class="search-popup-trigger"
+ />
+ </div>
+
+ <div class="right-menu">
+ <el-popover
+ v-model:visible="notificationVisible"
+ :width="500"
+ placement="bottom-end"
+ trigger="click"
+ :popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 10] } }] }"
+ popper-class="notification-popover"
+ >
+ <template #reference>
+ <div class="notification-container right-menu-item hover-effect">
+ <el-badge :value="unreadCount" :hidden="unreadCount === 0" class="notification-badge">
+ <el-icon :size="18">
+ <Bell />
+ </el-icon>
+ </el-badge>
+ </div>
+ </template>
+ <NotificationCenter @unreadCountChange="handleUnreadCountChange" ref="notificationCenterRef" />
+ </el-popover>
+
+ <div class="right-menu-item hover-effect screenfull-container">
+ <screenfull />
+ </div>
+
+ <div class="avatar-container">
+ <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
+ <div class="avatar-wrapper">
+ <div class="user-summary">
+ <div class="user-name">{{ userStore.nickName || userStore.name || "绠$悊鍛�" }}</div>
+ <div class="user-role">{{ userStore.roleName || "绯荤粺鐢ㄦ埛" }}</div>
+ </div>
+ <img :src="userStore.avatar" class="user-avatar" />
+ <el-icon><caret-bottom /></el-icon>
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <router-link to="/user/profile">
+ <el-dropdown-item>涓汉涓績</el-dropdown-item>
+ </router-link>
+ <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
+ <span>甯冨眬璁剧疆</span>
+ </el-dropdown-item>
+ <el-dropdown-item divided command="logout">
+ <span>閫�鍑虹櫥褰�</span>
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ElMessageBox } from "element-plus";
+import { Bell, Search } from "@element-plus/icons-vue";
+import Breadcrumb from "@/components/Breadcrumb";
+import Hamburger from "@/components/Hamburger";
+import Screenfull from "@/components/Screenfull";
+import HeaderSearch from "@/components/HeaderSearch";
+import NotificationCenter from "./NotificationCenter/index.vue";
+import useAppStore from "@/store/modules/app";
+import useUserStore from "@/store/modules/user";
+import useSettingsStore from "@/store/modules/settings";
+
+const appStore = useAppStore();
+const userStore = useUserStore();
+const settingsStore = useSettingsStore();
+
+const topSearchKeyword = ref("");
+const headerSearchRef = ref(null);
+const notificationVisible = ref(false);
+const notificationCenterRef = ref(null);
+const unreadCount = ref(0);
+
+function toggleSideBar() {
+ appStore.toggleSideBar();
+}
+
+function openHeaderSearch() {
+ headerSearchRef.value?.open(topSearchKeyword.value);
+}
+
+function handleCommand(command) {
+ switch (command) {
+ case "setLayout":
+ setLayout();
+ break;
+ case "logout":
+ logout();
+ break;
+ default:
+ break;
+ }
+}
+
+function logout() {
+ ElMessageBox.confirm("纭畾娉ㄩ攢骞堕��鍑虹郴缁熷悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ userStore.logOut().then(() => {
+ location.href = "/index";
+ });
+ })
+ .catch(() => {});
+}
+
+const emits = defineEmits(["setLayout"]);
+function setLayout() {
+ emits("setLayout");
+}
+
+function handleUnreadCountChange(count) {
+ unreadCount.value = count;
+}
+
+let unreadCountTimer = null;
+onMounted(() => {
+ nextTick(() => {
+ if (notificationCenterRef.value) {
+ notificationCenterRef.value.loadUnreadCount();
+ }
+ });
+
+ unreadCountTimer = setInterval(() => {
+ if (notificationCenterRef.value) {
+ notificationCenterRef.value.loadUnreadCount();
+ }
+ }, 30000);
+});
+
+watch(notificationVisible, (val) => {
+ if (val && notificationCenterRef.value) {
+ nextTick(() => {
+ notificationCenterRef.value.loadMessages();
+ });
+ }
+});
+
+onUnmounted(() => {
+ if (unreadCountTimer) {
+ clearInterval(unreadCountTimer);
+ }
+});
+</script>
+
+<style lang="scss" scoped>
+.navbar {
+ height: var(--topbar-height);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+ background: rgba(255, 255, 255, 0.86);
+ border: 1px solid rgba(148, 163, 184, 0.18);
+ border-radius: var(--content-radius);
+ backdrop-filter: blur(16px);
+ box-shadow: 0 8px 20px rgba(15, 23, 42, 0.05);
+ padding: 0 18px;
+}
+
+.left-zone {
+ flex: 0 1 420px;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.hamburger-container {
+ line-height: 36px;
+ height: 36px;
+ width: 36px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--navbar-text);
+
+ &:hover {
+ background: var(--navbar-hover);
+ }
+}
+
+.breadcrumb-container {
+ min-width: 0;
+}
+
+.center-zone {
+ width: clamp(360px, 34vw, 560px);
+ min-width: 320px;
+ height: 38px;
+ border-radius: 999px;
+ border: 1px solid rgba(148, 163, 184, 0.24);
+ background: rgba(248, 251, 255, 0.92);
+ display: flex;
+ align-items: center;
+ padding: 0 12px;
+ gap: 8px;
+}
+
+.search-icon {
+ color: #5b86c9;
+ cursor: pointer;
+}
+
+.center-zone :deep(.el-input__wrapper) {
+ border: 0;
+ box-shadow: none !important;
+ background: transparent;
+ padding: 0;
+}
+
+.center-zone :deep(.el-input__inner) {
+ color: #334155;
+ font-size: 13px;
+}
+
+.search-popup-trigger :deep(.search-icon) {
+ color: #5b86c9;
+ font-size: 16px;
+ cursor: pointer;
+}
+
+.right-menu {
+ height: 100%;
+ align-items: center;
+ display: flex;
+ gap: 14px;
+}
+
+.right-menu-item {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--navbar-text);
+ border-radius: 8px;
+}
+
+.hover-effect {
+ cursor: pointer;
+ transition: background 0.2s;
+
+ &:hover {
+ background: var(--navbar-hover);
+ }
+}
+
+.notification-container,
+.screenfull-container {
+ width: 36px;
+ height: 36px;
+}
+
+.notification-badge :deep(.el-badge__content) {
+ border: none;
+}
+
+.screenfull-container :deep(.svg-icon) {
+ width: 16px;
+ height: 16px;
+ color: var(--navbar-text);
+}
+
+.avatar-container {
+ height: 100%;
+ display: flex;
+ align-items: center;
+}
+
+.avatar-container :deep(.el-dropdown) {
+ height: 100%;
+ display: flex;
+ align-items: center;
+}
+
+.avatar-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 4px 10px 4px 8px;
+ height: 44px;
+ border-radius: 22px;
+ background: rgba(255, 255, 255, 0.9);
+ border: 1px solid rgba(148, 163, 184, 0.22);
+}
+
+.user-summary {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 2px;
+}
+
+.user-name {
+ color: var(--text-primary);
+ font-size: 13px;
+ line-height: 1;
+}
+
+.user-role {
+ color: var(--text-tertiary);
+ font-size: 11px;
+ line-height: 1;
+}
+
+.user-avatar {
+ cursor: pointer;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: 1px solid rgba(148, 163, 184, 0.3);
+}
+
+@media (max-width: 1200px) {
+ .center-zone {
+ display: none;
+ }
+
+ .user-summary {
+ display: none;
+ }
+}
+</style>
+
+<style lang="scss">
+.notification-popover {
+ padding: 0 !important;
+ border-radius: 16px !important;
+ border: 1px solid rgba(148, 163, 184, 0.22) !important;
+ box-shadow: 0 18px 40px rgba(15, 23, 42, 0.12) !important;
+ background: rgba(255, 255, 255, 0.94) !important;
+ backdrop-filter: blur(16px);
+
+ .el-popover__title {
+ display: none;
+ }
+
+ .el-popover__body {
+ padding: 0 !important;
+ }
+}
+
+.el-badge__content.is-fixed {
+ top: 8px;
+}
+</style>
diff --git a/src/layout/components/NotificationCenter/index.vue b/src/layout/components/NotificationCenter/index.vue
new file mode 100644
index 0000000..66f489a
--- /dev/null
+++ b/src/layout/components/NotificationCenter/index.vue
@@ -0,0 +1,386 @@
+<template>
+ <div class="notification-popover-content">
+ <div class="popover-header">
+ <span class="popover-title">娑堟伅閫氱煡</span>
+ <el-button type="primary"
+ size="small"
+ @click="handleMarkAllAsRead"
+ :disabled="unreadCount === 0">
+ 涓�閿凡璇�
+ </el-button>
+ </div>
+ <div class="notification-content">
+ <el-tabs v-model="activeTab"
+ @tab-change="handleTabChange">
+ <el-tab-pane :label="`鏈(${unreadCount})`"
+ name="unread">
+ <div v-if="unreadList.length === 0"
+ class="empty-state">
+ <el-empty description="鏆傛棤鏈娑堟伅" />
+ </div>
+ <div v-else
+ class="notification-list">
+ <div v-for="item in unreadList"
+ :key="item.id"
+ class="notification-item">
+ <div class="notification-icon">
+ <el-icon :size="24"
+ color="#67C23A">
+ <Bell />
+ </el-icon>
+ </div>
+ <div class="notification-content-wrapper">
+ <div class="notification-title">{{ item.noticeTitle }}</div>
+ <div class="notification-detail">{{ item.noticeContent }}</div>
+ <div class="notification-time">{{ item.createTime }}</div>
+ </div>
+ <div class="notification-action">
+ <el-button type="primary"
+ size="small"
+ @click="handleConfirm(item)">
+ 纭
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="宸茶"
+ name="read">
+ <div v-if="readList.length === 0"
+ class="empty-state">
+ <el-empty description="鏆傛棤宸茶娑堟伅" />
+ </div>
+ <div v-else
+ class="notification-list">
+ <div v-for="item in readList"
+ :key="item.id"
+ class="notification-item read">
+ <div class="notification-icon">
+ <el-icon :size="24"
+ color="#909399">
+ <Bell />
+ </el-icon>
+ </div>
+ <div class="notification-content-wrapper">
+ <div class="notification-title">{{ item.noticeTitle }}</div>
+ <div class="notification-detail">{{ item.noticeContent }}</div>
+ <div class="notification-time">{{ item.createTime }}</div>
+ </div>
+ </div>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ <!-- 鍒嗛〉 -->
+ <div class="pagination-wrapper"
+ v-if="total > 0">
+ <el-pagination v-model:current-page="pageNum"
+ v-model:page-size="pageSize"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="total"
+ layout="prev, pager, next, sizes"
+ @size-change="handleSizeChange"
+ @current-change="handlePageChange" />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { Bell } from "@element-plus/icons-vue";
+ import {
+ listMessage,
+ markAsRead,
+ markAllAsRead,
+ confirmMessage,
+ getUnreadCount,
+ } from "@/api/system/message";
+ import { ElMessage } from "element-plus";
+ import useUserStore from "@/store/modules/user";
+ import { useRouter } from "vue-router";
+
+ const userStore = useUserStore();
+ const router = useRouter();
+ const emit = defineEmits(["unreadCountChange"]);
+
+ const activeTab = ref("unread");
+ const unreadList = ref([]);
+ const readList = ref([]);
+ const unreadCount = ref(0);
+ const total = ref(0);
+ const pageNum = ref(1);
+ const pageSize = ref(10);
+
+ // 鍔犺浇娑堟伅鍒楄〃
+ const loadMessages = async () => {
+ try {
+ const consigneeId = userStore.id;
+ if (!consigneeId) {
+ console.warn("鏈幏鍙栧埌褰撳墠鐧诲綍鐢ㄦ埛ID");
+ return;
+ }
+ const params = {
+ consigneeId: consigneeId,
+ current: pageNum.value,
+ size: pageSize.value,
+ status: activeTab.value === "read" ? 1 : 0,
+ };
+ const res = await listMessage(params);
+ if (res.code === 200) {
+ if (activeTab.value === "unread") {
+ unreadList.value = res.data.records || [];
+ } else {
+ readList.value = res.data.records || [];
+ }
+ total.value = res.data.total || 0;
+ }
+ } catch (error) {
+ console.error("鍔犺浇娑堟伅鍒楄〃澶辫触:", error);
+ }
+ };
+
+ // 鍔犺浇鏈鏁伴噺
+ const loadUnreadCount = async () => {
+ try {
+ const consigneeId = userStore.id;
+ if (!consigneeId) {
+ console.warn("鏈幏鍙栧埌褰撳墠鐧诲綍鐢ㄦ埛ID");
+ return;
+ }
+ const res = await getUnreadCount(consigneeId);
+ if (res.code === 200) {
+ unreadCount.value = res.data || 0;
+ emit("unreadCountChange", unreadCount.value);
+ }
+ } catch (error) {
+ console.error("鍔犺浇鏈鏁伴噺澶辫触:", error);
+ }
+ };
+
+ // 鏍囩椤靛垏鎹�
+ const handleTabChange = tab => {
+ pageNum.value = 1;
+ loadMessages();
+ };
+
+ // 纭娑堟伅
+ const handleConfirm = async item => {
+ try {
+ console.log("item", item);
+ const res = await confirmMessage(item.noticeId, 1);
+ if (res.code === 200) {
+ ElMessage.success("纭鎴愬姛");
+ // 閲嶆柊鍔犺浇鏁版嵁
+ loadMessages();
+ loadUnreadCount();
+
+ // 鏍规嵁 jumpPath 杩涜椤甸潰璺宠浆
+ if (item.jumpPath) {
+ try {
+ // 瑙f瀽 jumpPath锛屽垎绂昏矾寰勫拰鏌ヨ鍙傛暟
+ const [path, queryString] = item.jumpPath.split("?");
+ let query = {};
+
+ if (queryString) {
+ // 瑙f瀽鏌ヨ鍙傛暟
+ queryString.split("&").forEach(param => {
+ const [key, value] = param.split("=");
+ if (key && value) {
+ query[key] = decodeURIComponent(value);
+ }
+ });
+ }
+
+ // 璺宠浆鍒版寚瀹氶〉闈�
+ router.push({
+ path: path,
+ query: query,
+ });
+ } catch (error) {
+ console.error("椤甸潰璺宠浆澶辫触:", error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error("纭娑堟伅澶辫触:", error);
+ ElMessage.error("纭澶辫触");
+ }
+ };
+
+ // 涓�閿凡璇�
+ const handleMarkAllAsRead = async () => {
+ try {
+ const res = await markAllAsRead();
+ if (res.code === 200) {
+ ElMessage.success("宸插叏閮ㄦ爣璁颁负宸茶");
+ loadMessages();
+ loadUnreadCount();
+ }
+ } catch (error) {
+ console.error("涓�閿凡璇诲け璐�:", error);
+ ElMessage.error("鎿嶄綔澶辫触");
+ }
+ };
+
+ // 鍒嗛〉澶у皬鏀瑰彉
+ const handleSizeChange = size => {
+ pageSize.value = size;
+ pageNum.value = 1;
+ loadMessages();
+ };
+
+ // 椤电爜鏀瑰彉
+ const handlePageChange = page => {
+ pageNum.value = page;
+ loadMessages();
+ };
+
+ // 缁勪欢鎸傝浇鏃跺姞杞芥湭璇绘暟閲�
+ onMounted(() => {
+ loadUnreadCount();
+ });
+
+ // 鐩戝惉鐖剁粍浠朵紶閫掔殑 visible 鐘舵�侊紙閫氳繃 watch 鍦� Navbar 涓鐞嗭級
+ // 杩欓噷鍙礋璐f暟鎹姞杞斤紝涓嶆帶鍒舵樉绀�
+
+ // 鏆撮湶鏂规硶渚涘閮ㄨ皟鐢�
+ defineExpose({
+ loadUnreadCount,
+ loadMessages,
+ });
+</script>
+
+<style lang="scss" scoped>
+ .notification-popover-content {
+ display: flex;
+ flex-direction: column;
+ width: 500px;
+ padding: 16px;
+ background: rgba(255, 255, 255, 0.92);
+ }
+
+ .popover-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 16px;
+ padding-bottom: 12px;
+ border-bottom: 1px solid var(--surface-border);
+
+ .popover-title {
+ font-size: 18px;
+ font-weight: 500;
+ color: var(--text-primary);
+ }
+ }
+
+ .notification-content {
+ max-height: 60vh;
+ display: flex;
+ flex-direction: column;
+
+ :deep(.el-tabs) {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+
+ .el-tabs__header {
+ margin-bottom: 0;
+ flex-shrink: 0;
+ padding: 0;
+ }
+
+ .el-tabs__content {
+ flex: 1;
+ overflow-y: auto;
+ min-height: 0;
+ padding-top: 16px;
+ }
+
+ .el-tab-pane {
+ height: 100%;
+ }
+ }
+ }
+
+ .empty-state {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 300px;
+ padding: 40px 0;
+ }
+
+ .notification-list {
+ .notification-item {
+ display: flex;
+ padding: 12px 0;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.18);
+ transition: background-color 0.3s;
+
+ &:hover {
+ background-color: #f8fbff;
+ }
+
+ &.read {
+ opacity: 0.7;
+ }
+
+ .notification-icon {
+ flex-shrink: 0;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(59, 130, 246, 0.12);
+ border-radius: 50%;
+ margin-right: 12px;
+ }
+
+ .notification-content-wrapper {
+ flex: 1;
+ min-width: 0;
+
+ .notification-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: var(--text-primary);
+ margin-bottom: 8px;
+ }
+
+ .notification-detail {
+ font-size: 13px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+ margin-bottom: 8px;
+ word-break: break-all;
+ }
+
+ .notification-time {
+ font-size: 12px;
+ color: var(--text-tertiary);
+ }
+ }
+
+ .notification-action {
+ flex-shrink: 0;
+ margin-left: 12px;
+ display: flex;
+ align-items: center;
+ }
+ }
+ }
+
+ .pagination-wrapper {
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid var(--surface-border);
+ display: flex;
+ justify-content: center;
+ padding-left: 0;
+ padding-right: 0;
+ }
+</style>
+
diff --git a/src/layout/components/Settings/index.vue b/src/layout/components/Settings/index.vue
new file mode 100644
index 0000000..917b28b
--- /dev/null
+++ b/src/layout/components/Settings/index.vue
@@ -0,0 +1,287 @@
+<template>
+ <el-drawer v-model="showSettings" direction="rtl" size="300px">
+<!-- <div class="setting-drawer-title">-->
+<!-- <h3 class="drawer-title">涓婚椋庢牸璁剧疆</h3>-->
+<!-- </div>-->
+<!-- <div class="setting-drawer-block-checbox">-->
+<!-- <div-->
+<!-- class="setting-drawer-block-checbox-item"-->
+<!-- @click="handleTheme('theme-dark')"-->
+<!-- >-->
+<!-- <img src="@/assets/images/dark.svg" alt="dark" />-->
+<!-- <div-->
+<!-- v-if="sideTheme === 'theme-dark'"-->
+<!-- class="setting-drawer-block-checbox-selectIcon"-->
+<!-- style="display: block"-->
+<!-- >-->
+<!-- <i aria-label="鍥炬爣: check" class="anticon anticon-check">-->
+<!-- <svg-->
+<!-- viewBox="64 64 896 896"-->
+<!-- data-icon="check"-->
+<!-- width="1em"-->
+<!-- height="1em"-->
+<!-- :fill="theme"-->
+<!-- aria-hidden="true"-->
+<!-- focusable="false"-->
+<!-- class-->
+<!-- >-->
+<!-- <path-->
+<!-- d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"-->
+<!-- />-->
+<!-- </svg>-->
+<!-- </i>-->
+<!-- </div>-->
+<!-- </div>-->
+<!-- <div-->
+<!-- class="setting-drawer-block-checbox-item"-->
+<!-- @click="handleTheme('theme-light')"-->
+<!-- >-->
+<!-- <img src="@/assets/images/light.svg" alt="light" />-->
+<!-- <div-->
+<!-- v-if="sideTheme === 'theme-light'"-->
+<!-- class="setting-drawer-block-checbox-selectIcon"-->
+<!-- style="display: block"-->
+<!-- >-->
+<!-- <i aria-label="鍥炬爣: check" class="anticon anticon-check">-->
+<!-- <svg-->
+<!-- viewBox="64 64 896 896"-->
+<!-- data-icon="check"-->
+<!-- width="1em"-->
+<!-- height="1em"-->
+<!-- :fill="theme"-->
+<!-- aria-hidden="true"-->
+<!-- focusable="false"-->
+<!-- class-->
+<!-- >-->
+<!-- <path-->
+<!-- d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"-->
+<!-- />-->
+<!-- </svg>-->
+<!-- </i>-->
+<!-- </div>-->
+<!-- </div>-->
+<!-- </div>-->
+ <div class="drawer-item">
+ <span>涓婚棰滆壊</span>
+ <span class="comp-style">
+ <el-color-picker
+ v-model="theme"
+ :predefine="predefineColors"
+ @change="themeChange"
+ />
+ </span>
+ </div>
+ <div class="drawer-item">
+ <span>鏄剧ず妯″紡</span>
+ <span class="comp-style">
+ <el-select
+ v-model="settingsStore.darkMode"
+ placeholder="璇烽�夋嫨"
+ style="width: 130px"
+ @change="darkModeChange"
+ >
+ <el-option
+ v-for="item in darkModeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </span>
+ </div>
+ <el-divider />
+
+ <h3 class="drawer-title">绯荤粺甯冨眬閰嶇疆</h3>
+
+ <div class="drawer-item">
+ <span>寮�鍚� TopNav</span>
+ <span class="comp-style">
+ <el-switch
+ v-model="settingsStore.topNav"
+ @change="topNavChange"
+ class="drawer-switch"
+ />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>寮�鍚� Tags-Views</span>
+ <span class="comp-style">
+ <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>鍥哄畾 Header</span>
+ <span class="comp-style">
+ <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>鏄剧ず Logo</span>
+ <span class="comp-style">
+ <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
+ </span>
+ </div>
+
+ <div class="drawer-item">
+ <span>鍔ㄦ�佹爣棰�</span>
+ <span class="comp-style">
+ <el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" />
+ </span>
+ </div>
+
+ <el-divider />
+
+ <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting"
+ >淇濆瓨閰嶇疆</el-button
+ >
+ <el-button plain icon="Refresh" @click="resetSetting">閲嶇疆閰嶇疆</el-button>
+ </el-drawer>
+</template>
+
+<script setup>
+import variables from "@/assets/styles/variables.module.scss";
+import axios from "axios";
+import { ElLoading, ElMessage } from "element-plus";
+import { useDynamicTitle } from "@/utils/dynamicTitle";
+import useAppStore from "@/store/modules/app";
+import useSettingsStore from "@/store/modules/settings";
+import usePermissionStore from "@/store/modules/permission";
+import { handleThemeStyle } from "@/utils/theme";
+
+const { proxy } = getCurrentInstance();
+const appStore = useAppStore();
+const settingsStore = useSettingsStore();
+const permissionStore = usePermissionStore();
+const showSettings = ref(false);
+const theme = ref(settingsStore.theme);
+const sideTheme = ref(settingsStore.sideTheme);
+const storeSettings = computed(() => settingsStore);
+const predefineColors = ref([
+ "#002fa7",
+ "#81D8D0",
+ "#E85827",
+ "#008C8C",
+ "#F9DC24",
+ "#B05923",
+ "#003153",
+ "#8F4B28",
+ "#4C0009",
+]);
+const darkModeOptions = ref([
+ { label: "璺熼殢绯荤粺", value: "auto" },
+ { label: "娴呰壊", value: "light" },
+ { label: "娣辫壊", value: "dark" },
+]);
+
+/** 鏄惁闇�瑕乼opnav */
+function topNavChange(val) {
+ if (!val) {
+ appStore.toggleSideBarHide(false);
+ permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
+ }
+}
+
+function themeChange(val) {
+ settingsStore.theme = val;
+ handleThemeStyle(val);
+}
+
+function darkModeChange(val) {
+ settingsStore.setDarkMode(val);
+}
+
+function handleTheme(val) {
+ settingsStore.sideTheme = val;
+ sideTheme.value = val;
+}
+
+function saveSetting() {
+ proxy.$modal.loading("姝e湪淇濆瓨鍒版湰鍦帮紝璇风◢鍊�...");
+ let layoutSetting = {
+ topNav: storeSettings.value.topNav,
+ tagsView: storeSettings.value.tagsView,
+ fixedHeader: storeSettings.value.fixedHeader,
+ sidebarLogo: storeSettings.value.sidebarLogo,
+ dynamicTitle: storeSettings.value.dynamicTitle,
+ sideTheme: storeSettings.value.sideTheme,
+ theme: storeSettings.value.theme,
+ darkMode: storeSettings.value.darkMode,
+ };
+ localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
+ setTimeout(proxy.$modal.closeLoading(), 1000);
+}
+
+function resetSetting() {
+ proxy.$modal.loading("姝e湪娓呴櫎璁剧疆缂撳瓨骞跺埛鏂帮紝璇风◢鍊�...");
+ localStorage.removeItem("layout-setting");
+ setTimeout("window.location.reload()", 1000);
+}
+
+function openSetting() {
+ showSettings.value = true;
+}
+
+defineExpose({
+ openSetting,
+});
+</script>
+
+<style lang="scss" scoped>
+.setting-drawer-title {
+ margin-bottom: 12px;
+ color: var(--el-text-color-primary, rgba(0, 0, 0, 0.85));
+ line-height: 22px;
+ font-weight: bold;
+
+ .drawer-title {
+ font-size: 14px;
+ }
+}
+
+.setting-drawer-block-checbox {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin-top: 10px;
+ margin-bottom: 20px;
+
+ .setting-drawer-block-checbox-item {
+ position: relative;
+ margin-right: 16px;
+ border-radius: 2px;
+ cursor: pointer;
+
+ img {
+ width: 48px;
+ height: 48px;
+ }
+
+ .setting-drawer-block-checbox-selectIcon {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ padding-top: 15px;
+ padding-left: 24px;
+ color: #1890ff;
+ font-weight: 700;
+ font-size: 14px;
+ }
+ }
+}
+
+.drawer-item {
+ color: var(--el-text-color-regular, rgba(0, 0, 0, 0.65));
+ padding: 12px 0;
+ font-size: 14px;
+
+ .comp-style {
+ float: right;
+ margin: -3px 8px 0px 0px;
+ }
+}
+</style>
diff --git a/src/layout/components/Sidebar/Link.vue b/src/layout/components/Sidebar/Link.vue
new file mode 100644
index 0000000..15692ba
--- /dev/null
+++ b/src/layout/components/Sidebar/Link.vue
@@ -0,0 +1,40 @@
+<template>
+ <component :is="type" v-bind="linkProps()">
+ <slot />
+ </component>
+</template>
+
+<script setup>
+import { isExternal } from '@/utils/validate'
+
+const props = defineProps({
+ to: {
+ type: [String, Object],
+ required: true
+ }
+})
+
+const isExt = computed(() => {
+ return isExternal(props.to)
+})
+
+const type = computed(() => {
+ if (isExt.value) {
+ return 'a'
+ }
+ return 'router-link'
+})
+
+function linkProps() {
+ if (isExt.value) {
+ return {
+ href: props.to,
+ target: '_blank',
+ rel: 'noopener'
+ }
+ }
+ return {
+ to: props.to
+ }
+}
+</script>
diff --git a/src/layout/components/Sidebar/Logo.vue b/src/layout/components/Sidebar/Logo.vue
new file mode 100644
index 0000000..1a66c4a
--- /dev/null
+++ b/src/layout/components/Sidebar/Logo.vue
@@ -0,0 +1,198 @@
+<template>
+ <div class="sidebar-logo-container" :class="{ collapse }">
+ <transition name="sidebarLogoFade">
+ <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
+ <img :src="faviconUrl" class="sidebar-logo sidebar-favicon" alt="绔欑偣鍥炬爣" />
+ </router-link>
+ <router-link v-else key="expand" class="sidebar-logo-link" :style="expandLogoLinkStyle" to="/">
+ <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="鍏徃Logo" />
+ <h1 v-if="!logoUrl" class="sidebar-title">{{ title }}</h1>
+ </router-link>
+ </transition>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, watch } from 'vue'
+import useUserStore from '@/store/modules/user'
+import defaultLogo from '@/assets/logo/logo.png'
+
+defineProps({
+ collapse: {
+ type: Boolean,
+ required: true
+ }
+})
+
+const title = import.meta.env.VITE_APP_TITLE
+const userStore = useUserStore()
+const baseUrl = import.meta.env.BASE_URL || '/'
+const faviconUrl = `${baseUrl.replace(/\/?$/, '/') }favicon.ico`.replace(/([^:]\/)\/+/g, '$1')
+
+const cleanFactoryName = computed(() => {
+ if (!userStore.currentFactoryName) return ''
+ return userStore.currentFactoryName.trim()
+})
+
+const logoUrl = ref('')
+
+const expandLogoLinkStyle = computed(() => {
+ if (!logoUrl.value) {
+ return { '--logo-bg-image': 'none' }
+ }
+ const escaped = String(logoUrl.value).replace(/"/g, '\\"')
+ return { '--logo-bg-image': `url("${escaped}")` }
+})
+
+const updateLogoUrl = () => {
+ if (!cleanFactoryName.value) {
+ logoUrl.value = defaultLogo
+ return
+ }
+
+ try {
+ const dynamicLogo = import.meta.glob('/src/assets/logo/*.png', { eager: true })
+ const logoPath = `/src/assets/logo/${cleanFactoryName.value}.png`
+
+ if (dynamicLogo[logoPath]) {
+ logoUrl.value = dynamicLogo[logoPath].default
+ } else {
+ logoUrl.value = defaultLogo
+ }
+ } catch (error) {
+ console.error('鍔犺浇宸ュ巶 Logo 澶辫触:', error)
+ logoUrl.value = defaultLogo
+ }
+}
+
+onMounted(() => {
+ updateLogoUrl()
+ watch(() => userStore.currentFactoryName, updateLogoUrl)
+})
+
+const handleImageError = () => {
+ logoUrl.value = defaultLogo
+}
+</script>
+
+<style lang="scss" scoped>
+@import '@/assets/styles/variables.module.scss';
+
+.sidebarLogoFade-enter-active {
+ transition: opacity 1.5s;
+}
+
+.sidebarLogoFade-enter,
+.sidebarLogoFade-leave-to {
+ opacity: 0;
+}
+
+.sidebar-logo-container {
+ position: relative;
+ width: 100% !important;
+ height: 78px !important;
+ line-height: 78px;
+ background: transparent;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ text-align: left;
+ overflow: hidden;
+ box-shadow: none;
+ backdrop-filter: none;
+ transition: all 0.3s ease;
+
+ .sidebar-logo-link {
+ position: relative;
+ isolation: isolate;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+
+ &::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background-image: var(--logo-bg-image);
+ background-size: cover;
+ background-position: center;
+ opacity: 0.26;
+ filter: blur(8px) saturate(0.9);
+ transform: scale(1.06);
+ pointer-events: none;
+ z-index: 0;
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(180deg, rgba(16, 49, 89, 0.18), rgba(16, 49, 89, 0.08));
+ pointer-events: none;
+ z-index: 0;
+ }
+ }
+
+ .sidebar-logo {
+ width: 100%;
+ height: 100%;
+ max-width: none;
+ max-height: none;
+ padding: 6px 10px;
+ vertical-align: middle;
+ object-fit: contain;
+ object-position: center;
+ filter: none;
+ display: block;
+ position: relative;
+ z-index: 1;
+ }
+
+ .sidebar-title {
+ display: inline-block;
+ margin: 0;
+ color: var(--text-primary);
+ font-weight: 600;
+ line-height: 1.2;
+ font-size: 14px;
+ font-family: "Segoe UI", "PingFang SC", sans-serif;
+ vertical-align: middle;
+ position: relative;
+ z-index: 1;
+ }
+
+ &.collapse {
+ .sidebar-logo-link {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+
+ &::before,
+ &::after {
+ display: none;
+ }
+ }
+
+ .sidebar-logo {
+ width: calc(100% - 8px);
+ height: calc(100% - 8px);
+ max-width: none;
+ max-height: none;
+ padding: 4px;
+ margin: 0 auto;
+ filter: none;
+ object-fit: contain;
+ object-position: center;
+ }
+
+ .sidebar-favicon {
+ width: calc(100% - 8px);
+ height: calc(100% - 8px);
+ max-width: none;
+ max-height: none;
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/Sidebar/SidebarItem.vue b/src/layout/components/Sidebar/SidebarItem.vue
new file mode 100644
index 0000000..3712c68
--- /dev/null
+++ b/src/layout/components/Sidebar/SidebarItem.vue
@@ -0,0 +1,171 @@
+<template>
+ <div v-if="!item.hidden" class="sidebar-item-wrapper">
+ <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
+ <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
+ <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
+ <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" class="menu-icon"/>
+ <template #title>
+ <span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span>
+ </template>
+ </el-menu-item>
+ </app-link>
+ </template>
+
+ <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
+ <template v-if="item.meta" #title>
+ <svg-icon :icon-class="item.meta && item.meta.icon" class="menu-icon" />
+ <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
+ </template>
+
+ <sidebar-item
+ v-for="(child, index) in item.children"
+ :key="child.path + index"
+ :is-nest="true"
+ :item="child"
+ :base-path="resolvePath(child.path)"
+ class="nest-menu"
+ />
+ </el-sub-menu>
+ </div>
+</template>
+
+<script setup>
+import { isExternal } from '@/utils/validate'
+import AppLink from './Link'
+import { getNormalPath } from '@/utils/ruoyi'
+
+const props = defineProps({
+ // route object
+ item: {
+ type: Object,
+ required: true
+ },
+ isNest: {
+ type: Boolean,
+ default: false
+ },
+ basePath: {
+ type: String,
+ default: ''
+ }
+})
+
+const onlyOneChild = ref({})
+
+function hasOneShowingChild(children = [], parent) {
+ if (!children) {
+ children = []
+ }
+ const showingChildren = children.filter(item => {
+ if (item.hidden) {
+ return false
+ }
+ onlyOneChild.value = item
+ return true
+ })
+
+ // When there is only one child router, the child router is displayed by default
+ if (showingChildren.length === 1) {
+ return true
+ }
+
+ // Show parent if there are no child router to display
+ if (showingChildren.length === 0) {
+ onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
+ return true
+ }
+
+ return false
+}
+
+function resolvePath(routePath, routeQuery) {
+ if (isExternal(routePath)) {
+ return routePath
+ }
+ if (isExternal(props.basePath)) {
+ return props.basePath
+ }
+ if (routeQuery) {
+ let query = JSON.parse(routeQuery)
+ return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
+ }
+ return getNormalPath(props.basePath + '/' + routePath)
+}
+
+function hasTitle(title){
+ if (title.length > 5) {
+ return title
+ } else {
+ return ""
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.sidebar-item-wrapper {
+ :deep(.menu-icon) {
+ width: 26px;
+ height: 26px;
+ margin-right: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ transition: color 0.2s ease;
+ color: var(--sidebar-text);
+ opacity: 0.88;
+ }
+
+ :deep(.el-menu-item:hover .menu-icon),
+ :deep(.el-sub-menu__title:hover .menu-icon) {
+ color: #ffffff;
+ opacity: 1;
+ }
+
+ :deep(.el-menu-item.is-active .menu-icon) {
+ color: var(--menu-active-text) !important;
+ opacity: 1;
+ }
+
+ :deep(.menu-title) {
+ font-weight: 500;
+ transition: color 0.2s ease;
+ color: var(--sidebar-text);
+ opacity: 0.82;
+ }
+
+ :deep(.el-menu-item:hover .menu-title),
+ :deep(.el-sub-menu__title:hover .menu-title) {
+ color: #ffffff;
+ opacity: 1;
+ }
+
+ :deep(.el-menu-item.is-active .menu-title) {
+ color: var(--menu-active-text) !important;
+ opacity: 1;
+ }
+
+ :deep(.nest-menu) {
+ .menu-icon {
+ width: 18px;
+ height: 18px;
+ margin-right: 8px;
+ opacity: 0.7;
+ }
+
+ .menu-title {
+ font-size: 13px;
+ opacity: 0.85;
+ }
+
+ .el-menu-item.is-active {
+ .menu-icon {
+ opacity: 1;
+ }
+ .menu-title {
+ opacity: 1;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/Sidebar/index.vue b/src/layout/components/Sidebar/index.vue
new file mode 100644
index 0000000..2a58c29
--- /dev/null
+++ b/src/layout/components/Sidebar/index.vue
@@ -0,0 +1,166 @@
+<template>
+ <div :class="{ 'has-logo': showLogo }" class="sidebar-container">
+ <logo v-if="showLogo" :collapse="isCollapse" />
+ <el-scrollbar wrap-class="scrollbar-wrapper">
+ <el-menu
+ :default-active="activeMenu"
+ :collapse="isCollapse"
+ :background-color="getMenuBackground"
+ :text-color="getMenuTextColor"
+ :unique-opened="true"
+ :active-text-color="theme"
+ :collapse-transition="false"
+ mode="vertical"
+ :class="sideTheme"
+ >
+ <sidebar-item
+ v-for="(route, index) in sidebarRouters"
+ :key="route.path + index"
+ :item="route"
+ :base-path="route.path"
+ />
+ </el-menu>
+ </el-scrollbar>
+ </div>
+</template>
+
+<script setup>
+import Logo from "./Logo";
+import SidebarItem from "./SidebarItem";
+import useAppStore from "@/store/modules/app";
+import useSettingsStore from "@/store/modules/settings";
+import usePermissionStore from "@/store/modules/permission";
+
+const route = useRoute();
+const appStore = useAppStore();
+const settingsStore = useSettingsStore();
+const permissionStore = usePermissionStore();
+
+const sidebarRouters = computed(() => permissionStore.sidebarRouters);
+const showLogo = computed(() => settingsStore.sidebarLogo);
+const sideTheme = computed(() => settingsStore.sideTheme);
+const theme = computed(() => settingsStore.theme);
+const isCollapse = computed(() => !appStore.sidebar.opened);
+
+const getMenuBackground = computed(() => "var(--sidebar-bg)");
+
+const getMenuTextColor = computed(() => "var(--sidebar-text)");
+
+const activeMenu = computed(() => {
+ const { meta, path } = route;
+ if (meta.activeMenu) return meta.activeMenu;
+ return path;
+});
+</script>
+
+<style lang="scss" scoped>
+.sidebar-container {
+ background: transparent;
+ border-radius: 0;
+ overflow: hidden;
+
+ .scrollbar-wrapper {
+ background: transparent;
+ }
+
+ .el-menu {
+ border: none !important;
+ height: 100%;
+ width: 100% !important;
+ border-radius: 0;
+ background: transparent !important;
+
+ .el-menu-item,
+ .el-sub-menu__title {
+ margin-bottom: 8px;
+ border-radius: 14px;
+ color: v-bind(getMenuTextColor);
+ font-size: 14px;
+ letter-spacing: 0;
+ transition:
+ transform 0.18s ease,
+ background 0.2s ease,
+ box-shadow 0.2s ease,
+ color 0.2s ease;
+ border: none !important;
+ display: flex;
+ align-items: center;
+
+ &:hover {
+ background: linear-gradient(128deg, rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.28), rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.08)) !important;
+ transform: translate3d(2px, 0, 0);
+ }
+ }
+
+ .el-menu-item {
+ color: var(--sidebar-text);
+
+ &.is-active {
+ background: var(--menu-active-bg, linear-gradient(135deg, var(--el-color-primary), var(--el-color-primary-light-3))) !important;
+ color: var(--menu-active-text) !important;
+ font-weight: 500;
+ border-radius: 14px;
+ box-shadow: var(--menu-active-glow, 0 10px 24px rgba(var(--el-color-primary-rgb, 37, 99, 235), 0.34));
+
+ .svg-icon {
+ color: var(--menu-active-text) !important;
+ }
+ }
+ }
+
+ .el-sub-menu__title {
+ color: v-bind(getMenuTextColor);
+ }
+
+ :deep(.el-sub-menu__icon-arrow) {
+ display: inline-flex !important;
+ align-items: center;
+ justify-content: center;
+ width: 14px;
+ height: 14px;
+ margin-top: -7px;
+ right: 14px;
+ font-size: 14px !important;
+ color: currentColor !important;
+ opacity: 0.7;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+ }
+
+ :deep(.el-sub-menu.is-opened .el-sub-menu__icon-arrow) {
+ transform: rotate(180deg);
+ }
+
+ :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
+ color: var(--menu-active-text) !important;
+ font-weight: 500;
+ border-radius: 12px;
+ margin: 0 12px 6px !important;
+ padding-left: 14px !important;
+ padding-right: 34px !important;
+ box-sizing: border-box;
+ overflow: hidden;
+ background-clip: padding-box;
+ background: var(--menu-active-bg) !important;
+ box-shadow: var(--menu-active-glow);
+ border: none !important;
+ }
+
+ :deep(.el-menu-item.is-active) {
+ margin: 0 12px 6px !important;
+ width: calc(100% - 24px) !important;
+ padding-left: 14px !important;
+ padding-right: 34px !important;
+ box-sizing: border-box;
+ overflow: hidden;
+ background-clip: padding-box;
+ border-radius: 12px;
+ }
+
+ :deep(.el-sub-menu.is-active > .el-sub-menu__title .menu-title),
+ :deep(.el-sub-menu.is-active > .el-sub-menu__title .svg-icon),
+ :deep(.el-menu-item.is-active .menu-title) {
+ color: var(--menu-active-text) !important;
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/TagsView/ScrollPane.vue b/src/layout/components/TagsView/ScrollPane.vue
new file mode 100644
index 0000000..71fa478
--- /dev/null
+++ b/src/layout/components/TagsView/ScrollPane.vue
@@ -0,0 +1,107 @@
+<template>
+ <el-scrollbar
+ ref="scrollContainer"
+ :vertical="false"
+ class="scroll-container"
+ @wheel.prevent="handleScroll"
+ >
+ <slot />
+ </el-scrollbar>
+</template>
+
+<script setup>
+import useTagsViewStore from '@/store/modules/tagsView'
+
+const tagAndTagSpacing = ref(4)
+const { proxy } = getCurrentInstance()
+
+const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef)
+
+onMounted(() => {
+ scrollWrapper.value.addEventListener('scroll', emitScroll, true)
+})
+
+onBeforeUnmount(() => {
+ scrollWrapper.value.removeEventListener('scroll', emitScroll)
+})
+
+function handleScroll(e) {
+ const eventDelta = e.wheelDelta || -e.deltaY * 40
+ const $scrollWrapper = scrollWrapper.value
+ $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+}
+
+const emits = defineEmits()
+const emitScroll = () => {
+ emits('scroll')
+}
+
+const tagsViewStore = useTagsViewStore()
+const visitedViews = computed(() => tagsViewStore.visitedViews)
+
+function moveToTarget(currentTag) {
+ const $container = proxy.$refs.scrollContainer.$el
+ const $containerWidth = $container.offsetWidth
+ const $scrollWrapper = scrollWrapper.value
+
+ let firstTag = null
+ let lastTag = null
+
+ // find first tag and last tag
+ if (visitedViews.value.length > 0) {
+ firstTag = visitedViews.value[0]
+ lastTag = visitedViews.value[visitedViews.value.length - 1]
+ }
+
+ if (firstTag === currentTag) {
+ $scrollWrapper.scrollLeft = 0
+ } else if (lastTag === currentTag) {
+ $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+ } else {
+ const tagListDom = document.getElementsByClassName('tags-view-item')
+ const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
+ let prevTag = null
+ let nextTag = null
+ for (const k in tagListDom) {
+ if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
+ if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
+ prevTag = tagListDom[k]
+ }
+ if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
+ nextTag = tagListDom[k]
+ }
+ }
+ }
+
+ // the tag's offsetLeft after of nextTag
+ const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
+
+ // the tag's offsetLeft before of prevTag
+ const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
+ if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+ $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+ } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+ $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+ }
+ }
+}
+
+defineExpose({
+ moveToTarget,
+})
+</script>
+
+<style lang='scss' scoped>
+.scroll-container {
+ white-space: nowrap;
+ position: relative;
+ overflow: hidden;
+ width: 100%;
+ :deep(.el-scrollbar__bar) {
+ bottom: 0px;
+ }
+ :deep(.el-scrollbar__wrap) {
+ height: var(--tagsbar-height);
+ }
+}
+</style>
diff --git a/src/layout/components/TagsView/index.vue b/src/layout/components/TagsView/index.vue
new file mode 100644
index 0000000..66b6014
--- /dev/null
+++ b/src/layout/components/TagsView/index.vue
@@ -0,0 +1,391 @@
+<template>
+ <div id="tags-view-container" class="tags-view-container">
+ <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
+ <router-link
+ v-for="tag in visitedViews"
+ :key="tag.path"
+ :data-path="tag.path"
+ :class="isActive(tag) ? 'active' : ''"
+ :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
+ class="tags-view-item"
+ :style="activeStyle(tag)"
+ @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
+ @contextmenu.prevent="openMenu(tag, $event)"
+ >
+ {{ tag.title }}
+ <span v-if="!isAffix(tag)" class="tags-view-close" @click.prevent.stop="closeSelectedTag(tag)">
+ <close class="el-icon-close" />
+ </span>
+ </router-link>
+ </scroll-pane>
+ <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
+ <li @click="refreshSelectedTag(selectedTag)">
+ <refresh-right style="width: 1em; height: 1em;" /> 鍒锋柊椤甸潰
+ </li>
+ <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
+ <close style="width: 1em; height: 1em;" /> 鍏抽棴褰撳墠
+ </li>
+ <li @click="closeOthersTags">
+ <circle-close style="width: 1em; height: 1em;" /> 鍏抽棴鍏朵粬
+ </li>
+ <li v-if="!isFirstView()" @click="closeLeftTags">
+ <back style="width: 1em; height: 1em;" /> 鍏抽棴宸︿晶
+ </li>
+ <li v-if="!isLastView()" @click="closeRightTags">
+ <right style="width: 1em; height: 1em;" /> 鍏抽棴鍙充晶
+ </li>
+ <li @click="closeAllTags(selectedTag)">
+ <circle-close style="width: 1em; height: 1em;" /> 鍏ㄩ儴鍏抽棴
+ </li>
+ </ul>
+ </div>
+</template>
+
+<script setup>
+import ScrollPane from './ScrollPane'
+import { getNormalPath } from '@/utils/ruoyi'
+import useTagsViewStore from '@/store/modules/tagsView'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+const visible = ref(false)
+const top = ref(0)
+const left = ref(0)
+const selectedTag = ref({})
+const affixTags = ref([])
+const scrollPaneRef = ref(null)
+
+const { proxy } = getCurrentInstance()
+const route = useRoute()
+const router = useRouter()
+
+const visitedViews = computed(() => useTagsViewStore().visitedViews)
+const routes = computed(() => usePermissionStore().routes)
+const theme = computed(() => useSettingsStore().theme)
+
+watch(route, () => {
+ addTags()
+ moveToCurrentTag()
+})
+
+watch(visible, (value) => {
+ if (value) {
+ document.body.addEventListener('click', closeMenu)
+ } else {
+ document.body.removeEventListener('click', closeMenu)
+ }
+})
+
+onMounted(() => {
+ initTags()
+ addTags()
+})
+
+function isActive(r) {
+ return r.path === route.path
+}
+
+function activeStyle(tag) {
+ if (!isActive(tag)) return {}
+ return {
+ "background-color": theme.value,
+ "border-color": theme.value
+ }
+}
+
+function isAffix(tag) {
+ return tag.meta && tag.meta.affix
+}
+
+function isFirstView() {
+ try {
+ return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
+ } catch (err) {
+ return false
+ }
+}
+
+function isLastView() {
+ try {
+ return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
+ } catch (err) {
+ return false
+ }
+}
+
+function filterAffixTags(routes, basePath = '') {
+ let tags = []
+ routes.forEach(route => {
+ if (route.meta && route.meta.affix) {
+ const tagPath = getNormalPath(basePath + '/' + route.path)
+ tags.push({
+ fullPath: tagPath,
+ path: tagPath,
+ name: route.name,
+ meta: { ...route.meta }
+ })
+ }
+ if (route.children) {
+ const tempTags = filterAffixTags(route.children, route.path)
+ if (tempTags.length >= 1) {
+ tags = [...tags, ...tempTags]
+ }
+ }
+ })
+ return tags
+}
+
+function initTags() {
+ const res = filterAffixTags(routes.value)
+ affixTags.value = res
+ for (const tag of res) {
+ // Must have tag name
+ if (tag.name) {
+ useTagsViewStore().addVisitedView(tag)
+ }
+ }
+}
+
+function addTags() {
+ const { name } = route
+ if (name) {
+ useTagsViewStore().addView(route)
+ }
+}
+
+function moveToCurrentTag() {
+ nextTick(() => {
+ for (const r of visitedViews.value) {
+ if (r.path === route.path) {
+ scrollPaneRef.value.moveToTarget(r)
+ // when query is different then update
+ if (r.fullPath !== route.fullPath) {
+ useTagsViewStore().updateVisitedView(route)
+ }
+ }
+ }
+ })
+}
+
+function refreshSelectedTag(view) {
+ proxy.$tab.refreshPage(view)
+ if (route.meta.link) {
+ useTagsViewStore().delIframeView(route)
+ }
+}
+
+function closeSelectedTag(view) {
+ proxy.$tab.closePage(view).then(({ visitedViews }) => {
+ if (isActive(view)) {
+ toLastView(visitedViews, view)
+ }
+ })
+}
+
+function closeRightTags() {
+ proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
+ if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
+ toLastView(visitedViews)
+ }
+ })
+}
+
+function closeLeftTags() {
+ proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
+ if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
+ toLastView(visitedViews)
+ }
+ })
+}
+
+function closeOthersTags() {
+ router.push(selectedTag.value).catch(() => { })
+ proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
+ moveToCurrentTag()
+ })
+}
+
+function closeAllTags(view) {
+ proxy.$tab.closeAllPage().then(({ visitedViews }) => {
+ if (affixTags.value.some(tag => tag.path === route.path)) {
+ return
+ }
+ toLastView(visitedViews, view)
+ })
+}
+
+function toLastView(visitedViews, view) {
+ const latestView = visitedViews.slice(-1)[0]
+ if (latestView) {
+ router.push(latestView.fullPath)
+ } else {
+ // now the default is to redirect to the home page if there is no tags-view,
+ // you can adjust it according to your needs.
+ if (view.name === 'Dashboard') {
+ // to reload home page
+ router.replace({ path: '/redirect' + view.fullPath })
+ } else {
+ router.push('/')
+ }
+ }
+}
+
+function openMenu(tag, e) {
+ const menuMinWidth = 105
+ const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
+ const offsetWidth = proxy.$el.offsetWidth // container width
+ const maxLeft = offsetWidth - menuMinWidth // left boundary
+ const l = e.clientX - offsetLeft + 15 // 15: margin right
+
+ if (l > maxLeft) {
+ left.value = maxLeft
+ } else {
+ left.value = l
+ }
+
+ top.value = e.clientY
+ visible.value = true
+ selectedTag.value = tag
+}
+
+function closeMenu() {
+ visible.value = false
+}
+
+function handleScroll() {
+ closeMenu()
+}
+</script>
+
+<style lang="scss" scoped>
+.tags-view-container {
+ height: var(--tagsbar-height);
+ width: 100%;
+ margin-top: 0;
+ padding: 0 2px;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ backdrop-filter: none;
+ box-shadow: none;
+
+ .tags-view-wrapper {
+ display: flex;
+ align-items: center;
+ min-height: var(--tagsbar-height);
+
+ .tags-view-item {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ position: relative;
+ cursor: pointer;
+ height: 30px;
+ line-height: 1;
+ color: var(--tags-item-text, #4E5463);
+ background: var(--tags-item-bg, #E5E7EA);
+ border: 1px solid var(--tags-item-border, #d8dce5);
+ border-radius: 999px;
+ padding: 0 14px;
+ font-size: 12px;
+ margin-right: 6px;
+ flex-shrink: 0;
+ gap: 6px;
+ transition: all 0.24s ease;
+
+ &:hover {
+ background: var(--tags-item-hover, #eee);
+ border-color: rgba(96, 165, 250, 0.36);
+ }
+
+ &.active {
+ background-image: linear-gradient(132deg, rgba(47, 128, 255, 0.95), rgba(56, 189, 248, 0.9));
+ color: #fff;
+ box-shadow: 0 10px 20px rgba(37, 99, 235, 0.22);
+ border-color: rgba(147, 197, 253, 0.72) !important;
+ }
+ }
+ }
+
+ .contextmenu {
+ margin: 0;
+ background: var(--el-bg-color-overlay, #fff);
+ z-index: 3000;
+ position: absolute;
+ list-style-type: none;
+ padding: 5px 0;
+ border-radius: 16px;
+ font-size: 12px;
+ font-weight: 400;
+ color: var(--tags-item-text, #333);
+ box-shadow: var(--shadow-md);
+ border: 1px solid var(--surface-border, #e4e7ed);
+
+ li {
+ margin: 0;
+ padding: 7px 16px;
+ cursor: pointer;
+
+ &:hover {
+ background: var(--tags-item-hover, #eee);
+ }
+ }
+ }
+}
+</style>
+
+<style lang="scss">
+//reset element css of el-icon-close
+.tags-view-wrapper {
+ .el-scrollbar__view {
+ display: flex;
+ align-items: center;
+ }
+
+ .tags-view-item {
+ .tags-view-close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 12px;
+ height: 12px;
+ line-height: 1;
+ align-self: center;
+ transform: translateY(1px);
+ }
+
+ .el-icon-close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 12px;
+ height: 12px;
+ line-height: 1;
+ vertical-align: initial !important;
+ border-radius: 50%;
+ text-align: center;
+ transition: all .3s cubic-bezier(.645, .045, .355, 1);
+ transform-origin: 100% 50%;
+ align-self: center;
+
+ &:before {
+ transform: scale(.6);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ svg {
+ display: block;
+ width: 10px;
+ height: 10px;
+ }
+
+ &:hover {
+ background-color: var(--tags-close-hover, #b4bccc);
+ color: #fff;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/layout/components/index.js b/src/layout/components/index.js
new file mode 100644
index 0000000..630b9fe
--- /dev/null
+++ b/src/layout/components/index.js
@@ -0,0 +1,5 @@
+export { default as AppMain } from './AppMain'
+export { default as Navbar } from './Navbar'
+export { default as Settings } from './Settings'
+export { default as TagsView } from './TagsView/index.vue'
+export { default as NotificationCenter } from './NotificationCenter/index.vue'
diff --git a/src/layout/index.vue b/src/layout/index.vue
new file mode 100644
index 0000000..aa1bfb5
--- /dev/null
+++ b/src/layout/index.vue
@@ -0,0 +1,171 @@
+<template>
+ <div :class="classObj"
+ class="app-wrapper"
+ :style="{ '--current-color': theme }">
+ <div v-if="device === 'mobile' && sidebar.opened"
+ class="drawer-bg"
+ @click="handleClickOutside" />
+ <sidebar v-if="!sidebar.hide"
+ class="sidebar-container" />
+ <div :class="{ hasTagsView: showTagsView, sidebarHide: sidebar.hide }"
+ class="main-container main-layout">
+ <div :class="{ 'fixed-header': fixedHeader, 'with-tags': showTagsView }">
+ <navbar @setLayout="setLayout" />
+ <tags-view />
+ </div>
+ <app-main />
+ <settings ref="settingRef" />
+ </div>
+ <AIChatSidebar v-if="showGlobalAiChat" />
+ </div>
+</template>
+
+<script setup>
+ import { useWindowSize } from "@vueuse/core";
+ import { useRoute } from "vue-router";
+ import Sidebar from "./components/Sidebar/index.vue";
+ import { AppMain, Navbar, Settings, TagsView } from "./components";
+ import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
+ import defaultSettings from "@/settings";
+
+ import useAppStore from "@/store/modules/app";
+ import useUserStore from "@/store/modules/user";
+ import useSettingsStore from "@/store/modules/settings";
+ import useTagsViewStore from "@/store/modules/tagsView";
+
+ const settingsStore = useSettingsStore();
+ const tagsViewStore = useTagsViewStore();
+ const userStore = useUserStore();
+ const route = useRoute();
+ const theme = computed(() => settingsStore.theme);
+ const sideTheme = computed(() => settingsStore.sideTheme);
+ const sidebar = computed(() => useAppStore().sidebar);
+ const device = computed(() => useAppStore().device);
+ const needTagsView = computed(() => settingsStore.tagsView);
+ const showTagsView = computed(
+ () => needTagsView.value && tagsViewStore.visitedViews.length > 1
+ );
+ const fixedHeader = computed(() => settingsStore.fixedHeader);
+ const aiEnabled = computed(() => Number(userStore.aiEnabled) === 1);
+ const showGlobalAiChat = computed(() => {
+ const isIndustrialBrainRoute = String(route.path || "").startsWith(
+ "/ai-industrial-brain"
+ );
+ return !isIndustrialBrainRoute && aiEnabled.value;
+ });
+
+ const classObj = computed(() => ({
+ hideSidebar: !sidebar.value.opened,
+ openSidebar: sidebar.value.opened,
+ withoutAnimation: sidebar.value.withoutAnimation,
+ mobile: device.value === "mobile",
+ }));
+
+ const { width, height } = useWindowSize();
+ const WIDTH = 992; // refer to Bootstrap's responsive design
+
+ watch(
+ () => device.value,
+ () => {
+ if (device.value === "mobile" && sidebar.value.opened) {
+ useAppStore().closeSideBar({ withoutAnimation: false });
+ }
+ }
+ );
+
+ watchEffect(() => {
+ if (width.value - 1 < WIDTH) {
+ useAppStore().toggleDevice("mobile");
+ useAppStore().closeSideBar({ withoutAnimation: true });
+ } else {
+ useAppStore().toggleDevice("desktop");
+ }
+ });
+
+ function handleClickOutside() {
+ useAppStore().closeSideBar({ withoutAnimation: false });
+ }
+
+ const settingRef = ref(null);
+ function setLayout() {
+ settingRef.value.openSetting();
+ }
+</script>
+
+<style lang="scss" scoped>
+ @import "@/assets/styles/mixin.scss";
+
+ .app-wrapper {
+ @include clearfix;
+ position: relative;
+ min-height: 100%;
+ width: 100%;
+ background: radial-gradient(
+ circle at 14% -8%,
+ rgba(59, 130, 246, 0.14),
+ transparent 36%
+ ),
+ radial-gradient(
+ circle at 88% -12%,
+ rgba(56, 189, 248, 0.1),
+ transparent 30%
+ ),
+ linear-gradient(165deg, #f3f7fc 0%, #eef5ff 56%, #f8fbff 100%);
+
+ &.mobile.openSidebar {
+ position: fixed;
+ top: 0;
+ }
+ }
+
+ .drawer-bg {
+ background: rgba(15, 23, 42, 0.22);
+ width: 100%;
+ top: 0;
+ height: 100%;
+ position: absolute;
+ z-index: 999;
+ }
+
+ .main-layout {
+ min-height: 100vh;
+ margin-left: var(--sidebar-width);
+ transition: margin-left 0.25s ease;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .fixed-header {
+ position: sticky;
+ top: 0;
+ z-index: var(--layout-header-z);
+ width: 100%;
+ padding: 8px var(--content-gap) 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ background: var(--app-bg, #f3f7fc);
+ }
+
+ .fixed-header.with-tags {
+ padding-bottom: 6px;
+ }
+
+ .hideSidebar .fixed-header {
+ width: 100%;
+ }
+
+ .hideSidebar .main-layout {
+ margin-left: var(--sidebar-collapsed-width);
+ }
+
+ .mobile .fixed-header {
+ width: 100%;
+ padding: 8px 10px 0;
+ }
+
+ .mobile .main-layout,
+ .sidebarHide.main-layout {
+ margin-left: 0;
+ }
+</style>
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..025ff14
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,119 @@
+import { createApp } from "vue";
+
+import Cookies from "js-cookie";
+
+import ElementPlus from "element-plus";
+import "element-plus/dist/index.css";
+import "element-plus/theme-chalk/dark/css-vars.css";
+import locale from "element-plus/es/locale/lang/zh-cn";
+
+import "@/assets/styles/index.scss"; // global css
+
+import App from "./App";
+import store from "./store";
+import router from "./router";
+import directive from "./directive"; // directive
+
+// 娉ㄥ唽鎸囦护
+import plugins from "./plugins"; // plugins
+import { download } from "@/utils/request";
+
+// svg鍥炬爣
+import "virtual:svg-icons-register";
+import SvgIcon from "@/components/SvgIcon";
+import elementIcons from "@/components/SvgIcon/svgicon";
+import "./assets/fonts/font.css";
+
+import "./permission"; // permission control
+
+import { useDict } from "@/utils/dict";
+import {
+ parseTime,
+ resetForm,
+ addDateRange,
+ handleTree,
+ selectDictLabel,
+ selectDictLabels,
+} from "@/utils/ruoyi";
+
+// 鍒嗛〉缁勪欢
+import Pagination from "@/components/Pagination";
+// 鑷畾涔夎〃鏍煎伐鍏风粍浠�
+import RightToolbar from "@/components/RightToolbar";
+// 瀵屾枃鏈粍浠�
+import Editor from "@/components/Editor";
+// 鏂囦欢涓婁紶缁勪欢
+import FileUpload from "@/components/AttachmentUpload/file";
+// 鍥剧墖涓婁紶缁勪欢
+import ImageUpload from "@/components/AttachmentUpload/image";
+// 鍥剧墖棰勮缁勪欢
+import ImagePreview from "@/components/AttachmentPreview/image";
+// 闄勪欢寮圭獥缁勪欢
+import FileListDialog from "@/components/Dialog/FileList.vue";
+// 瀛楀吀鏍囩缁勪欢
+import DictTag from "@/components/DictTag";
+// 琛ㄦ牸缁勪欢
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+// 椤甸潰澶撮儴缁勪欢
+import PageHeader from "@/components/PageHeader/index.vue";
+
+import { getToken } from "@/utils/auth";
+import {
+ calculateTaxExclusiveTotalPrice,
+ summarizeTable,
+ calculateTaxIncludeTotalPrice,
+} from "@/utils/summarizeTable.js";
+
+const app = createApp(App);
+
+// 鍏ㄥ眬鏂规硶鎸傝浇
+app.config.globalProperties.useDict = useDict;
+app.config.globalProperties.download = download;
+app.config.globalProperties.parseTime = parseTime;
+app.config.globalProperties.resetForm = resetForm;
+app.config.globalProperties.summarizeTable = summarizeTable;
+app.config.globalProperties.calculateTaxExclusiveTotalPrice =
+ calculateTaxExclusiveTotalPrice;
+app.config.globalProperties.calculateTaxIncludeTotalPrice =
+ calculateTaxIncludeTotalPrice;
+app.config.globalProperties.handleTree = handleTree;
+app.config.globalProperties.addDateRange = addDateRange;
+app.config.globalProperties.selectDictLabel = selectDictLabel;
+app.config.globalProperties.selectDictLabels = selectDictLabels;
+app.config.globalProperties.javaApi = __BASE_API__;
+app.config.globalProperties.HaveJson = (val) => {
+ return JSON.parse(JSON.stringify(val));
+};
+app.config.globalProperties.uploadHeader = {
+ Authorization: "Bearer " + getToken(),
+};
+
+// 鍏ㄥ眬缁勪欢鎸傝浇
+app.component("DictTag", DictTag);
+app.component("Pagination", Pagination);
+app.component("FileUpload", FileUpload);
+app.component("ImageUpload", ImageUpload);
+app.component("ImagePreview", ImagePreview);
+app.component("FileListDialog", FileListDialog);
+app.component("RightToolbar", RightToolbar);
+app.component("Editor", Editor);
+app.component("PIMTable", PIMTable);
+app.component("PageHeader", PageHeader);
+
+app.use(router);
+app.use(store);
+app.use(plugins);
+app.use(elementIcons);
+app.component("svg-icon", SvgIcon);
+
+directive(app);
+
+// 浣跨敤element-plus 骞朵笖璁剧疆鍏ㄥ眬鐨勫ぇ灏�
+app.use(ElementPlus, {
+ locale: locale,
+ // 鏀寔 large銆乨efault銆乻mall
+ size: Cookies.get("size") || "default",
+});
+app._context.components.ElDialog.props.closeOnClickModal.default = false;
+
+app.mount("#app");
diff --git a/src/permission.js b/src/permission.js
new file mode 100644
index 0000000..9bf4a91
--- /dev/null
+++ b/src/permission.js
@@ -0,0 +1,69 @@
+import router from './router'
+import { ElMessage } from 'element-plus'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import { getToken } from '@/utils/auth'
+import { isHttp, isPathMatch } from '@/utils/validate'
+import { isRelogin } from '@/utils/request'
+import useUserStore from '@/store/modules/user'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+NProgress.configure({ showSpinner: false })
+
+const whiteList = ['/login', '/register', '/callbacklccpn','/device-info']
+
+const isWhiteList = (path) => {
+ return whiteList.some(pattern => isPathMatch(pattern, path))
+}
+
+router.beforeEach((to, from, next) => {
+ NProgress.start()
+ if (getToken()) {
+ to.meta.title && useSettingsStore().setTitle(to.meta.title)
+ /* has token*/
+ if (to.path === '/login') {
+ next({ path: '/' })
+ NProgress.done()
+ } else if (isWhiteList(to.path)) {
+ next()
+ } else {
+ if (useUserStore().roles.length === 0) {
+ isRelogin.show = true
+ // 鍒ゆ柇褰撳墠鐢ㄦ埛鏄惁宸叉媺鍙栧畬user_info淇℃伅
+ useUserStore().getInfo().then(() => {
+ isRelogin.show = false
+ usePermissionStore().generateRoutes().then(accessRoutes => {
+ // 鏍规嵁roles鏉冮檺鐢熸垚鍙闂殑璺敱琛�
+ accessRoutes.forEach(route => {
+ if (!isHttp(route.path)) {
+ router.addRoute(route) // 鍔ㄦ�佹坊鍔犲彲璁块棶璺敱琛�
+ }
+ })
+ next({ ...to, replace: true }) // hack鏂规硶 纭繚addRoutes宸插畬鎴�
+ })
+ }).catch(err => {
+ useUserStore().logOut().then(() => {
+ ElMessage.error(err)
+ next({ path: '/' })
+ })
+ })
+ } else {
+ next()
+ }
+ }
+ } else {
+ // 娌℃湁token
+ if (isWhiteList(to.path)) {
+ // 鍦ㄥ厤鐧诲綍鐧藉悕鍗曪紝鐩存帴杩涘叆
+ next()
+ } else {
+ next(`/login?redirect=${to.fullPath}`) // 鍚﹀垯鍏ㄩ儴閲嶅畾鍚戝埌鐧诲綍椤�
+ NProgress.done()
+ }
+ }
+})
+
+router.afterEach(() => {
+ NProgress.done()
+})
diff --git a/src/plugins/auth.js b/src/plugins/auth.js
new file mode 100644
index 0000000..9b6ed1f
--- /dev/null
+++ b/src/plugins/auth.js
@@ -0,0 +1,60 @@
+import useUserStore from '@/store/modules/user'
+
+function authPermission(permission) {
+ const all_permission = "*:*:*"
+ const permissions = useUserStore().permissions
+ if (permission && permission.length > 0) {
+ return permissions.some(v => {
+ return all_permission === v || v === permission
+ })
+ } else {
+ return false
+ }
+}
+
+function authRole(role) {
+ const super_admin = "admin"
+ const roles = useUserStore().roles
+ if (role && role.length > 0) {
+ return roles.some(v => {
+ return super_admin === v || v === role
+ })
+ } else {
+ return false
+ }
+}
+
+export default {
+ // 楠岃瘉鐢ㄦ埛鏄惁鍏峰鏌愭潈闄�
+ hasPermi(permission) {
+ return authPermission(permission)
+ },
+ // 楠岃瘉鐢ㄦ埛鏄惁鍚湁鎸囧畾鏉冮檺锛屽彧闇�鍖呭惈鍏朵腑涓�涓�
+ hasPermiOr(permissions) {
+ return permissions.some(item => {
+ return authPermission(item)
+ })
+ },
+ // 楠岃瘉鐢ㄦ埛鏄惁鍚湁鎸囧畾鏉冮檺锛屽繀椤诲叏閮ㄦ嫢鏈�
+ hasPermiAnd(permissions) {
+ return permissions.every(item => {
+ return authPermission(item)
+ })
+ },
+ // 楠岃瘉鐢ㄦ埛鏄惁鍏峰鏌愯鑹�
+ hasRole(role) {
+ return authRole(role)
+ },
+ // 楠岃瘉鐢ㄦ埛鏄惁鍚湁鎸囧畾瑙掕壊锛屽彧闇�鍖呭惈鍏朵腑涓�涓�
+ hasRoleOr(roles) {
+ return roles.some(item => {
+ return authRole(item)
+ })
+ },
+ // 楠岃瘉鐢ㄦ埛鏄惁鍚湁鎸囧畾瑙掕壊锛屽繀椤诲叏閮ㄦ嫢鏈�
+ hasRoleAnd(roles) {
+ return roles.every(item => {
+ return authRole(item)
+ })
+ }
+}
diff --git a/src/plugins/cache.js b/src/plugins/cache.js
new file mode 100644
index 0000000..4d29dfe
--- /dev/null
+++ b/src/plugins/cache.js
@@ -0,0 +1,79 @@
+const sessionCache = {
+ set (key, value) {
+ if (!sessionStorage) {
+ return
+ }
+ if (key != null && value != null) {
+ sessionStorage.setItem(key, value)
+ }
+ },
+ get (key) {
+ if (!sessionStorage) {
+ return null
+ }
+ if (key == null) {
+ return null
+ }
+ return sessionStorage.getItem(key)
+ },
+ setJSON (key, jsonValue) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue))
+ }
+ },
+ getJSON (key) {
+ const value = this.get(key)
+ if (value != null) {
+ return JSON.parse(value)
+ }
+ return null
+ },
+ remove (key) {
+ sessionStorage.removeItem(key)
+ }
+}
+const localCache = {
+ set (key, value) {
+ if (!localStorage) {
+ return
+ }
+ if (key != null && value != null) {
+ localStorage.setItem(key, value)
+ }
+ },
+ get (key) {
+ if (!localStorage) {
+ return null
+ }
+ if (key == null) {
+ return null
+ }
+ return localStorage.getItem(key)
+ },
+ setJSON (key, jsonValue) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue))
+ }
+ },
+ getJSON (key) {
+ const value = this.get(key)
+ if (value != null) {
+ return JSON.parse(value)
+ }
+ return null
+ },
+ remove (key) {
+ localStorage.removeItem(key)
+ }
+}
+
+export default {
+ /**
+ * 浼氳瘽绾х紦瀛�
+ */
+ session: sessionCache,
+ /**
+ * 鏈湴缂撳瓨
+ */
+ local: localCache
+}
diff --git a/src/plugins/download.js b/src/plugins/download.js
new file mode 100644
index 0000000..35ac92f
--- /dev/null
+++ b/src/plugins/download.js
@@ -0,0 +1,101 @@
+锘縤mport axios from "axios";
+import { ElLoading, ElMessage } from "element-plus";
+import { saveAs } from "file-saver";
+import { getToken } from "@/utils/auth";
+import errorCode from "@/utils/errorCode";
+import { blobValidate } from "@/utils/ruoyi";
+
+const baseURL = import.meta.env.VITE_APP_BASE_API;
+let downloadLoadingInstance;
+
+export default {
+ name(name, isDelete = true) {
+ var url =
+ baseURL +
+ "/common/download?fileName=" +
+ encodeURIComponent(name) +
+ "&delete=" +
+ isDelete;
+ axios({
+ method: "get",
+ url: url,
+ responseType: "blob",
+ headers: { Authorization: "Bearer " + getToken() },
+ }).then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data]);
+ this.saveAs(blob, decodeURIComponent(res.headers["download-filename"]));
+ } else {
+ this.printErrMsg(res.data);
+ }
+ });
+ },
+ resource(resource) {
+ var url =
+ baseURL +
+ "/common/download/resource?resource=" +
+ encodeURIComponent(resource);
+ axios({
+ method: "get",
+ url: url,
+ responseType: "blob",
+ headers: { Authorization: "Bearer " + getToken() },
+ }).then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data]);
+ this.saveAs(blob, decodeURIComponent(res.headers["download-filename"]));
+ } else {
+ this.printErrMsg(res.data);
+ }
+ });
+ },
+ zip(url, name) {
+ var url = baseURL + url;
+ downloadLoadingInstance = ElLoading.service({
+ text: "姝e湪涓嬭浇鏁版嵁锛岃绋嶅��",
+ background: "rgba(0, 0, 0, 0.7)",
+ });
+ axios({
+ method: "get",
+ url: url,
+ responseType: "blob",
+ headers: { Authorization: "Bearer " + getToken() },
+ })
+ .then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data], { type: "application/zip" });
+ this.saveAs(blob, name);
+ } else {
+ this.printErrMsg(res.data);
+ }
+ downloadLoadingInstance.close();
+ })
+ .catch((r) => {
+ console.error(r);
+ ElMessage.error("涓嬭浇鏂囦欢鍑虹幇閿欒锛岃鑱旂郴绠$悊鍛橈紒");
+ downloadLoadingInstance.close();
+ });
+ },
+ saveAs(text, name, opts) {
+ saveAs(text, name, opts);
+ },
+ byUrl(url, filename) {
+ // 灏哢RL涓殑preview鏇挎崲鎴恉ownload
+ const downloadUrl = url.replace(/preview/g, 'download')
+ const link = document.createElement('a')
+ link.href = downloadUrl
+ link.download = filename || ''
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ },
+ async printErrMsg(data) {
+ const resText = await data.text();
+ const rspObj = JSON.parse(resText);
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode["default"];
+ ElMessage.error(errMsg);
+ },
+};
diff --git a/src/plugins/index.js b/src/plugins/index.js
new file mode 100644
index 0000000..fb75c63
--- /dev/null
+++ b/src/plugins/index.js
@@ -0,0 +1,18 @@
+import tab from './tab'
+import auth from './auth'
+import cache from './cache'
+import modal from './modal'
+import download from './download'
+
+export default function installPlugins(app){
+ // 椤电鎿嶄綔
+ app.config.globalProperties.$tab = tab
+ // 璁よ瘉瀵硅薄
+ app.config.globalProperties.$auth = auth
+ // 缂撳瓨瀵硅薄
+ app.config.globalProperties.$cache = cache
+ // 妯℃�佹瀵硅薄
+ app.config.globalProperties.$modal = modal
+ // 涓嬭浇鏂囦欢
+ app.config.globalProperties.$download = download
+}
diff --git a/src/plugins/modal.js b/src/plugins/modal.js
new file mode 100644
index 0000000..8695360
--- /dev/null
+++ b/src/plugins/modal.js
@@ -0,0 +1,82 @@
+import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
+
+let loadingInstance
+
+export default {
+ // 娑堟伅鎻愮ず
+ msg(content) {
+ ElMessage.info(content)
+ },
+ // 閿欒娑堟伅
+ msgError(content) {
+ ElMessage.error(content)
+ },
+ // 鎴愬姛娑堟伅
+ msgSuccess(content) {
+ ElMessage.success(content)
+ },
+ // 璀﹀憡娑堟伅
+ msgWarning(content) {
+ ElMessage.warning(content)
+ },
+ // 寮瑰嚭鎻愮ず
+ alert(content) {
+ ElMessageBox.alert(content, "绯荤粺鎻愮ず")
+ },
+ // 閿欒鎻愮ず
+ alertError(content) {
+ ElMessageBox.alert(content, "绯荤粺鎻愮ず", { type: 'error' })
+ },
+ // 鎴愬姛鎻愮ず
+ alertSuccess(content) {
+ ElMessageBox.alert(content, "绯荤粺鎻愮ず", { type: 'success' })
+ },
+ // 璀﹀憡鎻愮ず
+ alertWarning(content) {
+ ElMessageBox.alert(content, "绯荤粺鎻愮ず", { type: 'warning' })
+ },
+ // 閫氱煡鎻愮ず
+ notify(content) {
+ ElNotification.info(content)
+ },
+ // 閿欒閫氱煡
+ notifyError(content) {
+ ElNotification.error(content)
+ },
+ // 鎴愬姛閫氱煡
+ notifySuccess(content) {
+ ElNotification.success(content)
+ },
+ // 璀﹀憡閫氱煡
+ notifyWarning(content) {
+ ElNotification.warning(content)
+ },
+ // 纭绐椾綋
+ confirm(content) {
+ return ElMessageBox.confirm(content, "绯荤粺鎻愮ず", {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: "warning",
+ })
+ },
+ // 鎻愪氦鍐呭
+ prompt(content) {
+ return ElMessageBox.prompt(content, "绯荤粺鎻愮ず", {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: "warning",
+ })
+ },
+ // 鎵撳紑閬僵灞�
+ loading(content) {
+ loadingInstance = ElLoading.service({
+ lock: true,
+ text: content,
+ background: "rgba(0, 0, 0, 0.7)",
+ })
+ },
+ // 鍏抽棴閬僵灞�
+ closeLoading() {
+ loadingInstance.close()
+ }
+}
diff --git a/src/plugins/tab.js b/src/plugins/tab.js
new file mode 100644
index 0000000..16755f0
--- /dev/null
+++ b/src/plugins/tab.js
@@ -0,0 +1,71 @@
+import useTagsViewStore from '@/store/modules/tagsView'
+import router from '@/router'
+
+export default {
+ // 鍒锋柊褰撳墠tab椤电
+ refreshPage(obj) {
+ const { path, query, matched } = router.currentRoute.value
+ if (obj === undefined) {
+ matched.forEach((m) => {
+ if (m.components && m.components.default && m.components.default.name) {
+ if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
+ obj = { name: m.components.default.name, path: path, query: query }
+ }
+ }
+ })
+ }
+ return useTagsViewStore().delCachedView(obj).then(() => {
+ const { path, query } = obj
+ router.replace({
+ path: '/redirect' + path,
+ query: query
+ })
+ })
+ },
+ // 鍏抽棴褰撳墠tab椤电锛屾墦寮�鏂伴〉绛�
+ closeOpenPage(obj) {
+ useTagsViewStore().delView(router.currentRoute.value)
+ if (obj !== undefined) {
+ return router.push(obj)
+ }
+ },
+ // 鍏抽棴鎸囧畾tab椤电
+ closePage(obj) {
+ if (obj === undefined) {
+ return useTagsViewStore().delView(router.currentRoute.value).then(({ visitedViews }) => {
+ const latestView = visitedViews.slice(-1)[0]
+ if (latestView) {
+ return router.push(latestView.fullPath)
+ }
+ return router.push('/')
+ })
+ }
+ return useTagsViewStore().delView(obj)
+ },
+ // 鍏抽棴鎵�鏈塼ab椤电
+ closeAllPage() {
+ return useTagsViewStore().delAllViews()
+ },
+ // 鍏抽棴宸︿晶tab椤电
+ closeLeftPage(obj) {
+ return useTagsViewStore().delLeftTags(obj || router.currentRoute.value)
+ },
+ // 鍏抽棴鍙充晶tab椤电
+ closeRightPage(obj) {
+ return useTagsViewStore().delRightTags(obj || router.currentRoute.value)
+ },
+ // 鍏抽棴鍏朵粬tab椤电
+ closeOtherPage(obj) {
+ return useTagsViewStore().delOthersViews(obj || router.currentRoute.value)
+ },
+ // 鎵撳紑tab椤电
+ openPage(title, url, params) {
+ const obj = { path: url, meta: { title: title } }
+ useTagsViewStore().addView(obj)
+ return router.push({ path: url, query: params })
+ },
+ // 淇敼tab椤电
+ updatePage(obj) {
+ return useTagsViewStore().updateVisitedView(obj)
+ }
+}
diff --git a/src/router/index.js b/src/router/index.js
new file mode 100644
index 0000000..7be3668
--- /dev/null
+++ b/src/router/index.js
@@ -0,0 +1,342 @@
+import { createWebHistory, createRouter } from "vue-router";
+/* Layout */
+import Layout from "@/layout";
+
+/**
+ * Note: 璺敱閰嶇疆椤�
+ *
+ * hidden: true // 褰撹缃� true 鐨勬椂鍊欒璺敱涓嶄細鍐嶄晶杈规爮鍑虹幇 濡�401锛宭ogin绛夐〉闈紝鎴栬�呭涓�浜涚紪杈戦〉闈�/edit/1
+ * alwaysShow: true // 褰撲綘涓�涓矾鐢变笅闈㈢殑 children 澹版槑鐨勮矾鐢卞ぇ浜�1涓椂锛岃嚜鍔ㄤ細鍙樻垚宓屽鐨勬ā寮�--濡傜粍浠堕〉闈�
+ * // 鍙湁涓�涓椂锛屼細灏嗛偅涓瓙璺敱褰撳仛鏍硅矾鐢辨樉绀哄湪渚ц竟鏍�--濡傚紩瀵奸〉闈�
+ * // 鑻ヤ綘鎯充笉绠¤矾鐢变笅闈㈢殑 children 澹版槑鐨勪釜鏁伴兘鏄剧ず浣犵殑鏍硅矾鐢�
+ * // 浣犲彲浠ヨ缃� alwaysShow: true锛岃繖鏍峰畠灏变細蹇界暐涔嬪墠瀹氫箟鐨勮鍒欙紝涓�鐩存樉绀烘牴璺敱
+ * redirect: noRedirect // 褰撹缃� noRedirect 鐨勬椂鍊欒璺敱鍦ㄩ潰鍖呭睉瀵艰埅涓笉鍙鐐瑰嚮
+ * name:'router-name' // 璁惧畾璺敱鐨勫悕瀛楋紝涓�瀹氳濉啓涓嶇劧浣跨敤<keep-alive>鏃朵細鍑虹幇鍚勭闂
+ * query: '{"id": 1, "name": "ry"}' // 璁块棶璺敱鐨勯粯璁や紶閫掑弬鏁�
+ * roles: ['admin', 'common'] // 璁块棶璺敱鐨勮鑹叉潈闄�
+ * permissions: ['a:a:a', 'b:b:b'] // 璁块棶璺敱鐨勮彍鍗曟潈闄�
+ * meta : {
+ noCache: true // 濡傛灉璁剧疆涓簍rue锛屽垯涓嶄細琚� <keep-alive> 缂撳瓨(榛樿 false)
+ title: 'title' // 璁剧疆璇ヨ矾鐢卞湪渚ц竟鏍忓拰闈㈠寘灞戜腑灞曠ず鐨勫悕瀛�
+ icon: 'svg-name' // 璁剧疆璇ヨ矾鐢辩殑鍥炬爣锛屽搴旇矾寰剆rc/assets/icons/svg
+ breadcrumb: false // 濡傛灉璁剧疆涓篺alse锛屽垯涓嶄細鍦╞readcrumb闈㈠寘灞戜腑鏄剧ず
+ activeMenu: '/system/user' // 褰撹矾鐢辫缃簡璇ュ睘鎬э紝鍒欎細楂樹寒鐩稿搴旂殑渚ц竟鏍忋��
+ }
+ */
+
+// 鍏叡璺敱
+export const constantRoutes = [
+ {
+ path: "/redirect",
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: "/redirect/:path(.*)",
+ component: () => import("@/views/redirect/index.vue"),
+ },
+ ],
+ },
+ {
+ path: "/login",
+ component: () => import("@/views/login"),
+ hidden: true,
+ },
+ {
+ path: "/register",
+ component: () => import("@/views/register"),
+ hidden: true,
+ },
+ {
+ path: "/:pathMatch(.*)*",
+ component: () => import("@/views/error/404"),
+ hidden: true,
+ },
+ {
+ path: "/401",
+ component: () => import("@/views/error/401"),
+ hidden: true,
+ },
+
+ {
+ path: "",
+ component: Layout,
+ redirect: "/index",
+ children: [
+ {
+ path: "/index",
+ component: () => import("@/views/index"),
+ name: "Index",
+ meta: { title: "棣栭〉", icon: "dashboard", affix: true },
+ },
+ ],
+ },
+ // 绯荤粺鏋舵瀯鍥�
+ {
+ path: "/system-architecture",
+ component: Layout,
+ redirect: "/system-architecture/index",
+ children: [
+ {
+ path: "index",
+ component: () => import("@/views/systemArchitecture/index.vue"),
+ name: "SystemArchitecture",
+ meta: { title: "绯荤粺鏋舵瀯鍥�", icon: "tree" },
+ },
+ ],
+ },
+ {
+ path: "/ai-industrial-brain",
+ component: Layout,
+ children: [
+ {
+ path: "index",
+ component: () => import("@/views/aiIndustrialBrain/index.vue"),
+ name: "AiIndustrialBrain",
+ meta: { title: "AI宸ヤ笟澶ц剳", icon: "skill" },
+ },
+ ],
+ },
+ {
+ path: "/user",
+ component: Layout,
+ hidden: true,
+ redirect: "noredirect",
+ children: [
+ {
+ path: "profile",
+ component: () => import("@/views/system/user/profile/index"),
+ name: "Profile",
+ meta: { title: "涓汉涓績", icon: "user" },
+ },
+ ],
+ },
+ {
+ path: "/device-info",
+ component: () => import("@/views/equipmentManagement/deviceInfo/index.vue"),
+ hidden: true,
+ name: "DeviceInfo",
+ meta: { title: "璁惧淇℃伅", icon: "monitor" },
+ },
+ {
+ path: "/projectManagement/Management/detail",
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: ":id",
+ component: () => import("@/views/projectManagement/Management/projectDetail.vue"),
+ name: "ProjectManagementDetail",
+ meta: { title: "椤圭洰璇︽儏", activeMenu: "/projectManagement/Management" },
+ },
+ ],
+ },
+ // 璐㈠姟绠$悊妯″潡璺敱
+ // {
+ // path: "/financial",
+ // component: Layout,
+ // hidden: false,
+ // redirect: "/financial/general-ledger",
+ // alwaysShow: true,
+ // meta: { title: "璐㈠姟绠$悊", icon: "money" },
+ // children: [
+ // {
+ // path: "sales-out",
+ // component: () => import("@/views/financialManagement/receivable/salesOut.vue"),
+ // name: "SalesOut",
+ // meta: { title: "閿�鍞嚭搴�" },
+ // },
+ // {
+ // path: "sales-return",
+ // component: () => import("@/views/financialManagement/receivable/salesReturn.vue"),
+ // name: "SalesReturn",
+ // meta: { title: "閿�鍞��璐�" },
+ // },
+ //
+ // {
+ // path: "invoice-apply",
+ // component: () => import("@/views/financialManagement/receivable/invoiceApply.vue"),
+ // name: "InvoiceApply",
+ // meta: { title: "寮�绁ㄧ敵璇�" },
+ // },
+ // {
+ // path: "output-invoice",
+ // component: () => import("@/views/financialManagement/receivable/outputInvoice.vue"),
+ // name: "OutputInvoice",
+ // meta: { title: "閿�椤瑰彂绁�" },
+ // },
+ // {
+ // path: "receipt",
+ // component: () => import("@/views/financialManagement/receivable/receipt.vue"),
+ // name: "Receipt",
+ // meta: { title: "鏀舵鍗�" },
+ // },
+ // {
+ // path: "receivable-reconciliation",
+ // component: () => import("@/views/financialManagement/receivable/reconciliation.vue"),
+ // name: "ReceivableReconciliation",
+ // meta: { title: "搴旀敹瀵硅处" },
+ // },
+ // {
+ // path: "purchase-in",
+ // component: () => import("@/views/financialManagement/payable/purchaseIn.vue"),
+ // name: "PurchaseIn",
+ // meta: { title: "閲囪喘鍏ュ簱" },
+ // },
+ // {
+ // path: "purchase-return",
+ // component: () => import("@/views/financialManagement/payable/purchaseReturn.vue"),
+ // name: "PurchaseReturn",
+ // meta: { title: "閲囪喘閫�璐�" },
+ // },
+ // {
+ // path: "input-invoice",
+ // component: () => import("@/views/financialManagement/payable/input-invoice.vue"),
+ // name: "InputInvoice",
+ // meta: { title: "杩涢」鍙戠エ" },
+ // },
+ // {
+ // path: "payment-apply",
+ // component: () => import("@/views/financialManagement/payable/paymentApply.vue"),
+ // name: "PaymentApply",
+ // meta: { title: "浠樻鐢宠" },
+ // },
+ //
+ // {
+ // path: "payment",
+ // component: () => import("@/views/financialManagement/payable/payment.vue"),
+ // name: "Payment",
+ // meta: { title: "浠樻鍗�" },
+ // },
+ // {
+ // path: "payable-reconciliation",
+ // component: () => import("@/views/financialManagement/payable/reconciliation.vue"),
+ // name: "PayableReconciliation",
+ // meta: { title: "搴斾粯瀵硅处" },
+ // },
+ // {
+ // path: "fixed-assets",
+ // component: () => import("@/views/financialManagement/assets/fixedAssets.vue"),
+ // name: "FixedAssets",
+ // meta: { title: "鍥哄畾璧勪骇" },
+ // },
+ // {
+ // path: "intangible-assets",
+ // component: () => import("@/views/financialManagement/assets/intangibleAssets.vue"),
+ // name: "IntangibleAssets",
+ // meta: { title: "鏃犲舰璧勪骇" },
+ // },
+ // {
+ // path: "general-ledger",
+ // component: () => import("@/views/financialManagement/generalLedger/index.vue"),
+ // name: "GeneralLedger",
+ // meta: { title: "鎬诲笎绉戠洰" },
+ // },
+ // {
+ // path: "voucher",
+ // component: () => import("@/views/financialManagement/voucher/index.vue"),
+ // name: "Voucher",
+ // meta: { title: "鍑瘉" },
+ // },
+ // {
+ // path: "voucher-general-ledger",
+ // component: () => import("@/views/financialManagement/voucher/generalLedger.vue"),
+ // name: "VoucherGeneralLedger",
+ // meta: { title: "绉戠洰鎬诲笎" },
+ // },
+ // {
+ // path: "voucher-detail-ledger",
+ // component: () => import("@/views/financialManagement/voucher/detailLedger.vue"),
+ // name: "VoucherDetailLedger",
+ // meta: { title: "绉戠洰鏄庣粏甯�" },
+ // },
+ // ],
+ // },
+];
+
+// 鍔ㄦ�佽矾鐢憋紝鍩轰簬鐢ㄦ埛鏉冮檺鍔ㄦ�佸幓鍔犺浇
+export const dynamicRoutes = [
+ {
+ path: "/system/user-auth",
+ component: Layout,
+ hidden: true,
+ permissions: ["system:user:edit"],
+ children: [
+ {
+ path: "role/:userId(\\d+)",
+ component: () => import("@/views/system/user/authRole"),
+ name: "AuthRole",
+ meta: { title: "鍒嗛厤瑙掕壊", activeMenu: "/system/user" },
+ },
+ ],
+ },
+ {
+ path: "/system/role-auth",
+ component: Layout,
+ hidden: true,
+ permissions: ["system:role:edit"],
+ children: [
+ {
+ path: "user/:roleId(\\d+)",
+ component: () => import("@/views/system/role/authUser"),
+ name: "AuthUser",
+ meta: { title: "鍒嗛厤鐢ㄦ埛", activeMenu: "/system/role" },
+ },
+ ],
+ },
+ {
+ path: "/system/dict-data",
+ component: Layout,
+ hidden: true,
+ permissions: ["system:dict:list"],
+ children: [
+ {
+ path: "index/:dictId(\\d+)",
+ component: () => import("@/views/system/dict/data"),
+ name: "Data",
+ meta: { title: "瀛楀吀鏁版嵁", activeMenu: "/system/dict" },
+ },
+ ],
+ },
+ {
+ path: "/monitor/job-log",
+ component: Layout,
+ hidden: true,
+ permissions: ["monitor:job:list"],
+ children: [
+ {
+ path: "index/:jobId(\\d+)",
+ component: () => import("@/views/monitor/job/log"),
+ name: "JobLog",
+ meta: { title: "璋冨害鏃ュ織", activeMenu: "/monitor/job" },
+ },
+ ],
+ },
+ {
+ path: "/tool/gen-edit",
+ component: Layout,
+ hidden: true,
+ permissions: ["tool:gen:edit"],
+ children: [
+ {
+ path: "index/:tableId(\\d+)",
+ component: () => import("@/views/tool/gen/editTable"),
+ name: "GenEdit",
+ meta: { title: "淇敼鐢熸垚閰嶇疆", activeMenu: "/tool/gen" },
+ },
+ ],
+ },
+];
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: constantRoutes,
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return { top: 0 };
+ },
+});
+
+export default router;
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..276b2f6
--- /dev/null
+++ b/src/settings.js
@@ -0,0 +1,51 @@
+export default {
+ /**
+ * 缃戦〉鏍囬
+ */
+ title: import.meta.env.VITE_APP_TITLE,
+ /**
+ * 渚ц竟鏍忎富棰� 娣辫壊涓婚theme-dark锛屾祬鑹蹭富棰榯heme-light
+ */
+ sideTheme: 'theme-dark',
+ /**
+ * 鏄惁绯荤粺甯冨眬閰嶇疆
+ */
+ showSettings: true,
+
+ /**
+ * 鏄惁鏄剧ず椤堕儴瀵艰埅
+ */
+ topNav: false,
+
+ /**
+ * 鏄惁鏄剧ず tagsView
+ */
+ tagsView: true,
+
+ /**
+ * 鏄惁鍥哄畾澶撮儴
+ */
+ fixedHeader: true,
+
+ /**
+ * 鏄惁鏄剧ずlogo
+ */
+ sidebarLogo: true,
+
+ /**
+ * 鏄惁鏄剧ず鍔ㄦ�佹爣棰�
+ */
+ dynamicTitle: false,
+ /**
+ * 涓婚妯″紡 auto璺熼殢绯荤粺锛宭ight娴呰壊锛宒ark娣辫壊
+ */
+ darkMode: "light",
+
+ /**
+ * @type {string | array} 'production' | ['production', 'development']
+ * @description Need show err logs component.
+ * The default is only used in the production env
+ * If you want to also use it in dev, you can pass ['production', 'development']
+ */
+ errorLog: 'production'
+}
diff --git a/src/store/index.js b/src/store/index.js
new file mode 100644
index 0000000..cd73385
--- /dev/null
+++ b/src/store/index.js
@@ -0,0 +1,3 @@
+const store = createPinia()
+
+export default store
\ No newline at end of file
diff --git a/src/store/modules/app.js b/src/store/modules/app.js
new file mode 100644
index 0000000..71414be
--- /dev/null
+++ b/src/store/modules/app.js
@@ -0,0 +1,46 @@
+import Cookies from 'js-cookie'
+
+const useAppStore = defineStore(
+ 'app',
+ {
+ state: () => ({
+ sidebar: {
+ opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+ withoutAnimation: false,
+ hide: false
+ },
+ device: 'desktop',
+ size: Cookies.get('size') || 'default'
+ }),
+ actions: {
+ toggleSideBar(withoutAnimation) {
+ if (this.sidebar.hide) {
+ return false
+ }
+ this.sidebar.opened = !this.sidebar.opened
+ this.sidebar.withoutAnimation = withoutAnimation
+ if (this.sidebar.opened) {
+ Cookies.set('sidebarStatus', 1)
+ } else {
+ Cookies.set('sidebarStatus', 0)
+ }
+ },
+ closeSideBar({ withoutAnimation }) {
+ Cookies.set('sidebarStatus', 0)
+ this.sidebar.opened = false
+ this.sidebar.withoutAnimation = withoutAnimation
+ },
+ toggleDevice(device) {
+ this.device = device
+ },
+ setSize(size) {
+ this.size = size
+ Cookies.set('size', size)
+ },
+ toggleSideBarHide(status) {
+ this.sidebar.hide = status
+ }
+ }
+ })
+
+export default useAppStore
diff --git a/src/store/modules/dict.js b/src/store/modules/dict.js
new file mode 100644
index 0000000..af1a3fa
--- /dev/null
+++ b/src/store/modules/dict.js
@@ -0,0 +1,57 @@
+const useDictStore = defineStore(
+ 'dict',
+ {
+ state: () => ({
+ dict: new Array()
+ }),
+ actions: {
+ // 鑾峰彇瀛楀吀
+ getDict(_key) {
+ if (_key == null && _key == "") {
+ return null
+ }
+ try {
+ for (let i = 0; i < this.dict.length; i++) {
+ if (this.dict[i].key == _key) {
+ return this.dict[i].value
+ }
+ }
+ } catch (e) {
+ return null
+ }
+ },
+ // 璁剧疆瀛楀吀
+ setDict(_key, value) {
+ if (_key !== null && _key !== "") {
+ this.dict.push({
+ key: _key,
+ value: value
+ })
+ }
+ },
+ // 鍒犻櫎瀛楀吀
+ removeDict(_key) {
+ var bln = false
+ try {
+ for (let i = 0; i < this.dict.length; i++) {
+ if (this.dict[i].key == _key) {
+ this.dict.splice(i, 1)
+ return true
+ }
+ }
+ } catch (e) {
+ bln = false
+ }
+ return bln
+ },
+ // 娓呯┖瀛楀吀
+ cleanDict() {
+ this.dict = new Array()
+ },
+ // 鍒濆瀛楀吀
+ initDict() {
+ }
+ }
+ })
+
+export default useDictStore
diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js
new file mode 100644
index 0000000..0b11d2e
--- /dev/null
+++ b/src/store/modules/permission.js
@@ -0,0 +1,163 @@
+import auth from '@/plugins/auth'
+import router, { constantRoutes, dynamicRoutes } from '@/router'
+import { getRouters } from '@/api/menu'
+import Layout from '@/layout/index'
+import ParentView from '@/components/ParentView'
+import InnerLink from '@/layout/components/InnerLink'
+import useUserStore from '@/store/modules/user'
+
+// 鍖归厤views閲岄潰鎵�鏈夌殑.vue鏂囦欢
+const modules = import.meta.glob('./../../views/**/*.vue')
+
+const usePermissionStore = defineStore(
+ 'permission',
+ {
+ state: () => ({
+ routes: [],
+ addRoutes: [],
+ defaultRoutes: [],
+ topbarRouters: [],
+ sidebarRouters: []
+ }),
+ actions: {
+ setRoutes(routes) {
+ this.addRoutes = routes
+ this.routes = constantRoutes.concat(routes)
+ },
+ setDefaultRoutes(routes) {
+ this.defaultRoutes = constantRoutes.concat(routes)
+ },
+ setTopbarRoutes(routes) {
+ this.topbarRouters = routes
+ },
+ setSidebarRouters(routes) {
+ this.sidebarRouters = routes
+ },
+ generateRoutes(roles) {
+ return new Promise(resolve => {
+ // 鍚戝悗绔姹傝矾鐢辨暟鎹�
+ getRouters().then(res => {
+ const aiEnabled = Number(useUserStore().aiEnabled) === 1
+ const rawRoutes = filterAiFeatureRoutes(res.data, aiEnabled)
+ const sdata = JSON.parse(JSON.stringify(rawRoutes))
+ const rdata = JSON.parse(JSON.stringify(rawRoutes))
+ const defaultData = JSON.parse(JSON.stringify(rawRoutes))
+ const sidebarRoutes = filterAsyncRouter(sdata)
+ const rewriteRoutes = filterAsyncRouter(rdata, false, true)
+ const defaultRoutes = filterAsyncRouter(defaultData)
+ const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
+ asyncRoutes.forEach(route => { router.addRoute(route) })
+ this.setRoutes(rewriteRoutes)
+ const constantSidebarRoutes = filterAiFeatureRoutes(constantRoutes, aiEnabled)
+ // 灏嗚储鍔$鐞嗚矾鐢卞悎骞跺埌渚ц竟鏍�
+ this.setSidebarRouters(constantSidebarRoutes.concat(sidebarRoutes))
+ this.setDefaultRoutes(sidebarRoutes)
+ this.setTopbarRoutes(defaultRoutes)
+ resolve(rewriteRoutes)
+ })
+ })
+ }
+ }
+ })
+
+// 閬嶅巻鍚庡彴浼犳潵鐨勮矾鐢卞瓧绗︿覆锛岃浆鎹负缁勪欢瀵硅薄
+function filterAiFeatureRoutes(routes = [], aiEnabled = false) {
+ if (aiEnabled) {
+ return routes
+ }
+ return routes.reduce((acc, route) => {
+ if (!route || isAiFeatureRoute(route)) {
+ return acc
+ }
+ const nextRoute = { ...route }
+ if (Array.isArray(nextRoute.children) && nextRoute.children.length > 0) {
+ nextRoute.children = filterAiFeatureRoutes(nextRoute.children, aiEnabled)
+ }
+ acc.push(nextRoute)
+ return acc
+ }, [])
+}
+
+function isAiFeatureRoute(route = {}) {
+ const path = String(route.path || '').toLowerCase()
+ const component = String(route.component || '').toLowerCase()
+ const name = String(route.name || '').toLowerCase()
+ const title = String(route?.meta?.title ?? route?.title ?? '')
+
+ return (
+ path.includes('chathome') ||
+ component.includes('chathome') ||
+ name.includes('chathome') ||
+ title.includes('AI')
+ )
+}
+
+function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
+ return asyncRouterMap.filter(route => {
+ if (type && route.children) {
+ route.children = filterChildren(route.children)
+ }
+ if (route.component) {
+ // Layout ParentView 缁勪欢鐗规畩澶勭悊
+ if (route.component === 'Layout') {
+ route.component = Layout
+ } else if (route.component === 'ParentView') {
+ route.component = ParentView
+ } else if (route.component === 'InnerLink') {
+ route.component = InnerLink
+ } else {
+ route.component = loadView(route.component)
+ }
+ }
+ if (route.children != null && route.children && route.children.length) {
+ route.children = filterAsyncRouter(route.children, route, type)
+ } else {
+ delete route['children']
+ delete route['redirect']
+ }
+ return true
+ })
+}
+
+function filterChildren(childrenMap, lastRouter = false) {
+ var children = []
+ childrenMap.forEach(el => {
+ el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
+ if (el.children && el.children.length && el.component === 'ParentView') {
+ children = children.concat(filterChildren(el.children, el))
+ } else {
+ children.push(el)
+ }
+ })
+ return children
+}
+
+// 鍔ㄦ�佽矾鐢遍亶鍘嗭紝楠岃瘉鏄惁鍏峰鏉冮檺
+export function filterDynamicRoutes(routes) {
+ const res = []
+ routes.forEach(route => {
+ if (route.permissions) {
+ if (auth.hasPermiOr(route.permissions)) {
+ res.push(route)
+ }
+ } else if (route.roles) {
+ if (auth.hasRoleOr(route.roles)) {
+ res.push(route)
+ }
+ }
+ })
+ return res
+}
+
+export const loadView = (view) => {
+ let res
+ for (const path in modules) {
+ const dir = path.split('views/')[1].split('.vue')[0]
+ if (dir === view) {
+ res = () => modules[path]()
+ }
+ }
+ return res
+}
+
+export default usePermissionStore
diff --git a/src/store/modules/settings.js b/src/store/modules/settings.js
new file mode 100644
index 0000000..3a5f6a2
--- /dev/null
+++ b/src/store/modules/settings.js
@@ -0,0 +1,105 @@
+import defaultSettings from "@/settings";
+import { useColorMode, usePreferredDark } from "@vueuse/core";
+import { useDynamicTitle } from "@/utils/dynamicTitle";
+
+const preferredDark = usePreferredDark();
+const colorMode = useColorMode({
+ emitAuto: true,
+});
+
+const {
+ sideTheme,
+ showSettings,
+ topNav,
+ tagsView,
+ fixedHeader,
+ sidebarLogo,
+ dynamicTitle,
+ darkMode,
+} = defaultSettings;
+
+const storageSetting = JSON.parse(localStorage.getItem("layout-setting") || "{}");
+const defaultDarkMode = darkMode || "auto";
+const initialDarkMode = storageSetting.darkMode || defaultDarkMode;
+colorMode.value = initialDarkMode;
+const getIsDark = (mode) => mode === "dark" || (mode === "auto" && preferredDark.value);
+
+const useSettingsStore = defineStore("settings", () => {
+ const title = ref("");
+ const theme = ref(storageSetting.theme || "#002fa7");
+ const sideThemeValue = ref(storageSetting.sideTheme || sideTheme);
+ const showSettingsValue = ref(showSettings);
+ const topNavValue = ref(
+ storageSetting.topNav === undefined ? topNav : storageSetting.topNav
+ );
+ const tagsViewValue = ref(
+ storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView
+ );
+ const fixedHeaderValue = ref(
+ storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader
+ );
+ const sidebarLogoValue = ref(
+ storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo
+ );
+ const dynamicTitleValue = ref(
+ storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
+ );
+ const darkModeValue = ref(initialDarkMode);
+ const isDark = computed(() => getIsDark(darkModeValue.value));
+
+ function changeSetting(data) {
+ const { key, value } = data;
+ const settingMap = {
+ title,
+ theme,
+ sideTheme: sideThemeValue,
+ showSettings: showSettingsValue,
+ topNav: topNavValue,
+ tagsView: tagsViewValue,
+ fixedHeader: fixedHeaderValue,
+ sidebarLogo: sidebarLogoValue,
+ dynamicTitle: dynamicTitleValue,
+ darkMode: darkModeValue,
+ };
+ if (Object.prototype.hasOwnProperty.call(settingMap, key)) {
+ settingMap[key].value = value;
+ if (key === "darkMode") {
+ colorMode.value = value;
+ }
+ }
+ }
+
+ function setTitle(value) {
+ title.value = value;
+ useDynamicTitle();
+ }
+
+ function setDarkMode(mode) {
+ darkModeValue.value = mode;
+ colorMode.value = mode;
+ }
+
+ function toggleTheme() {
+ setDarkMode(isDark.value ? "light" : "dark");
+ }
+
+ return {
+ title,
+ theme,
+ sideTheme: sideThemeValue,
+ showSettings: showSettingsValue,
+ topNav: topNavValue,
+ tagsView: tagsViewValue,
+ fixedHeader: fixedHeaderValue,
+ sidebarLogo: sidebarLogoValue,
+ dynamicTitle: dynamicTitleValue,
+ darkMode: darkModeValue,
+ isDark,
+ changeSetting,
+ setTitle,
+ setDarkMode,
+ toggleTheme,
+ };
+});
+
+export default useSettingsStore;
diff --git a/src/store/modules/tagsView.js b/src/store/modules/tagsView.js
new file mode 100644
index 0000000..b0d4ca1
--- /dev/null
+++ b/src/store/modules/tagsView.js
@@ -0,0 +1,182 @@
+const useTagsViewStore = defineStore(
+ 'tags-view',
+ {
+ state: () => ({
+ visitedViews: [],
+ cachedViews: [],
+ iframeViews: []
+ }),
+ actions: {
+ addView(view) {
+ this.addVisitedView(view)
+ this.addCachedView(view)
+ },
+ addIframeView(view) {
+ if (this.iframeViews.some(v => v.path === view.path)) return
+ this.iframeViews.push(
+ Object.assign({}, view, {
+ title: view.meta.title || 'no-name'
+ })
+ )
+ },
+ addVisitedView(view) {
+ if (this.visitedViews.some(v => v.path === view.path)) return
+ this.visitedViews.push(
+ Object.assign({}, view, {
+ title: view.meta.title || 'no-name'
+ })
+ )
+ },
+ addCachedView(view) {
+ if (this.cachedViews.includes(view.name)) return
+ if (!view.meta.noCache) {
+ this.cachedViews.push(view.name)
+ }
+ },
+ delView(view) {
+ return new Promise(resolve => {
+ this.delVisitedView(view)
+ this.delCachedView(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delVisitedView(view) {
+ return new Promise(resolve => {
+ for (const [i, v] of this.visitedViews.entries()) {
+ if (v.path === view.path) {
+ this.visitedViews.splice(i, 1)
+ break
+ }
+ }
+ this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
+ resolve([...this.visitedViews])
+ })
+ },
+ delIframeView(view) {
+ return new Promise(resolve => {
+ this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
+ resolve([...this.iframeViews])
+ })
+ },
+ delCachedView(view) {
+ return new Promise(resolve => {
+ const index = this.cachedViews.indexOf(view.name)
+ index > -1 && this.cachedViews.splice(index, 1)
+ resolve([...this.cachedViews])
+ })
+ },
+ delOthersViews(view) {
+ return new Promise(resolve => {
+ this.delOthersVisitedViews(view)
+ this.delOthersCachedViews(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delOthersVisitedViews(view) {
+ return new Promise(resolve => {
+ this.visitedViews = this.visitedViews.filter(v => {
+ return v.meta.affix || v.path === view.path
+ })
+ this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
+ resolve([...this.visitedViews])
+ })
+ },
+ delOthersCachedViews(view) {
+ return new Promise(resolve => {
+ const index = this.cachedViews.indexOf(view.name)
+ if (index > -1) {
+ this.cachedViews = this.cachedViews.slice(index, index + 1)
+ } else {
+ this.cachedViews = []
+ }
+ resolve([...this.cachedViews])
+ })
+ },
+ delAllViews(view) {
+ return new Promise(resolve => {
+ this.delAllVisitedViews(view)
+ this.delAllCachedViews(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delAllVisitedViews(view) {
+ return new Promise(resolve => {
+ const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
+ this.visitedViews = affixTags
+ this.iframeViews = []
+ resolve([...this.visitedViews])
+ })
+ },
+ delAllCachedViews(view) {
+ return new Promise(resolve => {
+ this.cachedViews = []
+ resolve([...this.cachedViews])
+ })
+ },
+ updateVisitedView(view) {
+ for (let v of this.visitedViews) {
+ if (v.path === view.path) {
+ v = Object.assign(v, view)
+ break
+ }
+ }
+ },
+ delRightTags(view) {
+ return new Promise(resolve => {
+ const index = this.visitedViews.findIndex(v => v.path === view.path)
+ if (index === -1) {
+ return
+ }
+ this.visitedViews = this.visitedViews.filter((item, idx) => {
+ if (idx <= index || (item.meta && item.meta.affix)) {
+ return true
+ }
+ const i = this.cachedViews.indexOf(item.name)
+ if (i > -1) {
+ this.cachedViews.splice(i, 1)
+ }
+ if(item.meta.link) {
+ const fi = this.iframeViews.findIndex(v => v.path === item.path)
+ this.iframeViews.splice(fi, 1)
+ }
+ return false
+ })
+ resolve([...this.visitedViews])
+ })
+ },
+ delLeftTags(view) {
+ return new Promise(resolve => {
+ const index = this.visitedViews.findIndex(v => v.path === view.path)
+ if (index === -1) {
+ return
+ }
+ this.visitedViews = this.visitedViews.filter((item, idx) => {
+ if (idx >= index || (item.meta && item.meta.affix)) {
+ return true
+ }
+ const i = this.cachedViews.indexOf(item.name)
+ if (i > -1) {
+ this.cachedViews.splice(i, 1)
+ }
+ if(item.meta.link) {
+ const fi = this.iframeViews.findIndex(v => v.path === item.path)
+ this.iframeViews.splice(fi, 1)
+ }
+ return false
+ })
+ resolve([...this.visitedViews])
+ })
+ }
+ }
+ })
+
+export default useTagsViewStore
diff --git a/src/store/modules/user.js b/src/store/modules/user.js
new file mode 100644
index 0000000..0d2e61e
--- /dev/null
+++ b/src/store/modules/user.js
@@ -0,0 +1,150 @@
+import {login, logout, getInfo, loginCheck, loginCheckFactory,tideLogin} from '@/api/login'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import { isHttp, isEmpty } from "@/utils/validate"
+import defAva from '@/assets/images/profile.jpg'
+import { defineStore } from 'pinia'
+
+const useUserStore = defineStore(
+ 'user',
+ {
+ state: () => ({
+ token: getToken(),
+ id: '',
+ name: '',
+ avatar: '',
+ roles: [],
+ permissions: [],
+ aiEnabled: 0
+ }),
+ actions: {
+ // 鐧诲綍
+ login(userInfo) {
+ const username = userInfo.username.trim()
+ const password = userInfo.password
+ const code = userInfo.code
+ const uuid = userInfo.uuid
+ return new Promise((resolve, reject) => {
+ login(username, password, code, uuid).then(res => {
+ const token = res?.token || res?.data?.token
+ if (!token) {
+ reject(new Error('鏈幏鍙栧埌鐧诲綍浠ょ墝'))
+ return
+ }
+ setToken(token)
+ this.token = token
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ getCurrentTime() {
+ const now = new Date();
+ const year = now.getFullYear(); // 鑾峰彇骞翠唤
+ const month = String(now.getMonth() + 1).padStart(2, '0'); // 鏈堜唤浠�0寮�濮嬶紝瑕�+1锛屽苟琛ラ浂
+ const day = String(now.getDate()).padStart(2, '0'); // 鏃ユ湡琛ラ浂
+ const hours = String(now.getHours()).padStart(2, '0'); // 灏忔椂琛ラ浂
+ const minutes = String(now.getMinutes()).padStart(2, '0'); // 鍒嗛挓琛ラ浂
+ const seconds = String(now.getSeconds()).padStart(2, '0'); // 绉掓暟琛ラ浂
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+ },
+ // 鑾峰彇鐢ㄦ埛淇℃伅
+ getInfo() {
+ return new Promise((resolve, reject) => {
+ getInfo().then(res => {
+ const data = res?.data ?? res
+ const user = data.user || {}
+ let avatar = user.avatar || ""
+ avatar = import.meta.env.VITE_APP_BASE_API + '/profile/' + avatar
+ if (data.roles && data.roles.length > 0) { // 楠岃瘉杩斿洖鐨剅oles鏄惁鏄竴涓潪绌烘暟缁�
+ this.roles = data.roles
+ this.permissions = data.permissions
+ } else {
+ this.roles = ['ROLE_DEFAULT']
+ }
+ this.id = user.userId || ''
+ this.name = user.userName || ''
+ this.avatar = avatar
+ this.currentFactoryName = user.currentFactoryName || ''
+ this.nickName = user.nickName || ''
+ this.roleName = Array.isArray(user.roles) && user.roles.length > 0 ? (user.roles[0].roleName || '') : ''
+ this.currentDeptId = user.tenantId || ''
+ this.currentLoginTime = this.getCurrentTime()
+ this.aiEnabled = Number(data.aiEnabled) === 1 ? 1 : 0
+ resolve(data)
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ // 閫�鍑虹郴缁�
+ logOut() {
+ return new Promise((resolve, reject) => {
+ logout(this.token).then(() => {
+ this.token = ''
+ this.roles = []
+ this.permissions = []
+ this.aiEnabled = 0
+ removeToken()
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ // 鐧诲綍鏍¢獙
+ loginCheck(userInfo) {
+ const username = userInfo.username.trim()
+ const password = userInfo.password
+ return new Promise((resolve, reject) => {
+ loginCheck(username, password).then(res => {
+ resolve(res)
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ // 閮ㄩ棬鐧诲綍
+ loginCheckFactory(userInfo) {
+ const username = userInfo.username.trim()
+ const password = userInfo.password
+ return new Promise((resolve, reject) => {
+ loginCheckFactory(username, password).then(res => {
+ const token = res?.token || res?.data?.token
+ if (!token) {
+ reject(new Error('鏈幏鍙栧埌鐧诲綍浠ょ墝'))
+ return
+ }
+ setToken(token)
+ this.token = token
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ TideLogin(code) {
+ return new Promise((resolve, reject) => {
+ tideLogin(code)
+ .then((res) => {
+ const token = res?.token || res?.data?.token
+ if (!token) {
+ reject(new Error('鏈幏鍙栧埌鐧诲綍浠ょ墝'))
+ return
+ }
+ setToken(token);
+ this.token = token
+ Vue.prototype.uploadHeader = {
+ Authorization: "Bearer " + token,
+ };
+ resolve();
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ },
+ }
+ })
+
+export default useUserStore
diff --git a/src/utils/auth.js b/src/utils/auth.js
new file mode 100644
index 0000000..88d7b6c
--- /dev/null
+++ b/src/utils/auth.js
@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'Admin-Token'
+
+export function getToken() {
+ return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+ return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+ return Cookies.remove(TokenKey)
+}
diff --git a/src/utils/dict.js b/src/utils/dict.js
new file mode 100644
index 0000000..a6db068
--- /dev/null
+++ b/src/utils/dict.js
@@ -0,0 +1,24 @@
+import useDictStore from '@/store/modules/dict'
+import { getDicts } from '@/api/system/dict/data'
+
+/**
+ * 鑾峰彇瀛楀吀鏁版嵁
+ */
+export function useDict(...args) {
+ const res = ref({})
+ return (() => {
+ args.forEach((dictType, index) => {
+ res.value[dictType] = []
+ const dicts = useDictStore().getDict(dictType)
+ if (dicts) {
+ res.value[dictType] = dicts
+ } else {
+ getDicts(dictType).then(resp => {
+ res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
+ useDictStore().setDict(dictType, res.value[dictType])
+ })
+ }
+ })
+ return toRefs(res.value)
+ })()
+}
\ No newline at end of file
diff --git a/src/utils/dynamicTitle.js b/src/utils/dynamicTitle.js
new file mode 100644
index 0000000..2005758
--- /dev/null
+++ b/src/utils/dynamicTitle.js
@@ -0,0 +1,15 @@
+import store from '@/store'
+import defaultSettings from '@/settings'
+import useSettingsStore from '@/store/modules/settings'
+
+/**
+ * 鍔ㄦ�佷慨鏀规爣棰�
+ */
+export function useDynamicTitle() {
+ const settingsStore = useSettingsStore()
+ if (settingsStore.dynamicTitle) {
+ document.title = settingsStore.title + ' - ' + defaultSettings.title
+ } else {
+ document.title = defaultSettings.title
+ }
+}
\ No newline at end of file
diff --git a/src/utils/errorCode.js b/src/utils/errorCode.js
new file mode 100644
index 0000000..b72d026
--- /dev/null
+++ b/src/utils/errorCode.js
@@ -0,0 +1,6 @@
+export default {
+ '401': '璁よ瘉澶辫触锛屾棤娉曡闂郴缁熻祫婧�',
+ '403': '褰撳墠鎿嶄綔娌℃湁鏉冮檺',
+ '404': '璁块棶璧勬簮涓嶅瓨鍦�',
+ 'default': '绯荤粺鏈煡閿欒锛岃鍙嶉缁欑鐞嗗憳'
+}
diff --git a/src/utils/generator/config.js b/src/utils/generator/config.js
new file mode 100644
index 0000000..449715f
--- /dev/null
+++ b/src/utils/generator/config.js
@@ -0,0 +1,452 @@
+export const formConf = {
+ formRef: 'formRef',
+ formModel: 'formData',
+ size: 'default',
+ labelPosition: 'right',
+ labelWidth: 100,
+ formRules: 'rules',
+ gutter: 15,
+ disabled: false,
+ span: 24,
+ formBtns: true,
+}
+
+export const inputComponents = [
+ {
+ label: '鍗曡鏂囨湰',
+ tag: 'el-input',
+ tagIcon: 'input',
+ type: 'text',
+ placeholder: '璇疯緭鍏�',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ clearable: true,
+ prepend: '',
+ append: '',
+ 'prefix-icon': '',
+ 'suffix-icon': '',
+ maxlength: null,
+ 'show-word-limit': false,
+ readonly: false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input',
+ },
+ {
+ label: '澶氳鏂囨湰',
+ tag: 'el-input',
+ tagIcon: 'textarea',
+ type: 'textarea',
+ placeholder: '璇疯緭鍏�',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ autosize: {
+ minRows: 4,
+ maxRows: 4,
+ },
+ style: { width: '100%' },
+ maxlength: null,
+ 'show-word-limit': false,
+ readonly: false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input',
+ },
+ {
+ label: '瀵嗙爜',
+ tag: 'el-input',
+ tagIcon: 'password',
+ type: 'password',
+ placeholder: '璇疯緭鍏�',
+ defaultValue: undefined,
+ span: 24,
+ 'show-password': true,
+ labelWidth: null,
+ style: { width: '100%' },
+ clearable: true,
+ prepend: '',
+ append: '',
+ 'prefix-icon': '',
+ 'suffix-icon': '',
+ maxlength: null,
+ 'show-word-limit': false,
+ readonly: false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input',
+ },
+ {
+ label: '璁℃暟鍣�',
+ tag: 'el-input-number',
+ tagIcon: 'number',
+ placeholder: '',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ min: undefined,
+ max: undefined,
+ step: undefined,
+ 'step-strictly': false,
+ precision: undefined,
+ 'controls-position': '',
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/input-number',
+ },
+]
+
+export const selectComponents = [
+ {
+ label: '涓嬫媺閫夋嫨',
+ tag: 'el-select',
+ tagIcon: 'select',
+ placeholder: '璇烽�夋嫨',
+ defaultValue: undefined,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ clearable: true,
+ disabled: false,
+ required: true,
+ filterable: false,
+ multiple: false,
+ options: [
+ {
+ label: '閫夐」涓�',
+ value: 1,
+ },
+ {
+ label: '閫夐」浜�',
+ value: 2,
+ },
+ ],
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/select',
+ },
+ {
+ label: '绾ц仈閫夋嫨',
+ tag: 'el-cascader',
+ tagIcon: 'cascader',
+ placeholder: '璇烽�夋嫨',
+ defaultValue: [],
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ props: {
+ props: {
+ multiple: false,
+ },
+ },
+ 'show-all-levels': true,
+ disabled: false,
+ clearable: true,
+ filterable: false,
+ required: true,
+ options: [
+ {
+ id: 1,
+ value: 1,
+ label: '閫夐」1',
+ children: [
+ {
+ id: 2,
+ value: 2,
+ label: '閫夐」1-1',
+ },
+ ],
+ },
+ ],
+ dataType: 'dynamic',
+ labelKey: 'label',
+ valueKey: 'value',
+ childrenKey: 'children',
+ separator: '/',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/cascader',
+ },
+ {
+ label: '鍗曢�夋缁�',
+ tag: 'el-radio-group',
+ tagIcon: 'radio',
+ defaultValue: 0,
+ span: 24,
+ labelWidth: null,
+ style: {},
+ optionType: 'default',
+ border: false,
+ size: 'default',
+ disabled: false,
+ required: true,
+ options: [
+ {
+ label: '閫夐」涓�',
+ value: 1,
+ },
+ {
+ label: '閫夐」浜�',
+ value: 2,
+ },
+ ],
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/radio',
+ },
+ {
+ label: '澶氶�夋缁�',
+ tag: 'el-checkbox-group',
+ tagIcon: 'checkbox',
+ defaultValue: [],
+ span: 24,
+ labelWidth: null,
+ style: {},
+ optionType: 'default',
+ border: false,
+ size: 'default',
+ disabled: false,
+ required: true,
+ options: [
+ {
+ label: '閫夐」涓�',
+ value: 1,
+ },
+ {
+ label: '閫夐」浜�',
+ value: 2,
+ },
+ ],
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/checkbox',
+ },
+ {
+ label: '寮�鍏�',
+ tag: 'el-switch',
+ tagIcon: 'switch',
+ defaultValue: false,
+ span: 24,
+ labelWidth: null,
+ style: {},
+ disabled: false,
+ required: true,
+ 'active-text': '',
+ 'inactive-text': '',
+ 'active-color': null,
+ 'inactive-color': null,
+ 'active-value': true,
+ 'inactive-value': false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/switch',
+ },
+ {
+ label: '婊戝潡',
+ tag: 'el-slider',
+ tagIcon: 'slider',
+ defaultValue: null,
+ span: 24,
+ labelWidth: null,
+ disabled: false,
+ required: true,
+ min: 0,
+ max: 100,
+ step: 1,
+ 'show-stops': false,
+ range: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/slider',
+ },
+ {
+ label: '鏃堕棿閫夋嫨',
+ tag: 'el-time-picker',
+ tagIcon: 'time',
+ placeholder: '璇烽�夋嫨',
+ defaultValue: '',
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ disabled: false,
+ clearable: true,
+ required: true,
+ format: 'HH:mm:ss',
+ 'value-format': 'HH:mm:ss',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/time-picker',
+ },
+ {
+ label: '鏃堕棿鑼冨洿',
+ tag: 'el-time-picker',
+ tagIcon: 'time-range',
+ defaultValue: null,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ disabled: false,
+ clearable: true,
+ required: true,
+ 'is-range': true,
+ 'range-separator': '鑷�',
+ 'start-placeholder': '寮�濮嬫椂闂�',
+ 'end-placeholder': '缁撴潫鏃堕棿',
+ format: 'HH:mm:ss',
+ 'value-format': 'HH:mm:ss',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/time-picker',
+ },
+ {
+ label: '鏃ユ湡閫夋嫨',
+ tag: 'el-date-picker',
+ tagIcon: 'date',
+ placeholder: '璇烽�夋嫨',
+ defaultValue: null,
+ type: 'date',
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ disabled: false,
+ clearable: true,
+ required: true,
+ format: 'YYYY-MM-DD',
+ 'value-format': 'YYYY-MM-DD',
+ readonly: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/date-picker',
+ },
+ {
+ label: '鏃ユ湡鑼冨洿',
+ tag: 'el-date-picker',
+ tagIcon: 'date-range',
+ defaultValue: null,
+ span: 24,
+ labelWidth: null,
+ style: { width: '100%' },
+ type: 'daterange',
+ 'range-separator': '鑷�',
+ 'start-placeholder': '寮�濮嬫棩鏈�',
+ 'end-placeholder': '缁撴潫鏃ユ湡',
+ disabled: false,
+ clearable: true,
+ required: true,
+ format: 'YYYY-MM-DD',
+ 'value-format': 'YYYY-MM-DD',
+ readonly: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/date-picker',
+ },
+ {
+ label: '璇勫垎',
+ tag: 'el-rate',
+ tagIcon: 'rate',
+ defaultValue: 0,
+ span: 24,
+ labelWidth: null,
+ style: {},
+ max: 5,
+ 'allow-half': false,
+ 'show-text': false,
+ 'show-score': false,
+ disabled: false,
+ required: true,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/rate',
+ },
+ {
+ label: '棰滆壊閫夋嫨',
+ tag: 'el-color-picker',
+ tagIcon: 'color',
+ defaultValue: null,
+ labelWidth: null,
+ 'show-alpha': false,
+ 'color-format': '',
+ disabled: false,
+ required: true,
+ size: 'default',
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/color-picker',
+ },
+ {
+ label: '涓婁紶',
+ tag: 'el-upload',
+ tagIcon: 'upload',
+ action: 'https://jsonplaceholder.typicode.com/posts/',
+ defaultValue: null,
+ labelWidth: null,
+ disabled: false,
+ required: true,
+ accept: '',
+ name: 'file',
+ 'auto-upload': true,
+ showTip: false,
+ buttonText: '鐐瑰嚮涓婁紶',
+ fileSize: 2,
+ sizeUnit: 'MB',
+ 'list-type': 'text',
+ multiple: false,
+ regList: [],
+ changeTag: true,
+ document: 'https://element-plus.org/zh-CN/component/upload',
+ tip: '鍙兘涓婁紶涓嶈秴杩� 2MB 鐨勬枃浠�',
+ style: { width: '100%' },
+ },
+]
+
+export const layoutComponents = [
+ {
+ layout: 'rowFormItem',
+ tagIcon: 'row',
+ type: 'default',
+ justify: 'start',
+ align: 'top',
+ label: '琛屽鍣�',
+ layoutTree: true,
+ children: [],
+ document: 'https://element-plus.org/zh-CN/component/layout',
+ },
+ {
+ layout: 'colFormItem',
+ label: '鎸夐挳',
+ changeTag: true,
+ labelWidth: null,
+ tag: 'el-button',
+ tagIcon: 'button',
+ span: 24,
+ default: '涓昏鎸夐挳',
+ type: 'primary',
+ icon: 'Search',
+ size: 'default',
+ disabled: false,
+ document: 'https://element-plus.org/zh-CN/component/button',
+ },
+]
+
+// 缁勪欢rule鐨勮Е鍙戞柟寮忥紝鏃犺Е鍙戞柟寮忕殑缁勪欢涓嶇敓鎴恟ule
+export const trigger = {
+ 'el-input': 'blur',
+ 'el-input-number': 'blur',
+ 'el-select': 'change',
+ 'el-radio-group': 'change',
+ 'el-checkbox-group': 'change',
+ 'el-cascader': 'change',
+ 'el-time-picker': 'change',
+ 'el-date-picker': 'change',
+ 'el-rate': 'change',
+}
diff --git a/src/utils/generator/css.js b/src/utils/generator/css.js
new file mode 100644
index 0000000..c1c62e6
--- /dev/null
+++ b/src/utils/generator/css.js
@@ -0,0 +1,18 @@
+const styles = {
+ 'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}',
+ 'el-upload': '.el-upload__tip{line-height: 1.2;}'
+}
+
+function addCss(cssList, el) {
+ const css = styles[el.tag]
+ css && cssList.indexOf(css) === -1 && cssList.push(css)
+ if (el.children) {
+ el.children.forEach(el2 => addCss(cssList, el2))
+ }
+}
+
+export function makeUpCss(conf) {
+ const cssList = []
+ conf.fields.forEach(el => addCss(cssList, el))
+ return cssList.join('\n')
+}
diff --git a/src/utils/generator/drawingDefalut.js b/src/utils/generator/drawingDefalut.js
new file mode 100644
index 0000000..1105c28
--- /dev/null
+++ b/src/utils/generator/drawingDefalut.js
@@ -0,0 +1,29 @@
+export default [
+ {
+ layout: 'colFormItem',
+ tagIcon: 'input',
+ label: '鎵嬫満鍙�',
+ vModel: 'mobile',
+ formId: 6,
+ tag: 'el-input',
+ placeholder: '璇疯緭鍏ユ墜鏈哄彿',
+ defaultValue: '',
+ span: 24,
+ style: { width: '100%' },
+ clearable: true,
+ prepend: '',
+ append: '',
+ 'prefix-icon': 'Cellphone',
+ 'suffix-icon': '',
+ maxlength: 11,
+ 'show-word-limit': true,
+ readonly: false,
+ disabled: false,
+ required: true,
+ changeTag: true,
+ regList: [{
+ pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
+ message: '鎵嬫満鍙锋牸寮忛敊璇�'
+ }]
+ }
+]
diff --git a/src/utils/generator/html.js b/src/utils/generator/html.js
new file mode 100644
index 0000000..eacb48e
--- /dev/null
+++ b/src/utils/generator/html.js
@@ -0,0 +1,359 @@
+/* eslint-disable max-len */
+import { trigger } from './config'
+
+let confGlobal
+let someSpanIsNot24
+
+export function dialogWrapper(str) {
+ return `<el-dialog v-model="dialogVisible" @open="onOpen" @close="onClose" title="Dialog Titile">
+ ${str}
+ <template #footer>
+ <el-button type="primary" @click="handelConfirm">纭畾</el-button>
+ <el-button @click="close">鍙栨秷</el-button>
+ </template>
+ </el-dialog>`
+}
+
+export function vueTemplate(str) {
+ return `<template>
+ <div class="app-container">
+ ${str}
+ </div>
+ </template>`
+}
+
+export function vueScript(str) {
+ return `<script setup>
+ ${str}
+ </script>`
+}
+
+export function cssStyle(cssStr) {
+ return `<style>
+ ${cssStr}
+ </style>`
+}
+
+function buildFormTemplate(conf, child, type) {
+ let labelPosition = ''
+ if (conf.labelPosition !== 'right') {
+ labelPosition = `label-position="${conf.labelPosition}"`
+ }
+ const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : ''
+ let str = `<el-form ref="${conf.formRef}" :model="${conf.formModel}" :rules="${conf.formRules}" size="${conf.size}" ${disabled} label-width="${conf.labelWidth}px" ${labelPosition}>
+ ${child}
+ ${buildFromBtns(conf, type)}
+ </el-form>`
+ if (someSpanIsNot24) {
+ str = `<el-row :gutter="${conf.gutter}">
+ ${str}
+ </el-row>`
+ }
+ return str
+}
+
+function buildFromBtns(conf, type) {
+ let str = ''
+ if (conf.formBtns && type === 'file') {
+ str = `<el-form-item>
+ <el-button type="primary" @click="submitForm">鎻愪氦</el-button>
+ <el-button @click="resetForm">閲嶇疆</el-button>
+ </el-form-item>`
+ if (someSpanIsNot24) {
+ str = `<el-col :span="24">
+ ${str}
+ </el-col>`
+ }
+ }
+ return str
+}
+
+// span涓嶄负24鐨勭敤el-col鍖呰9
+function colWrapper(element, str) {
+ if (someSpanIsNot24 || element.span !== 24) {
+ return `<el-col :span="${element.span}">
+ ${str}
+ </el-col>`
+ }
+ return str
+}
+
+const layouts = {
+ colFormItem(element) {
+ let labelWidth = ''
+ if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) {
+ labelWidth = `label-width="${element.labelWidth}px"`
+ }
+ const required = !trigger[element.tag] && element.required ? 'required' : ''
+ const tagDom = tags[element.tag] ? tags[element.tag](element) : null
+ let str = `<el-form-item ${labelWidth} label="${element.label}" prop="${element.vModel}" ${required}>
+ ${tagDom}
+ </el-form-item>`
+ str = colWrapper(element, str)
+ return str
+ },
+ rowFormItem(element) {
+ const type = element.type === 'default' ? '' : `type="${element.type}"`
+ const justify = element.type === 'default' ? '' : `justify="${element.justify}"`
+ const align = element.type === 'default' ? '' : `align="${element.align}"`
+ const gutter = element.gutter ? `gutter="${element.gutter}"` : ''
+ const children = element.children.map(el => layouts[el.layout](el))
+ let str = `<el-row ${type} ${justify} ${align} ${gutter}>
+ ${children.join('\n')}
+ </el-row>`
+ str = colWrapper(element, str)
+ return str
+ }
+}
+
+const tags = {
+ 'el-button': el => {
+ const {
+ tag, disabled
+ } = attrBuilder(el)
+ const type = el.type ? `type="${el.type}"` : ''
+ const icon = el.icon ? `icon="${el.icon}"` : ''
+ const size = el.size ? `size="${el.size}"` : ''
+ let child = buildElButtonChild(el)
+
+ if (child) child = `\n${child}\n` // 鎹㈣
+ return `<${el.tag} ${type} ${icon} ${size} ${disabled}>${child}</${el.tag}>`
+ },
+ 'el-input': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : ''
+ const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
+ const readonly = el.readonly ? 'readonly' : ''
+ const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : ''
+ const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : ''
+ const showPassword = el['show-password'] ? 'show-password' : ''
+ const type = el.type ? `type="${el.type}"` : ''
+ const autosize = el.autosize && el.autosize.minRows
+ ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
+ : ''
+ let child = buildElInputChild(el)
+
+ if (child) child = `\n${child}\n` // 鎹㈣
+ return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${el.tag}>`
+ },
+ 'el-input-number': el => {
+ const { disabled, vModel, placeholder } = attrBuilder(el)
+ const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : ''
+ const min = el.min ? `:min='${el.min}'` : ''
+ const max = el.max ? `:max='${el.max}'` : ''
+ const step = el.step ? `:step='${el.step}'` : ''
+ const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
+ const precision = el.precision ? `:precision='${el.precision}'` : ''
+
+ return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}></${el.tag}>`
+ },
+ 'el-select': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const filterable = el.filterable ? 'filterable' : ''
+ const multiple = el.multiple ? 'multiple' : ''
+ let child = buildElSelectChild(el)
+
+ if (child) child = `\n${child}\n` // 鎹㈣
+ return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}</${el.tag}>`
+ },
+ 'el-radio-group': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const size = `size="${el.size}"`
+ let child = buildElRadioGroupChild(el)
+
+ if (child) child = `\n${child}\n` // 鎹㈣
+ return `<${el.tag} ${vModel} ${size} ${disabled}>${child}</${el.tag}>`
+ },
+ 'el-checkbox-group': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const size = `size="${el.size}"`
+ const min = el.min ? `:min="${el.min}"` : ''
+ const max = el.max ? `:max="${el.max}"` : ''
+ let child = buildElCheckboxGroupChild(el)
+
+ if (child) child = `\n${child}\n` // 鎹㈣
+ return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}</${el.tag}>`
+ },
+ 'el-switch': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : ''
+ const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : ''
+ const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : ''
+ const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : ''
+ const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : ''
+ const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : ''
+
+ return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}></${el.tag}>`
+ },
+ 'el-cascader': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const options = el.options ? `:options="${el.vModel}Options"` : ''
+ const props = el.props ? `:props="${el.vModel}Props"` : ''
+ const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
+ const filterable = el.filterable ? 'filterable' : ''
+ const separator = el.separator === '/' ? '' : `separator="${el.separator}"`
+
+ return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}></${el.tag}>`
+ },
+ 'el-slider': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const min = el.min ? `:min='${el.min}'` : ''
+ const max = el.max ? `:max='${el.max}'` : ''
+ const step = el.step ? `:step='${el.step}'` : ''
+ const range = el.range ? 'range' : ''
+ const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : ''
+
+ return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}></${el.tag}>`
+ },
+ 'el-time-picker': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
+ const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
+ const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
+ const isRange = el['is-range'] ? 'is-range' : ''
+ const format = el.format ? `format="${el.format}"` : ''
+ const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
+ const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : ''
+
+ return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}></${el.tag}>`
+ },
+ 'el-date-picker': el => {
+ const {
+ disabled, vModel, clearable, placeholder, width
+ } = attrBuilder(el)
+ const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
+ const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
+ const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
+ const format = el.format ? `format="${el.format}"` : ''
+ const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
+ const type = el.type === 'date' ? '' : `type="${el.type}"`
+ const readonly = el.readonly ? 'readonly' : ''
+
+ return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}></${el.tag}>`
+ },
+ 'el-rate': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const max = el.max ? `:max='${el.max}'` : ''
+ const allowHalf = el['allow-half'] ? 'allow-half' : ''
+ const showText = el['show-text'] ? 'show-text' : ''
+ const showScore = el['show-score'] ? 'show-score' : ''
+
+ return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}></${el.tag}>`
+ },
+ 'el-color-picker': el => {
+ const { disabled, vModel } = attrBuilder(el)
+ const size = `size="${el.size}"`
+ const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
+ const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : ''
+
+ return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}></${el.tag}>`
+ },
+ 'el-upload': el => {
+ const disabled = el.disabled ? ':disabled=\'true\'' : ''
+ const action = el.action ? `:action="${el.vModel}Action"` : ''
+ const multiple = el.multiple ? 'multiple' : ''
+ const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : ''
+ const accept = el.accept ? `accept="${el.accept}"` : ''
+ const name = el.name !== 'file' ? `name="${el.name}"` : ''
+ const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
+ const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"`
+ const fileList = `:file-list="${el.vModel}fileList"`
+ const ref = `ref="${el.vModel}"`
+ let child = buildElUploadChild(el)
+
+ if (child) child = `\n${child}\n` // 鎹㈣
+ return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}</${el.tag}>`
+ }
+}
+
+function attrBuilder(el) {
+ return {
+ vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`,
+ clearable: el.clearable ? 'clearable' : '',
+ placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '',
+ width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '',
+ disabled: el.disabled ? ':disabled=\'true\'' : ''
+ }
+}
+
+// el-buttin 瀛愮骇
+function buildElButtonChild(conf) {
+ const children = []
+ if (conf.default) {
+ children.push(conf.default)
+ }
+ return children.join('\n')
+}
+
+// el-input innerHTML
+function buildElInputChild(conf) {
+ const children = []
+ if (conf.prepend) {
+ children.push(`<template slot="prepend">${conf.prepend}</template>`)
+ }
+ if (conf.append) {
+ children.push(`<template slot="append">${conf.append}</template>`)
+ }
+ return children.join('\n')
+}
+
+function buildElSelectChild(conf) {
+ const children = []
+ if (conf.options && conf.options.length) {
+ children.push(`<el-option v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.label" :value="item.value" :disabled="item.disabled"></el-option>`)
+ }
+ return children.join('\n')
+}
+
+function buildElRadioGroupChild(conf) {
+ const children = []
+ if (conf.options && conf.options.length) {
+ const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio'
+ const border = conf.border ? 'border' : ''
+ children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :value="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
+ }
+ return children.join('\n')
+}
+
+function buildElCheckboxGroupChild(conf) {
+ const children = []
+ if (conf.options && conf.options.length) {
+ const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
+ const border = conf.border ? 'border' : ''
+ children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :value="item.label" :disabled="item.disabled" ${border} />`)
+ }
+ return children.join('\n')
+}
+
+function buildElUploadChild(conf) {
+ const list = []
+ if (conf['list-type'] === 'picture-card') list.push('<i class="el-icon-plus"></i>')
+ else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">${conf.buttonText}</el-button>`)
+ if (conf.showTip) list.push(`<div slot="tip" class="el-upload__tip">鍙兘涓婁紶涓嶈秴杩� ${conf.fileSize}${conf.sizeUnit} 鐨�${conf.accept}鏂囦欢</div>`)
+ return list.join('\n')
+}
+
+export function makeUpHtml(conf, type) {
+ const htmlList = []
+ confGlobal = conf
+ someSpanIsNot24 = conf.fields.some(item => item.span !== 24)
+ conf.fields.forEach(el => {
+ htmlList.push(layouts[el.layout](el))
+ })
+ const htmlStr = htmlList.join('\n')
+
+ let temp = buildFormTemplate(conf, htmlStr, type)
+ if (type === 'dialog') {
+ temp = dialogWrapper(temp)
+ }
+ confGlobal = null
+ return temp
+}
diff --git a/src/utils/generator/icon.json b/src/utils/generator/icon.json
new file mode 100644
index 0000000..2d9999a
--- /dev/null
+++ b/src/utils/generator/icon.json
@@ -0,0 +1 @@
+["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"]
\ No newline at end of file
diff --git a/src/utils/generator/js.js b/src/utils/generator/js.js
new file mode 100644
index 0000000..dc38bfe
--- /dev/null
+++ b/src/utils/generator/js.js
@@ -0,0 +1,370 @@
+import { titleCase } from '@/utils/index'
+import { trigger } from './config'
+// 鏂囦欢澶у皬璁剧疆
+const units = {
+ KB: '1024',
+ MB: '1024 / 1024',
+ GB: '1024 / 1024 / 1024',
+}
+/**
+ * @name: 鐢熸垚js闇�瑕佺殑鏁版嵁
+ * @description: 鐢熸垚js闇�瑕佺殑鏁版嵁
+ * @param {*} conf
+ * @param {*} type 寮圭獥鎴栬〃鍗�
+ * @return {*}
+ */
+export function makeUpJs(conf, type) {
+ conf = JSON.parse(JSON.stringify(conf))
+ const dataList = []
+ const ruleList = []
+ const optionsList = []
+ const propsList = []
+ const methodList = []
+ const uploadVarList = []
+
+ conf.fields.forEach((el) => {
+ buildAttributes(
+ el,
+ dataList,
+ ruleList,
+ optionsList,
+ methodList,
+ propsList,
+ uploadVarList
+ )
+ })
+
+ const script = buildexport(
+ conf,
+ type,
+ dataList.join('\n'),
+ ruleList.join('\n'),
+ optionsList.join('\n'),
+ uploadVarList.join('\n'),
+ propsList.join('\n'),
+ methodList.join('\n')
+ )
+
+ return script
+}
+/**
+ * @name: 鐢熸垚鍙傛暟
+ * @description: 鐢熸垚鍙傛暟锛屽寘鎷〃鍗曟暟鎹〃鍗曢獙璇佹暟鎹紝澶氶�夐�夐」鏁版嵁锛屼笂浼犳暟鎹瓑
+ * @return {*}
+ */
+function buildAttributes(
+ el,
+ dataList,
+ ruleList,
+ optionsList,
+ methodList,
+ propsList,
+ uploadVarList
+){
+ buildData(el, dataList)
+ buildRules(el, ruleList)
+
+ if (el.options && el.options.length) {
+ buildOptions(el, optionsList)
+ if (el.dataType === 'dynamic') {
+ const model = `${el.vModel}Options`
+ const options = titleCase(model)
+ buildOptionMethod(`get${options}`, model, methodList)
+ }
+ }
+
+ if (el.props && el.props.props) {
+ buildProps(el, propsList)
+ }
+
+ if (el.action && el.tag === 'el-upload') {
+ uploadVarList.push(
+ `
+ // 涓婁紶璇锋眰璺緞
+ const ${el.vModel}Action = ref('${el.action}')
+ // 涓婁紶鏂囦欢鍒楄〃
+ const ${el.vModel}fileList = ref([])`
+ )
+ methodList.push(buildBeforeUpload(el))
+ if (!el['auto-upload']) {
+ methodList.push(buildSubmitUpload(el))
+ }
+ }
+
+ if (el.children) {
+ el.children.forEach((el2) => {
+ buildAttributes(
+ el2,
+ dataList,
+ ruleList,
+ optionsList,
+ methodList,
+ propsList,
+ uploadVarList
+ )
+ })
+ }
+}
+/**
+ * @name: 鐢熸垚琛ㄥ崟鏁版嵁formData
+ * @description: 鐢熸垚琛ㄥ崟鏁版嵁formData
+ * @param {*} conf
+ * @param {*} dataList 鏁版嵁鍒楄〃
+ * @return {*}
+ */
+function buildData(conf, dataList) {
+ if (conf.vModel === undefined) return
+ let defaultValue
+ if (typeof conf.defaultValue === 'string' && !conf.multiple) {
+ defaultValue = `'${conf.defaultValue}'`
+ } else {
+ defaultValue = `${JSON.stringify(conf.defaultValue)}`
+ }
+ dataList.push(`${conf.vModel}: ${defaultValue},`)
+}
+/**
+ * @name: 鐢熸垚琛ㄥ崟楠岃瘉鏁版嵁rule
+ * @description: 鐢熸垚琛ㄥ崟楠岃瘉鏁版嵁rule
+ * @param {*} conf
+ * @param {*} ruleList 楠岃瘉鏁版嵁鍒楄〃
+ * @return {*}
+ */
+function buildRules(conf, ruleList) {
+ if (conf.vModel === undefined) return
+ const rules = []
+ if (trigger[conf.tag]) {
+ if (conf.required) {
+ const type = Array.isArray(conf.defaultValue) ? "type: 'array'," : ''
+ let message = Array.isArray(conf.defaultValue)
+ ? `璇疯嚦灏戦�夋嫨涓�涓�${conf.vModel}`
+ : conf.placeholder
+ if (message === undefined) message = `${conf.label}涓嶈兘涓虹┖`
+ rules.push(
+ `{ required: true, ${type} message: '${message}', trigger: '${
+ trigger[conf.tag]
+ }' }`
+ )
+ }
+ if (conf.regList && Array.isArray(conf.regList)) {
+ conf.regList.forEach((item) => {
+ if (item.pattern) {
+ rules.push(
+ `{ pattern: new RegExp(${item.pattern}), message: '${
+ item.message
+ }', trigger: '${trigger[conf.tag]}' }`
+ )
+ }
+ })
+ }
+ ruleList.push(`${conf.vModel}: [${rules.join(',')}],`)
+ }
+}
+/**
+ * @name: 鐢熸垚閫夐」鏁版嵁
+ * @description: 鐢熸垚閫夐」鏁版嵁锛屽崟閫夊閫変笅鎷夌瓑
+ * @param {*} conf
+ * @param {*} optionsList 閫夐」鏁版嵁鍒楄〃
+ * @return {*}
+ */
+function buildOptions(conf, optionsList) {
+ if (conf.vModel === undefined) return
+ if (conf.dataType === 'dynamic') {
+ conf.options = []
+ }
+ const str = `const ${conf.vModel}Options = ref(${JSON.stringify(conf.options)})`
+ optionsList.push(str)
+}
+/**
+ * @name: 鐢熸垚鏂规硶
+ * @description: 鐢熸垚鏂规硶
+ * @param {*} methodName 鏂规硶鍚�
+ * @param {*} model
+ * @param {*} methodList 鏂规硶鍒楄〃
+ * @return {*}
+ */
+function buildOptionMethod(methodName, model, methodList) {
+ const str = `function ${methodName}() {
+ // TODO 鍙戣捣璇锋眰鑾峰彇鏁版嵁
+ ${model}.value
+ }`
+ methodList.push(str)
+}
+/**
+ * @name: 鐢熸垚琛ㄥ崟缁勪欢闇�瑕佺殑props璁剧疆
+ * @description: 鐢熸垚琛ㄥ崟缁勪欢闇�瑕佺殑props璁剧疆锛屽锛涚骇鑱旂粍浠�
+ * @param {*} conf
+ * @param {*} propsList
+ * @return {*}
+ */
+function buildProps(conf, propsList) {
+ if (conf.dataType === 'dynamic') {
+ conf.valueKey !== 'value' && (conf.props.props.value = conf.valueKey)
+ conf.labelKey !== 'label' && (conf.props.props.label = conf.labelKey)
+ conf.childrenKey !== 'children' &&
+ (conf.props.props.children = conf.childrenKey)
+ }
+ const str = `
+ // props璁剧疆
+ const ${conf.vModel}Props = ref(${JSON.stringify(conf.props.props)})`
+ propsList.push(str)
+}
+/**
+ * @name: 鐢熸垚涓婁紶缁勪欢鐨勭浉鍏冲唴瀹�
+ * @description: 鐢熸垚涓婁紶缁勪欢鐨勭浉鍏冲唴瀹�
+ * @param {*} conf
+ * @return {*}
+ */
+function buildBeforeUpload(conf) {
+ const unitNum = units[conf.sizeUnit]
+ let rightSizeCode = ''
+ let acceptCode = ''
+ const returnList = []
+ if (conf.fileSize) {
+ rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize}
+ if(!isRightSize){
+ proxy.$modal.msgError('鏂囦欢澶у皬瓒呰繃 ${conf.fileSize}${conf.sizeUnit}')
+ }`
+ returnList.push('isRightSize')
+ }
+ if (conf.accept) {
+ acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type)
+ if(!isAccept){
+ proxy.$modal.msgError('搴旇閫夋嫨${conf.accept}绫诲瀷鐨勬枃浠�')
+ }`
+ returnList.push('isAccept')
+ }
+ const str = `
+ /**
+ * @name: 涓婁紶涔嬪墠鐨勬枃浠跺垽鏂�
+ * @description: 涓婁紶涔嬪墠鐨勬枃浠跺垽鏂紝鍒ゆ柇鏂囦欢澶у皬鏂囦欢绫诲瀷绛�
+ * @param {*} file
+ * @return {*}
+ */
+ function ${conf.vModel}BeforeUpload(file) {
+ ${rightSizeCode}
+ ${acceptCode}
+ return ${returnList.join('&&')}
+ }`
+ return returnList.length ? str : ''
+}
+/**
+ * @name: 鐢熸垚鎻愪氦琛ㄥ崟鏂规硶
+ * @description: 鐢熸垚鎻愪氦琛ㄥ崟鏂规硶
+ * @param {Object} conf vModel 琛ㄥ崟ref
+ * @return {*}
+ */
+function buildSubmitUpload(conf) {
+ const str = `function submitUpload() {
+ this.$refs['${conf.vModel}'].submit()
+ }`
+ return str
+}
+/**
+ * @name: 缁勮js浠g爜
+ * @description: 缁勮js浠g爜鏂规硶
+ * @return {*}
+ */
+function buildexport(
+ conf,
+ type,
+ data,
+ rules,
+ selectOptions,
+ uploadVar,
+ props,
+ methods
+) {
+ let str = `
+ const { proxy } = getCurrentInstance()
+ const ${conf.formRef} = ref()
+ const data = reactive({
+ ${conf.formModel}: {
+ ${data}
+ },
+ ${conf.formRules}: {
+ ${rules}
+ }
+ })
+
+ const {${conf.formModel}, ${conf.formRules}} = toRefs(data)
+
+ ${selectOptions}
+
+ ${uploadVar}
+
+ ${props}
+
+ ${methods}
+ `
+
+ if(type === 'dialog') {
+ str += `
+ // 寮圭獥璁剧疆
+ const dialogVisible = defineModel()
+ // 寮圭獥纭鍥炶皟
+ const emit = defineEmits(['confirm'])
+ /**
+ * @name: 寮圭獥鎵撳紑鍚庢墽琛�
+ * @description: 寮圭獥鎵撳紑鍚庢墽琛屾柟娉�
+ * @return {*}
+ */
+ function onOpen(){
+
+ }
+ /**
+ * @name: 寮圭獥鍏抽棴鏃舵墽琛�
+ * @description: 寮圭獥鍏抽棴鏂规硶锛岄噸缃〃鍗�
+ * @return {*}
+ */
+ function onClose(){
+ ${conf.formRef}.value.resetFields()
+ }
+ /**
+ * @name: 寮圭獥鍙栨秷
+ * @description: 寮圭獥鍙栨秷鏂规硶
+ * @return {*}
+ */
+ function close(){
+ dialogVisible.value = false
+ }
+ /**
+ * @name: 寮圭獥琛ㄥ崟鎻愪氦
+ * @description: 寮圭獥琛ㄥ崟鎻愪氦鏂规硶
+ * @return {*}
+ */
+ function handelConfirm(){
+ ${conf.formRef}.value.validate((valid) => {
+ if (!valid) return
+ // TODO 鎻愪氦琛ㄥ崟
+
+ close()
+ // 鍥炶皟鐖剁骇缁勪欢
+ emit('confirm')
+ })
+ }
+ `
+ } else {
+ str += `
+ /**
+ * @name: 琛ㄥ崟鎻愪氦
+ * @description: 琛ㄥ崟鎻愪氦鏂规硶
+ * @return {*}
+ */
+ function submitForm() {
+ ${conf.formRef}.value.validate((valid) => {
+ if (!valid) return
+ // TODO 鎻愪氦琛ㄥ崟
+ })
+ }
+ /**
+ * @name: 琛ㄥ崟閲嶇疆
+ * @description: 琛ㄥ崟閲嶇疆鏂规硶
+ * @return {*}
+ */
+ function resetForm() {
+ ${conf.formRef}.value.resetFields()
+ }
+ `
+ }
+ return str
+}
diff --git a/src/utils/generator/render.js b/src/utils/generator/render.js
new file mode 100644
index 0000000..d6d4414
--- /dev/null
+++ b/src/utils/generator/render.js
@@ -0,0 +1,156 @@
+import { defineComponent, h } from 'vue'
+import { makeMap } from '@/utils/index'
+
+const isAttr = makeMap(
+ 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
+ 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
+ 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
+ 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
+ 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
+ 'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' +
+ 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
+ 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
+ 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
+ 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
+ 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
+ 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
+ 'target,title,type,usemap,value,width,wrap' + 'prefix-icon'
+)
+const isNotProps = makeMap(
+ 'layout,prepend,regList,tag,document,changeTag,defaultValue'
+)
+
+function useVModel(props, emit) {
+ return {
+ modelValue: props.defaultValue,
+ 'onUpdate:modelValue': (val) => emit('update:modelValue', val),
+ }
+}
+const componentChild = {
+ 'el-button': {
+ default(h, conf, key) {
+ return conf[key]
+ },
+ },
+ 'el-select': {
+ options(h, conf, key) {
+ return conf.options.map(item => h(resolveComponent('el-option'), {
+ label: item.label,
+ value: item.value,
+ }))
+ }
+ },
+ 'el-radio-group': {
+ options(h, conf, key) {
+ return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), {
+ label: item.value,
+ }, () => item.label)) : conf.options.map(item => h(resolveComponent('el-radio'), {
+ label: item.value,
+ border: conf.border,
+ }, () => item.label))
+ }
+ },
+ 'el-checkbox-group': {
+ options(h, conf, key) {
+ return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), {
+ label: item.value,
+ }, () => item.label)) : conf.options.map(item => h(resolveComponent('el-checkbox'), {
+ label: item.value,
+ border: conf.border,
+ }, () => item.label))
+ }
+ },
+ 'el-upload': {
+ 'list-type': (h, conf, key) => {
+ const option = {}
+ // if (conf.showTip) {
+ // tip = h('div', {
+ // class: "el-upload__tip"
+ // }, () => '鍙兘涓婁紶涓嶈秴杩�' + conf.fileSize + conf.sizeUnit + '鐨�' + conf.accept + '鏂囦欢')
+ // }
+ if (conf['list-type'] === 'picture-card') {
+ return h(resolveComponent('el-icon'), option, () => h(resolveComponent('Plus')))
+ } else {
+ // option.size = "small"
+ option.type = "primary"
+ option.icon = "Upload"
+ return h(resolveComponent('el-button'), option, () => conf.buttonText)
+ }
+ },
+
+ }
+}
+const componentSlot = {
+ 'el-upload': {
+ 'tip': (h, conf, key) => {
+ if (conf.showTip) {
+ return () => h('div', {
+ class: "el-upload__tip"
+ }, '鍙兘涓婁紶涓嶈秴杩�' + conf.fileSize + conf.sizeUnit + '鐨�' + conf.accept + '鏂囦欢')
+ }
+ },
+ }
+}
+export default defineComponent({
+
+ // 浣跨敤 render 鍑芥暟
+ render() {
+ const dataObject = {
+ attrs: {},
+ props: {},
+ on: {},
+ style: {}
+ }
+ const confClone = JSON.parse(JSON.stringify(this.conf))
+ const children = []
+ const slot = {}
+ const childObjs = componentChild[confClone.tag]
+ if (childObjs) {
+ Object.keys(childObjs).forEach(key => {
+ const childFunc = childObjs[key]
+ if (confClone[key]) {
+ children.push(childFunc(h, confClone, key))
+ }
+ })
+ }
+ const slotObjs = componentSlot[confClone.tag]
+ if (slotObjs) {
+ Object.keys(slotObjs).forEach(key => {
+ const childFunc = slotObjs[key]
+ if (confClone[key]) {
+ slot[key] = childFunc(h, confClone, key)
+ }
+ })
+ }
+ Object.keys(confClone).forEach(key => {
+ const val = confClone[key]
+ if (dataObject[key]) {
+ dataObject[key] = val
+ } else if (isAttr(key)) {
+ dataObject.attrs[key] = val
+ } else if (!isNotProps(key)) {
+ dataObject.props[key] = val
+ }
+ })
+ if(children.length > 0){
+ slot.default = () => children
+ }
+
+ return h(resolveComponent(this.conf.tag),
+ {
+ modelValue: this.$attrs.modelValue,
+ ...dataObject.props,
+ ...dataObject.attrs,
+ style: {
+ ...dataObject.style
+ },
+ }
+ , slot ?? null)
+ },
+ props: {
+ conf: {
+ type: Object,
+ required: true,
+ },
+ }
+})
\ No newline at end of file
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 0000000..809593f
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,411 @@
+import { parseTime } from "./ruoyi";
+
+/**
+ * 琛ㄦ牸鏃堕棿鏍煎紡鍖�
+ */
+export function formatDate(cellValue) {
+ if (cellValue == null || cellValue == "") return "";
+ var date = new Date(cellValue);
+ var year = date.getFullYear();
+ var month =
+ date.getMonth() + 1 < 10
+ ? "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
+ );
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+ if (("" + time).length === 10) {
+ time = parseInt(time) * 1000;
+ } else {
+ time = +time;
+ }
+ const d = new Date(time);
+ const now = Date.now();
+
+ const diff = (now - d) / 1000;
+
+ if (diff < 30) {
+ return "鍒氬垰";
+ } else if (diff < 3600) {
+ // less 1 hour
+ return Math.ceil(diff / 60) + "鍒嗛挓鍓�";
+ } else if (diff < 3600 * 24) {
+ return Math.ceil(diff / 3600) + "灏忔椂鍓�";
+ } else if (diff < 3600 * 24 * 2) {
+ return "1澶╁墠";
+ }
+ if (option) {
+ return parseTime(time, option);
+ } else {
+ return (
+ d.getMonth() +
+ 1 +
+ "鏈�" +
+ d.getDate() +
+ "鏃�" +
+ d.getHours() +
+ "鏃�" +
+ d.getMinutes() +
+ "鍒�"
+ );
+ }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function getQueryObject(url) {
+ url = url == null ? window.location.href : url;
+ const search = url.substring(url.lastIndexOf("?") + 1);
+ const obj = {};
+ const reg = /([^?&=]+)=([^?&=]*)/g;
+ search.replace(reg, (rs, $1, $2) => {
+ const name = decodeURIComponent($1);
+ let val = decodeURIComponent($2);
+ val = String(val);
+ obj[name] = val;
+ return rs;
+ });
+ return obj;
+}
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export function byteLength(str) {
+ // returns the byte length of an utf8 string
+ let s = str.length;
+ for (var i = str.length - 1; i >= 0; i--) {
+ const code = str.charCodeAt(i);
+ if (code > 0x7f && code <= 0x7ff) s++;
+ else if (code > 0x7ff && code <= 0xffff) s += 2;
+ if (code >= 0xdc00 && code <= 0xdfff) i--;
+ }
+ return s;
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual) {
+ const newArray = [];
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i]) {
+ newArray.push(actual[i]);
+ }
+ }
+ return newArray;
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json) {
+ if (!json) return "";
+ return cleanArray(
+ Object.keys(json).map((key) => {
+ if (json[key] === undefined) return "";
+ return encodeURIComponent(key) + "=" + encodeURIComponent(json[key]);
+ })
+ ).join("&");
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+ const search = decodeURIComponent(url.split("?")[1]).replace(/\+/g, " ");
+ if (!search) {
+ return {};
+ }
+ const obj = {};
+ const searchArr = search.split("&");
+ searchArr.forEach((v) => {
+ const index = v.indexOf("=");
+ if (index !== -1) {
+ const name = v.substring(0, index);
+ const val = v.substring(index + 1, v.length);
+ obj[name] = val;
+ }
+ });
+ return obj;
+}
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export function html2Text(val) {
+ const div = document.createElement("div");
+ div.innerHTML = val;
+ return div.textContent || div.innerText;
+}
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target, source) {
+ if (typeof target !== "object") {
+ target = {};
+ }
+ if (Array.isArray(source)) {
+ return source.slice();
+ }
+ Object.keys(source).forEach((property) => {
+ const sourceProperty = source[property];
+ if (typeof sourceProperty === "object") {
+ target[property] = objectMerge(target[property], sourceProperty);
+ } else {
+ target[property] = sourceProperty;
+ }
+ });
+ return target;
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export function toggleClass(element, className) {
+ if (!element || !className) {
+ return;
+ }
+ let classString = element.className;
+ const nameIndex = classString.indexOf(className);
+ if (nameIndex === -1) {
+ classString += "" + className;
+ } else {
+ classString =
+ classString.substr(0, nameIndex) +
+ classString.substr(nameIndex + className.length);
+ }
+ element.className = classString;
+}
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export function getTime(type) {
+ if (type === "start") {
+ return new Date().getTime() - 3600 * 1000 * 24 * 90;
+ } else {
+ return new Date(new Date().toDateString());
+ }
+}
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+export function debounce(func, wait, immediate) {
+ let timeout, args, context, timestamp, result;
+
+ const later = function () {
+ // 鎹笂涓�娆¤Е鍙戞椂闂撮棿闅�
+ const last = +new Date() - timestamp;
+
+ // 涓婃琚寘瑁呭嚱鏁拌璋冪敤鏃堕棿闂撮殧 last 灏忎簬璁惧畾鏃堕棿闂撮殧 wait
+ if (last < wait && last > 0) {
+ timeout = setTimeout(later, wait - last);
+ } else {
+ timeout = null;
+ // 濡傛灉璁惧畾涓篿mmediate===true锛屽洜涓哄紑濮嬭竟鐣屽凡缁忚皟鐢ㄨ繃浜嗘澶勬棤闇�璋冪敤
+ if (!immediate) {
+ result = func.apply(context, args);
+ if (!timeout) context = args = null;
+ }
+ }
+ };
+
+ return function (...args) {
+ context = this;
+ timestamp = +new Date();
+ const callNow = immediate && !timeout;
+ // 濡傛灉寤舵椂涓嶅瓨鍦紝閲嶆柊璁惧畾寤舵椂
+ if (!timeout) timeout = setTimeout(later, wait);
+ if (callNow) {
+ result = func.apply(context, args);
+ context = args = null;
+ }
+
+ return result;
+ };
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+export function deepClone(source) {
+ if (!source && typeof source !== "object") {
+ throw new Error("error arguments", "deepClone");
+ }
+ const targetObj = source.constructor === Array ? [] : {};
+ Object.keys(source).forEach((keys) => {
+ if (source[keys] && typeof source[keys] === "object") {
+ targetObj[keys] = deepClone(source[keys]);
+ } else {
+ targetObj[keys] = source[keys];
+ }
+ });
+ return targetObj;
+}
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export function uniqueArr(arr) {
+ return Array.from(new Set(arr));
+}
+
+/**
+ * @returns {string}
+ */
+export function createUniqueString() {
+ const timestamp = +new Date() + "";
+ const randomNum = parseInt((1 + Math.random()) * 65536) + "";
+ return (+(randomNum + timestamp)).toString(32);
+}
+
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele, cls) {
+ return !!ele.className.match(new RegExp("(\\s|^)" + cls + "(\\s|$)"));
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function addClass(ele, cls) {
+ if (!hasClass(ele, cls)) ele.className += " " + cls;
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function removeClass(ele, cls) {
+ if (hasClass(ele, cls)) {
+ const reg = new RegExp("(\\s|^)" + cls + "(\\s|$)");
+ ele.className = ele.className.replace(reg, " ");
+ }
+}
+
+export function makeMap(str, expectsLowerCase) {
+ const map = Object.create(null);
+ const list = str.split(",");
+ for (let i = 0; i < list.length; i++) {
+ map[list[i]] = true;
+ }
+ return expectsLowerCase ? (val) => map[val.toLowerCase()] : (val) => map[val];
+}
+
+export const exportDefault = "export default ";
+
+export const beautifierConf = {
+ html: {
+ indent_size: "2",
+ indent_char: " ",
+ max_preserve_newlines: "-1",
+ preserve_newlines: false,
+ keep_array_indentation: false,
+ break_chained_methods: false,
+ indent_scripts: "separate",
+ brace_style: "end-expand",
+ space_before_conditional: true,
+ unescape_strings: false,
+ jslint_happy: false,
+ end_with_newline: true,
+ wrap_line_length: "110",
+ indent_inner_html: true,
+ comma_first: false,
+ e4x: true,
+ indent_empty_lines: true,
+ },
+ js: {
+ indent_size: "2",
+ indent_char: " ",
+ max_preserve_newlines: "-1",
+ preserve_newlines: false,
+ keep_array_indentation: false,
+ break_chained_methods: false,
+ indent_scripts: "normal",
+ brace_style: "end-expand",
+ space_before_conditional: true,
+ unescape_strings: false,
+ jslint_happy: true,
+ end_with_newline: true,
+ wrap_line_length: "110",
+ indent_inner_html: true,
+ comma_first: false,
+ e4x: true,
+ indent_empty_lines: true,
+ },
+};
+
+// 棣栧瓧姣嶅ぇ灏�
+export function titleCase(str) {
+ return str.replace(/( |^)[a-z]/g, (L) => L.toUpperCase());
+}
+
+// 涓嬪垝杞┘宄�
+export function camelCase(str) {
+ return str.replace(/_[a-z]/g, (str1) => str1.substr(-1).toUpperCase());
+}
+
+export function isNumberStr(str) {
+ return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str);
+}
+
+export function isEqual(obj1, obj2) {
+ return JSON.stringify(obj1) === JSON.stringify(obj2);
+}
+
+/**
+ * 鑾峰彇褰撳墠鏃ユ湡骞舵牸寮忓寲涓� YYYY-MM-DD
+ * @returns {string} 鏍煎紡鍖栫殑鏃ユ湡瀛楃涓�
+ */
+export function getCurrentDate() {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, '0'); // 鏈堜唤浠�0寮�濮�
+ const day = String(today.getDate()).padStart(2, '0');
+ return `${year}-${month}-${day}`;
+}
+
diff --git a/src/utils/jsencrypt.js b/src/utils/jsencrypt.js
new file mode 100644
index 0000000..9f3a280
--- /dev/null
+++ b/src/utils/jsencrypt.js
@@ -0,0 +1,30 @@
+import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
+
+// 瀵嗛挜瀵圭敓鎴� http://web.chacuo.net/netrsakeypair
+
+const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
+ 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+
+const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
+ '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
+ 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
+ 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
+ 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
+ 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
+ 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
+ 'UP8iWi1Qw0Y='
+
+// 鍔犲瘑
+export function encrypt(txt) {
+ const encryptor = new JSEncrypt()
+ encryptor.setPublicKey(publicKey) // 璁剧疆鍏挜
+ return encryptor.encrypt(txt) // 瀵规暟鎹繘琛屽姞瀵�
+}
+
+// 瑙e瘑
+export function decrypt(txt) {
+ const encryptor = new JSEncrypt()
+ encryptor.setPrivateKey(privateKey) // 璁剧疆绉侀挜
+ return encryptor.decrypt(txt) // 瀵规暟鎹繘琛岃В瀵�
+}
+
diff --git a/src/utils/permission.js b/src/utils/permission.js
new file mode 100644
index 0000000..c736c0c
--- /dev/null
+++ b/src/utils/permission.js
@@ -0,0 +1,51 @@
+import useUserStore from '@/store/modules/user'
+
+/**
+ * 瀛楃鏉冮檺鏍¢獙
+ * @param {Array} value 鏍¢獙鍊�
+ * @returns {Boolean}
+ */
+export function checkPermi(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const permissions = useUserStore().permissions
+ const permissionDatas = value
+ const all_permission = "*:*:*"
+
+ const hasPermission = permissions.some(permission => {
+ return all_permission === permission || permissionDatas.includes(permission)
+ })
+
+ if (!hasPermission) {
+ return false
+ }
+ return true
+ } else {
+ console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`)
+ return false
+ }
+}
+
+/**
+ * 瑙掕壊鏉冮檺鏍¢獙
+ * @param {Array} value 鏍¢獙鍊�
+ * @returns {Boolean}
+ */
+export function checkRole(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const roles = useUserStore().roles
+ const permissionRoles = value
+ const super_admin = "admin"
+
+ const hasRole = roles.some(role => {
+ return super_admin === role || permissionRoles.includes(role)
+ })
+
+ if (!hasRole) {
+ return false
+ }
+ return true
+ } else {
+ console.error(`need roles! Like checkRole="['admin','editor']"`)
+ return false
+ }
+}
\ No newline at end of file
diff --git a/src/utils/request.js b/src/utils/request.js
new file mode 100644
index 0000000..672bed8
--- /dev/null
+++ b/src/utils/request.js
@@ -0,0 +1,157 @@
+import axios from 'axios'
+import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import errorCode from '@/utils/errorCode'
+import { tansParams, blobValidate } from '@/utils/ruoyi'
+import cache from '@/plugins/cache'
+import { saveAs } from 'file-saver'
+import useUserStore from '@/store/modules/user'
+
+let downloadLoadingInstance
+// 鏄惁鏄剧ず閲嶆柊鐧诲綍
+export let isRelogin = { show: false }
+
+axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
+// 鍒涘缓axios瀹炰緥
+const service = axios.create({
+ // axios涓姹傞厤缃湁baseURL閫夐」锛岃〃绀鸿姹俇RL鍏叡閮ㄥ垎
+ baseURL: import.meta.env.VITE_APP_BASE_API,
+ // 瓒呮椂
+ timeout: 160000
+})
+
+// request鎷︽埅鍣�
+service.interceptors.request.use(config => {
+ config.headers = config.headers || {}
+ // 鏄惁闇�瑕佽缃� token
+ const isToken = config.headers.isToken === false
+ // 鏄惁闇�瑕侀槻姝㈡暟鎹噸澶嶆彁浜�
+ const isRepeatSubmit = config.headers.repeatSubmit === false
+ if (getToken() && !isToken) {
+ config.headers['Authorization'] = 'Bearer ' + getToken() // 璁╂瘡涓姹傛惡甯﹁嚜瀹氫箟token 璇锋牴鎹疄闄呮儏鍐佃嚜琛屼慨鏀�
+ }
+ // get璇锋眰鏄犲皠params鍙傛暟
+ if (config.method === 'get' && config.params) {
+ let url = config.url + '?' + tansParams(config.params)
+ url = url.slice(0, -1)
+ config.params = {}
+ config.url = url
+ }
+ if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
+ const requestObj = {
+ url: config.url,
+ data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
+ time: new Date().getTime()
+ }
+ const requestSize = Object.keys(JSON.stringify(requestObj)).length // 璇锋眰鏁版嵁澶у皬
+ const limitSize = 5 * 1024 * 1024 // 闄愬埗瀛樻斁鏁版嵁5M
+ if (requestSize >= limitSize) {
+ console.warn(`[${config.url}]: ` + '璇锋眰鏁版嵁澶у皬瓒呭嚭鍏佽鐨�5M闄愬埗锛屾棤娉曡繘琛岄槻閲嶅鎻愪氦楠岃瘉銆�')
+ return config
+ }
+ const sessionObj = cache.session.getJSON('sessionObj')
+ if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
+ cache.session.setJSON('sessionObj', requestObj)
+ } else {
+ const s_url = sessionObj.url // 璇锋眰鍦板潃
+ const s_data = sessionObj.data // 璇锋眰鏁版嵁
+ const s_time = sessionObj.time // 璇锋眰鏃堕棿
+ const interval = 1000 // 闂撮殧鏃堕棿(ms)锛屽皬浜庢鏃堕棿瑙嗕负閲嶅鎻愪氦
+ if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
+ const message = '鏁版嵁姝e湪澶勭悊锛岃鍕块噸澶嶆彁浜�'
+ console.warn(`[${s_url}]: ` + message)
+ return Promise.reject(new Error(message))
+ } else {
+ cache.session.setJSON('sessionObj', requestObj)
+ }
+ }
+ }
+ return config
+}, error => {
+ console.log(error)
+ return Promise.reject(error)
+})
+
+// 鍝嶅簲鎷︽埅鍣�
+service.interceptors.response.use(res => {
+ // 鏈缃姸鎬佺爜鍒欓粯璁ゆ垚鍔熺姸鎬�
+ const code = res.data.code || 200
+ const handleAuthError = (res.config && res.config.headers && res.config.headers.handleAuthError) !== false
+ // 鑾峰彇閿欒淇℃伅
+ const msg = errorCode[code] || res.data.msg || errorCode['default']
+ // 浜岃繘鍒舵暟鎹垯鐩存帴杩斿洖
+ if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
+ return res.data
+ }
+ if (code === 401) {
+ if (!handleAuthError) {
+ return Promise.reject(new Error(msg))
+ }
+ if (!isRelogin.show) {
+ isRelogin.show = true
+ ElMessageBox.confirm('鐧诲綍鐘舵�佸凡杩囨湡锛屾偍鍙互缁х画鐣欏湪璇ラ〉闈紝鎴栬�呴噸鏂扮櫥褰�', '绯荤粺鎻愮ず', { confirmButtonText: '閲嶆柊鐧诲綍', cancelButtonText: '鍙栨秷', type: 'warning' }).then(() => {
+ isRelogin.show = false
+ useUserStore().logOut().then(() => {
+ location.href = '/index'
+ })
+ }).catch(() => {
+ isRelogin.show = false
+ })
+ }
+ return Promise.reject('鏃犳晥鐨勪細璇濓紝鎴栬�呬細璇濆凡杩囨湡锛岃閲嶆柊鐧诲綍銆�')
+ } else if (code === 500) {
+ ElMessage({ message: msg, type: 'error' })
+ return Promise.reject(new Error(msg))
+ } else if (code === 601) {
+ ElMessage({ message: msg, type: 'warning' })
+ return Promise.reject(new Error(msg))
+ } else if (code !== 200) {
+ ElNotification.error({ title: msg })
+ return Promise.reject('error')
+ } else {
+ return Promise.resolve(res.data)
+ }
+ },
+ error => {
+ console.log('err' + error)
+ let { message } = error
+ if (message == "Network Error") {
+ message = "鍚庣鎺ュ彛杩炴帴寮傚父"
+ } else if (message.includes("timeout")) {
+ message = "绯荤粺鎺ュ彛璇锋眰瓒呮椂"
+ } else if (message.includes("Request failed with status code")) {
+ message = "绯荤粺鎺ュ彛" + message.substr(message.length - 3) + "寮傚父"
+ }
+ ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
+ return Promise.reject(error)
+ }
+)
+
+// 閫氱敤涓嬭浇鏂规硶
+export function download(url, params, filename, config) {
+ downloadLoadingInstance = ElLoading.service({ text: "姝e湪涓嬭浇鏁版嵁锛岃绋嶅��", background: "rgba(0, 0, 0, 0.7)", })
+ return service.post(url, params, {
+ transformRequest: [(params) => { return tansParams(params) }],
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ responseType: 'blob',
+ ...config
+ }).then(async (data) => {
+ const isBlob = blobValidate(data)
+ if (isBlob) {
+ const blob = new Blob([data])
+ saveAs(blob, filename)
+ } else {
+ const resText = await data.text()
+ const rspObj = JSON.parse(resText)
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+ ElMessage.error(errMsg)
+ }
+ downloadLoadingInstance.close()
+ }).catch((r) => {
+ console.error(r)
+ ElMessage.error('涓嬭浇鏂囦欢鍑虹幇閿欒锛岃鑱旂郴绠$悊鍛橈紒')
+ downloadLoadingInstance.close()
+ })
+}
+
+export default service
diff --git a/src/utils/ruoyi.js b/src/utils/ruoyi.js
new file mode 100644
index 0000000..0d72c1a
--- /dev/null
+++ b/src/utils/ruoyi.js
@@ -0,0 +1,228 @@
+/**
+ * 閫氱敤js鏂规硶灏佽澶勭悊
+ * Copyright (c) 2019 ruoyi
+ */
+
+// 鏃ユ湡鏍煎紡鍖�
+export function parseTime(time, pattern) {
+ if (arguments.length === 0 || !time) {
+ return null
+ }
+ const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+ let date
+ if (typeof time === 'object') {
+ date = time
+ } else {
+ if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+ time = parseInt(time)
+ } else if (typeof time === 'string') {
+ time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '')
+ }
+ if ((typeof time === 'number') && (time.toString().length === 10)) {
+ time = time * 1000
+ }
+ date = new Date(time)
+ }
+ const formatObj = {
+ y: date.getFullYear(),
+ m: date.getMonth() + 1,
+ d: date.getDate(),
+ h: date.getHours(),
+ i: date.getMinutes(),
+ s: date.getSeconds(),
+ a: date.getDay()
+ }
+ const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+ let value = formatObj[key]
+ // Note: getDay() returns 0 on Sunday
+ if (key === 'a') { return ['鏃�', '涓�', '浜�', '涓�', '鍥�', '浜�', '鍏�'][value] }
+ if (result.length > 0 && value < 10) {
+ value = '0' + value
+ }
+ return value || 0
+ })
+ return time_str
+}
+
+// 琛ㄥ崟閲嶇疆
+export function resetForm(refName) {
+ if (this.$refs[refName]) {
+ this.$refs[refName].resetFields()
+ }
+}
+
+// 娣诲姞鏃ユ湡鑼冨洿
+export function addDateRange(params, dateRange, propName) {
+ let search = params
+ search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}
+ dateRange = Array.isArray(dateRange) ? dateRange : []
+ if (typeof (propName) === 'undefined') {
+ search.params['beginTime'] = dateRange[0]
+ search.params['endTime'] = dateRange[1]
+ } else {
+ search.params['begin' + propName] = dateRange[0]
+ search.params['end' + propName] = dateRange[1]
+ }
+ return search
+}
+
+// 鍥炴樉鏁版嵁瀛楀吀
+export function selectDictLabel(datas, value) {
+ if (value === undefined) {
+ return ""
+ }
+ var actions = []
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + value)) {
+ actions.push(datas[key].label)
+ return true
+ }
+ })
+ if (actions.length === 0) {
+ actions.push(value)
+ }
+ return actions.join('')
+}
+
+// 鍥炴樉鏁版嵁瀛楀吀锛堝瓧绗︿覆銆佹暟缁勶級
+export function selectDictLabels(datas, value, separator) {
+ if (value === undefined || value.length ===0) {
+ return ""
+ }
+ if (Array.isArray(value)) {
+ value = value.join(",")
+ }
+ var actions = []
+ var currentSeparator = undefined === separator ? "," : separator
+ var temp = value.split(currentSeparator)
+ Object.keys(value.split(currentSeparator)).some((val) => {
+ var match = false
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + temp[val])) {
+ actions.push(datas[key].label + currentSeparator)
+ match = true
+ }
+ })
+ if (!match) {
+ actions.push(temp[val] + currentSeparator)
+ }
+ })
+ return actions.join('').substring(0, actions.join('').length - 1)
+}
+
+// 瀛楃涓叉牸寮忓寲(%s )
+export function sprintf(str) {
+ var args = arguments, flag = true, i = 1
+ str = str.replace(/%s/g, function () {
+ var arg = args[i++]
+ if (typeof arg === 'undefined') {
+ flag = false
+ return ''
+ }
+ return arg
+ })
+ return flag ? str : ''
+}
+
+// 杞崲瀛楃涓诧紝undefined,null绛夎浆鍖栦负""
+export function parseStrEmpty(str) {
+ if (!str || str == "undefined" || str == "null") {
+ return ""
+ }
+ return str
+}
+
+// 鏁版嵁鍚堝苟
+export function mergeRecursive(source, target) {
+ for (var p in target) {
+ try {
+ if (target[p].constructor == Object) {
+ source[p] = mergeRecursive(source[p], target[p])
+ } else {
+ source[p] = target[p]
+ }
+ } catch (e) {
+ source[p] = target[p]
+ }
+ }
+ return source
+}
+
+/**
+ * 鏋勯�犳爲鍨嬬粨鏋勬暟鎹�
+ * @param {*} data 鏁版嵁婧�
+ * @param {*} id id瀛楁 榛樿 'id'
+ * @param {*} parentId 鐖惰妭鐐瑰瓧娈� 榛樿 'parentId'
+ * @param {*} children 瀛╁瓙鑺傜偣瀛楁 榛樿 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+ let config = {
+ id: id || 'id',
+ parentId: parentId || 'parentId',
+ childrenList: children || 'children'
+ }
+
+ var childrenListMap = {}
+ var tree = []
+ for (let d of data) {
+ let id = d[config.id]
+ childrenListMap[id] = d
+ if (!d[config.childrenList]) {
+ d[config.childrenList] = []
+ }
+ }
+
+ for (let d of data) {
+ let parentId = d[config.parentId]
+ let parentObj = childrenListMap[parentId]
+ if (!parentObj) {
+ tree.push(d)
+ } else {
+ parentObj[config.childrenList].push(d)
+ }
+ }
+ return tree
+}
+
+/**
+* 鍙傛暟澶勭悊
+* @param {*} params 鍙傛暟
+*/
+export function tansParams(params) {
+ let result = ''
+ for (const propName of Object.keys(params)) {
+ const value = params[propName]
+ var part = encodeURIComponent(propName) + "="
+ if (value !== null && value !== "" && typeof (value) !== "undefined") {
+ if (typeof value === 'object') {
+ for (const key of Object.keys(value)) {
+ if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
+ let params = propName + '[' + key + ']'
+ var subPart = encodeURIComponent(params) + "="
+ result += subPart + encodeURIComponent(value[key]) + "&"
+ }
+ }
+ } else {
+ result += part + encodeURIComponent(value) + "&"
+ }
+ }
+ }
+ return result
+}
+
+// 杩斿洖椤圭洰璺緞
+export function getNormalPath(p) {
+ if (p.length === 0 || !p || p == 'undefined') {
+ return p
+ }
+ let res = p.replace('//', '/')
+ if (res[res.length - 1] === '/') {
+ return res.slice(0, res.length - 1)
+ }
+ return res
+}
+
+// 楠岃瘉鏄惁涓篵lob鏍煎紡
+export function blobValidate(data) {
+ return data.type !== 'application/json'
+}
diff --git a/src/utils/scroll-to.js b/src/utils/scroll-to.js
new file mode 100644
index 0000000..709fa57
--- /dev/null
+++ b/src/utils/scroll-to.js
@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+ t /= d / 2
+ if (t < 1) {
+ return c / 2 * t * t + b
+ }
+ t--
+ return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+ document.documentElement.scrollTop = amount
+ document.body.parentNode.scrollTop = amount
+ document.body.scrollTop = amount
+}
+
+function position() {
+ return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+ const start = position()
+ const change = to - start
+ const increment = 20
+ let currentTime = 0
+ duration = (typeof (duration) === 'undefined') ? 500 : duration
+ var animateScroll = function() {
+ // increment the time
+ currentTime += increment
+ // find the value with the quadratic in-out easing function
+ var val = Math.easeInOutQuad(currentTime, start, change, duration)
+ // move the document.body
+ move(val)
+ // do the animation unless its over
+ if (currentTime < duration) {
+ requestAnimFrame(animateScroll)
+ } else {
+ if (callback && typeof (callback) === 'function') {
+ // the animation is done so lets callback
+ callback()
+ }
+ }
+ }
+ animateScroll()
+}
diff --git a/src/utils/summarizeTable.js b/src/utils/summarizeTable.js
new file mode 100644
index 0000000..1ad480d
--- /dev/null
+++ b/src/utils/summarizeTable.js
@@ -0,0 +1,57 @@
+/**
+ * 閫氱敤鐨勮〃鏍煎悎璁℃柟娉�
+ * @param {Object} param - 鍖呭惈琛ㄦ牸鍒楅厤缃拰鏁版嵁婧愮殑瀵硅薄
+ * @param {Array<string>} summaryProps - 闇�瑕佹眹鎬荤殑瀛楁鍚嶆暟缁�
+ * @param {Object} specialFormat - 鐗规畩鏍煎紡鍖栬鍒欙細瀛楁鍚� -> 鏍煎紡鍖栭�夐」锛堝鏄惁鍘绘帀灏忔暟锛�
+ * @returns {Array} 鍚堣琛屾暟鎹�
+ */
+const summarizeTable = (param, summaryProps, specialFormat = {}) => {
+ const { columns, data } = param;
+ const sums = [];
+ columns.forEach((column, index) => {
+ if (index === 0) {
+ sums[index] = "鍚堣";
+ return;
+ }
+ const prop = column.property;
+ if (summaryProps.includes(prop)) {
+ const values = data.map((item) => Number(item[prop]));
+ // 鍙鏈夋晥鏁板瓧杩涜姹傚拰
+ if (!values.every(isNaN)) {
+ const sum = values.reduce(
+ (acc, val) => (!isNaN(val) ? acc + val : acc),
+ 0
+ );
+ if (specialFormat[prop] && specialFormat[prop].noDecimal) {
+ // 濡傛灉鎸囧畾浜嗕笉闇�瑕佷繚鐣欏皬鏁帮紝鍒欑洿鎺ヨ浆鎹负鏁存暟
+ sums[index] = Math.round(sum).toString();
+ } else {
+ // 榛樿淇濈暀涓や綅灏忔暟
+ sums[index] = parseFloat(sum).toFixed(
+ specialFormat[prop]?.decimalPlaces ?? 2
+ );
+ }
+ } else {
+ sums[index] = "";
+ }
+ } else {
+ sums[index] = "";
+ }
+ });
+ return sums;
+};
+// 涓嶅惈绋庢�讳环璁$畻
+const calculateTaxExclusiveTotalPrice = (taxInclusiveTotalPrice, taxRate) => {
+ const taxRateDecimal = taxRate / 100;
+ return (taxInclusiveTotalPrice / (1 + taxRateDecimal)).toFixed(2);
+};
+// 鍚◣鎬讳环璁$畻
+const calculateTaxIncludeTotalPrice = (taxInclusiveUnitPrice, quantity) => {
+ return (taxInclusiveUnitPrice * quantity).toFixed(2);
+};
+// 瀵煎嚭鍑芥暟渚涘叾浠栨枃浠朵娇鐢�
+export {
+ summarizeTable,
+ calculateTaxExclusiveTotalPrice,
+ calculateTaxIncludeTotalPrice,
+};
diff --git a/src/utils/theme.js b/src/utils/theme.js
new file mode 100644
index 0000000..90a511e
--- /dev/null
+++ b/src/utils/theme.js
@@ -0,0 +1,74 @@
+// 澶勭悊涓婚鏍峰紡
+export function handleThemeStyle(theme) {
+ const primary = normalizeHex(theme)
+ const [r, g, b] = hexToRgb(primary)
+ const light2 = getLightColor(primary, 0.2)
+ const light3 = getLightColor(primary, 0.3)
+ const light5 = getLightColor(primary, 0.5)
+
+ document.documentElement.style.setProperty('--el-color-primary', primary)
+ document.documentElement.style.setProperty('--el-color-primary-rgb', `${r}, ${g}, ${b}`)
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(primary, i / 10)}`)
+ }
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(primary, i / 10)}`)
+ }
+
+ // 绯荤粺涓婚鑱斿姩鍒颁晶杈规爮閫変腑鎬佷笌楂樹寒
+ document.documentElement.style.setProperty('--menu-active-bg', `linear-gradient(135deg, ${primary} 0%, ${light3} 100%)`)
+ document.documentElement.style.setProperty('--menu-active-glow', `0 10px 24px rgba(${r}, ${g}, ${b}, 0.32)`)
+ document.documentElement.style.setProperty('--menu-hover', `rgba(${r}, ${g}, ${b}, 0.2)`)
+ document.documentElement.style.setProperty('--accent-primary', primary)
+ document.documentElement.style.setProperty('--accent-light', light2)
+ document.documentElement.style.setProperty('--accent-lighter', light5)
+}
+
+// hex棰滆壊杞瑀gb棰滆壊
+export function hexToRgb(str) {
+ str = normalizeHex(str).replace('#', '')
+ const hexs = str.match(/../g) || ['40', '9e', 'ff']
+ for (let i = 0; i < 3; i++) {
+ hexs[i] = parseInt(hexs[i], 16)
+ }
+ return hexs
+}
+
+// rgb棰滆壊杞琱ex棰滆壊
+export function rgbToHex(r, g, b) {
+ const hexs = [r.toString(16), g.toString(16), b.toString(16)]
+ for (let i = 0; i < 3; i++) {
+ if (hexs[i].length === 1) {
+ hexs[i] = `0${hexs[i]}`
+ }
+ }
+ return `#${hexs.join('')}`
+}
+
+// 鍙樻祬棰滆壊鍊�
+export function getLightColor(color, level) {
+ const rgb = hexToRgb(color)
+ for (let i = 0; i < 3; i++) {
+ rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2])
+}
+
+// 鍙樻繁棰滆壊鍊�
+export function getDarkColor(color, level) {
+ const rgb = hexToRgb(color)
+ for (let i = 0; i < 3; i++) {
+ rgb[i] = Math.floor(rgb[i] * (1 - level))
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2])
+}
+
+function normalizeHex(color) {
+ if (!color || typeof color !== 'string') return '#409eff'
+ let value = color.trim().replace('#', '')
+ if (value.length === 3) {
+ value = value.split('').map((s) => s + s).join('')
+ }
+ if (!/^[0-9a-fA-F]{6}$/.test(value)) return '#409eff'
+ return `#${value.toLowerCase()}`
+}
diff --git a/src/utils/util.js b/src/utils/util.js
new file mode 100644
index 0000000..be08cc1
--- /dev/null
+++ b/src/utils/util.js
@@ -0,0 +1,121 @@
+//闃叉姈
+import dayjs from "dayjs";
+
+export function debounce(fn) {
+ console.log(1)
+ let t = null //鍙細鎵ц涓�娆�
+ debugger
+
+ return function (){
+ if(t){
+ clearTimeout(t)
+ }
+ t = setTimeout(()=>{
+ console.log(temp); //鍙互鑾峰彇
+ // console.log(arguments[0]) //undefined
+ fn.apply(this,arguments)
+ //鍦ㄨ繖涓洖璋冨嚱鏁伴噷闈㈢殑argument鏄繖涓洖璋冨嚱鏁扮殑鍙傛暟锛屽洜涓烘病鏈夊弬鏁版墍浠ndefined锛屽彲浠ラ�氳繃澶栭潰鐨勫嚱鏁拌祴鍊兼潵杩涜璁块棶
+ //涔熷彲浠ユ敼鍙樻垚绠ご鍑芥暟,绠ご鍑芥暟鐨則his鏄寚鍚戝畾涔夊嚱鏁扮殑閭d竴灞傜殑锛屾墍浠ヨ闂埌鐨刟rguments鏄笂涓�灞傚嚱鏁扮殑arguments
+ },1000)
+
+ }
+}
+//鑺傛祦
+export function throttle(fn, delay = 200) {
+ let timer = null
+ console.log(fn);
+ debugger
+ return function () {
+ if(timer) return
+ timer = setTimeout(() => {
+ debugger
+ fn.apply(this,arguments)
+ timer = null
+ })
+ }
+ }
+//涓嬫媺鍔ㄧ敾
+ export function animation(obj, target, fn1) {
+ // console.log(fn1);
+ // fn鏄竴涓洖璋冨嚱鏁帮紝鍦ㄥ畾鏃跺櫒缁撴潫鐨勬椂鍊欐坊鍔�
+ // 姣忔寮�瀹氭椂鍣ㄤ箣鍓嶅厛娓呴櫎鎺夊畾鏃跺櫒
+ clearInterval(obj.timer);
+ obj.timer = setInterval(function () {
+ // 姝ラ暱璁$畻鍏紡 瓒婃潵瓒婂皬
+ // 姝ラ暱鍙栨暣
+ var step = (target - obj.scrollTop) / 10;
+ step = step > 0 ? Math.ceil(step) : Math.floor(step);
+ if (obj.scrollTop >= target) {
+ clearInterval(obj.timer);
+ // 濡傛灉fn1瀛樺湪锛岃皟鐢╢n
+ if (fn1) {
+ fn1();
+ }
+ } else {
+ // 姣�30姣灏卞皢鏂扮殑鍊肩粰obj.left
+ obj.scrollTop = obj.scrollTop + step;
+ }
+ }, 10);
+ }
+
+ //鍒ゆ柇鏂囦欢绫诲瀷
+ export function judgeFileType(file) {
+ if (file == null||file == ""){
+ alert("璇烽�夋嫨瑕佷笂浼犵殑鍥剧墖!");
+ return false;
+ }
+ if (file.lastIndexOf('.')==-1){ //濡傛灉涓嶅瓨鍦�"."
+ alert("璺緞涓嶆纭�!");
+ return false;
+ }
+ var AllImgExt=".jpg|.jpeg|.gif|.bmp|.png|";
+ var extName = file.substring(file.lastIndexOf(".")).toLowerCase();//锛堟妸璺緞涓殑鎵�鏈夊瓧姣嶅叏閮ㄨ浆鎹负灏忓啓锛�
+ if(AllImgExt.indexOf(extName+"|")==-1)
+ {
+ ErrMsg="璇ユ枃浠剁被鍨嬩笉鍏佽涓婁紶銆傝涓婁紶 "+AllImgExt+" 绫诲瀷鐨勬枃浠讹紝褰撳墠鏂囦欢绫诲瀷涓�"+extName;
+ alert(ErrMsg);
+ return false;
+ }
+ }
+
+ //鏂囦欢绫诲瀷
+ export function fileType() {
+ return {
+ 'application/msword': 'word',
+ 'application/pdf': 'pdf',
+ 'application/vnd.ms-powerpoint': 'ppt',
+ 'application/vnd.ms-excel': 'excel',
+ 'aplication/zip': 'zpi',
+ }
+ }
+ export const deepCopySameProperties = (source, target) =>{
+ for (const key in source) {
+ if (target.hasOwnProperty(key)) {
+ if (typeof source[key] === 'object' && source[key] !== null &&
+ typeof target[key] === 'object' && target[key] !== null) {
+ // 閫掑綊澶勭悊瀵硅薄
+ deepCopySameProperties(source[key], target[key]);
+ } else {
+ // 鍩烘湰绫诲瀷鐩存帴璧嬪��
+ target[key] = source[key];
+ }
+ }
+ }
+ return target;
+}
+ export function filterArr(arr) {
+ return arr.filter(item => item.flag !== false);
+ }
+
+ export function getCurrentMonth () {
+ let month = dayjs().month() + 1
+ if (month <= 3) {
+ return '1';
+ } else if (month <= 6) {
+ return '2';
+ } else if (month <= 9) {
+ return '3';
+ } else if (month <= 12) {
+ return '4';
+ }
+}
\ No newline at end of file
diff --git a/src/utils/validate.js b/src/utils/validate.js
new file mode 100644
index 0000000..13b7a15
--- /dev/null
+++ b/src/utils/validate.js
@@ -0,0 +1,114 @@
+/**
+ * 璺緞鍖归厤鍣�
+ * @param {string} pattern
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isPathMatch(pattern, path) {
+ const regexPattern = pattern.replace(/\//g, '\\/').replace(/\*\*/g, '.*').replace(/\*/g, '[^\\/]*')
+ const regex = new RegExp(`^${regexPattern}$`)
+ return regex.test(path)
+}
+
+/**
+ * 鍒ゆ柇value瀛楃涓叉槸鍚︿负绌�
+ * @param {string} value
+ * @returns {Boolean}
+ */
+export function isEmpty(value) {
+ if (value == null || value == "" || value == undefined || value == "undefined") {
+ return true
+ }
+ return false
+}
+
+/**
+ * 鍒ゆ柇url鏄惁鏄痟ttp鎴杊ttps
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function isHttp(url) {
+ return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
+}
+
+/**
+ * 鍒ゆ柇path鏄惁涓哄閾�
+ * @param {string} path
+ * @returns {Boolean}
+ */
+export function isExternal(path) {
+ return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+ const valid_map = ['admin', 'editor']
+ return valid_map.indexOf(str.trim()) >= 0
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+ const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+ return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+ const reg = /^[a-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+ const reg = /^[A-Z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+ const reg = /^[A-Za-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+ return typeof str === 'string' || str instanceof String
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+ if (typeof Array.isArray === 'undefined') {
+ return Object.prototype.toString.call(arg) === '[object Array]'
+ }
+ return Array.isArray(arg)
+}
diff --git a/src/views/aiIndustrialBrain/MAINTAIN_RULES.md b/src/views/aiIndustrialBrain/MAINTAIN_RULES.md
new file mode 100644
index 0000000..97b9a5c
--- /dev/null
+++ b/src/views/aiIndustrialBrain/MAINTAIN_RULES.md
@@ -0,0 +1,7 @@
+# AI宸ヤ笟澶ц剳缁存姢瑙勫垯
+
+1. 褰� `src/views/aiIndustrialBrain/index.vue` 鏂板鏅鸿兘浣擄紙`agents`锛夐�昏緫鏃讹紝蹇呴』鍚屾纭寮圭獥鍔╂墜鍙敤鎬с��
+2. 寮圭獥鍔╂墜鐢� `src/components/AIChatSidebar/assistants/index.js` 鐨� `assistantRegistry` 缁熶竴娉ㄥ唽銆�
+3. 鏂板鏅鸿兘浣撶殑 `key` 鑻ヨ鍦ㄥ脊绐椾腑鍙敤锛屽繀椤诲湪 `assistantRegistry` 涓彁渚涘悓鍚嶉厤缃��
+4. 鏈湪 `assistantRegistry` 娉ㄥ唽鐨勬櫤鑳戒綋浼氬湪寮圭獥涓樉绀轰负 `pending`锛堝紑鍙戜腑锛夋�併��
+
diff --git a/src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue b/src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue
new file mode 100644
index 0000000..1eb5e7b
--- /dev/null
+++ b/src/views/aiIndustrialBrain/components/AiAssistantWorkspace.vue
@@ -0,0 +1,198 @@
+<template>
+ <transition name="fade">
+ <section v-if="visible" class="assistant-workspace">
+ <div class="assistant-workspace__panel">
+ <button
+ v-if="assistantMode === 'pending'"
+ type="button"
+ class="workspace-back-btn"
+ @click="$emit('close')"
+ >
+ <el-icon><ArrowLeftBold /></el-icon>
+ <span>杩斿洖宸ヤ笟澶у睆</span>
+ </button>
+
+ <div class="assistant-workspace__body">
+ <AIChatSidebar
+ v-if="assistantMode !== 'pending'"
+ :key="assistantMode"
+ class="workspace-chat"
+ :assistants="resolvedAssistants"
+ :default-assistant="assistantMode"
+ :hide-trigger="true"
+ :auto-open="true"
+ drawer-size="100%"
+ drawer-direction="ttb"
+ header-extra-action-text="杩斿洖宸ヤ笟澶у睆"
+ @header-extra-action="$emit('close')"
+ />
+
+ <div v-else class="workspace-pending">
+ <div class="workspace-pending__content">
+ <h3>{{ agentTitle }}</h3>
+ <p>姝e湪寮�鍙戯紝鏁鏈熷緟......</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ </transition>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { ArrowLeftBold } from "@element-plus/icons-vue";
+import AIChatSidebar from "@/components/AIChatSidebar/index.vue";
+import { assistantRegistry } from "@/components/AIChatSidebar/assistants";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ default: false,
+ },
+ agent: {
+ type: Object,
+ default: () => ({}),
+ },
+});
+
+defineEmits(["close"]);
+
+const agentKey = computed(() => String(props.agent?.key || ""));
+const agentTitle = computed(() => String(props.agent?.name || "AI鍔╂墜"));
+
+/**
+ * 缁存姢瑙勫垯锛�
+ * AI宸ヤ笟澶ц剳鏂板鏅鸿兘浣撴椂锛岃嫢甯屾湜鍙充晶寮圭獥鍙敤锛岄渶淇濊瘉鏅鸿兘浣� key 鍦� assistantRegistry 涓湁鍚屽悕閰嶇疆銆�
+ * 鏈厤缃椂浼氳繘鍏� pending锛堝紑鍙戜腑锛夋�侊紝浣滀负鏄惧紡鎻愰啋銆�
+ */
+const resolvedAssistant = computed(() => assistantRegistry[agentKey.value] || null);
+const assistantMode = computed(() => {
+ return resolvedAssistant.value ? agentKey.value : "pending";
+});
+const resolvedAssistants = computed(() => (resolvedAssistant.value ? [resolvedAssistant.value] : []));
+</script>
+
+<style scoped>
+.assistant-workspace {
+ position: fixed;
+ inset: 0;
+ z-index: 2100;
+ padding: 12px;
+ background: rgba(33, 49, 63, 0.24);
+ backdrop-filter: blur(2px);
+}
+
+.assistant-workspace__panel {
+ position: relative;
+ height: 100%;
+ border-radius: 22px;
+ border: 1px solid var(--surface-border);
+ background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%);
+ box-shadow: var(--shadow-md);
+ overflow: hidden;
+}
+
+.assistant-workspace__body {
+ height: 100%;
+ min-height: 100%;
+}
+
+.workspace-back-btn {
+ position: absolute;
+ top: 16px;
+ right: 20px;
+ z-index: 5;
+ height: 36px;
+ padding: 0 14px;
+ border: 1px solid rgba(38, 112, 183, 0.3);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.92);
+ color: #25528f;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.workspace-back-btn:hover {
+ border-color: rgba(31, 122, 114, 0.45);
+ color: #1f5ddf;
+ box-shadow: 0 8px 16px rgba(31, 122, 114, 0.14);
+ transform: translateY(-1px);
+}
+
+.workspace-chat {
+ width: 100%;
+ height: 100%;
+}
+
+.workspace-chat :deep(.ai-chat-sidebar-wrapper) {
+ height: 100%;
+}
+
+.workspace-chat :deep(.ai-chat-drawer) {
+ height: 100%;
+}
+
+.workspace-chat :deep(.el-drawer) {
+ height: 100% !important;
+ width: 100% !important;
+}
+
+.workspace-pending {
+ height: 100%;
+ display: grid;
+ place-items: center;
+ padding: 20px;
+ color: var(--text-secondary);
+}
+
+.workspace-pending__content {
+ display: grid;
+ gap: 12px;
+ text-align: center;
+}
+
+.workspace-pending__content h3 {
+ margin: 0;
+ font-size: 36px;
+ color: var(--text-primary);
+}
+
+.workspace-pending__content p {
+ margin: 0;
+ font-size: 24px;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.2s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+@media (max-width: 1600px) {
+ .workspace-back-btn {
+ top: 12px;
+ right: 14px;
+ height: 32px;
+ padding: 0 12px;
+ font-size: 13px;
+ }
+
+ .workspace-pending__content h3 {
+ font-size: 30px;
+ }
+
+ .workspace-pending__content p {
+ font-size: 20px;
+ }
+}
+</style>
diff --git a/src/views/aiIndustrialBrain/index.vue b/src/views/aiIndustrialBrain/index.vue
new file mode 100644
index 0000000..4afafa5
--- /dev/null
+++ b/src/views/aiIndustrialBrain/index.vue
@@ -0,0 +1,1500 @@
+<template>
+ <div ref="screenRef" class="ai-brain-screen">
+ <section class="brain-stage">
+ <header class="brain-head">
+ <div class="head-date">
+ <p>{{ weekLabel }}</p>
+ <p>{{ dateLabel }}</p>
+ </div>
+
+ <div class="head-title">
+ <span>AI宸ヤ笟澶ц剳</span>
+ </div>
+
+ <div class="head-actions">
+ <button type="button" class="head-back-btn" @click="goBack">
+ <el-icon><ArrowLeftBold /></el-icon>
+ <span>杩斿洖</span>
+ </button>
+ </div>
+ </header>
+
+ <section class="brain-intro">
+ <h2>宸ヤ笟AI鏁板瓧鍛樺伐锛岃祴鑳芥櫤閫犳柊绾厓</h2>
+ <p>鍏ぇAI鍔╂墜鍗忓悓浼佷笟绠$悊銆侀攢鍞�侀噰璐�佺敓浜с�佽储鍔″強鏁版嵁鍏ㄩ摼璺�</p>
+ <div class="intro-sign">闃块噷浜� 脳 鍗冮棶澶фā鍨� 脳 鏅鸿兘浣揂I</div>
+ </section>
+
+ <section class="carousel-area">
+ <button type="button" class="nav-btn nav-btn--left" @click="prevCard">
+ <el-icon><ArrowLeftBold /></el-icon>
+ </button>
+
+ <div class="carousel-track">
+ <article
+ v-for="card in visibleCards"
+ :key="card.agent.key"
+ class="agent-card"
+ :class="{ 'agent-card--active': card.offset === 0 }"
+ :style="getCardStyle(card.offset)"
+ @click="openAssistant(card.realIndex)"
+ >
+ <div class="agent-card__head" :class="{ 'agent-card__head--active': card.offset === 0 }">
+ {{ card.agent.name }}
+ </div>
+
+ <div class="agent-card__body" :class="{ 'agent-card__body--active': card.offset === 0 }">
+ <div class="avatar-shell" :class="{ 'avatar-shell--active': card.offset === 0 }">
+ <div class="avatar-base"></div>
+ <div class="avatar-cut">
+ <img v-if="card.agent.avatar" class="avatar-cut__img" :src="card.agent.avatar" :alt="card.agent.name" />
+ </div>
+ </div>
+ <div v-if="card.offset === 0" class="highlight-list">
+ <div
+ v-for="highlight in card.agent.highlights"
+ :key="highlight"
+ class="highlight-item"
+ >
+ {{ highlight }}
+ </div>
+ </div>
+ </div>
+ </article>
+ </div>
+
+ <button type="button" class="nav-btn nav-btn--right" @click="nextCard">
+ <el-icon><ArrowRightBold /></el-icon>
+ </button>
+ </section>
+
+ <section class="brain-footer">
+ <div class="footer-grid-overlay"></div>
+
+ <div class="footer-metrics">
+ <article class="footer-metric">
+ <span class="footer-metric__label">鍦ㄧ嚎鏅鸿兘浣�</span>
+ <strong class="footer-metric__value">{{ agents.length }}涓�</strong>
+ <small class="footer-metric__hint">鍏ㄩ摼璺崗鍚岃繍琛�</small>
+ </article>
+
+ <article class="footer-metric footer-metric--focus">
+ <span class="footer-metric__label">褰撳墠鐒︾偣</span>
+ <strong class="footer-metric__value">{{ getFooterAgentName(focusAgent.name) }}</strong>
+ <small class="footer-metric__hint">{{ focusAgent.highlights?.[0] || "鏅鸿兘鍒嗘瀽鑱斿姩" }}</small>
+ </article>
+
+ <article class="footer-metric footer-metric--period">
+ <span class="footer-metric__label">杞挱鍛ㄦ湡</span>
+ <strong class="footer-metric__value">{{ carouselSecondsText }}</strong>
+ <div class="footer-period-control">
+ <button type="button" class="period-btn" @click="adjustCarouselSeconds(-0.5)">-</button>
+ <input
+ v-model.number="carouselSeconds"
+ class="period-input"
+ type="number"
+ min="2"
+ max="12"
+ step="0.5"
+ />
+ <span class="period-unit">s</span>
+ <button type="button" class="period-btn" @click="adjustCarouselSeconds(0.5)">+</button>
+ </div>
+ <input
+ v-model.number="carouselSeconds"
+ class="footer-period-slider"
+ type="range"
+ min="2"
+ max="12"
+ step="0.5"
+ />
+ <small class="footer-metric__hint">鍙墜鍔ㄨ缃� 2.0s - 12.0s</small>
+ </article>
+ </div>
+
+ <div class="footer-rail">
+ <div class="footer-rail__line">
+ <span class="footer-rail__flow"></span>
+ </div>
+ <div class="footer-rail__nodes">
+ <button
+ v-for="node in footerNodes"
+ :key="node.key"
+ type="button"
+ class="footer-node"
+ :class="{ 'footer-node--active': node.index === carouselIndex }"
+ @click="openAssistant(node.index)"
+ >
+ <span class="footer-node__dot"></span>
+ <span class="footer-node__name">{{ getFooterAgentName(node.name) }}</span>
+ </button>
+ </div>
+ </div>
+ </section>
+ </section>
+
+ <AiAssistantWorkspace
+ :visible="fullscreenVisible"
+ :agent="currentAgent"
+ @close="closeFullscreen"
+ />
+ </div>
+</template>
+
+<script setup>
+import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
+import { useRouter } from "vue-router";
+import { ArrowLeftBold, ArrowRightBold } from "@element-plus/icons-vue";
+import AiAssistantWorkspace from "./components/AiAssistantWorkspace.vue";
+import todoAvatar from "@/assets/AI/寰呭姙鍔╂墜.png";
+import salesAvatar from "@/assets/AI/閿�鍞姪鎵�.png";
+import purchaseAvatar from "@/assets/AI/閲囪喘鍔╂墜.png";
+import productionAvatar from "@/assets/AI/鐢熶骇鍔╂墜.png";
+import financeAvatar from "@/assets/AI/璐㈠姟鍔╂墜.png";
+
+const router = useRouter();
+
+// 缁存姢绾﹀畾瑙侊細src/views/aiIndustrialBrain/MAINTAIN_RULES.md
+const agents = [
+ {
+ key: "general",
+ name: "AI寰呭姙鍔╂墜",
+ highlights: ["璺ㄦā鍧楁祦绋嬭瘖鏂�", "缁忚惀椋庨櫓鏅鸿兘鎻愰啋"],
+ },
+ {
+ key: "sales",
+ name: "AI閿�鍞姪鎵�",
+ highlights: ["瀹㈡埛娴佸け椋庨櫓鍒嗘瀽", "鍥炴涓庢姤浠风瓥鐣ュ缓璁�"],
+ },
+ {
+ key: "purchase",
+ name: "AI閲囪喘鍔╂墜",
+ highlights: ["渚涘簲閾炬寚鏍囧垎鏋�", "閲囪喘璁㈠崟鏅鸿兘鐢熸垚"],
+ },
+ {
+ key: "production",
+ name: "AI鐢熶骇鍔╂墜",
+ highlights: ["宸ュ簭鐡堕瀹氫綅", "浜ц兘涓庢姤搴熸櫤鑳介璀�"],
+ },
+ {
+ key: "finance",
+ name: "AI璐㈠姟鍔╂墜",
+ highlights: ["鐜伴噾娴佸帇鍔涢鍒�", "璐圭敤缁撴瀯鏅鸿兘鍒嗘瀽"],
+ },
+];
+
+const avatarByAgentKey = {
+ general: todoAvatar,
+ sales: salesAvatar,
+ purchase: purchaseAvatar,
+ production: productionAvatar,
+ finance: financeAvatar,
+};
+
+for (let i = agents.length - 1; i >= 0; i -= 1) {
+ const agent = agents[i];
+ const avatar = avatarByAgentKey[agent.key];
+ if (!avatar) {
+ agents.splice(i, 1);
+ continue;
+ }
+ agent.avatar = avatar;
+}
+
+const carouselIndex = ref(Math.min(2, Math.max(agents.length - 1, 0)));
+const fullscreenVisible = ref(false);
+const screenRef = ref(null);
+const carouselIntervalMs = ref(4500);
+
+let carouselTimer = null;
+
+const fallbackAgent = {
+ key: "fallback",
+ name: "AI鍔╂墜",
+ avatar: "",
+ highlights: [],
+};
+
+const currentAgent = computed(() => agents[carouselIndex.value] || agents[0] || fallbackAgent);
+const focusAgent = computed(() => currentAgent.value || fallbackAgent);
+const footerNodes = computed(() =>
+ agents.map((agent, index) => ({
+ key: agent.key,
+ name: agent.name,
+ index,
+ }))
+);
+const carouselSeconds = computed({
+ get: () => Number((carouselIntervalMs.value / 1000).toFixed(1)),
+ set: (value) => {
+ const next = Number(value);
+ if (!Number.isFinite(next)) return;
+ const clamped = Math.max(2, Math.min(12, Math.round(next * 2) / 2));
+ carouselIntervalMs.value = Math.round(clamped * 1000);
+ },
+});
+const carouselSecondsText = computed(() => `${carouselSeconds.value.toFixed(1)}s`);
+
+const weekLabel = computed(() => {
+ const weekMap = ["鏄熸湡鏃�", "鏄熸湡涓�", "鏄熸湡浜�", "鏄熸湡涓�", "鏄熸湡鍥�", "鏄熸湡浜�", "鏄熸湡鍏�"];
+ return weekMap[new Date().getDay()];
+});
+
+const dateLabel = computed(() => {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, "0");
+ const day = String(now.getDate()).padStart(2, "0");
+ return `${year}骞�${month}鏈�${day}鏃;
+});
+
+const visibleCards = computed(() => {
+ const total = agents.length;
+ return agents
+ .map((agent, index) => {
+ let offset = index - carouselIndex.value;
+ if (offset > total / 2) offset -= total;
+ if (offset < -total / 2) offset += total;
+ return { agent, offset, realIndex: index };
+ })
+ .filter((item) => Math.abs(item.offset) <= 2)
+ .sort((a, b) => a.offset - b.offset);
+});
+
+function getCardStyle(offset) {
+ const distance = Math.abs(offset);
+ const scale = distance === 0 ? 1 : distance === 1 ? 0.88 : 0.78;
+ const opacity = distance === 0 ? 1 : distance === 1 ? 0.92 : 0.76;
+ return {
+ transform: `translateX(${offset * 340}px) scale(${scale})`,
+ zIndex: String(50 - distance),
+ opacity,
+ };
+}
+
+function getFooterAgentName(name) {
+ return String(name || "AI鍔╂墜").replace(/^AI/, "");
+}
+
+function adjustCarouselSeconds(delta) {
+ carouselSeconds.value = carouselSeconds.value + delta;
+}
+
+function prevCard() {
+ const total = agents.length;
+ if (!total) return;
+ carouselIndex.value = (carouselIndex.value - 1 + total) % total;
+}
+
+function nextCard() {
+ const total = agents.length;
+ if (!total) return;
+ carouselIndex.value = (carouselIndex.value + 1) % total;
+}
+
+async function enterBrowserFullscreen() {
+ if (document.fullscreenElement) return;
+ const target = screenRef.value || document.documentElement;
+ if (!target || typeof target.requestFullscreen !== "function") return;
+ try {
+ await target.requestFullscreen();
+ } catch (error) {
+ // Ignore: browser may block fullscreen when there is no direct user activation.
+ }
+}
+
+async function exitBrowserFullscreen() {
+ if (!document.fullscreenElement || typeof document.exitFullscreen !== "function") return;
+ try {
+ await document.exitFullscreen();
+ } catch (error) {
+ // Ignore fullscreen exit failures.
+ }
+}
+
+function goBack() {
+ closeFullscreen();
+ exitBrowserFullscreen();
+ if (window.history.length > 1) {
+ router.back();
+ return;
+ }
+ router.push("/index");
+}
+
+function openAssistant(index) {
+ if (!agents.length) return;
+ carouselIndex.value = index;
+ fullscreenVisible.value = true;
+}
+
+function closeFullscreen() {
+ fullscreenVisible.value = false;
+}
+
+function startCarousel() {
+ stopCarousel();
+ if (fullscreenVisible.value) return;
+ carouselTimer = window.setInterval(() => {
+ nextCard();
+ }, carouselIntervalMs.value);
+}
+
+function stopCarousel() {
+ if (carouselTimer) {
+ window.clearInterval(carouselTimer);
+ carouselTimer = null;
+ }
+}
+
+function handleEscClose(event) {
+ if (event.key === "Escape" && fullscreenVisible.value) {
+ closeFullscreen();
+ }
+}
+
+watch(
+ () => fullscreenVisible.value,
+ (opened) => {
+ if (opened) {
+ stopCarousel();
+ } else {
+ startCarousel();
+ }
+ }
+);
+
+watch(
+ () => carouselIntervalMs.value,
+ () => {
+ if (!fullscreenVisible.value) {
+ startCarousel();
+ }
+ }
+);
+
+onMounted(() => {
+ startCarousel();
+ window.addEventListener("keydown", handleEscClose);
+ window.requestAnimationFrame(() => {
+ enterBrowserFullscreen();
+ });
+});
+
+onBeforeUnmount(() => {
+ stopCarousel();
+ window.removeEventListener("keydown", handleEscClose);
+ exitBrowserFullscreen();
+});
+</script>
+
+<style scoped>
+.ai-brain-screen {
+ position: fixed;
+ inset: 0;
+ z-index: 1900;
+ padding: 10px;
+ overflow: hidden;
+ background: var(--app-bg);
+}
+
+.brain-stage {
+ position: relative;
+ height: 100%;
+ min-height: 100%;
+ border-radius: 22px;
+ border: 1px solid var(--surface-border);
+ background:
+ radial-gradient(circle at 14% 8%, rgba(31, 122, 114, 0.14), transparent 40%),
+ radial-gradient(circle at 86% 12%, rgba(30, 91, 255, 0.1), transparent 42%),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.95), rgba(245, 249, 247, 0.94)),
+ repeating-linear-gradient(
+ 135deg,
+ rgba(255, 255, 255, 0.05) 0,
+ rgba(255, 255, 255, 0.05) 14px,
+ rgba(31, 122, 114, 0.03) 14px,
+ rgba(31, 122, 114, 0.03) 28px
+ );
+ box-shadow: var(--shadow-sm);
+}
+
+.brain-head {
+ display: grid;
+ grid-template-columns: 220px minmax(0, 1fr) 180px;
+ align-items: center;
+ padding: 12px 18px 0;
+}
+
+.head-date {
+ color: var(--text-secondary);
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.head-date p {
+ margin: 0;
+ line-height: 1.2;
+}
+
+.head-title {
+ justify-self: center;
+ width: min(760px, 95%);
+ height: 68px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0 0 46px 46px;
+ color: #fff;
+ font-size: 42px;
+ font-style: italic;
+ font-weight: 700;
+ letter-spacing: 1px;
+ background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
+ box-shadow: 0 16px 30px rgba(31, 122, 114, 0.24);
+}
+
+.head-actions {
+ justify-self: end;
+}
+
+.head-back-btn {
+ height: 40px;
+ padding: 0 14px;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ border: none;
+ border-radius: 999px;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--colorPrimary);
+ background: var(--surface-base);
+ box-shadow: 0 8px 18px rgba(31, 49, 38, 0.12);
+ cursor: pointer;
+}
+
+.brain-intro {
+ text-align: center;
+ margin-top: 34px;
+}
+
+.brain-intro h2 {
+ margin: 0;
+ font-size: 44px;
+ font-style: italic;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+.brain-intro p {
+ margin: 12px 0 10px;
+ font-size: 28px;
+ color: var(--text-secondary);
+}
+
+.intro-sign {
+ display: inline-block;
+ padding: 6px 18px;
+ border-radius: 999px;
+ font-size: 24px;
+ font-weight: 700;
+ color: #1e5bff;
+ background: rgba(255, 255, 255, 0.82);
+ border: 1px solid rgba(30, 91, 255, 0.18);
+}
+
+.carousel-area {
+ position: relative;
+ margin-top: 34px;
+ padding: 0 72px 12px;
+}
+
+.carousel-track {
+ position: relative;
+ height: 500px;
+ overflow: hidden;
+}
+
+.brain-footer {
+ position: relative;
+ margin: 0 72px;
+ height: clamp(226px, 25vh,0);
+ border-radius: 18px;
+ border: 1px solid rgba(31, 122, 114, 0.28);
+ background:
+ linear-gradient(120deg, rgba(31, 122, 114, 0.14), rgba(30, 91, 255, 0.14)),
+ linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(236, 244, 249, 0.9));
+ box-shadow:
+ 0 16px 34px rgba(31, 81, 131, 0.12),
+ inset 0 1px 0 rgba(255, 255, 255, 0.72);
+ overflow: hidden;
+}
+
+.brain-footer::before {
+ content: "";
+ position: absolute;
+ left: -22%;
+ bottom: -120%;
+ width: 52%;
+ height: 260%;
+ background: radial-gradient(ellipse at center, rgba(30, 91, 255, 0.2) 0%, rgba(30, 91, 255, 0) 72%);
+ pointer-events: none;
+}
+
+.brain-footer::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(110deg, transparent 12%, rgba(255, 255, 255, 0.24) 38%, transparent 64%);
+ transform: translateX(-120%);
+ animation: footerSweep 5.8s linear infinite;
+ pointer-events: none;
+}
+
+.footer-grid-overlay {
+ position: absolute;
+ inset: 0;
+ background:
+ repeating-linear-gradient(
+ 90deg,
+ rgba(31, 122, 114, 0.07) 0,
+ rgba(31, 122, 114, 0.07) 1px,
+ transparent 1px,
+ transparent 36px
+ ),
+ repeating-linear-gradient(
+ 0deg,
+ rgba(30, 91, 255, 0.06) 0,
+ rgba(30, 91, 255, 0.06) 1px,
+ transparent 1px,
+ transparent 28px
+ );
+ opacity: 0.72;
+ pointer-events: none;
+}
+
+.footer-metrics {
+ position: relative;
+ z-index: 2;
+ padding: 14px 20px 72px;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.footer-metric {
+ min-height: 76px;
+ border-radius: 12px;
+ padding: 10px 14px;
+ border: 1px solid rgba(37, 124, 188, 0.2);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(245, 250, 255, 0.82));
+ box-shadow: 0 10px 18px rgba(29, 83, 134, 0.08);
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ gap: 4px;
+}
+
+.footer-metric--focus {
+ border-color: rgba(38, 122, 194, 0.34);
+ box-shadow:
+ 0 12px 22px rgba(30, 91, 255, 0.12),
+ inset 0 0 0 1px rgba(85, 148, 232, 0.2);
+}
+
+.footer-metric__label {
+ font-size: 14px;
+ color: rgba(38, 72, 108, 0.88);
+ font-weight: 600;
+}
+
+.footer-metric__value {
+ font-size: 30px;
+ line-height: 1;
+ font-style: italic;
+ font-weight: 700;
+ color: #1f5ddf;
+ text-shadow: 0 3px 10px rgba(30, 91, 255, 0.18);
+}
+
+.footer-metric__hint {
+ margin-top: auto;
+ font-size: 13px;
+ color: rgba(52, 89, 128, 0.82);
+}
+
+.footer-metric--period .footer-metric__hint {
+ margin-top: 0;
+ line-height: 1.25;
+}
+
+.footer-metric--period {
+ min-height: 122px;
+ grid-template-rows: auto auto auto auto auto;
+ gap: 6px;
+}
+
+.footer-period-control {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.period-btn {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: 1px solid rgba(38, 112, 183, 0.28);
+ background: rgba(255, 255, 255, 0.9);
+ color: #2054c9;
+ font-size: 14px;
+ font-weight: 700;
+ line-height: 1;
+ cursor: pointer;
+}
+
+.period-input {
+ width: 56px;
+ height: 24px;
+ border-radius: 8px;
+ border: 1px solid rgba(38, 112, 183, 0.24);
+ background: rgba(255, 255, 255, 0.94);
+ color: #1f5ddf;
+ font-size: 13px;
+ font-weight: 600;
+ text-align: center;
+ padding: 0 4px;
+}
+
+.period-unit {
+ font-size: 12px;
+ font-weight: 600;
+ color: rgba(40, 80, 117, 0.86);
+}
+
+.footer-period-slider {
+ width: min(250px, 100%);
+ height: 3px;
+ accent-color: #2a6ded;
+ cursor: pointer;
+}
+
+.footer-rail {
+ position: absolute;
+ left: 20px;
+ right: 20px;
+ bottom: 18px;
+ z-index: 2;
+}
+
+.footer-rail__line {
+ position: relative;
+ height: 2px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, rgba(31, 122, 114, 0.12), rgba(30, 91, 255, 0.6), rgba(31, 122, 114, 0.12));
+ overflow: hidden;
+}
+
+.footer-rail__flow {
+ position: absolute;
+ top: -1px;
+ left: -18%;
+ width: 22%;
+ height: 4px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, rgba(31, 122, 114, 0), rgba(59, 146, 244, 0.92), rgba(30, 91, 255, 0));
+ filter: blur(0.2px);
+ animation: railFlow 3.1s ease-in-out infinite;
+}
+
+.footer-rail__nodes {
+ margin-top: 12px;
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.footer-node {
+ height: 34px;
+ border: 1px solid rgba(38, 112, 183, 0.18);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.76);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ color: rgba(40, 80, 117, 0.92);
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.26s ease;
+}
+
+.footer-node:hover {
+ transform: translateY(-1px);
+ border-color: rgba(31, 122, 114, 0.34);
+ box-shadow: 0 8px 14px rgba(31, 122, 114, 0.14);
+}
+
+.footer-node--active {
+ color: #fff;
+ border-color: transparent;
+ background: linear-gradient(135deg, rgba(31, 122, 114, 0.94), rgba(30, 91, 255, 0.94));
+ box-shadow: 0 10px 18px rgba(30, 91, 255, 0.28);
+}
+
+.footer-node__dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: rgba(30, 91, 255, 0.72);
+ box-shadow: 0 0 10px rgba(30, 91, 255, 0.52);
+}
+
+.footer-node--active .footer-node__dot {
+ background: #fff;
+ box-shadow: 0 0 12px rgba(255, 255, 255, 0.72);
+ animation: nodePulse 1.4s ease-in-out infinite;
+}
+
+.agent-card {
+ position: absolute;
+ left: 50%;
+ top: 0;
+ width: 460px;
+ margin-left: -230px;
+ cursor: pointer;
+ transform-origin: center bottom;
+ transition: transform 0.35s ease, opacity 0.35s ease;
+}
+
+.agent-card__head {
+ height: 56px;
+ border-radius: 12px 12px 0 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28px;
+ color: #fff;
+ font-weight: 700;
+ background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
+}
+
+.agent-card__head--active {
+ box-shadow:
+ 0 12px 22px rgba(30, 91, 255, 0.26),
+ inset 0 0 0 1px rgba(255, 255, 255, 0.28);
+ position: relative;
+}
+
+.agent-card__head--active::after {
+ content: "";
+ position: absolute;
+ left: 12px;
+ right: 12px;
+ bottom: 6px;
+ height: 3px;
+ border-radius: 999px;
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.96), rgba(255, 255, 255, 0.22));
+}
+
+.agent-card__body {
+ position: relative;
+ height: 430px;
+ border: 1px solid var(--surface-border-strong);
+ border-top: none;
+ border-radius: 0 0 20px 20px;
+ background: rgba(255, 255, 255, 0.96);
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: flex-end;
+ isolation: isolate;
+ box-shadow: 0 12px 24px rgba(31, 49, 38, 0.1);
+}
+
+.agent-card__body--active {
+ background: linear-gradient(180deg, rgba(248, 252, 251, 0.96), rgba(225, 241, 250, 0.9));
+ border-color: rgba(31, 122, 114, 0.35);
+}
+
+.agent-card__body--active::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: linear-gradient(108deg, transparent 28%, rgba(255, 255, 255, 0.34) 50%, transparent 72%);
+ transform: translateX(-125%);
+ animation: bodySweep 3.6s linear infinite;
+ pointer-events: none;
+ z-index: 1;
+}
+
+.avatar-shell {
+ position: relative;
+ width: 248px;
+ height: 430px;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ --base-core: rgba(53, 143, 222, 0.4);
+ --base-ring: rgba(39, 122, 201, 0.62);
+ --base-glow: rgba(46, 133, 214, 0.28);
+}
+
+.avatar-shell::before {
+ content: "";
+ position: absolute;
+ left: 50%;
+ bottom: -10px;
+ width: 268px;
+ height: 58px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ background: radial-gradient(
+ ellipse at center,
+ rgba(55, 140, 219, 0.22) 0%,
+ rgba(55, 140, 219, 0.11) 46%,
+ rgba(55, 140, 219, 0) 74%
+ );
+ filter: blur(2.4px);
+ animation: baseGlow 4.6s ease-in-out infinite;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.avatar-shell::after {
+ content: "";
+ position: absolute;
+ left: 50%;
+ bottom: 0;
+ width: 248px;
+ height: 46px;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ border: 2px solid var(--base-ring);
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.64),
+ 0 0 24px var(--base-glow);
+ animation: basePulse 3.1s ease-in-out infinite;
+ z-index: 4;
+}
+
+.avatar-base {
+ position: absolute;
+ left: 50%;
+ bottom: 2px;
+ width: 224px;
+ height: 38px;
+ transform: translateX(-50%);
+ z-index: 2;
+ pointer-events: none;
+}
+
+.avatar-base::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background:
+ radial-gradient(
+ ellipse at center,
+ rgba(255, 255, 255, 0.96) 0%,
+ rgba(255, 255, 255, 0.92) 36%,
+ var(--base-core) 68%,
+ rgba(38, 118, 195, 0.08) 100%
+ );
+ box-shadow:
+ 0 0 30px var(--base-core),
+ 0 0 10px rgba(255, 255, 255, 0.34) inset;
+ z-index: 3;
+}
+
+.avatar-base::after {
+ content: "";
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ width: 194px;
+ height: 194px;
+ transform: translate(-50%, -50%);
+ border-radius: 50%;
+ background:
+ conic-gradient(
+ from 180deg,
+ transparent 0deg,
+ var(--base-ring) 48deg,
+ transparent 112deg,
+ var(--base-ring) 208deg,
+ transparent 284deg,
+ rgba(33, 114, 191, 0.48) 332deg,
+ transparent 360deg
+ );
+ -webkit-mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ mask: radial-gradient(circle, transparent 61%, #000 62%, #000 68%, transparent 70%);
+ opacity: 0.62;
+ animation: baseRotate 10.5s linear infinite;
+ z-index: 2;
+}
+
+.avatar-shell--active {
+ --base-core: rgba(50, 141, 217, 0.52);
+ --base-ring: rgba(42, 127, 205, 0.76);
+ --base-glow: rgba(38, 130, 211, 0.38);
+}
+
+.avatar-cut {
+ position: relative;
+ width: 220px;
+ height: 430px;
+ z-index: 6;
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ filter: saturate(1.04) drop-shadow(0 14px 18px rgba(24, 44, 66, 0.14));
+ transform-origin: center 82%;
+ animation: avatarFloat 3.2s ease-in-out infinite;
+}
+
+.avatar-cut__img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ object-position: center bottom;
+ display: block;
+}
+
+.agent-card--active .avatar-cut {
+ animation-duration: 2.6s;
+}
+
+.highlight-list {
+ position: absolute;
+ right: 10px;
+ top: 14px;
+ display: grid;
+ gap: 8px;
+ width: 220px;
+ z-index: 16;
+}
+
+.highlight-item {
+ border-radius: 10px;
+ padding: 8px 10px;
+ font-size: 18px;
+ line-height: 1.4;
+ color: #fff;
+ background: rgba(33, 49, 63, 0.92);
+ box-shadow: 0 8px 16px rgba(21, 30, 40, 0.22);
+}
+
+.agent-card--active .highlight-item {
+ background: rgba(31, 122, 114, 0.9);
+}
+
+.nav-btn {
+ position: absolute;
+ top: 212px;
+ z-index: 80;
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ border: none;
+ font-size: 30px;
+ color: var(--colorPrimary);
+ background: var(--surface-base);
+ box-shadow: 0 10px 20px rgba(31, 49, 38, 0.16);
+ cursor: pointer;
+}
+
+.nav-btn--left {
+ left: 14px;
+}
+
+.nav-btn--right {
+ right: 14px;
+}
+
+.ai-fullscreen {
+ position: fixed;
+ inset: 0;
+ z-index: 2100;
+ padding: 12px;
+ background: rgba(33, 49, 63, 0.24);
+ backdrop-filter: blur(2px);
+}
+
+.ai-panel {
+ height: 100%;
+ border-radius: 22px;
+ border: 1px solid var(--surface-border);
+ background: linear-gradient(180deg, #f9fcfb 0%, #f0f5f2 100%);
+ display: grid;
+ grid-template-rows: 62px minmax(0, 1fr) 110px;
+ box-shadow: var(--shadow-md);
+}
+
+.ai-panel__top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 24px;
+}
+
+.ai-brand {
+ font-size: 34px;
+ color: var(--text-primary);
+ font-weight: 700;
+}
+
+.ai-close {
+ width: 40px;
+ height: 40px;
+ border: none;
+ border-radius: 50%;
+ background: transparent;
+ font-size: 30px;
+ color: var(--text-secondary);
+ cursor: pointer;
+}
+
+.ai-panel__center {
+ padding: 8px 20px 10px;
+ display: grid;
+ grid-template-rows: 120px 290px minmax(0, 1fr);
+ gap: 10px;
+ min-height: 0;
+}
+
+.welcome-card {
+ border-radius: 14px;
+ background: linear-gradient(135deg, rgba(232, 244, 242, 0.95), rgba(230, 237, 250, 0.9));
+ padding: 16px 18px;
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ border: 1px solid var(--surface-border);
+}
+
+.welcome-card__text h3 {
+ margin: 0;
+ font-size: 28px;
+ color: var(--text-primary);
+}
+
+.welcome-card__text p {
+ margin: 8px 0 0;
+ font-size: 20px;
+ color: var(--text-secondary);
+}
+
+.mini-avatar {
+ width: 120px;
+ height: 120px;
+ border-radius: 14px;
+ border: 1px solid var(--surface-border);
+ background-color: #fff;
+ background-clip: border-box;
+ overflow: hidden;
+}
+
+.mini-avatar__img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ object-position: center bottom;
+ display: block;
+}
+
+.recommend-card {
+ border-radius: 14px;
+ border: 1px solid var(--surface-border);
+ background: rgba(255, 255, 255, 0.86);
+ padding: 12px 14px;
+}
+
+.recommend-card__head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+ color: var(--text-primary);
+ font-size: 24px;
+ font-weight: 700;
+}
+
+.refresh-btn {
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ font-size: 18px;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+}
+
+.recommend-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 8px 18px;
+}
+
+.recommend-item {
+ border: 1px solid var(--surface-border);
+ border-radius: 8px;
+ text-align: left;
+ padding: 8px 10px;
+ font-size: 18px;
+ color: var(--text-secondary);
+ background: #fff;
+ cursor: pointer;
+}
+
+.recommend-item:hover {
+ background: rgba(31, 122, 114, 0.08);
+ color: var(--colorPrimary);
+}
+
+.chat-card {
+ border-radius: 14px;
+ border: 1px solid var(--surface-border);
+ background: #fff;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.chat-empty {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-tertiary);
+ font-size: 18px;
+}
+
+.chat-messages {
+ height: 100%;
+ overflow-y: auto;
+ padding: 14px;
+ display: grid;
+ gap: 10px;
+}
+
+.chat-row {
+ display: flex;
+}
+
+.chat-row--assistant {
+ justify-content: flex-start;
+}
+
+.chat-row--user {
+ justify-content: flex-end;
+}
+
+.chat-bubble {
+ max-width: 72%;
+ border-radius: 12px;
+ padding: 10px 12px;
+ font-size: 18px;
+ line-height: 1.5;
+ white-space: pre-wrap;
+ color: var(--text-primary);
+ background: var(--surface-soft);
+ border: 1px solid var(--surface-border);
+}
+
+.chat-row--user .chat-bubble {
+ color: #fff;
+ background: linear-gradient(135deg, #1f7a72 0%, #1e5bff 100%);
+ border: none;
+}
+
+.ai-panel__input {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 130px;
+ gap: 12px;
+ padding: 14px 20px 18px;
+}
+
+.ask-input :deep(.el-input__wrapper) {
+ height: 74px;
+ border-radius: 18px;
+ box-shadow: 0 0 0 1px var(--surface-border) inset;
+ background: #fff;
+}
+
+.ask-input :deep(.el-input__inner) {
+ font-size: 20px;
+}
+
+.send-btn {
+ align-self: center;
+ height: 56px;
+ font-size: 20px;
+ min-width: 98px;
+}
+
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.2s ease;
+}
+
+.fade-enter-from,
+.fade-leave-to {
+ opacity: 0;
+}
+
+@keyframes avatarFloat {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+ 50% {
+ transform: translateY(-8px);
+ }
+}
+
+@keyframes basePulse {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scale(1);
+ opacity: 0.88;
+ }
+ 50% {
+ transform: translateX(-50%) scale(1.045);
+ opacity: 0.95;
+ }
+}
+
+@keyframes baseRotate {
+ from {
+ transform: translate(-50%, -50%) rotate(0deg);
+ }
+ to {
+ transform: translate(-50%, -50%) rotate(360deg);
+ }
+}
+
+@keyframes baseGlow {
+ 0%,
+ 100% {
+ transform: translateX(-50%) scaleX(1);
+ opacity: 0.84;
+ }
+ 50% {
+ transform: translateX(-50%) scaleX(1.06);
+ opacity: 0.96;
+ }
+}
+
+@keyframes bodySweep {
+ 0% {
+ transform: translateX(-125%);
+ }
+ 100% {
+ transform: translateX(135%);
+ }
+}
+
+@keyframes footerSweep {
+ 0% {
+ transform: translateX(-120%);
+ }
+ 100% {
+ transform: translateX(140%);
+ }
+}
+
+@keyframes railFlow {
+ 0% {
+ transform: translateX(0);
+ opacity: 0;
+ }
+ 20% {
+ opacity: 1;
+ }
+ 80% {
+ opacity: 1;
+ }
+ 100% {
+ transform: translateX(520%);
+ opacity: 0;
+ }
+}
+
+@keyframes nodePulse {
+ 0%,
+ 100% {
+ transform: scale(1);
+ }
+ 50% {
+ transform: scale(1.25);
+ }
+}
+
+@media (max-width: 1600px) {
+ .head-title {
+ font-size: 34px;
+ height: 60px;
+ }
+
+ .brain-intro h2 {
+ font-size: 36px;
+ }
+
+ .brain-intro p {
+ font-size: 22px;
+ }
+
+ .intro-sign {
+ font-size: 20px;
+ }
+
+ .agent-card {
+ width: 380px;
+ margin-left: -190px;
+ }
+
+ .agent-card__head {
+ font-size: 24px;
+ height: 54px;
+ }
+
+ .agent-card__body {
+ height: 390px;
+ }
+
+ .highlight-list {
+ width: 184px;
+ }
+
+ .highlight-item {
+ font-size: 15px;
+ }
+
+ .avatar-shell {
+ width: 220px;
+ height: 390px;
+ }
+
+ .avatar-cut {
+ width: 202px;
+ height: 390px;
+ }
+
+ .avatar-base {
+ width: 194px;
+ height: 34px;
+ }
+
+ .avatar-base::after {
+ width: 164px;
+ height: 164px;
+ }
+
+ .avatar-shell::before {
+ width: 236px;
+ height: 48px;
+ bottom: -9px;
+ }
+
+ .avatar-shell::after {
+ width: 220px;
+ height: 40px;
+ bottom: 0;
+ }
+
+ .brain-footer {
+ margin: 0 52px;
+ height: clamp(210px, 23vh, 264px);
+ }
+
+ .footer-metrics {
+ padding: 12px 14px 66px;
+ gap: 8px;
+ }
+
+ .footer-metric {
+ min-height: 66px;
+ padding: 8px 10px;
+ }
+
+ .footer-metric--period {
+ min-height: 108px;
+ gap: 4px;
+ }
+
+ .footer-metric__label {
+ font-size: 12px;
+ }
+
+ .footer-metric__value {
+ font-size: 24px;
+ }
+
+ .footer-metric__hint {
+ font-size: 11px;
+ }
+
+ .footer-period-control {
+ gap: 6px;
+ }
+
+ .period-btn {
+ width: 20px;
+ height: 20px;
+ font-size: 12px;
+ }
+
+ .period-input {
+ width: 50px;
+ height: 22px;
+ font-size: 12px;
+ }
+
+ .footer-period-slider {
+ width: 100%;
+ }
+
+ .footer-rail {
+ left: 14px;
+ right: 14px;
+ bottom: 14px;
+ }
+
+ .footer-rail__nodes {
+ margin-top: 10px;
+ gap: 6px;
+ }
+
+ .footer-node {
+ height: 30px;
+ font-size: 12px;
+ gap: 6px;
+ }
+
+ .footer-node__dot {
+ width: 7px;
+ height: 7px;
+ }
+
+ .ai-brand {
+ font-size: 28px;
+ }
+
+ .welcome-card__text h3,
+ .recommend-card__head {
+ font-size: 22px;
+ }
+
+ .welcome-card__text p,
+ .recommend-item,
+ .chat-bubble,
+ .refresh-btn,
+ .chat-empty,
+ .ask-input :deep(.el-input__inner),
+ .send-btn {
+ font-size: 16px;
+ }
+}
+</style>
diff --git a/src/views/basicData/customerFileOpenSea/index.vue b/src/views/basicData/customerFileOpenSea/index.vue
new file mode 100644
index 0000000..bad28e1
--- /dev/null
+++ b/src/views/basicData/customerFileOpenSea/index.vue
@@ -0,0 +1,1803 @@
+<template>
+ <div class="app-container">
+ <div class="search_form" style="margin-bottom: 20px;">
+ <div>
+ <span class="search_title">瀹㈡埛鍚嶇О锛�</span>
+ <el-input v-model="searchForm.customerName"
+ style="width: 240px;margin-right: 10px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span class="search_title">瀹㈡埛鍒嗙被锛�</span>
+ <el-select v-model="searchForm.customerType"
+ placeholder="璇烽�夋嫨"
+ style="width: 240px"
+ clearable
+ @change="handleQuery">
+ <el-option label="闆跺敭瀹㈡埛"
+ value="闆跺敭瀹㈡埛" />
+ <el-option label="杩涢攢鍟嗗鎴�"
+ value="杩涢攢鍟嗗鎴�" />
+ </el-select>
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板瀹㈡埛</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="info"
+ plain
+ icon="Upload"
+ @click="handleImport">瀵煎叆</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"></PIMTable>
+ </div>
+ <el-dialog v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板瀹㈡埛淇℃伅' : '缂栬緫瀹㈡埛淇℃伅'"
+ width="70%"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�"
+ prop="customerName">
+ <el-input v-model="form.customerName"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绾崇◣浜鸿瘑鍒彿锛�"
+ prop="taxpayerIdentificationNumber">
+ <el-input v-model="form.taxpayerIdentificationNumber"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍏徃鍦板潃锛�"
+ prop="companyAddress">
+ <el-input v-model="form.companyAddress"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏徃鐢佃瘽锛�"
+ prop="companyPhone">
+ <el-input v-model="form.companyPhone"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="閾惰鍩烘湰鎴凤細"
+ prop="basicBankAccount">
+ <el-input v-model="form.basicBankAccount"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閾惰璐﹀彿锛�"
+ prop="bankAccount">
+ <el-input v-model="form.bankAccount"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="寮�鎴疯鍙凤細"
+ prop="bankCode">
+ <el-input v-model="form.bankCode"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍒嗙被锛�"
+ prop="customerType">
+ <el-select v-model="form.customerType"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option label="闆跺敭瀹㈡埛"
+ value="闆跺敭瀹㈡埛" />
+ <el-option label="杩涢攢鍟嗗鎴�"
+ value="杩涢攢鍟嗗鎴�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30"
+ v-for="(contact, index) in formYYs.contactList"
+ :key="index">
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴浜猴細"
+ prop="contactPerson">
+ <el-input v-model="contact.contactPerson"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽锛�"
+ prop="contactPhone">
+ <div style="display: flex; align-items: center;width: 100%;">
+ <el-input v-model="contact.contactPhone"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ <el-button @click="removeContact(index)"
+ type="danger"
+ circle
+ style="margin-left: 5px;">
+ <el-icon>
+ <Close />
+ </el-icon>
+ </el-button>
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-button @click="addNewContact"
+ style="margin-bottom: 10px;">+ 鏂板鑱旂郴浜�</el-button>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="缁存姢浜猴細"
+ prop="maintainer">
+ <el-select v-model="form.maintainer"
+ placeholder="璇烽�夋嫨"
+ clearable
+ disabled>
+ <el-option v-for="item in userList"
+ :key="item.nickName"
+ :label="item.nickName"
+ :value="item.nickName" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁存姢鏃堕棿锛�"
+ prop="maintenanceTime">
+ <el-date-picker style="width: 100%"
+ v-model="form.maintenanceTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <el-dialog v-model="assignDialogVisible"
+ title="鍒嗛厤瀹㈡埛"
+ width="500px"
+ @close="closeAssignDialog">
+ <el-form :model="assignForm"
+ :rules="assignRules"
+ ref="assignFormRef"
+ label-width="100px">
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <el-input v-model="assignForm.customerName"
+ disabled />
+ </el-form-item>
+ <el-form-item label="鍒嗛厤浜哄憳"
+ prop="boundId">
+ <el-select v-model="assignForm.boundId"
+ placeholder="璇烽�夋嫨鍒嗛厤浜哄憳"
+ style="width: 100%"
+ filterable>
+ <el-option v-for="item in userList"
+ :key="item.userId || item.nickName"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitAssignForm">纭</el-button>
+ <el-button @click="closeAssignDialog">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <el-dialog v-model="shareDialogVisible"
+ title="鍏变韩瀹㈡埛"
+ width="500px"
+ @close="closeShareDialog">
+ <el-form :model="shareForm"
+ :rules="shareRules"
+ ref="shareFormRef"
+ label-width="100px">
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <el-input v-model="shareForm.customerName"
+ disabled />
+ </el-form-item>
+ <el-form-item label="鍏变韩浜哄憳"
+ prop="boundIds">
+ <el-select v-model="shareForm.boundIds"
+ placeholder="璇烽�夋嫨鍏变韩浜哄憳"
+ style="width: 100%"
+ filterable
+ multiple
+ collapse-tags
+ collapse-tags-tooltip>
+ <el-option v-for="item in userList"
+ :key="item.userId || item.nickName"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitShareForm">纭</el-button>
+ <el-button @click="closeShareDialog">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 鐢ㄦ埛瀵煎叆瀵硅瘽妗� -->
+ <el-dialog :title="upload.title"
+ v-model="upload.open"
+ width="400px"
+ append-to-body>
+ <el-upload ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url"
+ :data="upload.data"
+ :disabled="upload.isUploading"
+ :before-upload="upload.beforeUpload"
+ :on-progress="upload.onProgress"
+ :on-success="upload.onSuccess"
+ :on-error="upload.onError"
+ :on-change="upload.onChange"
+ :auto-upload="false"
+ drag>
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link type="primary"
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ @click="importTemplate">涓嬭浇妯℃澘</el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 鍥炶鎻愰啋瀵硅瘽妗� -->
+ <el-dialog title="鍥炶鎻愰啋"
+ v-model="reminderDialogVisible"
+ width="500px"
+ @close="closeReminderDialog">
+ <el-form :model="reminderForm"
+ label-width="100px"
+ :rules="reminderRules"
+ ref="reminderFormRef">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�">
+ <el-input v-model="reminderForm.customerName"
+ disabled />
+ </el-form-item>
+ <el-form-item label="鎻愰啋寮�鍏筹細">
+ <el-switch v-model="reminderForm.reminderSwitch" />
+ </el-form-item>
+ <el-form-item label="鎻愰啋鍐呭锛�"
+ prop="reminderContent">
+ <el-input v-model="reminderForm.reminderContent"
+ type="textarea"
+ :maxlength="100"
+ show-word-limit
+ placeholder="璇疯緭鍏ユ彁閱掑唴瀹�" />
+ </el-form-item>
+ <el-form-item label="鎻愰啋鏃堕棿锛�"
+ prop="reminderTime">
+ <el-date-picker v-model="reminderForm.reminderTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm:ss"
+ placeholder="璇烽�夋嫨鎻愰啋鏃堕棿"
+ style="width: 100%" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitReminderForm">纭</el-button>
+ <el-button @click="closeReminderDialog">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 娣诲姞/淇敼娲借皥杩涘害瀵硅瘽妗� -->
+ <el-dialog :title="negotiationForm.editIndex !== undefined ? '淇敼杩涘害' : '娣诲姞杩涘害'"
+ v-model="negotiationDialogVisible"
+ width="600px"
+ @close="closeNegotiationDialog">
+ <el-form :model="negotiationForm"
+ label-width="100px"
+ :rules="negotiationRules"
+ ref="negotiationFormRef">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璺熻繘鏂瑰紡锛�"
+ prop="followUpMethod">
+ <el-select v-model="negotiationForm.followUpMethod"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%">
+ <el-option label="鐢佃瘽"
+ value="鐢佃瘽" />
+ <el-option label="閭欢"
+ value="閭欢" />
+ <el-option label="涓婇棬"
+ value="涓婇棬" />
+ <el-option label="寰俊"
+ value="寰俊" />
+ <el-option label="鍏朵粬"
+ value="鍏朵粬" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璺熻繘绋嬪害锛�"
+ prop="followUpLevel">
+ <el-select v-model="negotiationForm.followUpLevel"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%">
+ <el-option label="娼滃湪瀹㈡埛"
+ value="娼滃湪瀹㈡埛" />
+ <el-option label="鍒濇鎷滆"
+ value="鍒濇鎷滆" />
+ <el-option label="澶氭鎷滆"
+ value="澶氭鎷滆" />
+ <el-option label="鎰忓悜瀹㈡埛"
+ value="鎰忓悜瀹㈡埛" />
+ <el-option label="宸茬绾﹀鎴�"
+ value="宸茬绾﹀鎴�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璺熻繘鏃堕棿锛�"
+ prop="followUpTime">
+ <el-date-picker v-model="negotiationForm.followUpTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm:ss"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璺熻繘浜猴細">
+ <el-input v-model="negotiationForm.followerUserName"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍐呭锛�"
+ prop="content">
+ <el-input v-model="negotiationForm.content"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitNegotiationForm">纭</el-button>
+ <el-button @click="closeNegotiationDialog">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 瀹㈡埛璇︽儏瀵硅瘽妗� -->
+ <el-dialog title="瀹㈡埛璇︽儏"
+ v-model="detailDialogVisible"
+ width="1000px"
+ @close="closeDetailDialog">
+ <!-- 瀹㈡埛鍩烘湰淇℃伅 -->
+ <div class="detail-section">
+ <h3 class="section-title">瀹㈡埛鍩烘湰淇℃伅</h3>
+ <div class="info-display">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">瀹㈡埛鍚嶇О锛�</span>
+ <span class="info-value">{{ detailForm.customerName }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">瀹㈡埛鍒嗙被锛�</span>
+ <span class="info-value">{{ detailForm.customerType }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">绾崇◣浜鸿瘑鍒彿锛�</span>
+ <span class="info-value">{{ detailForm.taxpayerIdentificationNumber }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鍏徃鐢佃瘽锛�</span>
+ <span class="info-value">{{ detailForm.companyPhone }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鍏徃鍦板潃锛�</span>
+ <span class="info-value">{{ detailForm.companyAddress }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">閾惰鍩烘湰鎴凤細</span>
+ <span class="info-value">{{ detailForm.basicBankAccount }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">閾惰璐﹀彿锛�</span>
+ <span class="info-value">{{ detailForm.bankAccount }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">寮�鎴疯鍙凤細</span>
+ <span class="info-value">{{ detailForm.bankCode }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鑱旂郴浜猴細</span>
+ <span class="info-value">{{ detailForm.contactPerson }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">鑱旂郴鐢佃瘽锛�</span>
+ <span class="info-value">{{ detailForm.contactPhone }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">缁存姢浜猴細</span>
+ <span class="info-value">{{ detailForm.maintainer }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="info-item">
+ <span class="info-label">缁存姢鏃堕棿锛�</span>
+ <span class="info-value">{{ detailForm.maintenanceTime }}</span>
+ </div>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+ <!-- 娲借皥杩涘害璁板綍 -->
+ <div class="detail-section">
+ <div class="section-header">
+ <h3 class="section-title">娲借皥杩涘害璁板綍</h3>
+ <el-button type="primary"
+ size="small"
+ @click="openNegotiationDialog(detailForm)">
+ 娣诲姞杩涘害
+ </el-button>
+ </div>
+ <el-table :data="negotiationRecords"
+ border
+ style="width: 100%">
+ <el-table-column prop="followUpTime"
+ label="璺熻繘鏃堕棿"
+ width="160" />
+ <el-table-column prop="followUpMethod"
+ label="璺熻繘鏂瑰紡"
+ width="100" />
+ <el-table-column prop="followUpLevel"
+ label="璺熻繘绋嬪害" />
+ <el-table-column prop="followerUserName"
+ label="璺熻繘浜�"
+ width="100" />
+ <el-table-column prop="content"
+ label="鍐呭"
+ show-overflow-tooltip />
+ <el-table-column label="闄勪欢"
+ width="100"
+ align="center">
+ <template #default="{ row }">
+ <el-button type="info"
+ link
+ @click="openAttachmentDialog(row)">
+ <el-icon>
+ <Paperclip />
+ </el-icon>
+ 闄勪欢
+ <!-- {{ row.fileList && row.fileList.length > 0 ? row.fileList.length : '涓婁紶' }} -->
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="150"
+ align="center">
+ <template #default="{ row, $index }">
+ <el-button type="primary"
+ link
+ @click="editNegotiationRecord(row, $index)">
+ 淇敼
+ </el-button>
+ <el-button type="danger"
+ link
+ @click="deleteNegotiationRecord(row, $index)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div v-if="negotiationRecords.length === 0"
+ class="no-records">
+ 鏆傛棤娲借皥杩涘害璁板綍
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDetailDialog">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 闄勪欢涓婁紶寮圭獥 -->
+ <el-dialog title="闄勪欢绠$悊"
+ v-model="attachmentDialogVisible"
+ width="600px"
+ @close="closeAttachmentDialog">
+ <div class="attachment-section">
+ <div class="upload-area">
+ <el-upload ref="attachmentUploadRef"
+ :action="getAttachmentUploadUrl()"
+ :headers="attachmentUploadHeaders"
+ :file-list="currentAttachmentList"
+ :on-success="handleAttachmentSuccess"
+ :on-error="handleAttachmentError"
+ :on-remove="handleAttachmentRemove"
+ :before-upload="beforeAttachmentUpload"
+ multiple
+ :limit="10"
+ name="files">
+ <el-button type="primary">
+ <el-icon>
+ <Upload />
+ </el-icon>
+ 涓婁紶闄勪欢
+ </el-button>
+ <template #tip>
+ <div class="el-upload__tip">
+ 鏀寔涓婁紶鍥剧墖銆佹枃妗g瓑鏂囦欢锛屽崟涓枃浠朵笉瓒呰繃50MB
+ </div>
+ </template>
+ </el-upload>
+ </div>
+ <div v-if="currentAttachmentList.length > 0"
+ class="attachment-list">
+ <h4>宸蹭笂浼犻檮浠讹細</h4>
+ <el-table :data="currentAttachmentList"
+ border
+ size="small">
+ <el-table-column prop="name"
+ label="鏂囦欢鍚�"
+ show-overflow-tooltip />
+ <el-table-column prop="size"
+ label="澶у皬"
+ width="100">
+ <template #default="{ row }">
+ {{ formatFileSize(row.size) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="120"
+ align="center">
+ <template #default="{ row, $index }">
+ <el-button type="primary"
+ link
+ @click="downloadAttachment(row)">
+ 涓嬭浇
+ </el-button>
+ <el-button type="danger"
+ link
+ @click="deleteAttachment(row, $index)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <div v-else
+ class="no-attachment">
+ 鏆傛棤闄勪欢
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeAttachmentDialog">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
+ import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
+ import {
+ addCustomerFollow,
+ updateCustomerFollow,
+ delCustomerFollow,
+ addReturnVisit,
+ getReturnVisit,
+ } from "@/api/basicData/customerFile.js";
+ import {
+ listCustomer,
+ addCustomer,
+ delCustomer,
+ updateCustomer,
+ getCustomer,
+ assignCustomer,
+ recycleCustomer,
+ shareCustomer,
+ } from "@/api/basicData/customer.js";
+
+ import { ElMessageBox } from "element-plus";
+ import { userListNoPage } from "@/api/system/user.js";
+ import useUserStore from "@/store/modules/user";
+ import { getToken } from "@/utils/auth.js";
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
+ const assignDialogVisible = ref(false);
+ const assignFormRef = ref();
+ const assignForm = reactive({
+ id: undefined,
+ customerName: "",
+ boundId: undefined,
+ });
+ const assignRules = {
+ boundId: [{ required: true, message: "璇烽�夋嫨鍒嗛厤浜哄憳", trigger: "change" }],
+ };
+ const shareDialogVisible = ref(false);
+ const shareFormRef = ref();
+ const shareForm = reactive({
+ id: undefined,
+ customerName: "",
+ boundIds: [],
+ });
+ const shareRules = {
+ boundIds: [{ required: true, message: "璇烽�夋嫨鍏变韩浜哄憳", trigger: "change" }],
+ };
+
+ // 鍥炶鎻愰啋鐩稿叧
+ const reminderDialogVisible = ref(false);
+ const reminderFormRef = ref();
+ const currentCustomerId = ref();
+ const reminderForm = reactive({
+ customerName: "",
+ reminderSwitch: false,
+ reminderContent: "",
+ reminderTime: "",
+ });
+ const reminderRules = {
+ reminderContent: [
+ { required: true, message: "璇疯緭鍏ユ彁閱掑唴瀹�", trigger: "blur" },
+ ],
+ reminderTime: [
+ { required: true, message: "璇烽�夋嫨鎻愰啋鏃堕棿", trigger: "change" },
+ ],
+ };
+
+ // 娲借皥杩涘害鐩稿叧
+ const negotiationDialogVisible = ref(false);
+ const negotiationFormRef = ref();
+ const negotiationForm = reactive({
+ customerName: "",
+ customerId: "",
+ followUpMethod: "",
+ followUpLevel: "",
+ followUpTime: "",
+ followerUserName: "",
+ content: "",
+ });
+ const negotiationRules = {
+ followUpMethod: [
+ { required: true, message: "璇烽�夋嫨璺熻繘鏂瑰紡", trigger: "change" },
+ ],
+ followUpLevel: [
+ { required: true, message: "璇烽�夋嫨璺熻繘绋嬪害", trigger: "change" },
+ ],
+ followUpTime: [
+ { required: true, message: "璇烽�夋嫨璺熻繘鏃堕棿", trigger: "change" },
+ ],
+ content: [{ required: true, message: "璇疯緭鍏ュ唴瀹�", trigger: "blur" }],
+ };
+
+ // 璇︽儏鐩稿叧
+ const detailDialogVisible = ref(false);
+ const detailForm = reactive({
+ customerName: "",
+ customerType: "",
+ taxpayerIdentificationNumber: "",
+ companyPhone: "",
+ companyAddress: "",
+ basicBankAccount: "",
+ bankAccount: "",
+ bankCode: "",
+ contactPerson: "",
+ contactPhone: "",
+ maintainer: "",
+ maintenanceTime: "",
+ });
+ const negotiationRecords = ref([]);
+
+ // 闄勪欢鐩稿叧
+ const attachmentDialogVisible = ref(false);
+ const attachmentUploadRef = ref();
+ const currentAttachmentList = ref([]);
+ const currentFollowRecord = ref({});
+ const attachmentUploadHeaders = { Authorization: "Bearer " + getToken() };
+
+ // 鍔ㄦ�佹瀯寤轰笂浼燯RL
+ const getAttachmentUploadUrl = () => {
+ const baseUrl =
+ import.meta.env.VITE_APP_BASE_API + "/basic/customer-follow/upload";
+ return currentFollowRecord.value.id
+ ? `${baseUrl}/${currentFollowRecord.value.id}`
+ : baseUrl;
+ };
+
+ const tableColumn = ref([
+ {
+ label: "瀹㈡埛鍒嗙被",
+ prop: "customerType",
+ width: 120,
+ },
+ {
+ label: "瀹㈡埛鍚嶇О",
+ prop: "customerName",
+ width: 220,
+ },
+ {
+ label: "绾崇◣浜鸿瘑鍒爜",
+ prop: "taxpayerIdentificationNumber",
+ width: 220,
+ },
+ {
+ label: "鍦板潃鍙婅仈绯绘柟寮�",
+ prop: "addressPhone",
+ width: 250,
+ },
+ {
+ label: "鑱旂郴浜�",
+ prop: "contactPerson",
+ },
+ {
+ label: "鑱旂郴鐢佃瘽",
+ prop: "contactPhone",
+ width: 150,
+ },
+ // {
+ // label: "璺熻繘杩涘害",
+ // prop: "followUpLevel",
+ // width: 120,
+ // },
+ // {
+ // label: "璺熻繘鏃堕棿",
+ // prop: "followUpTime",
+ // width: 120,
+ // },
+ {
+ label: "閾惰鍩烘湰鎴�",
+ prop: "basicBankAccount",
+ width: 220,
+ },
+ {
+ label: "閾惰璐﹀彿",
+ prop: "bankAccount",
+ width: 220,
+ },
+ {
+ label: "寮�鎴疯鍙�",
+ prop: "bankCode",
+ width: 220,
+ },
+ {
+ label: "缁存姢浜�",
+ prop: "maintainer",
+ },
+ {
+ label: "缁存姢鏃堕棿",
+ prop: "maintenanceTime",
+ width: 100,
+ },
+ {
+ label: "棰嗙敤浜�",
+ prop: "usageUserName",
+ width: 120,
+ fixed: "right",
+ },
+ {
+ label: "棰嗙敤鐘舵��",
+ prop: "usageStatus",
+ dataType: "tag",
+ width: 100,
+ fixed: "right",
+ formatData: value => {
+ if (value === 1 || value === "1") {
+ return "宸查鐢�";
+ }
+ return "鏈鐢�";
+ },
+ formatType: value => {
+ if (value === 1 || value === "1") {
+ return "success";
+ }
+ return "info";
+ },
+ },
+ {
+ label: "鍏变韩浜�",
+ prop: "togetherUserNames",
+ width: 120,
+ fixed: "right",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 200,
+ operation: [
+ {
+ name: "鍒嗛厤",
+ type: "text",
+ showHide: row => row.usageStatus != 1,
+ clickFun: row => {
+ openAssignDialog(row);
+ },
+ },
+ {
+ name: "鍥炴敹",
+ type: "text",
+ showHide: row => row.usageStatus == 1,
+ clickFun: row => {
+ recycle(row);
+ },
+ },
+ {
+ name: "鍏变韩",
+ type: "text",
+ showHide: row => row.usageStatus == 1,
+ clickFun: row => {
+ openShareDialog(row);
+ },
+ },
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ },
+ // {
+ // name: "璇︽儏",
+ // type: "text",
+ // clickFun: row => {
+ // openDetailDialog(row);
+ // },
+ // },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const userList = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const total = ref(0);
+
+ // 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+ const operationType = ref("");
+ const dialogFormVisible = ref(false);
+ const formYYs = ref({
+ // 鍏朵粬瀛楁...
+ contactList: [
+ {
+ contactPerson: "",
+ contactPhone: "",
+ },
+ ],
+ });
+ const data = reactive({
+ searchForm: {
+ customerName: "",
+ customerType: "",
+ type: 1
+ },
+ form: {
+ customerName: "",
+ taxpayerIdentificationNumber: "",
+ companyAddress: "",
+ companyPhone: "",
+ contactPerson: "",
+ contactPhone: "",
+ maintainer: "",
+ maintenanceTime: "",
+ basicBankAccount: "",
+ bankAccount: "",
+ bankCode: "",
+ customerType: "",
+ type: 1
+ },
+ rules: {
+ customerName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ taxpayerIdentificationNumber: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ companyAddress: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ companyPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ // contactPerson: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ // contactPhone: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ maintainer: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
+ maintenanceTime: [
+ { required: false, message: "璇烽�夋嫨", trigger: "change" },
+ ],
+ basicBankAccount: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ bankAccount: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ bankCode: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ customerType: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙瀹㈡埛瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙瀹㈡埛瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData",
+ data: {
+ type: 1
+ },
+ // 鏂囦欢涓婁紶鍓嶇殑鍥炶皟
+ beforeUpload: file => {
+ console.log("鏂囦欢鍗冲皢涓婁紶", file);
+ // 鍙互鍦ㄦ澶勫仛鏂囦欢绫诲瀷鎴栧ぇ灏忔牎楠�
+ const isValid =
+ file.type ===
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ||
+ file.name.endsWith(".xlsx") ||
+ file.name.endsWith(".xls");
+ if (!isValid) {
+ proxy.$modal.msgError("鍙兘涓婁紶 Excel 鏂囦欢");
+ }
+ return isValid;
+ },
+ // 鏂囦欢鐘舵�佹敼鍙樻椂鐨勫洖璋�
+ onChange: (file, fileList) => {
+ console.log("鏂囦欢鐘舵�佹敼鍙�", file, fileList);
+ },
+ // 鏂囦欢涓婁紶鎴愬姛鏃剁殑鍥炶皟
+ onSuccess: (response, file, fileList) => {
+ console.log("涓婁紶鎴愬姛", response, file, fileList);
+ upload.isUploading = false;
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ upload.open = false;
+ proxy.$refs["uploadRef"].clearFiles();
+ getList();
+ } else if (response.code === 500) {
+ proxy.$modal.msgError(response.msg);
+ } else {
+ proxy.$modal.msgWarning(response.msg);
+ }
+ },
+ // 鏂囦欢涓婁紶澶辫触鏃剁殑鍥炶皟
+ onError: (error, file, fileList) => {
+ console.error("涓婁紶澶辫触", error, file, fileList);
+ upload.isUploading = false;
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ },
+ // 鏂囦欢涓婁紶杩涘害鍥炶皟
+ onProgress: (event, file, fileList) => {
+ console.log("涓婁紶涓�...", event.percent);
+ },
+ });
+ const { searchForm, form, rules } = toRefs(data);
+ const addNewContact = () => {
+ formYYs.value.contactList.push({
+ contactPerson: "",
+ contactPhone: "",
+ });
+ };
+
+ const removeContact = index => {
+ if (formYYs.value.contactList.length > 1) {
+ formYYs.value.contactList.splice(index, 1);
+ }
+ };
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const { total, ...queryPage } = page;
+ listCustomer({ ...searchForm.value, ...queryPage }).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+ /** 鎻愪氦涓婁紶鏂囦欢 */
+ function submitFileForm() {
+ upload.isUploading = true;
+ proxy.$refs["uploadRef"].submit();
+ }
+ /** 瀵煎叆鎸夐挳鎿嶄綔 */
+ function handleImport() {
+ upload.title = "瀹㈡埛瀵煎叆";
+ upload.open = true;
+ }
+ /** 涓嬭浇妯℃澘 */
+ function importTemplate() {
+ proxy.download("/basic/customer/downloadTemplate", {}, "瀹㈡埛瀵煎叆妯℃澘.xlsx");
+ }
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ operationType.value = type;
+ form.value = {};
+ form.value.maintainer = userStore.nickName;
+ formYYs.value.contactList = [
+ {
+ contactPerson: "",
+ contactPhone: "",
+ },
+ ];
+ form.value.maintenanceTime = getCurrentDate();
+ form.value.type = 1;
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ if (type === "edit") {
+ getCustomer(row.id).then(res => {
+ form.value = { ...res.data };
+ formYYs.value.contactList = res.data.contactPerson
+ .split(",")
+ .map((item, index) => {
+ return {
+ contactPerson: item,
+ contactPhone: res.data.contactPhone.split(",")[index],
+ };
+ });
+ });
+ }
+ dialogFormVisible.value = true;
+ };
+ // 鎻愪氦琛ㄥ崟
+ const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "edit") {
+ submitEdit();
+ } else {
+ submitAdd();
+ }
+ }
+ });
+ };
+ // 鎻愪氦鏂板
+ const submitAdd = () => {
+ if (formYYs.value.contactList.length < 1) {
+ return proxy.$modal.msgWarning("璇疯嚦灏戞坊鍔犱竴涓仈绯讳汉");
+ }
+ form.value.contactPerson = formYYs.value.contactList
+ .map(item => item.contactPerson)
+ .join(",");
+ form.value.contactPhone = formYYs.value.contactList
+ .map(item => item.contactPhone)
+ .join(",");
+ addCustomer(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ };
+ // 鎻愪氦淇敼
+ const submitEdit = () => {
+ form.value.contactPerson = formYYs.value.contactList
+ .map(item => item.contactPerson)
+ .join(",");
+ form.value.contactPhone = formYYs.value.contactList
+ .map(item => item.contactPhone)
+ .join(",");
+ updateCustomer(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ };
+ const ensureUserList = () => {
+ if (userList.value.length) {
+ return Promise.resolve();
+ }
+ return userListNoPage().then(res => {
+ userList.value = res.data || [];
+ });
+ };
+ const openAssignDialog = row => {
+ assignForm.id = row.id;
+ assignForm.customerName = row.customerName;
+ assignForm.boundId = undefined;
+ ensureUserList().then(() => {
+ assignDialogVisible.value = true;
+ });
+ };
+ const closeAssignDialog = () => {
+ proxy.resetForm("assignFormRef");
+ assignForm.id = undefined;
+ assignForm.customerName = "";
+ assignForm.boundId = undefined;
+ assignDialogVisible.value = false;
+ };
+ const openShareDialog = row => {
+ shareForm.id = row.id;
+ shareForm.customerName = row.customerName;
+ shareForm.boundIds = row.userIds || [];
+ ensureUserList().then(() => {
+ shareDialogVisible.value = true;
+ });
+ };
+ const closeShareDialog = () => {
+ proxy.resetForm("shareFormRef");
+ shareForm.id = undefined;
+ shareForm.customerName = "";
+ shareForm.boundIds = [];
+ shareDialogVisible.value = false;
+ };
+ const submitAssignForm = () => {
+ proxy.$refs.assignFormRef.validate(valid => {
+ if (!valid) {
+ return;
+ }
+ assignCustomer({
+ id: assignForm.id,
+ usageUser: assignForm.boundId,
+ }).then(() => {
+ proxy.$modal.msgSuccess("鍒嗛厤鎴愬姛");
+ closeAssignDialog();
+ getList();
+ });
+ });
+ };
+ const submitShareForm = () => {
+ proxy.$refs.shareFormRef.validate(valid => {
+ if (!valid) {
+ return;
+ }
+ shareCustomer({
+ id: shareForm.id,
+ userIds: shareForm.boundIds,
+ }).then(() => {
+ proxy.$modal.msgSuccess("鍏变韩鎴愬姛");
+ closeShareDialog();
+ getList();
+ });
+ });
+ };
+ const recycle = row => {
+ ElMessageBox.confirm("纭鍥炴敹瀹㈡埛鈥�" + row.customerName + "鈥濆悧锛�", "鍥炴敹鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ return recycleCustomer({id: row.id}).then(() => {
+ proxy.$modal.msgSuccess("鍥炴敹鎴愬姛");
+ getList();
+ })
+ })
+ .catch(error => {
+ if (error === "cancel" || error === "close") {
+ proxy.$modal.msg("宸插彇娑�");
+ }
+ });
+ };
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/basic/customer/export", {type: 1}, "瀹㈡埛妗f.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ // 妫�鏌ユ槸鍚︽湁浠栦汉缁存姢鐨勬暟鎹�
+ const unauthorizedData = selectedRows.value.filter(
+ item => item.maintainer !== userStore.nickName
+ );
+ if (unauthorizedData.length > 0) {
+ proxy.$modal.msgWarning("涓嶅彲鍒犻櫎浠栦汉缁存姢鐨勬暟鎹�");
+ return;
+ }
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ delCustomer(ids)
+ .then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鎵撳紑鍥炶鎻愰啋寮圭獥
+ const openReminderDialog = row => {
+ currentCustomerId.value = row.id;
+ reminderForm.customerName = row.customerName;
+ reminderForm.reminderSwitch = false;
+ reminderForm.reminderContent = "";
+ reminderForm.reminderTime = "";
+
+ // 灏濊瘯鑾峰彇宸叉湁鐨勫洖璁挎彁閱�
+ getReturnVisit(row.id)
+ .then(res => {
+ if (res.code === 200 && res.data) {
+ reminderForm.reminderSwitch = res.data.isEnabled === 1;
+ reminderForm.reminderContent = res.data.content;
+ reminderForm.reminderTime = res.data.reminderTime;
+ reminderForm.id = res.data.id;
+ }
+ })
+ .catch(error => {
+ console.error("鑾峰彇鍥炶鎻愰啋澶辫触:", error);
+ });
+
+ reminderDialogVisible.value = true;
+ };
+
+ // 鍏抽棴鍥炶鎻愰啋寮圭獥
+ const closeReminderDialog = () => {
+ proxy.resetForm("reminderFormRef");
+ reminderDialogVisible.value = false;
+ };
+ const submitvalue = ref({});
+
+ // 鎻愪氦鍥炶鎻愰啋
+ const submitReminderForm = () => {
+ console.log("鎻愪氦鍥炶鎻愰啋鏁版嵁:", userStore.id, userStore);
+ proxy.$refs.reminderFormRef.validate(valid => {
+ if (valid) {
+ if (reminderForm.id) {
+ submitvalue.value = {
+ id: reminderForm.id,
+ customerId: currentCustomerId.value,
+ isEnabled: reminderForm.reminderSwitch ? 1 : 0,
+ content: reminderForm.reminderContent,
+ reminderTime: reminderForm.reminderTime,
+ remindUserId: userStore.id,
+ };
+ } else {
+ submitvalue.value = {
+ customerId: currentCustomerId.value,
+ isEnabled: reminderForm.reminderSwitch ? 1 : 0,
+ content: reminderForm.reminderContent,
+ reminderTime: reminderForm.reminderTime,
+ remindUserId: userStore.id,
+ };
+ }
+
+ console.log("鎻愪氦鍥炶鎻愰啋鏁版嵁:", submitvalue.value);
+
+ // 璋冪敤鎺ュ彛
+ addReturnVisit(submitvalue.value)
+ .then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鍥炶鎻愰啋璁剧疆鎴愬姛");
+ closeReminderDialog();
+ } else {
+ proxy.$modal.msgError(res.msg || "璁剧疆澶辫触");
+ }
+ })
+ .catch(error => {
+ console.error("璁剧疆鍥炶鎻愰啋澶辫触:", error);
+ proxy.$modal.msgError("璁剧疆澶辫触");
+ });
+ }
+ });
+ };
+
+ // 鎵撳紑娲借皥杩涘害寮圭獥
+ const openNegotiationDialog = row => {
+ negotiationForm.customerName = row.customerName;
+ negotiationForm.customerId = row.id;
+ negotiationForm.followUpMethod = "";
+ negotiationForm.followUpLevel = "";
+ negotiationForm.followUpTime = "";
+ negotiationForm.followerUserName = userStore.nickName; // 榛樿褰撳墠鐧诲綍浜�
+ negotiationForm.content = "";
+ // {
+ // "customerId": 152,
+ // "followUpMethod": "鐢佃瘽娌熼��",
+ // "followUpLevel": "娌℃湁鎰忓悜",
+ // "followUpTime": "2026-03-04T15:30:00",
+ // "followerUserName": "绠$悊鍛樿处鍙�",
+ // "content": "111"
+ // }
+ negotiationDialogVisible.value = true;
+ };
+
+ // 鍏抽棴娲借皥杩涘害寮圭獥
+ const closeNegotiationDialog = () => {
+ proxy.resetForm("negotiationFormRef");
+ // 娓呴櫎缂栬緫鐘舵��
+ delete negotiationForm.editIndex;
+ delete negotiationForm.id;
+ negotiationDialogVisible.value = false;
+ };
+
+ // 鎻愪氦娲借皥杩涘害
+ const submitNegotiationForm = () => {
+ proxy.$refs.negotiationFormRef.validate(valid => {
+ if (valid) {
+ // 鍒ゆ柇鏄柊澧炶繕鏄慨鏀�
+ const isEdit = negotiationForm.editIndex !== undefined;
+
+ if (isEdit) {
+ // 淇敼鎿嶄綔
+ console.log("淇敼娲借皥杩涘害鏁版嵁:", negotiationForm);
+ // 杩欓噷鍙互璋冪敤鏇存柊鎺ュ彛
+ // 瀹為檯椤圭洰涓渶瑕佹牴鎹悗绔帴鍙h繘琛岃皟鏁�
+ // 绀轰緥锛歶pdateCustomerFollow(negotiationForm).then(res => {
+ // // 鏇存柊鏈湴鏁版嵁
+ // const index = negotiationForm.editIndex;
+ // negotiationRecords.value[index] = {
+ // followUpTime: negotiationForm.followUpTime,
+ // followUpMethod: negotiationForm.followUpMethod,
+ // followUpLevel: negotiationForm.followUpLevel,
+ // followerUserName: negotiationForm.followerUserName,
+ // content: negotiationForm.content,
+ // id: negotiationForm.id,
+ // };
+ // proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ // closeNegotiationDialog();
+ // });
+ updateCustomerFollow(negotiationForm).then(res => {
+ // 鏇存柊鏈湴鏁版嵁
+ getCustomer(negotiationForm.customerId).then(res => {
+ // 鏇存柊鏈湴鏁版嵁
+ negotiationRecords.value = res.data.followUpList || [];
+ });
+ });
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeNegotiationDialog();
+ } else {
+ // 鏂板鎿嶄綔
+ console.log("鎻愪氦娲借皥杩涘害鏁版嵁:", negotiationForm);
+ addCustomerFollow(negotiationForm).then(res => {
+ // 娣诲姞鎴愬姛鍚庢洿鏂拌鎯呴〉闈㈢殑杩涘害璁板綍
+ const newRecord = {
+ followUpTime: negotiationForm.followUpTime,
+ followUpMethod: negotiationForm.followUpMethod,
+ followUpLevel: negotiationForm.followUpLevel,
+ followerUserName: negotiationForm.followerUserName,
+ content: negotiationForm.content,
+ };
+ negotiationRecords.value.unshift(newRecord);
+
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeNegotiationDialog();
+ getList();
+ });
+ }
+ }
+ });
+ };
+
+ // 鎵撳紑璇︽儏寮圭獥
+ const openDetailDialog = row => {
+ // 璋冪敤getCustomer鎺ュ彛鑾峰彇瀹㈡埛璇︽儏
+ getCustomer(row.id).then(res => {
+ // 濉厖瀹㈡埛鍩烘湰淇℃伅
+ Object.assign(detailForm, res.data);
+
+ // 鑾峰彇娲借皥杩涘害璁板綍
+ negotiationRecords.value = res.data.followUpList || [];
+
+ detailDialogVisible.value = true;
+ });
+ };
+
+ // 鍏抽棴璇︽儏寮圭獥
+ const closeDetailDialog = () => {
+ detailDialogVisible.value = false;
+ };
+
+ // 淇敼娲借皥璁板綍
+ const editNegotiationRecord = (row, index) => {
+ // 灏嗗綋鍓嶈褰曟暟鎹~鍏呭埌琛ㄥ崟
+ Object.assign(negotiationForm, {
+ customerName: row.customerName,
+ customerId: row.customerId,
+ followUpMethod: row.followUpMethod,
+ followUpLevel: row.followUpLevel,
+ followUpTime: row.followUpTime,
+ followerUserName: row.followerUserName,
+ content: row.content,
+ id: row.id, // 璁板綍ID鐢ㄤ簬鏇存柊
+ editIndex: index, // 璁板綍绱㈠紩鐢ㄤ簬鏈湴鏇存柊
+ });
+ negotiationDialogVisible.value = true;
+ };
+
+ // 鍒犻櫎娲借皥璁板綍
+ const deleteNegotiationRecord = (row, index) => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ繖鏉℃唇璋堣褰曞悧锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 杩欓噷鍙互璋冪敤鍒犻櫎鎺ュ彛
+ // 瀹為檯椤圭洰涓渶瑕佹牴鎹悗绔帴鍙h繘琛岃皟鏁�
+ // 绀轰緥锛歞eleteCustomerFollow(row.id).then(() => {
+ // negotiationRecords.value.splice(index, 1);
+ // proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ // });
+ delCustomerFollow(row.id).then(() => {
+ // 鍒犻櫎鎴愬姛鍚庢洿鏂版湰鍦版暟鎹�
+ getCustomer(row.customerId).then(res => {
+ // 鏇存柊鏈湴鏁版嵁
+ negotiationRecords.value = res.data.followUpList || [];
+ });
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ });
+ // 鏈湴鍒犻櫎锛堟ā鎷燂級
+ negotiationRecords.value.splice(index, 1);
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑堝垹闄�");
+ });
+ };
+
+ // 鎵撳紑闄勪欢寮圭獥
+ const openAttachmentDialog = row => {
+ currentFollowRecord.value = row;
+ // 杞崲涓虹鍚圗lement Plus fileList鏍煎紡鐨勬暟缁�
+ currentAttachmentList.value = (row.fileList || []).map((file, index) => ({
+ name: file.fileName,
+ url: file.fileUrl,
+ size: file.fileSize,
+ id: file.id,
+ uid: file.id || index,
+ status: "success",
+ }));
+
+ attachmentDialogVisible.value = true;
+ };
+
+ // 鍏抽棴闄勪欢寮圭獥
+ const closeAttachmentDialog = () => {
+ attachmentDialogVisible.value = false;
+ currentFollowRecord.value = {};
+ currentAttachmentList.value = [];
+ };
+
+ // 闄勪欢涓婁紶鎴愬姛
+ const handleAttachmentSuccess = (response, file, fileList) => {
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+ // 鏇存柊褰撳墠璁板綍鐨勯檮浠跺垪琛�
+ currentAttachmentList.value = fileList.map(item => ({
+ name: item.name,
+ size: item.size,
+ url: item.response?.data?.url || item.url,
+ id: item.response?.data?.id,
+ uid: item.uid,
+ status: "success",
+ }));
+ // 鏇存柊鍘熻褰曚腑鐨刦iles瀛楁
+ if (currentFollowRecord.value) {
+ currentFollowRecord.value.files = [...currentAttachmentList.value];
+ }
+ } else {
+ proxy.$modal.msgError(response.msg || "涓婁紶澶辫触");
+ }
+ };
+
+ // 闄勪欢涓婁紶澶辫触
+ const handleAttachmentError = (error, file, fileList) => {
+ console.error("涓婁紶澶辫触:", error);
+ proxy.$modal.msgError("涓婁紶澶辫触");
+ };
+
+ // 闄勪欢绉婚櫎
+ const handleAttachmentRemove = (file, fileList) => {
+ currentAttachmentList.value = fileList;
+ // 鏇存柊鍘熻褰曚腑鐨刦iles瀛楁
+ if (currentFollowRecord.value) {
+ currentFollowRecord.value.files = [...fileList];
+ }
+ };
+
+ // 闄勪欢涓婁紶鍓嶆牎楠�
+ const beforeAttachmentUpload = file => {
+ const maxSize = 50 * 1024 * 1024; // 50MB
+ if (file.size > maxSize) {
+ proxy.$modal.msgError("鏂囦欢澶у皬涓嶈兘瓒呰繃50MB");
+ return false;
+ }
+ return true;
+ };
+
+ // 鏍煎紡鍖栨枃浠跺ぇ灏�
+ const formatFileSize = size => {
+ if (size < 1024) {
+ return size + " B";
+ } else if (size < 1024 * 1024) {
+ return (size / 1024).toFixed(2) + " KB";
+ } else {
+ return (size / (1024 * 1024)).toFixed(2) + " MB";
+ }
+ };
+
+ // 涓嬭浇闄勪欢
+ const downloadAttachment = row => {
+ if (row.url) {
+ // proxy.download(row.url, {}, row.name);
+ proxy.$download.byUrl(row.url, row.originalFilename);
+ } else {
+ proxy.$modal.msgError("涓嬭浇閾炬帴涓嶅瓨鍦�");
+ }
+ };
+
+ // 鍒犻櫎闄勪欢
+ const deleteAttachment = (row, index) => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ繖涓檮浠跺悧锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 璋冪敤鍚庣鎺ュ彛鍒犻櫎闄勪欢
+ const deleteUrl =
+ import.meta.env.VITE_APP_BASE_API +
+ "/basic/customer-follow/file/" +
+ row.id;
+ fetch(deleteUrl, {
+ method: "DELETE",
+ headers: {
+ Authorization: "Bearer " + getToken(),
+ "Content-Type": "application/json",
+ },
+ })
+ .then(response => response.json())
+ .then(res => {
+ if (res.code === 200) {
+ // 鍒犻櫎鎴愬姛鍚庢洿鏂版湰鍦版枃浠跺垪琛�
+ currentAttachmentList.value.splice(index, 1);
+ // 鏇存柊鍘熻褰曚腑鐨刦iles瀛楁
+ if (currentFollowRecord.value) {
+ currentFollowRecord.value.files = [
+ ...currentAttachmentList.value,
+ ];
+ }
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(error => {
+ console.error("鍒犻櫎闄勪欢澶辫触:", error);
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑堝垹闄�");
+ });
+ };
+
+ // 鑾峰彇褰撳墠鏃ユ湡骞舵牸寮忓寲涓� YYYY-MM-DD
+ function getCurrentDate() {
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, "0"); // 鏈堜唤浠�0寮�濮�
+ const day = String(today.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ }
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .detail-section {
+ margin-bottom: 20px;
+ padding: 15px;
+ background-color: #f9f9f9;
+ border-radius: 4px;
+ }
+
+ .section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+
+ .section-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin: 0 0 15px 0;
+ color: #333;
+ }
+
+ .info-display {
+ background-color: #fff;
+ padding: 15px;
+ border-radius: 4px;
+ }
+
+ .info-item {
+ margin-bottom: 12px;
+ display: flex;
+ align-items: flex-start;
+ }
+
+ .info-label {
+ width: 120px;
+ font-weight: 500;
+ color: #606266;
+ margin-right: 10px;
+ }
+
+ .info-value {
+ flex: 1;
+ color: #303133;
+ word-break: break-word;
+ }
+
+ .no-records {
+ text-align: center;
+ padding: 30px;
+ color: #999;
+ font-size: 14px;
+ }
+
+ .attachment-section {
+ .upload-area {
+ margin-bottom: 20px;
+ padding: 20px;
+ background-color: #f9f9f9;
+ border-radius: 4px;
+ border: 1px dashed #d9d9d9;
+
+ .el-upload__tip {
+ margin-top: 10px;
+ color: #909399;
+ }
+ }
+
+ .attachment-list {
+ h4 {
+ margin: 0 0 10px 0;
+ font-size: 14px;
+ color: #606266;
+ }
+ }
+
+ .no-attachment {
+ text-align: center;
+ padding: 40px;
+ color: #909399;
+ font-size: 14px;
+ }
+ }
+</style>
diff --git a/src/views/basicData/parameterMaintenance/index.vue b/src/views/basicData/parameterMaintenance/index.vue
new file mode 100644
index 0000000..38ddd4f
--- /dev/null
+++ b/src/views/basicData/parameterMaintenance/index.vue
@@ -0,0 +1,811 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title ml10">鍙傛暟鍚嶇О锛�</span>
+ <el-input v-model="searchForm.paramName"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�"
+ clearable />
+ <!-- 鍏宠仈浜у搧绫诲瀷鎼滅储 -->
+ <!-- <span class="search_title ml10">鍏宠仈浜у搧绫诲瀷锛�</span>
+ <el-input v-model="searchForm.productName"
+ style="width: 200px"d
+ placeholder="璇疯緭鍏ュ叧鑱斾骇鍝佺被鍨�"
+ clearable /> -->
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">鎼滅储</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ <el-button type="primary"
+ @click="handleAdd"
+ style="margin-left: 10px">鏂板鍙傛暟</el-button>
+ <!-- 浜у搧绫诲瀷缁存姢鎸夐挳 -->
+ <!-- <el-button type="primary"
+ @click="handleProductTypeMaintenance"
+ style="margin-left: 10px">浜у搧绫诲瀷缁存姢</el-button> -->
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="paramName"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ height="calc(100vh - 320px)"
+ :tableLoading="tableLoading"
+ :isSelection="false"
+ :isShowPagination="true"
+ @pagination="pagination">
+ </PIMTable>
+ </div>
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="500px">
+ <el-form :model="formData"
+ :rules="rules"
+ ref="formRef"
+ label-width="120px">
+ <el-form-item label="鍙傛暟缂栫爜"
+ prop="paramCode">
+ <el-input v-model="formData.paramCode"
+ disabled
+ placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ <el-form-item label="鍙傛暟鍚嶇О"
+ prop="paramName">
+ <el-input v-model="formData.paramName"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍙傛暟绫诲瀷"
+ prop="paramType">
+ <el-select v-model="formData.paramType"
+ @change="handleParamTypeChange"
+ placeholder="璇烽�夋嫨鍙傛暟绫诲瀷">
+ <el-option label="鏁板�兼牸寮�"
+ :value="1" />
+ <el-option label="鏂囨湰鏍煎紡"
+ :value="2" />
+ <el-option label="涓嬫媺閫夐」"
+ :value="3" />
+ <el-option label="鏃堕棿鏍煎紡"
+ :value="4" />
+ </el-select>
+ </el-form-item>
+ <!-- <el-form-item label="鍙栧�兼ā寮�"
+ prop="valueMode">
+ <el-select v-model="formData.valueMode"
+ placeholder="璇烽�夋嫨鍙栧�兼ā寮�">
+ <el-option label="鍗曞��"
+ value="1" />
+ <el-option label="鍖洪棿"
+ value="2" />
+ </el-select>
+ </el-form-item> -->
+ <el-form-item label="鍒涘缓鏃堕棿"
+ prop="createTime">
+ <el-date-picker v-model="formCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="鍗曚綅"
+ prop="unit">
+ <el-input v-model="formData.unit"
+ placeholder="璇疯緭鍏ュ崟浣�" />
+ </el-form-item>
+ <el-form-item label="鍙栧�兼牸寮�"
+ v-if="formData.paramType == 1 || formData.paramType == 2"
+ prop="paramFormat">
+ <el-input v-model="formData.paramFormat"
+ placeholder="璇疯緭鍏ュ彇鍊兼牸寮�" />
+ <!-- <el-select v-model="formData.paramFormat"
+ placeholder="璇烽�夋嫨鍙栧�兼ā寮�">
+ <el-option label="#.00000"
+ value="#.00000" />
+ <el-option label="#.0000"
+ value="#.0000" />
+ <el-option label="#.000"
+ value="#.000" />
+ <el-option label="#.00"
+ value="#.00" />
+ </el-select> -->
+ </el-form-item>
+ <el-form-item label="涓嬫媺瀛楀吀"
+ v-else-if="formData.paramType == 3"
+ prop="paramFormat">
+ <el-select v-model="formData.paramFormat"
+ placeholder="璇烽�夋嫨鍙栧�兼ā寮�">
+ <el-option v-for="item in dictTypes"
+ :key="item.dictType"
+ :label="item.dictName"
+ :value="item.dictType" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃堕棿鏍煎紡"
+ v-else-if="formData.paramType == 4"
+ prop="paramFormat">
+ <el-select v-model="formData.paramFormat"
+ placeholder="璇烽�夋嫨鍙栧�兼ā寮�">
+ <el-option label="YYYY-MM-DD"
+ value="YYYY-MM-DD" />
+ <el-option label="YYYY-MM-DD HH:mm:ss"
+ value="YYYY-MM-DD HH:mm:ss" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏄惁蹇呭~"
+ prop="isRequired">
+ <el-switch v-model="formData.isRequired"
+ :active-value="1"
+ :inactive-value="0" />
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="formData.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 浜у搧绫诲瀷缁存姢瀵硅瘽妗� -->
+ <!-- <el-dialog v-model="productTypeDialogVisible"
+ title="浜у搧绫诲瀷缁存姢"
+ width="600px">
+ <div class="product-type-header">
+ <el-button type="primary"
+ @click="handleAddProductType">鏂板浜у搧绫诲瀷</el-button>
+ </div>
+ <el-table :data="productTypeList"
+ border
+ style="width: 100%; margin-top: 10px; margin-bottom: 20px">
+ <el-table-column prop="typeCode"
+ label="绫诲瀷缂栫爜"
+ width="150" />
+ <el-table-column prop="typeName"
+ label="绫诲瀷鍚嶇О" />
+ <el-table-column label="鎿嶄綔"
+ width="150">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ @click="handleEditProductType(scope.row)">缂栬緫</el-button>
+ <el-button link
+ type="danger"
+ @click="handleDeleteProductType(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog> -->
+ <!-- 鏂板/缂栬緫浜у搧绫诲瀷瀵硅瘽妗� -->
+ <!-- <el-dialog v-model="productTypeFormVisible"
+ :title="productTypeDialogTitle"
+ width="400px">
+ <el-form :model="productTypeForm"
+ :rules="productTypeRules"
+ ref="productTypeFormRef"
+ label-width="100px">
+ <el-form-item label="绫诲瀷缂栫爜"
+ prop="typeCode">
+ <el-input v-model="productTypeForm.typeCode"
+ placeholder="璇疯緭鍏ョ被鍨嬬紪鐮�" />
+ </el-form-item>
+ <el-form-item label="绫诲瀷鍚嶇О"
+ prop="typeName">
+ <el-input v-model="productTypeForm.typeName"
+ placeholder="璇疯緭鍏ョ被鍨嬪悕绉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="productTypeFormVisible = false">鍙栨秷</el-button>
+ <el-button type="primary"
+ @click="handleProductTypeSubmit">纭畾</el-button>
+ </span>
+ </template>
+ </el-dialog> -->
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref, reactive, computed } from "vue";
+ import dayjs from "dayjs";
+ import {
+ parameterListPage,
+ addParameter,
+ updateParameter,
+ delParameter,
+ addBaseParam,
+ editBaseParam,
+ getBaseParamList,
+ removeBaseParam,
+ // getProductTypes as getProductTypesApi,
+ } from "@/api/basicData/parameterMaintenance.js";
+ import { listType } from "@/api/system/dict/type";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+
+ const tableColumn = ref([
+ {
+ label: "鍙傛暟缂栫爜",
+ prop: "paramCode",
+ },
+ {
+ label: "鍙傛暟鍚嶇О",
+ prop: "paramName",
+ },
+ {
+ label: "鍙傛暟绫诲瀷",
+ prop: "paramType",
+ dataType: "tag",
+ formatType: params => {
+ const typeMap = {
+ 1: "primary",
+ 2: "info",
+ 3: "warning",
+ 4: "success",
+ };
+ return typeMap[params] || "default";
+ },
+ formatData: val => {
+ const labelMap = {
+ 1: "鏁板�兼牸寮�",
+ 2: "鏂囨湰鏍煎紡",
+ 3: "涓嬫媺閫夐」",
+ 4: "鏃堕棿鏍煎紡",
+ };
+ return labelMap[val] || val;
+ },
+ },
+ // {
+ // label: "鍙栧�兼ā寮�",
+ // prop: "valueMode",
+ // dataType: "tag",
+ // formatType: params => {
+ // return params === 2 ? "warning" : "success";
+ // },
+ // formatData: val => {
+ // return val === 2 ? "鍖洪棿" : "鍗曞��";
+ // },
+ // },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鍙栧�兼牸寮�",
+ prop: "paramFormat",
+ },
+ {
+ label: "鏄惁蹇呭~",
+ prop: "isRequired",
+ dataType: "tag",
+ formatType: val => {
+ return val === 1 ? "success" : "info";
+ },
+ formatData: val => {
+ return val === 1 ? "鏄�" : "鍚�";
+ },
+ },
+ {
+ label: "澶囨敞",
+ prop: "remark",
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ },
+ {
+ label: "鎿嶄綔",
+ dataType: "action",
+ width: "150",
+ operation: [
+ {
+ name: "缂栬緫",
+ clickFun: row => {
+ handleEdit(row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ // 鎼滅储琛ㄥ崟
+ const searchForm = reactive({
+ paramName: "",
+ productName: "",
+ });
+
+ // 瀵硅瘽妗嗙浉鍏�
+ const dialogVisible = ref(false);
+ const dialogTitle = ref("");
+ const formRef = ref(null);
+ const formData = reactive({
+ id: null,
+ paramCode: "",
+ paramName: "",
+ paramType: "",
+ // valueMode: "1",
+ unit: "",
+ remark: "",
+ isRequired: 0,
+ paramFormat: "",
+ createTime: "",
+ });
+ const rules = reactive({
+ paramName: [{ required: true, message: "璇疯緭鍏ュ弬鏁板悕绉�", trigger: "blur" }],
+ paramType: [{ required: true, message: "璇烽�夋嫨鍙傛暟绫诲瀷", trigger: "change" }],
+ // valueMode: [{ required: true, message: "璇烽�夋嫨鍙栧�兼ā寮�", trigger: "change" }],
+ unit: [
+ {
+ required: false,
+ message: "璇疯緭鍏ュ崟浣�",
+ trigger: "blur",
+ validator: (rule, value, callback) => {
+ if (formData.paramType === 1 && !value) {
+ callback(new Error("鏁板�肩被鍨嬪繀椤诲~鍐欏崟浣�"));
+ } else {
+ callback();
+ }
+ },
+ },
+ ],
+ });
+ const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
+ // const productTypes = ref([]);
+ const isEdit = ref(false);
+
+ // 浜у搧绫诲瀷缁存姢鐩稿叧 - 宸叉敞閲�
+ // const productTypeDialogVisible = ref(false);
+ // const productTypeFormVisible = ref(false);
+ // const productTypeDialogTitle = ref("");
+ // const productTypeFormRef = ref(null);
+ // const productTypeList = ref([]);
+ // const productTypeForm = reactive({
+ // id: null,
+ // typeCode: "",
+ // typeName: "",
+ // });
+ // const productTypeRules = reactive({
+ // typeCode: [{ required: true, message: "璇疯緭鍏ョ被鍨嬬紪鐮�", trigger: "blur" }],
+ // typeName: [{ required: true, message: "璇疯緭鍏ョ被鍨嬪悕绉�", trigger: "blur" }],
+ // });
+ // const isProductTypeEdit = ref(false);
+ const handleParamTypeChange = () => {
+ if (formData.paramType === 1) {
+ formData.paramFormat = "#.00000";
+ } else if (formData.paramType === 4) {
+ formData.paramFormat = "YYYY-MM-DD HH:mm:ss";
+ } else {
+ formData.paramFormat = "";
+ }
+ // 瑙﹀彂鍗曚綅瀛楁楠岃瘉
+ if (formRef.value) {
+ formRef.value.validateField("unit");
+ }
+ };
+ // 浜у搧绫诲瀷缁存姢鎸夐挳鐐瑰嚮浜嬩欢 - 宸叉敞閲�
+ // const handleProductTypeMaintenance = () => {
+ // productTypeDialogVisible.value = true;
+ // getProductTypeList();
+ // };
+
+ // 鑾峰彇浜у搧绫诲瀷鍒楄〃 - 宸叉敞閲�
+ // const getProductTypeList = () => {
+ // productTypeList.value = [
+ // { id: 1, typeCode: "TYPE001", typeName: "3.5鐮屽潡" },
+ // { id: 2, typeCode: "TYPE002", typeName: "5.0鐮屽潡" },
+ // { id: 3, typeCode: "TYPE003", typeName: "鏉挎潗" },
+ // ];
+ // };
+
+ // 鏂板浜у搧绫诲瀷 - 宸叉敞閲�
+ // const handleAddProductType = () => {
+ // isProductTypeEdit.value = false;
+ // productTypeDialogTitle.value = "鏂板浜у搧绫诲瀷";
+ // productTypeForm.id = null;
+ // productTypeForm.typeCode = "";
+ // productTypeForm.typeName = "";
+ // productTypeFormVisible.value = true;
+ // };
+
+ // 缂栬緫浜у搧绫诲瀷 - 宸叉敞閲�
+ // const handleEditProductType = row => {
+ // isProductTypeEdit.value = true;
+ // productTypeDialogTitle.value = "缂栬緫浜у搧绫诲瀷";
+ // productTypeForm.id = row.id;
+ // productTypeForm.typeCode = row.typeCode;
+ // productTypeForm.typeName = row.typeName;
+ // productTypeFormVisible.value = true;
+ // };
+
+ // 鍒犻櫎浜у搧绫诲瀷 - 宸叉敞閲�
+ // const handleDeleteProductType = row => {
+ // ElMessageBox.confirm("纭畾瑕佸垹闄よ浜у搧绫诲瀷鍚楋紵", "鎻愮ず", {
+ // confirmButtonText: "纭畾",
+ // cancelButtonText: "鍙栨秷",
+ // type: "warning",
+ // })
+ // .then(() => {
+ // ElMessage.success("鍒犻櫎鎴愬姛");
+ // getProductTypeList();
+ // })
+ // .catch(() => {});
+ // };
+
+ // 鎻愪氦浜у搧绫诲瀷琛ㄥ崟 - 宸叉敞閲�
+ // const handleProductTypeSubmit = () => {
+ // productTypeFormRef.value.validate(valid => {
+ // if (valid) {
+ // ElMessage.success(isProductTypeEdit.value ? "缂栬緫鎴愬姛" : "鏂板鎴愬姛");
+ // productTypeFormVisible.value = false;
+ // getProductTypeList();
+ // }
+ // });
+ // };
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ /** 閲嶇疆鎸夐挳鎿嶄綔 */
+ const handleReset = () => {
+ searchForm.paramName = "";
+ searchForm.productName = "";
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ // 璋冪敤鏂版帴鍙� /baseParam/list
+ getBaseParamList({
+ paramName: searchForm.paramName,
+ current: page.current,
+ size: page.size,
+ })
+ .then(res => {
+ tableLoading.value = false;
+ if (res.code === 200) {
+ tableData.value = res.data.records || [];
+ page.total = res.data.total || 0;
+ console.log(tableData.value, "tableData.value");
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ ElMessage.error("鏌ヨ澶辫触");
+ });
+ };
+
+ // 鑾峰彇浜у搧绫诲瀷鍒楄〃 - 宸叉敞閲�
+ // const getProductTypes = () => {
+ // productTypes.value = [
+ // { label: "3.5鐮屽潡", value: "type1" },
+ // { label: "5.0鐮屽潡", value: "type2" },
+ // { label: "鏉挎潗", value: "type3" },
+ // ];
+ // };
+
+ // 鏂板鎸夐挳鐐瑰嚮浜嬩欢
+ const handleAdd = () => {
+ isEdit.value = false;
+ dialogTitle.value = "鏂板鍙傛暟";
+ // 閲嶇疆琛ㄥ崟
+ formData.id = null;
+ formData.paramCode = "";
+ formData.paramName = "";
+ formData.paramType = "";
+ // formData.valueMode = "1";
+ formData.unit = "";
+ formData.remark = "";
+ formData.isRequired = 0;
+ formData.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ dialogVisible.value = true;
+ };
+
+ // 缂栬緫鎸夐挳鐐瑰嚮浜嬩欢
+ const handleEdit = row => {
+ isEdit.value = true;
+ dialogTitle.value = "缂栬緫鍙傛暟";
+ // 濉厖琛ㄥ崟鏁版嵁
+ formData.id = row.id;
+ formData.paramCode = row.paramCode || "";
+ formData.paramName = row.paramName || "";
+ formData.paramType = row.paramType !== undefined ? row.paramType : null;
+ // formData.valueMode =
+ // row.valueMode !== undefined ? String(row.valueMode) : "1";
+ formData.unit = row.unit || "";
+ formData.remark = row.remark || "";
+ formData.paramFormat = row.paramFormat || "";
+ formData.isRequired = row.isRequired || 0;
+ formData.createTime = row.createTime || "";
+ dialogVisible.value = true;
+ };
+
+ // 鍒犻櫎鎸夐挳鐐瑰嚮浜嬩欢
+ const handleDelete = row => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ繖鏉℃暟鎹悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 璋冪敤鏂版帴鍙� /baseParam/remove/{id}
+ removeBaseParam([row.id])
+ .then(res => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ // 鍙栨秷鍒犻櫎
+ });
+ };
+
+ // 鎻愪氦琛ㄥ崟
+ const handleSubmit = () => {
+ if (formData.paramType == 3 && !formData.paramFormat) {
+ ElMessage.warning("涓嬫媺瀛楀吀涓嶈兘涓虹┖锛�");
+ return;
+ }
+ formRef.value.validate(valid => {
+ if (valid) {
+ if (formData.id) {
+ // 缂栬緫浣跨敤鏂版帴鍙� /technologyParam/edit
+ editBaseParam(formData)
+ .then(res => {
+ ElMessage.success("缂栬緫鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ // ElMessage.error("缂栬緫澶辫触");
+ });
+ } else {
+ // 鏂板浣跨敤鏂版帴鍙� /technologyParam/add
+ addBaseParam(formData)
+ .then(res => {
+ ElMessage.success("鏂板鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ ElMessage.error("鏂板澶辫触");
+ });
+ }
+ } else {
+ return false;
+ }
+ });
+ };
+ const dictTypes = ref([]);
+ const getDictTypes = () => {
+ listType({ pageNum: 1, pageSize: 1000 }).then(res => {
+ dictTypes.value = res.rows || [];
+ });
+ };
+
+ onMounted(() => {
+ getDictTypes();
+ getList();
+ // getProductTypes();
+ });
+</script>
+
+<style scoped lang="scss">
+ .app-container {
+ padding: 24px;
+ background-color: #f0f2f5;
+ min-height: calc(100vh - 48px);
+ }
+
+ .search_form {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24px;
+ padding: 20px;
+ background-color: #ffffff;
+ border-radius: 6px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08);
+ }
+
+ .search_title {
+ color: #606266;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .ml10 {
+ margin-left: 10px;
+ }
+ }
+
+ .table_list {
+ background-color: #ffffff;
+ border-radius: 6px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ overflow: hidden;
+ height: calc(100vh - 230px);
+ }
+
+ :deep(.el-table) {
+ border: none;
+ border-radius: 6px;
+ overflow: hidden;
+ box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
+
+ .el-table__header-wrapper {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+
+ th {
+ background: transparent;
+ font-weight: 600;
+ // color: #ffffff;
+ border-bottom: none;
+ padding: 16px 0;
+ letter-spacing: 0.5px;
+ }
+ }
+
+ .el-table__body-wrapper {
+ tr {
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: linear-gradient(
+ 90deg,
+ rgba(102, 126, 234, 0.05) 0%,
+ rgba(118, 75, 162, 0.05) 100%
+ );
+ transform: scale(1.002);
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
+ }
+
+ td {
+ border-bottom: 1px solid #f0f0f0;
+ padding: 14px 0;
+ color: #303133;
+ }
+ }
+
+ tr.current-row {
+ background: linear-gradient(
+ 90deg,
+ rgba(102, 126, 234, 0.08) 0%,
+ rgba(118, 75, 162, 0.08) 100%
+ );
+ }
+
+ // 鏁板�煎瓧娈垫牱寮�
+ .quantity-cell,
+ .volume-cell,
+ .dimension-cell {
+ font-weight: 600;
+ color: #409eff;
+ font-family: "Courier New", monospace;
+ text-shadow: 0 1px 2px rgba(64, 158, 255, 0.2);
+ }
+
+ // 瑙勬牸瀛楁鏍峰紡
+ .spec-cell {
+ color: #67c23a;
+ font-weight: 500;
+ padding: 4px 8px;
+ border-radius: 4px;
+ }
+
+ // 缂栫爜瀛楁鏍峰紡
+ .code-cell {
+ color: #e6a23c;
+ font-family: "Courier New", monospace;
+ font-weight: 500;
+ padding: 4px 8px;
+ border-radius: 4px;
+ }
+
+ // 鏃ユ湡瀛楁鏍峰紡
+ .date-cell {
+ color: #909399;
+ font-style: italic;
+ }
+ }
+
+ .el-table__empty-block {
+ padding: 60px 0;
+ background-color: #fafafa;
+ }
+ }
+
+ .pagination-container {
+ display: flex;
+ justify-content: flex-end;
+ padding: 16px 20px;
+ background-color: #ffffff;
+ border-top: 1px solid #ebeef5;
+ border-radius: 0 0 12px 12px;
+ }
+
+ :deep(.el-button) {
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ }
+ }
+
+ @media (max-width: 768px) {
+ .app-container {
+ padding: 16px;
+ }
+
+ .search_form {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+
+ .el-form {
+ width: 100%;
+
+ .el-form-item {
+ width: 100%;
+ }
+ }
+
+ .el-button {
+ margin-right: 12px;
+ }
+ }
+
+ :deep(.el-table) {
+ th,
+ td {
+ padding: 10px 0;
+ font-size: 12px;
+ }
+ }
+ }
+</style>
diff --git a/src/views/basicData/product/ImportExcel/index.vue b/src/views/basicData/product/ImportExcel/index.vue
new file mode 100644
index 0000000..2afbb64
--- /dev/null
+++ b/src/views/basicData/product/ImportExcel/index.vue
@@ -0,0 +1,115 @@
+<template>
+ <el-button type="info" plain icon="Upload" @click="handleImport">
+ 瀵煎叆
+ </el-button>
+ <el-dialog v-model="upload.open" :title="upload.title" @close="handleDialogClose">
+ <FileUpload ref="fileUploadRef" accept=".xlsx, .xls" :headers="upload.headers" :action="uploadUrl"
+ :disabled="upload.isUploading" :showTip="true" @success="handleFileSuccess"
+ :downloadTemplate="handleDownloadTemplate" />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { reactive, computed } from "vue";
+import { getToken } from "@/utils/auth.js";
+import { FileUpload } from "@/components/Upload";
+import { ElMessage } from "element-plus";
+import { downloadProductModelImportTemplate } from "@/api/basicData/product.js";
+
+defineOptions({
+ name: "浜у搧缁存姢瀵煎叆",
+});
+
+const props = defineProps({
+ productId: { type: [String, Number], default: "" },
+});
+const emits = defineEmits(["uploadSuccess"]);
+const fileUploadRef = ref();
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙渚涘簲鍟嗗鍏ワ級
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙渚涘簲鍟嗗鍏ワ級
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+});
+// 涓婁紶鐨勫湴鍧�锛堟惡甯� productId 鍙傛暟锛屼紶缁欏悗绔殑 importProduct 鎺ュ彛锛�
+const uploadUrl = computed(
+ () =>
+ import.meta.env.VITE_APP_BASE_API +
+ "/basic/product/import" +
+ (props.productId ? `?productId=${props.productId}` : "")
+);
+// 鐐瑰嚮瀵煎叆
+const handleImport = () => {
+ if (!props.productId) {
+ ElMessage({ message: "璇峰厛閫夋嫨浜у搧", type: "warning" });
+ return;
+ }
+ upload.open = true;
+ upload.title = "浜у搧瀵煎叆";
+};
+
+const submitFileForm = () => {
+ fileUploadRef.value.uploadApi();
+};
+
+// 鍏抽棴寮圭獥鏃舵竻闄ゅ凡閫夋枃浠�
+const handleDialogClose = () => {
+ fileUploadRef.value?.clearFiles?.();
+};
+
+const handleFileSuccess = (response) => {
+ const { code, msg } = response;
+ if (code == 200) {
+ ElMessage({ message: msg || "瀵煎叆鎴愬姛", type: "success" });
+ upload.open = false;
+ emits("uploadSuccess");
+ } else {
+ ElMessage({ message: msg, type: "error" });
+ }
+};
+
+// 涓嬭浇 Excel 瀵煎叆妯℃澘
+const handleDownloadTemplate = () => {
+ downloadProductModelImportTemplate()
+ .then((blobData) => {
+ const blob =
+ blobData instanceof Blob
+ ? blobData
+ : new Blob([blobData], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = "浜у搧瀵煎叆妯℃澘.xlsx";
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ ElMessage({ message: "妯℃澘涓嬭浇鎴愬姛", type: "success" });
+ })
+ .catch(() => {
+ ElMessage({ message: "妯℃澘涓嬭浇澶辫触", type: "error" });
+ });
+};
+</script>
+
+<style scoped>
+.import-tip {
+ margin-top: 12px;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+}
+
+.import-tip .el-button {
+ margin-left: 8px;
+}
+</style>
diff --git a/src/views/basicData/product/ProductSelectDialog.vue b/src/views/basicData/product/ProductSelectDialog.vue
new file mode 100644
index 0000000..ad27baa
--- /dev/null
+++ b/src/views/basicData/product/ProductSelectDialog.vue
@@ -0,0 +1,187 @@
+<template>
+ <el-dialog v-model="visible" title="閫夋嫨浜у搧" width="900px" destroy-on-close :close-on-click-modal="false">
+ <el-form :inline="true" :model="query" class="mb-2">
+ <el-form-item label="浜у搧鍚嶇О">
+ <el-input v-model="query.productName" placeholder="杈撳叆浜у搧鍚嶇О" clearable @keyup.enter="onSearch" />
+ </el-form-item>
+
+ <el-form-item label="浜у搧鍨嬪彿">
+ <el-input v-model="query.model" placeholder="杈撳叆浜у搧鍨嬪彿" clearable @keyup.enter="onSearch" />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="onReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鍒楄〃 -->
+ <el-table ref="tableRef" v-loading="loading" :data="tableData" height="420" highlight-current-row row-key="id"
+ @selection-change="handleSelectionChange" @select="handleSelect">
+ <el-table-column type="selection" width="55" />
+ <el-table-column type="index" label="搴忓彿" width="60" />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="160" />
+ <el-table-column prop="model" label="浜у搧鍨嬪彿" min-width="200" />
+ <el-table-column prop="unit" label="鍗曚綅" min-width="160" />
+ </el-table>
+
+ <div class="mt-3 flex justify-end">
+ <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
+ v-model:page-size="page.pageSize" v-model:current-page="page.pageNum" :page-sizes="[10, 20, 50, 100]"
+ @size-change="onPageChange" @current-change="onPageChange" />
+ </div>
+
+ <template #footer>
+ <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm">
+ 纭畾
+ </el-button>
+ <el-button @click="close()">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import { productModelList, productModelListByUrl } from '@/api/basicData/productModel'
+
+export type ProductRow = {
+ id: number;
+ productName: string;
+ model: string;
+ unit?: string;
+};
+
+const props = defineProps<{
+ modelValue: boolean;
+ single?: boolean; // 鏄惁鍙兘閫夋嫨涓�涓紝榛樿false锛堝彲閫夋嫨澶氫釜锛�
+ topProductParentId?: number; // 涓�绾т骇鍝乮d
+ requestUrl?: string; // 鑷畾涔夋煡璇㈡帴鍙�
+}>();
+
+const emit = defineEmits(['update:modelValue', 'confirm']);
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: (v) => emit("update:modelValue", v),
+});
+
+const query = reactive({
+ productName: "",
+ model: "",
+});
+
+const page = reactive({
+ pageNum: 1,
+ pageSize: 10,
+});
+
+const loading = ref(false);
+const tableData = ref<ProductRow[]>([]);
+const total = ref(0);
+const multipleSelection = ref<ProductRow[]>([]);
+const tableRef = ref();
+
+function close() {
+ visible.value = false;
+}
+
+const handleSelectionChange = (val: ProductRow[]) => {
+ if (props.single && val.length > 1) {
+ // 濡傛灉闄愬埗涓哄崟涓�夋嫨锛屽彧淇濈暀鏈�鍚庝竴涓�変腑鐨�
+ const lastSelected = val[val.length - 1];
+ multipleSelection.value = [lastSelected];
+ // 娓呯┖琛ㄦ牸閫変腑鐘舵�侊紝鐒跺悗閲嶆柊閫変腑鏈�鍚庝竴涓�
+ nextTick(() => {
+ if (tableRef.value) {
+ tableRef.value.clearSelection();
+ tableRef.value.toggleRowSelection(lastSelected, true);
+ }
+ });
+ } else {
+ multipleSelection.value = val;
+ }
+}
+
+// 澶勭悊鍗曚釜閫夋嫨
+const handleSelect = (selection: ProductRow[], row: ProductRow) => {
+ if (props.single) {
+ // 濡傛灉闄愬埗涓哄崟涓紝娓呯┖鍏朵粬閫夋嫨锛屽彧淇濈暀褰撳墠琛�
+ if (selection.includes(row)) {
+ // 閫変腑褰撳墠琛屾椂锛屾竻绌哄叾浠栭�変腑
+ multipleSelection.value = [row];
+ nextTick(() => {
+ if (tableRef.value) {
+ tableData.value.forEach((item) => {
+ if (item.id !== row.id) {
+ tableRef.value.toggleRowSelection(item, false);
+ }
+ });
+ }
+ });
+ }
+ }
+}
+
+function onSearch() {
+ page.pageNum = 1;
+ loadData();
+}
+
+function onReset() {
+ query.productName = "";
+ query.model = "";
+ page.pageNum = 1;
+ loadData();
+}
+
+function onPageChange() {
+ loadData();
+}
+
+function onConfirm() {
+ if (multipleSelection.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�鏉′骇鍝�");
+ return;
+ }
+ if (props.single && multipleSelection.value.length > 1) {
+ ElMessage.warning("鍙兘閫夋嫨涓�涓骇鍝�");
+ return;
+ }
+ emit("confirm", props.single ? [multipleSelection.value[0]] : multipleSelection.value);
+ close();
+}
+
+async function loadData() {
+ loading.value = true;
+ try {
+ multipleSelection.value = []; // 缈婚〉/鎼滅储鍚庢竻绌洪�夋嫨鏇寸鍚堥鏈�
+ const params = {
+ productName: query.productName.trim(),
+ model: query.model.trim(),
+ current: page.pageNum,
+ size: page.pageSize,
+ topProductParentId: props.topProductParentId,
+ };
+ const res: any = props.requestUrl
+ ? await productModelListByUrl(props.requestUrl, params)
+ : await productModelList(params);
+ const records = res?.records || res?.data?.records || res?.data || [];
+ tableData.value = Array.isArray(records) ? records : [];
+ total.value = Number(res?.total ?? res?.data?.total ?? tableData.value.length);
+ } finally {
+ loading.value = false;
+ }
+}
+
+// 鐩戝惉寮圭獥鎵撳紑锛岄噸缃�夋嫨
+watch(() => props.modelValue, (visible) => {
+ if (visible) {
+ multipleSelection.value = [];
+ }
+});
+
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/basicData/product/index.vue b/src/views/basicData/product/index.vue
new file mode 100644
index 0000000..b05b215
--- /dev/null
+++ b/src/views/basicData/product/index.vue
@@ -0,0 +1,663 @@
+<template>
+ <div class="app-container product-view">
+ <div class="left">
+ <div>
+ <el-input v-model="search"
+ style="width: 210px"
+ placeholder="杈撳叆鍏抽敭瀛楄繘琛屾悳绱�"
+ @input="debouncedSearch"
+ @clear="searchFilter"
+ clearable
+ prefix-icon="Search" />
+ <el-button v-if="false"
+ type="primary"
+ @click="openProDia('addOne')"
+ style="margin-left: 10px">鏂板浜у搧澶х被</el-button>
+ </div>
+ <div ref="containerRef">
+ <el-tree ref="tree"
+ v-loading="treeLoad"
+ :data="list"
+ @node-click="handleNodeClick"
+ :expand-on-click-node="false"
+ @node-expand="handleNodeExpand"
+ @node-collapse="handleNodeCollapse"
+ :key="treeKey"
+ :default-expanded-keys="expandedKeys"
+ :filter-node-method="filterNode"
+ :props="{ children: 'children', label: 'label' }"
+ highlight-current
+ node-key="id"
+ class="product-tree-scroll"
+ style="height: calc(100vh - 190px); overflow-y: auto">
+ <template #default="{ node, data }">
+ <div class="custom-tree-node">
+ <span class="tree-node-content">
+ <el-icon class="orange-icon">
+ <component :is="data.children && data.children.length > 0
+ ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
+ </el-icon>
+ <span class="tree-node-label">{{ data.label }}</span>
+ </span>
+ <div>
+ <el-button type="primary"
+ link
+ :disabled="isTopLevelNode(data, node)"
+ @click="openProDia('edit', data, node)">
+ 缂栬緫
+ </el-button>
+ <el-button type="primary"
+ link
+ @click="openProDia('add', data, node)">
+ 娣诲姞浜у搧
+ </el-button>
+ <el-button v-if="!node.childNodes.length"
+ style="margin-left: 4px"
+ type="danger"
+ link
+ :disabled="isTopLevelNode(data, node)"
+ @click="remove(node, data)">
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ </template>
+ </el-tree>
+ </div>
+ </div>
+ <div class="right">
+ <div style="margin-bottom: 10px"
+ v-if="isShowButton">
+ <el-button type="primary"
+ @click="openModelDia('add')">
+ 鏂板瑙勬牸鍨嬪彿
+ </el-button>
+ <ImportExcel :product-id="currentId"
+ @uploadSuccess="getModelList" />
+ <el-button type="danger"
+ @click="handleDelete"
+ style="margin-left: 10px"
+ plain>
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"></PIMTable>
+ </div>
+ <el-dialog v-model="productDia"
+ title="浜у搧"
+ width="400px"
+ @keydown.enter.prevent>
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="浜у搧鍚嶇О锛�"
+ prop="productName">
+ <el-input v-model="form.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ maxlength="20"
+ show-word-limit
+ clearable
+ @keydown.enter.prevent />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
+ <el-button @click="closeProDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <el-dialog v-model="modelDia"
+ title="瑙勬牸鍨嬪彿"
+ width="400px"
+ @close="closeModelDia"
+ @keydown.enter.prevent>
+ <el-form :model="modelForm"
+ label-width="140px"
+ label-position="top"
+ :rules="modelRules"
+ ref="modelFormRef">
+ <el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="浜у搧缂栧彿锛�"
+ prop="productCode">
+ <el-input v-model="modelForm.productCode"
+ placeholder="璇疯緭鍏ヤ骇鍝佺紪鍙�"
+ clearable
+ @keydown.enter.prevent />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-col :span="24">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�"
+ prop="model">
+ <el-input v-model="modelForm.model"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ clearable
+ @keydown.enter.prevent />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鍗曚綅锛�"
+ prop="unit">
+ <el-input v-model="modelForm.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable
+ @keydown.enter.prevent />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitModelForm">纭</el-button>
+ <el-button @click="closeModelDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { nextTick, ref } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ addOrEditProduct,
+ addOrEditProductModel,
+ delProduct,
+ delProductModel,
+ modelListPage,
+ productTreeList,
+ } from "@/api/basicData/product.js";
+ import ImportExcel from "./ImportExcel/index.vue";
+
+ const { proxy } = getCurrentInstance();
+ const tree = ref(null);
+ const containerRef = ref(null);
+ const treeKey = ref(0);
+ const expandedKeySet = new Set();
+ const EXPANDED_STORAGE_KEY = "basicData_product_tree_expanded_keys_v2";
+
+ const loadExpandedKeys = () => {
+ if (typeof window === "undefined") {
+ return [];
+ }
+ try {
+ const saved = localStorage.getItem(EXPANDED_STORAGE_KEY);
+ return saved ? JSON.parse(saved) : [];
+ } catch (error) {
+ console.error(error);
+ return [];
+ }
+ };
+
+ const saveExpandedKeys = () => {
+ if (typeof window === "undefined") {
+ return;
+ }
+ localStorage.setItem(
+ EXPANDED_STORAGE_KEY,
+ JSON.stringify(Array.from(expandedKeySet))
+ );
+ };
+
+ loadExpandedKeys().forEach(key => expandedKeySet.add(key));
+
+ const syncExpandedKeysFromTree = () => {
+ const keys = [];
+ const walk = nodes => {
+ (nodes || []).forEach(item => {
+ if (item.expanded && item.data?.id !== undefined) {
+ keys.push(item.data.id);
+ }
+ if (item.childNodes && item.childNodes.length) {
+ walk(item.childNodes);
+ }
+ });
+ };
+
+ walk(tree.value?.root?.childNodes);
+ expandedKeySet.clear();
+ keys.forEach(key => expandedKeySet.add(key));
+ expandedKeys.value = keys;
+ saveExpandedKeys();
+ };
+
+ const normalizeExpandedKeys = treeData => {
+ const parentMap = new Map();
+ const walk = (nodes, parentId = null) => {
+ (nodes || []).forEach(item => {
+ parentMap.set(item.id, parentId);
+ if (item.children && item.children.length) {
+ walk(item.children, item.id);
+ }
+ });
+ };
+
+ walk(treeData);
+
+ const normalizedKeys = Array.from(expandedKeySet).filter(key => {
+ if (!parentMap.has(key)) {
+ return false;
+ }
+ let currentId = key;
+ while (parentMap.has(currentId)) {
+ const parentId = parentMap.get(currentId);
+ if (!parentId) {
+ return true;
+ }
+ if (!expandedKeySet.has(parentId)) {
+ return false;
+ }
+ currentId = parentId;
+ }
+ return true;
+ });
+
+ if (normalizedKeys.length !== expandedKeySet.size) {
+ expandedKeySet.clear();
+ normalizedKeys.forEach(key => expandedKeySet.add(key));
+ saveExpandedKeys();
+ }
+ };
+
+ const productDia = ref(false);
+ const modelDia = ref(false);
+ const modelOperationType = ref("");
+ const search = ref("");
+ const currentId = ref("");
+ const currentParentId = ref("");
+ /** 浜у搧寮圭獥锛歛dd 瀛樼埗鑺傜偣 id锛沞dit 瀛樺綋鍓嶈妭鐐� id 涓� parentId锛堜笉渚濊禆鏍戦�変腑椤癸級 */
+ const productDialogTarget = ref(null);
+ const operationType = ref("");
+ const treeLoad = ref(false);
+ const list = ref([]);
+ const expandedKeys = ref([]);
+ const tableColumn = ref([
+ {
+ label: "浜у搧缂栧彿",
+ prop: "productCode",
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openModelDia("edit", row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const isShowButton = ref(false);
+ const selectedRows = ref([]);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const data = reactive({
+ form: {
+ productName: "",
+ },
+ rules: {
+ productName: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ { max: 20, message: "浜у搧鍚嶇О涓嶈兘瓒呰繃20涓瓧绗�", trigger: "blur" },
+ ],
+ },
+ modelForm: {
+ model: "",
+ unit: "",
+ productCode: "",
+ },
+ modelRules: {
+ model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ productCode: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ },
+ });
+ const { form, rules, modelForm, modelRules } = toRefs(data);
+ // 鏌ヨ浜у搧鏍�
+ const getProductTreeList = () => {
+ treeLoad.value = true;
+ productTreeList()
+ .then(res => {
+ list.value = res || [];
+ normalizeExpandedKeys(list.value);
+ expandedKeys.value = Array.from(expandedKeySet);
+ treeKey.value += 1;
+ nextTick(() => {
+ tree.value?.setDefaultExpandedKeys?.(expandedKeys.value);
+ });
+ })
+ .catch(err => {
+ console.error(err);
+ })
+ .finally(() => {
+ treeLoad.value = false;
+ });
+ };
+ const handleNodeExpand = data => {
+ nextTick(syncExpandedKeysFromTree);
+ };
+ const handleNodeCollapse = (data, node) => {
+ node?.eachNode?.(item => {
+ item.collapse();
+ });
+ nextTick(syncExpandedKeysFromTree);
+ };
+ // 杩囨护浜у搧鏍�
+ const searchFilter = () => {
+ proxy.$refs.tree.filter(search.value);
+ };
+ const isTopLevelNode = (data, node) => {
+ if (node?.level !== undefined) {
+ return node.level === 1;
+ }
+ return [null, undefined, "", 0, "0"].includes(data?.parentId);
+ };
+ // 鎵撳紑浜у搧寮规
+ const openProDia = (type, data, node) => {
+ if (data && type === "edit" && isTopLevelNode(data, node)) {
+ proxy.$modal.msgWarning("涓�绾ц妭鐐逛笉鑳界紪杈戞垨鍒犻櫎");
+ return;
+ }
+ operationType.value = type;
+ productDialogTarget.value = null;
+ if (type === "add" && data) {
+ productDialogTarget.value = { parentId: data.id };
+ } else if (type === "edit" && data) {
+ let parentId = data.parentId;
+ if (
+ [null, undefined, ""].includes(parentId) &&
+ node?.parent?.data?.id != null
+ ) {
+ parentId = node.parent.data.id;
+ }
+ productDialogTarget.value = { id: data.id, parentId };
+ }
+ productDia.value = true;
+ form.value.productName =
+ type === "edit" && data ? data.productName : "";
+ };
+ // 鎵撳紑瑙勬牸鍨嬪彿寮规
+ const openModelDia = (type, data) => {
+ modelOperationType.value = type;
+ modelDia.value = true;
+ modelForm.value.model = "";
+ modelForm.value.unit = "";
+ modelForm.value.productCode = "";
+ modelForm.value.id = "";
+ if (type === "edit") {
+ modelForm.value = { ...data };
+ }
+ };
+ // 鎻愪氦浜у搧鍚嶇О淇敼
+ const submitForm = () => {
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ form.value.parentId =
+ productDialogTarget.value?.parentId ?? currentId.value;
+ form.value.id = "";
+ } else if (operationType.value === "addOne") {
+ form.value.id = "";
+ form.value.parentId = "";
+ } else {
+ form.value.id =
+ productDialogTarget.value?.id ?? currentId.value;
+ form.value.parentId = productDialogTarget.value?.parentId ?? "";
+ }
+ addOrEditProduct(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeProDia();
+ getProductTreeList();
+ });
+ }
+ });
+ };
+ // 鍏抽棴浜у搧寮规
+ const closeProDia = () => {
+ proxy.$refs.formRef.resetFields();
+ productDialogTarget.value = null;
+ productDia.value = false;
+ };
+
+ // 鍒犻櫎浜у搧
+ const remove = (node, data) => {
+ if (isTopLevelNode(data, node)) {
+ proxy.$modal.msgWarning("涓�绾ц妭鐐逛笉鑳界紪杈戞垨鍒犻櫎");
+ return;
+ }
+ let ids = [];
+ ids.push(data.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ delProduct(ids)
+ .then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getProductTreeList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ // 閫夋嫨浜у搧
+ const handleNodeClick = (val, node, el) => {
+ // 鍒ゆ柇鏄惁涓哄彾瀛愯妭鐐�
+ isShowButton.value = !(val.children && val.children.length > 0);
+ // 鍙湁鍙跺瓙鑺傜偣鎵嶆墽琛屼互涓嬮�昏緫
+ currentId.value = val.id;
+ currentParentId.value = val.parentId;
+ getModelList();
+ };
+
+ // 鎻愪氦瑙勬牸鍨嬪彿淇敼
+ const submitModelForm = () => {
+ proxy.$refs.modelFormRef.validate(valid => {
+ if (valid) {
+ modelForm.value.productId = currentId.value;
+ addOrEditProductModel(modelForm.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeModelDia();
+ getModelList();
+ });
+ }
+ });
+ };
+ // 鍏抽棴鍨嬪彿寮规
+ const closeModelDia = () => {
+ proxy.$refs.modelFormRef.resetFields();
+ modelDia.value = false;
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鏌ヨ瑙勬牸鍨嬪彿
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getModelList();
+ };
+ const getModelList = () => {
+ tableLoading.value = true;
+ modelListPage({
+ id: currentId.value,
+ current: page.current,
+ size: page.size,
+ }).then(res => {
+ console.log("res", res);
+ tableData.value = res.records;
+ page.total = res.total;
+ tableLoading.value = false;
+ });
+ };
+ // 鍒犻櫎瑙勬牸鍨嬪彿
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ delProductModel(ids)
+ .then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getModelList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ const debounce = (fn, delay = 300) => {
+ let timer;
+ return (...args) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => fn(...args), delay);
+ };
+ };
+
+ const debouncedSearch = debounce(() => {
+ searchFilter();
+ }, 300);
+
+ const filterNode = (value, data) => {
+ if (!value) return true;
+ return chooseNode(value.toLowerCase(), data);
+ };
+
+ const chooseNode = (value, data) => {
+ const label = (data.label || '').toLowerCase();
+ if (label.indexOf(value) !== -1) {
+ return true;
+ }
+ if (data.children && data.children.length > 0) {
+ return data.children.some(child => chooseNode(value, child));
+ }
+ return false;
+ };
+ getProductTreeList();
+</script>
+
+<style scoped>
+ .product-view {
+ display: flex;
+ }
+ .left {
+ width: 450px;
+ min-width: 450px;
+ padding: 16px;
+ background: #ffffff;
+ }
+ .right {
+ flex: 1;
+ min-width: 0;
+ padding: 16px;
+ margin-left: 20px;
+ background: #ffffff;
+ }
+ .custom-tree-node {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ padding-right: 8px;
+ }
+ .tree-node-content {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ height: 100%;
+ overflow: hidden;
+ }
+ .tree-node-content .orange-icon {
+ flex-shrink: 0;
+ }
+ .tree-node-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .orange-icon {
+ color: orange;
+ font-size: 18px;
+ margin-right: 8px; /* 鍥炬爣涓庢枃瀛椾箣闂村姞鐐归棿璺� */
+ }
+ .product-tree-scroll {
+ scrollbar-width: thin;
+ scrollbar-color: #c0c4cc #f5f7fa;
+ }
+ .product-tree-scroll::-webkit-scrollbar {
+ width: 8px;
+ }
+ .product-tree-scroll::-webkit-scrollbar-track {
+ background: #f5f7fa;
+ border-radius: 4px;
+ }
+ .product-tree-scroll::-webkit-scrollbar-thumb {
+ background: #c0c4cc;
+ border-radius: 4px;
+ }
+ .product-tree-scroll::-webkit-scrollbar-thumb:hover {
+ background: #909399;
+ }
+</style>
diff --git a/src/views/basicData/supplierManage/filesDia.vue b/src/views/basicData/supplierManage/filesDia.vue
new file mode 100644
index 0000000..ef41985
--- /dev/null
+++ b/src/views/basicData/supplierManage/filesDia.vue
@@ -0,0 +1,203 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ style="display: inline;margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ height="500"
+ @pagination-change="paginationSearch"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ >
+ </PIMTable>
+ <pagination
+ style="margin: 10px 0"
+ v-show="total > 0"
+ @pagination="paginationSearch"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import filePreview from '@/components/filePreview/index.vue'
+import {
+ fileAdd,
+ fileDel,
+ fileListPage
+} from "@/api/basicData/supplierManageFile.js";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const filePreviewRef = ref()
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: (row) => {
+ lookFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row) => {
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ fileListPage({supplierId: currentId.value, ...page}).then(res => {
+ tableData.value = res.data.records;
+ total.value = res.data.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const fileRow = {}
+ fileRow.name = res.data.originalName
+ fileRow.url = res.data.tempPath
+ uploadFile(fileRow)
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+function uploadFile(file) {
+ file.supplierId = currentId.value;
+ fileAdd(file).then(res => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ fileDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 棰勮闄勪欢
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/basicData/supplierManage/index.vue b/src/views/basicData/supplierManage/index.vue
new file mode 100644
index 0000000..d87a45b
--- /dev/null
+++ b/src/views/basicData/supplierManage/index.vue
@@ -0,0 +1,43 @@
+<!-- 鍦ㄤ綘鐨勪富椤甸潰涓� -->
+<template>
+ <div class="app-container">
+ <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+ <el-tab-pane label="姝e父渚涘簲鍟�" name="home">
+ <HomeTab ref="homeTab" />
+ </el-tab-pane>
+ <el-tab-pane label="榛戝悕鍗�" name="blacklist">
+ <BlacklistTab ref="blacklistTab" />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+</template>
+
+<script>
+import HomeTab from './components/HomeTab.vue'
+import BlacklistTab from './components/BlacklistTab.vue'
+
+export default {
+ name: 'MainPage',
+ components: {
+ HomeTab,
+ BlacklistTab
+ },
+ data() {
+ return {
+ activeTab: 'home'
+ }
+ },
+ methods: {
+ handleTabChange(tabName) {
+ this.activeTab = tabName
+ this.$nextTick(() => {
+ if (tabName === 'home') {
+ this.$refs.homeTab && this.$refs.homeTab.getList && this.$refs.homeTab.getList()
+ } else if (tabName === 'blacklist') {
+ this.$refs.blacklistTab && this.$refs.blacklistTab.getList && this.$refs.blacklistTab.getList()
+ }
+ })
+ },
+ }
+}
+</script>
diff --git a/src/views/chatHome/chatHomeIndex/MobileChat.vue b/src/views/chatHome/chatHomeIndex/MobileChat.vue
new file mode 100644
index 0000000..adeb5e7
--- /dev/null
+++ b/src/views/chatHome/chatHomeIndex/MobileChat.vue
@@ -0,0 +1,461 @@
+<template>
+ <div class="mobile-chat-wrapper" style="height: 91vh;">
+ <div class="chat-history">
+ <div class="chat-content" ref="chatContent">
+ <div class="chat-wrapper" v-for="(item, index) in chatList" :key="index">
+ <div class="chat-friend" v-if="item.uid !== '1001'">
+ <div class="info-time">
+ <img :src="item.headImg" alt="" />
+ <span>{{ item.name }}</span>
+ <span>{{ item.time }}</span>
+ </div>
+ <div class="chat-text" v-if="item.chatType == 0">
+ <template v-if="isSend && index === chatList.length - 1">
+ <span class="flash_cursor"></span>
+ </template>
+ <template v-else>
+ <pre style="font-family: none;">{{ item.msg }}</pre>
+ </template>
+ </div>
+ <div class="chat-img" v-if="item.chatType == 1">
+ <img :src="item.msg" alt="琛ㄦ儏" v-if="item.extend.imgType == 1" style="width: 100px; height: 100px" />
+ <el-image :src="item.msg" :preview-src-list="srcImgList" v-else> </el-image>
+ </div>
+ <div class="chat-img" v-if="item.chatType == 2">
+ <div class="word-file">
+ <FileCard :fileType="item.extend.fileType" :file="item.msg"></FileCard>
+ </div>
+ </div>
+ </div>
+ <div class="chat-me" v-else>
+ <div class="info-time">
+ <span>{{ item.name }}</span>
+ <span>{{ item.time }}</span>
+ <img :src="item.headImg" alt="" />
+ </div>
+ <div class="chat-text" v-if="item.chatType == 0">
+ {{ item.msg }}
+ </div>
+ <div class="chat-img" v-if="item.chatType == 1">
+ <img :src="item.msg" alt="琛ㄦ儏" v-if="item.extend.imgType == 1" style="width: 100px; height: 100px" />
+ <el-image style="max-width: 300px; border-radius: 10px" :src="item.msg" :preview-src-list="srcImgList" v-else> </el-image>
+ </div>
+ <div class="chat-img" v-if="item.chatType == 2">
+ <div class="word-file">
+ <FileCard :fileType="item.extend.fileType" :file="item.msg"></FileCard>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="chat-input-wrapper">
+ <div style="display: flex; align-items: center">
+ <input v-model="inputMsg" @change="sendText" :disabled="loading" class="input-text" autofocus placeholder="缁欏皬鏅哄彂閫佹秷鎭�" />
+ <img class="send-icon" src="@/assets/img/emoji/rocket.png" alt="" @click="sendText" />
+
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick,onActivated } from 'vue'
+import { useRoute } from 'vue-router'
+import { animation } from '@/utils/util'
+import chatGPTHeadImg from '@/assets/img/head_portrait1.png'
+import headPortrait from '@/assets/img/head_portrait.jpg'
+import FileCard from '@/components/FileCard.vue'
+import { ElMessage } from "element-plus"
+import {checking} from './ai-wd.js'
+
+// 瀹氫箟鍝嶅簲寮忔暟鎹�
+const route = useRoute()
+const chatContent = ref(null)
+const ws = ref(null)
+const chatList = ref([
+ {
+ headImg: chatGPTHeadImg,
+ name: '灏忔櫤',
+ time: new Date().toLocaleTimeString(),
+ msg: ' 灏忔櫤涓烘偍鏈嶅姟',
+ chatType: 0,
+ uid: '1002'
+ }
+])
+const inputMsg = ref('')
+const isSend = ref(false)
+const fileList = ref([])
+const isProcessing = ref(false)
+const loading = ref(true)
+const srcImgList = ref([])
+
+// 鍒犻櫎鍥剧墖
+const deleteImg = (index) => {
+ if (index >= 0 && index < fileList.value.length) {
+ fileList.value.splice(index, 1)
+ }
+}
+
+// WebSocket娑堟伅鎺ユ敹
+const websocketonmessage = (e) => {
+ const redata = JSON.parse(e.data)
+ //鏁版嵁鎺ユ敹
+ let chatGPT = {
+ headImg: headPortrait,
+ name: 'DeepSeek',
+ time: new Date().toLocaleTimeString(),
+ msg: redata[0].text,
+ chatType: 0, //淇℃伅绫诲瀷锛�0鏂囧瓧锛�1鍥剧墖
+ uid: '1002' //uid
+ }
+ sendMsg(chatGPT)
+ isSend.value = false
+}
+
+// WebSocket鍙戦�佹秷鎭�
+const websocketsend = (Data) => {
+ console.log("鍗冲皢鍙戦�佹秷鎭�", Data)
+ if (ws.value && ws.value.readyState === WebSocket.OPEN) {
+ console.log("鍙戦�佹秷鎭�", ws.value)
+ console.log("鍙戦�佹秷鎭�", Data)
+ let fileUrls = fileList.value.map(item => item.file.fileUrl)
+ //鏁版嵁鍙戦��
+ ws.value.send(Data + ":" + fileUrls.join(","))
+ fileList.value = []
+ inputMsg.value = ''
+ }
+}
+
+// 鍙戦�佹枃鏈秷鎭�
+const sendText = () => {
+ if (inputMsg.value) {
+ let chatMsg = {
+ headImg: headPortrait,
+ name: '鍗ч緳',
+ time: new Date().toLocaleTimeString(),
+ msg: inputMsg.value,
+ chatType: 0, //淇℃伅绫诲瀷锛�0鏂囧瓧锛�1鍥剧墖
+ uid: '1001' //uid
+ }
+ chatList.value.push(chatMsg)
+ let chatGPT = {
+ headImg: chatGPTHeadImg,
+ name: '灏忔櫤',
+ time: new Date().toLocaleTimeString(),
+ msg: "",
+ chatType: 0, //淇℃伅绫诲瀷锛�0鏂囧瓧锛�1鍥剧墖
+ uid: '1002' //uid
+ }
+ chatList.value.push(chatGPT) // 灏嗘帴鏀跺埌鐨勬秷鎭瓨鍌ㄥ埌 messages 鏁扮粍
+ simulateStreamingOutput(chatGPT, inputMsg.value)
+ inputMsg.value = ''
+
+ } else {
+ ElMessage({
+ message: '娑堟伅涓嶈兘涓虹┖鍝',
+ type: 'warning'
+ })
+ }
+}
+
+// 鍙戦�佷俊鎭�
+const sendMsg = (msgList) => {
+ chatList.value.push(msgList)
+ scrollBottom()
+}
+
+// 鑾峰彇绐楀彛楂樺害骞舵粴鍔ㄨ嚦鏈�搴曞眰
+const scrollBottom = () => {
+ nextTick(() => {
+ const scrollDom = chatContent.value
+ animation(scrollDom, scrollDom.scrollHeight - scrollDom.offsetHeight)
+ })
+}
+
+// 缁勪欢鎸傝浇鏃舵墽琛�
+onActivated(() => {
+ chatList.value = []
+ chatList.value.push({
+ headImg: chatGPTHeadImg,
+ name: '灏忔櫤',
+ time: new Date().toLocaleTimeString(),
+ msg: '灏忔櫤涓烘偍鏈嶅姟',
+ chatType: 0,
+ uid: '1002'
+ })
+ chatList.value.push({
+ headImg: headPortrait,
+ name: '鍗ч緳',
+ time: new Date().toLocaleTimeString(),
+ msg: route.query.keyWord,
+ chatType: 0,
+ uid: '1001'
+ })
+ // 娣诲姞涓�涓┖鐨勫洖澶嶆秷鎭崰浣�
+ const replyMsg = {
+ headImg: chatGPTHeadImg,
+ name: '灏忔櫤',
+ time: new Date().toLocaleTimeString(),
+ msg: '',
+ chatType: 0,
+ uid: '1002'
+ }
+ chatList.value.push(replyMsg)
+ scrollBottom()
+ loading.value = false
+ // 濡傛灉鏈夋煡璇㈠叧閿瓧锛屽垯妯℃嫙娴佸紡杈撳嚭
+ if (route.query.keyWord) {
+ simulateStreamingOutput(replyMsg, route.query.keyWord)
+ }
+})
+// 妯℃嫙娴佸紡杈撳嚭
+const simulateStreamingOutput = async (msgObj, keyWord) => {
+ loading.value = true
+ // 鐢熸垚0.8-1.3绉掍箣闂寸殑闅忔満寤惰繜
+ const delay = Math.random() * 500 + 800
+
+ // 妯℃嫙鍥炲鍐呭锛堝疄闄呭簲鐢ㄤ腑搴斾粠API鑾峰彇锛�
+ const responseText = `鍏充簬"${keyWord}"鐨勯棶棰橈紝鎴戞潵涓烘偍瑙g瓟锛歕n` + checking(keyWord)
+
+ isSend.value = true
+
+ let index = 0
+ setTimeout(() => {
+ const interval = setInterval(() => {
+ isSend.value = true
+ if (index < responseText.length) {
+ msgObj.msg += responseText.charAt(index)
+ index++
+ isSend.value = false
+ scrollBottom()
+ } else {
+ clearInterval(interval)
+ isSend.value = false
+ loading.value = false
+ }
+ }, 50) // 姣�50ms杈撳嚭涓�涓瓧绗︼紝妯℃嫙娴佸紡鏁堟灉
+ }, delay)
+
+}
+</script>
+
+<style lang="scss" scoped>
+.mobile-chat-wrapper {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ height: 91vh;
+ position: relative;
+ background-color: white;
+
+ .chat-history {
+ flex: 1 1 0;
+ overflow-y: auto;
+ }
+
+ .chat-input-wrapper {
+ padding: 8px 16px 8px 8px;
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ .file-tt{
+ flex-direction: column;
+ width: 200px;
+ display: flex;
+ padding: 5px;
+ border-radius: 5px;
+ margin-right: 5px;
+ background: #cacaca;
+ .file-item{
+ width: 200px;
+ overflow:hidden;
+ word-wrap: break-word;
+ text-overflow:ellipsis;
+ display:-webkit-box;
+ -webkit-box-orient:vertical;
+ -webkit-line-clamp:2;
+ }
+ }
+
+ .send-icon {
+ height: 40px;
+ margin-left: 16px;
+ }
+ .input-text{
+ font-size: 18px;
+ width: 100%;
+ border-radius: 20px;
+ height: 80px;
+ padding-left: 10px;
+ //padding-top: 10px;
+ border: none;
+ color: black; /* 淇敼鏂囨湰棰滆壊涓虹櫧鑹� */
+ background-color: #f5f4f4; /* 淇敼鑳屾櫙棰滆壊涓烘繁鐏拌壊 */
+ }
+ }
+
+ .chat-content {
+ width: 100%;
+ height: 80%;
+ overflow-y: scroll;
+ padding: 20px;
+ box-sizing: border-box;
+
+ &::-webkit-scrollbar {
+ width: 0;
+ /* Safari,Chrome 闅愯棌婊氬姩鏉� */
+ height: 0;
+ /* Safari,Chrome 闅愯棌婊氬姩鏉� */
+ display: none;
+ /* 绉诲姩绔�乸ad 涓奡afari锛孋hrome锛岄殣钘忔粴鍔ㄦ潯 */
+ }
+
+ .chat-wrapper {
+ position: relative;
+ word-break: break-all;
+
+ .chat-friend {
+ width: 100%;
+ float: left;
+ margin-bottom: 20px;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+
+ .chat-text {
+ max-width: 90%;
+ padding: 20px;
+ border-radius: 20px 20px 20px 5px;
+ background-color: rgb(245, 248, 248);
+ color: black;
+
+ &:hover {
+ background-color: rgb(232, 232, 232);
+ }
+
+ pre {
+ white-space: break-spaces;
+ }
+ }
+
+ .chat-img {
+ img {
+ width: 100px;
+ height: 100px;
+ }
+ }
+
+ .info-time {
+ margin: 10px 0;
+ color: black;
+ font-size: 14px;
+
+ img {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ vertical-align: middle;
+ margin-right: 10px;
+ }
+
+ span:last-child {
+ color: rgb(101, 104, 115);
+ margin-left: 10px;
+ vertical-align: middle;
+ }
+ }
+ }
+
+ .chat-me {
+ width: 100%;
+ float: right;
+ margin-bottom: 20px;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: flex-end;
+
+ .chat-text {
+ float: right;
+ max-width: 90%;
+ padding: 20px;
+ border-radius: 20px 20px 5px 20px;
+ background-color: rgb(29, 144, 245);
+ color: #fff;
+
+ &:hover {
+ background-color: rgb(26, 129, 219);
+ }
+ }
+
+ .chat-img {
+ img {
+ max-width: 300px;
+ max-height: 200px;
+ border-radius: 10px;
+ }
+ }
+
+ .info-time {
+ margin: 10px 0;
+ color: black;
+ font-size: 14px;
+ display: flex;
+ justify-content: flex-end;
+
+ img {
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ vertical-align: middle;
+ margin-left: 10px;
+ }
+
+ span {
+ line-height: 30px;
+ }
+
+ span:first-child {
+ color: rgb(101, 104, 115);
+ margin-right: 10px;
+ vertical-align: middle;
+ }
+ }
+ }
+ }
+ }
+ .flash_cursor {
+ width: 20px;
+ height: 30px;
+ display: inline-block;
+ background: #d6e3f5;
+ opacity: 1;
+ animation: glow 800ms ease-out infinite alternate;
+ }
+ @keyframes glow {
+ 0% {
+ opacity: 1;
+ }
+
+ 25% {
+ opacity: 0.5;
+ }
+
+ 50% {
+ opacity: 0;
+ }
+
+ 75% {
+ opacity: 0.5;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+ }
+}
+</style>
diff --git a/src/views/chatHome/chatHomeIndex/ai-jz.js b/src/views/chatHome/chatHomeIndex/ai-jz.js
new file mode 100644
index 0000000..02ee6dc
--- /dev/null
+++ b/src/views/chatHome/chatHomeIndex/ai-jz.js
@@ -0,0 +1,3 @@
+export function jam() {
+ return ""
+}
\ No newline at end of file
diff --git a/src/views/chatHome/chatHomeIndex/ai-wd.js b/src/views/chatHome/chatHomeIndex/ai-wd.js
new file mode 100644
index 0000000..c5b83d5
--- /dev/null
+++ b/src/views/chatHome/chatHomeIndex/ai-wd.js
@@ -0,0 +1,393 @@
+export function gasLeaks() {
+ return "1. 璁捐涓庤澶囬�夋嫨\n" +
+ "閫夌敤楂樿川閲忔潗鏂欙細绠¢亾銆侀榾闂ㄣ�佸偍缃愮瓑璁惧搴旈�夌敤鑰愯厫铓�銆佽�愰珮鍘嬬殑鏉愭枡锛屽苟绗﹀悎瀹夊叏鏍囧噯锛堝ASME銆丄PI绛夛級銆俓n" +
+ "\n" +
+ "瀹夊叏璁捐锛歕n" +
+ "\n" +
+ "瀹夎鍐椾綑鐨勫畨鍏ㄨ缃紙濡傚弻閲嶉榾闂ㄣ�佺垎鐮寸墖銆佸畨鍏ㄩ榾绛夛級銆俓n" +
+ "\n" +
+ "璁剧疆姘斾綋娉勬紡妫�娴嬬郴缁燂紙濡傚彲鐕冩皵浣撴姤璀﹀櫒銆佹湁姣掓皵浣撲紶鎰熷櫒锛夈�俓n" +
+ "\n" +
+ "閲囩敤灏侀棴寮忕郴缁熻璁★紝鍑忓皯寮�鏀炬帴鍙c�俓n" +
+ "\n" +
+ "閫氶绯荤粺锛氬湪鍙兘娉勬紡鐨勫尯鍩熷畨瑁呭己鍒堕�氶璁惧锛岄槻姝㈡皵浣撹仛闆嗐�俓n" +
+ "\n" +
+ "2. 瀹夎涓庣淮鎶n" +
+ "瑙勮寖瀹夎锛氱敱涓撲笟浜哄憳杩涜璁惧瀹夎锛岀‘淇濈閬撶剨鎺ャ�佸瘑灏佺瓑鐜妭鏃犵己闄枫�俓n" +
+ "\n" +
+ "瀹氭湡妫�鏌ワ細\n" +
+ "\n" +
+ "瀵圭閬撱�侀榾闂ㄣ�佽繛鎺ュ杩涜娉勬紡妫�娴嬶紙濡傚帇鍔涙祴璇曘�佽秴澹版尝妫�娴嬨�佽偉鐨傛按妫�婕忥級銆俓n" +
+ "\n" +
+ "鏇存崲鑰佸寲鎴栬厫铓�鐨勯儴浠躲�俓n" +
+ "\n" +
+ "棰勯槻鎬х淮鎶わ細鍒跺畾缁存姢璁″垝锛屽畾鏈熸鼎婊戦榾闂ㄣ�佹洿鎹㈠瘑灏佷欢绛夈�俓n" +
+ "\n" +
+ "3. 鎿嶄綔绠$悊\n" +
+ "涓ユ牸鎿嶄綔瑙勭▼锛歕n" +
+ "\n" +
+ "鎿嶄綔浜哄憳闇�鍩硅涓婂矖锛岀啛鎮夋皵浣撶壒鎬у拰搴旀�ユ祦绋嬨�俓n" +
+ "\n" +
+ "閬垮厤瓒呭帇銆佽秴娓╂垨閿欒鎿嶄綔銆俓n" +
+ "\n" +
+ "鐩戞帶绯荤粺锛歕n" +
+ "\n" +
+ "瀹炴椂鐩戞祴鍘嬪姏銆佹俯搴︺�佹祦閲忕瓑鍙傛暟锛岃缃嚜鍔ㄦ姤璀﹀拰鑱旈攣鍋滄満瑁呯疆銆俓n" +
+ "\n" +
+ "浣跨敤姘斾綋妫�娴嬩华锛堝绾㈠銆佺數鍖栧浼犳劅鍣級鐩戞祴鐜娴撳害銆俓n" +
+ "\n" +
+ "鏄庣‘鏍囪瘑锛氬湪鍗遍櫓鍖哄煙鏍囨槑姘斾綋绫诲瀷銆侀闄╃瓑绾у強闃叉姢瑕佹眰銆俓n" +
+ "\n" +
+ "4. 娉勬紡搴旀�ユ帾鏂絓n" +
+ "搴旀�ヨ澶囷細\n" +
+ "\n" +
+ "閰嶅闃叉瘨闈㈠叿銆佸懠鍚稿櫒銆侀槻鎶ゆ湇绛変釜浜洪槻鎶よ澶囷紙PPE锛夈�俓n" +
+ "\n" +
+ "灏辫繎鏀剧疆娉勬紡搴旀�ュ寘锛堝鍫垫紡鑳躲�佸瘑灏佸甫锛夈�俓n" +
+ "\n" +
+ "绱ф�ュ搷搴旓細\n" +
+ "\n" +
+ "绔嬪嵆鍒囨柇姘旀簮锛堝叧闂笂娓搁榾闂ㄦ垨鍚姩绱ф�ュ垏鏂郴缁燂級銆俓n" +
+ "\n" +
+ "鍚姩閫氶璁惧绋�閲婃皵浣撴祿搴︺�俓n" +
+ "\n" +
+ "鐤忔暎浜哄憳骞朵笂鎶ヤ笓涓氶儴闂紙濡傛秷闃层�佺幆淇濓級銆俓n" +
+ "\n" +
+ "搴旀�ラ妗堬細瀹氭湡婕旂粌娉勬紡澶勭疆娴佺▼锛岀‘淇濅汉鍛樼啛鎮夊垎宸ャ�俓n" +
+ "\n" +
+ "5. 鍌ㄥ瓨涓庤繍杈撳畨鍏╘n" +
+ "鍌ㄥ瓨瑕佹眰锛歕n" +
+ "\n" +
+ "鍌ㄧ綈杩滅鐏簮銆侀珮娓╁尯锛屽苟璁剧疆鍥村牥闃叉鎵╂暎銆俓n" +
+ "\n" +
+ "娑插寲姘斾綋鍌ㄧ綈闇�閰嶅娉勫帇瑁呯疆銆俓n" +
+ "\n" +
+ "杩愯緭瀹夊叏锛歕n" +
+ "\n" +
+ "浣跨敤鍚堣鐨勮繍杈撹溅杈嗭紝鍥哄畾姘旂摱闃叉纰版挒銆俓n" +
+ "\n" +
+ "杩愯緭閫斾腑鐩戞帶杞﹁締鐘舵�侊紙濡侴PS銆佹俯搴︿紶鎰熷櫒锛夈�俓n" +
+ "\n" +
+ "6. 浜哄憳鍩硅涓庢枃鍖朶n" +
+ "瀹夊叏鍩硅锛氬畾鏈熷紑灞曟皵浣撳嵄瀹炽�侀槻鎶ゆ帾鏂藉拰搴旀�ュ鐞嗙殑鍩硅銆俓n" +
+ "\n" +
+ "瀹夊叏鏂囧寲锛氶紦鍔卞憳宸ユ姤鍛婃綔鍦ㄩ闄╋紝寤虹珛鈥滈浂娉勬紡鈥濈鐞嗙洰鏍囥�俓n" +
+ "\n" +
+ "7. 娉曡涓庢爣鍑哱n" +
+ "閬靛畧鐩稿叧娉曡锛堝OSHA銆丟B 50493銆婄煶娌瑰寲宸ュ彲鐕冩皵浣撳拰鏈夋瘨姘斾綋妫�娴嬫姤璀﹁璁¤鑼冦�嬶級銆俓n" +
+ "\n" +
+ "瀹氭湡杩涜瀹夊叏瀹¤锛岀‘淇濈鍚堣涓氭爣鍑嗐�俓n" +
+ "\n" +
+ "甯歌鍗遍櫓姘斾綋娉勬紡鐨勯拡瀵规�ф帾鏂絓n" +
+ "鍙噧姘斾綋锛堝鐢茬兎銆佹阿姘旓級锛氶槻鐖嗙數鍣ㄣ�佹秷闄ら潤鐢点�俓n" +
+ "\n" +
+ "鏈夋瘨姘斾綋锛堝姘皵銆佺~鍖栨阿锛夛細閰嶅涓撶敤杩囨护寮忔垨渚涙哀寮忓懠鍚稿櫒銆俓n" +
+ "\n" +
+ "绐掓伅鎬ф皵浣擄紙濡傛爱姘斻�佷簩姘у寲纰筹級锛氱洃娴嬫哀姘旀祿搴︼紝閬垮厤瀵嗛棴绌洪棿浣滀笟銆俓n" +
+ "\n" +
+ "閫氳繃浠ヤ笂鎺柦鐨勭患鍚堝簲鐢紝鍙ぇ骞呴檷浣庢皵浣撴硠婕忛闄╋紝淇濋殰浜哄憳瀹夊叏鍜岀幆澧冨仴搴枫�傝嫢鍙戠敓娉勬紡锛岄渶浼樺厛纭繚浜哄憳鎾ょ锛屽啀鐢变笓涓氫汉鍛樺缃�俓n" +
+ "\n" +
+ "鏈洖绛旂敱 AI 鐢熸垚锛屽唴瀹逛粎渚涘弬鑰冿紝璇蜂粩缁嗙攧鍒��"
+}
+
+export function shipping() {
+ return "涓�銆佺珛鍗冲簲鎬ュ搷搴擻n" +
+ "1. 鍙戠幇娉勬紡鏃剁殑绱ф�ヨ鍔╘n" +
+ "鍋滆溅骞堕殧绂荤幇鍦猴細\n" +
+ "\n" +
+ "杩愯緭杞﹁締绔嬪嵆鍋滈潬鍦ㄧ┖鏃枫�佽繙绂讳汉缇ゅ拰鐏簮鐨勪綅缃�俓n" +
+ "\n" +
+ "璁剧疆璀︽垝绾匡紙鑷冲皯50~100绫冲崐寰勶紝鏍规嵁姘斾綋鎬ц川璋冩暣锛夛紝绂佹鏃犲叧浜哄憳杩涘叆銆俓n" +
+ "\n" +
+ "鍒囨柇娉勬紡婧愶細\n" +
+ "\n" +
+ "鍏抽棴瀹瑰櫒闃�闂ㄦ垨灏佸牭鐮存崯澶勶紙濡備娇鐢ㄥ簲鎬ュ牭婕忓伐鍏凤級銆俓n" +
+ "\n" +
+ "鑻ラ榾闂ㄦ崯鍧忥紝灏濊瘯杞Щ鍓╀綑姘斾綋鑷冲鐢ㄥ鍣紙闇�涓撲笟浜哄憳鎿嶄綔锛夈�俓n" +
+ "\n" +
+ "鎶ヨ涓庝笂鎶ワ細\n" +
+ "\n" +
+ "鎷ㄦ墦搴旀�ョ數璇濓紙濡傛秷闃�119銆佺幆淇濋儴闂級锛岃鏄庢皵浣撶被鍨嬨�佹硠婕忛噺銆佷綅缃瓑淇℃伅銆俓n" +
+ "\n" +
+ "鑱旂郴杩愯緭鍏徃鍙婅揣涓伙紝鑾峰彇鎶�鏈敮鎻达紙濡侻SDS瀹夊叏鏁版嵁琛級銆俓n" +
+ "\n" +
+ "2. 浜哄憳闃叉姢涓庣枏鏁n" +
+ "绌挎埓闃叉姢瑁呭锛歕n" +
+ "\n" +
+ "鍙噧姘斾綋锛氶槻鐖嗗伐鍏�+闃查潤鐢垫湇锛涙湁姣掓皵浣擄細姝e帇寮忓懠鍚稿櫒+闃插寲鏈嶃�俓n" +
+ "\n" +
+ "鏃犻槻鎶よ澶囨椂锛岀珛鍗虫挙绂昏嚦涓婇鏂瑰悜銆俓n" +
+ "\n" +
+ "鐤忔暎鍛ㄨ竟鍖哄煙锛歕n" +
+ "\n" +
+ "鏍规嵁姘斾綋鎵╂暎鑼冨洿锛堝弬鑰冨簲鎬ュ搷搴旀寚鍗桬RG锛夌枏鏁e眳姘戞垨浣滀笟浜哄憳銆俓n" +
+ "\n" +
+ "閬垮厤浣庢醇澶勬粸鐣欙紙鏌愪簺姘斾綋姣旂┖姘旈噸锛屽纭寲姘級銆俓n" +
+ "\n" +
+ "3. 鎺у埗娉勬紡鎵╂暎\n" +
+ "鐗╃悊鏂规硶锛歕n" +
+ "\n" +
+ "瑕嗙洊娉勬紡鍙o紙濡傜敤娴告按妫夎鍑忓皯鎸ュ彂锛屼絾绂佺敤浜庨亣姘村弽搴旀皵浣撳姘皵锛夈�俓n" +
+ "\n" +
+ "绛戝牑鍥村牭娑蹭綋娉勬紡鐗╋紝闃叉娴佸叆涓嬫按閬撴垨娌虫祦銆俓n" +
+ "\n" +
+ "鍖栧鏂规硶锛歕n" +
+ "\n" +
+ "涓拰澶勭悊锛堝姘ㄦ皵娉勬紡鍠锋磼绋�鐩愰吀锛岄渶涓撲笟浜哄憳鎿嶄綔锛夈�俓n" +
+ "\n" +
+ "浣跨敤鍚搁檮鏉愭枡锛堝娲绘�х偔銆佹矙鍦熷惛闄勬湁鏈烘皵浣擄級銆�"
+}
+
+export function operate(){
+ return "涓�銆佹搷浣滀笉褰撳彂鐢熷悗鐨勫簲鎬ュ鐞哱n" +
+ "1. 绔嬪嵆鎺у埗浜嬫晠\n" +
+ "鍋滄鎿嶄綔锛歕n" +
+ "\n" +
+ "鎸変笅绱ф�ュ仠鏈烘寜閽紝鍏抽棴鏈�杩戠殑涓婃父闃�闂ㄣ�俓n" +
+ "\n" +
+ "鍚姩搴旀�ラ妗堬細\n" +
+ "\n" +
+ "灏忓瀷娉勬紡锛氫娇鐢ㄥ簲鎬ュ牭婕忓伐鍏凤紙濡傚瘑灏佽兌銆佸す鍏凤級銆俓n" +
+ "\n" +
+ "澶у瀷娉勬紡锛氱枏鏁d汉鍛橈紝鎶ヨ姹傚姪锛�119/鐜繚閮ㄩ棬锛夈�俓n" +
+ "\n" +
+ "2. 浜哄憳瀹夊叏\n" +
+ "鎾ょ涓庨殧绂伙細\n" +
+ "\n" +
+ "閫嗛鎾ょ鑷充笂椋庡悜瀹夊叏鍖猴紝閬垮厤浣庢醇澶勶紙閲嶆皵浣撶Н鑱氾級銆俓n" +
+ "\n" +
+ "鎬ユ晳鎺柦锛歕n" +
+ "\n" +
+ "鍚稿叆鏈夋瘨姘斾綋锛氱Щ鑷崇┖姘旀柊椴滃锛屽繀瑕佹椂浜哄伐鍛煎惛銆俓n" +
+ "\n" +
+ "鐨偆鎺ヨЕ锛氱珛鍗崇敤娓呮按鍐叉礂15鍒嗛挓锛堣厫铓�鎬ф皵浣擄級銆俓n" +
+ "\n" +
+ "3. 浜嬫晠璋冩煡涓庢暣鏀筡n" +
+ "鏍规湰鍘熷洜鍒嗘瀽锛圧CA锛夛細\n" +
+ "\n" +
+ "鏄搷浣滃け璇�佸煿璁笉瓒筹紝杩樻槸璁惧缂洪櫡锛焅n" +
+ "\n" +
+ "鏀硅繘鎺柦锛歕n" +
+ "\n" +
+ "淇鎿嶄綔瑙勭▼锛屽鍔犺绀烘爣璇嗐�俓n" +
+ "\n" +
+ "瀵硅矗浠讳汉鍐嶅煿璁紝蹇呰鏃惰皟宀椼��"
+}
+
+export function emergency(){
+ return "涓�銆佷紭鍖栧簲鎬ュ搷搴旂殑鍏抽敭鎺柦\n" +
+ "1. 瀹屽杽搴旀�ラ妗圽n" +
+ "閽堝鎬ц璁★細\n" +
+ "\n" +
+ "鍩轰簬HAZOP锛堝嵄闄╀笌鍙搷浣滄�у垎鏋愶級璇嗗埆鎵�鏈夋綔鍦ㄩ闄╁満鏅�俓n" +
+ "\n" +
+ "鍒跺畾鍒嗙骇鍝嶅簲鏈哄埗锛堝皬娉勬紡鐜板満澶勭疆銆佸ぇ娉勬紡鍏ㄥ憳鐤忔暎锛夈�俓n" +
+ "\n" +
+ "鏄庣‘鑱岃矗锛歕n" +
+ "\n" +
+ "璁剧珛搴旀�ユ寚鎸ラ儴锛屾寚瀹氭�绘寚鎸ャ�侀�氳缁勩�佹姠闄╃粍銆佸尰鐤楃粍绛夈�俓n" +
+ "\n" +
+ "纭繚24灏忔椂鍊肩彮鍒跺害锛岃仈绯绘柟寮忓疄鏃舵洿鏂般�俓n" +
+ "\n" +
+ "鑱斿姩澶栭儴璧勬簮锛歕n" +
+ "\n" +
+ "涓庢秷闃层�佺幆淇濄�佸尰闄㈡彁鍓嶇璁㈡晳鎻村崗璁紝鏄庣‘鍗忎綔娴佺▼銆俓n" +
+ "\n" +
+ "2. 寮哄寲搴旀�ヨ祫婧愪繚闅淺n" +
+ "瑁呭涓庣墿璧勶細\n" +
+ "\n" +
+ "閰嶅瓒抽噺涓旀湁鏁堢殑搴旀�ヨ澶囷紝鍖呮嫭锛歕n" +
+ "\n" +
+ "涓汉闃叉姢瑁呭锛圥PE锛夛細闃叉瘨闈㈠叿銆佸寲瀛﹂槻鎶ゆ湇銆俓n" +
+ "\n" +
+ "鍫垫紡宸ュ叿锛氬瘑灏佽兌銆佸す鍏枫�佸揩閫熷皝鍫靛櫒銆俓n" +
+ "\n" +
+ "鍚搁檮/涓拰鏉愭枡锛氭椿鎬х偔銆佹矙鍦熴�佺█纰辨恫锛堢敤浜庨吀鎬ф皵浣擄級銆俓n" +
+ "\n" +
+ "瀹氭湡妫�鏌ャ�佺淮鎶ゅ拰鏇存崲锛堝姘旂摱鍘嬪姏銆佷紶鎰熷櫒鐢垫睜锛夈�俓n" +
+ "\n" +
+ "搴旀�ヨ溅杈嗕笌閫氶亾锛歕n" +
+ "\n" +
+ "纭繚鏁戞彺杞﹁締鍙揩閫熸姷杈炬硠婕忕偣锛堟竻闄よ矾闅滐紝鏍囪瘑搴旀�ヨ矾绾匡級銆俓n" +
+ "\n" +
+ "鍏抽敭鍖哄煙璁剧疆搴旀�ユ礂鐪煎櫒銆佸柗娣嬬郴缁熴�俓n" +
+ "\n" +
+ "3. 鍔犲己浜哄憳鍩硅涓庤兘鍔涘缓璁綷n" +
+ "鍒嗗眰鍩硅锛歕n" +
+ "\n" +
+ "鍩哄眰浜哄憳锛氭帉鎻″熀鏈簲鎬ュ缃紙濡傚叧闂榾闂ㄣ�佷娇鐢ㄧ伃鐏櫒锛夈�俓n" +
+ "\n" +
+ "搴旀�ュ皬缁勶細涓撲笟鍫垫紡銆佷激鍛樻�ユ晳銆佹皵浣撴娴嬫妧鑳姐�俓n" +
+ "\n" +
+ "绠$悊灞傦細鎸囨尌鍐崇瓥銆佸獟浣撴矡閫氥�佹硶寰嬪悎瑙勩�俓n" +
+ "\n" +
+ "瀹炴垬鍖栬�冩牳锛歕n" +
+ "\n" +
+ "閫氳繃妯℃嫙绐佸彂娉勬紡锛堝鐩叉紨锛夋楠屽搷搴旈�熷害銆俓n" +
+ "\n" +
+ "涓嶅悎鏍艰�呴渶閲嶆柊鍩硅銆俓n" +
+ "\n" +
+ "4. 瀹氭湡婕旂粌涓庢寔缁敼杩沑n" +
+ "婕旂粌棰戠巼锛歕n" +
+ "\n" +
+ "姣忓搴﹁嚦灏�1娆℃闈㈡帹婕旓紝姣忓勾2娆$患鍚堝疄鎴樻紨缁冦�俓n" +
+ "\n" +
+ "鍦烘櫙璁捐锛歕n" +
+ "\n" +
+ "妯℃嫙澶嶆潅鎯呭喌锛堝澶滈棿鍋滅數銆佸浜哄彈浼わ級銆俓n" +
+ "\n" +
+ "寮曞叆鈥滅獊鍙戝彉閲忊�濓紙濡傞鍚戠獊鍙樸�佷簩娆℃硠婕忥級銆俓n" +
+ "\n" +
+ "澶嶇洏涓庝紭鍖栵細\n" +
+ "\n" +
+ "璁板綍婕旂粌涓殑闂锛堝閫氳寤惰繜銆佽澶囩己澶憋級銆俓n" +
+ "\n" +
+ "鏇存柊棰勬骞朵笅鍙戝涔犮�俓n" +
+ "\n" +
+ "5. 鎶�鏈崌绾т笌鏅鸿兘鍖栨敮鎸乗n" +
+ "瀹炴椂鐩戞祴涓庨璀︼細\n" +
+ "\n" +
+ "閮ㄧ讲鐗╄仈缃戯紙IoT锛変紶鎰熷櫒锛岀洃娴嬫皵浣撴祿搴︺�佽澶囩姸鎬併�俓n" +
+ "\n" +
+ "璁剧疆鑷姩鑱旈攣鎺у埗锛堝娉勬紡鏃惰仈鍔ㄥ叧闂榾闂ㄥ苟鍚姩閫氶锛夈�俓n" +
+ "\n" +
+ "搴旀�ラ�氳绯荤粺锛歕n" +
+ "\n" +
+ "浣跨敤闃茬垎瀵硅鏈恒�佸崼鏄熺數璇濓紙淇濋殰淇″彿瑕嗙洊锛夈�俓n" +
+ "\n" +
+ "寤虹珛搴旀�ュ箍鎾郴缁燂紙濡傚巶鍖鸿鎶ャ�佺煭淇$兢鍙戯級銆俓n" +
+ "\n" +
+ "鏁板瓧鍖栭妗堬細\n" +
+ "\n" +
+ "灏嗗簲鎬ラ妗堝綍鍏ョЩ鍔ㄧ粓绔紝瀹炵幇涓�閿皟闃呫�佹楠ゆ寚寮曘��"
+}
+
+export function compliance(){
+ return "涓�銆佸父瑙佺殑鍚堣鎬ч棶棰榎n" +
+ "1. 璁稿彲涓庤祫璐ㄧ己澶盶n" +
+ "闂锛歕n" +
+ "\n" +
+ "鏈彇寰楀嵄闄╁寲瀛﹀搧缁忚惀璁稿彲璇佹垨瀹夊叏鐢熶骇璁稿彲璇併�俓n" +
+ "\n" +
+ "鐗圭浣滀笟浜哄憳锛堝鍘嬪姏瀹瑰櫒鎿嶄綔宸ワ級鏃犺瘉涓婂矖銆俓n" +
+ "\n" +
+ "椋庨櫓锛歕n" +
+ "\n" +
+ "鐩戠閮ㄩ棬澶勭綒锛堝缃氭銆佽矗浠ゅ仠浜э級銆俓n" +
+ "\n" +
+ "淇濋櫓鎷掕禂锛堜簨鏁呭彂鐢熸椂锛夈�俓n" +
+ "\n" +
+ "2. 瀹夊叏璁捐涓嶈揪鏍嘰n" +
+ "闂锛歕n" +
+ "\n" +
+ "鍌ㄧ綈銆佺閬撴湭鎸塆B/T 150锛堝帇鍔涘鍣ㄦ爣鍑嗭級璁捐銆俓n" +
+ "\n" +
+ "鏈畨瑁呭彲鐕�/鏈夋瘨姘斾綋鎶ヨ鍣紙杩濆弽GB 50493锛夈�俓n" +
+ "\n" +
+ "椋庨櫓锛歕n" +
+ "\n" +
+ "璁惧澶辨晥瀵艰嚧娉勬紡鎴栫垎鐐搞�俓n" +
+ "\n" +
+ "涓嶇鍚堝簲鎬ョ鐞嗛儴鎴朞SHA妫�鏌ヨ姹傘�俓n" +
+ "\n" +
+ "3. 鎿嶄綔涓庣淮鎶よ繚瑙刓n" +
+ "闂锛歕n" +
+ "\n" +
+ "鏈墽琛屼綔涓氱エ鍒跺害锛堝鍔ㄧ伀浣滀笟鏈鎵癸級銆俓n" +
+ "\n" +
+ "鏈畾鏈熸楠屽帇鍔涘鍣紙杩濆弽TSG 21-2016锛夈�俓n" +
+ "\n" +
+ "椋庨櫓锛歕n" +
+ "\n" +
+ "杩濊鎿嶄綔寮曞彂浜嬫晠锛堝鐒婃帴寮曞彂鍙噧姘斾綋鐖嗙偢锛夈�俓n" +
+ "\n" +
+ "璁惧鑰佸寲瀵艰嚧绐佸彂娉勬紡銆俓n" +
+ "\n" +
+ "4. 搴旀�ョ鐞嗕笉鍚堣\n" +
+ "闂锛歕n" +
+ "\n" +
+ "鏈埗瀹氬簲鎬ラ妗堟垨鏈妗堬紙杩濆弽銆婄敓浜у畨鍏ㄤ簨鏁呭簲鎬ユ潯渚嬨�嬶級銆俓n" +
+ "\n" +
+ "鏈厤澶囧簲鎬ョ墿璧勶紙濡傞槻姣掗潰鍏枫�佸牭婕忓伐鍏凤級銆俓n" +
+ "\n" +
+ "椋庨櫓锛歕n" +
+ "\n" +
+ "浜嬫晠鍙戠敓鏃舵棤娉曟湁鏁堟帶鍒讹紝瀵艰嚧鎹熷け鎵╁ぇ銆俓n" +
+ "\n" +
+ "闈复鐢熸�佺幆澧冮儴杩借矗锛堝鍖栧鍝佹薄鏌撳湡澹�/姘翠綋锛夈�俓n" +
+ "\n" +
+ "5. 璁板綍涓庢姤鍛婄己澶盶n" +
+ "闂锛歕n" +
+ "\n" +
+ "鏈繚瀛樺畨鍏ㄦ鏌ヨ褰曟垨鍩硅妗f銆俓n" +
+ "\n" +
+ "鏈寜瑙勫畾涓婃姤娉勬紡浜嬫晠锛堝鐬掓姤銆佽繜鎶ワ級銆俓n" +
+ "\n" +
+ "椋庨櫓锛歕n" +
+ "\n" +
+ "浜嬫晠璋冩煡鏃舵棤娉曡嚜璇佸悎瑙勶紝鎵挎媴鍏ㄨ矗銆俓n" +
+ "\n" +
+ "琚垪鍏ュ畨鍏ㄧ敓浜ч粦鍚嶅崟锛屽奖鍝嶄紒涓氫俊瑾夈��"
+}
+
+export function monitoring(){
+ return "涓�銆佸浐瀹氬紡鐩戞祴鎶�鏈痋n" +
+ "1. 鍌寲鐕冪儳寮忎紶鎰熷櫒\n" +
+ "鍘熺悊锛氬彲鐕冩皵浣撳湪閾備笣琛ㄩ潰鐕冪儳瀵艰嚧鐢甸樆鍙樺寲\n" +
+ "\n" +
+ "浼樺娍锛氭垚鏈綆锛堬骏500-2000/涓級銆佸搷搴斿揩锛�<10s锛塡n" +
+ "\n" +
+ "灞�闄愶細鏄撲腑姣掞紙纭�/纭寲鍚堢墿锛夈�佸鍛界煭锛�2-3骞达級\n" +
+ "\n" +
+ "閫傜敤锛氱煶鍖栧巶鍙噧姘旂洃娴嬶紙鐢茬兎銆佹阿姘旂瓑锛塡n" +
+ "\n" +
+ "2. 鐢靛寲瀛︿紶鎰熷櫒\n" +
+ "鍘熺悊锛氭皵浣撲笌鐢佃В娑插彂鐢熸哀鍖栬繕鍘熷弽搴斾骇鐢熺數娴乗n" +
+ "\n" +
+ "浼樺娍锛歱pb绾ф娴嬶紙濡侶2S妫�娴嬩笅闄�0.1ppm锛塡n" +
+ "\n" +
+ "灞�闄愶細鍙楁俯婀垮害褰卞搷锛堥渶瀹氭湡鏍″噯锛塡n" +
+ "\n" +
+ "閫傜敤锛氭湁姣掓皵浣擄紙Cl鈧傘�丯H鈧冦�丆O绛夛級\n" +
+ "\n" +
+ "3. 绾㈠鍚告敹寮忥紙NDIR锛塡n" +
+ "鍘熺悊锛氭皵浣撳鐗瑰畾绾㈠娉㈡鐨勫惛鏀剁巼妫�娴媆n" +
+ "\n" +
+ "浼樺娍锛氬厤鏍″噯锛堝鍛�5-10骞达級銆佹姉涓瘨\n" +
+ "\n" +
+ "灞�闄愶細楂樻垚鏈紙锟�5000+/涓級\n" +
+ "\n" +
+ "閫傜敤锛欳O鈧傘�丆H鈧勭瓑娓╁姘斾綋鐩戞祴\n" +
+ "\n" +
+ "4. 婵�鍏夊厜璋憋紙TDLAS锛塡n" +
+ "鍘熺悊锛氬彲璋冭皭婵�鍏変簩鏋佺鎵弿姘斾綋鍚告敹绾縗n" +
+ "\n" +
+ "浼樺娍锛歱pm绾х簿搴︺�佸搷搴攎s绾n" +
+ "\n" +
+ "灞�闄愶細闇�鍏夊瀵瑰噯锛堝畨瑁呭鏉傦級\n" +
+ "\n" +
+ "閫傜敤锛氱閬撳井娉勬紡妫�娴嬶紙澶╃劧姘旈暱杈撶绾匡級"
+}
+
+export function checking(keyWord){
+ if(keyWord.includes("姘斾綋娉勬紡")){
+ return gasLeaks();
+ }
+ if(keyWord.includes("瀹瑰櫒澶辨晥")){
+ return shipping();
+ }
+ if(keyWord.includes("鎿嶄綔涓嶅綋")){
+ return operate();
+ }
+ if(keyWord.includes("鍝嶅簲涓嶈冻")){
+ return emergency();
+ }
+ if(keyWord.includes("鍚堣鎬�")){
+ return compliance();
+ }
+ if(keyWord.includes("鐩戞祴鎶�鏈�")){
+ return monitoring();
+ }
+ return "涓嶅ソ鎰忔�濓紝灏忔櫤杩樺湪鎴愰暱杩囩▼涓紝鎮ㄧ殑闂宸茬粡瓒呰繃灏忔櫤鐨勫鐞嗚寖鍥翠簡銆�";
+
+}
\ No newline at end of file
diff --git a/src/views/chatHome/chatHomeIndex/home.vue b/src/views/chatHome/chatHomeIndex/home.vue
new file mode 100644
index 0000000..7796284
--- /dev/null
+++ b/src/views/chatHome/chatHomeIndex/home.vue
@@ -0,0 +1,175 @@
+<template>
+<div class="home">
+ <div style="background: white;color: black;font-size: 30px;" class="logo">
+ <div class="logo-one" style="font-weight: bold">
+<!-- <img src="/src/assets/img/logo.png" style="width: 50px;height: 50px;margin: 0 10px" />-->
+ <div><i>澶фā鍨婣I灏忔櫤姝e湪涓烘偍鏈嶅姟</i></div>
+ </div>
+ <div class="input">
+ <input type="text" v-model="keyWord" class="input-text" placeholder="缁欏皬鏅哄彂閫佹秷鎭�" @keyup.enter="sendMsg" />
+ <div style="font-size: 13px;color: #808080;display: flex;justify-content: space-between;padding: 10px;">
+ <div style="display: flex;justify-content: center;align-items: center;">
+<!-- <div style="display: flex;justify-content: center;align-items: center;">-->
+<!-- <img src="/src/assets/img/logo.png" style="width: 15px;height: 15px;margin: 0 5px" />-->
+<!-- <span>娣卞害鎬濊��(R1)</span>-->
+<!-- </div>-->
+<!-- <div style="display: flex;justify-content: center;align-items: center;">-->
+<!-- <img src="/src/assets/img/logo.png" style="width: 15px;height: 15px;margin: 0 5px" />-->
+<!-- <span>鑱旂綉鎼滅储</span>-->
+<!-- </div>-->
+ </div>
+ <div style="display: flex;justify-content: center;align-items: center;margin-right: 5px;">
+<!-- <img src="/src/assets/img/logo.png" style="width: 25px;height: 25px;margin: 0 5px" />-->
+ <img src="@/assets/img/emoji/rocket.png" style="width: 25px;height: 25px;margin: 0 5px" @click="sendMsg"/>
+ </div>
+ </div>
+ </div>
+ <div style="width: 780px;">
+ <div style="font-weight: bold;margin: 30px 0;">鐑棬鎺ㄨ崘</div>
+ <div class="keywords">
+ <div class="keywordss" @click="sendMsgDefault(keyWordOne)">
+ <p class="fontSize aaa">{{keyWordOne}}</p>
+ <p class="fontSize">闃�闂ㄣ�佺閬撴垨瀹瑰櫒瀵嗗皝澶辨晥瀵艰嚧姘斾綋娉勬紡锛堝姘皵銆佹皑姘旓級銆�</p>
+ <p class="fontSize">鍚庢灉锛氫腑姣掋�佺垎鐐搞�佺幆澧冩薄鏌撱��</p>
+ </div>
+ <div class="keywordss" @click="sendMsgDefault(keyWordTwo)">
+ <p class="fontSize aaa">{{keyWordTwo}}</p>
+ <p class="fontSize">閽㈢摱鎴栫綈浣撳洜鏉愭枡鐤插姵銆佽厫铓�鎴栬秴鍘嬬牬瑁�</p>
+ <p class="fontSize">鍘熷洜锛氭湭瀹氭湡妫�娴嬨�佽繚瑙勫厖瑁呮垨澶栭儴鎾炲嚮銆傘��</p>
+ </div>
+ </div>
+ <div class="keywords">
+ <div class="keywordss" @click="sendMsgDefault(keyWordFive)">
+ <p class="fontSize aaa">{{keyWordFive}}</p>
+ <p class="fontSize">瑁呭嵏杩囩▼涓繚瑙勬搷浣滐紙濡傞噹铔惉杩愩�佹贩瑁呯蹇岀墿璐級銆�</p>
+ <p class="fontSize">杩愯緭閫斾腑鏈浐瀹氬鍣紝瀵艰嚧纰版挒鎴栧�惧�掋��</p>
+ </div>
+ <div class="keywordss" @click="sendMsgDefault(keyWordSix)">
+ <p class="fontSize aaa">{{keyWordSix}}</p>
+ <p class="fontSize">缂轰箯娉勬紡搴旀�ラ妗堬紝浜哄憳鍩硅涓嶈冻銆�</p>
+ <p class="fontSize">鏁戞彺璁惧锛堝闃叉瘨闈㈠叿銆佸牭婕忓伐鍏凤級缂哄け鎴栧け鏁堛��</p>
+ </div>
+ </div>
+ <div class="keywords">
+ <div class="keywordss" @click="sendMsgDefault(keyWordServen)">
+ <p class="fontSize aaa">{{keyWordServen}}</p>
+ <p class="fontSize">鏈彇寰楄繍杈撹祫璐紙濡侫DR/RID绛夊浗闄呰鑼冿級銆�</p>
+ <p class="fontSize">璺嚎瑙勫垝涓嶅悎瑙勶紙濡傜┛瓒婁汉鍙e瘑闆嗗尯锛夈��</p>
+ </div>
+ <div class="keywordss" @click="sendMsgDefault(keyWordEight)">
+ <p class="fontSize aaa">{{keyWordEight}}</p>
+ <p class="fontSize">浼犳劅鍣ㄩ儴缃诧紙濡傜孩澶栨皵浣撴帰娴嬪櫒銆佺數鍖栧浼犳劅鍣級銆�</p>
+ <p class="fontSize">瀹炴椂鏁版嵁浼犺緭鑷崇洃鎺у钩鍙帮紝瑙﹀彂鎶ヨ銆�</p>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div></div>
+</div>
+</template>
+
+<script setup>
+import { ref,onMounted } from "vue";
+import {useRoute,useRouter} from "vue-router"
+const route = useRoute();
+const router = useRouter();
+const keyWord = ref('');
+const keyWordOne = ref('鍗遍櫓姘斾綋娉勬紡鎬庝箞鍔�');
+const keyWordTwo = ref('杩愯緭瀹瑰櫒澶辨晥鎬庝箞鍔�');
+const keyWordFive = ref('鎿嶄綔涓嶅綋鎬庝箞鍔�');
+const keyWordSix = ref('搴旀�ュ搷搴斾笉瓒虫�庝箞鍔�');
+const keyWordServen = ref('鍚堣鎬ч棶棰�');
+const keyWordEight = ref('娉勬紡鐩戞祴鎶�鏈湁鍝簺');
+
+const sendMsg = () => {
+ router.push({ path: '/main/MobileChat',query:{ keyWord: keyWord.value} })
+}
+
+const sendMsgDefault = (value) => {
+ router.push({ path: '/main/MobileChat',query:{ keyWord: value} })
+}
+
+</script>
+
+<style lang="scss" scoped>
+.home {
+ width: 100%;
+ height: 91vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .logo {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+ z-index: 99;
+ width: 100%;
+ height: 100%;
+ color: #fff;
+ cursor: pointer;
+ overflow: hidden;
+ background-color: #F0F6F9;
+
+ .keywords {
+ display: flex;
+ width: 100%;
+ height: 90px;
+ line-height: 80px;
+ justify-content: space-between;
+ margin: 10px 0;
+
+ .keywordss {
+ box-shadow: 0px 2px 5px #b8b8b8;
+ width: 48%;
+ background: #e0edfc;
+ border-radius: 10px;
+
+ .aaa {
+ font-weight: bold;
+ font-size: 15px !important;
+ }
+
+ .fontSize {
+ font-size: 13px;
+ height: 20px;
+ line-height: 20px;
+ margin: 6px;
+ }
+ }
+ }
+
+ .logo-one {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-bottom: 20px;
+ }
+
+ .input {
+ width: 780px;
+ height: 150px;
+ background: #f5f4f4;
+ border-radius: 20px;
+
+ .input-text {
+ font-size: 18px;
+ width: 568px;
+ border-radius: 20px 20px 0 0;
+ height: 90px;
+ padding-left: 10px;
+ border: none;
+ color: black;
+ background-color: #f5f4f4;
+ }
+
+ .input-text:focus {
+ outline: none;
+ border: none;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/approvalManagement/index.vue b/src/views/collaborativeApproval/approvalManagement/index.vue
new file mode 100644
index 0000000..4638392
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalManagement/index.vue
@@ -0,0 +1,881 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <div class="header-title">
+ <el-icon class="title-icon"><Setting /></el-icon>
+ <span>瀹℃壒娴佺▼閰嶇疆</span>
+ </div>
+ <div class="header-desc">涓轰笉鍚屽鎵圭被鍨嬮厤缃鎵规祦绋嬪拰瀹℃壒浜�</div>
+ </div>
+
+ <!-- 瀹℃壒绫诲瀷鍒囨崲 - 绱у噾鏍囩寮� -->
+ <div class="type-tabs">
+ <div
+ v-for="type in approveTypes"
+ :key="type.value"
+ class="type-tab"
+ :class="{ active: activeTab === type.value }"
+ @click="activeTab = type.value; handleTabChange()"
+ >
+ <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }">
+ <component :is="type.icon" />
+ </el-icon>
+ <span class="tab-name">{{ type.label }}</span>
+ </div>
+ </div>
+
+ <!-- 涓昏鍐呭鍖哄煙 -->
+ <el-card class="config-card" shadow="hover" v-loading="loading">
+ <template #header>
+ <div class="card-header">
+ <div class="header-left">
+ <div class="type-icon" :style="{ backgroundColor: getTypeColor(currentApproveType) }">
+ <el-icon :size="20" color="#fff"><component :is="getTypeIcon(currentApproveType)" /></el-icon>
+ </div>
+ <div class="header-info">
+ <span class="type-name">{{ currentApproveTypeName }}</span>
+ <el-tag :type="approverList.length > 0 ? 'success' : 'warning'" size="small" effect="light">
+ {{ approverList.length > 0 ? `宸查厤缃� ${approverList.length} 涓鎵逛汉` : '鏈厤缃鎵逛汉' }}
+ </el-tag>
+ </div>
+ </div>
+ <div class="header-actions" v-if="approverList.length > 0">
+ <el-button @click="handleReset" size="default">
+ <el-icon><RefreshLeft /></el-icon>
+ 閲嶇疆
+ </el-button>
+ <el-button type="primary" @click="handleSave" :loading="saveLoading" size="default">
+ <el-icon><Check /></el-icon>
+ 淇濆瓨閰嶇疆
+ </el-button>
+ </div>
+ </div>
+ </template>
+
+ <!-- 瀹℃壒娴佺▼灞曠ず -->
+ <div class="flow-wrapper" v-if="approverList.length > 0">
+ <div class="flow-container">
+ <div
+ v-for="(item, index) in approverList"
+ :key="index"
+ class="flow-item"
+ >
+ <!-- 瀹℃壒鑺傜偣鍗$墖 -->
+ <div class="node-card" :class="{ 'empty': !item.approverId }">
+ <!-- 椤堕儴搴忓彿鍜岀骇鍒� -->
+ <div class="node-badge">{{ index + 1 }}</div>
+
+ <!-- 澶村儚鍖哄煙 -->
+ <div class="node-avatar-section">
+ <div
+ class="node-avatar"
+ :class="{ 'has-user': item.approverId }"
+ :style="item.approverId ? { backgroundColor: getAvatarColor(item.approverName) } : {}"
+ >
+ <span v-if="item.approverId">{{ item.approverName.charAt(0) }}</span>
+ <el-icon v-else :size="24"><User /></el-icon>
+ </div>
+ <div class="node-level">{{ getLevelText(index) }}</div>
+ </div>
+
+ <!-- 閫夋嫨鍖哄煙 -->
+ <div class="node-select-section">
+ <el-select
+ v-model="item.approverId"
+ placeholder="閫夋嫨瀹℃壒浜�"
+ filterable
+ size="default"
+ @change="(val) => handleApproverChange(val, item)"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId"
+ />
+ </el-select>
+ </div>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="node-actions">
+ <el-button
+ type="primary"
+ circle
+ :disabled="index === 0"
+ @click="moveLeft(index)"
+ size="small"
+ class="action-btn"
+ title="鍓嶇Щ"
+ >
+ <el-icon><ArrowLeft /></el-icon>
+ </el-button>
+ <el-button
+ type="primary"
+ circle
+ :disabled="index === approverList.length - 1"
+ @click="moveRight(index)"
+ size="small"
+ class="action-btn"
+ title="鍚庣Щ"
+ >
+ <el-icon><ArrowRight /></el-icon>
+ </el-button>
+ <el-button
+ type="danger"
+ circle
+ @click="handleDelete(index)"
+ size="small"
+ class="action-btn"
+ >
+ <el-icon><Delete /></el-icon>
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 杩炴帴绠ご -->
+ <div class="arrow-connector" v-if="index < approverList.length - 1">
+ <div class="arrow-line"></div>
+ <el-icon class="arrow-icon"><ArrowRight /></el-icon>
+ </div>
+ </div>
+
+ <!-- 鏂板鑺傜偣鎸夐挳 - 鏀惧湪娴佺▼鏈�鍚� -->
+ <div class="add-node-item">
+ <div class="arrow-connector" v-if="approverList.length > 0">
+ <div class="arrow-line"></div>
+ <el-icon class="arrow-icon"><ArrowRight /></el-icon>
+ </div>
+ <div class="add-node-card" @click="handleAdd">
+ <div class="add-icon-wrapper">
+ <el-icon :size="28"><Plus /></el-icon>
+ </div>
+ <span class="add-text">鏂板瀹℃壒浜�</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 绌虹姸鎬� -->
+ <div class="empty-state" v-else>
+ <div class="empty-content">
+ <div class="empty-icon-wrapper">
+ <el-icon :size="48" color="#c0c4cc"><User /></el-icon>
+ </div>
+ <div class="empty-text">鏆傛棤瀹℃壒浜洪厤缃�</div>
+ <div class="empty-subtext">鐐瑰嚮涓嬫柟鎸夐挳娣诲姞绗竴涓鎵逛汉</div>
+ <el-button type="primary" size="large" @click="handleAdd" class="empty-add-btn">
+ <el-icon><Plus /></el-icon>
+ 鏂板瀹℃壒浜�
+ </el-button>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 搴曢儴鎻愮ず -->
+ <div class="bottom-tips">
+ <el-icon><InfoFilled /></el-icon>
+ <span>鎻愮ず锛氭瘡涓祦绋嬭嚦灏戦厤缃竴涓鎵逛汉锛屽鎵规寜椤哄簭娴佽浆锛屽彲閫氳繃绠ご璋冩暣椤哄簭</span>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import {
+ Plus, ArrowLeft, Delete, Check, RefreshLeft, Setting,
+ Suitcase, Calendar, Location, Money, ShoppingCart, DocumentChecked,
+ Van, ArrowRight, User, InfoFilled
+} from '@element-plus/icons-vue';
+import { getApproveProcessConfigNodeList, addApproveProcessConfigNode } from '@/api/collaborativeApproval/approvalManagement';
+import { userListNoPage } from '@/api/system/user';
+
+// 褰撳墠閫変腑鐨勬爣绛鹃〉
+const activeTab = ref('1');
+
+// 瀹℃壒绫诲瀷閰嶇疆鏁扮粍
+const approveTypes = [
+ { value: '1', label: '鍏嚭绠$悊', icon: 'Suitcase', color: '#409EFF' },
+ { value: '2', label: '璇峰亣绠$悊', icon: 'Calendar', color: '#67C23A' },
+ { value: '3', label: '鍑哄樊绠$悊', icon: 'Location', color: '#E6A23C' },
+ { value: '4', label: '鎶ラ攢绠$悊', icon: 'Money', color: '#F56C6C' },
+ { value: '5', label: '閲囪喘瀹℃壒', icon: 'ShoppingCart', color: '#909399' },
+ { value: '6', label: '鎶ヤ环瀹℃壒', icon: 'DocumentChecked', color: '#9B59B6' },
+ { value: '7', label: '鍙戣揣瀹℃壒', icon: 'Van', color: '#1ABC9C' },
+];
+
+// 瀹℃壒绫诲瀷鍚嶇О鏄犲皠
+const approveTypeNameMap = {
+ 1: '鍏嚭绠$悊',
+ 2: '璇峰亣绠$悊',
+ 3: '鍑哄樊绠$悊',
+ 4: '鎶ラ攢绠$悊',
+ 5: '閲囪喘瀹℃壒',
+ 6: '鎶ヤ环瀹℃壒',
+ 7: '鍙戣揣瀹℃壒',
+};
+
+// 瀹℃壒绫诲瀷鍥炬爣鏄犲皠
+const typeIconMap = {
+ 1: 'Suitcase',
+ 2: 'Calendar',
+ 3: 'Location',
+ 4: 'Money',
+ 5: 'ShoppingCart',
+ 6: 'DocumentChecked',
+ 7: 'Van',
+};
+
+// 瀹℃壒绫诲瀷棰滆壊鏄犲皠
+const typeColorMap = {
+ 1: '#409EFF',
+ 2: '#67C23A',
+ 3: '#E6A23C',
+ 4: '#F56C6C',
+ 5: '#909399',
+ 6: '#9B59B6',
+ 7: '#1ABC9C',
+};
+
+// 澶村儚棰滆壊姹�
+const avatarColors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#9B59B6', '#1ABC9C', '#FF6B6B', '#4ECDC4'];
+
+// 褰撳墠瀹℃壒绫诲瀷鍚嶇О
+const currentApproveTypeName = computed(() => {
+ return approveTypeNameMap[activeTab.value] || '鏈煡绫诲瀷';
+});
+
+// 褰撳墠瀹℃壒绫诲瀷
+const currentApproveType = computed(() => {
+ return Number(activeTab.value);
+});
+
+// 鑾峰彇绫诲瀷鍥炬爣
+const getTypeIcon = (type) => typeIconMap[type] || 'Setting';
+
+// 鑾峰彇绫诲瀷棰滆壊
+const getTypeColor = (type) => typeColorMap[type] || '#409EFF';
+
+// 鑾峰彇澶村儚棰滆壊
+const getAvatarColor = (name) => {
+ if (!name) return '#C0C4CC';
+ let hash = 0;
+ for (let i = 0; i < name.length; i++) {
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ return avatarColors[Math.abs(hash) % avatarColors.length];
+};
+
+// 鑾峰彇绾у埆鏂囨湰
+const getLevelText = (index) => {
+ const texts = ['绗竴绾�', '绗簩绾�', '绗笁绾�', '绗洓绾�', '绗簲绾�', '绗叚绾�', '绗竷绾�', '绗叓绾�'];
+ return texts[index] || `绗�${index + 1}绾;
+};
+
+// 瀹℃壒浜哄垪琛紙鐪熷疄鎺ュ彛锛�
+const userList = ref([]);
+
+// 瀹℃壒浜哄垪琛�
+const approverList = ref([]);
+
+// 鍘熷鏁版嵁锛岀敤浜庨噸缃�
+const originalList = ref([]);
+
+// 鍔犺浇鐘舵��
+const loading = ref(false);
+const saveLoading = ref(false);
+
+// 鏍囩椤靛垏鎹㈠鐞�
+const handleTabChange = () => {
+ loadData();
+};
+
+// 鍔犺浇瀹℃壒閰嶇疆鏁版嵁锛堟ā鎷燂級
+const loadData = async () => {
+ loading.value = true;
+ try {
+ const res = await getApproveProcessConfigNodeList(currentApproveType.value);
+ const source = Array.isArray(res?.data)
+ ? res.data
+ : Array.isArray(res?.rows)
+ ? res.rows
+ : Array.isArray(res?.data?.records)
+ ? res.data.records
+ : [];
+ const data = source.map((item, index) => ({
+ ...item,
+ sortOrder: item.nodeOrder ?? item.sortOrder ?? index + 1,
+ }));
+ approverList.value = data.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
+ originalList.value = JSON.parse(JSON.stringify(approverList.value));
+ } catch (error) {
+ approverList.value = [];
+ originalList.value = [];
+ ElMessage.error('鍔犺浇瀹℃壒閰嶇疆澶辫触');
+ } finally {
+ loading.value = false;
+ }
+};
+
+const loadUserList = async () => {
+ try {
+ const res = await userListNoPage();
+ userList.value = Array.isArray(res?.data) ? res.data : [];
+ } catch (error) {
+ userList.value = [];
+ ElMessage.error('鍔犺浇浜哄憳鍒楄〃澶辫触');
+ }
+};
+
+// 瀹℃壒浜洪�夋嫨鍙樺寲
+const handleApproverChange = (userId, row) => {
+ const user = userList.value.find((u) => u.userId === userId);
+ if (user) {
+ row.approverName = user.nickName;
+ }
+};
+
+// 鏂板瀹℃壒浜�
+const handleAdd = () => {
+ const newOrder = approverList.value.length + 1;
+ approverList.value.push({
+ id: null,
+ approveType: currentApproveType.value,
+ approverId: null,
+ approverName: '',
+ sortOrder: newOrder,
+ });
+};
+
+// 鍒犻櫎瀹℃壒浜�
+const handleDelete = (index) => {
+ ElMessageBox.confirm('纭畾鍒犻櫎璇ュ鎵逛汉鍚楋紵', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ })
+ .then(() => {
+ approverList.value.splice(index, 1);
+ approverList.value.forEach((item, idx) => {
+ item.sortOrder = idx + 1;
+ });
+ ElMessage.success('鍒犻櫎鎴愬姛');
+ })
+ .catch(() => {});
+};
+
+// 鍓嶇Щ
+const moveLeft = (index) => {
+ if (index === 0) return;
+ const temp = approverList.value[index];
+ approverList.value[index] = approverList.value[index - 1];
+ approverList.value[index - 1] = temp;
+ approverList.value[index].sortOrder = index + 1;
+ approverList.value[index - 1].sortOrder = index;
+};
+
+// 鍚庣Щ
+const moveRight = (index) => {
+ if (index === approverList.value.length - 1) return;
+ const temp = approverList.value[index];
+ approverList.value[index] = approverList.value[index + 1];
+ approverList.value[index + 1] = temp;
+ approverList.value[index].sortOrder = index + 1;
+ approverList.value[index + 1].sortOrder = index + 2;
+};
+
+// 淇濆瓨閰嶇疆
+const handleSave = async () => {
+ if (approverList.value.length === 0) {
+ ElMessage.warning('璇疯嚦灏戦厤缃竴涓鎵逛汉');
+ return;
+ }
+
+ const hasEmptyApprover = approverList.value.some((item) => !item.approverId);
+ if (hasEmptyApprover) {
+ ElMessage.warning('璇烽�夋嫨鎵�鏈夊鎵逛汉');
+ return;
+ }
+
+ const approverIds = approverList.value.map((item) => item.approverId);
+ const uniqueIds = [...new Set(approverIds)];
+ if (uniqueIds.length !== approverIds.length) {
+ ElMessage.warning('瀹℃壒浜轰笉鑳介噸澶�');
+ return;
+ }
+
+ saveLoading.value = true;
+ try {
+ const payload = approverList.value.map((item, index) => ({
+ approveType: currentApproveType.value,
+ nodeOrder: index + 1,
+ approverId: item.approverId,
+ approverName: item.approverName,
+ }));
+ await addApproveProcessConfigNode(payload);
+ ElMessage.success('淇濆瓨鎴愬姛');
+ await loadData();
+ } catch (error) {
+ ElMessage.error('淇濆瓨澶辫触');
+ } finally {
+ saveLoading.value = false;
+ }
+};
+
+// 閲嶇疆
+const handleReset = () => {
+ if (originalList.value.length === 0) {
+ approverList.value = [];
+ return;
+ }
+ ElMessageBox.confirm('纭畾瑕侀噸缃綋鍓嶉厤缃悧锛熸湭淇濆瓨鐨勬洿鏀瑰皢涓㈠け銆�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ })
+ .then(() => {
+ approverList.value = JSON.parse(JSON.stringify(originalList.value));
+ ElMessage.success('宸查噸缃�');
+ })
+ .catch(() => {});
+};
+
+onMounted(async () => {
+ await loadUserList();
+ await loadData();
+});
+</script>
+
+<style scoped>
+.page-header {
+ margin-bottom: 20px;
+}
+
+.header-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--el-text-color-primary, #303133);
+ margin-bottom: 6px;
+}
+
+.title-icon {
+ font-size: 24px;
+ color: var(--el-color-primary, #409EFF);
+}
+
+.header-desc {
+ font-size: 13px;
+ color: var(--el-text-color-secondary, #909399);
+ margin-left: 34px;
+}
+
+/* 瀹℃壒绫诲瀷鍒囨崲 - 绱у噾鏍囩寮� */
+.type-tabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 16px;
+ padding: 4px;
+ background: var(--el-fill-color-light, #f5f7fa);
+ border-radius: 8px;
+ overflow-x: auto;
+}
+
+.type-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 14px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ font-size: 13px;
+ color: var(--el-text-color-regular, #606266);
+}
+
+.type-tab:hover {
+ background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
+ color: var(--el-color-primary, #409EFF);
+}
+
+.type-tab.active {
+ background: var(--el-bg-color, #fff);
+ color: var(--el-color-primary, #409EFF);
+ font-weight: 600;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.tab-name {
+ font-size: 13px;
+}
+
+.tab-count {
+ min-width: 16px;
+ height: 16px;
+ padding: 0 5px;
+ background: var(--el-color-success, #67C23A);
+ color: #fff;
+ border-radius: 8px;
+ font-size: 11px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.config-card {
+ margin-bottom: 16px;
+ border-radius: 12px;
+}
+
+:deep(.el-card__header) {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+}
+
+.type-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.header-info {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.type-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--el-text-color-primary, #303133);
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.flow-wrapper {
+ overflow-x: auto;
+ padding: 8px 4px;
+}
+
+.flow-container {
+ display: flex;
+ align-items: center;
+ gap: 0;
+ min-width: min-content;
+}
+
+.flow-item {
+ display: flex;
+ align-items: center;
+}
+
+.node-card {
+ width: 200px;
+ background: var(--el-bg-color, #fff);
+ border: 2px solid var(--el-border-color, #e4e7ed);
+ border-radius: 12px;
+ padding: 16px;
+ position: relative;
+ transition: all 0.3s ease;
+ flex-shrink: 0;
+}
+
+.node-card:hover {
+ border-color: var(--el-color-primary, #409EFF);
+ box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
+ transform: translateY(-2px);
+}
+
+.node-card.empty {
+ border-style: dashed;
+ border-color: var(--el-border-color, #c0c4cc);
+ background: var(--el-fill-color-light, #fafbfc);
+}
+
+.node-card.empty:hover {
+ border-color: var(--el-color-primary, #409EFF);
+ background: var(--el-fill-color-light, #f5f7fa);
+}
+
+.node-badge {
+ position: absolute;
+ top: -10px;
+ left: 16px;
+ width: 24px;
+ height: 24px;
+ background: var(--el-color-primary, #409EFF);
+ color: #fff;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 13px;
+ font-weight: 700;
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.4);
+}
+
+.node-avatar-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 12px;
+ margin-top: 4px;
+}
+
+.node-avatar {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: var(--el-fill-color, #f0f2f5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 8px;
+ color: var(--el-text-color-placeholder, #c0c4cc);
+ transition: all 0.3s ease;
+}
+
+.node-avatar.has-user {
+ color: #fff;
+ font-size: 22px;
+ font-weight: 600;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.node-level {
+ font-size: 12px;
+ color: var(--el-text-color-secondary, #909399);
+ font-weight: 500;
+}
+
+.node-select-section {
+ margin-bottom: 12px;
+}
+
+.node-select-section :deep(.el-select) {
+ width: 100%;
+}
+
+.node-actions {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding-top: 12px;
+ border-top: 1px solid var(--el-border-color-light, #ebeef5);
+}
+
+.action-btn {
+ transition: all 0.2s;
+}
+
+.action-btn:hover:not(:disabled) {
+ transform: scale(1.1);
+}
+
+.arrow-connector {
+ display: flex;
+ align-items: center;
+ width: 50px;
+ position: relative;
+}
+
+.arrow-line {
+ flex: 1;
+ height: 2px;
+ background: var(--el-border-color, #c0c4cc);
+}
+
+.arrow-icon {
+ color: var(--el-text-color-placeholder, #c0c4cc);
+ font-size: 14px;
+ margin-left: -2px;
+}
+
+/* 鏂板鑺傜偣鏍峰紡 */
+.add-node-item {
+ display: flex;
+ align-items: center;
+}
+
+.add-node-card {
+ width: 140px;
+ height: 200px;
+ border: 2px dashed var(--el-border-color, #c0c4cc);
+ border-radius: 12px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: var(--el-fill-color-light, #fafbfc);
+ flex-shrink: 0;
+ margin-left: 0;
+}
+
+.add-node-card:hover {
+ border-color: var(--el-color-primary, #409EFF);
+ background: var(--el-color-primary-light-9, #f0f7ff);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(64, 158, 255, 0.15);
+}
+
+.add-icon-wrapper {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: var(--el-color-primary, #409EFF);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
+ transition: all 0.3s ease;
+}
+
+.add-node-card:hover .add-icon-wrapper {
+ transform: scale(1.1);
+ box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
+}
+
+.add-text {
+ font-size: 14px;
+ color: var(--el-text-color-regular, #606266);
+ font-weight: 500;
+}
+
+.add-node-card:hover .add-text {
+ color: var(--el-color-primary, #409EFF);
+}
+
+/* 绌虹姸鎬� */
+.empty-state {
+ padding: 50px 20px;
+}
+
+.empty-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
+
+.empty-icon-wrapper {
+ width: 80px;
+ height: 80px;
+ border-radius: 50%;
+ background: var(--el-fill-color-light, #f5f7fa);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 8px;
+}
+
+.empty-text {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--el-text-color-regular, #606266);
+}
+
+.empty-subtext {
+ font-size: 13px;
+ color: var(--el-text-color-secondary, #909399);
+}
+
+.empty-add-btn {
+ margin-top: 8px;
+ padding: 12px 28px;
+}
+
+/* 搴曢儴鎻愮ず */
+.bottom-tips {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 12px 20px;
+ background: var(--el-fill-color-light, #f5f7fa);
+ border-radius: 8px;
+ color: var(--el-text-color-regular, #606266);
+ font-size: 13px;
+}
+
+.bottom-tips .el-icon {
+ color: var(--el-color-primary, #409EFF);
+ font-size: 16px;
+}
+
+@media (max-width: 768px) {
+ .type-tabs {
+ padding: 3px;
+ }
+
+ .type-tab {
+ padding: 6px 10px;
+ font-size: 12px;
+ }
+
+ .tab-name {
+ font-size: 12px;
+ }
+
+ .flow-container {
+ flex-wrap: wrap;
+ justify-content: center;
+ }
+
+ .arrow-connector {
+ width: 100%;
+ height: 30px;
+ flex-direction: row;
+ justify-content: center;
+ }
+
+ .arrow-line {
+ width: 2px;
+ height: 30px;
+ }
+
+ .arrow-icon {
+ right: auto;
+ top: auto;
+ bottom: -5px;
+ transform: rotate(90deg);
+ }
+
+ .add-node-item {
+ width: 100%;
+ justify-content: center;
+ margin-top: 10px;
+ }
+
+ .add-node-item .arrow-connector {
+ display: none;
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue b/src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
new file mode 100644
index 0000000..0605fe5
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -0,0 +1,634 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible"
+ :title="operationType === 'approval' ? '瀹℃壒' : '璇︽儏'"
+ width="700px"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ ref="formRef">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="娴佺▼缂栧彿锛�"
+ prop="approveId">
+ <el-input v-model="form.approveId"
+ placeholder="鑷姩缂栧彿"
+ clearable
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鐢宠閮ㄩ棬锛�">
+ <el-select disabled
+ v-model="form.approveDeptId"
+ placeholder="閫夋嫨閮ㄩ棬">
+ <el-option v-for="user in productOptions"
+ :key="user.deptId"
+ :label="user.deptName"
+ :value="user.deptId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="!isQuotationApproval && !isPurchaseApproval">
+ <el-col :span="24">
+ <el-form-item :label="props.approveType == 5 ? '閲囪喘鍚堝悓鍙凤細' : '瀹℃壒浜嬬敱锛�'"
+ prop="approveReason">
+ <el-input v-model="form.approveReason"
+ placeholder="璇疯緭鍏�"
+ clearable
+ type="textarea"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <!-- 鎶ヤ环瀹℃壒锛氬睍绀烘姤浠疯鎯咃紙澶嶇敤閿�鍞姤浠�"鏌ョ湅璇︽儏瀵硅瘽妗�"鍐呭缁撴瀯锛� -->
+ <div v-if="isQuotationApproval"
+ style="margin: 10px 0 18px;">
+ <el-divider content-position="left">鎶ヤ环璇︽儏</el-divider>
+ <el-skeleton :loading="quotationLoading"
+ animated>
+ <template #template>
+ <el-skeleton-item variant="h3"
+ style="width: 30%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ </template>
+ <template #default>
+ <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo"
+ description="鏈煡璇㈠埌瀵瑰簲鎶ヤ环璇︽儏" />
+ <template v-else>
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="鎶ヤ环鍗曞彿">{{ currentQuotation.quotationNo }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ currentQuotation.customer }}</el-descriptions-item>
+ <el-descriptions-item label="涓氬姟鍛�">{{ currentQuotation.salesperson }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ヤ环鏃ユ湡">{{ currentQuotation.quotationDate }}</el-descriptions-item>
+ <el-descriptions-item label="鏈夋晥鏈熻嚦">{{ currentQuotation.validDate }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ヤ环鎬婚"
+ :span="2">
+ <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
+ </span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="currentQuotation.products || []"
+ border
+ style="width: 100%">
+ <el-table-column prop="product"
+ label="浜у搧鍚嶇О" />
+ <el-table-column prop="specification"
+ label="瑙勬牸鍨嬪彿" />
+ <el-table-column prop="unit"
+ label="鍗曚綅" />
+ <el-table-column prop="unitPrice"
+ label="鍗曚环">
+ <template #default="scope">楼{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <div v-if="currentQuotation.remark"
+ style="margin-top: 20px;">
+ <h4>澶囨敞</h4>
+ <p>{{ currentQuotation.remark }}</p>
+ </div>
+ </template>
+ </template>
+ </el-skeleton>
+ </div>
+ <!-- 閲囪喘瀹℃壒锛氬睍绀洪噰璐鎯� -->
+ <div v-if="isPurchaseApproval"
+ style="margin: 10px 0 18px;">
+ <el-divider content-position="left">閲囪喘璇︽儏</el-divider>
+ <el-skeleton :loading="purchaseLoading"
+ animated>
+ <template #template>
+ <el-skeleton-item variant="h3"
+ style="width: 30%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ </template>
+ <template #default>
+ <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
+ description="鏈煡璇㈠埌瀵瑰簲閲囪喘璇︽儏" />
+ <template v-else>
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="閲囪喘鍚堝悓鍙�">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item>
+ <el-descriptions-item label="渚涘簲鍟嗗悕绉�">{{ currentPurchase.supplierName }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鍚嶇О">{{ currentPurchase.projectName }}</el-descriptions-item>
+ <el-descriptions-item label="閿�鍞悎鍚屽彿">{{ currentPurchase.salesContractNo }}</el-descriptions-item>
+ <el-descriptions-item label="绛捐鏃ユ湡">{{ currentPurchase.executionDate }}</el-descriptions-item>
+ <el-descriptions-item label="褰曞叆鏃ユ湡">{{ currentPurchase.entryDate }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡">{{ currentPurchase.paymentMethod }}</el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓閲戦"
+ :span="2">
+ <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
+ </span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="currentPurchase.productData || []"
+ border
+ style="width: 100%">
+ <el-table-column prop="productCategory"
+ label="浜у搧鍚嶇О" />
+ <el-table-column prop="specificationModel"
+ label="瑙勬牸鍨嬪彿" />
+ <el-table-column prop="unit"
+ label="鍗曚綅" />
+ <el-table-column prop="quantity"
+ label="鏁伴噺" />
+ <el-table-column prop="taxInclusiveUnitPrice"
+ label="鍚◣鍗曚环">
+ <template #default="scope">楼{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ <el-table-column prop="taxInclusiveTotalPrice"
+ label="鍚◣鎬讳环">
+ <template #default="scope">楼{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </template>
+ </template>
+ </el-skeleton>
+ </div>
+ <!-- 鍙戣揣瀹℃壒锛氬睍绀哄彂璐ц鎯� -->
+ <div v-if="isDeliveryApproval"
+ style="margin: 10px 0 18px;">
+ <el-divider content-position="left">鍙戣揣璇︽儏</el-divider>
+ <el-skeleton :loading="deliveryLoading"
+ animated>
+ <template #template>
+ <el-skeleton-item variant="h3"
+ style="width: 30%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ </template>
+ <template #default>
+ <el-empty v-if="!currentDelivery || !currentDelivery.shippingInfo"
+ description="鏈煡璇㈠埌瀵瑰簲鍙戣揣璇︽儏" />
+ <template v-else>
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="閿�鍞鍗�">{{ currentDelivery.shippingInfo.salesContractNo || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣璁㈠崟鍙�">{{ currentDelivery.shippingInfo.shippingNo || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ currentDelivery.shippingInfo.customerName || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣绫诲瀷">{{ currentDelivery.shippingInfo.type || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣鏃ユ湡">{{ currentDelivery.shippingInfo.shippingDate || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳鐘舵��">{{ currentDelivery.shippingInfo.status || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣杞︾墝鍙�">{{ currentDelivery.shippingInfo.shippingCarNumber || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="蹇�掑叕鍙�">{{ currentDelivery.shippingInfo.expressCompany || '--' }}</el-descriptions-item>
+ <el-descriptions-item label="蹇�掑崟鍙�"
+ :span="2">{{ currentDelivery.shippingInfo.expressNumber || '--' }}</el-descriptions-item>
+ </el-descriptions>
+ <div style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="deliveryProductList"
+ border
+ size="small"
+ style="width: 100%">
+ <el-table-column prop="batchNo"
+ label="鎵瑰彿"
+ show-overflow-tooltip />
+ <el-table-column prop="productName"
+ label="浜у搧鍚嶇О"
+ show-overflow-tooltip />
+ <el-table-column prop="specificationModel"
+ label="瑙勬牸鍨嬪彿"
+ show-overflow-tooltip />
+ <el-table-column prop="deliveryQuantity"
+ label="鍙戣揣鏁伴噺"
+ align="center" />
+ </el-table>
+ </div>
+ <div v-if="currentDelivery.shippingInfo.storageBlobVOs && currentDelivery.shippingInfo.storageBlobVOs.length"
+ style="margin-top: 20px;">
+ <h4>鍙戣揣鍥剧墖</h4>
+ <ImagePreview :file-list="currentDelivery.shippingInfo.storageBlobVOs" />
+ </div>
+ </template>
+ </template>
+ </el-skeleton>
+ </div>
+ <el-form :model="{ activities }"
+ ref="formRef"
+ label-position="top">
+ <el-steps :active="getActiveStep()"
+ finish-status="success"
+ process-status="process"
+ align-center
+ direction="vertical">
+ <el-step v-for="(activity, index) in activities"
+ :key="index"
+ finish-status="success"
+ :title="getNodeTitle(index, activities.length)"
+ :description="activity.approveNodeUser"
+ :icon="getNodeIcon(activity, index)">
+ <template #icon>
+ <el-icon v-if="activity.approveNodeStatus === 2"
+ color="red"
+ :size="22">
+ <WarningFilled />
+ </el-icon>
+ <el-icon v-else-if="activity.isShen"
+ color="#1890ff"
+ :size="22">
+ <Edit />
+ </el-icon>
+ <el-icon v-else-if="activity.approveNodeStatus === 1"
+ color="#67C23A"
+ :size="26">
+ <Check />
+ </el-icon>
+ <el-icon v-else
+ color="#C0C4CC"
+ :size="22">
+ <MoreFilled />
+ </el-icon>
+ </template>
+ <template #title>
+ <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
+ </template>
+ <template #description>
+ <div class="node-user">
+ <div class="avatar-wrapper">
+ <img :src="userStore.avatar"
+ class="user-avatar"
+ alt="" />
+ </div>
+ <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
+ </div>
+ <div v-if="!activity.isShen"
+ class="node-reason">
+ <span>瀹℃壒鎰忚锛�</span>{{ activity.approveNodeReason }}
+ </div>
+ <div v-else-if="activity.isShen">
+ <el-form-item :prop="'activities.' + index + '.approveNodeReason'"
+ :rules="[{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]">
+ <el-input v-model="activity.approveNodeReason"
+ clearable
+ type="textarea"
+ :disabled="operationType === 'view'"></el-input>
+ </el-form-item>
+ </div>
+ </template>
+ </el-step>
+ </el-steps>
+ </el-form>
+ <template #footer
+ v-if="operationType === 'approval'">
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm(2)">涓嶉�氳繃</el-button>
+ <el-button type="primary"
+ @click="submitForm(1)">閫氳繃</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import {
+ computed,
+ getCurrentInstance,
+ nextTick,
+ reactive,
+ ref,
+ toRefs,
+ } from "vue";
+ import {
+ approveProcessDetails,
+ getDept,
+ updateApproveNode,
+ } from "@/api/collaborativeApproval/approvalProcess.js";
+ import useUserStore from "@/store/modules/user.js";
+ import {
+ WarningFilled,
+ Edit,
+ Check,
+ MoreFilled,
+ } from "@element-plus/icons-vue";
+ import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
+ import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
+ import { getDeliveryDetailByShippingNo } from "@/api/salesManagement/deliveryLedger.js";
+ import ImagePreview from "@/components/AttachmentPreview/image/index.vue";
+ const emit = defineEmits(["close"]);
+ const { proxy } = getCurrentInstance();
+
+ const props = defineProps({
+ approveType: {
+ type: [Number, String],
+ default: 0,
+ },
+ });
+
+ const dialogFormVisible = ref(false);
+ const operationType = ref("");
+ const activities = ref([]);
+ const formRef = ref(null);
+ const userStore = useUserStore();
+ const productOptions = ref([]);
+ const quotationLoading = ref(false);
+ const currentQuotation = ref({});
+ const purchaseLoading = ref(false);
+ const currentPurchase = ref({});
+ const deliveryLoading = ref(false);
+ const currentDelivery = ref({});
+ const deliveryProductList = ref([]);
+ const isQuotationApproval = computed(() => Number(props.approveType) === 6);
+ const isPurchaseApproval = computed(() => Number(props.approveType) === 5);
+ const isDeliveryApproval = computed(() => Number(props.approveType) === 7);
+
+ const data = reactive({
+ form: {
+ approveId: "",
+ approveDeptId: "",
+ approveReason: "",
+ checkResult: "",
+ },
+ });
+ const { form } = toRefs(data);
+
+ // 鑺傜偣鏍囬
+ const getNodeTitle = (index, len) => {
+ if (index === len - 1) return "缁撴潫";
+ return "瀹℃壒";
+ };
+
+ // 鑾峰彇褰撳墠婵�娲绘楠�
+ const getActiveStep = () => {
+ // 濡傛灉鎵�鏈� isShen 閮戒负 false锛岃繑鍥炴渶鍚庝竴涓楠わ紙鍏ㄩ儴瀹屾垚锛�
+ const hasActive = activities.value.some(a => a.isShen === true);
+ if (!hasActive) return activities.value.length;
+ // 褰撳墠鑺傜偣绱㈠紩
+ return activities.value.findIndex(a => a.isShen == true);
+ };
+ // 姝ラicon
+ const getNodeIcon = (activity, index) => {
+ if (activity.approveNodeStatus === 2) return "el-icon-warning"; // 涓嶉�氳繃
+ if (activity.isShen) return "Edit";
+ return "";
+ };
+
+ // 鎵撳紑寮规
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ currentQuotation.value = {};
+ currentPurchase.value = {};
+ form.value = { ...row };
+ // 绔嬪嵆娓呴櫎琛ㄥ崟楠岃瘉鐘舵�侊紙鍥犱负瀛楁鏄痙isabled鐨勶紝涓嶉渶瑕侀獙璇侊級
+ nextTick(() => {
+ if (formRef.value) {
+ formRef.value.clearValidate();
+ }
+ });
+ // 纭繚閫夐」鍔犺浇瀹屾垚鍚庡啀鍖归厤鍊肩被鍨�
+ getProductOptions().then(() => {
+ // 纭繚鍊肩被鍨嬪尮閰嶏紙濡傛灉閫夐」宸插姞杞斤級
+ if (productOptions.value.length > 0 && form.value.approveDeptId) {
+ const matchedOption = productOptions.value.find(
+ opt =>
+ opt.deptId == form.value.approveDeptId ||
+ String(opt.deptId) === String(form.value.approveDeptId)
+ );
+ if (matchedOption) {
+ form.value.approveDeptId = matchedOption.deptId;
+ }
+ }
+ // 鍐嶆娓呴櫎楠岃瘉锛岀‘淇濋�夐」鍔犺浇鍚庡�煎尮閰嶆纭�
+ nextTick(() => {
+ if (formRef.value) {
+ formRef.value.clearValidate();
+ }
+ });
+ });
+
+ // 鎶ヤ环瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"鎶ヤ环鍗曞彿"鍘绘煡鎶ヤ环鍒楄〃
+ if (isQuotationApproval.value) {
+ const quotationNo = row?.approveReason;
+ if (quotationNo) {
+ quotationLoading.value = true;
+ getQuotationList({ quotationNo })
+ .then(res => {
+ const records = res?.data?.records || [];
+ currentQuotation.value = records[0] || {};
+ })
+ .finally(() => {
+ quotationLoading.value = false;
+ });
+ }
+ }
+
+ // 閲囪喘瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"閲囪喘鍚堝悓鍙�"鍘绘煡閲囪喘璇︽儏
+ if (isPurchaseApproval.value) {
+ const purchaseContractNumber = row?.approveReason;
+ if (purchaseContractNumber) {
+ purchaseLoading.value = true;
+ getPurchaseByCode({ purchaseContractNumber })
+ .then(res => {
+ currentPurchase.value = res;
+ })
+ .catch(err => {
+ console.error("鏌ヨ閲囪喘璇︽儏澶辫触:", err);
+ proxy.$modal.msgError("鏌ヨ閲囪喘璇︽儏澶辫触");
+ })
+ .finally(() => {
+ purchaseLoading.value = false;
+ });
+ }
+ }
+ // 鍙戣揣瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"鍙戣揣鍗曞彿"鍘绘煡鍙戣揣璇︽儏
+ if (isDeliveryApproval.value) {
+ const deliveryNo = row?.approveReason;
+ if (deliveryNo) {
+ deliveryLoading.value = true;
+ currentDelivery.value = {};
+ deliveryProductList.value = [];
+ getDeliveryDetailByShippingNo({ shippingNo: deliveryNo })
+ .then(res => {
+ const detailData = res?.data || res || {};
+ currentDelivery.value = detailData;
+ deliveryProductList.value =
+ detailData.shippingProductDetailDtoList || [];
+ })
+ .catch(err => {
+ console.error("鏌ヨ鍙戣揣璇︽儏澶辫触:", err);
+ proxy.$modal.msgError("鏌ヨ鍙戣揣璇︽儏澶辫触");
+ })
+ .finally(() => {
+ deliveryLoading.value = false;
+ });
+ }
+ }
+
+ approveProcessDetails(row.approveId).then(res => {
+ activities.value = res.data;
+ // 澧炲姞isApproval瀛楁
+ activities.value.forEach(item => {
+ if (item.url && item.url.includes("word")) {
+ item.urlTem = item.url.replaceAll("word", "img");
+ } else {
+ item.urlTem = item.url;
+ }
+ if (item.approveNodeStatus === 2) {
+ item.isApproval = "宸查┏鍥�";
+ } else if (item.approveNodeStatus === 1) {
+ item.isApproval = "宸插悓鎰�";
+ } else {
+ item.isApproval = "鏈鎵�";
+ }
+ });
+ });
+ };
+
+ const getDeliveryProductInfoList = () => {
+ const row = currentDelivery.value;
+ if (!row) return [];
+ const normalizeBatchNoList = value => {
+ if (Array.isArray(value)) return value;
+ if (typeof value === "string" && value.includes(",")) {
+ return value
+ .split(",")
+ .map(item => item.trim())
+ .filter(Boolean);
+ }
+ return value ? [value] : [];
+ };
+ const detailList = deliveryProductList.value.length
+ ? deliveryProductList.value
+ : [
+ row.batchNoDetailList,
+ row.batchNoList,
+ row.shippingBatchList,
+ row.shippingInfoDetailList,
+ row.detailList,
+ row.batchDetailList,
+ ].find(value => Array.isArray(value) && value.length);
+ const batchNoList = normalizeBatchNoList(row.batchNo);
+ const toTableRow = (item = {}) => ({
+ batchNo:
+ typeof item === "string" || typeof item === "number"
+ ? item
+ : item.batchNo ?? item.batchNumber ?? row.batchNo ?? "--",
+ productName: item.productName ?? row.productName ?? "--",
+ specificationModel:
+ item.specificationModel ?? item.model ?? row.specificationModel ?? "--",
+ deliveryQuantity:
+ item.deliveryQuantity ??
+ item.quantity ??
+ item.shippingQuantity ??
+ row.deliveryQuantity ??
+ row.quantity ??
+ "--",
+ });
+ if (detailList?.length) {
+ return detailList.map(toTableRow);
+ }
+ if (batchNoList.length) {
+ return batchNoList.map(batchNo => toTableRow({ batchNo }));
+ }
+ return [toTableRow()];
+ };
+ const getApprovalStatusText = status => {
+ const statusMap = {
+ 0: "寰呭鏍�",
+ 1: "瀹℃牳閫氳繃",
+ 2: "瀹℃牳鎷掔粷",
+ 3: "瀹℃牳涓�",
+ };
+ return statusMap[status] || "寰呭鏍�";
+ };
+
+ const getProductOptions = () => {
+ return getDept().then(res => {
+ productOptions.value = res.data;
+ });
+ };
+ // 鎻愪氦瀹℃壒
+ const submitForm = status => {
+ const filteredActivities = activities.value.filter(
+ activity => activity.isShen
+ );
+ if (!filteredActivities || filteredActivities.length === 0) {
+ proxy.$modal.msgError("鏈壘鍒板緟瀹℃壒鐨勮妭鐐�");
+ return;
+ }
+ const currentActivity = filteredActivities[0];
+ if (!currentActivity) {
+ proxy.$modal.msgError("鏈壘鍒板緟瀹℃壒鐨勮妭鐐�");
+ return;
+ }
+ currentActivity.approveNodeStatus = status;
+ // 鍒ゆ柇鏄惁涓烘渶鍚庝竴姝�
+ const isLast =
+ activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
+ updateApproveNode({ ...currentActivity, isLast }).then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ quotationLoading.value = false;
+ currentQuotation.value = {};
+ purchaseLoading.value = false;
+ currentPurchase.value = {};
+ emit("close");
+ };
+ defineExpose({
+ openDialog,
+ });
+</script>
+
+<style scoped>
+ .node-user {
+ margin: 10px 0;
+ font-size: 16px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .node-status {
+ color: #1890ff;
+ margin-left: 8px;
+ font-size: 14px;
+ }
+ .node-reason {
+ font-size: 15px;
+ color: #333;
+ margin: 10px 0;
+ }
+ .user-avatar {
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ border-radius: 50px;
+ }
+ .signImg {
+ cursor: pointer;
+ width: 200px;
+ height: 60px;
+ }
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue b/src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
new file mode 100644
index 0000000..bacdebd
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -0,0 +1,352 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板瀹℃壒娴佺▼' : '缂栬緫瀹℃壒娴佺▼'"
+ width="50%"
+ @close="closeDia"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="娴佺▼缂栧彿锛�" prop="approveId">
+ <el-input v-model="form.approveId" placeholder="鑷姩缂栧彿" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鐢宠閮ㄩ棬锛�" prop="approveDeptName">
+<!-- <el-input v-model="form.approveDeptName" placeholder="璇疯緭鍏�" clearable/>-->
+ <el-select
+ v-model="form.approveDeptId"
+ placeholder="閫夋嫨閮ㄩ棬"
+ @change="handleDeptChange"
+ >
+ <el-option
+ v-for="user in productOptions"
+ :key="user.deptId"
+ :label="user.deptName"
+ :value="user.deptId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item :label="props.approveType == 5 ? '閲囪喘鍚堝悓鍙凤細' : '瀹℃壒浜嬬敱锛�'" prop="approveReason">
+ <el-input v-model="form.approveReason" placeholder="璇疯緭鍏�" clearable type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 璇峰亣鏃堕棿锛堜粎褰� approveType 涓� 2 鏃舵樉绀猴級 -->
+ <el-row :gutter="30" v-if="props.approveType == 2">
+ <el-col :span="12">
+ <el-form-item label="璇峰亣寮�濮嬫椂闂达細" prop="startDate">
+ <el-date-picker
+ v-model="form.startDate"
+ type="date"
+ placeholder="璇烽�夋嫨寮�濮嬫棩鏈�"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇峰亣缁撴潫鏃堕棿锛�" prop="endDate">
+ <el-date-picker
+ v-model="form.endDate"
+ type="date"
+ placeholder="璇烽�夋嫨缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 鎶ラ攢閲戦锛堜粎褰� approveType 涓� 4 鏃舵樉绀猴級 -->
+ <el-row v-if="props.approveType == 4">
+ <el-col :span="24">
+ <el-form-item label="鎶ラ攢閲戦锛�" prop="price">
+ <el-input-number
+ v-model="form.price"
+ placeholder="璇疯緭鍏ユ姤閿�閲戦"
+ :min="0"
+ :precision="2"
+ :step="0.01"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 鍑哄樊鏃堕棿锛堜粎褰� approveType 涓� 3 鏃舵樉绀猴級 -->
+ <el-row :gutter="30" v-if="props.approveType == 3">
+ <el-col :span="12">
+ <el-form-item label="鍑哄樊寮�濮嬫椂闂达細" prop="startDateTime">
+ <el-date-picker
+ v-model="form.startDateTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
+ value-format="YYYY-MM-DD HH:mm"
+ format="YYYY-MM-DD HH:mm"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍑哄樊缁撴潫鏃堕棿锛�" prop="endDateTime">
+ <el-date-picker
+ v-model="form.endDateTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
+ value-format="YYYY-MM-DD HH:mm"
+ format="YYYY-MM-DD HH:mm"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 鍑哄樊鍦扮偣锛堜粎褰� approveType 涓� 3 鏃舵樉绀猴級 -->
+ <el-row v-if="props.approveType == 3">
+ <el-col :span="24">
+ <el-form-item label="鍑哄樊鍦扮偣锛�" prop="location">
+ <el-input
+ v-model="form.location"
+ placeholder="璇疯緭鍏ュ嚭宸湴鐐�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢鏉愭枡锛�" prop="remark">
+ <FileUpload v-model:file-list="fileList" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, toRefs, getCurrentInstance} from "vue";
+import {
+ approveProcessAdd, approveProcessGetInfo,
+ approveProcessUpdate,
+ getDept
+} from "@/api/collaborativeApproval/approvalProcess.js";
+import {
+ delLedgerFile,
+} from "@/api/salesManagement/salesLedger.js";
+import { getToken } from "@/utils/auth";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+import useUserStore from "@/store/modules/user";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+const userStore = useUserStore();
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const fileList = ref([]);
+const upload = reactive({
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+});
+const data = reactive({
+ form: {
+ approveId: "",
+ approveDeptId: "",
+ approveDeptName: "",
+ approveReason: "",
+ checkResult: "",
+ startDate: "", // 璇峰亣寮�濮嬫椂闂�
+ endDate: "", // 璇峰亣缁撴潫鏃堕棿
+ price: null, // 鎶ラ攢閲戦
+ startDateTime: "", // 鍑哄樊寮�濮嬫椂闂�
+ endDateTime: "", // 鍑哄樊缁撴潫鏃堕棿
+ location: "", // 鍑哄樊鍦扮偣
+ storageBlobDTOS: []
+ },
+ rules: {
+ approveId: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ approveDeptName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ approveReason: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkResult: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ startDate: [{ required: true, message: "璇烽�夋嫨璇峰亣寮�濮嬫椂闂�", trigger: "change" }],
+ endDate: [{ required: true, message: "璇烽�夋嫨璇峰亣缁撴潫鏃堕棿", trigger: "change" }],
+ price: [{ required: true, message: "璇疯緭鍏ユ姤閿�閲戦", trigger: "blur" }],
+ startDateTime: [{ required: true, message: "璇烽�夋嫨鍑哄樊寮�濮嬫椂闂�", trigger: "change" }],
+ endDateTime: [{ required: true, message: "璇烽�夋嫨鍑哄樊缁撴潫鏃堕棿", trigger: "change" }],
+ location: [{ required: true, message: "璇疯緭鍏ュ嚭宸湴鐐�", trigger: "blur" }],
+ },
+});
+const { form, rules } = toRefs(data);
+const productOptions = ref([]);
+const currentApproveStatus = ref(0)
+const props = defineProps({
+ approveType: {
+ type: [Number, String],
+ default: 0
+ }
+})
+
+
+// 澶勭悊閮ㄩ棬閫夋嫨鍙樺寲
+const handleDeptChange = (deptId) => {
+ if (deptId) {
+ const selectedDept = productOptions.value.find(dept => dept.deptId === deptId);
+ if (selectedDept) {
+ form.value.approveDeptName = selectedDept.deptName;
+ }
+ } else {
+ form.value.approveDeptName = '';
+ }
+};
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ form.value = {}
+
+ // 鑾峰彇褰撳墠鐢ㄦ埛淇℃伅骞惰缃儴闂↖D
+ form.value.approveDeptId = userStore.currentDeptId
+
+ // 鍔犺浇閮ㄩ棬閫夐」锛屽苟鍦ㄥ姞杞藉畬鎴愬悗璁剧疆閮ㄩ棬鍚嶇О
+ getProductOptions();
+ if (operationType.value === 'edit') {
+ fileList.value = row.commonFileList
+ form.value.tempFileIds = fileList.value.map(file => file.id)
+ currentApproveStatus.value = row.approveStatus
+ approveProcessGetInfo({id: row.approveId,approveReason: '1'}).then(res => {
+ form.value = {...res.data}
+ fileList.value = res.data.storageBlobVOS
+ })
+ }
+}
+const getProductOptions = () => {
+ return getDept().then((res) => {
+ productOptions.value = res.data;
+ // 濡傛灉宸叉湁閮ㄩ棬ID锛岃嚜鍔ㄨ缃儴闂ㄥ悕绉帮紙鐢ㄤ簬楠岃瘉锛�
+ if (form.value.approveDeptId && productOptions.value.length > 0) {
+ const matchedDept = productOptions.value.find(dept =>
+ dept.deptId == form.value.approveDeptId ||
+ String(dept.deptId) === String(form.value.approveDeptId)
+ );
+ if (matchedDept) {
+ form.value.approveDeptName = matchedDept.deptName;
+ }
+ }
+ });
+};
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item;
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+
+ return newItem;
+ });
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ form.value.approveType = props.approveType
+ // 褰� approveType 涓� 2 鏃讹紝鏍¢獙璇峰亣鏃堕棿
+ if (props.approveType == 2) {
+ if (!form.value.startDate) {
+ proxy.$modal.msgError("璇烽�夋嫨璇峰亣寮�濮嬫椂闂达紒")
+ return
+ }
+ if (!form.value.endDate) {
+ proxy.$modal.msgError("璇烽�夋嫨璇峰亣缁撴潫鏃堕棿锛�")
+ return
+ }
+ // 鏍¢獙缁撴潫鏃堕棿涓嶈兘鏃╀簬寮�濮嬫椂闂�
+ if (new Date(form.value.endDate) < new Date(form.value.startDate)) {
+ proxy.$modal.msgError("璇峰亣缁撴潫鏃堕棿涓嶈兘鏃╀簬寮�濮嬫椂闂达紒")
+ return
+ }
+ }
+ // 褰� approveType 涓� 3 鏃讹紝鏍¢獙鍑哄樊鏃堕棿鍜屽湴鐐�
+ if (props.approveType == 3) {
+ if (!form.value.startDateTime) {
+ proxy.$modal.msgError("璇烽�夋嫨鍑哄樊寮�濮嬫椂闂达紒")
+ return
+ }
+ if (!form.value.endDateTime) {
+ proxy.$modal.msgError("璇烽�夋嫨鍑哄樊缁撴潫鏃堕棿锛�")
+ return
+ }
+ if (new Date(form.value.endDateTime) < new Date(form.value.startDateTime)) {
+ proxy.$modal.msgError("鍑哄樊缁撴潫鏃堕棿涓嶈兘鏃╀簬寮�濮嬫椂闂达紒")
+ return
+ }
+ if (!form.value.location || form.value.location.trim() === '') {
+ proxy.$modal.msgError("璇疯緭鍏ュ嚭宸湴鐐癸紒")
+ return
+ }
+ }
+ // 褰� approveType 涓� 4 鏃讹紝鏍¢獙鎶ラ攢閲戦
+ if (props.approveType == 4) {
+ if (!form.value.price || form.value.price <= 0) {
+ proxy.$modal.msgError("璇疯緭鍏ユ湁鏁堢殑鎶ラ攢閲戦锛�")
+ return
+ }
+ }
+ form.value.storageBlobDTOS = fileList.value
+
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ if (operationType.value === "add" || currentApproveStatus.value == 3) {
+ approveProcessAdd(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ } else {
+ approveProcessUpdate(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ fileList.value = []
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/approvalProcess/fileList.vue b/src/views/collaborativeApproval/approvalProcess/fileList.vue
new file mode 100644
index 0000000..1f1b671
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/fileList.vue
@@ -0,0 +1,66 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="闄勪欢" width="40%" :before-close="handleClose" draggable>
+ <el-table :data="tableData" border height="40vh">
+ <el-table-column label="闄勪欢鍚嶇О" prop="name" min-width="400" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="downLoadFile(scope.row)">涓嬭浇</el-button>
+ <el-button link type="primary" @click="lookFile(scope.row)">棰勮</el-button>
+ <el-button link type="danger" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import filePreview from '@/components/filePreview/index.vue'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { delCommonFile } from '@/api/publicApi/commonFile.js'
+
+const dialogVisible = ref(false)
+const tableData = ref([])
+const { proxy } = getCurrentInstance();
+const filePreviewRef = ref()
+const handleClose = () => {
+ dialogVisible.value = false
+}
+const open = (list) => {
+ dialogVisible.value = true
+ tableData.value = list
+}
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+// 鍒犻櫎闄勪欢
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎闄勪欢"${row.name}"鍚楋紵`, '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ delCommonFile([row.id]).then(() => {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ // 浠庡垪琛ㄤ腑绉婚櫎宸插垹闄ょ殑闄勪欢
+ const index = tableData.value.findIndex(item => item.id === row.id)
+ if (index !== -1) {
+ tableData.value.splice(index, 1)
+ }
+ }).catch(() => {
+ ElMessage.error('鍒犻櫎澶辫触')
+ })
+ }).catch(() => {
+ ElMessage.info('宸插彇娑堝垹闄�')
+ })
+}
+defineExpose({
+ open
+})
+</script>
+
+<style></style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/approvalProcess/index.vue b/src/views/collaborativeApproval/approvalProcess/index.vue
new file mode 100644
index 0000000..158916c
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/index.vue
@@ -0,0 +1,774 @@
+<template>
+ <div class="app-container">
+ <!-- 瀹℃壒绫诲瀷鍒囨崲 - 绱у噾鏍囩寮� -->
+ <div class="type-tabs">
+ <div
+ v-for="type in approveTypes"
+ :key="type.value"
+ class="type-tab"
+ :class="{ active: activeTab === type.value }"
+ @click="activeTab = type.value; handleTabChange()"
+ >
+ <el-icon :size="14" :style="{ color: activeTab === type.value ? type.color : '#909399' }">
+ <component :is="type.icon" />
+ </el-icon>
+ <span class="tab-name">{{ type.label }}</span>
+ </div>
+ </div>
+
+ <!-- 鎼滅储鍜屾搷浣滃尯鍩� -->
+ <el-card class="search-card" shadow="never">
+ <div class="search-content">
+ <div class="search-filters">
+ <div class="filter-item">
+ <span class="filter-label">娴佺▼缂栧彿</span>
+ <el-input
+ v-model="searchForm.approveId"
+ placeholder="璇疯緭鍏ユ祦绋嬬紪鍙�"
+ clearable
+ :prefix-icon="Search"
+ @keyup.enter="handleQuery"
+ class="search-input"
+ />
+ </div>
+ <div class="filter-item">
+ <span class="filter-label">瀹℃壒鐘舵��</span>
+ <el-select
+ v-model="searchForm.approveStatus"
+ clearable
+ @change="handleQuery"
+ placeholder="璇烽�夋嫨鐘舵��"
+ class="search-select"
+ >
+ <el-option label="寰呭鏍�" :value="0">
+ <el-tag size="small" type="warning">寰呭鏍�</el-tag>
+ </el-option>
+ <el-option label="瀹℃牳涓�" :value="1">
+ <el-tag size="small" type="primary">瀹℃牳涓�</el-tag>
+ </el-option>
+ <el-option label="瀹℃牳瀹屾垚" :value="2">
+ <el-tag size="small" type="success">瀹℃牳瀹屾垚</el-tag>
+ </el-option>
+ <el-option label="瀹℃牳鏈�氳繃" :value="3">
+ <el-tag size="small" type="danger">瀹℃牳鏈�氳繃</el-tag>
+ </el-option>
+ <el-option label="宸查噸鏂版彁浜�" :value="4">
+ <el-tag size="small" type="info">宸查噸鏂版彁浜�</el-tag>
+ </el-option>
+ </el-select>
+ </div>
+ <el-button type="primary" @click="handleQuery" class="search-btn">
+ <el-icon><Search /></el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery" class="reset-btn">
+ <el-icon><RefreshRight /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </div>
+ <div class="search-actions">
+ <el-button
+ type="primary"
+ @click="openForm('add')"
+ v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
+ class="action-btn primary"
+ >
+ <el-icon><Plus /></el-icon>
+ 鏂板
+ </el-button>
+ <el-button @click="handleOut" class="action-btn">
+ <el-icon><Download /></el-icon>
+ 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ plain
+ @click="handleDelete"
+ v-if="currentApproveType !== 5 && currentApproveType !== 6 && currentApproveType !== 7"
+ class="action-btn danger"
+ >
+ <el-icon><Delete /></el-icon>
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <el-card class="table-card" shadow="never" v-loading="tableLoading">
+ <template #header>
+ <div class="table-header">
+ <div class="table-title">
+ <div class="type-tag" :style="{ backgroundColor: currentTypeInfo.color }">
+ <el-icon color="#fff" :size="16"><component :is="currentTypeInfo.icon" /></el-icon>
+ </div>
+ <span>{{ currentTypeInfo.label }}鍒楄〃</span>
+ <el-tag type="info" size="small" effect="plain" class="count-tag">
+ 鍏� {{ page.total }} 鏉�
+ </el-tag>
+ </div>
+ </div>
+ </template>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumnCopy"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ class="custom-table"
+ ></PIMTable>
+ </el-card>
+
+ <!-- 寮圭獥缁勪欢 -->
+ <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia>
+ <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia>
+ <FileList v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ record-type="approve_process"
+ :record-id="recordId" />
+ </div>
+</template>
+
+<script setup>
+import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } from "@element-plus/icons-vue";
+import {onMounted, ref, computed, reactive, toRefs, nextTick, getCurrentInstance, defineAsyncComponent} from "vue";
+import {ElMessageBox} from "element-plus";
+import { useRoute } from 'vue-router';
+import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
+import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
+import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
+import useUserStore from "@/store/modules/user";
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+const userStore = useUserStore();
+const route = useRoute();
+
+// 褰撳墠閫変腑鐨勬爣绛鹃〉锛岄粯璁や负鍏嚭绠$悊
+const activeTab = ref('1');
+
+// 鍚勭被鍨嬫暟閲忕粺璁�
+const typeCounts = ref({});
+
+// 瀹℃壒绫诲瀷閰嶇疆
+const approveTypes = [
+ { value: '1', label: '鍏嚭绠$悊', icon: 'Suitcase', color: '#409EFF' },
+ { value: '2', label: '璇峰亣绠$悊', icon: 'Calendar', color: '#67C23A' },
+ { value: '3', label: '鍑哄樊绠$悊', icon: 'Location', color: '#E6A23C' },
+ { value: '4', label: '鎶ラ攢绠$悊', icon: 'Money', color: '#F56C6C' },
+ { value: '5', label: '閲囪喘瀹℃壒', icon: 'ShoppingCart', color: '#909399' },
+ { value: '6', label: '鎶ヤ环瀹℃壒', icon: 'DocumentChecked', color: '#9B59B6' },
+ { value: '7', label: '鍙戣揣瀹℃壒', icon: 'Van', color: '#1ABC9C' },
+];
+
+// 褰撳墠瀹℃壒绫诲瀷淇℃伅
+const currentTypeInfo = computed(() => {
+ return approveTypes.find(t => t.value === activeTab.value) || approveTypes[0];
+});
+
+// 鑾峰彇绫诲瀷鏁伴噺
+const getTypeCount = (value) => {
+ return typeCounts.value[value] || 0;
+};
+
+// 褰撳墠瀹℃壒绫诲瀷锛屾牴鎹�変腑鐨勬爣绛鹃〉璁$畻
+const currentApproveType = computed(() => {
+ return Number(activeTab.value);
+});
+
+// 鏍囩椤靛垏鎹㈠鐞�
+const handleTabChange = () => {
+ // 鍒囨崲鏍囩椤垫椂閲嶇疆鎼滅储鏉′欢鍜屽垎椤碉紝骞堕噸鏂板姞杞芥暟鎹�
+ searchForm.value.approveId = '';
+ searchForm.value.approveStatus = '';
+ page.current = 1;
+ getList();
+};
+
+
+const data = reactive({
+ searchForm: {
+ approveId: "",
+ approveStatus: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+// 閲嶇疆鎼滅储
+const resetQuery = () => {
+ searchForm.value.approveId = '';
+ searchForm.value.approveStatus = '';
+ handleQuery();
+};
+
+// 鍔ㄦ�佽〃鏍煎垪閰嶇疆锛屾牴鎹鎵圭被鍨嬬敓鎴愬垪
+const tableColumnCopy = computed(() => {
+ const isLeaveType = currentApproveType.value === 2; // 璇峰亣绠$悊
+ const isBusinessTripType = currentApproveType.value === 3; // 鍑哄樊绠$悊
+ const isReimburseType = currentApproveType.value === 4; // 鎶ラ攢绠$悊
+ const isQuotationType = currentApproveType.value === 6; // 鎶ヤ环瀹℃壒
+ const isPurchaseType = currentApproveType.value === 5; // 閲囪喘瀹℃壒
+
+ // 鍩虹鍒楅厤缃�
+ const baseColumns = [
+ {
+ label: "瀹℃壒鐘舵��",
+ prop: "approveStatus",
+ dataType: "tag",
+ width: 100,
+ formatData: (params) => {
+ if (params == 0) {
+ return "寰呭鏍�";
+ } else if (params == 1) {
+ return "瀹℃牳涓�";
+ } else if (params == 2) {
+ return "瀹℃牳瀹屾垚";
+ } else if (params == 4) {
+ return "宸查噸鏂版彁浜�";
+ } else {
+ return '涓嶉�氳繃';
+ }
+ },
+ formatType: (params) => {
+ if (params == 0) {
+ return "warning";
+ } else if (params == 1) {
+ return "primary";
+ } else if (params == 2) {
+ return "success";
+ } else if (params == 4) {
+ return "info";
+ } else {
+ return 'danger';
+ }
+ },
+ },
+ {
+ label: "娴佺▼缂栧彿",
+ prop: "approveId",
+ width: 170
+ },
+ {
+ label: "鐢宠閮ㄩ棬",
+ prop: "approveDeptName",
+ width: 220
+ },
+ {
+ label: isQuotationType ? "鎶ヤ环鍗曞彿" : isPurchaseType ? "閲囪喘鍚堝悓鍙�" : "瀹℃壒浜嬬敱",
+ prop: "approveReason",
+ },
+ {
+ label: "鐢宠浜�",
+ prop: "approveUserName",
+ width: 120
+ }
+ ];
+
+ // 閲戦鍒楋紙浠呮姤閿�绠$悊鏄剧ず锛�
+ if (isReimburseType) {
+ baseColumns.push({
+ label: "閲戦锛堝厓锛�",
+ prop: "price",
+ width: 120
+ });
+ }
+
+ // 璇峰亣绠$悊锛氬紑濮嬫棩鏈� / 缁撴潫鏃ユ湡
+ if (isLeaveType) {
+ baseColumns.push(
+ { label: "寮�濮嬫棩鏈�", prop: "startDate", width: 120 },
+ { label: "缁撴潫鏃ユ湡", prop: "endDate", width: 120 }
+ );
+ }
+
+ // 鍑哄樊绠$悊锛氬紑濮嬫椂闂� / 缁撴潫鏃堕棿锛堜笉鍚锛�
+ if (isBusinessTripType) {
+ baseColumns.push(
+ {
+ label: "寮�濮嬫椂闂�",
+ prop: "startDateTime",
+ width: 180,
+ formatData: (val) => val ? val.substring(0, 16) : ''
+ },
+ {
+ label: "缁撴潫鏃堕棿",
+ prop: "endDateTime",
+ width: 180,
+ formatData: (val) => val ? val.substring(0, 16) : ''
+ }
+ );
+ }
+
+ // 褰撳墠瀹℃壒浜哄垪
+ baseColumns.push({
+ label: "褰撳墠瀹℃壒浜�",
+ prop: "approveUserCurrentName",
+ width: 120
+ });
+
+ // 鐢宠鏃堕棿 - 鎵�鏈夌被鍨嬮兘鏄剧ず
+ baseColumns.push({
+ label: "鐢宠鏃堕棿",
+ prop: "approveTime",
+ width: 180,
+ });
+
+ // 瀹℃壒鏃堕棿 - 鎵�鏈夌被鍨嬮兘鏄剧ず
+ baseColumns.push({
+ label: "瀹℃壒鏃堕棿",
+ prop: "approveOverTime",
+ width: 180,
+ });
+
+ // 鎿嶄綔鍒�
+ const actionOperations = [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ disabled: (row) =>
+ currentApproveType.value === 5 ||
+ currentApproveType.value === 6 ||
+ currentApproveType.value === 7 ||
+ row.approveStatus == 2 ||
+ row.approveStatus == 1 ||
+ row.approveStatus == 4
+ },
+ {
+ name: "瀹℃牳",
+ type: "text",
+ clickFun: (row) => {
+ openApprovalDia("approval", row);
+ },
+ disabled: (row) =>
+ row.approveUserCurrentId == null ||
+ row.approveStatus == 2 ||
+ row.approveStatus == 3 ||
+ row.approveStatus == 4 ||
+ row.approveUserCurrentId !== userStore.id
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => {
+ openApprovalDia("view", row);
+ },
+ },
+ ];
+
+ // 鎶ヤ环瀹℃壒锛堢被鍨� 6锛変笉灞曠ず"闄勪欢"鎿嶄綔
+ if (!isQuotationType) {
+ actionOperations.push({
+ name: "闄勪欢",
+ type: "text",
+ clickFun: (row) => {
+ openFilesFormDia(row);
+ },
+ });
+ }
+
+ baseColumns.push({
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 230,
+ operation: actionOperations,
+ });
+
+ return baseColumns;
+});
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0
+});
+const infoFormDia = ref()
+const approvalDia = ref()
+const { proxy } = getCurrentInstance()
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+// 鎵撳紑闄勪欢寮圭獥
+const recordId =ref(0)
+const fileDialogVisible = ref(false)
+
+// 鎵撳紑闄勪欢寮规
+const openFilesFormDia = async (row) => {
+ recordId.value = row.id
+ fileDialogVisible.value = true
+}
+
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ approveProcessListPage({...page, ...searchForm.value, approveType: currentApproveType.value}).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ // 鏇存柊褰撳墠绫诲瀷鏁伴噺
+ typeCounts.value[activeTab.value] = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 瀵煎嚭
+const handleOut = () => {
+ const type = currentApproveType.value
+ const urlMap = {
+ 0: "/approveProcess/exportZero",
+ 1: "/approveProcess/exportOne",
+ 2: "/approveProcess/exportTwo",
+ 3: "/approveProcess/exportThree",
+ 4: "/approveProcess/exportFour",
+ 5: "/approveProcess/exportFive",
+ 6: "/approveProcess/exportSix",
+ 7: "/approveProcess/exportSeven",
+ }
+ const url = urlMap[type] || urlMap[0]
+ const nameMap = {
+ 0: "鍗忓悓瀹℃壒绠$悊琛�",
+ 1: "鍏嚭绠$悊瀹℃壒琛�",
+ 2: "璇峰亣绠$悊瀹℃壒琛�",
+ 3: "鍑哄樊绠$悊瀹℃壒琛�",
+ 4: "鎶ラ攢绠$悊瀹℃壒琛�",
+ 5: "閲囪喘鐢宠瀹℃壒琛�",
+ 6: "鎶ヤ环瀹℃壒琛�",
+ 7: "鍙戣揣瀹℃壒琛�",
+ }
+ const fileName = nameMap[type] || nameMap[0]
+ proxy.download(url, {}, `${fileName}.xlsx`)
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑鏂板銆佺紪杈戝脊妗�
+const openForm = (type, row) => {
+ nextTick(() => {
+ infoFormDia.value?.openDialog(type, row)
+ })
+};
+// 鎵撳紑鏂板妫�楠屽脊妗�
+const openApprovalDia = (type, row) => {
+ nextTick(() => {
+ approvalDia.value?.openDialog(type, row)
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.approveId);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ approveProcessDelete(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+onMounted(() => {
+ // 鏍规嵁URL鍙傛暟璁剧疆鏍囩椤靛拰鏌ヨ鏉′欢
+ const approveType = route.query.approveType;
+ const approveId = route.query.approveId;
+
+ if (approveType) {
+ // 璁剧疆鏍囩椤碉紙approveType 瀵瑰簲 activeTab 鐨� name锛�
+ activeTab.value = String(approveType);
+ }
+
+ if (approveId) {
+ // 璁剧疆娴佺▼缂栧彿鏌ヨ鏉′欢
+ searchForm.value.approveId = String(approveId);
+ }
+
+ // 鏌ヨ鍒楄〃
+ getList();
+});
+</script>
+
+<style scoped>
+.page-header {
+ margin-bottom: 20px;
+}
+
+.header-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.title-icon {
+ font-size: 28px;
+ color: var(--el-color-primary, #409EFF);
+}
+
+.header-text {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.main-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--el-text-color-primary, #303133);
+}
+
+.sub-title {
+ font-size: 13px;
+ color: var(--el-text-color-secondary, #909399);
+}
+
+/* 瀹℃壒绫诲瀷鍒囨崲 - 绱у噾鏍囩寮� */
+.type-tabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 16px;
+ padding: 4px;
+ background: var(--el-fill-color-light, #f5f7fa);
+ border-radius: 8px;
+ overflow-x: auto;
+}
+
+.type-tab {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 14px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ white-space: nowrap;
+ font-size: 13px;
+ color: var(--el-text-color-regular, #606266);
+}
+
+.type-tab:hover {
+ background: var(--el-color-primary-light-9, rgba(64, 158, 255, 0.1));
+ color: var(--el-color-primary, #409EFF);
+}
+
+.type-tab.active {
+ background: var(--el-bg-color, #fff);
+ color: var(--el-color-primary, #409EFF);
+ font-weight: 600;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.tab-name {
+ font-size: 13px;
+}
+
+.tab-count {
+ min-width: 16px;
+ height: 16px;
+ padding: 0 5px;
+ background: var(--el-color-success, #67C23A);
+ color: #fff;
+ border-radius: 8px;
+ font-size: 11px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 鎼滅储鍗$墖 */
+.search-card {
+ margin-bottom: 16px;
+ border-radius: 12px;
+}
+
+:deep(.el-card__body) {
+ padding: 20px;
+}
+
+.search-content {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 16px;
+}
+
+.search-filters {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.filter-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.filter-label {
+ font-size: 14px;
+ color: var(--el-text-color-regular, #606266);
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.search-input,
+.search-select {
+ width: 200px;
+}
+
+.search-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.reset-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.search-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.action-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.action-btn.primary {
+ background: var(--el-color-primary, #409EFF);
+ border: none;
+}
+
+.action-btn.danger {
+ transition: all 0.3s;
+}
+
+.action-btn.danger:hover {
+ background: #f56c6c;
+ color: #fff;
+}
+
+/* 琛ㄦ牸鍗$墖 */
+.table-card {
+ border-radius: 12px;
+}
+
+:deep(.el-card__header) {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
+}
+
+.table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.table-title {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--el-text-color-primary, #303133);
+}
+
+.type-tag {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.count-tag {
+ margin-left: 8px;
+}
+
+.custom-table {
+ margin-top: 8px;
+}
+
+/* 鍝嶅簲寮� */
+@media (max-width: 1200px) {
+ .search-content {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .search-filters {
+ justify-content: flex-start;
+ }
+
+ .search-actions {
+ justify-content: flex-end;
+ }
+}
+
+@media (max-width: 768px) {
+ .type-tabs {
+ padding: 3px;
+ }
+
+ .type-tab {
+ padding: 6px 10px;
+ font-size: 12px;
+ }
+
+ .tab-name {
+ font-size: 12px;
+ }
+
+ .search-filters {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .filter-item {
+ width: 100%;
+ }
+
+ .search-input,
+ .search-select {
+ width: 100%;
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/approvalProcess/index1.vue b/src/views/collaborativeApproval/approvalProcess/index1.vue
new file mode 100644
index 0000000..c46c68a
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/index1.vue
@@ -0,0 +1,22 @@
+<template>
+ <div class="container">
+ <!-- 寮曞叆index.vue缁勪欢骞朵紶閫掑弬鏁� -->
+ <ApprovalProcessIndex :approveType="1" />
+ </div>
+</template>
+
+<script setup>
+import ApprovalProcessIndex from './index.vue'
+
+// 瀹氫箟缁勪欢鍚嶇О
+defineOptions({
+ name: 'ApprovalProcessIndex1'
+})
+</script>
+
+<style scoped>
+.container {
+ width: 100%;
+ height: 100%;
+}
+</style>
diff --git a/src/views/collaborativeApproval/approvalProcess/index2.vue b/src/views/collaborativeApproval/approvalProcess/index2.vue
new file mode 100644
index 0000000..7c15c3e
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/index2.vue
@@ -0,0 +1,22 @@
+<template>
+ <div class="container">
+ <!-- 寮曞叆index.vue缁勪欢骞朵紶閫掑弬鏁� -->
+ <ApprovalProcessIndex :approveType="2" />
+ </div>
+</template>
+
+<script setup>
+import ApprovalProcessIndex from './index.vue'
+
+// 瀹氫箟缁勪欢鍚嶇О
+defineOptions({
+ name: 'ApprovalProcessIndex1'
+})
+</script>
+
+<style scoped>
+.container {
+ width: 100%;
+ height: 100%;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/approvalProcess/index3.vue b/src/views/collaborativeApproval/approvalProcess/index3.vue
new file mode 100644
index 0000000..3afb6f5
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/index3.vue
@@ -0,0 +1,22 @@
+<template>
+ <div class="container">
+ <!-- 寮曞叆index.vue缁勪欢骞朵紶閫掑弬鏁� -->
+ <ApprovalProcessIndex :approveType="3" />
+ </div>
+</template>
+
+<script setup>
+import ApprovalProcessIndex from './index.vue'
+
+// 瀹氫箟缁勪欢鍚嶇О
+defineOptions({
+ name: 'ApprovalProcessIndex1'
+})
+</script>
+
+<style scoped>
+.container {
+ width: 100%;
+ height: 100%;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/approvalProcess/index4.vue b/src/views/collaborativeApproval/approvalProcess/index4.vue
new file mode 100644
index 0000000..77236af
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/index4.vue
@@ -0,0 +1,22 @@
+<template>
+ <div class="container">
+ <!-- 寮曞叆index.vue缁勪欢骞朵紶閫掑弬鏁� -->
+ <ApprovalProcessIndex :approveType="4" />
+ </div>
+</template>
+
+<script setup>
+import ApprovalProcessIndex from './index.vue'
+
+// 瀹氫箟缁勪欢鍚嶇О
+defineOptions({
+ name: 'ApprovalProcessIndex1'
+})
+</script>
+
+<style scoped>
+.container {
+ width: 100%;
+ height: 100%;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/approvalProcess/index5.vue b/src/views/collaborativeApproval/approvalProcess/index5.vue
new file mode 100644
index 0000000..2e0f4f3
--- /dev/null
+++ b/src/views/collaborativeApproval/approvalProcess/index5.vue
@@ -0,0 +1,22 @@
+<template>
+ <div class="container">
+ <!-- 寮曞叆index.vue缁勪欢骞朵紶閫掑弬鏁� -->
+ <ApprovalProcessIndex :approveType="5" />
+ </div>
+</template>
+
+<script setup>
+import ApprovalProcessIndex from './index.vue'
+
+// 瀹氫箟缁勪欢鍚嶇О
+defineOptions({
+ name: 'ApprovalProcessIndex1'
+})
+</script>
+
+<style scoped>
+.container {
+ width: 100%;
+ height: 100%;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/attendanceManagement/index.vue b/src/views/collaborativeApproval/attendanceManagement/index.vue
new file mode 100644
index 0000000..1f5f956
--- /dev/null
+++ b/src/views/collaborativeApproval/attendanceManagement/index.vue
@@ -0,0 +1,1244 @@
+<template>
+ <div class="app-container">
+ <el-tabs v-model="activeTab" type="border-card">
+ <!-- 鍋囨湡璁剧疆 -->
+ <el-tab-pane label="鍋囨湡璁剧疆" name="holiday">
+ <div class="tab-content">
+ <el-button type="primary" @click="openDialog('holiday', 'add')">鏂板鍋囨湡</el-button>
+
+ <el-table :data="holidayData" border style="width: 100%; margin-top: 20px;">
+ <el-table-column prop="name" label="鍋囨湡鍚嶇О" />
+ <el-table-column prop="type" label="鍋囨湡绫诲瀷">
+ <template #default="scope">
+ <el-tag :type="getTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="startDate" label="寮�濮嬫棩鏈�" />
+ <el-table-column prop="endDate" label="缁撴潫鏃ユ湡" />
+ <el-table-column prop="days" label="澶╂暟" align="center" />
+ <el-table-column prop="status" label="鐘舵��" >
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
+ {{ scope.row.status === 'active' ? '鍚敤' : '鍋滅敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" size="small" @click="openDialog('holiday', 'edit', scope.row)">缂栬緫</el-button>
+ <el-button type="danger" size="small" @click="deleteItem('holiday', scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-tab-pane>
+
+ <!-- 骞村亣璁剧疆 -->
+ <el-tab-pane label="骞村亣璁剧疆" name="annual">
+ <div class="tab-content">
+ <el-button type="primary" @click="openDialog('annual', 'add')">鏂板骞村亣瑙勫垯</el-button>
+
+ <el-table :data="annualData" border style="width: 100%; margin-top: 20px;">
+ <el-table-column prop="employeeType" label="鍛樺伐绫诲瀷">
+ <template #default="scope">
+ <el-tag :type="getTagType(scope.row.employeeType)">{{ getTypeLabel(scope.row.employeeType) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="workYears" label="宸ヤ綔骞撮檺" />
+ <el-table-column prop="annualDays" label="骞村亣澶╂暟" align="center" />
+ <el-table-column prop="maxCarryOver" label="鏈�澶х粨杞ぉ鏁�" align="center" />
+ <el-table-column prop="status" label="鐘舵��">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
+ {{ scope.row.status === 'active' ? '鍚敤' : '鍋滅敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" size="small" @click="openDialog('annual', 'edit', scope.row)">缂栬緫</el-button>
+ <el-button type="danger" size="small" @click="deleteItem('annual', scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-tab-pane>
+
+ <!-- 鍔犵彮璁剧疆 -->
+ <el-tab-pane label="鍔犵彮璁剧疆" name="overtime">
+ <div class="tab-content">
+ <el-button type="primary" @click="openDialog('overtime', 'add')">鏂板鍔犵彮瑙勫垯</el-button>
+
+ <el-table :data="overtimeData" border style="width: 100%; margin-top: 20px;">
+ <el-table-column prop="name" label="瑙勫垯鍚嶇О" />
+ <el-table-column prop="type" label="鍔犵彮绫诲瀷" >
+ <template #default="scope">
+ <el-tag :type="getTagType(scope.row.type)">{{ getTypeLabel(scope.row.type) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="startTime" label="寮�濮嬫椂闂�" />
+ <el-table-column prop="endTime" label="缁撴潫鏃堕棿" />
+ <el-table-column prop="rate" label="鍊嶇巼" align="center" />
+ <el-table-column prop="status" label="鐘舵��" >
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
+ {{ scope.row.status === 'active' ? '鍚敤' : '鍋滅敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" size="small" @click="openDialog('overtime', 'edit', scope.row)">缂栬緫</el-button>
+ <el-button type="danger" size="small" @click="deleteItem('overtime', scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-tab-pane>
+
+ <!-- 涓婄彮鏃堕棿璁剧疆 -->
+ <el-tab-pane label="涓婄彮鏃堕棿璁剧疆" name="worktime">
+ <div class="tab-content">
+ <el-button type="primary" @click="openDialog('worktime', 'add')">鏂板鏃堕棿娈�</el-button>
+
+ <el-table :data="worktimeData" border style="width: 100%; margin-top: 20px;">
+ <el-table-column prop="name" label="鏃堕棿娈靛悕绉�" />
+ <el-table-column prop="startTime" label="涓婄彮鏃堕棿"/>
+ <el-table-column prop="endTime" label="涓嬬彮鏃堕棿" />
+ <el-table-column prop="flexibleStart" label="寮规�т笂鐝�">
+ <template #default="scope">
+ <el-tag :type="scope.row.flexibleStart === 'true' ? 'success' : 'info'">
+ {{ scope.row.flexibleStart === 'true' ? '鏄�' : '鍚�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="flexibleMinutes" label="寮规�ф椂闂�(鍒嗛挓)" width="120" align="center" />
+ <el-table-column prop="status" label="鐘舵��" >
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
+ {{ scope.row.status === 'active' ? '鍚敤' : '鍋滅敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" size="small" @click="openDialog('worktime', 'edit', scope.row)">缂栬緫</el-button>
+ <el-button type="danger" size="small" @click="deleteItem('worktime', scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-tab-pane>
+
+ <!-- 鎵撳崱璁板綍 -->
+ <el-tab-pane label="鎵撳崱璁板綍" name="attendance">
+ <div class="tab-content">
+ <div style="margin-bottom: 20px;">
+ <el-date-picker
+ v-model="attendanceDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="margin-right: 10px;"
+ @change="filterAttendanceData"
+ />
+ <el-select
+ v-model="attendanceStatus"
+ placeholder="閫夋嫨鐘舵��"
+ style="width: 120px; margin-right: 10px;"
+ @change="filterAttendanceData"
+ >
+ <el-option label="鍏ㄩ儴" value="" />
+ <el-option label="姝e父" value="normal" />
+ <el-option label="杩熷埌" value="late" />
+ <el-option label="鏃╅��" value="early" />
+ <el-option label="缂哄嫟" value="absent" />
+ </el-select>
+ <el-button type="primary" @click="exportAttendance">瀵煎嚭璁板綍</el-button>
+ </div>
+
+ <el-table :data="filteredAttendanceData" border style="width: 100%;">
+ <el-table-column prop="employeeName" label="鍛樺伐濮撳悕" width="120" />
+ <el-table-column prop="department" label="閮ㄩ棬" width="120" />
+ <el-table-column prop="date" label="鏃ユ湡" width="120" />
+ <el-table-column prop="clockInTime" label="涓婄彮鎵撳崱" width="120" />
+ <el-table-column prop="clockOutTime" label="涓嬬彮鎵撳崱" width="120" />
+ <el-table-column prop="workHours" label="宸ヤ綔鏃堕暱" width="100" align="center" />
+ <el-table-column prop="status" label="鐘舵��" width="100" align="center">
+ <template #default="scope">
+ <el-tag :type="getAttendanceTagType(scope.row.status)">{{ getAttendanceStatusLabel(scope.row.status) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="location" label="鎵撳崱鍦扮偣" width="150" />
+ <el-table-column prop="remark" label="澶囨敞" min-width="150" />
+ </el-table>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+
+ <!-- 閫氱敤寮圭獥 -->
+ <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+ <el-form-item label="鍚嶇О" prop="name" v-if="currentType !== 'annual'">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+
+ <el-form-item label="绫诲瀷" prop="type" v-if="currentType === 'holiday' || currentType === 'overtime'">
+ <el-select v-model="form.type" placeholder="璇烽�夋嫨绫诲瀷" style="width: 100%">
+ <el-option
+ v-for="option in getTypeOptions()"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="鍛樺伐绫诲瀷" prop="employeeType" v-if="currentType === 'annual'">
+ <el-select v-model="form.employeeType" placeholder="璇烽�夋嫨鍛樺伐绫诲瀷" style="width: 100%">
+ <!-- <el-option label="姝e紡鍛樺伐" value="regular" />
+ <el-option label="璇曠敤鏈熷憳宸�" value="probation" />
+ <el-option label="瀹炰範鐢�" value="intern" /> -->
+ <el-option
+ v-for="option in getTypeOptions()"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="宸ヤ綔骞撮檺" prop="workYears" v-if="currentType === 'annual'">
+ <el-input v-model="form.workYears" placeholder="濡傦細1-3骞淬��3-5骞寸瓑" />
+ </el-form-item>
+
+ <el-form-item label="骞村亣澶╂暟" prop="annualDays" v-if="currentType === 'annual'">
+ <el-input-number v-model="form.annualDays" :min="0" :max="365" style="width: 100%" />
+ </el-form-item>
+
+ <el-form-item label="鏈�澶х粨杞ぉ鏁�" prop="maxCarryOver" v-if="currentType === 'annual'">
+ <el-input-number v-model="form.maxCarryOver" :min="0" :max="30" style="width: 100%" />
+ </el-form-item>
+
+ <el-form-item label="鏃ユ湡鑼冨洿" prop="dateRange" v-if="currentType === 'holiday'">
+ <el-date-picker
+ v-model="form.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ style="width: 100%"
+ @change="calculateDays"
+ />
+ </el-form-item>
+
+ <el-form-item label="澶╂暟" prop="days" v-if="currentType === 'holiday'">
+ <el-input-number v-model="form.days" :min="0" style="width: 100%" />
+ </el-form-item>
+
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime" v-if="currentType === 'overtime'">
+ <el-time-picker
+ v-model="form.startTime"
+ placeholder="寮�濮嬫椂闂�"
+ format="HH:mm"
+ value-format="HH:mm"
+ style="width: 100%"
+ @change="validateTimeField('startTime')"
+ />
+ </el-form-item>
+
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime" v-if="currentType === 'overtime'">
+ <el-time-picker
+ v-model="form.endTime"
+ placeholder="缁撴潫鏃堕棿"
+ format="HH:mm"
+ value-format="HH:mm"
+ style="width: 100%"
+ @change="validateTimeField('endTime')"
+ />
+ </el-form-item>
+
+ <el-form-item label="鍊嶇巼" prop="rate" v-if="currentType === 'overtime'">
+ <el-input-number v-model="form.rate" :min="1" :max="3" :step="0.5" style="width: 100%" />
+ </el-form-item>
+
+ <el-form-item label="涓婄彮鏃堕棿" prop="workStartTime" v-if="currentType === 'worktime'">
+ <el-time-picker
+ v-model="form.workStartTime"
+ placeholder="涓婄彮鏃堕棿"
+ format="HH:mm"
+ value-format="HH:mm"
+ style="width: 100%"
+ @change="validateTimeField('workStartTime')"
+ />
+ </el-form-item>
+
+ <el-form-item label="涓嬬彮鏃堕棿" prop="workEndTime" v-if="currentType === 'worktime'">
+ <el-time-picker
+ v-model="form.workEndTime"
+ placeholder="涓嬬彮鏃堕棿"
+ format="HH:mm"
+ value-format="HH:mm"
+ style="width: 100%"
+ @change="validateTimeField('workEndTime')"
+ />
+ </el-form-item>
+
+ <el-form-item label="寮规�т笂鐝�" prop="flexibleStart" v-if="currentType === 'worktime'">
+ <el-switch v-model="form.flexibleStart" />
+ </el-form-item>
+
+ <el-form-item label="寮规�ф椂闂�(鍒嗛挓)" prop="flexibleMinutes" v-if="currentType === 'worktime' && form.flexibleStart">
+ <el-input-number v-model="form.flexibleMinutes" :min="0" :max="120" style="width: 100%" />
+ </el-form-item>
+
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio value="active">鍚敤</el-radio>
+ <el-radio value="inactive">鍋滅敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { listHolidaySettings, addHolidaySettings, updateHolidaySettings, delHolidaySettings, listAnnualLeaveSettingList, addAnnualLeaveSetting, updateAnnualLeaveSetting, delAnnualLeaveSetting, listOvertimeSettingList, addOvertimeSetting, updateOvertimeSetting, delOvertimeSetting, listWorkingHoursSettingList, addWorkingHoursSetting, updateWorkingHoursSetting, delWorkingHoursSetting } from '@/api/collaborativeApproval/attendanceManagement.js'
+
+// 褰撳墠婵�娲荤殑鏍囩椤�
+const activeTab = ref('holiday')
+
+// 寮圭獥鐩稿叧
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const currentType = ref('')
+const currentAction = ref('')
+const currentEditId = ref('')
+const formRef = ref()
+const page = {
+ current: 1,
+ size: 20,
+ total: 0,
+ }
+const holidayData = ref([])
+const annualData = ref([])
+const overtimeData = ref([])
+const worktimeData = ref([])
+
+// 鎵撳崱璁板綍鐩稿叧鏁版嵁
+const attendanceData = ref([])
+const filteredAttendanceData = ref([])
+const attendanceDate = ref('')
+const attendanceStatus = ref('')
+
+// 琛ㄥ崟鏁版嵁
+const form = reactive({
+ name: '',
+ type: '',
+ dateRange: [],
+ startDate: '',
+ endDate: '',
+ days: 0,
+ employeeType: '',
+ workYears: '',
+ annualDays: 0,
+ maxCarryOver: 0,
+ startTime: '', // 鍔犵彮寮�濮嬫椂闂�
+ endTime: '', // 鍔犵彮缁撴潫鏃堕棿
+ workStartTime: '', // 涓婄彮鏃堕棿
+ workEndTime: '', // 涓嬬彮鏃堕棿
+ rate: 1.5,
+ flexibleStart: false,
+ flexibleMinutes: 30,
+ status: 'active'
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }],
+ type: [{ required: true, message: '璇烽�夋嫨绫诲瀷', trigger: 'change' }],
+ dateRange: [{ required: true, message: '璇烽�夋嫨鏃ユ湡鑼冨洿', trigger: 'change' }],
+ days: [{ required: true, message: '璇疯緭鍏ュぉ鏁�', trigger: 'blur' }],
+ employeeType: [{ required: true, message: '璇烽�夋嫨鍛樺伐绫诲瀷', trigger: 'change' }],
+ workYears: [{ required: true, message: '璇疯緭鍏ュ伐浣滃勾闄�', trigger: 'blur' }],
+ annualDays: [{ required: true, message: '璇疯緭鍏ュ勾鍋囧ぉ鏁�', trigger: 'blur' }],
+ maxCarryOver: [{ required: true, message: '璇疯緭鍏ユ渶澶х粨杞ぉ鏁�', trigger: 'blur' }],
+ startTime: [{
+ required: true,
+ message: '璇烽�夋嫨寮�濮嬫椂闂�',
+ trigger: 'change',
+ validator: (rule, value, callback) => {
+ if (!value) {
+ callback(new Error('璇烽�夋嫨寮�濮嬫椂闂�'))
+ } else {
+ callback()
+ }
+ }
+ }],
+ endTime: [{
+ required: true,
+ message: '璇烽�夋嫨缁撴潫鏃堕棿',
+ trigger: 'change',
+ validator: (rule, value, callback) => {
+ if (!value) {
+ callback(new Error('璇烽�夋嫨缁撴潫鏃堕棿'))
+ } else {
+ callback()
+ }
+ }
+ }],
+ workStartTime: [{
+ required: true,
+ message: '璇烽�夋嫨涓婄彮鏃堕棿',
+ trigger: 'change',
+ validator: (rule, value, callback) => {
+ if (!value) {
+ callback(new Error('璇烽�夋嫨涓婄彮鏃堕棿'))
+ } else {
+ callback()
+ }
+ }
+ }],
+ workEndTime: [{
+ required: true,
+ message: '璇烽�夋嫨涓嬬彮鏃堕棿',
+ trigger: 'change',
+ validator: (rule, value, callback) => {
+ if (!value) {
+ callback(new Error('璇烽�夋嫨涓嬬彮鏃堕棿'))
+ } else {
+ callback()
+ }
+ }
+ }],
+ rate: [{ required: true, message: '璇疯緭鍏ュ�嶇巼', trigger: 'blur' }]
+}
+// 宸ュ叿鍑芥暟
+const getTagType = (type) => {
+ const tagMap = {
+ legal: 'success', adjustment: 'warning', special: 'info', company: 'primary',
+ weekday: 'primary', weekend: 'warning', holiday: 'danger', night: 'info',
+ regular: 'success', probation: 'info', intern: 'danger'
+ }
+ return tagMap[type] || 'info'
+}
+
+const getTypeLabel = (type) => {
+ const labelMap = {
+ legal: '娉曞畾鑺傚亣鏃�', adjustment: '璋冧紤鏃�', special: '鐗规畩鍋囨湡', company: '鍏徃鍋囨湡',
+ weekday: '宸ヤ綔鏃ュ姞鐝�', weekend: '鍛ㄦ湯鍔犵彮', holiday: '鑺傚亣鏃ュ姞鐝�', night: '娣卞鍔犵彮',
+ regular: '姝e紡鍛樺伐', probation: '璇曠敤鏈熷憳宸�', intern: '瀹炰範鐢�'
+ }
+ return labelMap[type] || type
+}
+
+// 鎵撳崱璁板綍鐩稿叧宸ュ叿鍑芥暟
+const getAttendanceTagType = (status) => {
+ const tagMap = {
+ normal: 'success',
+ late: 'warning',
+ early: 'warning',
+ absent: 'danger'
+ }
+ return tagMap[status] || 'info'
+}
+
+const getAttendanceStatusLabel = (status) => {
+ const labelMap = {
+ normal: '姝e父',
+ late: '杩熷埌',
+ early: '鏃╅��',
+ absent: '缂哄嫟'
+ }
+ return labelMap[status] || status
+}
+
+const getTypeOptions = () => {
+ if (currentType.value === 'holiday') {
+ return [
+ { label: '娉曞畾鑺傚亣鏃�', value: 'legal' },
+ { label: '璋冧紤鏃�', value: 'adjustment' },
+ { label: '鐗规畩鍋囨湡', value: 'special' },
+ { label: '鍏徃鍋囨湡', value: 'company' }
+ ]
+ } else if (currentType.value === 'overtime') {
+ return [
+ { label: '宸ヤ綔鏃ュ姞鐝�', value: 'weekday' },
+ { label: '鍛ㄦ湯鍔犵彮', value: 'weekend' },
+ { label: '鑺傚亣鏃ュ姞鐝�', value: 'holiday' },
+ { label: '娣卞鍔犵彮', value: 'night' }
+ ]
+ } else if (currentType.value === 'annual') {
+ return [
+ { label: '姝e紡鍛樺伐', value: 'regular' },
+ { label: '璇曠敤鏈熷憳宸�', value: 'probation' },
+ { label: '瀹炰範鐢�', value: 'intern' }
+ ]
+ }
+ return []
+}
+
+// 璁$畻鍋囨湡澶╂暟
+const calculateDays = () => {
+ try {
+ if (form.dateRange && form.dateRange.length === 2 && form.dateRange[0] && form.dateRange[1]) {
+ const start = new Date(form.dateRange[0])
+ const end = new Date(form.dateRange[1])
+ form.startDate = start.toISOString().split('T')[0]
+ form.endDate = end.toISOString().split('T')[0]
+
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+ console.warn('鏃犳晥鐨勬棩鏈熸牸寮�')
+ return
+ }
+
+ const diffTime = Math.abs(end - start)
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
+ form.days = diffDays
+ }
+ } catch (error) {
+ console.error('璁$畻澶╂暟澶辫触:', error)
+ }
+}
+
+// 楠岃瘉鏃堕棿鏍煎紡
+// const validateTime = (time) => {
+// if (!time) return ''
+// if (typeof time === 'string') return time
+// if (time instanceof Date) {
+// return time.toTimeString().slice(0, 5)
+// }
+// return ''
+// }
+
+// 楠岃瘉鏃堕棿瀛楁
+const validateTimeField = (fieldName) => {
+ try {
+ const value = form[fieldName]
+ if (value && typeof value === 'object' && value.hour !== undefined) {
+ // 濡傛灉鏄椂闂村璞★紝杞崲涓哄瓧绗︿覆鏍煎紡
+ const hours = value.hour.toString().padStart(2, '0')
+ const minutes = value.minute.toString().padStart(2, '0')
+ form[fieldName] = `${hours}:${minutes}`
+ }
+ } catch (error) {
+ console.error(`楠岃瘉鏃堕棿瀛楁 ${fieldName} 澶辫触:`, error)
+ form[fieldName] = ''
+ }
+}
+
+// 鎵撳紑寮圭獥
+const openDialog = (type, action, row = null) => {
+ try {
+ currentType.value = type
+ currentAction.value = action
+
+ if (action === 'add') {
+ dialogTitle.value = `鏂板${getTypeName(type)}`
+ currentEditId.value = ''
+ resetForm()
+ } else if (action === 'edit' && row) {
+ dialogTitle.value = `缂栬緫${getTypeName(type)}`
+ currentEditId.value = row.id
+ fillForm(row)
+ }
+
+ dialogVisible.value = true
+ } catch (error) {
+ console.error('鎵撳紑寮圭獥澶辫触:', error)
+ ElMessage.error('鎵撳紑寮圭獥澶辫触锛岃閲嶈瘯')
+ }
+}
+
+const getTypeName = (type) => {
+ const nameMap = {
+ holiday: '鍋囨湡',
+ annual: '骞村亣瑙勫垯',
+ overtime: '鍔犵彮瑙勫垯',
+ worktime: '鏃堕棿娈�'
+ }
+ return nameMap[type] || ''
+}
+
+const resetForm = () => {
+ Object.assign(form, {
+ name: '',
+ type: '',
+ dateRange: [],
+ startDate: '',
+ endDate: '',
+ days: 0,
+ employeeType: '',
+ workYears: '',
+ annualDays: 0,
+ maxCarryOver: 0,
+ startTime: '',
+ endTime: '',
+ workStartTime: '',
+ workEndTime: '',
+ rate: 1.5,
+ flexibleStart: false,
+ flexibleMinutes: 30,
+ status: 'active'
+ })
+}
+
+const fillForm = (row) => {
+ if (currentType.value === 'holiday') {
+ Object.assign(form, {
+ name: row.name,
+ type: row.type,
+ dateRange: [new Date(row.startDate), new Date(row.endDate)],
+ startDate: row.startDate,
+ endDate: row.endDate,
+ days: row.days,
+ status: row.status
+ })
+ } else if (currentType.value === 'annual') {
+ Object.assign(form, {
+ employeeType: row.employeeType,
+ workYears: row.workYears,
+ annualDays: row.annualDays,
+ maxCarryOver: row.maxCarryOver,
+ status: row.status
+ })
+ } else if (currentType.value === 'overtime') {
+ Object.assign(form, {
+ name: row.name,
+ type: row.type,
+ startTime: row.startTime || '',
+ endTime: row.endTime || '',
+ rate: row.rate,
+ status: row.status
+ })
+ } else if (currentType.value === 'worktime') {
+ Object.assign(form, {
+ name: row.name,
+ workStartTime: row.startTime || '',
+ workEndTime: row.endTime || '',
+ flexibleStart: row.flexibleStart,
+ flexibleMinutes: row.flexibleMinutes,
+ status: row.status
+ })
+ }
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = async () => {
+ try {
+ if (!formRef.value) {
+ ElMessage.error('琛ㄥ崟寮曠敤涓嶅瓨鍦�')
+ return
+ }
+
+ await formRef.value.validate()
+
+ if (currentAction.value === 'add') {
+ addItem()
+ } else if (currentAction.value === 'edit') {
+ editItem()
+ }
+
+ dialogVisible.value = false
+ ElMessage.success('鎿嶄綔鎴愬姛')
+ } catch (error) {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', error)
+ ElMessage.error('琛ㄥ崟楠岃瘉澶辫触锛岃妫�鏌ヨ緭鍏�')
+ }
+}
+
+const addItem = () => {
+
+ if (currentType.value === 'holiday') {
+ const params = {
+ name: form.name,
+ type: form.type,
+ startDate: form.startDate,
+ endDate: form.endDate,
+ days: form.days,
+ status: form.status
+ }
+ addHolidaySettings(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ // dialogVisible.value = false;
+ getHolidaySettingsList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } else if (currentType.value === 'annual') {
+ // annualData.value.push(newItem)
+ const params = {
+ employeeType: form.employeeType,
+ workYears: form.workYears,
+ annualDays: form.annualDays,
+ maxCarryOver: form.maxCarryOver,
+ status: form.status
+ }
+ addAnnualLeaveSetting(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ // dialogVisible.value = false;
+ getAnnualLeaveSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } else if (currentType.value === 'overtime') {
+ const params = {
+ name: form.name,
+ type: form.type,
+ startTime: form.startTime || '',
+ endTime: form.endTime || '',
+ rate: form.rate,
+ status: form.status
+ }
+ addOvertimeSetting(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ // dialogVisible.value = false;
+ getOvertimeSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ // newItem.startTime = form.startTime || ''
+ // newItem.endTime = form.endTime || ''
+ // overtimeData.value.push(newItem)
+ } else if (currentType.value === 'worktime') {
+ const params = {
+ name: form.name,
+ startTime: form.workStartTime || '',
+ endTime: form.workEndTime || '',
+ flexibleStart: form.flexibleStart,
+ flexibleMinutes: form.flexibleMinutes,
+ status: form.status
+ }
+ addWorkingHoursSetting(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ getWorkingHoursSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ // newItem.startTime = form.workStartTime || ''
+ // newItem.endTime = form.workEndTime || ''
+ // worktimeData.value.push(newItem)
+ }
+}
+
+const editItem = () => {
+ let dataArray
+ let index
+
+ if (currentType.value === 'holiday') {
+ const params = {
+ id: currentEditId.value,
+ name: form.name,
+ type: form.type,
+ startDate: form.dateRange[0].toISOString().split('T')[0],
+ endDate: form.dateRange[1].toISOString().split('T')[0],
+ days: form.days,
+ status: form.status
+ }
+ updateHolidaySettings(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ // dialogVisible.value = false;
+ getHolidaySettingsList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } else if (currentType.value === 'annual') {
+ const params = {
+ id: currentEditId.value,
+ employeeType: form.employeeType,
+ workYears: form.workYears,
+ annualDays: form.annualDays,
+ maxCarryOver: form.maxCarryOver,
+ status: form.status
+ }
+ updateAnnualLeaveSetting(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ getAnnualLeaveSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } else if (currentType.value === 'overtime') {
+ const params = {
+ id: currentEditId.value,
+ name: form.name,
+ type: form.type,
+ startTime: form.startTime || '',
+ endTime: form.endTime || '',
+ rate: form.rate,
+ status: form.status
+ }
+ updateOvertimeSetting(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ getOvertimeSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+
+ // dataArray = overtimeData.value
+ // index = dataArray.findIndex(item => item.id === currentEditId.value)
+ // if (index > -1) {
+ // dataArray[index] = {
+ // ...dataArray[index],
+ // name: form.name,
+ // type: form.type,
+ // startTime: form.startTime || '',
+ // endTime: form.endTime || '',
+ // rate: form.rate,
+ // status: form.status
+ // }
+ // }
+ } else if (currentType.value === 'worktime') {
+ const params = {
+ id: currentEditId.value,
+ name: form.name,
+ startTime: form.workStartTime || '',
+ endTime: form.workEndTime || '',
+ flexibleStart: form.flexibleStart,
+ flexibleMinutes: form.flexibleMinutes,
+ status: form.status
+ }
+ updateWorkingHoursSetting(params).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ getWorkingHoursSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ // dataArray = worktimeData.value
+ // index = dataArray.findIndex(item => item.id === currentEditId.value)
+ // if (index > -1) {
+ // dataArray[index] = {
+ // ...dataArray[index],
+ // name: form.name,
+ // startTime: form.workStartTime || '',
+ // endTime: form.workEndTime || '',
+ // flexibleStart: form.flexibleStart,
+ // flexibleMinutes: form.flexibleMinutes,
+ // status: form.status
+ // }
+ // }
+ }
+}
+
+// 鎵撳崱璁板綍杩囨护鍔熻兘
+const filterAttendanceData = () => {
+ let filtered = attendanceData.value
+
+ // 鎸夋棩鏈熻繃婊�
+ if (attendanceDate.value) {
+ filtered = filtered.filter(item => item.date === attendanceDate.value)
+ }
+
+ // 鎸夌姸鎬佽繃婊�
+ if (attendanceStatus.value) {
+ filtered = filtered.filter(item => item.status === attendanceStatus.value)
+ }
+
+ filteredAttendanceData.value = filtered
+}
+
+// 瀵煎嚭鎵撳崱璁板綍
+const exportAttendance = () => {
+ ElMessage.success('瀵煎嚭鍔熻兘寮�鍙戜腑...')
+}
+
+// 鍒濆鍖栨墦鍗¤褰曞亣鏁版嵁
+const initAttendanceData = () => {
+ const mockData = [
+ {
+ id: 1,
+ employeeName: '闄堝織寮�',
+ department: '鎶�鏈儴',
+ date: '2025-08-15',
+ clockInTime: '09:00:00',
+ clockOutTime: '18:00:00',
+ workHours: '8.0h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 2,
+ employeeName: '鏉庨洩姊�',
+ department: '甯傚満閮�',
+ date: '2025-08-16',
+ clockInTime: '08:58:00',
+ clockOutTime: '18:05:00',
+ workHours: '8.12h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 3,
+ employeeName: '鐜嬪缓鍗�',
+ department: '浜轰簨閮�',
+ date: '2025-08-16',
+ clockInTime: '09:02:00',
+ clockOutTime: '18:00:00',
+ workHours: '7.97h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 4,
+ employeeName: '璧垫檽涓�',
+ department: '璐㈠姟閮�',
+ date: '2025-09-02',
+ clockInTime: '08:55:00',
+ clockOutTime: '18:10:00',
+ workHours: '8.25h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 5,
+ employeeName: '寮犲浗搴�',
+ department: '鎶�鏈儴',
+ date: '2025-09-02',
+ clockInTime: '09:00:00',
+ clockOutTime: '18:30:00',
+ workHours: '8.5h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: '鍔犵彮'
+ },
+ {
+ id: 6,
+ employeeName: '鍒樻槑杈�',
+ department: '杩愯惀閮�',
+ date: '2025-09-03',
+ clockInTime: '09:05:00',
+ clockOutTime: '18:00:00',
+ workHours: '7.92h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 7,
+ employeeName: '瀛欎附鍗�',
+ department: '璁捐閮�',
+ date: '2025-09-03',
+ clockInTime: '08:59:00',
+ clockOutTime: '18:02:00',
+ workHours: '8.05h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 8,
+ employeeName: '鍛ㄥ缓鍐�',
+ department: '閿�鍞儴',
+ date: '2025-09-04',
+ clockInTime: '09:15:00',
+ clockOutTime: '18:00:00',
+ workHours: '7.75h',
+ status: 'late',
+ location: '鍏徃鎬婚儴',
+ remark: '浜ら�氬牭濉�'
+ },
+ {
+ id: 9,
+ employeeName: '鍚村皬鑺�',
+ department: '瀹㈡湇閮�',
+ date: '2025-09-04',
+ clockInTime: '09:01:00',
+ clockOutTime: '18:00:00',
+ workHours: '7.98h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 10,
+ employeeName: '椹枃鏉�',
+ department: '鎶�鏈儴',
+ date: '2025-09-05',
+ clockInTime: '08:57:00',
+ clockOutTime: '17:30:00',
+ workHours: '7.55h',
+ status: 'early',
+ location: '鍏徃鎬婚儴',
+ remark: '鏈夋�ヤ簨鎻愬墠绂诲紑'
+ },
+ {
+ id: 11,
+ employeeName: '鏋楁檽涓�',
+ department: '琛屾斂閮�',
+ date: '2025-09-05',
+ clockInTime: '09:03:00',
+ clockOutTime: '18:08:00',
+ workHours: '8.08h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 12,
+ employeeName: '榛勭編鐜�',
+ department: '璐㈠姟閮�',
+ date: '2025-09-06',
+ clockInTime: '',
+ clockOutTime: '',
+ workHours: '0h',
+ status: 'absent',
+ location: '',
+ remark: '璇风梾鍋�'
+ },
+ {
+ id: 13,
+ employeeName: '閮戞捣娑�',
+ department: '甯傚満閮�',
+ date: '2025-08-14',
+ clockInTime: '09:00:00',
+ clockOutTime: '18:00:00',
+ workHours: '8.0h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 14,
+ employeeName: '璋附濞�',
+ department: '浜轰簨閮�',
+ date: '2025-08-20',
+ clockInTime: '08:58:00',
+ clockOutTime: '18:03:00',
+ workHours: '8.08h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 15,
+ employeeName: '浣曞織浼�',
+ department: '鎶�鏈儴',
+ date: '2025-08-21',
+ clockInTime: '09:10:00',
+ clockOutTime: '18:00:00',
+ workHours: '7.83h',
+ status: 'late',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 16,
+ employeeName: '璁搁泤鑺�',
+ department: '璁捐閮�',
+ date: '2025-08-22',
+ clockInTime: '09:01:00',
+ clockOutTime: '18:00:00',
+ workHours: '7.98h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 17,
+ employeeName: '閭撳缓骞�',
+ department: '杩愯惀閮�',
+ date: '2025-09-10',
+ clockInTime: '08:59:00',
+ clockOutTime: '18:05:00',
+ workHours: '8.1h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ },
+ {
+ id: 18,
+ employeeName: '鏇惧皬绾�',
+ department: '瀹㈡湇閮�',
+ date: '2025-09-11',
+ clockInTime: '09:02:00',
+ clockOutTime: '18:00:00',
+ workHours: '7.97h',
+ status: 'normal',
+ location: '鍏徃鎬婚儴',
+ remark: ''
+ }
+ ]
+
+ attendanceData.value = mockData
+ filteredAttendanceData.value = mockData
+}
+
+// 鍒犻櫎椤圭洰
+const deleteItem = (type, row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖涓」鐩悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = [];
+ let dataArray
+ if (type === 'holiday') {
+ ids.push(row.id)
+ delHolidaySettings(ids).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ ids = []
+ getHolidaySettingsList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }
+ else if (type === 'annual') {
+ ids.push(row.id)
+ delAnnualLeaveSetting(ids).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ ids = []
+ getAnnualLeaveSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }
+ else if (type === 'overtime') {
+ ids.push(row.id)
+ delOvertimeSetting(ids).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ ids = []
+ getOvertimeSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }
+ else if (type === 'worktime') {
+ ids.push(row.id)
+ delWorkingHoursSetting(ids).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ ids = []
+ getWorkingHoursSettingList()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }
+
+ // const index = dataArray.findIndex(item => item.id === row.id)
+ // if (index > -1) {
+ // dataArray.splice(index, 1)
+ // ElMessage.success('鍒犻櫎鎴愬姛')
+ // }
+ })
+}
+// 鑾峰彇鍋囨湡璁剧疆鍒楄〃
+const getHolidaySettingsList = () => {
+ // tableLoading.value = true;
+ listHolidaySettings({...page.value})
+ .then(res => {
+ // tableLoading.value = false;
+ holidayData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ // tableLoading.value = false;
+ })
+};
+// 鑾峰彇骞村亣瑙勫垯鍒楄〃
+const getAnnualLeaveSettingList = () => {
+
+ listAnnualLeaveSettingList({...page.value})
+ .then(res => {
+ // console.log(res.data)
+ annualData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ })
+};
+// 鑾峰彇鍔犵彮瑙勫垯鍒楄〃
+const getOvertimeSettingList = () => {
+
+ listOvertimeSettingList({...page.value})
+ .then(res => {
+ // console.log(res.data)
+ overtimeData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ })
+};
+// 鑾峰彇宸ヤ綔鏃堕棿瑙勫垯鍒楄〃
+const getWorkingHoursSettingList = () => {
+
+ listWorkingHoursSettingList({...page.value})
+ .then(res => {
+ // console.log(res.data)
+ worktimeData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ })
+};
+onMounted(() => {
+ getHolidaySettingsList()
+ getAnnualLeaveSettingList()
+ getOvertimeSettingList()
+ getWorkingHoursSettingList()
+ initAttendanceData()
+ console.log('鑰冨嫟绠$悊椤甸潰鍔犺浇瀹屾垚')
+})
+
+onUnmounted(() => {
+ // 娓呯悊宸ヤ綔
+ dialogVisible.value = false
+ currentType.value = ''
+ currentAction.value = ''
+ currentEditId.value = ''
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.tab-content {
+ padding: 20px 0;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+
+:deep(.el-tabs__content) {
+ padding: 20px;
+}
+
+:deep(.el-form-item) {
+ margin-bottom: 20px;
+}
+</style>
diff --git a/src/views/collaborativeApproval/customerVisit/index.vue b/src/views/collaborativeApproval/customerVisit/index.vue
new file mode 100644
index 0000000..68396eb
--- /dev/null
+++ b/src/views/collaborativeApproval/customerVisit/index.vue
@@ -0,0 +1,269 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�">
+ <el-input
+ v-model="searchForm.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎷滆浜猴細">
+ <el-input
+ v-model="searchForm.visitingPeople"
+ placeholder="璇疯緭鍏ユ嫓璁夸汉"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)"
+ >
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" width="150" show-overflow-tooltip />
+ <el-table-column label="鑱旂郴浜�" prop="contact" width="120" show-overflow-tooltip />
+ <el-table-column label="鑱旂郴鐢佃瘽" prop="contactPhone" width="140" show-overflow-tooltip />
+ <el-table-column label="鎷滆鐩殑" prop="purposeVisit" width="150" show-overflow-tooltip />
+ <el-table-column label="鎷滆鏃堕棿" prop="purposeDate" width="180" show-overflow-tooltip />
+ <el-table-column label="鎷滆鍦扮偣" prop="visitAddress" min-width="200" show-overflow-tooltip />
+ <el-table-column label="鎷滆浜�" prop="visitingPeople" width="120" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" width="100" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="viewDetail(scope.row)" style="color: #67C23A">鏌ョ湅</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+
+ <!-- 璇︽儏寮圭獥 -->
+ <el-dialog
+ v-model="detailVisible"
+ title="瀹㈡埛鎷滆璁板綍璇︽儏"
+ width="600px"
+ @close="closeDetail"
+ >
+ <div class="content-container">
+ <!-- 瀹㈡埛淇℃伅 -->
+ <div class="section">
+ <div class="section-title">瀹㈡埛淇℃伅</div>
+ <div class="info-item">
+ <span class="info-label">瀹㈡埛鍚嶇О</span>
+ <span class="info-value">{{ detailForm.customerName || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鑱旂郴浜�</span>
+ <span class="info-value">{{ detailForm.contact || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鑱旂郴鐢佃瘽</span>
+ <span class="info-value">{{ detailForm.contactPhone || '-' }}</span>
+ </div>
+ </div>
+
+ <!-- 鎷滆淇℃伅 -->
+ <div class="section">
+ <div class="section-title">鎷滆淇℃伅</div>
+ <div class="info-item">
+ <span class="info-label">鎷滆鐩殑</span>
+ <span class="info-value">{{ detailForm.purposeVisit || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鎷滆鏃堕棿</span>
+ <span class="info-value">{{ detailForm.purposeDate || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鎷滆鍦扮偣</span>
+ <span class="info-value multi-line">{{ detailForm.visitAddress || '-' }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鎷滆浜�</span>
+ <span class="info-value">{{ detailForm.visitingPeople || '-' }}</span>
+ </div>
+ <div class="info-item" v-if="detailForm.latitude && detailForm.longitude">
+ <span class="info-label">缁忕含搴�</span>
+ <span class="info-value">{{ detailForm.latitude }}, {{ detailForm.longitude }}</span>
+ </div>
+ </div>
+
+ <!-- 澶囨敞淇℃伅 -->
+ <div class="section">
+ <div class="section-title">澶囨敞淇℃伅</div>
+ <div class="info-item remark-item">
+ <span class="info-label">澶囨敞</span>
+ <span class="info-value multi-line">{{ detailForm.remark || '-' }}</span>
+ </div>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDetail">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
+import pagination from '@/components/PIMTable/Pagination.vue'
+import { getVisitRecords } from '@/api/collaborativeApproval/customerVisit.js'
+
+const { proxy } = getCurrentInstance()
+const tableData = ref([])
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 10,
+})
+const total = ref(0)
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ customerName: '',
+ visitingPeople: '',
+})
+
+// 璇︽儏鐩稿叧
+const detailVisible = ref(false)
+const detailForm = ref({})
+
+// 鏌ヨ鍒楄〃
+const handleQuery = () => {
+ page.current = 1
+ getList()
+}
+
+// 鍒嗛〉鍙樺寲
+const paginationChange = (obj) => {
+ page.current = obj.page
+ page.size = obj.limit
+ getList()
+}
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = () => {
+ tableLoading.value = true
+ getVisitRecords({ ...searchForm, ...page })
+ .then((res) => {
+ tableLoading.value = false
+ if (res.code === 200) {
+ tableData.value = res.data?.records || res.records || []
+ total.value = res.data?.total || res.total || 0
+ } else {
+ proxy.$modal.msgError(res.msg || '鑾峰彇鏁版嵁澶辫触')
+ }
+ })
+ .catch(() => {
+ tableLoading.value = false
+ })
+}
+
+// 鏌ョ湅璇︽儏
+const viewDetail = (row) => {
+ detailForm.value = { ...row }
+ detailVisible.value = true
+}
+
+// 鍏抽棴璇︽儏
+const closeDetail = () => {
+ detailVisible.value = false
+ detailForm.value = {}
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+.table_list {
+ margin-top: unset;
+}
+
+.content-container {
+ padding: 10px;
+}
+
+.section {
+ margin-bottom: 24px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.section-title {
+ font-size: 16px;
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 16px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #e4e7ed;
+}
+
+.info-item {
+ display: flex;
+ margin-bottom: 12px;
+ line-height: 1.6;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ &.remark-item {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .info-label {
+ margin-bottom: 8px;
+ }
+
+ .info-value {
+ width: 100%;
+ }
+ }
+}
+
+.info-label {
+ font-weight: 500;
+ color: #606266;
+ min-width: 100px;
+ margin-right: 12px;
+ flex-shrink: 0;
+}
+
+.info-value {
+ color: #303133;
+ flex: 1;
+ word-break: break-all;
+
+ &.multi-line {
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/enterpriseBook/index.vue b/src/views/collaborativeApproval/enterpriseBook/index.vue
new file mode 100644
index 0000000..8239811
--- /dev/null
+++ b/src/views/collaborativeApproval/enterpriseBook/index.vue
@@ -0,0 +1,798 @@
+<template>
+ <div class="app-container">
+ <!-- 澶撮儴瀵艰埅 -->
+ <!-- <div class="header">
+ <h2>浼佷笟閫氳褰曠鐞�</h2>
+ <p>绠$悊涓汉銆佸叕鍏卞拰鍗曚綅鐨勮仈绯绘柟寮�</p>
+ </div> -->
+
+ <!-- 鏍囩椤靛垏鎹� -->
+ <el-tabs v-model="activeTab" @tab-change="handleTabChange" type="border-card">
+ <el-tab-pane label="涓汉閫氳褰�" name="personal">
+ <div class="tab-content">
+ <!-- 鎼滅储妗� -->
+ <el-input
+ v-model="personalSearch.staffName"
+ placeholder="鎼滅储鑱旂郴浜�"
+ clearable
+ prefix-icon="Search"
+ class="search-input"
+ @keyup.enter="getPersonalContactsList"
+ />
+ <el-button style="margin: 0 0 20px 20px;" type="primary" @click="showAddContactDialog=true">娣诲姞鑱旂郴浜�</el-button>
+ <!-- 鑱旂郴浜哄垪琛� -->
+ <div class="contact-list">
+ <div
+ v-for="contact in personalContacts"
+ :key="contact.id"
+ class="contact-card"
+ @click="showContactDetail(contact)"
+ >
+ <div class="contact-avatar">{{ contact.staffName.charAt(0) }}</div>
+ <div class="contact-info">
+ <h4>{{ contact.staffName }}</h4>
+ <p>{{ contact.profession }} - {{ contact.postJob }}</p>
+ <div class="contact-phone">{{ contact.phone }}</div>
+ </div>
+ <div class="contact-actions">
+ <!-- <el-button
+ type="text"
+ icon="Phone"
+ @click.stop="callContact(contact)"
+ ></el-button> -->
+ <el-button
+ type="text"
+ icon="Message"
+ @click.stop="messageContact(contact)"
+ ></el-button>
+ <el-button
+ type="text"
+ icon="Delete"
+ @click.stop="removeFromPersonalContacts(contact.id)"
+ ></el-button>
+ </div>
+ </div>
+
+ <!-- 绌虹姸鎬�
+ <div v-if="personalContacts.length === 0 && !loading" class="empty-state">
+ <el-empty description="鏆傛棤鑱旂郴浜�" />
+ <el-button type="primary" @click="showAddContactDialog=true">娣诲姞鑱旂郴浜�</el-button>
+ </div> -->
+ </div>
+ </div>
+ </el-tab-pane>
+
+ <el-tab-pane label="鍏叡閫氳褰�" name="public">
+ <div class="tab-content">
+ <!-- 鎼滅储妗� -->
+ <el-input
+ v-model="publicSearch.staffName"
+ placeholder="鎼滅储鍏叡鑱旂郴浜�"
+ clearable
+ prefix-icon="Search"
+ class="search-input"
+ @keyup.enter="getPublicContactsList"
+ />
+
+ <!-- 鑱旂郴浜哄垪琛� publicContacts-->
+ <div class="contact-list">
+ <div
+ v-for="contact in EmployeeList"
+ :key="contact.id"
+ class="contact-card"
+ @click="showContactDetail(contact)"
+ >
+ <div class="contact-avatar">{{ contact.staffName.charAt(0) }}</div>
+ <div class="contact-info">
+ <h4>{{ contact.staffName }}</h4>
+ <p>{{ contact.postJob }} - {{ contact.profession }}</p>
+ <div class="contact-phone">{{ contact.phone }}</div>
+ </div>
+ <div class="contact-actions">
+ <!-- <el-button
+ type="text"
+ icon="Phone"
+ @click.stop="callContact(contact)"
+ ></el-button> -->
+ <el-button
+ type="text"
+ icon="Message"
+ @click.stop="messageContact(contact)"
+ ></el-button>
+ <el-button
+ type="text"
+ icon="Delete"
+ :type="isInPersonalContacts(contact.id) ? 'primary' : ''"
+ @click.stop="togglePersonalContact(contact)"
+ ></el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-tab-pane>
+
+ <el-tab-pane label="鍗曚綅閫氳褰�" name="company">
+ <div class="tab-content">
+ <div class="company-contacts-layout">
+ <!-- 宸︿晶閮ㄩ棬鏍� -->
+ <div class="department-tree">
+ <!-- <h3>閮ㄩ棬缁撴瀯</h3>
+ <el-tree
+ :data="departmentTree"
+ :props="{ label: 'deptName', children: 'children' }"
+ node-key="deptId"
+ ref="departmentTreeRef"
+ highlight-current
+ default-expand-all
+ @node-click="handleDepartmentClick"
+ /> -->
+ <el-col >
+ <div class="head-container">
+ <el-input
+ v-model="deptName"
+ placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�"
+ clearable
+ prefix-icon="Search"
+ style="margin-bottom: 20px"
+ />
+ </div>
+ <div class="head-container">
+ <el-tree
+ :data="departmentTree"
+ :props="{ label: 'label', children: 'children' }"
+ :expand-on-click-node="false"
+ :filter-node-method="filterNode"
+ ref="deptTreeRef"
+ node-key="id"
+ highlight-current
+ default-expand-all
+ @node-click="handleDepartmentClick"
+ />
+ </div>
+ </el-col>
+ </div>
+
+ <!-- 鍙充晶閮ㄩ棬鎴愬憳 -->
+ <div class="department-members">
+ <h3>{{ currentDepartment?.label || '鍏ㄩ儴鎴愬憳' }}</h3>
+ <el-input
+ v-model="companySearch.staffName"
+ placeholder="鎼滅储閮ㄩ棬鎴愬憳"
+ clearable
+ prefix-icon="Search"
+ class="search-input"
+ @keyup.enter="getCompanyContactsList"
+ />
+
+ <div class="contact-list">
+ <div
+ v-for="contact in companyContacts"
+ :key="contact.id"
+ class="contact-card"
+ @click="showContactDetail(contact)"
+ >
+ <div class="contact-avatar">{{ contact.staffName.charAt(0) }}</div>
+ <div class="contact-info">
+ <h4>{{ contact.staffName }}</h4>
+ <p>{{ contact.profession }}</p>
+ <div class="contact-phone">{{ contact.phone }}</div>
+ </div>
+ <div class="contact-actions">
+
+ <el-button
+ type="text"
+ icon="Message"
+ @click.stop="messageContact(contact)"
+ ></el-button>
+ <el-button
+ type="text"
+ icon="Delete"
+ :type="isInPersonalContacts(contact.id) ? 'primary' : ''"
+ @click.stop="togglePersonalContact(contact)"
+ ></el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+
+ <!-- 鑱旂郴浜鸿鎯呭脊绐� -->
+ <el-dialog
+ v-model="showDetailDialog"
+ title="鑱旂郴浜鸿鎯�"
+ width="400px"
+ >
+ <div v-if="selectedContact" class="contact-detail">
+ <div class="detail-avatar">{{ selectedContact.staffName?.charAt(0) }}</div>
+ <h3>{{ selectedContact.staffName }}</h3>
+ <p class="detail-position">{{ selectedContact.profession }} - {{ selectedContact.postJob }}</p>
+
+ <div class="detail-info">
+ <div class="info-item">
+ <span class="label">缂栧彿锛�</span>
+ <span class="value">{{ selectedContact.staffNo }}</span>
+ </div>
+ <div class="info-item">
+ <span class="label">鎵嬫満鍙风爜锛�</span>
+ <span class="value">{{ selectedContact.phone }}</span>
+ </div>
+ <div class="info-item">
+ <span class="label">閭锛�</span>
+ <span class="value">{{ selectedContact.sex }}</span>
+ </div>
+ <div class="info-item">
+ <span class="label">浣忓潃锛�</span>
+ <span class="value">{{ selectedContact.adress || '鏆傛棤' }}</span>
+ </div>
+ </div>
+ </div>
+ <template #footer>
+ <el-button @click="showDetailDialog = false">鍏抽棴</el-button>
+ <el-button
+ type="primary"
+ v-if="activeTab !== 'personal'"
+ @click="togglePersonalContact(selectedContact); showDetailDialog = false"
+ >
+ {{ isInPersonalContacts(selectedContact?.id) ? '浠庝釜浜洪�氳褰曠Щ闄�' : '娣诲姞鍒颁釜浜洪�氳褰�' }}
+ </el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 娣诲姞鑱旂郴浜哄脊绐� -->
+ <el-dialog
+ v-model="showAddContactDialog"
+ title="娣诲姞鑱旂郴浜�"
+ width="500px"
+ >
+ <el-form :model="addContactForm" ref="addContactFormRef" label-width="80px">
+ <!-- <el-form-item label="濮撳悕" prop="name">
+ <el-input v-model="addContactForm.name" placeholder="璇疯緭鍏ュ鍚�" />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="phone">
+ <el-input v-model="addContactForm.phone" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" />
+ </el-form-item>
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="addContactForm.email" placeholder="璇疯緭鍏ラ偖绠�" />
+ </el-form-item>
+ <el-form-item label="閮ㄩ棬" prop="department">
+ <el-input v-model="addContactForm.department" placeholder="璇疯緭鍏ラ儴闂�" />
+ </el-form-item> -->
+ <el-form-item label="濮撳悕" prop="name">
+ <!-- <select v-model="addContactForm.contactId">
+ <option v-for="item in EmployeeList" :key="item.id" :value="item.id">{{ item.staffName }}</option>
+ </select> -->
+ <el-select v-model="addContactForm.contactId" placeholder="璇烽�夋嫨" style="width: 100%">
+ <el-option
+ v-for="option in EmployeeList"
+ :key="option.id"
+ :label="option.staffName"
+ :value="option.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" @click="addContact">纭畾</el-button>
+ <el-button @click="showAddContactDialog = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+ getPersonalContacts,
+ addPersonalContact,
+ removePersonalContact,
+ getPublicContacts,
+ getCompanyContacts,
+ getDepartmentTree,
+ getEmployeeDetail
+} from '@/api/collaborativeApproval/enterpriseBook.js'
+import { getUserProfile } from '@/api/system/user.js'
+import {
+ changeUserStatus,
+ listUser,
+ resetUserPwd,
+ delUser,
+ getUser,
+ updateUser,
+ addUser,
+ deptTreeSelect,
+} from "@/api/system/user";
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+
+// 鏍囩椤电姸鎬�
+const activeTab = ref('personal')
+const loading = ref(false)
+const EmployeeList = ref([])
+const page = reactive({
+ pageNum: 1,
+ pageSize: 10,
+ total: 0,
+})
+// 涓汉閫氳褰曟暟鎹�
+const personalContacts = ref([])
+const personalSearch = ref({
+ staffName: '',
+})
+
+// 鍏叡閫氳褰曟暟鎹�
+const publicContacts = ref([])
+const publicSearch = ref({
+ staffName: '',
+ staffState: 1
+})
+
+// 鍗曚綅閫氳褰曟暟鎹�
+const companyContacts = ref([])
+const companySearch = ref({
+ staffName: '',
+ staffState: 1
+})
+const departmentTree = ref([])
+const departmentTreeRef = ref(null)
+const currentDepartment = ref(null)
+
+// 寮圭獥鐘舵��
+const showDetailDialog = ref(false)
+const showAddContactDialog = ref(false)
+const selectedContact = ref(null)
+
+// 娣诲姞鑱旂郴浜鸿〃鍗�
+const addContactForm = reactive({
+ contactId: '',
+ name: '',
+ phone: '',
+ email: '',
+ department: '',
+ position: ''
+})
+const addContactFormRef = ref(null)
+
+// 鍒濆鍖栨暟鎹�
+onMounted(() => {
+ getEmployeeList()
+ getPersonalContactsList()
+ if (activeTab.value === 'public') {
+ getPublicContactsList()
+ } else if (activeTab.value === 'company') {
+ getDepartmentTreeData()
+ getCompanyContactsList()
+ }
+})
+
+// 澶勭悊鏍囩椤靛垏鎹�
+const handleTabChange = (tabName) => {
+ if (tabName === 'public') {
+ getPublicContactsList()
+ } else if (tabName === 'company') {
+ getDepartmentTreeData()
+ getCompanyContactsList()
+ }
+}
+
+// 鑾峰彇涓汉閫氳褰曞垪琛�
+const getPersonalContactsList = async () => {
+ loading.value = true
+ getPersonalContacts(page,personalSearch.value).then(res => {
+ personalContacts.value = res.data.records
+ })
+ loading.value = false
+}
+
+// 鑾峰彇鍏叡閫氳褰曞垪琛�
+const getPublicContactsList = async () => {
+ loading.value = true
+ getEmployeeList()
+ // publicContacts.value = generateMockPublicContacts()
+ loading.value = false
+}
+ //鑾峰彇鍛樺伐鍒楄〃
+const getEmployeeList = async () => {
+ staffOnJobListPage(Object.assign({current: -1, size: -1},publicSearch.value)).then(res => {
+ console.log(res.data.records)
+ EmployeeList.value = res.data.records
+ }).catch(err => {})
+}
+// 鑾峰彇鍗曚綅閫氳褰曞垪琛�
+const getCompanyContactsList = async () => {
+ loading.value = true
+ staffOnJobListPage(Object.assign({current: -1, size: -1},companySearch.value)).then(res => {
+ // console.log(res.data.records)
+ companyContacts.value = res.data.records
+ }).catch(err => {})
+
+ loading.value = false
+ loading.value = false
+ // }
+}
+
+// 鑾峰彇閮ㄩ棬鏍戠粨鏋�
+const getDepartmentTreeData = async () => {
+ deptTreeSelect().then((response) => {
+ // console.log("Tree",response.data)
+ departmentTree.value = response.data;
+ // enabledDeptOptions.value = filterDisabledDept(
+ // JSON.parse(JSON.stringify(response.data))
+ // );
+ });
+}
+// /** 杩囨护绂佺敤鐨勯儴闂� */
+// function filterDisabledDept(deptList) {
+// return deptList.filter((dept) => {
+// if (dept.disabled) {
+// return false;
+// }
+// if (dept.children && dept.children.length) {
+// dept.children = filterDisabledDept(dept.children);
+// }
+// return true;
+// });
+// }
+// 澶勭悊閮ㄩ棬鐐瑰嚮
+const handleDepartmentClick = (data) => {
+ // console.log("鐐瑰嚮",data)
+ companySearch.value = {
+ ...companySearch.value,
+ deptId: data.id,
+ }
+ // currentDepartment.value = data.id
+ // 鑾峰彇璇ラ儴闂ㄧ殑鎴愬憳鍒楄〃
+
+ getCompanyContactsList()
+}
+
+// 鏄剧ず鑱旂郴浜鸿鎯�
+const showContactDetail = async (contact) => {
+ selectedContact.value = contact
+ showDetailDialog.value = true
+}
+
+// 鎷ㄦ墦鐢佃瘽
+const callContact = (contact) => {
+ ElMessage.info(`姝e湪鎷ㄦ墦 ${contact.name} 鐨勭數璇�: ${contact.phone}`)
+}
+
+// 鍙戦�佹秷鎭�
+const messageContact = (contact) => {
+ ElMessage.info(`姝e湪鍙戦�佹秷鎭粰 ${contact.name}`)
+}
+
+
+// 娣诲姞鑱旂郴浜�
+const addContact = async () => {
+
+ try {
+ // 琛ㄥ崟楠岃瘉
+ // if (!addContactForm.name || !addContactForm.phone) {
+ // ElMessage.warning('璇峰~鍐欏鍚嶅拰鎵嬫満鍙风爜')
+ // return
+ // }
+
+ const res = await addPersonalContact(addContactForm)
+ if (res.code === 200) {
+ ElMessage.success('娣诲姞鎴愬姛')
+ showAddContactDialog.value = false
+ getPersonalContactsList()
+ // 閲嶇疆琛ㄥ崟
+ Object.keys(addContactForm).forEach(key => {
+ addContactForm[key] = ''
+ })
+ }
+ } catch (error) {
+ ElMessage.error('娣诲姞澶辫触')
+ // 妯℃嫙娣诲姞鎴愬姛
+ personalContacts.value.push({
+ ...addContactForm,
+ id: Date.now(),
+ createTime: new Date().toISOString()
+ })
+ ElMessage.success('娣诲姞鎴愬姛')
+ showAddContactDialog.value = false
+ // 閲嶇疆琛ㄥ崟
+ Object.keys(addContactForm).forEach(key => {
+ addContactForm[key] = ''
+ })
+ }
+}
+
+// 浠庝釜浜洪�氳褰曠Щ闄�
+const removeFromPersonalContacts = async (contactId) => {
+ ElMessageBox.confirm(
+ '纭畾瑕佷粠涓汉閫氳褰曚腑绉婚櫎璇ヨ仈绯讳汉鍚楋紵',
+ '鎻愮ず',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ ).then(async () => {
+ try {
+ const res = await removePersonalContact(contactId)
+ if (res.code === 200) {
+ ElMessage.success('绉婚櫎鎴愬姛')
+ getPersonalContactsList()
+ }
+ } catch (error) {
+ ElMessage.error('绉婚櫎澶辫触')
+ // 妯℃嫙绉婚櫎鎴愬姛
+ // personalContacts.value = personalContacts.value.filter(item => item.id !== contactId)
+ ElMessage.success('绉婚櫎鎴愬姛')
+ }
+ })
+}
+
+// 鍒囨崲涓汉閫氳褰�
+const togglePersonalContact = async (contact) => {
+ const isInPersonal = isInPersonalContacts(contact.id)
+ const contactId = contact.id
+ if (isInPersonal) {
+ // 浠庝釜浜洪�氳褰曠Щ闄�
+ //鏍规嵁contactId鏌ユ壘personalContacts涓搴旂殑椤癸紝鐒跺悗鍒犻櫎璇ラ」
+ const index = personalContacts.value.findIndex(item => item.contactId === contactId)
+ const personId = personalContacts.value[index].id
+ // console.log(personId)
+ await removeFromPersonalContacts(personId)
+ } else {
+ // 娣诲姞鍒颁釜浜洪�氳褰�
+ try {
+ const res = await addPersonalContact({contactId: contactId})
+ if (res.code === 200) {
+ ElMessage.success('娣诲姞鎴愬姛')
+ getPersonalContactsList()
+ }
+ } catch (error) {
+ ElMessage.error('娣诲姞澶辫触')
+ // 妯℃嫙娣诲姞鎴愬姛
+ // personalContacts.value.push({
+ // ...contact,
+ // id: contact.id || Date.now(),
+ // createTime: new Date().toISOString()
+ // })
+ // ElMessage.success('娣诲姞鎴愬姛')
+ }
+ }
+}
+
+// 妫�鏌ユ槸鍚﹀湪涓汉閫氳褰曚腑
+const isInPersonalContacts = (contactId) => {
+ return personalContacts.value.some(item => item.contactId === contactId)
+}
+
+// 鐢熸垚妯℃嫙閮ㄩ棬鏍戞暟鎹�
+const generateMockDepartmentTree = () => {
+ return [
+ {
+ deptId: 1,
+ deptName: '鎶�鏈儴',
+ children: [
+ {
+ deptId: 101,
+ deptName: '鍓嶇缁�'
+ },
+ {
+ deptId: 102,
+ deptName: '鍚庣缁�'
+ },
+ {
+ deptId: 103,
+ deptName: '娴嬭瘯缁�'
+ }
+ ]
+ },
+ {
+ deptId: 2,
+ deptName: '浜у搧閮�'
+ },
+ {
+ deptId: 3,
+ deptName: '浜轰簨閮�'
+ },
+ {
+ deptId: 4,
+ deptName: '璐㈠姟閮�'
+ }
+ ]
+}
+
+// 鐢熸垚妯℃嫙鍗曚綅閫氳褰曟暟鎹�
+// const generateMockCompanyContacts = (deptName) => {
+// const allContacts = getEmployeeList()
+
+// if (deptName) {
+// return allContacts.filter(contact => contact.postJob === deptName)
+// }
+// return allContacts
+// }
+
+</script>
+
+<style scoped>
+.header {
+ margin-bottom: 20px;
+ padding: 15px;
+ background: #f5f7fa;
+ border-radius: 8px;
+}
+
+.header h2 {
+ margin: 0 0 5px 0;
+ color: #303133;
+}
+
+.header p {
+ margin: 0;
+ color: #909399;
+ font-size: 14px;
+}
+
+.tab-content {
+ padding: 15px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.search-input {
+ margin-bottom: 20px;
+ width: 300px;
+}
+
+.contact-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 15px;
+}
+
+.contact-card {
+ display: flex;
+ align-items: center;
+ padding: 15px;
+ width: 500px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.contact-card:hover {
+ background: #e9ecef;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.contact-avatar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 48px;
+ height: 48px;
+ background: #409eff;
+ color: #fff;
+ font-size: 20px;
+ font-weight: bold;
+ border-radius: 50%;
+ margin-right: 15px;
+}
+
+.contact-info {
+ flex: 1;
+}
+
+.contact-info h4 {
+ margin: 0 0 5px 0;
+ color: #303133;
+ font-size: 16px;
+}
+
+.contact-info p {
+ margin: 0 0 5px 0;
+ color: #606266;
+ font-size: 14px;
+}
+
+.contact-phone {
+ color: #409eff;
+ font-size: 14px;
+}
+
+.contact-actions {
+ display: flex;
+ gap: 5px;
+}
+
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 60px 20px;
+ color: #909399;
+}
+
+.company-contacts-layout {
+ display: flex;
+ gap: 20px;
+}
+
+.department-tree {
+ width: 250px;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 8px;
+}
+
+.department-tree h3 {
+ margin: 0 0 15px 0;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #e4e7ed;
+ color: #303133;
+ font-size: 16px;
+}
+
+.department-members {
+ flex: 1;
+}
+
+.department-members h3 {
+ margin: 0 0 15px 0;
+ color: #303133;
+ font-size: 16px;
+}
+
+.contact-detail {
+ text-align: center;
+}
+
+.detail-avatar {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 80px;
+ background: #409eff;
+ color: #fff;
+ font-size: 32px;
+ font-weight: bold;
+ border-radius: 50%;
+ margin: 0 auto 20px;
+}
+
+.contact-detail h3 {
+ margin: 0 0 10px 0;
+ color: #303133;
+ font-size: 20px;
+}
+
+.detail-position {
+ margin: 0 0 30px 0;
+ color: #606266;
+ font-size: 14px;
+}
+
+.detail-info {
+ text-align: left;
+}
+
+.info-item {
+ margin-bottom: 15px;
+}
+
+.info-item .label {
+ display: inline-block;
+ width: 100px;
+ color: #909399;
+ font-size: 14px;
+}
+
+.info-item .value {
+ color: #303133;
+ font-size: 14px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/knowledgeBase/index.vue b/src/views/collaborativeApproval/knowledgeBase/index.vue
new file mode 100644
index 0000000..43fee33
--- /dev/null
+++ b/src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -0,0 +1,758 @@
+<template>
+ <div class="app-container">
+ <div class="search_form" style="margin-bottom: 20px;">
+ <div>
+ <span class="search_title">鐭ヨ瘑鏍囬锛�</span>
+ <el-input
+ v-model="searchForm.title"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ョ煡璇嗘爣棰樻悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <span class="search_title ml10">鐭ヨ瘑绫诲瀷锛�</span>
+ <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
+ <el-option
+ v-for="item in knowledgeTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button @click="handleExport" style="margin-right: 10px">瀵煎嚭</el-button>
+ <el-button type="primary" @click="openForm('add')">鏂板鐭ヨ瘑</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+
+ <!-- 鏂板/缂栬緫鐭ヨ瘑寮圭獥 -->
+ <FormDialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ :width="'800px'"
+ @close="closeKnowledgeDialog"
+ @confirm="submitForm"
+ @cancel="closeKnowledgeDialog"
+ >
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐭ヨ瘑鏍囬" prop="title">
+ <el-input v-model="form.title" placeholder="璇疯緭鍏ョ煡璇嗘爣棰�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐭ヨ瘑绫诲瀷" prop="type">
+ <el-select v-model="form.type" placeholder="璇烽�夋嫨鐭ヨ瘑绫诲瀷" style="width: 100%">
+ <el-option
+ v-for="item in knowledgeTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閫傜敤鍦烘櫙" prop="scenario">
+ <el-input v-model="form.scenario" placeholder="璇疯緭鍏ラ�傜敤鍦烘櫙" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙e喅鏁堢巼" prop="efficiency">
+ <el-select v-model="form.efficiency" placeholder="璇烽�夋嫨瑙e喅鏁堢巼" style="width: 100%">
+ <el-option label="鏄捐憲鎻愬崌" value="high" />
+ <el-option label="涓�鑸彁鍗�" value="medium" />
+ <el-option label="杞诲井鎻愬崌" value="low" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="闂鎻忚堪" prop="problem">
+ <el-input
+ v-model="form.problem"
+ type="textarea"
+ :rows="3"
+ placeholder="璇锋弿杩伴亣鍒扮殑闂"
+ />
+ </el-form-item>
+ <el-form-item label="瑙e喅鏂规" prop="solution">
+ <el-input
+ v-model="form.solution"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯缁嗘弿杩拌В鍐虫柟妗�"
+ />
+ </el-form-item>
+ <el-form-item label="鍏抽敭瑕佺偣" prop="keyPoints">
+ <el-input
+ v-model="form.keyPoints"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ叧閿鐐癸紝鐢ㄩ�楀彿鍒嗛殧"
+ />
+ </el-form-item>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓浜�" prop="creator">
+ <el-select v-model="form.creator" placeholder="璇烽�夋嫨鍒涘缓浜�" style="width: 100%" filterable>
+ <el-option
+ v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.nickName"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浣跨敤娆℃暟" prop="usageCount">
+ <el-input-number v-model="form.usageCount" :min="0" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+
+ <!-- 鏌ョ湅鐭ヨ瘑璇︽儏寮圭獥 -->
+ <FormDialog
+ v-model="viewDialogVisible"
+ title="鐭ヨ瘑璇︽儏"
+ :width="'900px'"
+ @close="closeViewDialog"
+ @confirm="handleViewDialogConfirm"
+ @cancel="closeViewDialog"
+ >
+ <div class="knowledge-detail">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鐭ヨ瘑鏍囬" :span="2">
+ <span class="detail-title">{{ currentKnowledge.title }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐭ヨ瘑绫诲瀷">
+ <el-tag :type="getTypeTagType(currentKnowledge.type)">
+ {{ getTypeLabel(currentKnowledge.type) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="閫傜敤鍦烘櫙">
+ {{ currentKnowledge.scenario }}
+ </el-descriptions-item>
+ <el-descriptions-item label="瑙e喅鏁堢巼">
+ <el-tag :type="getEfficiencyTagType(currentKnowledge.efficiency)">
+ {{ getEfficiencyLabel(currentKnowledge.efficiency) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="浣跨敤娆℃暟">
+ <el-tag type="info">{{ currentKnowledge.usageCount }} 娆�</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">
+ {{ currentKnowledge.creator }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">
+ {{ currentKnowledge.createTime }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <div class="detail-section">
+ <h4>闂鎻忚堪</h4>
+ <div class="detail-content">{{ currentKnowledge.problem }}</div>
+ </div>
+
+ <div class="detail-section">
+ <h4>瑙e喅鏂规</h4>
+ <div class="detail-content">{{ currentKnowledge.solution }}</div>
+ </div>
+
+ <div class="detail-section">
+ <h4>鍏抽敭瑕佺偣</h4>
+ <div class="key-points">
+ <el-tag
+ v-for="(point, index) in currentKnowledge.keyPoints?.split(',') || []"
+ :key="index"
+ type="success"
+ style="margin-right: 8px; margin-bottom: 8px;"
+ >
+ {{ point.trim() }}
+ </el-tag>
+ </div>
+ </div>
+
+ <div class="detail-section">
+ <h4>浣跨敤缁熻</h4>
+ <div class="usage-stats">
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <div class="stat-item">
+ <div class="stat-number">{{ currentKnowledge.usageCount }}</div>
+ <div class="stat-label">浣跨敤娆℃暟</div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="stat-item">
+ <div class="stat-number">{{ getEfficiencyScore(currentKnowledge.efficiency) }}%</div>
+ <div class="stat-label">鏁堢巼鎻愬崌</div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="stat-item">
+ <div class="stat-number">{{ getTimeSaved(currentKnowledge.efficiency) }}</div>
+ <div class="stat-label">骞冲潎鑺傜渷鏃堕棿</div>
+ </div>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+ </div>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import { listKnowledgeBase, delKnowledgeBase,addKnowledgeBase,updateKnowledgeBase } from "@/api/collaborativeApproval/knowledgeBase.js";
+import useUserStore from '@/store/modules/user';
+import { userListNoPageByTenantId } from '@/api/system/user.js';
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+ title: [
+ { required: true, message: "璇疯緭鍏ョ煡璇嗘爣棰�", trigger: "blur" }
+ ],
+ type: [
+ { required: true, message: "璇烽�夋嫨鐭ヨ瘑绫诲瀷", trigger: "change" }
+ ],
+ problem: [
+ { required: true, message: "璇锋弿杩伴亣鍒扮殑闂", trigger: "blur" }
+ ],
+ solution: [
+ { required: true, message: "璇疯缁嗘弿杩拌В鍐虫柟妗�", trigger: "blur" }
+ ]
+};
+
+// 鍝嶅簲寮忔暟鎹�
+const data = reactive({
+ searchForm: {
+ title: "",
+ type: "",
+ },
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+ selectedIds: [],
+ form: {
+ title: "",
+ type: "",
+ scenario: "",
+ efficiency: "",
+ problem: "",
+ solution: "",
+ keyPoints: "",
+ creator: "",
+ usageCount: 0
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ viewDialogVisible: false,
+ currentKnowledge: {}
+});
+
+const {
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ selectedIds,
+ form,
+ dialogVisible,
+ dialogTitle,
+ dialogType,
+ viewDialogVisible,
+ currentKnowledge
+} = toRefs(data);
+
+// 琛ㄥ崟寮曠敤
+const formRef = ref();
+// 鐢ㄦ埛鐩稿叧
+const userStore = useUserStore();
+const userList = ref([]);
+
+// 琛ㄦ牸鍒楅厤缃�
+const tableColumn = ref([
+ {
+ label: "鐭ヨ瘑鏍囬",
+ prop: "title",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鐭ヨ瘑绫诲瀷",
+ prop: "type",
+ dataType: "tag",
+ formatData: (params) => {
+ return getKnowledgeTypeLabel(params);
+ },
+ formatType: (params) => {
+ return getKnowledgeTypeTagType(params);
+ }
+ },
+ {
+ label: "閫傜敤鍦烘櫙",
+ prop: "scenario",
+ width: 150,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "瑙e喅鏁堢巼",
+ prop: "efficiency",
+ dataType: "tag",
+ formatData: (params) => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌"
+ };
+ return efficiencyMap[params] || params;
+ },
+ formatType: (params) => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info"
+ };
+ return typeMap[params] || "info";
+ }
+ },
+ {
+ label: "浣跨敤娆℃暟",
+ prop: "usageCount",
+ width: 100,
+ align: "center"
+ },
+ {
+ label: "鍒涘缓浜�",
+ prop: "creator",
+ width: 120,
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 180,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 200,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ }
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => {
+ viewKnowledge(row);
+ }
+ }
+ ]
+ }
+]);
+
+// 鐩戝惉瀵硅瘽妗嗘墦寮�锛岃幏鍙栫敤鎴峰垪琛�
+watch(dialogVisible, (newVal) => {
+ if (newVal) {
+ userListNoPageByTenantId().then((res) => {
+ userList.value = res.data || [];
+ });
+ }
+});
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ getList();
+ startAutoRefresh();
+});
+
+// 寮�濮嬭嚜鍔ㄥ埛鏂�
+const startAutoRefresh = () => {
+ setInterval(() => {
+ getList();
+ }, 600000); // 10鍒嗛挓鍒锋柊涓�娆� (10 * 60 * 1000 = 600000ms)
+};
+
+// 鏌ヨ鏁版嵁
+const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ listKnowledgeBase({...page.value, ...searchForm.value})
+ .then(res => {
+ tableLoading.value = false;
+ page.value.total = res.data.total;
+ // 濡傛灉褰撳墠椤垫暟瓒呰繃鎬婚〉鏁帮紝閲嶇疆鍒扮1椤靛苟閲嶆柊鏌ヨ
+ const maxPage = Math.ceil(res.data.total / page.value.size) || 1;
+ if (page.value.current > maxPage && maxPage > 0) {
+ page.value.current = 1;
+ // 閲嶆柊鏌ヨ绗�1椤垫暟鎹�
+ return getList();
+ }
+ tableData.value = res.data.records;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+
+// 鍒嗛〉澶勭悊
+const pagination = (obj) => {
+ const oldSize = page.value.size;
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ // 濡傛灉 size 鏀瑰彉浜嗭紝閲嶇疆鍒扮1椤碉紝閬垮厤褰撳墠椤佃秴鍑鸿寖鍥�
+ if (oldSize !== obj.limit) {
+ page.value.current = 1;
+ }
+ getList();
+};
+
+// 閫夋嫨鍙樺寲澶勭悊
+const handleSelectionChange = (selection) => {
+ selectedIds.value = selection.map(item => item.id);
+};
+
+// 鎵撳紑琛ㄥ崟
+const openForm = (type, row = null) => {
+ dialogType.value = type;
+ if (type === "add") {
+ dialogTitle.value = "鏂板鐭ヨ瘑";
+ // 閲嶇疆琛ㄥ崟锛岄粯璁ゅ垱寤轰汉涓哄綋鍓嶇敤鎴�
+ Object.assign(form.value, {
+ title: "",
+ type: "",
+ scenario: "",
+ efficiency: "",
+ problem: "",
+ solution: "",
+ keyPoints: "",
+ creator: userStore.nickName || "",
+ usageCount: 0
+ });
+ } else if (type === "edit" && row) {
+ dialogTitle.value = "缂栬緫鐭ヨ瘑";
+ Object.assign(form.value, {
+ id: row.id,
+ title: row.title,
+ type: row.type,
+ scenario: row.scenario,
+ efficiency: row.efficiency,
+ problem: row.problem,
+ solution: row.solution,
+ keyPoints: row.keyPoints,
+ creator: row.creator,
+ usageCount: row.usageCount
+ });
+ }
+ dialogVisible.value = true;
+};
+
+// 鏌ョ湅鐭ヨ瘑璇︽儏
+const viewKnowledge = (row) => {
+ currentKnowledge.value = { ...row };
+ viewDialogVisible.value = true;
+};
+
+// 鑾峰彇绫诲瀷鏍囩绫诲瀷
+const getTypeTagType = (type) => {
+ const typeMap = {
+ contract: "success",
+ approval: "warning",
+ solution: "primary",
+ experience: "info",
+ guide: "danger"
+ };
+ return typeMap[type] || "info";
+};
+
+// 鑾峰彇绫诲瀷鏍囩鏂囨湰
+const getTypeLabel = (type) => {
+ return getKnowledgeTypeLabel(type);
+};
+
+// 鑾峰彇鏁堢巼鏍囩绫诲瀷
+const getEfficiencyTagType = (efficiency) => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info"
+ };
+ return typeMap[efficiency] || "info";
+};
+
+// 鑾峰彇鏁堢巼鏍囩鏂囨湰
+const getEfficiencyLabel = (efficiency) => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌"
+ };
+ return efficiencyMap[efficiency] || efficiency;
+};
+
+// 鑾峰彇鏁堢巼鎻愬崌鐧惧垎姣�
+const getEfficiencyScore = (efficiency) => {
+ const scoreMap = {
+ high: 40,
+ medium: 25,
+ low: 15
+ };
+ return scoreMap[efficiency] || 0;
+};
+
+// 鑾峰彇骞冲潎鑺傜渷鏃堕棿
+const getTimeSaved = (efficiency) => {
+ const timeMap = {
+ high: "2-3澶�",
+ medium: "1-2澶�",
+ low: "0.5-1澶�"
+ };
+ return timeMap[efficiency] || "鏈煡";
+};
+
+// 澶嶅埗鐭ヨ瘑
+const copyKnowledge = () => {
+ const knowledgeText = `
+ 鐭ヨ瘑鏍囬锛�${currentKnowledge.value.title}
+ 鐭ヨ瘑绫诲瀷锛�${getTypeLabel(currentKnowledge.value.type)}
+ 閫傜敤鍦烘櫙锛�${currentKnowledge.value.scenario}
+ 闂鎻忚堪锛�${currentKnowledge.value.problem}
+ 瑙e喅鏂规锛�${currentKnowledge.value.solution}
+ 鍏抽敭瑕佺偣锛�${currentKnowledge.value.keyPoints}
+ 鍒涘缓浜猴細${currentKnowledge.value.creator}
+ `.trim();
+
+ // 澶嶅埗鍒板壀璐存澘
+ navigator.clipboard.writeText(knowledgeText).then(() => {
+ ElMessage.success("鐭ヨ瘑鍐呭宸插鍒跺埌鍓创鏉�");
+ }).catch(() => {
+ ElMessage.error("澶嶅埗澶辫触锛岃鎵嬪姩澶嶅埗");
+ });
+};
+
+// 鍏抽棴鐭ヨ瘑琛ㄥ崟瀵硅瘽妗�
+const closeKnowledgeDialog = () => {
+ // 娓呯┖琛ㄥ崟鏁版嵁锛岄粯璁ゅ垱寤轰汉涓哄綋鍓嶇敤鎴�
+ Object.assign(form.value, {
+ id: undefined,
+ title: "",
+ type: "",
+ scenario: "",
+ efficiency: "",
+ problem: "",
+ solution: "",
+ keyPoints: "",
+ creator: userStore.nickName || "",
+ usageCount: 0
+ });
+ // 娓呴櫎琛ㄥ崟楠岃瘉鐘舵��
+ if (formRef.value) {
+ formRef.value.clearValidate();
+ }
+ dialogVisible.value = false;
+};
+
+// 鍏抽棴鏌ョ湅璇︽儏瀵硅瘽妗�
+const closeViewDialog = () => {
+ viewDialogVisible.value = false;
+};
+
+// 澶勭悊鏌ョ湅璇︽儏瀵硅瘽妗嗙‘璁わ紙鎵ц澶嶅埗鎿嶄綔锛�
+const handleViewDialogConfirm = () => {
+ copyKnowledge();
+ closeViewDialog();
+};
+
+// 鎻愪氦鐭ヨ瘑琛ㄥ崟
+const submitForm = async () => {
+ try {
+ await formRef.value.validate();
+ if (dialogType.value === "add") {
+ // 鏂板鐭ヨ瘑
+ addKnowledgeBase({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ closeKnowledgeDialog();
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } else {
+ updateKnowledgeBase({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ closeKnowledgeDialog();
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+};
+
+// 鍒犻櫎鐭ヨ瘑
+const handleDelete = () => {
+ if (selectedIds.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑鐭ヨ瘑");
+ return;
+ }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ // console.log(selectedIds.value);
+ delKnowledgeBase(selectedIds.value).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ selectedIds.value = [];
+ getList();
+ }
+ })
+ }).catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+};
+
+// 瀵煎嚭
+const { proxy } = getCurrentInstance()
+const { knowledge_type } = proxy.useDict("knowledge_type")
+
+// 瀛楀吀宸ュ叿
+const knowledgeTypeOptions = computed(() => knowledge_type?.value || [])
+const getKnowledgeTypeLabel = (val) => {
+ const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val))
+ return item ? item.label : val
+}
+const getKnowledgeTypeTagType = (val) => {
+ const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val))
+ return item?.elTagType || "info"
+}
+const handleExport = () => {
+ proxy.download('/knowledgeBase/export', { ...searchForm.value }, '鐭ヨ瘑搴�.xlsx')
+}
+</script>
+
+<style scoped>
+.auto-refresh-info {
+ margin-bottom: 15px;
+}
+
+.auto-refresh-info .el-alert {
+ border-radius: 8px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+
+.knowledge-detail {
+ padding: 20px 0;
+}
+
+.detail-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.detail-section {
+ margin-top: 24px;
+}
+
+.detail-section h4 {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+}
+
+.detail-content {
+ background: #f8f9fa;
+ padding: 16px;
+ border-radius: 6px;
+ line-height: 1.6;
+ color: #606266;
+ white-space: pre-wrap;
+}
+
+.key-points {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.usage-stats {
+ margin-top: 16px;
+}
+
+.stat-item {
+ text-align: center;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+}
+
+.stat-number {
+ font-size: 24px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 8px;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #909399;
+}
+</style>
diff --git a/src/views/collaborativeApproval/meetingBoard/index.vue b/src/views/collaborativeApproval/meetingBoard/index.vue
new file mode 100644
index 0000000..ebedd1f
--- /dev/null
+++ b/src/views/collaborativeApproval/meetingBoard/index.vue
@@ -0,0 +1,344 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <h2>浼氳鐪嬫澘</h2>
+<!-- <el-button type="primary" @click="createMeeting">鍒涘缓浼氳</el-button>-->
+ </div>
+
+ <!-- 浼氳缁熻鍗$墖 -->
+ <div class="stats-cards">
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-number">{{ stats.total }}</div>
+ <div class="stat-label">鎬讳細璁暟</div>
+ </div>
+ </el-card>
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-number">{{ stats.underWay }}</div>
+ <div class="stat-label">杩涜涓�</div>
+ </div>
+ </el-card>
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-number">{{ stats.completed }}</div>
+ <div class="stat-label">宸插畬鎴�</div>
+ </div>
+ </el-card>
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-number">{{ stats.toStart }}</div>
+ <div class="stat-label">鍗冲皢寮�濮�</div>
+ </div>
+ </el-card>
+ </div>
+
+ <!-- 浼氳鍒楄〃 -->
+ <div class="meeting-list">
+ <el-card v-for="meeting in meetings" :key="meeting.id" class="meeting-card">
+ <div class="meeting-header">
+ <div class="meeting-title">
+ <h3>{{ meeting.title }}</h3>
+ <el-tag :type="getStatusType(meeting.status)" size="small">
+ {{ getStatusText(meeting.status) }}
+ </el-tag>
+ </div>
+ <div class="meeting-time">
+ {{dayjs(meeting.startTime).format("YYYY-MM-DD")}}<el-icon><Clock /></el-icon>
+ {{ formatTime(meeting.startTime) }} - {{ formatTime(meeting.endTime) }}
+ </div>
+ </div>
+
+ <div class="meeting-info">
+ <div class="info-item">
+ <el-icon><Location /></el-icon>
+ <span>{{ meeting.location }}</span>
+ </div>
+ <div class="info-item">
+ <el-icon><User /></el-icon>
+ <span>涓绘寔浜�: {{ meeting.host }}</span>
+ </div>
+ <div class="info-item">
+ <el-icon><UserFilled /></el-icon>
+ <span>鍙備細浜烘暟: {{ meeting.participants.length }}浜�</span>
+ </div>
+ </div>
+
+ <div class="meeting-agenda">
+ <h4>浼氳绾</h4>
+ <div class="agenda-list">
+ <div class="editor-container">
+ <div
+ v-html="meeting.content"
+ />
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </div>
+
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { Clock, Location, User, UserFilled } from '@element-plus/icons-vue'
+import {getMeetSummaryItems,getMeetSummary} from '@/api/collaborativeApproval/meeting.js'
+import dayjs from "dayjs";
+
+// 缁熻鏁版嵁
+const stats = ref({
+ total: 0,
+ underWay: 0,
+ completed: 0,
+ toStart: 0
+})
+
+// 浼氳鏁版嵁
+const meetings = ref([
+
+])
+
+// 瀵硅瘽妗嗙浉鍏�
+const dialogVisible = ref(false)
+const meetingForm = reactive({
+ title: '',
+ timeRange: [],
+ location: '',
+ host: '',
+ description: ''
+})
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const statusMap = {
+ '2': 'success',
+ '1': 'warning',
+ '0': 'info'
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ '2': '杩涜涓�',
+ '1': '鍗冲皢寮�濮�',
+ '0': '宸插畬鎴�'
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+// 鑾峰彇璁▼鐘舵�佺被鍨�
+const getAgendaStatusType = (status) => {
+ const statusMap = {
+ 'completed': 'success',
+ 'active': 'warning',
+ 'pending': 'info'
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇璁▼鐘舵�佹枃鏈�
+const getAgendaStatusText = (status) => {
+ const statusMap = {
+ 'completed': '宸插畬鎴�',
+ 'active': '杩涜涓�',
+ 'pending': '寰呭紑濮�'
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+// 鏍煎紡鍖栨椂闂�
+const formatTime = (timeStr) => {
+ const date = new Date(timeStr)
+ return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
+}
+
+
+onMounted( async () => {
+ let [resp1,resp2] = await Promise.all([getMeetSummary(),getMeetSummaryItems()])
+ stats.value = resp1.data
+ meetings.value = resp2.data.map(item => {
+ return {
+ ...item,
+ participants: JSON.parse(item.participants)
+ }
+ })
+ console.log('浼氳鐪嬫澘椤甸潰鍔犺浇瀹屾垚')
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.stats-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.stat-card {
+ text-align: center;
+}
+
+.stat-content {
+ padding: 10px;
+}
+
+.stat-number {
+ font-size: 32px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 8px;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #606266;
+}
+
+.meeting-list {
+ display: grid;
+ gap: 20px;
+}
+
+.meeting-card {
+ border-radius: 8px;
+}
+
+.meeting-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 15px;
+}
+
+.meeting-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.meeting-title h3 {
+ margin: 0;
+ color: #303133;
+}
+
+.meeting-time {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: #606266;
+ font-size: 14px;
+}
+
+.meeting-info {
+ display: flex;
+ gap: 20px;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+}
+
+.info-item {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: #606266;
+ font-size: 14px;
+}
+
+.meeting-agenda {
+ margin-bottom: 20px;
+}
+
+.meeting-agenda h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+ font-size: 16px;
+}
+
+.agenda-list {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.agenda-item {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ padding: 10px;
+ border-radius: 6px;
+ background-color: #f5f7fa;
+}
+
+.agenda-item.active {
+ background-color: #fdf6ec;
+ border-left: 3px solid #e6a23c;
+}
+
+.agenda-item.completed {
+ background-color: #f0f9ff;
+ border-left: 3px solid #409eff;
+}
+
+.agenda-time {
+ font-weight: bold;
+ color: #606266;
+ min-width: 80px;
+}
+
+.agenda-content {
+ flex: 1;
+ color: #303133;
+}
+
+.meeting-actions {
+ display: flex;
+ gap: 10px;
+ justify-content: flex-end;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+@media (max-width: 768px) {
+ .stats-cards {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .meeting-header {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .meeting-info {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .meeting-actions {
+ flex-direction: column;
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/meetingManagement/index.vue b/src/views/collaborativeApproval/meetingManagement/index.vue
new file mode 100644
index 0000000..69b0275
--- /dev/null
+++ b/src/views/collaborativeApproval/meetingManagement/index.vue
@@ -0,0 +1,63 @@
+<template>
+ <div class="app-container">
+ <div class="tabs-wrapper">
+ <el-tabs
+ v-model="activeTab"
+ class="meeting-tabs"
+ @tab-change="handleTabChange"
+ >
+ <el-tab-pane label="浼氳璁剧疆" name="setting" />
+ <el-tab-pane label="浼氳鍒楄〃" name="index" />
+ <el-tab-pane label="浼氳鐢宠" name="application" />
+ <el-tab-pane label="浼氳瀹℃壒" name="examine" />
+ <el-tab-pane label="浼氳鍙戝竷" name="publish" />
+ <el-tab-pane label="浼氳鎬荤粨" name="summary" />
+ </el-tabs>
+ </div>
+
+ <div class="tab-content">
+ <keep-alive>
+ <component :is="currentComponent" />
+ </keep-alive>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import MeetSetting from '../notificationManagement/meetSetting/index.vue'
+import MeetIndex from '../notificationManagement/meetIndex/index.vue'
+import MeetApplication from '../notificationManagement/meetApplication/index.vue'
+import MeetExamine from '../notificationManagement/meetExamine/index.vue'
+import MeetPublish from '../notificationManagement/meetPublish/index.vue'
+import MeetSummary from '../notificationManagement/summary/index.vue'
+
+const activeTab = ref('setting')
+
+const tabComponentMap = {
+ setting: MeetSetting,
+ index: MeetIndex,
+ application: MeetApplication,
+ examine: MeetExamine,
+ publish: MeetPublish,
+ summary: MeetSummary
+}
+
+const currentComponent = computed(() => tabComponentMap[activeTab.value] || MeetSetting)
+
+function handleTabChange(name) {
+ activeTab.value = name
+}
+</script>
+
+<style scoped lang="scss">
+
+.tabs-wrapper {
+ margin-bottom: 10px;
+}
+
+.tab-content {
+ min-height: 400px;
+}
+</style>
+
diff --git a/src/views/collaborativeApproval/noticeManagement/index.vue b/src/views/collaborativeApproval/noticeManagement/index.vue
new file mode 100644
index 0000000..a36b950
--- /dev/null
+++ b/src/views/collaborativeApproval/noticeManagement/index.vue
@@ -0,0 +1,960 @@
+<template>
+ <div class="app-container">
+ <!-- 鎼滅储琛ㄥ崟 -->
+ <div class="search_form">
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板鍏憡</el-button>
+ <el-button type="info" @click="openNoticeTypeDialog">鍏憡绫诲瀷閰嶇疆</el-button>
+ </div>
+ </div>
+
+ <!-- 閫氱煡鍏憡鏉� -->
+ <div class="notice-board">
+ <el-tabs v-model="activeNoticeTypeTab" @tab-change="handleNoticeTypeTabChange">
+ <el-tab-pane
+ v-for="noticeType in noticeTypeList"
+ :key="noticeType.id"
+ :label="noticeType.noticeType"
+ :name="String(noticeType.id)"
+ >
+ <template #label>
+ <span>{{ noticeType.noticeType }}
+ <span class="tab-count" v-if="getNoticeCountByType(noticeType.id) > 0">
+ ({{ getNoticeCountByType(noticeType.id) }})
+ </span>
+ </span>
+ </template>
+
+ <div class="notice-section">
+ <div class="notice-cards">
+ <div
+ v-for="notice in getNoticesByType(noticeType.id)"
+ :key="notice.id"
+ class="notice-card"
+ :class="{ 'urgent': notice.priority === '3' }"
+ >
+ <div class="card-header">
+ <div class="card-title">
+ <el-icon class="notice-icon">
+ <Calendar/>
+ </el-icon>
+ {{ notice.title }}
+ </div>
+ <div class="card-actions">
+ <el-button link type="primary" @click="handleEdit(notice)" :disabled="isNoticeExpired(notice)" v-if="notice.status !== 1">缂栬緫</el-button>
+ <el-button link type="success" @click="handlePublish(notice)" v-if="notice.status === 0">鍙戝竷</el-button>
+ <el-button link type="danger" @click="handleDelete(notice.id)" v-if="notice.status !== 1">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="card-content">
+ <p>{{ notice.content }}</p>
+ </div>
+ <div class="card-footer">
+ <div class="card-meta">
+ <span class="priority" :class="'priority-' + notice.priority">
+ {{ getPriorityText(notice.priority) }}
+ </span>
+ <span class="status" :class="'status-' + getNoticeStatus(notice)">
+ {{ getStatusText(getNoticeStatus(notice)) }}
+ </span>
+ </div>
+ <div class="card-info">
+ <span class="creator">{{ notice.createUserName }}</span>
+ <span class="expiration" v-if="notice.expirationDate">鎴鏃ユ湡锛歿{ notice.expirationDate }}</span>
+ </div>
+ </div>
+ <div class="card-remark" v-if="notice.remark">
+ <el-icon>
+ <InfoFilled/>
+ </el-icon>
+ <span>{{ notice.remark }}</span>
+ </div>
+ </div>
+ </div>
+
+ <pagination
+ v-if="getNoticePageByType(noticeType.id).total > 0"
+ :total="getNoticePageByType(noticeType.id).total"
+ :page="getNoticePageByType(noticeType.id).current"
+ :limit="getNoticePageByType(noticeType.id).size"
+ @pagination="(val) => handleNoticeCurrentChange(noticeType.id, val)"
+ />
+
+ <!-- 绌虹姸鎬� -->
+ <div class="empty-state" v-if="getNoticesByType(noticeType.id).length === 0">
+ <el-empty description="鏆傛棤閫氱煡鍏憡"/>
+ </div>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ append-to-body
+ @close="resetForm"
+ >
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍏憡鏍囬" prop="title">
+ <el-input v-model="form.title" placeholder="璇疯緭鍏ュ叕鍛婃爣棰�"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏憡绫诲瀷" prop="type">
+ <el-select v-model="form.type" placeholder="璇烽�夋嫨鍏憡绫诲瀷" style="width: 100%">
+ <el-option
+ v-for="item in noticeTypeList"
+ :key="item.id"
+ :label="item.noticeType"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio :value="0">鑽夌</el-radio>
+ <el-radio :value="1">姝e紡鍙戝竷</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浼樺厛绾�">
+ <el-select v-model="form.priority" placeholder="璇烽�夋嫨浼樺厛绾�" style="width: 100%">
+ <el-option label="鏅��" :value="1"/>
+ <el-option label="閲嶈" :value="2"/>
+ <el-option label="绱ф��" :value="3"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="杩囨湡鏃堕棿" prop="expirationDate">
+ <el-date-picker v-model="form.expirationDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="date"
+ placeholder="璇烽�夋嫨" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鍏憡鍐呭" prop="content">
+ <el-input
+ v-model="form.content"
+ type="textarea"
+ :rows="6"
+ placeholder="璇疯緭鍏ュ叕鍛婂唴瀹�"
+ maxlength="500"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞">
+ <el-input
+ v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�"
+ maxlength="200"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 鍏憡绫诲瀷閰嶇疆寮规 -->
+ <el-dialog
+ v-model="noticeTypeDialogVisible"
+ title="鍏憡绫诲瀷閰嶇疆"
+ width="800px"
+ @close="handleNoticeTypeDialogClose"
+ >
+ <div class="notice-type-container">
+ <div class="notice-type-header">
+ <el-button type="primary" @click="handleAddNoticeType">鏂板绫诲瀷</el-button>
+ </div>
+ <el-table :data="noticeTypeList" border style="width: 100%">
+ <el-table-column prop="id" label="ID" width="80" align="center"/>
+ <el-table-column prop="noticeType" label="鍏憡绫诲瀷" align="center">
+ <template #default="scope">
+ <el-input
+ v-if="scope.row.editing"
+ v-model="scope.row.noticeType"
+ placeholder="璇疯緭鍏ュ叕鍛婄被鍨�"
+ />
+ <span v-else>{{ scope.row.noticeType }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.editing"
+ link
+ type="primary"
+ @click="handleSaveNoticeType(scope.row)"
+ >
+ 淇濆瓨
+ </el-button>
+ <el-button
+ v-if="scope.row.editing"
+ link
+ type="info"
+ @click="handleCancelEdit(scope.row)"
+ >
+ 鍙栨秷
+ </el-button>
+ <el-button
+ v-if="!scope.row.editing"
+ link
+ type="primary"
+ @click="handleEditNoticeType(scope.row)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ v-if="!scope.row.editing"
+ link
+ type="danger"
+ @click="handleDeleteNoticeType(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {Calendar, InfoFilled} from "@element-plus/icons-vue";
+import {onMounted, ref, reactive, toRefs, computed} from "vue";
+import {ElMessage, ElMessageBox} from "element-plus";
+import {useRoute} from "vue-router";
+import useUserStore from "@/store/modules/user";
+import {
+ addNotice,
+ delNotice,
+ getCount,
+ listNotice,
+ updateNotice,
+ listNoticeType,
+ addNoticeType,
+ delNoticeType
+} from "../../../api/collaborativeApproval/noticeManagement.js";
+import pagination from "../../../components/PIMTable/Pagination.vue";
+
+const userStore = useUserStore();
+const route = useRoute();
+
+// 鍝嶅簲寮忔暟鎹�
+const data = reactive({
+ searchForm: {
+ title: "",
+ type: undefined,
+ status: undefined,
+ },
+ form: {
+ id: undefined,
+ title: "",
+ type: null,
+ content: "",
+ status: 0,
+ priority: 1,
+ remark: "",
+ expirationDate: "",
+ },
+ rules: {
+ title: [
+ {required: true, message: "鍏憡鏍囬涓嶈兘涓虹┖", trigger: "blur"}
+ ],
+ type: [
+ {required: true, message: "璇烽�夋嫨鍏憡绫诲瀷", trigger: "change"}
+ ],
+ content: [
+ {required: true, message: "鍏憡鍐呭涓嶈兘涓虹┖", trigger: "blur"}
+ ],
+ expirationDate: [
+ {required: true, message: "璇烽�夋嫨鏃ユ湡", trigger: "change"}
+ ]
+ }
+});
+
+const {searchForm, form, rules} = toRefs(data);
+
+// 椤甸潰鐘舵��
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref();
+
+// 鍏憡绫诲瀷閰嶇疆鐩稿叧
+const noticeTypeDialogVisible = ref(false);
+const noticeTypeList = ref([]);
+const activeNoticeTypeTab = ref('');
+
+// 閫氱煡鏁版嵁 - 浣跨敤 Map 瀛樺偍锛宬ey 涓虹被鍨� id
+const noticesMap = ref({});
+const noticePagesMap = ref({});
+
+
+// 璁$畻灞炴��
+const filteredNotices = computed(() => {
+ let filtered = [...mockData];
+
+ if (searchForm.value.noticeTitle) {
+ filtered = filtered.filter(item =>
+ item.noticeTitle.includes(searchForm.value.noticeTitle)
+ );
+ }
+ if (searchForm.value.noticeType) {
+ filtered = filtered.filter(item =>
+ item.noticeType === searchForm.value.noticeType
+ );
+ }
+ if (searchForm.value.status !== "") {
+ filtered = filtered.filter(item =>
+ item.status === searchForm.value.status
+ );
+ }
+
+ return filtered;
+});
+
+// 鏂规硶瀹氫箟
+const handleQuery = () => {
+ // 鎼滅储鍔熻兘淇濇寔涓嶅彉锛屼絾鏁版嵁閫氳繃璁$畻灞炴�ц嚜鍔ㄨ繃婊�
+};
+
+const resetQuery = () => {
+ searchForm.value = {
+ title: "",
+ type: "",
+ status: ""
+ };
+};
+
+const getPriorityText = (priority) => {
+ const priorityMap = {"1": "鏅��", "2": "閲嶈", "3": "绱ф��"};
+ return priorityMap[priority] || "鏅��";
+};
+
+const getStatusText = (status) => {
+ const statusMap = {"0": "鑽夌", "1": "宸插彂甯�", "2": "宸茶繃鏈�"};
+ return statusMap[status] || "鏈煡";
+};
+
+const isNoticeExpired = (notice) => {
+ if (!notice || !notice.expirationDate) {
+ return false;
+ }
+
+ const expiration = new Date(notice.expirationDate);
+
+ if (Number.isNaN(expiration.getTime())) {
+ return false;
+ }
+
+ expiration.setHours(23, 59, 59, 999);
+
+ return new Date() > expiration;
+};
+
+const getNoticeStatus = (notice) => {
+ const normalizedStatus = notice && notice.status !== undefined && notice.status !== null
+ ? String(notice.status)
+ : "0";
+
+ return isNoticeExpired(notice) ? "2" : normalizedStatus;
+};
+
+const openForm = (type) => {
+ if (type === 'add') {
+ dialogTitle.value = "鏂板鍏憡";
+ form.value = {
+ id: undefined,
+ title: "",
+ type: undefined,
+ content: "",
+ status: 0,
+ priority: 1,
+ remark: "",
+ expirationDate: "",
+ };
+ }
+ dialogVisible.value = true;
+};
+
+const handleEdit = (row) => {
+ if (isNoticeExpired(row)) {
+ ElMessage.warning("宸茶繃鏈熺殑鍏憡涓嶅彲缂栬緫");
+ return;
+ }
+ dialogTitle.value = "缂栬緫鍏憡";
+ form.value = {...row};
+ dialogVisible.value = true;
+};
+
+const handleDelete = (id) => {
+ ElMessageBox.confirm(
+ "纭鍒犻櫎杩欐潯鍏憡鍚楋紵",
+ "鎻愮ず",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }
+ ).then(() => {
+ delNotice(id).then(res => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ resetTable()
+ })
+ });
+};
+
+const handlePublish = (notice) => {
+ ElMessageBox.confirm(
+ "纭鍙戝竷杩欐潯鍏憡鍚楋紵",
+ "鎻愮ず",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "info"
+ }
+ ).then(() => {
+ updateNotice({
+ ...notice,
+ status: 1
+ }).then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍙戝竷鎴愬姛");
+ resetTable()
+ }
+ })
+ });
+};
+
+const submitForm = () => {
+ formRef.value.validate((valid) => {
+ if (valid) {
+ if (form.value.id) {
+ // 缂栬緫妯″紡
+ updateNotice(form.value).then(res => {
+ ElMessage.success("淇敼鎴愬姛");
+ resetTable()
+ })
+ } else {
+ // 鏂板妯″紡
+ addNotice(form.value).then(res => {
+ ElMessage.success("鏂板鎴愬姛");
+ resetTable()
+ })
+ }
+ dialogVisible.value = false;
+ }
+ });
+};
+
+// 鍒濆鍖栨煇涓被鍨嬬殑鍒嗛〉鏁版嵁
+const initNoticePage = (typeId) => {
+ if (!noticePagesMap.value[typeId]) {
+ noticePagesMap.value[typeId] = {
+ total: 0,
+ current: 1,
+ size: 10
+ };
+ }
+ if (!noticesMap.value[typeId]) {
+ noticesMap.value[typeId] = [];
+ }
+};
+
+// 鑾峰彇鏌愪釜绫诲瀷鐨勯�氱煡鍒楄〃
+const getNoticesByType = (typeId) => {
+ return noticesMap.value[typeId] || [];
+};
+
+// 鑾峰彇鏌愪釜绫诲瀷鐨勫垎椤垫暟鎹�
+const getNoticePageByType = (typeId) => {
+ return noticePagesMap.value[typeId] || { total: 0, current: 1, size: 10 };
+};
+
+// 鑾峰彇鏌愪釜绫诲瀷鐨勬暟閲�
+const getNoticeCountByType = (typeId) => {
+ return getNoticePageByType(typeId).total || 0;
+};
+
+// 鑾峰彇鏌愪釜绫诲瀷鐨勯�氱煡鏁版嵁
+const fetchNoticesByType = (typeId) => {
+ initNoticePage(typeId);
+ const pageData = noticePagesMap.value[typeId];
+ listNotice({...pageData, type: typeId}).then(res => {
+ if (res.code === 200) {
+ noticesMap.value[typeId] = res.data.records || [];
+ noticePagesMap.value[typeId].total = res.data.total || 0;
+ }
+ });
+};
+
+// 澶勭悊鍒嗛〉鍙樺寲
+const handleNoticeCurrentChange = (typeId, val) => {
+ initNoticePage(typeId);
+ noticePagesMap.value[typeId].size = val.limit;
+ noticePagesMap.value[typeId].current = val.page;
+ fetchNoticesByType(typeId);
+};
+
+// 澶勭悊 tab 鍒囨崲
+const handleNoticeTypeTabChange = (tabName) => {
+ activeNoticeTypeTab.value = tabName;
+ const typeId = Number(tabName);
+ fetchNoticesByType(typeId);
+};
+
+const resetTable = () => {
+ // 閲嶇疆鎵�鏈夌被鍨嬬殑鍒嗛〉骞堕噸鏂拌幏鍙栨暟鎹�
+ noticeTypeList.value.forEach(type => {
+ initNoticePage(type.id);
+ noticePagesMap.value[type.id].current = 1;
+ noticePagesMap.value[type.id].size = 10;
+ fetchNoticesByType(type.id);
+ });
+};
+
+const resetForm = () => {
+ formRef.value?.resetFields();
+};
+
+// 鍏憡绫诲瀷閰嶇疆鐩稿叧鏂规硶
+const openNoticeTypeDialog = () => {
+ noticeTypeDialogVisible.value = true;
+ fetchNoticeTypeList();
+};
+
+const fetchNoticeTypeList = () => {
+ return listNoticeType().then(res => {
+ if (res.code === 200) {
+ noticeTypeList.value = res.data.map(item => ({
+ ...item,
+ editing: false
+ }));
+
+ // 妫�鏌ヨ矾鐢卞弬鏁颁腑鐨� type
+ const routeType = route.query.type;
+ let targetTypeId = null;
+
+ if (routeType) {
+ // 濡傛灉璺敱鍙傛暟涓湁 type锛屾煡鎵惧搴旂殑绫诲瀷
+ const typeId = Number(routeType);
+ const foundType = noticeTypeList.value.find(item => item.id === typeId);
+ if (foundType) {
+ targetTypeId = typeId;
+ }
+ }
+
+ // 濡傛灉鏈夌被鍨嬫暟鎹�
+ if (noticeTypeList.value.length > 0) {
+ // 濡傛灉璺敱鍙傛暟鎸囧畾浜嗙被鍨嬩笖瀛樺湪锛屼娇鐢ㄨ矾鐢卞弬鏁扮殑绫诲瀷
+ // 鍚﹀垯濡傛灉娌℃湁閫変腑 tab锛岄粯璁ら�変腑绗竴涓�
+ if (targetTypeId !== null) {
+ activeNoticeTypeTab.value = String(targetTypeId);
+ fetchNoticesByType(targetTypeId);
+ } else if (!activeNoticeTypeTab.value) {
+ activeNoticeTypeTab.value = String(noticeTypeList.value[0].id);
+ fetchNoticesByType(noticeTypeList.value[0].id);
+ }
+ }
+ }
+ });
+};
+
+const handleAddNoticeType = () => {
+ const newItem = {
+ id: undefined,
+ noticeType: '',
+ editing: true
+ };
+ noticeTypeList.value.push(newItem);
+};
+
+const handleEditNoticeType = (row) => {
+ // 淇濆瓨鍘熷鍊�
+ row.originalNoticeType = row.noticeType;
+ row.editing = true;
+};
+
+const handleSaveNoticeType = (row) => {
+ if (!row.noticeType || row.noticeType.trim() === '') {
+ ElMessage.warning('鍏憡绫诲瀷涓嶈兘涓虹┖');
+ return;
+ }
+
+ const data = {
+ noticeType: row.noticeType.trim()
+ };
+
+ if (row.id) {
+ // 缂栬緫妯″紡 - 浼犲叆id
+ data.id = row.id;
+ }
+
+ addNoticeType(data).then(res => {
+ if (res.code === 200) {
+ ElMessage.success(row.id ? '缂栬緫鎴愬姛' : '鏂板鎴愬姛');
+ row.editing = false;
+ delete row.originalNoticeType;
+ fetchNoticeTypeList().then(() => {
+ // 濡傛灉褰撳墠閫変腑鐨勭被鍨嬭缂栬緫锛岄渶瑕侀噸鏂拌幏鍙栨暟鎹�
+ if (row.id && activeNoticeTypeTab.value === String(row.id)) {
+ fetchNoticesByType(res.data?.id || row.id);
+ }
+ });
+ }
+ });
+};
+
+const handleDeleteNoticeType = (row) => {
+ // 濡傛灉娌℃湁id锛岃鏄庢槸鏂板浣嗘湭淇濆瓨鐨勮锛岀洿鎺ヤ粠鍓嶇鍒犻櫎
+ if (!row.id) {
+ const index = noticeTypeList.value.indexOf(row);
+ if (index > -1) {
+ noticeTypeList.value.splice(index, 1);
+ }
+ return;
+ }
+
+ // 濡傛灉鏈塱d锛岃皟鐢ㄥ悗绔帴鍙e垹闄�
+ ElMessageBox.confirm(
+ "纭鍒犻櫎杩欎釜鍏憡绫诲瀷鍚楋紵",
+ "鎻愮ず",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }
+ ).then(() => {
+ delNoticeType(row.id).then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ // 濡傛灉鍒犻櫎鐨勬槸褰撳墠閫変腑鐨勭被鍨嬶紝鍒囨崲鍒扮涓�涓被鍨�
+ if (activeNoticeTypeTab.value === String(row.id)) {
+ fetchNoticeTypeList().then(() => {
+ if (noticeTypeList.value.length > 0) {
+ activeNoticeTypeTab.value = String(noticeTypeList.value[0].id);
+ fetchNoticesByType(noticeTypeList.value[0].id);
+ } else {
+ activeNoticeTypeTab.value = '';
+ }
+ });
+ } else {
+ fetchNoticeTypeList();
+ }
+ }
+ });
+ });
+};
+
+const handleCancelEdit = (row) => {
+ if (!row.id) {
+ // 濡傛灉鏄柊澧炰絾鏈繚瀛樼殑琛岋紝绉婚櫎瀹�
+ const index = noticeTypeList.value.indexOf(row);
+ if (index > -1) {
+ noticeTypeList.value.splice(index, 1);
+ }
+ } else {
+ // 濡傛灉鏄紪杈戜腑鐨勮锛屽彇娑堢紪杈戠姸鎬佸苟鎭㈠鍘熷��
+ row.editing = false;
+ if (row.originalNoticeType !== undefined) {
+ row.noticeType = row.originalNoticeType;
+ delete row.originalNoticeType;
+ }
+ }
+};
+
+const handleNoticeTypeDialogClose = () => {
+ // 鍏抽棴寮规鏃讹紝鍙栨秷鎵�鏈夌紪杈戠姸鎬�
+ noticeTypeList.value.forEach(item => {
+ if (item.editing && !item.id) {
+ // 濡傛灉鏄柊澧炰絾鏈繚瀛樼殑琛岋紝绉婚櫎瀹�
+ const index = noticeTypeList.value.indexOf(item);
+ if (index > -1) {
+ noticeTypeList.value.splice(index, 1);
+ }
+ } else if (item.editing) {
+ // 濡傛灉鏄紪杈戜腑鐨勮锛屽彇娑堢紪杈戠姸鎬佸苟鎭㈠鍘熷��
+ item.editing = false;
+ if (item.originalNoticeType !== undefined) {
+ item.noticeType = item.originalNoticeType;
+ delete item.originalNoticeType;
+ }
+ }
+ });
+};
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ // 鍏堣幏鍙栧叕鍛婄被鍨嬪垪琛紝鐒跺悗鏍规嵁绫诲瀷鑾峰彇閫氱煡鏁版嵁
+ fetchNoticeTypeList();
+});
+</script>
+
+<style scoped>
+.search_form {
+ background: #fff;
+ padding: 20px;
+ margin-bottom: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.search_title {
+ font-weight: 500;
+ color: #333;
+ margin-right: 8px;
+}
+
+.ml10 {
+ margin-left: 10px;
+}
+
+.notice-board {
+ background: #f5f7fa;
+ padding: 20px;
+ border-radius: 8px;
+}
+
+.notice-section {
+ margin-bottom: 30px;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+ padding: 0 10px;
+}
+
+.section-header h3 {
+ margin: 0;
+ color: #303133;
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.section-count {
+ margin-left: 10px;
+ background: #409eff;
+ color: white;
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+}
+
+.notice-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
+ gap: 20px;
+}
+
+.notice-card {
+ background: white;
+ border-radius: 12px;
+ padding: 20px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ border-left: 4px solid transparent;
+}
+
+.notice-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+}
+
+.notice-icon {
+ color: #409eff;
+ margin-right: 8px;
+ font-size: 18px;
+}
+
+.tab-count {
+ color: #909399;
+ font-size: 12px;
+ margin-left: 4px;
+}
+
+.urgent {
+ border-left-color: #f56c6c;
+ background: linear-gradient(135deg, #fff5f5 0%, #ffffff 100%);
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 15px;
+}
+
+.card-title {
+ display: flex;
+ align-items: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ flex: 1;
+}
+
+
+.card-actions {
+ display: flex;
+ gap: 8px;
+}
+
+.card-content {
+ margin-bottom: 15px;
+}
+
+.card-content p {
+ margin: 0;
+ color: #606266;
+ line-height: 1.6;
+ font-size: 14px;
+ word-break: break-all;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+}
+
+.card-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10px;
+}
+
+.card-meta {
+ display: flex;
+ gap: 8px;
+}
+
+.priority, .status {
+ padding: 2px 8px;
+ border-radius: 12px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.priority-1 {
+ background: #f0f9ff;
+ color: #0369a1;
+}
+
+.priority-2 {
+ background: #fef3c7;
+ color: #d97706;
+}
+
+.priority-3 {
+ background: #fef2f2;
+ color: #dc2626;
+}
+
+.status-0 {
+ background: #f3f4f6;
+ color: #6b7280;
+}
+
+.status-1 {
+ background: #d1fae5;
+ color: #059669;
+}
+
+.status-2 {
+ background: #fef3c7;
+ color: #d97706;
+}
+
+.card-info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ font-size: 12px;
+ color: #909399;
+}
+
+.creator {
+ font-weight: 500;
+ margin-bottom: 2px;
+}
+
+.expiration {
+ margin-top: 2px;
+}
+
+.card-remark {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ font-size: 12px;
+ color: #606266;
+ border-left: 3px solid #409eff;
+}
+
+.empty-state {
+ text-align: center;
+ padding: 60px 20px;
+}
+
+.dialog-footer {
+ text-align: center;
+}
+
+.notice-type-container {
+ padding: 10px 0;
+}
+
+.notice-type-header {
+ margin-bottom: 15px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .notice-cards {
+ grid-template-columns: 1fr;
+ }
+
+ .search_form {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .search_form > div {
+ width: 100%;
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/index.vue b/src/views/collaborativeApproval/notificationManagement/index.vue
new file mode 100644
index 0000000..fa02f47
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/index.vue
@@ -0,0 +1,1200 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">閫氱煡鏍囬锛�</span>
+ <el-input
+ v-model="searchForm.title"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ラ�氱煡鏍囬鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <span class="search_title ml10">閫氱煡绫诲瀷锛�</span>
+ <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px">
+ <el-option label="鏀惧亣閫氱煡" :value="'holiday'" />
+ <el-option label="澶勭綒閫氱煡" :value="'penalty'" />
+ <el-option label="寮�浼氶�氱煡" :value="'meeting'" />
+ <el-option label="涓存椂閫氱煡" :value="'temporary'" />
+ <el-option label="姝e紡閫氱煡" :value="'formal'" />
+ </el-select>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板閫氱煡</el-button>
+ <el-button type="success" @click="openMeetingDialog">鍦ㄧ嚎浼氳</el-button>
+ <el-button type="warning" @click="openFileShareDialog">鏂囦欢鍏变韩</el-button>
+ <!-- <el-button type="info" @click="refreshEmployees">鍒锋柊鍛樺伐</el-button> -->
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+
+ <!-- 鏂板/缂栬緫閫氱煡寮圭獥 -->
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="800px"
+ :close-on-click-modal="false"
+ >
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閫氱煡鏍囬" prop="title">
+ <el-input v-model="form.title" placeholder="璇疯緭鍏ラ�氱煡鏍囬" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閫氱煡绫诲瀷" prop="type">
+ <el-select v-model="form.type" placeholder="璇烽�夋嫨閫氱煡绫诲瀷" style="width: 100%">
+ <el-option label="鏀惧亣閫氱煡" value="holiday" />
+ <el-option label="澶勭綒閫氱煡" value="penalty" />
+ <el-option label="寮�浼氶�氱煡" value="meeting" />
+ <el-option label="涓存椂閫氱煡" value="temporary" />
+ <el-option label="姝e紡閫氱煡" value="formal" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼樺厛绾�" prop="priority">
+ <el-select v-model="form.priority" placeholder="璇烽�夋嫨浼樺厛绾�" style="width: 100%">
+ <el-option label="鏅��" value="low" />
+ <el-option label="閲嶈" value="medium" />
+ <el-option label="绱ф��" value="high" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏈夋晥鏈熻嚦" prop="expireDate">
+ <el-date-picker
+ v-model="form.expireDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏈夋晥鏈�"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鎺ユ敹閮ㄩ棬" prop="departments">
+ <el-select
+ v-model="form.departments"
+ multiple
+ placeholder="璇烽�夋嫨鎺ユ敹閮ㄩ棬"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="dept in departments"
+ :key="dept"
+ :label="dept"
+ :value="dept"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍚屾鏂瑰紡" prop="syncMethods">
+ <el-checkbox-group v-model="form.syncMethods">
+ <el-checkbox
+ v-for="method in syncMethods"
+ :key="method.value"
+ :label="method.value"
+ >
+ {{ method.label }}
+ </el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ <el-form-item label="閫氱煡鍐呭" prop="content">
+ <el-input
+ v-model="form.content"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ラ�氱煡鍐呭"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
+ <!-- 鍦ㄧ嚎浼氳寮圭獥 -->
+ <el-dialog
+ v-model="meetingDialogVisible"
+ title="鍒涘缓鍦ㄧ嚎浼氳"
+ width="700px"
+ :close-on-click-modal="false"
+ >
+ <el-form ref="meetingFormRef" :model="meetingForm" :rules="meetingRules" label-width="120px">
+ <el-form-item label="浼氳鏍囬" prop="title">
+ <el-input v-model="meetingForm.title" placeholder="璇疯緭鍏ヤ細璁爣棰�" />
+ </el-form-item>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
+ <el-date-picker
+ v-model="meetingForm.startTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm:ss"
+ placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浼氳鏃堕暱" prop="duration">
+ <el-input-number
+ v-model="meetingForm.duration"
+ :min="15"
+ :max="480"
+ :step="15"
+ style="width: 100%"
+ />
+ <span style="margin-left: 10px">鍒嗛挓</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="浼氳骞冲彴" prop="platform">
+ <el-select v-model="meetingForm.platform" placeholder="璇烽�夋嫨浼氳骞冲彴" style="width: 100%">
+ <el-option
+ v-for="platform in meetingPlatforms"
+ :key="platform.value"
+ :label="platform.label"
+ :value="platform.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙備細浜哄憳" prop="participants">
+ <el-select
+ v-model="meetingForm.participants"
+ multiple
+ filterable
+ remote
+ :remote-method="filterEmployees"
+ :loading="employeesLoading"
+ placeholder="璇烽�夋嫨鍙備細浜哄憳"
+ style="width: 100%"
+ >
+ <el-option-group
+ v-for="group in employeeGroups"
+ :key="group.label"
+ :label="group.label"
+ >
+ <el-option
+ v-for="employee in group.options"
+ :key="employee.value"
+ :label="`${employee.label} (${employee.dept})`"
+ :value="employee.value"
+ >
+ <div style="display: flex; justify-content: space-between; align-items: center;">
+ <div>
+ <div style="font-weight: 500;">{{ employee.label }}</div>
+ <div style="color: #909399; font-size: 12px;">{{ employee.dept }}</div>
+ </div>
+ <div style="text-align: right; font-size: 12px; color: #909399;">
+ <div v-if="employee.phone">{{ employee.phone }}</div>
+ <div v-if="employee.email">{{ employee.email }}</div>
+ </div>
+ </div>
+ </el-option>
+ </el-option-group>
+ </el-select>
+ <div style="margin-top: 8px; color: #909399; font-size: 12px;">
+ 宸查�夋嫨 {{ meetingForm.participants.length }} 浜�
+ </div>
+ <!-- 宸查�夋嫨浜哄憳璇︽儏 -->
+ <div v-if="meetingForm.participants.length > 0" style="margin-top: 10px;">
+ <el-tag
+ v-for="participantId in meetingForm.participants"
+ :key="participantId"
+ closable
+ @close="removeParticipant(participantId)"
+ style="margin-right: 8px; margin-bottom: 8px;"
+ >
+ {{ getEmployeeName(participantId) }}
+ </el-tag>
+ </div>
+ </el-form-item>
+ <el-form-item label="浼氳鎻忚堪" prop="description">
+ <el-input
+ v-model="meetingForm.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ細璁弿杩�"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="meetingDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="createMeeting">鍒涘缓浼氳</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
+ <!-- 鏂囦欢鍏变韩寮圭獥 -->
+ <el-dialog
+ v-model="fileShareDialogVisible"
+ title="鏂囦欢鍏变韩"
+ width="700px"
+ :close-on-click-modal="false"
+ >
+ <el-form ref="fileShareFormRef" :model="fileShareForm" :rules="fileShareRules" label-width="120px">
+ <el-form-item label="鍏变韩鏍囬" prop="title">
+ <el-input v-model="fileShareForm.title" placeholder="璇疯緭鍏ュ叡浜爣棰�" />
+ </el-form-item>
+ <el-form-item label="鍏变韩鎻忚堪" prop="description">
+ <el-input
+ v-model="fileShareForm.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ叡浜弿杩�"
+ />
+ </el-form-item>
+ <el-form-item label="鎺ユ敹閮ㄩ棬" prop="departments">
+ <el-select
+ v-model="fileShareForm.departments"
+ multiple
+ placeholder="璇烽�夋嫨鎺ユ敹閮ㄩ棬"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="dept in departments"
+ :key="dept"
+ :label="dept"
+ :value="dept"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="涓婁紶鏂囦欢" prop="files">
+ <el-upload
+ ref="uploadRef"
+ :auto-upload="false"
+ :on-change="handleFileChange"
+ :on-remove="removeFile"
+ :file-list="fileList"
+ multiple
+ :limit="10"
+ accept=".doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.gif"
+ >
+ <el-button type="primary">閫夋嫨鏂囦欢</el-button>
+ <template #tip>
+ <div class="el-upload__tip">
+ 鏀寔涓婁紶鏂囨。銆佸浘鐗囩瓑鏍煎紡锛屽崟涓枃浠朵笉瓒呰繃10MB锛屾渶澶�10涓枃浠�
+ </div>
+ </template>
+ </el-upload>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="fileShareDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="shareFiles">鍏变韩鏂囦欢</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import { onMounted, ref, reactive, toRefs, computed } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
+import { listNotification, addNotification, updateNotification, delNotification,addOnlineMeeting,addFileSharing } from "@/api/collaborativeApproval/notificationManagement.js";
+import { id } from "element-plus/es/locales.mjs";
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+ title: [
+ { required: true, message: "璇疯緭鍏ラ�氱煡鏍囬", trigger: "blur" }
+ ],
+ type: [
+ { required: true, message: "璇烽�夋嫨閫氱煡绫诲瀷", trigger: "change" }
+ ],
+ content: [
+ { required: true, message: "璇疯緭鍏ラ�氱煡鍐呭", trigger: "blur" }
+ ]
+};
+
+const meetingRules = {
+ title: [
+ { required: true, message: "璇疯緭鍏ヤ細璁爣棰�", trigger: "blur" }
+ ],
+ startTime: [
+ { required: true, message: "璇烽�夋嫨浼氳寮�濮嬫椂闂�", trigger: "change" }
+ ],
+ participants: [
+ { required: true, message: "璇烽�夋嫨鍙備細浜哄憳", trigger: "change" }
+ ]
+};
+
+const fileShareRules = {
+ title: [
+ { required: true, message: "璇疯緭鍏ュ叡浜爣棰�", trigger: "blur" }
+ ],
+ description: [
+ { required: true, message: "璇疯緭鍏ュ叡浜弿杩�", trigger: "blur" }
+ ]
+};
+
+// 鍝嶅簲寮忔暟鎹�
+const data = reactive({
+ searchForm: {
+ title: "",
+ type: "",
+ status: "",
+ },
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+ selectedIds: [],
+ // 鏂板閫氱煡鐩稿叧
+ form: {
+ title: "",
+ type: "",
+ priority: "",
+ content: "",
+ departments: [],
+ expireDate: "",
+ syncMethods: []
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ // 鍦ㄧ嚎浼氳鐩稿叧
+ meetingDialogVisible: false,
+ meetingForm: {
+ title: "",
+ startTime: "",
+ duration: 60,
+ participants: [],
+ description: "",
+ platform: "wechat"
+ },
+ // 鏂囦欢鍏变韩鐩稿叧
+ fileShareDialogVisible: false,
+ fileShareForm: {
+ title: "",
+ description: "",
+ departments: [],
+ files: []
+ },
+ fileList: []
+});
+
+const {
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ selectedIds,
+ form,
+ dialogVisible,
+ dialogTitle,
+ dialogType,
+ meetingDialogVisible,
+ meetingForm,
+ fileShareDialogVisible,
+ fileShareForm,
+ fileList
+} = toRefs(data);
+
+// 琛ㄥ崟寮曠敤
+const formRef = ref();
+const meetingFormRef = ref();
+const fileShareFormRef = ref();
+
+// 琛ㄦ牸鍒楅厤缃�
+const tableColumn = ref([
+ {
+ label: "閫氱煡鏍囬",
+ prop: "title",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "閫氱煡绫诲瀷",
+ prop: "type",
+ dataType: "tag",
+ formatData: (params) => {
+ const typeMap = {
+ holiday: "鏀惧亣閫氱煡",
+ penalty: "澶勭綒閫氱煡",
+ meeting: "寮�浼氶�氱煡",
+ temporary: "涓存椂閫氱煡",
+ formal: "姝e紡閫氱煡"
+ };
+ return typeMap[params] || params;
+ },
+ formatType: (params) => {
+ const typeMap = {
+ holiday: "success",
+ penalty: "danger",
+ meeting: "warning",
+ temporary: "info",
+ formal: "primary"
+ };
+ return typeMap[params] || "info";
+ }
+ },
+ {
+ label: "浼樺厛绾�",
+ prop: "priority",
+ dataType: "tag",
+ formatData: (params) => {
+ const priorityMap = {
+ low: "鏅��",
+ medium: "閲嶈",
+ high: "绱ф��"
+ };
+ return priorityMap[params] || params;
+ },
+ formatType: (params) => {
+ const typeMap = {
+ low: "info",
+ medium: "warning",
+ high: "danger"
+ };
+ return typeMap[params] || "info";
+ }
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ formatData: (params) => {
+ const statusMap = {
+ draft: "鑽夌",
+ published: "宸插彂甯�",
+ expired: "宸茶繃鏈�"
+ };
+ return statusMap[params] || params;
+ },
+ formatType: (params) => {
+ const typeMap = {
+ draft: "info",
+ published: "success",
+ expired: "danger"
+ };
+ return typeMap[params] || "info";
+ }
+ },
+ {
+ label: "鎺ユ敹閮ㄩ棬",
+ prop: "departments",
+ width: 150,
+ showOverflowTooltip: true,
+ formatData: (params) => {
+ if (!params || params.length === 0) return "鍏ㄩ儴閮ㄩ棬";
+ return params.join(", ");
+ }
+ },
+ {
+ label: "鏈夋晥鏈熻嚦",
+ prop: "expireDate",
+ width: 150,
+ formatData: (params) => {
+ if (!params) return "姘镐箙鏈夋晥";
+ return params;
+ }
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 180,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ }
+ },
+ {
+ name: "鍙戝竷",
+ type: "text",
+ clickFun: (row) => {
+ publishNotification(row);
+ },
+ },
+ {
+ name: "鎾ゅ洖",
+ type: "text",
+ clickFun: (row) => {
+ revokeNotification(row);
+ },
+ }
+ ]
+ }
+]);
+// 閫氱煡鏍囬妯℃澘
+const titleTemplates = [
+ "鍏充簬{year}骞磠holiday}鏀惧亣瀹夋帓鐨勯�氱煡",
+ "{dept}閮ㄩ棬{meeting}浼氳閫氱煡",
+ "鍛樺伐{behavior}琛屼负瑙勮寖鎻愰啋",
+ "{company}閲嶈浜嬮」閫氱煡",
+ "{dept}閮ㄩ棬宸ヤ綔瀹夋帓閫氱煡",
+ "鍏充簬{project}椤圭洰杩涘害鐨勯�氱煡",
+ "{dept}閮ㄩ棬浜哄憳璋冩暣閫氱煡",
+ "鍏徃{policy}鏀跨瓥鏇存柊閫氱煡"
+];
+
+// 閫氱煡绫诲瀷閰嶇疆
+const notificationTypes = [
+ { type: "holiday", label: "鏀惧亣閫氱煡", priority: "high" },
+ { type: "meeting", label: "寮�浼氶�氱煡", priority: "medium" },
+ { type: "penalty", label: "澶勭綒閫氱煡", priority: "high" },
+ { type: "temporary", label: "涓存椂閫氱煡", priority: "low" },
+ { type: "formal", label: "姝e紡閫氱煡", priority: "medium" }
+];
+
+// 閮ㄩ棬鍒楄〃
+const departments = ["鎶�鏈儴", "閿�鍞儴", "浜轰簨閮�", "璐㈠姟閮�", "杩愯惀閮�", "甯傚満閮�", "瀹㈡湇閮�"];
+
+// 浜哄憳鍒楄〃
+const employees = ref([]);
+const employeesLoading = ref(false);
+
+// 鑾峰彇鍦ㄨ亴鍛樺伐鍒楄〃
+const getEmployeesList = async () => {
+ try {
+ employeesLoading.value = true;
+ // 浼樺厛浣跨敤绯荤粺鐢ㄦ埛鎺ュ彛锛堟寜绉熸埛鑾峰彇锛�
+ const userResponse = await userListNoPageByTenantId();
+
+ if (userResponse.data) {
+ employees.value = userResponse.data.map(user => ({
+ label: user.nickName || user.userName || '鏈煡濮撳悕',
+ value: user.userId || user.id,
+ dept: user.dept?.deptName || '鏈煡閮ㄩ棬',
+ phone: user.phonenumber || '',
+ email: user.email || '',
+ status: user.status || '0'
+ })).filter(user => user.status === '0'); // 鍙樉绀烘甯哥姸鎬佺殑鐢ㄦ埛
+ } else {
+ // 濡傛灉绯荤粺鐢ㄦ埛鎺ュ彛澶辫触锛屼娇鐢ㄥ憳宸ュ彴璐︽帴鍙�
+ const response = await staffOnJobListPage({
+ pageNum: 1,
+ pageSize: 1000,
+ staffState: 1 // 鍦ㄨ亴鐘舵��
+ });
+
+ if (response.data && response.data.records) {
+ employees.value = response.data.records.map(employee => ({
+ label: employee.staffName || employee.name || '鏈煡濮撳悕',
+ value: employee.staffNo || employee.id || employee.staffId,
+ dept: employee.deptName || employee.department || '鏈煡閮ㄩ棬',
+ phone: employee.phone || employee.mobile || '',
+ email: employee.email || '',
+ status: '0'
+ }));
+ }
+ }
+ } catch (error) {
+ console.error('鑾峰彇鍛樺伐鍒楄〃澶辫触:', error);
+ // 濡傛灉鎺ュ彛閮藉け璐ワ紝浣跨敤榛樿鏁版嵁
+ employees.value = [
+ { label: "寮犱笁", value: "001", dept: "鎶�鏈儴", phone: "13800138001", email: "zhangsan@company.com", status: "0" },
+ { label: "鏉庡洓", value: "002", dept: "閿�鍞儴", phone: "13800138002", email: "lisi@company.com", status: "0" },
+ { label: "鐜嬩簲", value: "003", dept: "浜轰簨閮�", phone: "13800138003", email: "wangwu@company.com", status: "0" }
+ ];
+ } finally {
+ employeesLoading.value = false;
+ }
+};
+
+// 鍛樺伐鍒嗙粍
+const employeeGroups = computed(() => {
+ const groups = {};
+ employees.value.forEach(employee => {
+ const dept = employee.dept || '鍏朵粬閮ㄩ棬';
+ if (!groups[dept]) {
+ groups[dept] = [];
+ }
+ groups[dept].push(employee);
+ });
+
+ // 鎸夐儴闂ㄥ悕绉版帓搴忥紝纭繚鏄剧ず椤哄簭涓�鑷�
+ return Object.keys(groups)
+ .sort()
+ .map(dept => ({
+ label: dept,
+ options: groups[dept].sort((a, b) => a.label.localeCompare(b.label, 'zh-CN'))
+ }));
+});
+
+// 杩囨护鍛樺伐锛堣繙绋嬫悳绱級
+const filterEmployees = (query) => {
+ if (query !== '') {
+ const lowerQuery = query.toLowerCase();
+ return employees.value.filter(employee =>
+ employee.label.toLowerCase().includes(lowerQuery) ||
+ employee.dept.toLowerCase().includes(lowerQuery) ||
+ (employee.phone && employee.phone.includes(query)) ||
+ (employee.email && employee.email.toLowerCase().includes(lowerQuery))
+ );
+ } else {
+ return employees.value;
+ }
+};
+
+// 鍒锋柊鍛樺伐鍒楄〃
+const refreshEmployees = async () => {
+ ElMessage.info("姝e湪鍒锋柊鍛樺伐鍒楄〃...");
+ await getEmployeesList();
+
+ // 缁熻鍚勯儴闂ㄤ汉鏁�
+ const deptStats = {};
+ employees.value.forEach(emp => {
+ const dept = emp.dept || '鍏朵粬閮ㄩ棬';
+ deptStats[dept] = (deptStats[dept] || 0) + 1;
+ });
+
+ const deptInfo = Object.entries(deptStats)
+ .map(([dept, count]) => `${dept}: ${count}浜篳)
+ .join(', ');
+
+ ElMessage.success(`鍛樺伐鍒楄〃鍒锋柊瀹屾垚锛屽叡 ${employees.value.length} 浜� (${deptInfo})`);
+};
+
+// 鑾峰彇鍛樺伐濮撳悕
+const getEmployeeName = (employeeId) => {
+ const employee = employees.value.find(emp => emp.value === employeeId);
+ return employee ? employee.label : '鏈煡浜哄憳';
+};
+
+// 鑾峰彇鍛樺伐璇︾粏淇℃伅
+const getEmployeeInfo = (employeeId) => {
+ const employee = employees.value.find(emp => emp.value === employeeId);
+ if (!employee) return null;
+
+ return {
+ name: employee.label,
+ dept: employee.dept,
+ phone: employee.phone,
+ email: employee.email
+ };
+};
+
+// 绉婚櫎鍙備細浜哄憳
+const removeParticipant = (participantId) => {
+ const index = meetingForm.value.participants.indexOf(participantId);
+ if (index > -1) {
+ meetingForm.value.participants.splice(index, 1);
+ }
+};
+
+// 鍚屾鏂瑰紡閫夐」
+const syncMethods = [
+ { label: "浼佷笟寰俊", value: "wechat" },
+ { label: "閽夐拤", value: "dingtalk" },
+ { label: "閭欢", value: "email" },
+ { label: "鐭俊", value: "sms" }
+];
+
+// 浼氳骞冲彴閫夐」
+const meetingPlatforms = [
+ { label: "浼佷笟寰俊浼氳", value: "wechat" },
+ { label: "閽夐拤浼氳", value: "dingtalk" },
+ { label: "鑵捐浼氳", value: "tencent" },
+ { label: "Zoom", value: "zoom" }
+];
+
+// 鑷姩鐢熸垚鏂版暟鎹�
+const generateNewData = () => {
+ const newId = (mockData.length + 1).toString();
+ const now = new Date();
+ const randomType = notificationTypes[Math.floor(Math.random() * notificationTypes.length)];
+ const randomDept = departments[Math.floor(Math.random() * departments.length)];
+
+ // 鐢熸垚闅忔満鏍囬
+ let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)];
+ title = title
+ .replace('{year}', now.getFullYear())
+ .replace('{holiday}', ['鏄ヨ妭', '鍥藉簡', '涓', '鍏冩棪'][Math.floor(Math.random() * 4)])
+ .replace('{dept}', randomDept)
+ .replace('{meeting}', ['鍛ㄤ緥浼�', '鏈堝害鎬荤粨', '椤圭洰璇勫', '鍩硅浼氳'][Math.floor(Math.random() * 4)])
+ .replace('{behavior}', ['鑰冨嫟', '鐫�瑁�', '宸ヤ綔鎬佸害', '鍥㈤槦鍗忎綔'][Math.floor(Math.random() * 4)])
+ .replace('{company}', ['鍏徃', '闆嗗洟', '鎬婚儴'][Math.floor(Math.random() * 4)])
+ .replace('{project}', ['鏁板瓧鍖栬浆鍨�', '浜у搧鍗囩骇', '甯傚満鎷撳睍', '浜烘墠鍩瑰吇'][Math.floor(Math.random() * 4)])
+ .replace('{policy}', ['鑰冨嫟', '钖叕', '绂忓埄', '鏅嬪崌'][Math.floor(Math.random() * 4)]);
+
+ // 闅忔満鐘舵��
+ const statuses = ['draft', 'published'];
+ const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
+
+ // 闅忔満浼樺厛绾�
+ const priorities = ['low', 'medium', 'high'];
+ const randomPriority = priorities[Math.floor(Math.random() * priorities.length)];
+
+ const newNotification = {
+ id: newId,
+ title: title,
+ type: randomType.type,
+ priority: randomPriority,
+ status: randomStatus,
+ content: `杩欐槸${title}鐨勮缁嗗唴瀹癸紝璇风浉鍏充汉鍛樻敞鎰忔煡鐪�...`,
+ departments: [randomDept],
+ expireDate: new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0], // 30澶╁悗杩囨湡
+ syncMethods: ["wechat", "dingtalk"],
+ createTime: now.toLocaleString()
+ };
+
+ // 娣诲姞鍒版暟鎹紑澶�
+ mockData.unshift(newNotification);
+
+ // 淇濇寔鏁版嵁閲忓湪鍚堢悊鑼冨洿鍐咃紙鏈�澶氫繚鐣�20鏉★級
+ if (mockData.length > 20) {
+ mockData = mockData.slice(0, 20);
+ }
+
+ console.log(`[${new Date().toLocaleString()}] 鑷姩鐢熸垚鏂伴�氱煡: ${title}`);
+};
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ getList();
+ getEmployeesList(); // 鑾峰彇鍛樺伐鍒楄〃
+ startAutoRefresh();
+});
+
+// 寮�濮嬭嚜鍔ㄥ埛鏂�
+const startAutoRefresh = () => {
+ setInterval(() => {
+ generateNewData();
+ getList();
+ }, 600000); // 10鍒嗛挓鍒锋柊涓�娆� (10 * 60 * 1000 = 600000ms)
+};
+
+// 鏌ヨ鏁版嵁
+const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ listNotification({...page.value, ...searchForm.value})
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.value.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+
+// 鍒嗛〉澶勭悊
+const pagination = (obj) => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ handleQuery();
+};
+
+// 閫夋嫨鍙樺寲澶勭悊
+const handleSelectionChange = (selection) => {
+ selectedIds.value = selection.map(item => item.id);
+};
+
+// 鎵撳紑琛ㄥ崟
+const openForm = (type, row = null) => {
+ dialogType.value = type;
+ if (type === "add") {
+ dialogTitle.value = "鏂板閫氱煡";
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form.value, {
+ id: "",
+ title: "",
+ type: "",
+ priority: "",
+ content: "",
+ departments: [],
+ expireDate: "",
+ status: "draft",
+ syncMethods: []
+ });
+ } else if (type === "edit" && row) {
+ dialogTitle.value = "缂栬緫閫氱煡";
+ Object.assign(form.value, {
+ id: row.id,
+ title: row.title,
+ type: row.type,
+ priority: row.priority,
+ content: row.content || "",
+ departments: row.departments || [],
+ expireDate: row.expireDate || "",
+ status: row.status,
+ syncMethods: row.syncMethods || []
+ });
+ }
+ dialogVisible.value = true;
+};
+
+// 鎵撳紑鍦ㄧ嚎浼氳寮圭獥
+const openMeetingDialog = () => {
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(meetingForm.value, {
+ title: "",
+ startTime: "",
+ duration: 60,
+ participants: [],
+ description: "",
+ platform: "wechat"
+ });
+ meetingDialogVisible.value = true;
+};
+
+// 鎵撳紑鏂囦欢鍏变韩寮圭獥
+const openFileShareDialog = () => {
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(fileShareForm.value, {
+ title: "",
+ description: "",
+ departments: [],
+ files: []
+ });
+ fileList.value = [];
+ fileShareDialogVisible.value = true;
+};
+
+// 鎵嬪姩鍒锋柊鏁版嵁
+const manualRefresh = () => {
+ generateNewData();
+ getList();
+ ElMessage.success("鎵嬪姩鍒锋柊瀹屾垚锛屽凡鐢熸垚鏂伴�氱煡");
+};
+
+// 鎻愪氦閫氱煡琛ㄥ崟
+const submitForm = async () => {
+ try {
+ await formRef.value.validate();
+
+ if (dialogType.value === "add") {
+ // 鏂板閫氱煡
+ addNotification({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } else {
+ // 缂栬緫閫氱煡
+ updateNotification({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+};
+
+// 鍒涘缓浼氳
+const createMeeting = async () => {
+ try {
+ await meetingFormRef.value.validate();
+
+ // 妯℃嫙鍒涘缓浼氳
+ const meetingInfo = {
+ title: meetingForm.value.title,
+ startTime: meetingForm.value.startTime,
+ duration: meetingForm.value.duration,
+ participants: meetingForm.value.participants,
+ description: meetingForm.value.description,
+ platform: meetingForm.value.platform
+ };
+ // 鏂板浼氳
+ addOnlineMeeting({...meetingInfo}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("浼氳娣诲姞鎴愬姛");
+ meetingDialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ // 妯℃嫙鍙戦�佸埌浼佷笟寰俊/閽夐拤
+ // const platformName = meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "鏈煡骞冲彴";
+ // ElMessage.success(`浼氳鍒涘缓鎴愬姛锛佷細璁甀D: ${meetingInfo.meetingId}锛屽皢閫氳繃${platformName}鍙戦�侀�氱煡`);
+
+ // 鑾峰彇鍙備細浜哄憳淇℃伅
+ const participantNames = meetingForm.value.participants.map(participantId => {
+ const employee = employees.value.find(emp => emp.value === participantId);
+ return employee ? employee.label : '鏈煡浜哄憳';
+ }).join('銆�');
+
+ // 鑾峰彇鍙備細浜哄憳璇︾粏淇℃伅
+ const participantDetails = meetingForm.value.participants.map(participantId => {
+ const employee = employees.value.find(emp => emp.value === participantId);
+ return employee ? {
+ name: employee.label,
+ dept: employee.dept,
+ phone: employee.phone,
+ email: employee.email
+ } : null;
+ }).filter(Boolean);
+
+ // 灏嗕細璁俊鎭坊鍔犲埌閫氱煡鍒楄〃
+ const meetingNotification = {
+ title: `[浼氳閫氱煡] ${meetingInfo.title}`,
+ type: "meeting",
+ priority: "high",
+ status: "published",
+ content: `浼氳鏃堕棿: ${meetingInfo.startTime}锛屾椂闀�: ${meetingInfo.duration}鍒嗛挓锛屽钩鍙�: ${meetingPlatforms.find(p => p.value === meetingForm.value.platform)?.label || "鏈煡骞冲彴"}锛屽弬浼氫汉鍛�: ${participantNames}锛屽叡${participantDetails.length}浜篳,
+ departments: [],
+ expireDate: "",
+ syncMethods: [meetingForm.value.platform]
+ };
+ addNotification({...meetingNotification}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ // dialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ // mockData.unshift(meetingNotification);
+ // getList();
+ } catch (error) {
+ console.error("浼氳琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+};
+
+// 鏂囦欢涓婁紶澶勭悊
+const handleFileChange = (file) => {
+ const isLt10M = file.size / 1024 / 1024 < 10;
+ if (!isLt10M) {
+ ElMessage.error("涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!");
+ return false;
+ }
+
+ const fileInfo = {
+ name: file.name,
+ size: file.size,
+ type: file.type,
+ uid: file.uid
+ };
+
+ fileList.value.push(fileInfo);
+ fileShareForm.value.files.push(fileInfo.name);
+ return false; // 闃绘鑷姩涓婁紶
+};
+
+// 绉婚櫎鏂囦欢
+const removeFile = (file) => {
+ const index = fileList.value.findIndex(item => item.uid === file.uid);
+ if (index !== -1) {
+ const index2 = fileShareForm.value.files.findIndex(item => item.uid === file.uid);
+ if (index2 !== -1) {
+ fileShareForm.value.files.splice(index2, 1);
+ }
+ fileList.value.splice(index, 1);
+ }
+};
+
+// 鍏变韩鏂囦欢
+const shareFiles = async () => {
+ try {
+ await fileShareFormRef.value.validate();
+
+ if (fileShareForm.value.files.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�涓枃浠�");
+ return;
+ }
+
+ // 妯℃嫙鏂囦欢鍏变韩
+ const shareInfo = {
+ title: fileShareForm.value.title,
+ description: fileShareForm.value.description,
+ departments: fileShareForm.value.departments,
+ files: fileShareForm.value.files,
+ };
+ addFileSharing({...shareInfo}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏂囦欢鍏变韩鎴愬姛");
+ fileShareDialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+
+ // ElMessage.success(`鏂囦欢鍏变韩鎴愬姛锛佸叡浜獻D: ${shareInfo.shareId}锛屽凡閫氱煡鐩稿叧閮ㄩ棬`);
+
+
+ // 灏嗘枃浠跺叡浜俊鎭坊鍔犲埌閫氱煡鍒楄〃
+ const fileShareNotification = {
+ title: `[鏂囦欢鍏变韩] ${shareInfo.title}`,
+ type: "temporary",
+ priority: "medium",
+ status: "published",
+ content: `鍏变韩鎻忚堪: ${shareInfo.description}锛屾枃浠舵暟閲�: ${shareInfo.files.length}涓猔,
+ departments: shareInfo.departments,
+ expireDate: "",
+ syncMethods: ["wechat", "dingtalk"],
+ };
+ addNotification({...fileShareNotification}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ // dialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+
+ // mockData.unshift(fileShareNotification);
+ // getList();
+ } catch (error) {
+ console.error("鏂囦欢鍏变韩琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+};
+
+// 鍙戝竷閫氱煡
+const publishNotification = (row) => {
+ Object.assign(form.value, {
+ id: row.id,
+ title: row.title,
+ type: row.type,
+ priority: row.priority,
+ content: row.content || "",
+ departments: row.departments || [],
+ expireDate: row.expireDate || "",
+ status: row.status,
+ syncMethods: row.syncMethods || []
+ });
+ form.value.status = "published";
+ updateNotification({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("閫氱煡鍙戝竷鎴愬姛");
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+};
+
+// 鎾ゅ洖閫氱煡
+const revokeNotification = (row) => {
+ Object.assign(form.value, {
+ id: row.id,
+ title: row.title,
+ type: row.type,
+ priority: row.priority,
+ content: row.content || "",
+ departments: row.departments || [],
+ expireDate: row.expireDate || "",
+ status: row.status,
+ syncMethods: row.syncMethods || []
+ });
+ form.value.status = "draft";
+ updateNotification({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("閫氱煡宸叉挙鍥�");
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+};
+
+// 鍒犻櫎閫氱煡
+const handleDelete = () => {
+ let ids = [];
+ if (selectedIds.value.length > 0) {
+ ids = selectedIds.value;
+ }else{
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑閫氱煡");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ delNotification(ids).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ selectedIds.value = [];
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }).catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+};
+</script>
+
+<style scoped>
+.auto-refresh-info {
+ margin-bottom: 15px;
+}
+
+.auto-refresh-info .el-alert {
+ border-radius: 8px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+
+.el-upload__tip {
+ color: #909399;
+ font-size: 12px;
+ margin-top: 8px;
+}
+
+.el-checkbox-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.el-checkbox {
+ margin-right: 0;
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
new file mode 100644
index 0000000..b2fb88d
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
@@ -0,0 +1,511 @@
+<template>
+ <div>
+
+ <!-- 鐢宠绫诲瀷閫夋嫨 -->
+ <el-card class="type-card">
+ <div class="type-selector">
+ <div
+ v-for="type in applicationTypes"
+ :key="type.value"
+ class="type-item"
+ :class="{ active: currentType === type.value }"
+ @click="changeType(type.value)"
+ >
+ <div class="type-icon">
+ <el-icon :size="24"><component :is="type.icon"/></el-icon>
+ </div>
+ <div class="type-info">
+ <div class="type-name">{{ type.name }}</div>
+ <div class="type-desc">{{ type.desc }}</div>
+ </div>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 浼氳鐢宠琛ㄥ崟 -->
+ <el-card>
+ <div class="form-header">
+ <h3>{{ getCurrentTypeName() }}鐢宠</h3>
+ </div>
+
+ <el-form
+ ref="meetingFormRef"
+ :model="meetingForm"
+ :rules="rules"
+ label-width="100px"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼氳涓婚" prop="title">
+ <el-input v-model="meetingForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼氳瀹�" prop="roomId">
+ <el-select v-model="meetingForm.roomId" placeholder="璇烽�夋嫨浼氳瀹�" style="width: 100%">
+ <el-option
+ v-for="room in meetingRooms"
+ :key="room.id"
+ :label="`${room.name} (${room.location})`"
+ :value="room.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓绘寔浜�" prop="host">
+ <el-input v-model="meetingForm.host" placeholder="璇疯緭鍏ヤ富鎸佷汉濮撳悕"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼氳鏃ユ湡" prop="meetingDate">
+ <el-date-picker
+ v-model="meetingForm.meetingDate"
+ type="date"
+ placeholder="璇烽�夋嫨浼氳鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ :disabled-date="disabledDate"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <!-- 绌哄垪锛屼繚鎸佸竷灞� -->
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
+ <el-select
+ v-model="meetingForm.startTime"
+ placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="time in startTimeOptions"
+ :key="time.value"
+ :label="time.label"
+ :value="time.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
+ <el-select
+ v-model="meetingForm.endTime"
+ placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="time in endTimeOptions"
+ :key="time.value"
+ :label="time.label"
+ :value="time.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="鍙備細浜哄憳" prop="participants">
+ <el-select
+ v-model="meetingForm.participants"
+ multiple
+ filterable
+ placeholder="璇烽�夋嫨鍙備細浜哄憳"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="person in employees"
+ :key="person.id"
+ :label="`${person.staffName}${person.postName ? ` (${person.postName})` : ''}`"
+ :value="person.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="浼氳璇存槑" prop="description">
+ <el-input
+ v-model="meetingForm.description"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ヤ細璁鏄�"
+ />
+ </el-form-item>
+ </el-form>
+
+ <div class="form-footer">
+ <el-button @click="resetForm">閲嶇疆</el-button>
+ <el-button type="primary" @click="submitForm">鎻愪氦</el-button>
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, onMounted, computed, watch} from 'vue'
+import {ElMessage} from 'element-plus'
+import {Plus, Document, Promotion, Bell} from '@element-plus/icons-vue'
+import {getRoomEnum, saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+
+// 褰撳墠鐢宠绫诲瀷
+const currentType = ref('department') // approval: 瀹℃壒娴佺▼, department: 閮ㄩ棬绾�, notification: 閫氱煡鍙戝竷
+
+// 鐢宠绫诲瀷閫夐」
+const applicationTypes = ref([
+ {
+ value: 'approval',
+ name: '瀹℃壒娴佺▼浼氳',
+ desc: '闇�瑕佺粡杩囧绾у鎵圭殑浼氳鐢宠',
+ icon: Document
+ },
+ {
+ value: 'department',
+ name: '閮ㄩ棬绾т細璁�',
+ desc: '閮ㄩ棬鍐呴儴浼氳鐢宠娴佺▼',
+ icon: Promotion
+ },
+ {
+ value: 'notification',
+ name: '浼氳閫氱煡',
+ desc: '鏃犻渶瀹℃壒鐩存帴鍙戝竷鐨勪細璁�氱煡',
+ icon: Bell
+ }
+])
+
+// 琛ㄥ崟鏁版嵁
+const meetingForm = reactive({
+ title: '',
+ type: '',
+ roomId: '',
+ host: '',
+ meetingDate: '',
+ startTime: '',
+ endTime: '',
+ participants: [],
+ description: ''
+})
+
+// 琛ㄥ崟寮曠敤
+const meetingFormRef = ref(null)
+
+// 浼氳瀹ゅ垪琛�
+const meetingRooms = ref([])
+
+// 鍛樺伐鍒楄〃
+const employees = ref([])
+
+// 鏃堕棿閫夐」锛堜互鍗婂皬鏃朵负闂撮殧锛�
+const timeOptions = ref([])
+
+const getTimeInMinutes = (time) => {
+ if (!time) return -1
+ const [hour, minute] = time.split(':').map(Number)
+ return hour * 60 + minute
+}
+
+const isToday = (dateText) => {
+ if (!dateText) return false
+ const [year, month, day] = dateText.split('-').map(Number)
+ const now = new Date()
+ return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate()
+}
+
+const validateStartTime = (_rule, value, callback) => {
+ if (!value) {
+ callback()
+ return
+ }
+
+ if (isToday(meetingForm.meetingDate)) {
+ const now = new Date()
+ const currentMinutes = now.getHours() * 60 + now.getMinutes()
+ if (getTimeInMinutes(value) > currentMinutes) {
+ callback(new Error('褰撳ぉ寮�濮嬫椂闂翠笉鑳芥櫄浜庡綋鍓嶆椂闂�'))
+ return
+ }
+ }
+
+ callback()
+}
+
+const validateEndTime = (_rule, value, callback) => {
+ if (!value || !meetingForm.startTime) {
+ callback()
+ return
+ }
+
+ if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) {
+ callback(new Error('缁撴潫鏃堕棿蹇呴』澶т簬寮�濮嬫椂闂�'))
+ return
+ }
+
+ callback()
+}
+
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const rules = {
+ title: [{required: true, message: '璇疯緭鍏ヤ細璁富棰�', trigger: 'blur'}],
+ roomId: [{required: true, message: '璇烽�夋嫨浼氳瀹�', trigger: 'change'}],
+ host: [{required: true, message: '璇疯緭鍏ヤ富鎸佷汉', trigger: 'blur'}],
+ meetingDate: [{required: true, message: '璇烽�夋嫨浼氳鏃ユ湡', trigger: 'change'}],
+ startTime: [
+ {required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change'},
+ {validator: validateStartTime, trigger: 'change'}
+ ],
+ endTime: [
+ {required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change'},
+ {validator: validateEndTime, trigger: 'change'}
+ ],
+ participants: [{required: true, message: '璇烽�夋嫨鍙備細浜哄憳', trigger: 'change'}]
+}
+
+const startTimeOptions = computed(() => {
+ if (!isToday(meetingForm.meetingDate)) {
+ return timeOptions.value
+ }
+ const now = new Date()
+ const currentMinutes = now.getHours() * 60 + now.getMinutes()
+ return timeOptions.value.filter(item => getTimeInMinutes(item.value) <= currentMinutes)
+})
+
+const endTimeOptions = computed(() => {
+ if (!meetingForm.startTime) {
+ return timeOptions.value
+ }
+ const startMinutes = getTimeInMinutes(meetingForm.startTime)
+ return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes)
+})
+
+// 鍒濆鍖栨椂闂撮�夐」
+const initTimeOptions = () => {
+ const options = []
+ const now = new Date()
+ const currentHour = now.getHours()
+ const currentMinute = now.getMinutes()
+ // meetingDate 鏄� "yyyy-MM-dd"
+ const meetingDate = new Date(meetingForm.meetingDate)
+
+ const isSameDay =
+ now.getFullYear() === meetingDate.getFullYear() &&
+ now.getMonth() === meetingDate.getMonth() &&
+ now.getDate() === meetingDate.getDate()
+
+ console.log('鏄惁鍚屼竴澶�:', isSameDay)
+ for (let hour = 8; hour <= 18; hour++) {
+ // 寮�濮嬫椂闂村繀椤绘櫄浜庡綋鍓嶆椂闂�
+ if (hour < currentHour && isSameDay) {
+ continue
+ }
+ if (hour === currentHour && currentMinute > 30 && isSameDay) {
+ continue
+ }
+ // 姣忎釜灏忔椂娣诲姞涓や釜閫夐」锛氭暣鐐瑰拰鍗婄偣
+ options.push({
+ value: `${hour.toString().padStart(2, '0')}:00`,
+ label: `${hour.toString().padStart(2, '0')}:00`
+ })
+
+ if (hour < 18) { // 18:00涔嬪悗娌℃湁鍗婄偣閫夐」
+ options.push({
+ value: `${hour.toString().padStart(2, '0')}:30`,
+ label: `${hour.toString().padStart(2, '0')}:30`
+ })
+ }
+ }
+ timeOptions.value = options
+}
+
+watch(() => meetingForm.meetingDate, () => {
+ if (meetingForm.startTime && !startTimeOptions.value.some(item => item.value === meetingForm.startTime)) {
+ meetingForm.startTime = ''
+ }
+ if (meetingForm.endTime && !endTimeOptions.value.some(item => item.value === meetingForm.endTime)) {
+ meetingForm.endTime = ''
+ }
+ if (meetingForm.startTime) {
+ meetingFormRef.value?.validateField('startTime')
+ }
+ if (meetingForm.endTime) {
+ meetingFormRef.value?.validateField('endTime')
+ }
+ initTimeOptions()
+})
+
+watch(() => meetingForm.startTime, () => {
+ if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) {
+ meetingForm.endTime = ''
+ }
+ if (meetingForm.endTime) {
+ meetingFormRef.value?.validateField('endTime')
+ }
+
+})
+
+// 绂佺敤鏃ユ湡锛堢鐢ㄤ粖澶╀箣鍓嶇殑鏃ユ湡锛�
+const disabledDate = (time) => {
+ // 绂佺敤浠婂ぉ涔嬪墠鐨勬棩鏈�
+ return time.getTime() < Date.now() - 86400000
+}
+
+// 鍒囨崲鐢宠绫诲瀷
+const changeType = (type) => {
+ currentType.value = type
+}
+
+// 鑾峰彇褰撳墠绫诲瀷鍚嶇О
+const getCurrentTypeName = () => {
+ const type = applicationTypes.value.find(t => t.value === currentType.value)
+ return type ? type.name : ''
+}
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+ meetingFormRef.value?.resetFields()
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ meetingFormRef.value?.validate((valid) => {
+ if (valid) {
+
+ let formData = {...meetingForm}
+ formData.applicationType = currentType.value
+ formData.startTime = `${meetingForm.meetingDate} ${meetingForm.startTime}:00`
+ formData.endTime = `${meetingForm.meetingDate} ${meetingForm.endTime}:00`
+ formData.participants = JSON.stringify(formData.participants)
+ console.log(formData)
+ saveMeetingApplication(formData).then(() => {
+
+ // 妯℃嫙鎻愪氦鎿嶄綔
+ ElMessage.success(`${getCurrentTypeName()}鎻愪氦鎴愬姛`)
+
+ // 鏍规嵁涓嶅悓绫诲瀷鎵ц涓嶅悓鎿嶄綔
+ switch (currentType.value) {
+ case 'approval':
+ ElMessage.info('浼氳宸叉彁浜ゅ鎵规祦绋�')
+ break
+ case 'department':
+ ElMessage.info('閮ㄩ棬绾т細璁敵璇峰凡鎻愪氦')
+ break
+ case 'notification':
+ ElMessage.info('浼氳閫氱煡宸插彂甯�')
+ break
+ }
+ resetForm()
+ })
+
+ }
+ })
+}
+
+// 椤甸潰鍔犺浇鏃跺垵濮嬪寲
+onMounted(() => {
+ initTimeOptions()
+ getRoomEnum().then(res => {
+ meetingRooms.value = res.data
+ })
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1
+ }).then(res => {
+ employees.value = res.data.records.sort((a, b) => (a.postName || '').localeCompare(b.postName || ''))
+ })
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.type-card {
+ margin-bottom: 20px;
+}
+
+.type-selector {
+ display: flex;
+ gap: 20px;
+}
+
+.type-item {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ padding: 20px;
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.type-item:hover {
+ border-color: #409eff;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.type-item.active {
+ border-color: #409eff;
+ background-color: #ecf5ff;
+}
+
+.type-icon {
+ margin-right: 15px;
+ color: #409eff;
+}
+
+.type-name {
+ font-size: 16px;
+ font-weight: 500;
+ color: #303133;
+ margin-bottom: 5px;
+}
+
+.type-desc {
+ font-size: 14px;
+ color: #909399;
+}
+
+.form-header {
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid #ebeef5;
+}
+
+.form-header h3 {
+ margin: 0;
+ color: #303133;
+}
+
+.form-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px solid #ebeef5;
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue b/src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue
new file mode 100644
index 0000000..11d1774
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/meetDraft/index.vue
@@ -0,0 +1,495 @@
+<template>
+ <div>
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <h2>浼氳鑽夌</h2>
+ <el-button type="primary" @click="handleAdd">
+ <el-icon><Plus /></el-icon>
+ 鏂板缓鑽夌
+ </el-button>
+ </div>
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-card class="search-card">
+ <el-form :model="searchForm" label-width="100px" inline>
+ <el-form-item label="浼氳涓婚">
+ <el-input v-model="searchForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�" clearable />
+ </el-form-item>
+ <el-form-item label="浼氳鏃ユ湡">
+ <el-date-picker
+ v-model="searchForm.meetingDate"
+ type="date"
+ placeholder="璇烽�夋嫨浼氳鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 鑽夌鍒楄〃 -->
+ <el-card>
+ <el-table v-loading="loading" :data="draftList" border>
+ <el-table-column prop="title" label="浼氳涓婚" align="center" min-width="200" show-overflow-tooltip />
+ <el-table-column prop="room" label="浼氳瀹�" align="center" width="120" />
+ <el-table-column prop="host" label="涓绘寔浜�" align="center" width="120" />
+ <el-table-column prop="meetingTime" label="浼氳鏃堕棿" align="center" width="180">
+ <template #default="scope">
+ {{ formatDateTime(scope.row.meetingTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="participants" label="鍙備細浜烘暟" align="center" width="100">
+ <template #default="scope">
+ {{ scope.row.participants }}浜�
+ </template>
+ </el-table-column>
+ <el-table-column prop="createTime" label="鍒涘缓鏃堕棿" align="center" width="180" />
+ <el-table-column label="鎿嶄綔" align="center" width="200" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" link @click="viewDraft(scope.row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="editDraft(scope.row)">缂栬緫</el-button>
+ <el-button type="danger" link @click="deleteDraft(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <!-- 浼氳鑽夌璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ title="浼氳鑽夌璇︽儏"
+ v-model="detailDialogVisible"
+ width="800px"
+ >
+ <div v-if="currentDraft">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="浼氳涓婚">{{ currentDraft.title }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳缂栧彿">{{ currentDraft.meetingId }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳瀹�">{{ currentDraft.room }}</el-descriptions-item>
+ <el-descriptions-item label="涓绘寔浜�">{{ currentDraft.host }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿" :span="2">
+ {{ formatDateTime(currentDraft.meetingTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">{{ currentDraft.createTime }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="content-section mt-20">
+ <h4>鍙備細浜哄憳</h4>
+ <div class="participants-list">
+ {{ currentDraft.participantList }}
+ </div>
+ </div>
+
+ <div class="content-section mt-20">
+ <h4>浼氳璇存槑</h4>
+ <div class="meeting-description">{{ currentDraft.description }}</div>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="detailDialogVisible = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 鏂板缓/缂栬緫鑽夌瀵硅瘽妗� -->
+ <el-dialog
+ :title="dialogTitle"
+ v-model="editDialogVisible"
+ width="700px"
+ >
+ <el-form :model="meetingForm" :rules="rules" ref="meetingFormRef" label-width="100px">
+ <el-form-item label="浼氳涓婚" prop="title">
+ <el-input v-model="meetingForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�" />
+ </el-form-item>
+ <el-form-item label="浼氳瀹�" prop="room">
+ <el-select v-model="meetingForm.roomId" placeholder="璇烽�夋嫨浼氳瀹�" style="width: 100%">
+ <el-option v-for="(v,k) in roomList" :label="v.name" :value="v.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="涓绘寔浜�" prop="host">
+ <el-input v-model="meetingForm.host" placeholder="璇疯緭鍏ヤ富鎸佷汉" />
+ </el-form-item>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浼氳鏃ユ湡" prop="meetingDate">
+ <el-date-picker
+ v-model="meetingForm.meetingDate"
+ type="date"
+ placeholder="璇烽�夋嫨浼氳鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ :disabled-date="disabledDate"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <!-- 绌哄垪锛屼繚鎸佸竷灞� -->
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�濮嬫椂闂�" prop="startTime">
+ <el-select
+ v-model="meetingForm.startTime"
+ placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="time in timeOptions"
+ :key="time.value"
+ :label="time.label"
+ :value="time.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁撴潫鏃堕棿" prop="endTime">
+ <el-select
+ v-model="meetingForm.endTime"
+ placeholder="璇烽�夋嫨缁撴潫鏃堕棿"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="time in timeOptions"
+ :key="time.value"
+ :label="time.label"
+ :value="time.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍙備細浜烘暟" prop="participants">
+ <el-input
+ v-model="meetingForm.participants"
+ type="number"
+ placeholder="璇疯緭鍏ュ弬浼氫汉鏁�"
+ />
+ </el-form-item>
+ <el-form-item label="鍙備細浜哄憳" prop="participants">
+ <el-input
+ v-model="meetingForm.participantList"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ弬浼氫汉鍛橈紝鐢ㄩ�楀彿鍒嗛殧"
+ />
+ </el-form-item>
+ <el-form-item label="浼氳璇存槑">
+ <el-input
+ v-model="meetingForm.description"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ヤ細璁鏄�"
+ />
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="editDialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submitForm">淇� 瀛�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import Pagination from '@/components/Pagination/index.vue'
+import {getRoomEnum,getDraftList,saveDraft,delDraft} from '@/api/collaborativeApproval/meeting.js'
+import dayjs from "dayjs";
+// 鏁版嵁鍒楄〃鍔犺浇鐘舵��
+const loading = ref(false)
+
+// 鎬绘潯鏁�
+const total = ref(0)
+
+// 鑽夌鍒楄〃鏁版嵁
+const draftList = ref([])
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10
+})
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ title: '',
+ meetingDate: ''
+})
+
+// 鏄惁鏄剧ず瀵硅瘽妗�
+const detailDialogVisible = ref(false)
+const editDialogVisible = ref(false)
+
+const roomList = ref([])
+
+// 瀵硅瘽妗嗘爣棰�
+const dialogTitle = ref('')
+
+// 褰撳墠鏌ョ湅鐨勮崏绋�
+const currentDraft = ref(null)
+
+// 琛ㄥ崟寮曠敤
+const meetingFormRef = ref(null)
+
+// 鏃堕棿閫夐」锛堜互鍗婂皬鏃朵负闂撮殧锛屽伐浣滄椂闂�8:00-18:00锛�
+const timeOptions = ref([])
+
+// 琛ㄥ崟鏁版嵁
+const meetingForm = reactive({
+ id: '',
+ meetingId: '',
+ title: '',
+ roomId: '',
+ host: '',
+ meetingDate: '',
+ startTime: '',
+ endTime: '',
+ participants: 0,
+ participantList: '',
+ description: '',
+ createTime: ''
+})
+
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const rules = {
+ title: [{ required: true, message: '璇疯緭鍏ヤ細璁富棰�', trigger: 'blur' }],
+ roomId: [{ required: true, message: '璇烽�夋嫨浼氳瀹�', trigger: 'change' }],
+ host: [{ required: true, message: '璇疯緭鍏ヤ富鎸佷汉', trigger: 'blur' }],
+ meetingDate: [{ required: true, message: '璇烽�夋嫨浼氳鏃ユ湡', trigger: 'change' }],
+ startTime: [{ required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change' }],
+ endTime: [{ required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change' }]
+}
+
+// 鍒濆鍖栨椂闂撮�夐」锛堜互鍗婂皬鏃朵负闂撮殧锛屽伐浣滄椂闂�8:00-18:00锛�
+const initTimeOptions = () => {
+ const options = []
+ for (let hour = 8; hour <= 18; hour++) {
+ // 姣忎釜灏忔椂娣诲姞涓や釜閫夐」锛氭暣鐐瑰拰鍗婄偣
+ options.push({
+ value: `${hour.toString().padStart(2, '0')}:00`,
+ label: `${hour.toString().padStart(2, '0')}:00`
+ })
+
+ if (hour < 18) { // 18:00涔嬪悗娌℃湁鍗婄偣閫夐」
+ options.push({
+ value: `${hour.toString().padStart(2, '0')}:30`,
+ label: `${hour.toString().padStart(2, '0')}:30`
+ })
+ }
+ }
+ timeOptions.value = options
+}
+
+// 绂佺敤鏃ユ湡锛堢鐢ㄤ粖澶╀箣鍓嶇殑鏃ユ湡锛�
+const disabledDate = (time) => {
+ // 绂佺敤浠婂ぉ涔嬪墠鐨勬棩鏈�
+ return time.getTime() < Date.now() - 86400000
+}
+
+// 鏌ヨ鏁版嵁
+const getList = async () => {
+ loading.value = true
+
+ let resp = await getDraftList({...queryParams,...searchForm})
+ queryParams.current = resp.data.current
+ draftList.value = resp.data.records.map(it=>{
+ it.room = roomList.value.find(room=>it.roomId===room.id).name ?? ""
+ it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format("HH:mm")} ~ ${dayjs(it.endTime).format("HH:mm")}`
+ return it
+ })
+
+ loading.value = false
+
+}
+
+// 鎼滅储鎸夐挳鎿嶄綔
+const handleSearch = () => {
+ queryParams.pageNum = 1
+ getList()
+}
+
+// 閲嶇疆鎼滅储琛ㄥ崟
+const resetSearch = () => {
+ Object.assign(searchForm, {
+ title: '',
+ createTime: []
+ })
+ handleSearch()
+}
+
+// 娣诲姞鎸夐挳鎿嶄綔
+const handleAdd = () => {
+ dialogTitle.value = '鏂板缓鑽夌'
+ resetForm()
+ editDialogVisible.value = true
+}
+
+// 鏌ョ湅鑽夌璇︽儏
+const viewDraft = (row) => {
+ currentDraft.value = row
+ detailDialogVisible.value = true
+}
+
+// 缂栬緫鑽夌
+const editDraft = (row) => {
+ dialogTitle.value = '缂栬緫鑽夌'
+ Object.assign(meetingForm, {
+ id: row.id,
+ meetingId: row.meetingId,
+ title: row.title,
+ room: row.room,
+ roomId: row.id,
+ host: row.host,
+ meetingDate: row.meetingTime.split(' ')[0],
+ startTime: row.meetingTime.split(' ')[1],
+ endTime: row.meetingTime.split(' ')[3],
+ participants: row.participants,
+ participantList: row.participantList,
+ description: row.description,
+ createTime: row.createTime
+ })
+ editDialogVisible.value = true
+}
+
+// 鍒犻櫎鑽夌
+const deleteDraft = (row) => {
+ ElMessageBox.confirm(
+ `纭鍒犻櫎浼氳鑽夌 "${row.title}"?`,
+ '鍒犻櫎鑽夌',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ ).then(() => {
+ delDraft(row.id).then(resp=>{
+ ElMessage.success('鑽夌鍒犻櫎鎴愬姛')
+ getList()
+ })
+
+ }).catch(() => {})
+}
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+ Object.assign(meetingForm, {
+ id: '',
+ meetingId: '',
+ title: '',
+ room: '',
+ host: '',
+ meetingDate: '',
+ startTime: '',
+ endTime: '',
+ participants: 0,
+ participantList: '',
+ description: '',
+ createTime: ''
+ })
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ meetingFormRef.value.validate((valid) => {
+ if (valid) {
+ let formData = {...meetingForm}
+ formData.startTime = dayjs(meetingForm.meetingDate + ' ' + meetingForm.startTime).format("YYYY-MM-DD HH:mm:ss")
+ formData.endTime = dayjs(meetingForm.meetingDate + ' ' + meetingForm.endTime).format("YYYY-MM-DD HH:mm:ss")
+ saveDraft(formData).then(()=>{
+ ElMessage.success('淇濆瓨鎴愬姛')
+ editDialogVisible.value = false
+ getList()
+ })
+ }
+ })
+}
+
+// 鏍煎紡鍖栨棩鏈熸椂闂�
+const formatDateTime = (dateTime) => {
+ if (!dateTime) return ''
+ return dateTime.replace(' ', '\n')
+}
+
+// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
+onMounted(() => {
+ initTimeOptions()
+ getList()
+ getRoomEnum().then((res) => {
+ roomList.value = res.data
+ })
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.search-card {
+ margin-bottom: 20px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.content-section h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+}
+
+.mt-20 {
+ margin-top: 20px;
+}
+
+.participants-list {
+ min-height: 40px;
+ padding: 15px;
+ border-radius: 4px;
+ line-height: 1.6;
+}
+
+.meeting-description {
+ padding: 15px;
+ border-radius: 4px;
+ line-height: 1.6;
+ white-space: pre-wrap;
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue b/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
new file mode 100644
index 0000000..26e2c24
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
@@ -0,0 +1,414 @@
+<template>
+ <div>
+
+ <el-form :model="searchForm" inline>
+ <el-form-item label="浼氳涓婚">
+ <el-input v-model="searchForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�" clearable/>
+ </el-form-item>
+ <el-form-item label="鐢宠浜�">
+ <el-input v-model="searchForm.applicant" placeholder="璇疯緭鍏ョ敵璇蜂汉" clearable/>
+ </el-form-item>
+ <el-form-item label="瀹℃壒鐘舵��">
+ <el-select style="width: 100px" v-model="searchForm.status" placeholder="璇烽�夋嫨瀹℃壒鐘舵��" clearable>
+ <el-option label="寰呭鎵�" value="0"/>
+ <el-option label="宸查�氳繃" value="1"/>
+ <el-option label="鏈鎵�" value="2"/>
+ <el-option label="宸插彇娑�" value="3"/>
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 浼氳瀹℃壒鍒楄〃 -->
+ <el-card>
+ <el-table v-loading="loading" :data="approvalList" border :height="tableHeight">
+ <el-table-column prop="title" label="浼氳涓婚" align="center" min-width="200" show-overflow-tooltip/>
+ <el-table-column prop="applicant" label="鐢宠浜�" align="center" width="120"/>
+ <el-table-column prop="host" label="涓荤悊浜�" align="center" width="120"/>
+ <el-table-column prop="meetingTime" label="浼氳鏃堕棿" align="center" width="180">
+ <template #default="scope">
+ {{ formatDateTime(scope.row.meetingTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="location" label="浼氳鍦扮偣" align="center" width="150"/>
+ <el-table-column prop="participants" label="鍙備細浜烘暟" align="center" width="100">
+ <template #default="scope">
+ {{ scope.row.participants.length }}浜�
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="瀹℃壒鐘舵��" align="center" width="120">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ getStatusText(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="200" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" link @click="viewDetail(scope.row)">鏌ョ湅</el-button>
+ <el-button
+ v-if="scope.row.status == '0'"
+ type="primary"
+ link
+ @click="handleApproval(scope.row)"
+ >
+ 瀹℃壒
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <!-- 浼氳璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ title="浼氳璇︽儏"
+ v-model="detailDialogVisible"
+ width="800px"
+ >
+ <div v-if="currentMeeting">
+ <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
+ <el-descriptions-item label="浼氳涓婚" label-class-name="nowrap-label">{{
+ currentMeeting.title
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�" label-class-name="nowrap-label">{{
+ currentMeeting.applicant
+ }}</el-descriptions-item>
+ <el-descriptions-item label="涓荤悊浜�" label-class-name="nowrap-label">{{
+ currentMeeting.host
+ }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿" :span="2" label-class-name="nowrap-label">
+ {{ formatDateTime(currentMeeting.meetingTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍦扮偣" label-class-name="nowrap-label">{{
+ currentMeeting.location
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜烘暟" label-class-name="nowrap-label">{{
+ currentMeeting.participants.length
+ }}浜�</el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒鐘舵��" label-class-name="nowrap-label">
+ <el-tag :type="getStatusType(currentMeeting.status)">
+ {{ getStatusText(currentMeeting.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏃堕棿" label-class-name="nowrap-label">{{
+ currentMeeting.createTime
+ }}</el-descriptions-item>
+ <el-descriptions-item style="max-height: 400px" label="浼氳璇存槑" :span="2"
+ label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
+ </el-descriptions>
+
+
+ <div class="content-section mt-20">
+ <h4>鍙備細浜哄憳</h4>
+ <div class="participants-list">
+ <el-tag
+ v-for="participant in currentMeeting.participants"
+ :key="participant.id"
+ style="margin-right: 10px; margin-bottom: 10px;"
+ >
+ {{ participant.name }}
+ </el-tag>
+ </div>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="detailDialogVisible = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 浼氳瀹℃壒瀵硅瘽妗� -->
+ <el-dialog
+ title="浼氳瀹℃壒"
+ v-model="approvalDialogVisible"
+ >
+ <div v-if="currentMeeting">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="浼氳涓婚">{{ currentMeeting.title }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�">{{ currentMeeting.applicant }}</el-descriptions-item>
+ <el-descriptions-item label="涓荤悊浜�">{{ currentMeeting.host }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿" :span="2">
+ {{ formatDateTime(currentMeeting.meetingTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍦扮偣">{{ currentMeeting.location }}</el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜烘暟">{{ currentMeeting.participants.length }}浜�</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="content-section mt-20">
+ <h4>鍙備細浜哄憳</h4>
+ <div class="participants-list">
+ <el-tag
+ v-for="participant in currentMeeting.participants"
+ :key="participant.id"
+ style="margin-right: 10px; margin-bottom: 10px;"
+ >
+ {{ participant.name }}
+ </el-tag>
+ </div>
+ </div>
+
+ <div v-show="false" class="approval-opinion mt-20">
+ <h4>瀹℃壒鎰忚</h4>
+ <el-input
+ v-model="approvalOpinion"
+ type="textarea"
+ placeholder="璇疯緭鍏ュ鎵规剰瑙�"
+ :rows="4"
+ />
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="approvalDialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="danger" @click="submitApproval('2')">涓嶉�氳繃</el-button>
+ <el-button type="primary" @click="submitApproval('1')">閫� 杩�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, onMounted} from 'vue'
+import {ElMessage, ElMessageBox} from 'element-plus'
+import Pagination from '@/components/Pagination/index.vue'
+import {getRoomEnum, getExamineList,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
+import dayjs from "dayjs";
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+
+// 鏁版嵁鍒楄〃鍔犺浇鐘舵��
+const loading = ref(false)
+
+// 鎬绘潯鏁�
+const total = ref(0)
+
+// 琛ㄦ牸楂樺害锛堟牴鎹獥鍙i珮搴﹁嚜閫傚簲锛�
+const tableHeight = ref(window.innerHeight - 380)
+const roomEnum = ref([])
+const staffList = ref([])
+// 瀹℃壒鍒楄〃鏁版嵁
+const approvalList = ref([])
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10
+})
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ title: '',
+ applicant: '',
+ status: ''
+})
+
+// 鏄惁鏄剧ず瀵硅瘽妗�
+const detailDialogVisible = ref(false)
+const approvalDialogVisible = ref(false)
+
+// 褰撳墠鏌ョ湅鐨勪細璁�
+const currentMeeting = ref(null)
+
+// 瀹℃壒鎰忚
+const approvalOpinion = ref('')
+
+// 鏌ヨ鏁版嵁
+const getList = async () => {
+ loading.value = true
+ let resp = await getExamineList({...searchForm, ...queryParams})
+ approvalList.value = resp.data.records.map(it => {
+ let room = roomEnum.value.find(room => it.roomId === room.id)
+ it.location = `${room.name}(${room.location})`
+ let staffs = JSON.parse(it.participants)
+ it.staffCount = staffs.size
+ it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
+ it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
+ return {
+ id: staff.id,
+ name: `${staff.staffName}${staff.postName ? ` (${staff.postName})` : ''}`
+ }
+ })
+
+
+ return it
+ })
+ total.value = resp.data.total
+ loading.value = false
+}
+
+// 鎼滅储鎸夐挳鎿嶄綔
+const handleSearch = () => {
+ queryParams.pageNum = 1
+ getList()
+}
+
+// 閲嶇疆鎼滅储琛ㄥ崟
+const resetSearch = () => {
+ Object.assign(searchForm, {
+ title: '',
+ applicant: '',
+ status: ''
+ })
+ handleSearch()
+}
+
+// 鏌ョ湅璇︽儏
+const viewDetail = (row) => {
+ currentMeeting.value = row
+ detailDialogVisible.value = true
+}
+
+// 澶勭悊瀹℃壒
+const handleApproval = (row) => {
+ currentMeeting.value = row
+ approvalOpinion.value = ''
+ approvalDialogVisible.value = true
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const statusMap = {
+ '0': 'info', // 寰呭鎵�
+ '1': 'success', // 宸查�氳繃
+ '2': 'warning', // 鏈�氳繃
+ '3': 'danger' // 鍙栨秷
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ '0': '寰呭鎵�',
+ '1': '宸查�氳繃',
+ '2': '鏈�氳繃',
+ '3': '宸插彇娑�'
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+// 鏍煎紡鍖栨棩鏈熸椂闂�
+const formatDateTime = (dateTime) => {
+ if (!dateTime) return ''
+ return dateTime.replace(' ', '\n')
+}
+
+// 鎻愪氦瀹℃壒
+const submitApproval = (status) => {
+ // if (status === 'approved' && !approvalOpinion.value.trim()) {
+ // ElMessage.warning('璇峰~鍐欏鎵规剰瑙�')
+ // return
+ // }
+
+ ElMessageBox.confirm(
+ `纭${status === '1' ? '閫氳繃' : '涓嶉�氳繃'}璇ヤ細璁敵璇凤紵`,
+ '瀹℃壒纭',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ ).then(() => {
+ saveMeetingApplication({
+ id: currentMeeting.value.id,
+ status: status
+ }).then(resp=>{
+ // 鏇存柊浼氳鐘舵��
+ currentMeeting.value.status = status
+
+ ElMessage.success('瀹℃壒鎻愪氦鎴愬姛')
+ approvalDialogVisible.value = false
+ getList()
+ })
+
+ }).catch(() => {
+ })
+}
+
+// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
+onMounted(async () => {
+ const [resp1, resp2]= await Promise.all([getRoomEnum(), staffOnJobListPage({current: -1, size: -1, staffState: 1})])
+ roomEnum.value = resp1.data
+ staffList.value = resp2.data.records
+
+ await getList()
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.search-card {
+ margin-bottom: 20px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.content-section h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+}
+
+.mt-20 {
+ margin-top: 20px;
+}
+
+.participants-list {
+ min-height: 40px;
+ padding: 15px;
+ border-radius: 4px;
+ line-height: 1.6;
+}
+
+.approval-opinion h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+}
+
+.nowrap-label {
+ white-space: nowrap !important;
+}
+
+.description-content {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ line-height: 1.6;
+ padding: 10px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ min-height: 60px;
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue b/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
new file mode 100644
index 0000000..9b5325f
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
@@ -0,0 +1,363 @@
+<template>
+ <div>
+ <el-form :model="queryForm" label-width="80px" inline>
+ <el-form-item label="鏌ヨ鏃ユ湡">
+ <el-date-picker
+ v-model="queryForm.meetingDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ :clearable="false"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鏌ヨ</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 浼氳瀹や娇鐢ㄦ儏鍐� -->
+ <el-card class="table-container" :loading="loading">
+ <div class="time-table">
+ <!-- 琛ㄥご -->
+ <div class="table-header">
+ <div class="header-cell room-header">浼氳瀹�</div>
+ <div
+ v-for="timeSlot in timeSlots"
+ :key="timeSlot.value"
+ class="header-cell time-header"
+ >
+ {{ timeSlot.label }}
+ </div>
+ </div>
+
+ <!-- 琛ㄦ牸鍐呭 -->
+ <div class="table-body">
+ <div
+ v-for="room in roomUsage"
+ :key="room.id"
+ class="table-row"
+ >
+ <div class="cell room-cell">{{ room.name }}</div>
+ <div class="cells-container">
+ <template v-for="(cell, index) in generateMeetingCells(room)" :key="index">
+ <div
+ class="cell content-cell"
+ :class="[cell.type, `status-${cell.meeting?.status || '0'}`]"
+ :style="{ flex: cell.span-0.2 }"
+ @click="viewMeetingDetails(cell)"
+ >
+ <div v-if="cell.type === 'meeting'" class="meeting-content">
+ <div class="meeting-title">{{ cell.meeting.title }}</div>
+ <div class="meeting-time">{{ cell.startTime }}-{{ cell.endTime }}</div>
+ </div>
+ <div v-else class="free-content">
+ 绌洪棽
+ </div>
+ </div>
+ </template>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 浼氳璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ title="浼氳璇︽儏"
+ v-model="detailDialogVisible"
+ width="800px"
+ >
+ <div v-if="currentMeeting">
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="浼氳涓婚">{{ currentMeeting.title }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳瀹�">{{ currentMeeting.room }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿">{{ currentMeeting.time }}</el-descriptions-item>
+ <el-descriptions-item label="涓绘寔浜�">{{ currentMeeting.host }}</el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜烘暟">{{ currentMeeting.participants }}浜�</el-descriptions-item>
+ <el-descriptions-item label="浼氳璇存槑">{{ currentMeeting.description }}</el-descriptions-item>
+ </el-descriptions>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="detailDialogVisible = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, onMounted} from 'vue'
+import {ElMessage} from 'element-plus'
+import {getMeetingUseList} from "@/api/collaborativeApproval/meeting.js"
+import dayjs from "dayjs";
+
+// 鏌ヨ琛ㄥ崟
+const queryForm = reactive({
+ meetingDate: dayjs().format('YYYY-MM-DD')
+})
+let loading = ref(false)
+// 鏃堕棿娈碉紙浠ュ崐灏忔椂涓洪棿闅旓級
+const timeSlots = ref([])
+
+// 浼氳瀹や娇鐢ㄦ儏鍐�
+const roomUsage = ref([])
+
+// 褰撳墠鏌ョ湅鐨勪細璁�
+const currentMeeting = ref(null)
+
+// 鏄惁鏄剧ず璇︽儏瀵硅瘽妗�
+const detailDialogVisible = ref(false)
+
+// 鍒濆鍖栨椂闂存Ы锛堜互鍗婂皬鏃朵负闂撮殧锛屼粠8:00鍒�18:00锛�
+const initTimeSlots = () => {
+ const slots = []
+ for (let hour = 8; hour < 18; hour++) {
+ // 姣忎釜灏忔椂娣诲姞涓や釜鏃堕棿娈碉細鏁寸偣鍜屽崐鐐�
+ slots.push({
+ label: `${hour.toString().padStart(2, '0')}:00`,
+ value: `${hour.toString().padStart(2, '0')}:00`
+ })
+
+ if (hour < 18) { // 鍒�17:30涓烘
+ slots.push({
+ label: `${hour.toString().padStart(2, '0')}:30`,
+ value: `${hour.toString().padStart(2, '0')}:30`
+ })
+ }
+ }
+ timeSlots.value = slots
+}
+
+// 鐢熸垚浼氳瀹ょ殑鏃堕棿鍗曞厓鏍�
+const generateMeetingCells = (room) => {
+ const cells = []
+ const meetings = room.meetings || []
+ const occupiedSlots = new Set()
+
+ // 澶勭悊姣忎釜浼氳
+ for (const meeting of meetings) {
+
+ const startIdx = timeSlots.value.findIndex(slot => slot.value === meeting.startTime)
+ let endIdx = timeSlots.value.findIndex(slot => slot.value === meeting.endTime)
+ if (endIdx === -1) {
+ endIdx = timeSlots.value.length
+ }
+ console.log('endIdx111', endIdx)
+ if (startIdx !== -1 && endIdx !== -1) {
+ // 鏍囪琚崰鐢ㄧ殑鏃堕棿娈�
+ for (let i = startIdx; i < endIdx; i++) {
+ occupiedSlots.add(timeSlots.value[i].value)
+ }
+
+ // 鍒涘缓浼氳鍗曞厓鏍�
+ cells.push({
+ type: 'meeting',
+ meeting: meeting,
+ span: endIdx - startIdx,
+ startTime: meeting.startTime,
+ endTime: meeting.endTime
+ })
+ }
+ }
+
+ // 澶勭悊绌洪棽鏃堕棿娈�
+ for (let i = 0; i < timeSlots.value.length; i++) {
+ const slot = timeSlots.value[i]
+ if (!occupiedSlots.has(slot.value)) {
+ // 鏌ユ壘杩炵画鐨勭┖闂叉椂闂存
+ let span = 1
+ while (i + span < timeSlots.value.length &&
+ !occupiedSlots.has(timeSlots.value[i + span].value)) {
+ occupiedSlots.add(timeSlots.value[i + span].value)
+ span++
+ }
+
+ cells.push({
+ type: 'free',
+ span: span,
+ time: slot.value
+ })
+ }
+ }
+
+ // 鎸夋椂闂存帓搴�
+ cells.sort((a, b) => {
+ const timeA = a.startTime || a.time
+ const timeB = b.startTime || b.time
+ return timeSlots.value.findIndex(s => s.value === timeA) -
+ timeSlots.value.findIndex(s => s.value === timeB)
+ })
+ console.log('cells', cells)
+ return cells
+}
+
+// 鏌ョ湅浼氳璇︽儏
+const viewMeetingDetails = (cell) => {
+ if (cell && cell.type === 'meeting') {
+ currentMeeting.value = cell.meeting
+ detailDialogVisible.value = true
+ } else {
+ ElMessage.info('璇ユ椂闂存浼氳瀹ょ┖闂�')
+ }
+}
+
+// 鏌ヨ鎸夐挳鎿嶄綔
+const handleSearch = async () => {
+ loading.value = true
+ let resp = await getMeetingUseList({...queryForm})
+ roomUsage.value = resp.data
+ loading.value = false
+}
+
+// 閲嶇疆鎼滅储琛ㄥ崟
+const resetSearch = () => {
+ queryForm.date = dayjs().format('YYYY-MM-DD')
+}
+
+// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
+onMounted(() => {
+ // 鍒濆鍖栨椂闂存Ы
+ initTimeSlots()
+
+ // 榛樿鏌ヨ浠婂ぉ鐨勬暟鎹�
+ const today = new Date()
+ queryForm.date = today.toISOString().split('T')[0]
+ handleSearch()
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.search-card {
+ margin-bottom: 20px;
+}
+
+.table-container {
+ padding: 0;
+}
+
+.time-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.table-header {
+ display: flex;
+ border: 1px solid;
+}
+
+.table-row {
+ display: flex;
+ border: 1px solid #ebeef5;
+ border-top: none;
+}
+
+.header-cell {
+ padding: 12px 5px;
+ text-align: center;
+ font-weight: bold;
+ border-right: 1px solid;
+ min-height: 20px;
+}
+
+.room-header {
+ width: 120px;
+}
+
+.time-header {
+ flex: 1;
+}
+
+.cell {
+ padding: 15px 5px;
+ text-align: center;
+ border-right: 1px solid;
+ min-height: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ word-break: break-word;
+ line-height: 1.2;
+}
+
+.room-cell {
+ width: 120px;
+ font-weight: bold;
+}
+
+.cells-container {
+ flex: 1;
+ display: flex;
+}
+
+.content-cell {
+ min-height: 60px;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.content-cell:hover {
+ opacity: 0.8;
+}
+
+.free {
+ color: #f56c6c;
+}
+
+.meeting {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.status-1 {
+ background-color: #fef0f0;
+ color: #d14646;
+}
+
+.status-0 {
+ background-color: #c7ddc8;
+ color: rgba(230, 162, 60, 0.29);
+}
+
+.meeting-content {
+ width: 100%;
+}
+
+.meeting-title {
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+.meeting-time {
+ font-size: 12px;
+}
+
+.free-content {
+ color: #909399;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue b/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
new file mode 100644
index 0000000..dace531
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
@@ -0,0 +1,412 @@
+<template>
+ <div>
+
+ <el-form :model="searchForm" inline>
+ <el-form-item label="浼氳涓婚">
+ <el-input v-model="searchForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�" clearable/>
+ </el-form-item>
+ <el-form-item label="鐢宠浜�">
+ <el-input v-model="searchForm.applicant" placeholder="璇疯緭鍏ョ敵璇蜂汉" clearable/>
+ </el-form-item>
+ <el-form-item label="鍙戝竷鐘舵��">
+ <el-select style="width: 100px" v-model="searchForm.status" placeholder="璇烽�夋嫨鍙戝竷鐘舵��" clearable>
+ <el-option label="寰呭彂甯�" value="0"/>
+ <el-option label="宸插彂甯�" value="1"/>
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 浼氳鍙戝竷鍒楄〃 -->
+ <el-card>
+ <el-table v-loading="loading" :data="approvalList" border :height="tableHeight">
+ <el-table-column prop="title" label="浼氳涓婚" align="center" min-width="200" show-overflow-tooltip/>
+ <el-table-column prop="applicant" label="鐢宠浜�" align="center" width="120"/>
+ <el-table-column prop="host" label="涓荤悊浜�" align="center" width="120"/>
+ <el-table-column prop="meetingTime" label="浼氳鏃堕棿" align="center" width="180">
+ <template #default="scope">
+ {{ formatDateTime(scope.row.meetingTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="location" label="浼氳鍦扮偣" align="center" width="150"/>
+ <el-table-column prop="participants" label="鍙備細浜烘暟" align="center" width="100">
+ <template #default="scope">
+ {{ scope.row.participants.length }}浜�
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="鍙戝竷鐘舵��" align="center" width="120">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ getStatusText(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="200" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" link @click="viewDetail(scope.row)">鏌ョ湅</el-button>
+ <el-button
+ v-if="scope.row.status == '0'"
+ type="primary"
+ link
+ @click="handleApproval(scope.row)"
+ >
+ 鍙戝竷
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <!-- 浼氳璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ title="浼氳璇︽儏"
+ v-model="detailDialogVisible"
+ width="800px"
+ >
+ <div v-if="currentMeeting">
+ <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
+ <el-descriptions-item label="浼氳涓婚" label-class-name="nowrap-label">{{
+ currentMeeting.title
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�" label-class-name="nowrap-label">{{
+ currentMeeting.applicant
+ }}</el-descriptions-item>
+ <el-descriptions-item label="涓荤悊浜�" label-class-name="nowrap-label">{{
+ currentMeeting.host
+ }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿" :span="2" label-class-name="nowrap-label">
+ {{ formatDateTime(currentMeeting.meetingTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍦扮偣" label-class-name="nowrap-label">{{
+ currentMeeting.location
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜烘暟" label-class-name="nowrap-label">{{
+ currentMeeting.participants.length
+ }}浜�</el-descriptions-item>
+ <el-descriptions-item label="鍙戝竷鐘舵��" label-class-name="nowrap-label">
+ <el-tag :type="getStatusType(currentMeeting.status)">
+ {{ getStatusText(currentMeeting.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏃堕棿" label-class-name="nowrap-label">{{
+ currentMeeting.createTime
+ }}</el-descriptions-item>
+ <el-descriptions-item style="max-height: 400px" label="浼氳璇存槑" :span="2"
+ label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
+ </el-descriptions>
+
+
+ <div class="content-section mt-20">
+ <h4>鍙備細浜哄憳</h4>
+ <div class="participants-list">
+ <el-tag
+ v-for="participant in currentMeeting.participants"
+ :key="participant.id"
+ style="margin-right: 10px; margin-bottom: 10px;"
+ >
+ {{ participant.name }}
+ </el-tag>
+ </div>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="detailDialogVisible = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 浼氳鍙戝竷瀵硅瘽妗� -->
+ <el-dialog
+ title="浼氳鍙戝竷"
+ v-model="approvalDialogVisible"
+ >
+ <div v-if="currentMeeting">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="浼氳涓婚">{{ currentMeeting.title }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�">{{ currentMeeting.applicant }}</el-descriptions-item>
+ <el-descriptions-item label="涓荤悊浜�">{{ currentMeeting.host }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿" :span="2">
+ {{ formatDateTime(currentMeeting.meetingTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍦扮偣">{{ currentMeeting.location }}</el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜烘暟">{{ currentMeeting.participants.length }}浜�</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="content-section mt-20">
+ <h4>鍙備細浜哄憳</h4>
+ <div class="participants-list">
+ <el-tag
+ v-for="participant in currentMeeting.participants"
+ :key="participant.id"
+ style="margin-right: 10px; margin-bottom: 10px;"
+ >
+ {{ participant.name }}
+ </el-tag>
+ </div>
+ </div>
+
+ <div class="approval-opinion mt-20">
+ <h4>鍙戝竷鎰忚</h4>
+ <el-input
+ v-model="publishComment"
+ type="textarea"
+ placeholder="璇疯緭鍏ュ彂甯冩剰瑙�"
+ :rows="4"
+ />
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="approvalDialogVisible = false">鍙� 娑�</el-button>
+<!-- <el-button type="danger" @click="submitApproval('2')">涓嶉�氳繃</el-button>-->
+ <el-button type="primary" @click="submitApproval('1')">鍙� 甯�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, onMounted} from 'vue'
+import {ElMessage, ElMessageBox} from 'element-plus'
+import Pagination from '@/components/Pagination/index.vue'
+import {getRoomEnum, getMeetingPublish,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js'
+import dayjs from "dayjs";
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+
+// 鏁版嵁鍒楄〃鍔犺浇鐘舵��
+const loading = ref(false)
+
+// 鎬绘潯鏁�
+const total = ref(0)
+
+// 琛ㄦ牸楂樺害锛堟牴鎹獥鍙i珮搴﹁嚜閫傚簲锛�
+const tableHeight = ref(window.innerHeight - 380)
+const roomEnum = ref([])
+const staffList = ref([])
+// 鍙戝竷鍒楄〃鏁版嵁
+const approvalList = ref([])
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10
+})
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ title: '',
+ applicant: '',
+ status: ''
+})
+
+// 鏄惁鏄剧ず瀵硅瘽妗�
+const detailDialogVisible = ref(false)
+const approvalDialogVisible = ref(false)
+
+// 褰撳墠鏌ョ湅鐨勪細璁�
+const currentMeeting = ref(null)
+
+// 鍙戝竷鎰忚
+const publishComment = ref('')
+
+// 鏌ヨ鏁版嵁
+const getList = async () => {
+ loading.value = true
+ let resp = await getMeetingPublish({...searchForm, ...queryParams})
+ approvalList.value = resp.data.records.map(it => {
+ let room = roomEnum.value.find(room => it.roomId === room.id)
+ it.location = `${room.name}(${room.location})`
+ let staffs = JSON.parse(it.participants)
+ it.staffCount = staffs.size
+ it.status = it.publishStatus
+ it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
+ it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => {
+ return {
+ id: staff.id,
+ name: `${staff.staffName}${staff.postName ? ` (${staff.postName})` : ''}`
+ }
+ })
+
+
+ return it
+ })
+ total.value = resp.data.total
+ loading.value = false
+}
+
+// 鎼滅储鎸夐挳鎿嶄綔
+const handleSearch = () => {
+ queryParams.pageNum = 1
+ getList()
+}
+
+// 閲嶇疆鎼滅储琛ㄥ崟
+const resetSearch = () => {
+ Object.assign(searchForm, {
+ title: '',
+ applicant: '',
+ status: ''
+ })
+ handleSearch()
+}
+
+// 鏌ョ湅璇︽儏
+const viewDetail = (row) => {
+ currentMeeting.value = row
+ detailDialogVisible.value = true
+}
+
+// 澶勭悊鍙戝竷
+const handleApproval = (row) => {
+ currentMeeting.value = row
+ publishComment.value = ''
+ approvalDialogVisible.value = true
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const statusMap = {
+ '0': 'info', // 寰呭彂甯�
+ '1': 'success', // 宸查�氳繃
+ '2': 'danger', // 鏈�氳繃
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ '0': '寰呭彂甯�',
+ '1': '宸插彂甯�',
+ '2': '宸插彇娑�',
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+// 鏍煎紡鍖栨棩鏈熸椂闂�
+const formatDateTime = (dateTime) => {
+ if (!dateTime) return ''
+ return dateTime.replace(' ', '\n')
+}
+
+// 鎻愪氦鍙戝竷
+const submitApproval = (status) => {
+ // if (status === 'approved' && !publishComment.value.trim()) {
+ // ElMessage.warning('璇峰~鍐欏彂甯冩剰瑙�')
+ // return
+ // }
+
+ ElMessageBox.confirm(
+ `纭${status === '1' ? '鍙戝竷' : '鍙栨秷'}璇ヤ細璁紵`,
+ '鍙戝竷纭',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ ).then(() => {
+ saveMeetingApplication({
+ id: currentMeeting.value.id,
+ publishStatus: status,
+ publishComment: publishComment.value
+ }).then(resp=>{
+ // 鏇存柊浼氳鐘舵��
+ currentMeeting.value.status = status
+
+ ElMessage.success('鍙戝竷鎻愪氦鎴愬姛')
+ approvalDialogVisible.value = false
+ getList()
+ })
+
+ }).catch(() => {
+ })
+}
+
+// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
+onMounted(async () => {
+ const [resp1, resp2]= await Promise.all([getRoomEnum(), staffOnJobListPage({current: -1, size: -1, staffState: 1})])
+ roomEnum.value = resp1.data
+ staffList.value = resp2.data.records
+
+ await getList()
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.search-card {
+ margin-bottom: 20px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.content-section h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+}
+
+.mt-20 {
+ margin-top: 20px;
+}
+
+.participants-list {
+ min-height: 40px;
+ padding: 15px;
+ border-radius: 4px;
+ line-height: 1.6;
+}
+
+.approval-opinion h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+}
+
+.nowrap-label {
+ white-space: nowrap !important;
+}
+
+.description-content {
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ line-height: 1.6;
+ padding: 10px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ min-height: 60px;
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue b/src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue
new file mode 100644
index 0000000..636fd79
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue
@@ -0,0 +1,318 @@
+<template>
+ <div>
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-form :model="searchForm" label-width="100px" class="search-form">
+ <el-form-item label="浼氳瀹ゅ悕绉�">
+ <el-input v-model="searchForm.name" placeholder="璇疯緭鍏ヤ細璁鍚嶇О" clearable />
+ </el-form-item>
+ <el-form-item label="浣嶇疆">
+ <el-input v-model="searchForm.location" placeholder="璇疯緭鍏ヤ綅缃�" clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ <el-form-item class="search-actions">
+ <el-button @click="handleExport">瀵煎嚭</el-button>
+ <el-button type="primary" @click="handleAdd">
+ <el-icon><Plus /></el-icon>
+ 鏂板浼氳瀹�
+ </el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 浼氳瀹ゅ垪琛� -->
+ <el-card>
+ <el-table v-loading="loading" :data="meetingRoomList" border :height="tableHeight">
+ <el-table-column prop="name" label="浼氳瀹ゅ悕绉�" align="center" />
+ <el-table-column prop="location" label="浣嶇疆" align="center" />
+ <el-table-column prop="capacity" label="瀹圭撼浜烘暟" align="center" />
+ <el-table-column prop="equipment" label="璁惧閰嶇疆" align="center">
+ <template #default="scope">
+ <el-tag v-for="item in scope.row.equipment" :key="item" style="margin-right: 5px; margin-bottom: 5px;">
+ {{ item }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="鐘舵��" align="center" width="100">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
+ {{ scope.row.status === 1 ? '鍚敤' : '绂佺敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="200">
+ <template #default="scope">
+ <el-button type="primary" link @click="handleEdit(scope.row)">缂栬緫</el-button>
+ <el-button type="danger" link @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <!-- 娣诲姞/缂栬緫瀵硅瘽妗� -->
+ <el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" @close="cancel">
+ <el-form ref="meetingRoomFormRef" :model="meetingRoomForm" :rules="rules" label-width="100px">
+ <el-form-item label="浼氳瀹ゅ悕绉�" prop="name">
+ <el-input v-model="meetingRoomForm.name" placeholder="璇疯緭鍏ヤ細璁鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="浣嶇疆" prop="location">
+ <el-input v-model="meetingRoomForm.location" placeholder="璇疯緭鍏ヤ細璁浣嶇疆" />
+ </el-form-item>
+ <el-form-item label="瀹圭撼浜烘暟" prop="capacity">
+ <el-input-number v-model="meetingRoomForm.capacity" :min="1" placeholder="璇疯緭鍏ュ绾充汉鏁�" />
+ </el-form-item>
+ <el-form-item label="璁惧閰嶇疆" prop="equipment">
+ <el-select v-model="meetingRoomForm.equipment" multiple placeholder="璇烽�夋嫨璁惧閰嶇疆" style="width: 100%">
+ <el-option
+ v-for="item in equipmentOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="meetingRoomForm.status">
+ <el-radio :label="1">鍚敤</el-radio>
+ <el-radio :label="0">绂佺敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="meetingRoomForm.remark" type="textarea" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import Pagination from '@/components/Pagination/index.vue'
+import {getMeetingRoomList,saveRoom,delRoom} from '@/api/collaborativeApproval/meeting.js'
+
+// 鏁版嵁鍒楄〃鍔犺浇鐘舵��
+const loading = ref(false)
+
+// 鎬绘潯鏁�
+const total = ref(0)
+
+// 琛ㄦ牸楂樺害锛堟牴鎹獥鍙i珮搴﹁嚜閫傚簲锛�
+const tableHeight = ref(window.innerHeight - 380)
+
+// 浼氳瀹ゅ垪琛ㄦ暟鎹�
+const meetingRoomList = ref([])
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10
+})
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ name: '',
+ location: ''
+})
+
+// 瀵硅瘽妗嗘爣棰�
+const dialogTitle = ref('')
+
+// 鏄惁鏄剧ず瀵硅瘽妗�
+const dialogVisible = ref(false)
+
+// 璁惧閰嶇疆閫夐」
+const equipmentOptions = ref([
+ { value: '鎶曞奖浠�', label: '鎶曞奖浠�' },
+ { value: '鐢佃', label: '鐢佃' },
+ { value: '闊冲搷', label: '闊冲搷' },
+ { value: '鐢佃瘽', label: '鐢佃瘽' },
+ { value: '瑙嗛浼氳绯荤粺', label: '瑙嗛浼氳绯荤粺' },
+ { value: '鐧芥澘', label: '鐧芥澘' },
+ { value: '鍐欏瓧鏉�', label: '鍐欏瓧鏉�' },
+ { value: '鏃犵嚎缃戠粶', label: '鏃犵嚎缃戠粶' }
+])
+
+// 琛ㄥ崟鏁版嵁
+const meetingRoomForm = reactive({
+ id: undefined,
+ name: '',
+ location: '',
+ capacity: 10,
+ equipment: [],
+ status: 1,
+ remark: ''
+})
+
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const rules = {
+ name: [{ required: true, message: '浼氳瀹ゅ悕绉颁笉鑳戒负绌�', trigger: 'blur' }],
+ location: [{ required: true, message: '浣嶇疆涓嶈兘涓虹┖', trigger: 'blur' }],
+ capacity: [{ required: true, message: '瀹圭撼浜烘暟涓嶈兘涓虹┖', trigger: 'blur' }]
+}
+
+// 琛ㄥ崟寮曠敤
+const meetingRoomFormRef = ref(null)
+
+// 鏌ヨ鏁版嵁
+const getList = async () => {
+ loading.value = true
+
+ let resp = await getMeetingRoomList({...searchForm,...queryParams})
+ meetingRoomList.value = resp.data.records.map(it=>{
+ it.equipment = it.equipment.split(',')
+ return it;
+ })
+ total.value = resp.data.total
+ loading.value = false
+
+}
+
+// 鎼滅储鎸夐挳鎿嶄綔
+const handleSearch = () => {
+ queryParams.current = 1
+ getList()
+}
+
+// 閲嶇疆鎼滅储琛ㄥ崟
+const resetSearch = () => {
+ Object.assign(searchForm, {
+ name: '',
+ location: ''
+ })
+ handleSearch()
+}
+
+// 娣诲姞鎸夐挳鎿嶄綔
+const handleAdd = () => {
+ dialogTitle.value = '娣诲姞浼氳瀹�'
+ dialogVisible.value = true
+}
+
+// 淇敼鎸夐挳鎿嶄綔
+const handleEdit = (row) => {
+ dialogTitle.value = '淇敼浼氳瀹�'
+ Object.assign(meetingRoomForm, row)
+ dialogVisible.value = true
+}
+
+// 鍒犻櫎鎸夐挳鎿嶄綔
+const handleDelete = (row) => {
+ ElMessageBox.confirm(
+ `鏄惁纭鍒犻櫎浼氳瀹� "${row.name}"?`,
+ '璀﹀憡',
+ {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ ).then(() => {
+ // 妯℃嫙鍒犻櫎鎿嶄綔
+ delRoom(row.id).then(resp=>{
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getList()
+ })
+
+ }).catch(() => {})
+}
+
+// 鍙栨秷鎸夐挳
+const cancel = () => {
+ dialogVisible.value = false
+ reset()
+}
+
+// 琛ㄥ崟閲嶇疆
+const reset = () => {
+ Object.assign(meetingRoomForm, {
+ id: undefined,
+ name: '',
+ location: '',
+ capacity: 10,
+ equipment: [],
+ status: 1,
+ remark: ''
+ })
+ meetingRoomFormRef.value?.resetFields()
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ meetingRoomFormRef.value?.validate((valid) => {
+ if (valid) {
+ // 妯℃嫙鎻愪氦鎿嶄綔
+
+ let formData = {... meetingRoomForm}
+ formData.equipment = formData.equipment.join(',')
+ saveRoom(formData).then(resp=>{
+ ElMessage.success('淇濆瓨鎴愬姛')
+ dialogVisible.value = false
+ getList()
+ })
+ }
+ })
+}
+
+// 瀵煎嚭
+const { proxy } = getCurrentInstance()
+const handleExport = () => {
+ proxy.download('/meeting/export', { ...searchForm }, '浼氳瀹よ缃�.xlsx')
+}
+
+// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.search-form {
+ display: flex;
+ /* align-items: center; */
+}
+
+.search-actions {
+ margin-left: auto;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.search-card {
+ margin-bottom: 20px;
+}
+
+.dialog-footer {
+ text-align: center;
+}
+</style>
diff --git a/src/views/collaborativeApproval/notificationManagement/summary/index.vue b/src/views/collaborativeApproval/notificationManagement/summary/index.vue
new file mode 100644
index 0000000..bf6e230
--- /dev/null
+++ b/src/views/collaborativeApproval/notificationManagement/summary/index.vue
@@ -0,0 +1,397 @@
+<template>
+ <div>
+
+ <el-form :model="searchForm" inline>
+ <el-form-item label="浼氳涓婚">
+ <el-input v-model="searchForm.title" placeholder="璇疯緭鍏ヤ細璁富棰�" clearable />
+ </el-form-item>
+ <el-form-item label="鐢宠浜�">
+ <el-input v-model="searchForm.applicant" placeholder="璇疯緭鍏ョ敵璇蜂汉" clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 浼氳鍒楄〃 -->
+ <el-card>
+ <el-table v-loading="loading" :data="meetingList" border :height="tableHeight">
+ <el-table-column prop="title" label="浼氳涓婚" align="center" min-width="200" show-overflow-tooltip />
+ <el-table-column prop="applicant" label="鐢宠浜�" align="center" width="120" />
+ <el-table-column prop="host" label="涓绘寔浜�" align="center" width="120" />
+ <el-table-column prop="meetingTime" label="浼氳鏃堕棿" align="center" width="180">
+ <template #default="scope">
+ {{ formatDateTime(scope.row.meetingTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="location" label="浼氳鍦扮偣" align="center" width="150" />
+ <el-table-column prop="participants" label="鍙備細浜烘暟" align="center" width="100">
+ <template #default="scope">
+ {{ scope.row.participants.length }}浜�
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="200" fixed="right">
+ <template #default="scope">
+ <el-button type="primary" link @click="viewDetail(scope.row)">鏌ョ湅</el-button>
+ <el-button
+ type="primary"
+ link
+ @click="addMinutes(scope.row)"
+ >
+ 娣诲姞绾
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <!-- 浼氳璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ title="浼氳璇︽儏"
+ v-model="detailDialogVisible"
+ width="800px"
+ >
+ <div v-if="currentMeeting">
+ <el-descriptions label-width="100px" class="meeting-desc" :column="2" border>
+ <el-descriptions-item label="浼氳涓婚" label-class-name="nowrap-label">{{
+ currentMeeting.title
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�" label-class-name="nowrap-label">{{
+ currentMeeting.applicant
+ }}</el-descriptions-item>
+ <el-descriptions-item label="涓绘寔浜�" label-class-name="nowrap-label">{{
+ currentMeeting.host
+ }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿" :span="2" label-class-name="nowrap-label">
+ {{ formatDateTime(currentMeeting.meetingTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍦扮偣" label-class-name="nowrap-label">{{
+ currentMeeting.location
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜烘暟" label-class-name="nowrap-label">{{
+ currentMeeting.participants.length
+ }}浜�</el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒鐘舵��" label-class-name="nowrap-label">
+ <el-tag :type="getStatusType(currentMeeting.status)">
+ {{ getStatusText(currentMeeting.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏃堕棿" label-class-name="nowrap-label">{{
+ currentMeeting.createTime
+ }}</el-descriptions-item>
+ <el-descriptions-item style="max-height: 400px" label="浼氳璇存槑" :span="2"
+ label-class-name="nowrap-label">{{ currentMeeting.description }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="content-section mt-20">
+ <h4>鍙備細浜哄憳</h4>
+ <div class="participants-list">
+ <el-tag
+ v-for="participant in currentMeeting.participants"
+ :key="participant.id"
+ style="margin-right: 10px; margin-bottom: 10px;"
+ >
+ {{ participant.name }}
+ </el-tag>
+ </div>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="detailDialogVisible = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 娣诲姞浼氳绾瀵硅瘽妗� -->
+ <el-dialog
+ title="娣诲姞浼氳绾"
+ v-model="minutesDialogVisible"
+ width="80%"
+ @close="handleCloseMinutesDialog"
+ >
+ <div v-if="currentMeeting">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="浼氳涓婚">{{ currentMeeting.title }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�">{{ currentMeeting.applicant }}</el-descriptions-item>
+ <el-descriptions-item label="涓绘寔浜�">{{ currentMeeting.host }}</el-descriptions-item>
+ <el-descriptions-item label="浼氳鏃堕棿" :span="2">
+ {{ formatDateTime(currentMeeting.meetingTime) }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浼氳鍦扮偣">{{ currentMeeting.location }}</el-descriptions-item>
+ <el-descriptions-item label="鍙備細浜烘暟">{{ currentMeeting.participants.length }}浜�</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="content-section mt-20">
+ <h4>浼氳绾鍐呭</h4>
+ <div class="editor-container">
+ <Editor
+ v-model="minutesContent"
+ :min-height="400"
+ />
+ </div>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitMinutes">淇� 瀛�</el-button>
+ <el-button @click="minutesDialogVisible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import Pagination from '@/components/Pagination/index.vue'
+import Editor from '@/components/Editor/index.vue'
+import { getRoomEnum, getMeetingPublish ,getMeetingMinutesByMeetingId,saveMeetingMinutes} from '@/api/collaborativeApproval/meeting.js'
+import dayjs from "dayjs"
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+
+// 鏁版嵁鍒楄〃鍔犺浇鐘舵��
+const loading = ref(false)
+
+// 鎬绘潯鏁�
+const total = ref(0)
+
+// 琛ㄦ牸楂樺害锛堟牴鎹獥鍙i珮搴﹁嚜閫傚簲锛�
+const tableHeight = ref(window.innerHeight - 380)
+const roomEnum = ref([])
+const staffList = ref([])
+
+// 浼氳鍒楄〃鏁版嵁
+const meetingList = ref([])
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10
+})
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ title: '',
+ applicant: '',
+ // status: '1' // 榛樿鍙樉绀哄凡閫氳繃瀹℃壒鐨勪細璁�
+})
+
+// 鏄惁鏄剧ず瀵硅瘽妗�
+const detailDialogVisible = ref(false)
+const minutesDialogVisible = ref(false)
+
+// 褰撳墠鏌ョ湅鐨勪細璁�
+const currentMeeting = ref(null)
+
+// 浼氳绾鍐呭
+const minutesContent = ref('')
+const minutesContentId = ref('')
+
+// 鏌ヨ鏁版嵁
+const getList = async () => {
+ loading.value = true
+ let resp = await getMeetingPublish({ ...searchForm, ...queryParams })
+ meetingList.value = resp.data.records.map(it => {
+ let room = roomEnum.value.find(room => it.roomId === room.id)
+ it.location = `${room.name}(${room.location})`
+ let staffs = JSON.parse(it.participants)
+ it.staffCount = staffs.size
+ it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}`
+ it.participants = staffList.value.filter(staff => staffs.some(id => id === staff.id)).map(staff => {
+ return {
+ id: staff.id,
+ name: `${staff.staffName}${staff.postName ? ` (${staff.postName})` : ''}`
+ }
+ })
+
+ return it
+ })
+ total.value = resp.data.total
+ loading.value = false
+}
+
+// 鎼滅储鎸夐挳鎿嶄綔
+const handleSearch = () => {
+ queryParams.current = 1
+ getList()
+}
+
+// 閲嶇疆鎼滅储琛ㄥ崟
+const resetSearch = () => {
+ Object.assign(searchForm, {
+ title: '',
+ applicant: '',
+ // status: '1'
+ })
+ handleSearch()
+}
+
+// 鏌ョ湅璇︽儏
+const viewDetail = (row) => {
+ currentMeeting.value = row
+ detailDialogVisible.value = true
+}
+
+// 娣诲姞浼氳绾
+const addMinutes = async (row) => {
+ let resp = await getMeetingMinutesByMeetingId(row.id)
+ currentMeeting.value = row
+ if (resp.data){
+ minutesContent.value = resp.data.content
+ minutesContentId.value = resp.data.id
+ }else {
+ minutesContent.value = `<h2>${row.title}浼氳绾</h2>
+<p><strong>浼氳鏃堕棿锛�</strong>${row.meetingTime}</p>
+<p><strong>浼氳鍦扮偣锛�</strong>${row.location}</p>
+<p><strong>涓绘寔浜猴細</strong>${row.host}</p>
+<p><strong>鍙備細浜哄憳锛�</strong></p>
+<ol>
+ ${row.participants.map(p => `<li>${p.name}</li>`).join('')}
+</ol>
+<p><strong>浼氳鍐呭锛�</strong></p>
+<ol>
+ <li>璁涓�锛�
+ <ul>
+ <li>璁ㄨ鍐呭锛�</li>
+ <li>鍐宠浜嬮」锛�</li>
+ </ul>
+ </li>
+ <li>璁浜岋細
+ <ul>
+ <li>璁ㄨ鍐呭锛�</li>
+ <li>鍐宠浜嬮」锛�</li>
+ </ul>
+ </li>
+</ol>
+<p><strong>澶囨敞锛�</strong></p>`
+ }
+
+ minutesDialogVisible.value = true
+}
+
+// 鎻愪氦浼氳绾
+const submitMinutes = () => {
+ if (!minutesContent.value) {
+ ElMessage.warning('璇疯緭鍏ヤ細璁邯瑕佸唴瀹�')
+ return
+ }
+ saveMeetingMinutes({
+ id: minutesContentId.value,
+ content: minutesContent.value,
+ meetingId: currentMeeting.value.id,
+ title: currentMeeting.value.title
+ }).then(resp=>{
+ console.log('浼氳绾鍐呭:', minutesContent.value)
+ ElMessage.success('浼氳绾淇濆瓨鎴愬姛')
+ minutesDialogVisible.value = false
+ })
+
+}
+
+// 鍏抽棴浼氳绾瀵硅瘽妗�
+const handleCloseMinutesDialog = () => {
+ minutesContent.value = ''
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const statusMap = {
+ '0': 'info', // 寰呭鎵�
+ '1': 'success', // 宸查�氳繃
+ '2': 'warning', // 鏈�氳繃
+ '3': 'danger' // 鍙栨秷
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ '0': '寰呭鎵�',
+ '1': '宸查�氳繃',
+ '2': '鏈�氳繃',
+ '3': '宸插彇娑�'
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+// 鏍煎紡鍖栨棩鏈熸椂闂�
+const formatDateTime = (dateTime) => {
+ if (!dateTime) return ''
+ return dateTime.replace(' ', '\n')
+}
+
+// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
+onMounted(async () => {
+ const [resp1, resp2] = await Promise.all([getRoomEnum(), staffOnJobListPage({current: -1, size: -1, staffState: 1})])
+ roomEnum.value = resp1.data
+ staffList.value = resp2.data.records
+
+ await getList()
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.search-card {
+ margin-bottom: 20px;
+}
+
+.dialog-footer {
+ text-align: center;
+}
+
+.content-section h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+}
+
+.mt-20 {
+ margin-top: 20px;
+}
+
+.participants-list {
+ min-height: 40px;
+ padding: 15px;
+ border-radius: 4px;
+ line-height: 1.6;
+}
+
+.nowrap-label {
+ white-space: nowrap !important;
+}
+
+.editor-container {
+ border: 1px solid #dcdfe6;
+ border-radius: 4px;
+}
+</style>
diff --git a/src/views/collaborativeApproval/officeSupplies/index.vue b/src/views/collaborativeApproval/officeSupplies/index.vue
new file mode 100644
index 0000000..a2d1c6d
--- /dev/null
+++ b/src/views/collaborativeApproval/officeSupplies/index.vue
@@ -0,0 +1,512 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <template #header>
+ <div class="card-header">
+ <span>鍔炲叕鐗╄祫鐢宠绠$悊</span>
+ <el-button type="primary" @click="openShow()">
+ <el-icon><Plus /></el-icon>
+ 鏂板缓鐢宠
+ </el-button>
+ </div>
+ </template>
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="鐢宠缂栧彿" prop="code">
+ <el-input
+ v-model="queryParams.code"
+ placeholder="璇疯緭鍏ョ敵璇风紪鍙�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢宠浜�" prop="applicant">
+ <el-input
+ v-model="queryParams.applicant"
+ placeholder="璇疯緭鍏ョ敵璇蜂汉"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢宠鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 200px">
+ <el-option label="寰呭鎵�" value="1" />
+ <el-option label="宸查�氳繃" value="3" />
+ <el-option label="宸叉嫆缁�" value="2" />
+ <el-option label="宸插彂鏀�" value="4" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">
+ <el-icon><Search /></el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetQuery">
+ <el-icon><Refresh /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleExport">
+ <el-icon><Download /></el-icon>
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 琛ㄦ牸鍖哄煙 -->
+ <el-table
+ v-loading="loading"
+ :data="officeList"
+ @selection-change="handleSelectionChange"
+ style="width: 100%"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鐢宠缂栧彿" align="center" prop="code" width="180" />
+ <el-table-column label="鐢宠浜�" align="center" prop="applicant" width="120" />
+ <el-table-column label="閮ㄩ棬" align="center" prop="dept" width="120" />
+ <el-table-column label="鐗╄祫绫诲瀷" align="center" prop="materialType" width="120">
+ <template #default="scope">
+ <el-tag v-if="scope.row.materialType === 1" type="info">鍏朵粬</el-tag>
+ <el-tag v-if="scope.row.materialType === 2" type="success">娓呮磥鐢ㄥ搧</el-tag>
+ <el-tag v-if="scope.row.materialType === 3" type="warning">鐢靛瓙璁惧</el-tag>
+ <el-tag v-if="scope.row.materialType === 4" type="danger">鍔炲叕鐢ㄥ搧</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢宠鏁伴噺" align="center" prop="applyNum" width="100" />
+ <el-table-column label="鐢宠鍘熷洜" align="center" prop="reason" min-width="200" show-overflow-tooltip />
+ <el-table-column label="鐢宠鐘舵��" align="center" prop="status" width="100">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ getStatusText(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢宠鏃堕棿" align="center" prop="applyTime" width="180" />
+ <el-table-column label="瀹℃壒浜�" align="center" prop="approval" width="120" />
+ <el-table-column label="瀹℃壒鏃堕棿" align="center" prop="approvalTime" width="180" />
+ <el-table-column label="鍙戞斁鏃堕棿" align="center" prop="issueTime" width="180" />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" class-name="small-padding fixed-width" width="200">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.status === 1"
+ type="primary"
+ link
+ @click="handleApprove(scope.row)"
+ >
+ 瀹℃壒
+ </el-button>
+ <el-button
+ v-if="scope.row.status === 3"
+ type="success"
+ link
+ @click="handleIssue(scope.row)"
+ >
+ 鍙戞斁
+ </el-button>
+ <el-button
+ type="info"
+ link
+ @click="handleDetail(scope.row)"
+ >
+ 璇︽儏
+ </el-button>
+ <el-button
+ v-if="scope.row.status === 2"
+ type="danger"
+ link
+ @click="handleDelete(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+ </el-card>
+
+ <!-- 鐢宠瀵硅瘽妗� -->
+ <el-dialog
+ v-model="showApplyDialog"
+ title="鍔炲叕鐗╄祫鐢宠"
+ width="600px"
+ append-to-body
+ >
+ <el-form ref="applyFormRef" :model="applyForm" :rules="applyRules" label-width="100px">
+ <el-form-item label="鐢宠浜�" prop="applicant">
+ <el-input v-model="applyForm.applicant" placeholder="璇疯緭鍏ョ敵璇蜂汉鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="閮ㄩ棬" prop="dept">
+ <el-input v-model="applyForm.dept" placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�" />
+ </el-form-item>
+ <el-form-item label="鐗╄祫绫诲瀷" prop="materialType">
+ <el-select v-model="applyForm.materialType" placeholder="璇烽�夋嫨鐗╄祫绫诲瀷" style="width: 100%">
+ <el-option label="鍔炲叕鐢ㄥ搧" value="4" />
+ <el-option label="鐢靛瓙璁惧" value="3" />
+ <el-option label="娓呮磥鐢ㄥ搧" value="2" />
+ <el-option label="鍏朵粬" value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏蜂綋鐗╁搧" prop="itemName">
+ <el-input v-model="applyForm.itemName" placeholder="璇疯緭鍏ュ叿浣撶墿鍝佸悕绉�" />
+ </el-form-item>
+ <el-form-item label="鐢宠鏁伴噺" prop="applyNum">
+ <el-input-number v-model="applyForm.applyNum" :min="1" :max="999" style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="鐢宠鍘熷洜" prop="reason">
+ <el-input
+ v-model="applyForm.reason"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ョ敵璇峰師鍥�"
+ />
+ </el-form-item>
+ <el-form-item label="绱ф�ョ▼搴�" prop="urgency">
+ <el-radio-group v-model="applyForm.urgency">
+ <el-radio label="1">鏅��</el-radio>
+ <el-radio label="2">绱ф��</el-radio>
+ <el-radio label="3">闈炲父绱ф��</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="showApplyDialog = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submitApply">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 瀹℃壒瀵硅瘽妗� -->
+ <el-dialog
+ v-model="showApproveDialog"
+ title="瀹℃壒鐢宠"
+ width="500px"
+ append-to-body
+ >
+ <el-form ref="approveFormRef" :model="approveForm" :rules="approveRules" label-width="100px">
+ <el-form-item label="瀹℃壒缁撴灉" prop="approveResult">
+ <el-radio-group v-model="approveForm.approveResult">
+ <el-radio label="3">閫氳繃</el-radio>
+ <el-radio label="2">鎷掔粷</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="瀹℃壒鎰忚" prop="approvalOpinions">
+ <el-input
+ v-model="approveForm.approvalOpinions"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ鎵规剰瑙�"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="showApproveDialog = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submitApprove">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ v-model="showDetailDialog"
+ title="鐢宠璇︽儏"
+ width="700px"
+ append-to-body
+ >
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鐢宠缂栧彿">{{ currentDetail.code }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�">{{ currentDetail.applicant }}</el-descriptions-item>
+ <el-descriptions-item label="閮ㄩ棬">{{ currentDetail.dept }}</el-descriptions-item>
+ <el-descriptions-item label="鐗╄祫绫诲瀷">{{ currentDetail.materialType }}</el-descriptions-item>
+ <el-descriptions-item label="鍏蜂綋鐗╁搧">{{ currentDetail.itemName }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏁伴噺">{{ currentDetail.applyNum }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鍘熷洜" :span="2">{{ currentDetail.reason }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鐘舵��">
+ <el-tag :type="getStatusType(currentDetail.status)">
+ {{ getStatusText(currentDetail.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏃堕棿">{{ currentDetail.applyTime }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒浜�">{{ currentDetail.approval || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒鏃堕棿">{{ currentDetail.approvalTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒鎰忚" :span="2">{{ currentDetail.approvalOpinions || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戞斁鏃堕棿">{{ currentDetail.issueTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戞斁浜�">{{ currentDetail.issueUser || '-' }}</el-descriptions-item>
+ </el-descriptions>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {listPage,add,update,deleteOff} from "@/api/collaborativeApproval/officeSupplies.js"
+import {ref, reactive, onMounted, getCurrentInstance} from 'vue'
+import Cookies from 'js-cookie'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Search, Refresh, Download, Check } from '@element-plus/icons-vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const showSearch = ref(true)
+const showApplyDialog = ref(false)
+const showApproveDialog = ref(false)
+const showDetailDialog = ref(false)
+const multipleSelection = ref([])
+const officeList = ref([])
+const total = ref(0)
+const suppliesList = ref([])
+const currentDetail = ref({})
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10,
+ code: '',
+ applicant: '',
+ status: ''
+})
+
+// 鐢宠琛ㄥ崟
+const applyForm = reactive({
+ applicant: '',
+ dept: '',
+ materialType: '',
+ itemName: '',
+ applyNum: 1,
+ reason: '',
+ urgency: '1'
+})
+
+// 瀹℃壒琛ㄥ崟
+const approveForm = reactive({
+ approveResult: '3',
+ approvalOpinions: ''
+})
+
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const applyRules = {
+ applicant: [{ required: true, message: '璇烽�夋嫨鐗╄祫绫诲瀷', trigger: 'blur' }],
+ dept: [{ required: true, message: '璇烽�夋嫨鐗╄祫绫诲瀷', trigger: 'blur' }],
+ materialType: [{ required: true, message: '璇烽�夋嫨鐗╄祫绫诲瀷', trigger: 'change' }],
+ itemName: [{ required: true, message: '璇疯緭鍏ュ叿浣撶墿鍝佸悕绉�', trigger: 'blur' }],
+ applyNum: [{ required: true, message: '璇疯緭鍏ョ敵璇锋暟閲�', trigger: 'blur' }],
+ reason: [{ required: true, message: '璇疯緭鍏ョ敵璇峰師鍥�', trigger: 'blur' }]
+}
+
+const approveRules = {
+ approveResult: [{ required: true, message: '璇烽�夋嫨瀹℃壒缁撴灉', trigger: 'change' }],
+ approvalOpinions: [{ required: true, message: '璇疯緭鍏ュ鎵规剰瑙�', trigger: 'blur' }]
+}
+
+const openShow = () => {
+ showApplyDialog.value = true
+ resetApplyForm()
+}
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = () => {
+ loading.value = true
+ listPage(queryParams).then(res => {
+ total.value = res.data.total
+ loading.value = false
+ officeList.value = res.data.records
+ })
+}
+
+// 鏌ヨ
+const handleQuery = () => {
+ queryParams.current = 1
+ getList()
+}
+
+// 閲嶇疆鏌ヨ
+const resetQuery = () => {
+ queryParams.code = ''
+ queryParams.applicant = ''
+ queryParams.status = ''
+ handleQuery()
+}
+
+// 澶氶��
+const handleSelectionChange = (selection) => {
+ multipleSelection.value = selection
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const statusMap = {
+ 1: 'warning',
+ 3: 'success',
+ 2: 'danger',
+ 4: 'info'
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ 1: '寰呭鎵�',
+ 3: '宸查�氳繃',
+ 2: '宸叉嫆缁�',
+ 4: '宸插彂鏀�'
+ }
+ return statusMap[status] || status
+}
+
+// 鎻愪氦鐢宠
+const submitApply = () => {
+ add(applyForm).then(() => {
+ ElMessage.success('鐢宠鎴愬姛')
+ getList()
+ showApplyDialog.value = false
+ resetApplyForm()
+ })
+
+
+
+}
+
+//閲嶇疆琛ㄥ崟
+const resetApplyForm = () => {
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(applyForm, {
+ applicant: '',
+ dept: '',
+ materialType: '',
+ itemName: '',
+ applyNum: 1,
+ reason: '',
+ urgency: '1'
+ })
+}
+
+// 瀹℃壒
+const handleApprove = (row) => {
+ currentDetail.value = row
+ showApproveDialog.value = true
+}
+
+const formatDate = (date) => {
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ const hours = String(date.getHours()).padStart(2, '0')
+ const minutes = String(date.getMinutes()).padStart(2, '0')
+ const sends = String(date.getSeconds()).padStart(2, '0')
+ return `${year}-${month}-${day} ${hours}:${minutes}:${sends}`
+}
+
+// 鎻愪氦瀹℃壒
+const submitApprove = () => {
+ currentDetail.value.status = approveForm.approveResult
+ // 浠巆ookie涓幏鍙栧綋鍓嶇櫥褰曠敤鎴峰悕绉�
+ currentDetail.value.approval = Cookies.get('username')
+ currentDetail.value.approvalTime = formatDate(new Date())
+ currentDetail.value.approvalOpinions = approveForm.approvalOpinions
+ update(currentDetail.value).then((res) => {
+ if(res.code === 200){
+ showApproveDialog.value = false
+ ElMessage.success('瀹℃壒瀹屾垚')
+ getList()
+
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(approveForm, {
+ approveResult: '3',
+ approvalOpinions: ''
+ })
+ }
+ })
+
+}
+
+// 鍙戞斁
+const handleIssue = (row) => {
+ row.status = 4
+ row.issueTime = formatDate(new Date())
+ row.issueUser = Cookies.get('username')
+ update(row).then((res) =>{
+ if(res.code === 200){
+ ElMessage.success('鍙戞斁瀹屾垚')
+ getList()
+ }
+ })
+}
+
+// 鏌ョ湅璇︽儏
+const handleDetail = (row) => {
+ currentDetail.value = row
+ showDetailDialog.value = true
+}
+
+// 鍒犻櫎
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭鍒犻櫎璇ョ敵璇峰悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = [row.id]
+ deleteOff(ids).then((res) =>{
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getList()
+ })
+ })
+}
+const { proxy } = getCurrentInstance();
+// 瀵煎嚭
+const handleExport = () => {
+ ElMessageBox.confirm("鎵�鏈夌殑鍐呭灏嗚瀵煎嚭锛屾槸鍚︾‘璁ゅ鍑猴紵", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/officeSupplies/export", {}, "鍔炲叕鐗╄祫.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+}
+
+// 椤甸潰鍔犺浇鏃惰幏鍙栨暟鎹�
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.mb8 {
+ margin-bottom: 8px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+
+:deep(.el-descriptions__label) {
+ width: 120px;
+}
+</style>
diff --git a/src/views/collaborativeApproval/planTemplate/index.vue b/src/views/collaborativeApproval/planTemplate/index.vue
new file mode 100644
index 0000000..fa2bfd9
--- /dev/null
+++ b/src/views/collaborativeApproval/planTemplate/index.vue
@@ -0,0 +1,867 @@
+<template>
+ <div class="app-container">
+ <!-- 椤堕儴鎿嶄綔鏍� -->
+ <div class="header-actions">
+ <div class="left-actions">
+ <el-select v-model="currentLevel" placeholder="閫夋嫨璁″垝绾у埆" style="width: 150px" @change="handleLevelChange">
+ <el-option label="涓汉璁″垝" value="personal" />
+ <el-option label="灏忕粍璁″垝" value="group" />
+ <el-option label="閮ㄩ棬璁″垝" value="department" />
+ <el-option label="鍏徃璁″垝" value="company" />
+ </el-select>
+ <el-select v-model="currentPeriod" placeholder="閫夋嫨鏃堕棿鍛ㄦ湡" style="width: 120px; margin-left: 10px" @change="handlePeriodChange">
+ <el-option label="鍛ㄨ鍒�" value="week" />
+ <el-option label="鏈堣鍒�" value="month" />
+ <el-option label="骞磋鍒�" value="year" />
+ </el-select>
+ <el-date-picker
+ v-model="currentDate"
+ :type="datePickerType"
+ placeholder="閫夋嫨鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 180px; margin-left: 10px"
+ @change="handleDateChange"
+ />
+ </div>
+ <div class="right-actions">
+ <el-button type="primary" @click="handleAddPlan">鏂板璁″垝</el-button>
+ <el-button @click="handleExport">瀵煎嚭璁″垝</el-button>
+ <!-- <el-button @click="handleShare">鍏变韩璁″垝@</el-button> -->
+ </div>
+ </div>
+
+ <!-- 璁″垝姒傝鍗$墖 -->
+ <div class="overview-cards">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-card class="overview-card">
+ <div class="card-content">
+ <div class="card-icon personal">
+ <el-icon><User /></el-icon>
+ </div>
+ <div class="card-info">
+ <div class="card-title">涓汉璁″垝</div>
+ <div class="card-number">{{ overviewData.personal.total }}</div>
+ <div class="card-progress">
+ <el-progress :percentage="overviewData.personal.completion" :stroke-width="6" />
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="overview-card">
+ <div class="card-content">
+ <div class="card-icon group">
+ <el-icon><UserFilled /></el-icon>
+ </div>
+ <div class="card-info">
+ <div class="card-title">灏忕粍璁″垝</div>
+ <div class="card-number">{{ overviewData.group.total }}</div>
+ <div class="card-progress">
+ <el-progress :percentage="overviewData.group.completion" :stroke-width="6" />
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="overview-card">
+ <div class="card-content">
+ <div class="card-icon department">
+ <el-icon><OfficeBuilding /></el-icon>
+ </div>
+ <div class="card-info">
+ <div class="card-title">閮ㄩ棬璁″垝</div>
+ <div class="card-number">{{ overviewData.department.total }}</div>
+ <div class="card-progress">
+ <el-progress :percentage="overviewData.department.completion" :stroke-width="6" />
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="overview-card">
+ <div class="card-content">
+ <div class="card-icon company">
+ <el-icon><House /></el-icon>
+ </div>
+ <div class="card-info">
+ <div class="card-title">鍏徃璁″垝</div>
+ <div class="card-number">{{ overviewData.company.total }}</div>
+ <div class="card-progress">
+ <el-progress :percentage="overviewData.company.completion" :stroke-width="6" />
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 璁″垝鍒楄〃 -->
+ <div class="plan-content">
+ <el-card>
+ <template #header>
+ <div class="card-header">
+ <span>{{ getCurrentLevelText() }} - {{ getCurrentPeriodText() }}</span>
+ <div>
+ <el-button size="small" @click="handleRefresh">鍒锋柊</el-button>
+ <!-- <el-button size="small" @click="handleFilter">绛涢�堾</el-button> -->
+ </div>
+ </div>
+ </template>
+
+ <div class="plan-list">
+ <div v-for="plan in planList" :key="plan.id" class="plan-item">
+ <div class="plan-header">
+ <div class="plan-title">
+ <el-tag :type="getPriorityType(plan.priority)" size="small">{{ getPriorityText(plan.priority) }}</el-tag>
+ <span class="title-text">{{ plan.title }}</span>
+ </div>
+ <div class="plan-actions">
+ <el-button size="small" @click="handleEditPlan(plan)">缂栬緫</el-button>
+ <el-button size="small" @click="handleViewDetail(plan)" style="color: #67C23A">璇︽儏</el-button>
+ <el-dropdown @command="(command) => handleMoreAction(plan, command)">
+ <el-button size="small">
+ 鏇村<el-icon class="el-icon--right"><ArrowDown /></el-icon>
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <!-- <el-dropdown-item command="share">鍏变韩@</el-dropdown-item> -->
+ <el-dropdown-item command="copy">澶嶅埗</el-dropdown-item>
+ <el-dropdown-item command="delete" divided>鍒犻櫎</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </div>
+
+ <div class="plan-content">
+ <div class="plan-description">{{ plan.description }}</div>
+ <div class="plan-meta">
+ <div class="meta-item">
+ <el-icon><Calendar /></el-icon>
+ <span>{{ plan.startDate }} - {{ plan.endDate }}</span>
+ </div>
+ <div class="meta-item">
+ <el-icon><User /></el-icon>
+ <span>{{ plan.assignee }}</span>
+ </div>
+ <div class="meta-item">
+ <el-icon><Clock /></el-icon>
+ <span>杩涘害: {{ plan.progress }}%</span>
+ </div>
+ <div class="meta-item">
+ <el-icon><Flag /></el-icon>
+ <span>{{ getStatusText(plan.status) }}</span>
+ </div>
+ </div>
+
+ <div class="plan-progress">
+ <el-progress
+ :percentage="plan.progress"
+ :color="getProgressColor(plan.progress)"
+ :stroke-width="8"
+ />
+ </div>
+
+ <div class="plan-tags">
+ <el-tag v-for="tag in plan.tags" :key="tag" size="small" style="margin-right: 5px">
+ {{ tag }}
+ </el-tag>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </div>
+
+ <!-- 鏂板/缂栬緫璁″垝瀵硅瘽妗� -->
+ <el-dialog
+ v-model="planDialogVisible"
+ :title="operationType === 'add' ? '鍙戝竷璁″垝' : '缂栬緫璁″垝'"
+ width="600px"
+ @close="handleDialogClose"
+ >
+ <el-form :model="planForm" :rules="planRules" ref="planFormRef" label-width="100px">
+ <el-form-item label="璁″垝鏍囬" prop="title">
+ <el-input v-model="planForm.title" placeholder="璇疯緭鍏ヨ鍒掓爣棰�" />
+ </el-form-item>
+ <el-form-item label="璁″垝鎻忚堪" prop="description">
+ <el-input
+ v-model="planForm.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヨ鍒掓弿杩�"
+ />
+ </el-form-item>
+ <el-form-item label="璁″垝绾у埆" prop="level">
+ <el-select v-model="planForm.level" placeholder="閫夋嫨璁″垝绾у埆" style="width: 100%">
+ <el-option label="涓汉璁″垝" value="personal" />
+ <el-option label="灏忕粍璁″垝" value="group" />
+ <el-option label="閮ㄩ棬璁″垝" value="department" />
+ <el-option label="鍏徃璁″垝" value="company" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃堕棿鍛ㄦ湡" prop="period">
+ <el-select v-model="planForm.period" placeholder="閫夋嫨鏃堕棿鍛ㄦ湡" style="width: 100%">
+ <el-option label="鍛ㄨ鍒�" value="week" />
+ <el-option label="鏈堣鍒�" value="month" />
+ <el-option label="骞磋鍒�" value="year" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�濮嬫椂闂�" prop="startDate">
+ <el-date-picker
+ v-model="planForm.startDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="閫夋嫨寮�濮嬫椂闂�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ <el-form-item label="缁撴潫鏃堕棿" prop="endDate">
+ <el-date-picker
+ v-model="planForm.endDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="閫夋嫨缁撴潫鏃堕棿"
+ style="width: 100%"
+ />
+ </el-form-item>
+ <el-form-item label="璐熻矗浜�" prop="assignee">
+ <el-input v-model="planForm.assignee" placeholder="璇疯緭鍏ヨ礋璐d汉" />
+ </el-form-item>
+ <el-form-item label="浼樺厛绾�" prop="priority">
+ <el-select v-model="planForm.priority" placeholder="閫夋嫨浼樺厛绾�" style="width: 100%">
+ <el-option label="楂�" value="high" />
+ <el-option label="涓�" value="medium" />
+ <el-option label="浣�" value="low" />
+ </el-select>
+ </el-form-item>
+ <!-- <el-form-item label="鏍囩">
+ <el-input v-model="planForm.tags" placeholder="璇疯緭鍏ユ爣绛撅紝鐢ㄩ�楀彿鍒嗛殧" />
+ </el-form-item> -->
+ <el-form-item label="鏍囩" prop="tags">
+ <!-- <el-checkbox-group v-model="planForm.tags">
+ <el-checkbox label="all"></el-checkbox>
+ <el-checkbox label="manager">绠$悊灞�</el-checkbox>
+ <el-checkbox label="hr">浜轰簨閮ㄩ棬</el-checkbox>
+ <el-checkbox label="finance">璐㈠姟閮ㄩ棬</el-checkbox>
+ <el-checkbox label="tech">鎶�鏈儴闂�</el-checkbox>
+ </el-checkbox-group> -->
+ <el-select
+ v-model="planForm.tags"
+ multiple
+ placeholder="璇烽�夋嫨鏍囩"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="dept in departments"
+ :key="dept"
+ :label="dept"
+ :value="dept"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="planForm.status" placeholder="閫夋嫨鐘舵��" style="width: 100%">
+ <el-option label="鏈紑濮�" value="not_started" />
+ <el-option label="杩涜涓�" value="in_progress" />
+ <el-option label="宸插畬鎴�" value="completed" />
+ <el-option label="宸叉殏鍋�" value="paused" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="杩涘害" prop="progress">
+ <el-input-number
+ v-model="planForm.progress"
+ min="0"
+ max="100"
+ step="1"
+ placeholder="璇疯緭鍏ヨ繘搴�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="planDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="handleSavePlan">淇濆瓨</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 璁″垝璇︽儏瀵硅瘽妗� -->
+ <el-dialog v-model="showPlanDetailDialog" title="璁″垝璇︽儏" width="700px">
+ <div v-if="currentPlanDetail" class="mb10">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="璁″垝鏍囬">{{ currentPlanDetail.title }}</el-descriptions-item>
+ <el-descriptions-item label="璁″垝鎻忚堪">{{ currentPlanDetail.description }}</el-descriptions-item>
+ <el-descriptions-item label="璁″垝绾у埆">{{ getCurrentLevelText(currentPlanDetail.level) }}</el-descriptions-item>
+ <el-descriptions-item label="鏃堕棿鍛ㄦ湡">{{ getCurrentPeriodText(currentPlanDetail.period) }}</el-descriptions-item>
+ <el-descriptions-item label="寮�濮嬫椂闂�">{{ currentPlanDetail.startDate }}</el-descriptions-item>
+ <el-descriptions-item label="缁撴潫鏃堕棿">{{ currentPlanDetail.endDate }}</el-descriptions-item>
+ <el-descriptions-item label="璐熻矗浜�">{{ currentPlanDetail.assignee }}</el-descriptions-item>
+ <el-descriptions-item label="浼樺厛绾�">{{ getPriorityText(currentPlanDetail.priority) }}</el-descriptions-item>
+ <el-descriptions-item label="鏍囩">{{ currentPlanDetail.tags.join(', ') }}</el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">{{ getStatusText(currentPlanDetail.status) }}</el-descriptions-item>
+ <el-descriptions-item label="杩涘害">{{ currentPlanDetail.progress }}%</el-descriptions-item>
+ </el-descriptions>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+const { proxy } = getCurrentInstance();
+import {
+ User,
+ UserFilled,
+ OfficeBuilding,
+ House,
+ Calendar,
+ Clock,
+ Flag,
+ ArrowDown
+} from '@element-plus/icons-vue'
+import { listDutyPlan, addDutyPlan, updateDutyPlan, delDutyPlan,NumDutyPlan,exportDutyPlan } from '@/api/collaborativeApproval/planTemplate.js'
+
+// 鍝嶅簲寮忔暟鎹�
+const operationType = ref('add')
+const currentLevel = ref('personal')
+const currentPeriod = ref('week')
+const currentDate = ref(new Date())
+const planDialogVisible = ref(false)
+const dialogTitle = ref('鏂板璁″垝')
+const planFormRef = ref()
+const showPlanDetailDialog = ref(false)
+const currentPlanDetail = ref(null)
+
+// 琛ㄥ崟鏁版嵁
+const planForm = reactive({
+ id: '',
+ title: '',
+ description: '',
+ level: 'personal',
+ period: 'week',
+ startDate: '',
+ endDate: '',
+ assignee: '',
+ priority: 'medium',
+ tags: [],
+ status: '',
+ progress: 0
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const planRules = {
+ title: [{ required: true, message: '璇疯緭鍏ヨ鍒掓爣棰�', trigger: 'blur' }],
+ description: [{ required: true, message: '璇疯緭鍏ヨ鍒掓弿杩�', trigger: 'blur' }],
+ level: [{ required: true, message: '璇烽�夋嫨璁″垝绾у埆', trigger: 'change' }],
+ period: [{ required: true, message: '璇烽�夋嫨鏃堕棿鍛ㄦ湡', trigger: 'change' }],
+ startDate: [{ required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change' }],
+ endDate: [{ required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change' }],
+ assignee: [{ required: true, message: '璇疯緭鍏ヨ礋璐d汉', trigger: 'blur' }],
+ priority: [{ required: true, message: '璇烽�夋嫨浼樺厛绾�', trigger: 'change' }]
+}
+const departments = ["浜у搧", "鍒嗘瀽", "璋冪爺",'鎶�鏈�', '鏋舵瀯', '璁捐','甯傚満', '鎺ㄥ箍', '钀ラ攢'];
+// 姒傝鏁版嵁
+const overviewData = reactive({
+ personal: { total: 0, completion: 0 },
+ group: { total: 0, completion: 0 },
+ department: { total: 0, completion: 0 },
+ company: { total: 0, completion: 0 }
+})
+
+// 璁″垝鍒楄〃鏁版嵁
+const planList = ref([])
+
+// 璁$畻灞炴��
+const datePickerType = computed(() => {
+ switch (currentPeriod.value) {
+ case 'week':
+ return 'week'
+ case 'month':
+ return 'month'
+ case 'year':
+ return 'year'
+ default:
+ return 'date'
+ }
+})
+
+// 鏂规硶
+const handleLevelChange = (value) => {
+ console.log('璁″垝绾у埆鍙樻洿:', value)
+ getPlanList()
+ // 杩欓噷鍙互鏍规嵁绾у埆绛涢�夋暟鎹�
+}
+
+const handlePeriodChange = (value) => {
+ console.log('鏃堕棿鍛ㄦ湡鍙樻洿:', value)
+ getPlanList()
+ // 杩欓噷鍙互鏍规嵁鍛ㄦ湡绛涢�夋暟鎹�
+}
+
+const handleDateChange = (value) => {
+ console.log('鏃ユ湡鍙樻洿:', value)
+ getPlanList()
+ // 杩欓噷鍙互鏍规嵁鏃ユ湡绛涢�夋暟鎹�
+}
+
+const handleAddPlan = () => {
+ operationType.value = 'add'
+ dialogTitle.value = '鏂板璁″垝'
+ planDialogVisible.value = true
+ // 閲嶇疆琛ㄥ崟
+ Object.keys(planForm).forEach(key => {
+ planForm[key] = ''
+ })
+ planForm.level = 'personal'
+ planForm.period = 'week'
+ planForm.priority = 'medium'
+ planForm.status = 'not_started'
+ planForm.progress = 0
+}
+
+const handleEditPlan = (plan) => {
+ operationType.value = 'edit'
+ dialogTitle.value = '缂栬緫璁″垝'
+ planDialogVisible.value = true
+ Object.assign(planForm, plan)
+ // // 濉厖琛ㄥ崟鏁版嵁
+ // Object.keys(planForm).forEach(key => {
+ // if (key === 'tags') {
+ // planForm[key] = plan[key].join(', ')
+ // } else {
+ // planForm[key] = plan[key]
+ // }
+ // })
+}
+
+const handleViewDetail = (plan) => {
+ currentPlanDetail.value = plan
+ showPlanDetailDialog.value = true
+ // ElMessage.info(`鏌ョ湅璁″垝璇︽儏: ${plan.title}`)
+}
+
+const handleMoreAction = async(plan,command) => {
+ let ids = [];
+ ids.push(plan.id);
+ console.log("ids",ids)
+ switch (command) {
+ case 'share':
+ ElMessage.success('璁″垝宸插叡浜�')
+ break
+ case 'copy':
+ const knowledgeText = `
+ 璁″垝鏍囬锛�${plan.title}
+ 璁″垝鎻忚堪锛�${plan.description}
+ 璁″垝绾у埆锛�${getCurrentLevelText(plan.level)}
+ 鏃堕棿鍛ㄦ湡锛�${getCurrentPeriodText(plan.period)}
+ 寮�濮嬫椂闂达細${plan.startDate}
+ 缁撴潫鏃堕棿锛�${plan.endDate}
+ 璐熻矗浜猴細${plan.assignee}
+ 浼樺厛绾э細${getPriorityText(plan.priority)}
+ 鏍囩锛�${plan.tags.join(', ')}
+ 鐘舵�侊細${getStatusText(plan.status)}
+ 杩涘害锛�${plan.progress}%
+ `.trim();
+
+ // 澶嶅埗鍒板壀璐存澘
+ navigator.clipboard.writeText(knowledgeText).then(() => {
+ ElMessage.success("鐭ヨ瘑鍐呭宸插鍒跺埌鍓创鏉�");
+ }).catch(() => {
+ ElMessage.error("澶嶅埗澶辫触锛岃鎵嬪姩澶嶅埗");
+ });
+ // ElMessage.success('璁″垝宸插鍒�')
+ break
+ case 'delete':
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖涓鍒掑悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+
+ delDutyPlan(ids).then(res => {
+ if (res.code === 200) {
+ ElMessage.success('璁″垝宸插垹闄�')
+ ids.value = [];
+ getPlanList()
+ }
+ })
+ })
+ break
+ }
+}
+//
+const handleSavePlan = async () => {
+ try {
+ await planFormRef.value.validate()
+ if (operationType.value === 'add') {
+ addDutyPlan(planForm).then(res => {
+ if (res.code === 200) {
+ ElMessage.success('璁″垝淇濆瓨鎴愬姛')
+ planDialogVisible.value = false
+ }
+ getPlanList()
+ })
+ } else {
+
+ updateDutyPlan(planForm).then(res => {
+ if (res.code === 200) {
+ ElMessage.success('璁″垝淇濆瓨鎴愬姛')
+ planDialogVisible.value = false
+ }
+ getPlanList()
+ })
+ }
+ } catch (error) {
+ console.log('琛ㄥ崟楠岃瘉澶辫触:', error)
+ }
+}
+
+const handleDialogClose = () => {
+ planFormRef.value?.resetFields()
+}
+
+const handleRefresh = () => {
+ getPlanList()
+ // ElMessage.success('鏁版嵁宸插埛鏂�')
+}
+
+const handleFilter = () => {
+ ElMessage.info('鎵撳紑绛涢�夐潰鏉�')
+}
+
+const handleExport = () => {
+ ElMessageBox.confirm("鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // exportDutyPlan().then(res => {
+
+ // })
+ proxy.download("/dutyPlan/export", {}, "璁″垝绠$悊.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+const handleShare = () => {
+ ElMessage.success('璁″垝宸插叡浜�')
+}
+
+const getCurrentLevelText = () => {
+ const levelMap = {
+ personal: '涓汉璁″垝',
+ group: '灏忕粍璁″垝',
+ department: '閮ㄩ棬璁″垝',
+ company: '鍏徃璁″垝'
+ }
+ return levelMap[currentLevel.value] || '涓汉璁″垝'
+}
+
+const getCurrentPeriodText = () => {
+ const periodMap = {
+ week: '鍛ㄨ鍒�',
+ month: '鏈堣鍒�',
+ year: '骞磋鍒�'
+ }
+ return periodMap[currentPeriod.value] || '鍛ㄨ鍒�'
+}
+
+const getPriorityType = (priority) => {
+ const typeMap = {
+ high: 'danger',
+ medium: 'warning',
+ low: 'info'
+ }
+ return typeMap[priority] || 'info'
+}
+
+const getPriorityText = (priority) => {
+ const textMap = {
+ high: '楂�',
+ medium: '涓�',
+ low: '浣�'
+ }
+ return textMap[priority] || '涓�'
+}
+
+const getStatusText = (status) => {
+ const statusMap = {
+ not_started: '鏈紑濮�',
+ in_progress: '杩涜涓�',
+ completed: '宸插畬鎴�',
+ paused: '宸叉殏鍋�'
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+const getProgressColor = (progress) => {
+ if (progress >= 80) return '#67C23A'
+ if (progress >= 50) return '#E6A23C'
+ return '#F56C6C'
+}
+//鑾峰彇鏁版嵁鍒楄〃
+const getPlanList = async () => {
+ const params = {
+ level: currentLevel.value,
+ period: currentPeriod.value,
+ queryDate:currentDate.value
+ }
+ listDutyPlan(params).then(res => {
+ if (res.code === 200) {
+ planList.value = res.data.records
+ }
+ }).catch(err => {
+ console.log(err)
+ })
+}
+//鑾峰彇鏁版嵁
+const getPlanNum = async () => {
+ NumDutyPlan().then(res => {
+ if (res.code === 200) {
+ // console.log(res.data)
+ //璁茬粨鏋滈噷闈㈢殑鏁版嵁鏍规嵁level 璧嬪�肩粰overviewData
+ res.data.forEach(item => {
+ overviewData[item.level].total = item.num
+ overviewData[item.level].completion = item.completion
+ })
+
+ }
+ }).catch(err => {
+ console.log(err)
+ })
+}
+
+onMounted(() => {
+ getPlanList()
+ getPlanNum()
+ console.log('澶氱骇璁″垝妯℃澘椤甸潰宸插姞杞�')
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+ background-color: #f5f5f5;
+ min-height: 100vh;
+}
+
+.header-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ background: white;
+ padding: 20px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.left-actions {
+ display: flex;
+ align-items: center;
+}
+
+.right-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.overview-cards {
+ margin-bottom: 20px;
+}
+
+.overview-card {
+ height: 120px;
+}
+
+.card-content {
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
+.card-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 15px;
+ font-size: 24px;
+ color: white;
+}
+
+.card-icon.personal {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.card-icon.group {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.card-icon.department {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.card-icon.company {
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+}
+
+.card-info {
+ flex: 1;
+}
+
+.card-title {
+ font-size: 14px;
+ color: #666;
+ margin-bottom: 5px;
+}
+
+.card-number {
+ font-size: 24px;
+ font-weight: bold;
+ color: #333;
+ margin-bottom: 10px;
+}
+
+.card-progress {
+ width: 100%;
+}
+
+.plan-content {
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: bold;
+ color: #333;
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.plan-list {
+ padding: 20px 0;
+}
+
+.plan-item {
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ margin-bottom: 15px;
+ padding: 20px;
+ transition: all 0.3s ease;
+}
+
+.plan-item:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+}
+
+.plan-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.plan-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.title-text {
+ font-size: 16px;
+ font-weight: bold;
+ color: #333;
+}
+
+.plan-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.plan-content {
+ margin-bottom: 15px;
+}
+
+.plan-description {
+ color: #666;
+ margin-bottom: 15px;
+ line-height: 1.6;
+}
+
+.plan-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 20px;
+ margin-bottom: 15px;
+}
+
+.meta-item {
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ color: #666;
+ font-size: 14px;
+}
+
+.plan-progress {
+ margin-bottom: 15px;
+}
+
+.plan-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .header-actions {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .left-actions {
+ flex-wrap: wrap;
+ gap: 10px;
+ }
+
+ .plan-meta {
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .plan-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+ }
+}
+</style>
diff --git a/src/views/collaborativeApproval/processTracking/index.vue b/src/views/collaborativeApproval/processTracking/index.vue
new file mode 100644
index 0000000..197a438
--- /dev/null
+++ b/src/views/collaborativeApproval/processTracking/index.vue
@@ -0,0 +1,498 @@
+<template>
+ <div class="process-tracking">
+ <div class="header">
+ <h2>杩囩▼杩借釜</h2>
+ <el-button type="primary" @click="refreshData">鍒锋柊鏁版嵁</el-button>
+ </div>
+
+ <!-- 椤圭洰鐘舵�佺粺璁� -->
+ <div class="status-cards">
+ <el-row :gutter="20">
+ <el-col :span="6" v-for="(item, index) in statusStats" :key="index">
+ <el-card class="status-card" :class="item.type">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon :size="24">
+ <component :is="item.icon" />
+ </el-icon>
+ </div>
+ <div class="card-info">
+ <div class="card-title">{{ item.label }}</div>
+ <div class="card-count">{{ item.count }}</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 椤圭洰鍒楄〃 -->
+ <el-card class="project-list">
+ <template #header>
+ <div class="card-header">
+ <span>椤圭洰鍒楄〃</span>
+ <el-button type="text" @click="toggleView">
+ {{ viewMode === 'table' ? '鍒囨崲鍒扮敇鐗瑰浘' : '鍒囨崲鍒板垪琛�' }}
+ </el-button>
+ </div>
+ </template>
+
+ <!-- 琛ㄦ牸瑙嗗浘 -->
+ <div v-if="viewMode === 'table'">
+ <el-table :data="projectList" style="width: 100%">
+ <el-table-column prop="name" label="椤圭洰鍚嶇О" />
+ <el-table-column prop="manager" label="璐熻矗浜�"/>
+ <el-table-column label="鐘舵��" >
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)">
+ {{ getStatusText(row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="杩涘害" width="150">
+ <template #default="{ row }">
+ <el-progress :percentage="row.progress" :status="getProgressStatus(row.status)" />
+ </template>
+ </el-table-column>
+ <el-table-column prop="startDate" label="寮�濮嬫椂闂�" width="120" />
+ <el-table-column prop="endDate" label="缁撴潫鏃堕棿" width="120" />
+ <el-table-column label="鎿嶄綔" width="150">
+ <template #default="{ row }">
+ <el-button type="text" @click="updateStatus(row)">鏇存柊鐘舵��</el-button>
+ <el-button type="text" @click="viewDetails(row)" style="color: #67C23A">璇︽儏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 鐢樼壒鍥捐鍥� -->
+ <div v-else class="gantt-container">
+ <div ref="ganttChart" style="width: 100%; height: 400px;"></div>
+ </div>
+ </el-card>
+
+ <!-- 鐘舵�佹洿鏂板璇濇 -->
+ <el-dialog v-model="statusDialogVisible" title="鏇存柊椤圭洰鐘舵��" width="400px">
+ <el-form :model="statusForm" label-width="80px">
+ <el-form-item label="椤圭洰鍚嶇О">
+ <el-input v-model="statusForm.name" disabled />
+ </el-form-item>
+ <el-form-item label="褰撳墠鐘舵��">
+ <el-select v-model="statusForm.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option label="鏈紑濮�" value="not_started" />
+ <el-option label="杩涜涓�" value="in_progress" />
+ <el-option label="宸插畬鎴�" value="completed" />
+ <el-option label="寤舵湡" value="delayed" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="杩涘害">
+ <el-slider v-model="statusForm.progress" :max="100" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="statusDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="confirmStatusUpdate">纭</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Clock, Loading, Check, Warning } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+
+// 鍝嶅簲寮忔暟鎹�
+const viewMode = ref('table')
+const statusDialogVisible = ref(false)
+const ganttChart = ref(null)
+let chartInstance = null
+
+// 鐘舵�佺粺璁℃暟鎹�
+const statusStats = reactive([
+ { label: '鏈紑濮�', count: 3, type: 'not-started', icon: Clock },
+ { label: '杩涜涓�', count: 5, type: 'in-progress', icon: Loading },
+ { label: '宸插畬鎴�', count: 8, type: 'completed', icon: Check },
+ { label: '寤舵湡', count: 2, type: 'delayed', icon: Warning }
+])
+
+// 椤圭洰鍒楄〃鏁版嵁
+const projectList = reactive([
+ {
+ id: 1,
+ name: '姹囨槦閽欎笟鐢熶骇绾挎墿寤洪」鐩�',
+ manager: '闄堝織寮�',
+ status: 'completed',
+ progress: 100,
+ startDate: '2024-01-01',
+ endDate: '2024-01-15'
+ },
+ {
+ id: 2,
+ name: '鏂板瀷鐜繚閽欑矇宸ヨ壓鐮斿彂',
+ manager: '鏋楅洩宄�',
+ status: 'in_progress',
+ progress: 75,
+ startDate: '2024-01-10',
+ endDate: '2024-01-25'
+ },
+ {
+ id: 3,
+ name: '姹囨槦閽欎笟ERP绯荤粺鍗囩骇',
+ manager: '鐜嬮泤鐞�',
+ status: 'in_progress',
+ progress: 60,
+ startDate: '2024-01-20',
+ endDate: '2024-02-10'
+ },
+ {
+ id: 4,
+ name: '鐭垮北寮�閲囪鍙瘉缁湡鐢宠',
+ manager: '璧典紵涓�',
+ status: 'in_progress',
+ progress: 45,
+ startDate: '2024-01-25',
+ endDate: '2024-02-15'
+ },
+ {
+ id: 5,
+ name: '鐜繚璁惧鍗囩骇鏀归��',
+ manager: '鏉庝匠娆�',
+ status: 'delayed',
+ progress: 30,
+ startDate: '2024-01-15',
+ endDate: '2024-01-30'
+ },
+ {
+ id: 6,
+ name: '骞村害瀹夊叏鐢熶骇鍩硅璁″垝',
+ manager: '寮犲缓鍥�',
+ status: 'not_started',
+ progress: 0,
+ startDate: '2024-02-01',
+ endDate: '2024-02-20'
+ }
+])
+
+// 鐘舵�佹洿鏂拌〃鍗�
+const statusForm = reactive({
+ id: null,
+ name: '',
+ status: '',
+ progress: 0
+})
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const typeMap = {
+ not_started: 'info',
+ in_progress: 'warning',
+ completed: 'success',
+ delayed: 'danger'
+ }
+ return typeMap[status] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const textMap = {
+ not_started: '鏈紑濮�',
+ in_progress: '杩涜涓�',
+ completed: '宸插畬鎴�',
+ delayed: '寤舵湡'
+ }
+ return textMap[status] || '鏈煡'
+}
+
+// 鑾峰彇杩涘害鐘舵��
+const getProgressStatus = (status) => {
+ if (status === 'completed') return 'success'
+ if (status === 'delayed') return 'exception'
+ return null
+}
+
+// 鍒囨崲瑙嗗浘妯″紡
+const toggleView = () => {
+ viewMode.value = viewMode.value === 'table' ? 'gantt' : 'table'
+ if (viewMode.value === 'gantt') {
+ nextTick(() => {
+ initGanttChart()
+ })
+ }
+}
+
+// 鍒濆鍖栫敇鐗瑰浘
+const initGanttChart = () => {
+ if (!ganttChart.value) return
+
+ if (chartInstance) {
+ chartInstance.dispose()
+ }
+
+ chartInstance = echarts.init(ganttChart.value)
+
+ // 鍑嗗鐢樼壒鍥炬暟鎹�
+ const data = projectList.map((project, index) => ({
+ name: project.name,
+ value: [
+ index,
+ new Date(project.startDate).getTime(),
+ new Date(project.endDate).getTime(),
+ project.progress
+ ],
+ itemStyle: {
+ color: getGanttColor(project.status)
+ }
+ }))
+
+ const option = {
+ title: {
+ text: '椤圭洰鐢樼壒鍥�',
+ left: 'center'
+ },
+ tooltip: {
+ formatter: (params) => {
+ const project = projectList[params.value[0]]
+ return `
+ <div>
+ <strong>${project.name}</strong><br/>
+ 璐熻矗浜�: ${project.manager}<br/>
+ 鐘舵��: ${getStatusText(project.status)}<br/>
+ 杩涘害: ${project.progress}%<br/>
+ 寮�濮嬫椂闂�: ${project.startDate}<br/>
+ 缁撴潫鏃堕棿: ${project.endDate}
+ </div>
+ `
+ }
+ },
+ grid: {
+ left: '15%',
+ right: '10%',
+ top: '15%',
+ bottom: '15%'
+ },
+ xAxis: {
+ type: 'time',
+ axisLabel: {
+ formatter: (value) => {
+ return echarts.format.formatTime('MM-dd', value)
+ }
+ }
+ },
+ yAxis: {
+ type: 'category',
+ data: projectList.map(p => p.name),
+ inverse: true
+ },
+ series: [{
+ type: 'custom',
+ renderItem: (params, api) => {
+ const categoryIndex = api.value(0)
+ const start = api.coord([api.value(1), categoryIndex])
+ const end = api.coord([api.value(2), categoryIndex])
+ const height = api.size([0, 1])[1] * 0.6
+
+ return {
+ type: 'rect',
+ shape: {
+ x: start[0],
+ y: start[1] - height / 2,
+ width: end[0] - start[0],
+ height: height
+ },
+ style: api.style()
+ }
+ },
+ data: data
+ }]
+ }
+
+ chartInstance.setOption(option)
+}
+
+// 鑾峰彇鐢樼壒鍥鹃鑹�
+const getGanttColor = (status) => {
+ const colorMap = {
+ not_started: '#909399',
+ in_progress: '#E6A23C',
+ completed: '#67C23A',
+ delayed: '#F56C6C'
+ }
+ return colorMap[status] || '#909399'
+}
+
+// 鏇存柊鐘舵��
+const updateStatus = (project) => {
+ statusForm.id = project.id
+ statusForm.name = project.name
+ statusForm.status = project.status
+ statusForm.progress = project.progress
+ statusDialogVisible.value = true
+}
+
+// 纭鐘舵�佹洿鏂�
+const confirmStatusUpdate = () => {
+ const project = projectList.find(p => p.id === statusForm.id)
+ if (project) {
+ project.status = statusForm.status
+ project.progress = statusForm.progress
+
+ // 鏇存柊缁熻鏁版嵁
+ updateStatusStats()
+
+ // 濡傛灉鏄敇鐗瑰浘瑙嗗浘锛岄噸鏂版覆鏌�
+ if (viewMode.value === 'gantt') {
+ nextTick(() => {
+ initGanttChart()
+ })
+ }
+
+ ElMessage.success('鐘舵�佹洿鏂版垚鍔�')
+ }
+ statusDialogVisible.value = false
+}
+
+// 鏇存柊鐘舵�佺粺璁�
+const updateStatusStats = () => {
+ const stats = {
+ not_started: 0,
+ in_progress: 0,
+ completed: 0,
+ delayed: 0
+ }
+
+ projectList.forEach(project => {
+ stats[project.status]++
+ })
+
+ statusStats[0].count = stats.not_started
+ statusStats[1].count = stats.in_progress
+ statusStats[2].count = stats.completed
+ statusStats[3].count = stats.delayed
+}
+
+// 鏌ョ湅璇︽儏
+const viewDetails = (project) => {
+ ElMessage.info(`鏌ョ湅椤圭洰璇︽儏: ${project.name}`)
+}
+
+// 鍒锋柊鏁版嵁
+const refreshData = () => {
+ // 妯℃嫙瀹炴椂鏇存柊
+ const randomProject = projectList[Math.floor(Math.random() * projectList.length)]
+ if (randomProject.status === 'in_progress') {
+ randomProject.progress = Math.min(100, randomProject.progress + Math.floor(Math.random() * 10))
+ if (randomProject.progress === 100) {
+ randomProject.status = 'completed'
+ }
+ }
+
+ updateStatusStats()
+
+ if (viewMode.value === 'gantt') {
+ nextTick(() => {
+ initGanttChart()
+ })
+ }
+
+ ElMessage.success('鏁版嵁宸插埛鏂�')
+}
+
+onMounted(() => {
+ updateStatusStats()
+})
+</script>
+
+<style scoped>
+.process-tracking {
+ padding: 20px;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.status-cards {
+ margin-bottom: 20px;
+}
+
+.status-card {
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.status-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.card-content {
+ display: flex;
+ align-items: center;
+}
+
+.card-icon {
+ margin-right: 15px;
+ padding: 10px;
+ border-radius: 8px;
+}
+
+.status-card.not-started .card-icon {
+ background-color: #f4f4f5;
+ color: #909399;
+}
+
+.status-card.in-progress .card-icon {
+ background-color: #fdf6ec;
+ color: #E6A23C;
+}
+
+.status-card.completed .card-icon {
+ background-color: #f0f9ff;
+ color: #67C23A;
+}
+
+.status-card.delayed .card-icon {
+ background-color: #fef0f0;
+ color: #F56C6C;
+}
+
+.card-info {
+ flex: 1;
+}
+
+.card-title {
+ font-size: 14px;
+ color: #909399;
+ margin-bottom: 5px;
+}
+
+.card-count {
+ font-size: 24px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.project-list {
+ margin-top: 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.gantt-container {
+ min-height: 400px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/purchaseApproval/index.vue b/src/views/collaborativeApproval/purchaseApproval/index.vue
new file mode 100644
index 0000000..d39748f
--- /dev/null
+++ b/src/views/collaborativeApproval/purchaseApproval/index.vue
@@ -0,0 +1,1095 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <el-form :model="searchForm"
+ :inline="true">
+ <el-form-item label="渚涘簲鍟嗗悕绉帮細">
+ <el-input v-model="searchForm.supplierName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="閲囪喘鍚堝悓鍙凤細">
+ <el-input v-model="searchForm.purchaseContractNumber"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ </el-form-item>
+ <el-form-item label="閿�鍞悎鍚屽彿锛�">
+ <el-input v-model="searchForm.salesContractNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="椤圭洰鍚嶇О锛�">
+ <el-input v-model="searchForm.projectName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery"> 鎼滅储 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ </div>
+ <div class="table_list">
+ <div style="display: flex;justify-content: flex-end;margin-bottom: 20px;">
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys"
+ :row-key="(row) => row.id"
+ show-summary
+ :summary-method="summarizeMainTable"
+ @expand-change="expandChange"
+ height="calc(100vh - 18.5em)"
+ :row-class-name="tableRowClassName">
+ <el-table-column align="center"
+ type="selection"
+ width="55" />
+ <el-table-column type="expand">
+ <template #default="props">
+ <el-table :data="props.row.children"
+ border
+ show-summary
+ :summary-method="summarizeChildrenTable">
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="浜у搧澶х被"
+ prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="specificationModel" />
+ <el-table-column label="鍗曚綅"
+ prop="unit" />
+ <el-table-column label="鏁伴噺"
+ prop="quantity" />
+ <el-table-column label="绋庣巼(%)"
+ prop="taxRate" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="formattedNumber" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ :formatter="formattedNumber" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ :formatter="formattedNumber" />
+ </el-table>
+ </template>
+ </el-table-column>
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="閲囪喘鍚堝悓鍙�"
+ prop="purchaseContractNumber"
+ width="200"
+ show-overflow-tooltip />
+ <el-table-column label="閿�鍞悎鍚屽彿"
+ prop="salesContractNo"
+ width="200"
+ show-overflow-tooltip />
+ <el-table-column label="渚涘簲鍟嗗悕绉�"
+ width="240"
+ prop="supplierName"
+ show-overflow-tooltip />
+ <el-table-column label="璁㈠崟鐘舵��"
+ width="100"
+ align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.isInvalid"
+ type="danger"
+ size="small">澶辨晥</el-tag>
+ <el-tag v-else
+ type="success"
+ size="small">姝e父</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="椤圭洰鍚嶇О"
+ prop="projectName"
+ width="420"
+ show-overflow-tooltip />
+ <el-table-column label="瀹℃壒鐘舵��"
+ prop="approvalStatus"
+ width="200"
+ show-overflow-tooltip>
+ <template #default="scope">
+ <el-tag size="small">
+ {{ approvalStatusText[scope.row.approvalStatus] || '鏈煡鐘舵��' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浠樻鏂瑰紡"
+ width="100"
+ prop="paymentMethod"
+ show-overflow-tooltip />
+ <el-table-column label="鍚堝悓閲戦(鍏�)"
+ prop="contractAmount"
+ width="200"
+ show-overflow-tooltip
+ :formatter="formattedNumber" />
+ <el-table-column label="褰曞叆浜�"
+ prop="recorderName"
+ width="100"
+ show-overflow-tooltip />
+ <el-table-column label="褰曞叆鏃ユ湡"
+ prop="entryDate"
+ width="100"
+ show-overflow-tooltip />
+ <el-table-column fixed="right"
+ label="鎿嶄綔"
+ min-width="150"
+ align="center">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ size="small"
+ @click="approvePurchase(scope.row)"
+ :disabled="scope.row.approvalStatus !== 0">瀹℃壒</el-button>
+ <el-button link
+ type="primary"
+ size="small"
+ @click="rejectPurchase(scope.row)"
+ :disabled="scope.row.approvalStatus !== 0">鎷掔粷瀹℃壒</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { getToken } from "@/utils/auth";
+ import pagination from "@/components/PIMTable/Pagination.vue";
+ import {
+ ref,
+ onMounted,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+ } from "vue";
+ import { Search } from "@element-plus/icons-vue";
+ import { ElMessageBox } from "element-plus";
+ import { userListNoPage } from "@/api/system/user.js";
+ import {
+ getSalesLedgerWithProducts,
+ addOrUpdateSalesLedgerProduct,
+ delProduct,
+ delLedgerFile,
+ getProductInfoByContractNo,
+ } from "@/api/salesManagement/salesLedger.js";
+ import {
+ addOrEditPurchase,
+ delPurchase,
+ getSalesNo,
+ purchaseListPage,
+ productList,
+ getPurchaseById,
+ getOptions,
+ createPurchaseNo,
+ updateApprovalStatus,
+ } from "@/api/procurementManagement/procurementLedger.js";
+ import useFormData from "@/hooks/useFormData.js";
+ import QRCode from "qrcode";
+
+ const { proxy } = getCurrentInstance();
+ const tableData = ref([]);
+ const productData = ref([]);
+ const selectedRows = ref([]);
+ const productSelectedRows = ref([]);
+ const modelOptions = ref([]);
+ const userList = ref([]);
+ const productOptions = ref([]);
+ const salesContractList = ref([]);
+ const supplierList = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ });
+ const total = ref(0);
+ const fileList = ref([]);
+ import useUserStore from "@/store/modules/user";
+ import { modelList, productTreeList } from "@/api/basicData/product.js";
+ import dayjs from "dayjs";
+ import { getCurrentDate } from "@/utils/index.js";
+
+ const userStore = useUserStore();
+
+ // 浜岀淮鐮佺浉鍏冲彉閲�
+ const qrCodeDialogVisible = ref(false);
+ const qrCodeUrl = ref("");
+
+ // 璁㈠崟瀹℃壒鐘舵�佹樉绀烘枃鏈�
+ const approvalStatusText = {
+ 0: "寰呭鎵�",
+ 1: "瀹℃壒閫氳繃",
+ 2: "瀹℃壒澶辫触",
+ };
+
+ // 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+ const operationType = ref("");
+ const dialogFormVisible = ref(false);
+ const data = reactive({
+ searchForm: {
+ supplierName: "", // 渚涘簲鍟嗗悕绉�
+ purchaseContractNumber: "", // 閲囪喘鍚堝悓缂栧彿
+ salesContractNo: "", // 閿�鍞悎鍚岀紪鍙�
+ projectName: "", // 椤圭洰鍚嶇О
+ entryDate: null, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ form: {
+ purchaseContractNumber: "",
+ salesLedgerId: "",
+ projectName: "",
+ recorderId: "",
+ entryDate: "",
+ productData: [],
+ supplierName: "",
+ supplierId: "",
+ paymentMethod: "",
+ executionDate: "",
+ approvalStatus: "0",
+ },
+ rules: {
+ purchaseContractNumber: [
+ { required: false, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ projectName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ supplierId: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ entryDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ executionDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const { form, rules } = toRefs(data);
+ const { form: searchForm } = useFormData(data.searchForm);
+
+ // 浜у搧琛ㄥ崟寮规鏁版嵁
+ const productFormVisible = ref(false);
+ const productOperationType = ref("");
+ const productOperationIndex = ref("");
+ const currentId = ref("");
+ const productFormData = reactive({
+ productForm: {
+ productId: "",
+ productCategory: "",
+ productModelId: "",
+ specificationModel: "",
+ unit: "",
+ quantity: "",
+ taxInclusiveUnitPrice: "",
+ taxRate: "",
+ taxInclusiveTotalPrice: "",
+ taxExclusiveTotalPrice: "",
+ invoiceType: "",
+ warnNum: "",
+ },
+ productRules: {
+ productId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ productModelId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ taxInclusiveUnitPrice: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ taxRate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ warnNum: [{ required: false, message: "璇烽�夋嫨", trigger: "change" }],
+ taxInclusiveTotalPrice: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ taxExclusiveTotalPrice: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const { productForm, productRules } = toRefs(productFormData);
+ // const upload = reactive({
+ // // 涓婁紶鐨勫湴鍧�
+ // url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
+ // // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ // headers: { Authorization: "Bearer " + getToken() },
+ // });
+
+ const changeDaterange = value => {
+ if (value) {
+ searchForm.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ } else {
+ searchForm.entryDateStart = undefined;
+ searchForm.entryDateEnd = undefined;
+ }
+ handleQuery();
+ };
+
+ const formattedNumber = (row, column, cellValue) => {
+ return parseFloat(cellValue).toFixed(2);
+ };
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ // 瀛愯〃鍚堣鏂规硶
+ const summarizeChildrenTable = param => {
+ return proxy.summarizeTable(
+ param,
+ [
+ "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice",
+ "taxExclusiveTotalPrice",
+ "ticketsNum",
+ "ticketsAmount",
+ "futureTickets",
+ "futureTicketsAmount",
+ ],
+ {
+ ticketsNum: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ futureTickets: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ }
+ );
+ };
+ const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const { entryDate, ...rest } = searchForm;
+ purchaseListPage({ ...rest, ...page })
+ .then(res => {
+ tableLoading.value = false;
+ // tableData.value = res.data.records;
+ // 澶勭悊鏁版嵁锛屾坊鍔犲け鏁堢姸鎬佹爣璁�
+ tableData.value = res.data.records.map(record => ({
+ ...record,
+ isInvalid: record.isWhite === 1,
+ }));
+ tableData.value.map(item => {
+ item.children = [];
+ });
+ total.value = res.data.total;
+ expandedRowKeys.value = [];
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+ const productSelected = selectedRows => {
+ productSelectedRows.value = selectedRows;
+ };
+ const expandedRowKeys = ref([]);
+ // 灞曞紑琛�
+ const expandChange = (row, expandedRows) => {
+ if (expandedRows.length > 0) {
+ expandedRowKeys.value = [];
+ try {
+ productList({ salesLedgerId: row.id, type: 2 }).then(res => {
+ const index = tableData.value.findIndex(item => item.id === row.id);
+ if (index > -1) {
+ tableData.value[index].children = res.data;
+ }
+ expandedRowKeys.value.push(row.id);
+ });
+ } catch (error) {
+ console.log(error);
+ }
+ } else {
+ expandedRowKeys.value = [];
+ }
+ };
+ // 涓昏〃鍚堣鏂规硶
+ const summarizeMainTable = param => {
+ return proxy.summarizeTable(param, ["contractAmount"]);
+ };
+ // 瀛愯〃鍚堣鏂规硶
+ const summarizeProTable = param => {
+ return proxy.summarizeTable(param, [
+ "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice",
+ "taxExclusiveTotalPrice",
+ ]);
+ };
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ operationType.value = type;
+ form.value = {};
+ productData.value = [];
+ fileList.value = [];
+ if (operationType.value == "add") {
+ form.value.purchaseContractNumber = "";
+ }
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ getSalesNo().then(res => {
+ salesContractList.value = res;
+ });
+ getOptions().then(res => {
+ // 渚涘簲鍟嗚繃婊ゅ嚭isWhite=0 鐨勬暟鎹�
+ supplierList.value = res.data.filter(item => item.isWhite == 0);
+ });
+ form.value.recorderId = userStore.id;
+ form.value.entryDate = getCurrentDate();
+ if (type === "edit") {
+ currentId.value = row.id;
+ getPurchaseById({ id: row.id, type: 2 }).then(res => {
+ form.value = { ...res };
+ productData.value = form.value.productData;
+ if (form.value.salesLedgerFiles) {
+ fileList.value = form.value.salesLedgerFiles;
+ } else {
+ fileList.value = [];
+ }
+ });
+ }
+ dialogFormVisible.value = true;
+ };
+ // 涓婁紶鍓嶆牎妫�
+ function handleBeforeUpload(file) {
+ // 鏍℃鏂囦欢澶у皬
+ if (file.size > 1024 * 1024 * 10) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃10MB!");
+ return false;
+ }
+ proxy.$modal.loading("姝e湪涓婁紶鏂囦欢锛岃绋嶅��...");
+ return true;
+ }
+ // 涓婁紶澶辫触
+ function handleUploadError(err) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶辫触");
+ proxy.$modal.closeLoading();
+ }
+ // 涓婁紶鎴愬姛鍥炶皟
+ function handleUploadSuccess(res, file, uploadFiles) {
+ proxy.$modal.closeLoading();
+ if (res.code === 200) {
+ file.tempId = res.data.tempId;
+ proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ proxy.$refs.fileUpload.handleRemove(file);
+ }
+ }
+ // 绉婚櫎鏂囦欢
+ function handleRemove(file) {
+ console.log("handleRemove", file.id);
+ if (file.size > 1024 * 1024 * 10) {
+ // 浠呭墠绔竻鐞嗭紝涓嶈皟鐢ㄥ垹闄ゆ帴鍙e拰鎻愮ず
+ return;
+ }
+ if (operationType.value === "edit") {
+ let ids = [];
+ ids.push(file.id);
+ delLedgerFile(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ });
+ }
+ }
+ // 鎻愪氦琛ㄥ崟
+ const submitForm = n => {
+ proxy.$refs["formRef"].validate(async valid => {
+ if (valid) {
+ if (productData.value.length > 0) {
+ form.value.productData = proxy.HaveJson(productData.value);
+ } else {
+ proxy.$modal.msgWarning("璇锋坊鍔犱骇鍝佷俊鎭�");
+ return;
+ }
+ let tempFileIds = [];
+ if (fileList.value.length > 0) {
+ tempFileIds = fileList.value.map(item => item.tempId);
+ }
+ form.value.tempFileIds = tempFileIds;
+ form.value.type = 2;
+ form.value.approvalStatus = n;
+
+ // 濡傛灉閲囪喘鍚堝悓鍙蜂负绌猴紝鍒欐牴鎹綍鍏ユ棩鏈熻嚜鍔ㄧ敓鎴�
+ if (!form.value.purchaseContractNumber) {
+ try {
+ const purchaseNoRes = await createPurchaseNo(form.value.entryDate);
+ if (purchaseNoRes?.data) {
+ form.value.purchaseContractNumber = purchaseNoRes.data;
+ }
+ } catch (error) {
+ console.error("鐢熸垚閲囪喘鍚堝悓鍙峰け璐�:", error);
+ proxy.$modal.msgWarning("鐢熸垚閲囪喘鍚堝悓鍙峰け璐�");
+ return;
+ }
+ }
+
+ addOrEditPurchase(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ });
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ };
+ // 鎵撳紑浜у搧寮规
+ const openProductForm = (type, row, index) => {
+ productOperationType.value = type;
+ productOperationIndex.value = index;
+ productForm.value = {};
+ proxy.resetForm("productFormRef");
+ if (type === "edit") {
+ productForm.value = { ...row };
+ }
+ productFormVisible.value = true;
+ getProductOptions();
+ };
+ const getProductOptions = () => {
+ productTreeList().then(res => {
+ productOptions.value = convertIdToValue(res);
+ });
+ };
+ const getModels = value => {
+ if (value) {
+ productForm.value.productCategory =
+ findNodeById(productOptions.value, value) || "";
+ modelList({ id: value }).then(res => {
+ modelOptions.value = res;
+ });
+ } else {
+ productForm.value.productCategory = "";
+ modelOptions.value = [];
+ }
+ };
+ const getProductModel = value => {
+ const index = modelOptions.value.findIndex(item => item.id === value);
+ if (index !== -1) {
+ productForm.value.specificationModel = modelOptions.value[index].model;
+ productForm.value.unit = modelOptions.value[index].unit;
+ } else {
+ productForm.value.specificationModel = null;
+ productForm.value.unit = null;
+ }
+ };
+ const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥炶鑺傜偣鐨刲abel
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId);
+ if (foundNode) {
+ return foundNode; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝鐩存帴杩斿洖锛堝凡缁忔槸label瀛楃涓诧級
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+ };
+ function convertIdToValue(data) {
+ return data.map(item => {
+ const { id, children, ...rest } = item;
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+
+ return newItem;
+ });
+ }
+ // 鎻愪氦浜у搧琛ㄥ崟
+ const submitProduct = () => {
+ proxy.$refs["productFormRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "edit") {
+ submitProductEdit();
+ } else {
+ if (productOperationType.value === "add") {
+ productData.value.push({ ...productForm.value });
+ console.log("productData.value---", productData.value);
+ } else {
+ productData.value[productOperationIndex.value] = {
+ ...productForm.value,
+ };
+ }
+ closeProductDia();
+ }
+ }
+ });
+ };
+ const submitProductEdit = () => {
+ productForm.value.salesLedgerId = currentId.value;
+ productForm.value.type = 2;
+ addOrUpdateSalesLedgerProduct(productForm.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeProductDia();
+ getPurchaseById({ id: currentId.value, type: 2 }).then(res => {
+ productData.value = res.productData;
+ });
+ });
+ };
+ // 鍒犻櫎浜у搧
+ const deleteProduct = () => {
+ if (productSelectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ if (operationType.value === "add") {
+ productData.value = productData.value.filter(
+ item => !productSelectedRows.value.includes(item)
+ );
+ productSelectedRows.value = [];
+ } else {
+ let ids = [];
+ if (productSelectedRows.value.length > 0) {
+ ids = productSelectedRows.value.map(item => item.id);
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delProduct(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ closeProductDia();
+ getSalesLedgerWithProducts({ id: currentId.value, type: 2 }).then(
+ res => {
+ productData.value = res.productData;
+ }
+ );
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ }
+ };
+ // 鍏抽棴浜у搧寮规
+ const closeProductDia = () => {
+ proxy.resetForm("productFormRef");
+ productFormVisible.value = false;
+ };
+ // 瀹℃壒閫氳繃鏂规硶
+ const approvePurchase = row => {
+ ElMessageBox.confirm(
+ `纭閫氳繃閲囪喘鍚堝悓鍙蜂负 ${row.purchaseContractNumber} 鐨勫鎵癸紵`,
+ "瀹℃壒纭",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ updateApprovalStatus({ id: row.id, approvalStatus: 1 }).then(res => {
+ proxy.$modal.msgSuccess("瀹℃壒鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑堝鎵�");
+ });
+ };
+
+ // 瀹℃壒鎷掔粷鏂规硶
+ const rejectPurchase = row => {
+ ElMessageBox.confirm(
+ `纭鎷掔粷閲囪喘鍚堝悓鍙蜂负 ${row.purchaseContractNumber} 鐨勫鎵癸紵`,
+ "瀹℃壒纭",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ updateApprovalStatus({ id: row.id, approvalStatus: 2 }).then(res => {
+ proxy.$modal.msgSuccess("瀹℃壒鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑堝鎵�");
+ });
+ };
+
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/purchase/ledger/export", {}, "閲囪喘鍙拌处.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ // 妫�鏌ユ槸鍚︽湁浠栦汉缁存姢鐨勬暟鎹�
+ const unauthorizedData = selectedRows.value.filter(
+ item => item.recorderName !== userStore.nickName
+ );
+ if (unauthorizedData.length > 0) {
+ proxy.$modal.msgWarning("涓嶅彲鍒犻櫎浠栦汉缁存姢鐨勬暟鎹�");
+ return;
+ }
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delPurchase(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ const mathNum = () => {
+ if (!productForm.value.taxRate) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨绋庣巼");
+ return;
+ }
+ if (!productForm.value.taxInclusiveUnitPrice) {
+ return;
+ }
+ if (!productForm.value.quantity) {
+ return;
+ }
+ // 鍚◣鎬讳环璁$畻
+ productForm.value.taxInclusiveTotalPrice =
+ proxy.calculateTaxIncludeTotalPrice(
+ productForm.value.taxInclusiveUnitPrice,
+ productForm.value.quantity
+ );
+ if (productForm.value.taxRate) {
+ // 涓嶅惈绋庢�讳环璁$畻
+ productForm.value.taxExclusiveTotalPrice =
+ proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ );
+ }
+ };
+ const reverseMathNum = field => {
+ if (!productForm.value.taxRate) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨绋庣巼");
+ return;
+ }
+ const taxRate = Number(productForm.value.taxRate);
+ if (!taxRate) return;
+ if (field === "taxInclusiveTotalPrice") {
+ // 宸茬煡鍚◣鎬讳环鍜屾暟閲忥紝鍙嶇畻鍚◣鍗曚环
+ if (productForm.value.quantity) {
+ productForm.value.taxInclusiveUnitPrice = (
+ Number(productForm.value.taxInclusiveTotalPrice) /
+ Number(productForm.value.quantity)
+ ).toFixed(2);
+ }
+ // 宸茬煡鍚◣鎬讳环鍜屽惈绋庡崟浠凤紝鍙嶇畻鏁伴噺
+ else if (productForm.value.taxInclusiveUnitPrice) {
+ productForm.value.quantity = (
+ Number(productForm.value.taxInclusiveTotalPrice) /
+ Number(productForm.value.taxInclusiveUnitPrice)
+ ).toFixed(2);
+ }
+ // 鍙嶇畻涓嶅惈绋庢�讳环
+ productForm.value.taxExclusiveTotalPrice = (
+ Number(productForm.value.taxInclusiveTotalPrice) /
+ (1 + taxRate / 100)
+ ).toFixed(2);
+ } else if (field === "taxExclusiveTotalPrice") {
+ // 鍙嶇畻鍚◣鎬讳环
+ productForm.value.taxInclusiveTotalPrice = (
+ Number(productForm.value.taxExclusiveTotalPrice) *
+ (1 + taxRate / 100)
+ ).toFixed(2);
+ // 宸茬煡鏁伴噺锛屽弽绠楀惈绋庡崟浠�
+ if (productForm.value.quantity) {
+ productForm.value.taxInclusiveUnitPrice = (
+ Number(productForm.value.taxInclusiveTotalPrice) /
+ Number(productForm.value.quantity)
+ ).toFixed(2);
+ }
+ // 宸茬煡鍚◣鍗曚环锛屽弽绠楁暟閲�
+ else if (productForm.value.taxInclusiveUnitPrice) {
+ productForm.value.quantity = (
+ Number(productForm.value.taxInclusiveTotalPrice) /
+ Number(productForm.value.taxInclusiveUnitPrice)
+ ).toFixed(2);
+ }
+ }
+ };
+ // 閿�鍞悎鍚岄�夋嫨鏀瑰彉鏂规硶
+ const salesLedgerChange = async row => {
+ console.log("row", row);
+ var index = salesContractList.value.findIndex(item => item.id == row);
+ console.log("index", index);
+ if (index > -1) {
+ form.value.projectName = salesContractList.value[index].projectName;
+ await querygProductInfoByContractNo();
+ }
+ };
+
+ const querygProductInfoByContractNo = async () => {
+ const { code, data } = await getProductInfoByContractNo({
+ contractNo: form.value.salesLedgerId,
+ });
+ if (code == 200) {
+ productData.value = data;
+ }
+ };
+
+ // 鏄剧ず浜岀淮鐮�
+ const showQRCode = async row => {
+ try {
+ // 鏋勫缓浜岀淮鐮佸唴瀹癸紝鍙寘鍚噰璐悎鍚屽彿锛堢函鏂囨湰锛�
+ const qrContent = row.purchaseContractNumber || "";
+ // 妫�鏌ュ唴瀹规槸鍚︿负绌�
+ if (!qrContent || qrContent.trim() === "") {
+ proxy.$modal.msgWarning("璇ヨ娌℃湁閲囪喘鍚堝悓鍙凤紝鏃犳硶鐢熸垚浜岀淮鐮�");
+ return;
+ }
+ qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
+ width: 200,
+ margin: 2,
+ color: {
+ dark: "#000000",
+ light: "#FFFFFF",
+ },
+ });
+ qrCodeDialogVisible.value = true;
+ } catch (error) {
+ console.error("鐢熸垚浜岀淮鐮佸け璐�:", error);
+ proxy.$modal.msgError("鐢熸垚浜岀淮鐮佸け璐ワ細" + error.message);
+ }
+ };
+
+ // 涓嬭浇浜岀淮鐮�
+ const downloadQRCode = () => {
+ if (!qrCodeUrl.value) {
+ proxy.$modal.msgWarning("浜岀淮鐮佹湭鐢熸垚");
+ return;
+ }
+
+ const a = document.createElement("a");
+ a.href = qrCodeUrl.value;
+ a.download = `閲囪喘鍚堝悓鍙蜂簩缁寸爜_${new Date().getTime()}.png`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ proxy.$modal.msgSuccess("涓嬭浇鎴愬姛");
+ };
+
+ // 鎵爜鏂板瀵硅瘽妗嗙浉鍏冲彉閲�
+ const scanAddDialogVisible = ref(false);
+ const scanAddForm = reactive({
+ scanContent: "",
+ purchaseContractNumber: "",
+ supplierName: "",
+ projectName: "",
+ contractAmount: "",
+ paymentMethod: "",
+ recorderName: "",
+ scanRemark: "",
+ });
+ const scanAddRules = {
+ purchaseContractNumber: [
+ { required: true, message: "璇疯緭鍏ラ噰璐悎鍚屽彿", trigger: "blur" },
+ ],
+ supplierName: [
+ { required: true, message: "璇疯緭鍏ヤ緵搴斿晢鍚嶇О", trigger: "blur" },
+ ],
+ projectName: [{ required: true, message: "璇疯緭鍏ラ」鐩悕绉�", trigger: "blur" }],
+ };
+
+ // 鎵爜鐧昏瀵硅瘽妗嗙浉鍏冲彉閲�
+ const scanDialogVisible = ref(false);
+ const scanForm = reactive({
+ purchaseContractNumber: "",
+ supplierName: "",
+ projectName: "",
+ scanTime: "",
+ scannerName: "",
+ scanStatus: "鏈壂鐮�",
+ scanRemark: "",
+ });
+ const scanRules = {
+ scanRemark: [{ required: true, message: "璇疯緭鍏ユ壂鐮佸娉�", trigger: "blur" }],
+ };
+ const scanRecords = ref([]);
+
+ // 鎵撳紑鎵爜鏂板瀵硅瘽妗�
+ const openScanAddDialog = () => {
+ scanAddForm.scanContent = "";
+ scanAddForm.purchaseContractNumber = "";
+ scanAddForm.supplierName = "";
+ scanAddForm.projectName = "";
+ scanAddForm.contractAmount = "";
+ scanAddForm.paymentMethod = "";
+ scanAddForm.recorderName = userStore.nickName;
+ scanAddForm.scanRemark = "";
+ scanAddDialogVisible.value = true;
+ };
+
+ // 瑙f瀽鎵爜鍐呭锛堟ā鎷熻В鏋愪簩缁寸爜鏁版嵁锛�
+ const parseScanContent = content => {
+ if (!content) return;
+
+ // 妯℃嫙瑙f瀽浜岀淮鐮佸唴瀹癸紝杩欓噷鍙互鏍规嵁瀹為檯闇�姹傝皟鏁磋В鏋愰�昏緫
+ // 鍋囪鎵爜鍐呭鏍煎紡涓猴細鍚堝悓鍙穦渚涘簲鍟唡椤圭洰|閲戦|浠樻鏂瑰紡
+ const parts = content.split("|");
+ if (parts.length >= 3) {
+ scanAddForm.purchaseContractNumber = parts[0] || "";
+ scanAddForm.supplierName = parts[1] || "";
+ scanAddForm.projectName = parts[2] || "";
+ scanAddForm.contractAmount = parts[3] || "";
+ scanAddForm.paymentMethod = parts[4] || "";
+ }
+ };
+
+ // 鍏抽棴鎵爜鏂板瀵硅瘽妗�
+ const closeScanAddDialog = () => {
+ scanAddDialogVisible.value = false;
+ proxy.resetForm("scanAddFormRef");
+ };
+
+ // 鎻愪氦鎵爜鏂板
+ const submitScanAdd = () => {
+ proxy.$refs["scanAddFormRef"].validate(valid => {
+ if (valid) {
+ // 鏋勫缓鏂板鏁版嵁
+ const newData = {
+ purchaseContractNumber: scanAddForm.purchaseContractNumber,
+ supplierName: scanAddForm.supplierName,
+ projectName: scanAddForm.projectName,
+ contractAmount: scanAddForm.contractAmount,
+ paymentMethod: scanAddForm.paymentMethod,
+ recorderName: scanAddForm.recorderName,
+ entryDate: getCurrentDate(),
+ remark: scanAddForm.scanRemark,
+ type: 2,
+ };
+
+ // 妯℃嫙鏂板鎴愬姛
+ proxy.$modal.msgSuccess("鎵爜鏂板鎴愬姛锛�");
+ closeScanAddDialog();
+
+ // 鍙互閫夋嫨鏄惁鍒锋柊鍒楄〃
+ // getList();
+ }
+ });
+ };
+
+ // 鎵撳紑鎵爜鐧昏瀵硅瘽妗�
+ const openScanDialog = row => {
+ scanForm.purchaseContractNumber = row.purchaseContractNumber;
+ scanForm.supplierName = row.supplierName;
+ scanForm.projectName = row.projectName;
+ scanForm.scanTime = getCurrentDateTime();
+ scanForm.scannerName = userStore.nickName;
+ scanForm.scanStatus = "鏈壂鐮�";
+ scanForm.scanRemark = "";
+ scanRecords.value = [];
+ scanDialogVisible.value = true;
+ };
+
+ // 鍏抽棴鎵爜鐧昏瀵硅瘽妗�
+ const closeScanDialog = () => {
+ scanDialogVisible.value = false;
+ proxy.resetForm("scanFormRef");
+ };
+
+ // 鎻愪氦鎵爜鐧昏
+ const submitScan = () => {
+ proxy.$refs["scanFormRef"].validate(valid => {
+ if (valid) {
+ // 娣诲姞鎵爜璁板綍
+ scanRecords.value.push({
+ ...scanForm,
+ id: Date.now(), // 妯℃嫙ID
+ scanTime: getCurrentDateTime(),
+ });
+ scanForm.scanStatus = "宸叉壂鐮�";
+ scanForm.scanRemark = scanForm.scanRemark || "鏃�";
+ proxy.$modal.msgSuccess("鎵爜鐧昏鎴愬姛锛�");
+ closeScanDialog();
+ }
+ });
+ };
+
+ // 鑾峰彇褰撳墠鏃ユ湡鏃堕棿
+ function getCurrentDateTime() {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = String(now.getMonth() + 1).padStart(2, "0");
+ const day = String(now.getDate()).padStart(2, "0");
+ const hours = String(now.getHours()).padStart(2, "0");
+ const minutes = String(now.getMinutes()).padStart(2, "0");
+ const seconds = String(now.getSeconds()).padStart(2, "0");
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+ }
+
+ // 娣诲姞琛岀被鍚嶆柟娉�
+ const tableRowClassName = ({ row }) => {
+ return row.isInvalid ? "invalid-row" : "";
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .invalid-row {
+ opacity: 0.6;
+ background-color: #f5f7fa;
+ }
+</style>
diff --git a/src/views/collaborativeApproval/reportGeneration/index.vue b/src/views/collaborativeApproval/reportGeneration/index.vue
new file mode 100644
index 0000000..c160ad7
--- /dev/null
+++ b/src/views/collaborativeApproval/reportGeneration/index.vue
@@ -0,0 +1,596 @@
+<template>
+ <div class="report-generation">
+ <!-- 椤甸潰澶撮儴 -->
+ <div class="page-header">
+ <h2>椤圭洰鎬荤粨鎶ュ憡鐢熸垚</h2>
+ <div class="header-actions">
+ <el-button v-if="!reportGenerated" type="primary" @click="generateReport" :loading="generating">
+ <el-icon><Document /></el-icon>
+ 鐢熸垚鎶ュ憡
+ </el-button>
+ <el-button v-if="reportGenerated" type="primary" @click="resetConfig">
+ <el-icon><Refresh /></el-icon>
+ 鐢熸垚鏂版姤鍛�
+ </el-button>
+ <el-button @click="exportReport" :disabled="!reportGenerated">
+ <el-icon><Download /></el-icon>
+ 瀵煎嚭鎶ュ憡
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 鎶ュ憡閰嶇疆鍖哄煙 -->
+ <el-card class="config-card" v-if="!reportGenerated">
+ <template #header>
+ <span>鎶ュ憡閰嶇疆</span>
+ </template>
+ <el-form :model="reportConfig" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="椤圭洰鍚嶇О">
+ <el-select v-model="reportConfig.projectId" placeholder="璇烽�夋嫨椤圭洰">
+ <el-option
+ v-for="project in projectList"
+ :key="project.id"
+ :label="project.name"
+ :value="project.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎶ュ憡鍛ㄦ湡">
+ <el-date-picker
+ v-model="reportConfig.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </el-card>
+
+ <!-- 鎶ュ憡鍐呭灞曠ず鍖哄煙 -->
+ <div v-if="reportGenerated" class="report-content">
+ <!-- 椤圭洰鍩烘湰淇℃伅 -->
+ <el-card class="report-section">
+ <template #header>
+ <span>椤圭洰鍩烘湰淇℃伅</span>
+ </template>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="椤圭洰鍚嶇О">{{ reportData.projectInfo.name }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰缁忕悊">{{ reportData.projectInfo.manager }}</el-descriptions-item>
+ <el-descriptions-item label="寮�濮嬫椂闂�">{{ reportData.projectInfo.startDate }}</el-descriptions-item>
+ <el-descriptions-item label="缁撴潫鏃堕棿">{{ reportData.projectInfo.endDate }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鐘舵��">
+ <el-tag :type="getStatusType(reportData.projectInfo.status)">
+ {{ reportData.projectInfo.status }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎬婚绠�">{{ reportData.projectInfo.budget }}</el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+
+ <!-- 浠诲姟瀹屾垚鐜囩粺璁� -->
+ <el-card class="report-section">
+ <template #header>
+ <span>浠诲姟瀹屾垚鐜囩粺璁�</span>
+ </template>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="completion-stats">
+ <div class="stat-item">
+ <div class="stat-label">鎬讳换鍔℃暟</div>
+ <div class="stat-value">{{ reportData.taskStats.total }}</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-label">宸插畬鎴�</div>
+ <div class="stat-value completed">{{ reportData.taskStats.completed }}</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-label">杩涜涓�</div>
+ <div class="stat-value in-progress">{{ reportData.taskStats.inProgress }}</div>
+ </div>
+ <div class="stat-item">
+ <div class="stat-label">鏈紑濮�</div>
+ <div class="stat-value pending">{{ reportData.taskStats.pending }}</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="completion-rate">
+ <el-progress
+ type="circle"
+ :percentage="reportData.taskStats.completionRate"
+ :width="150"
+ :stroke-width="8"
+ >
+ <template #default="{ percentage }">
+ <span class="percentage-value">{{ percentage }}%</span>
+ <div class="percentage-label">瀹屾垚鐜�</div>
+ </template>
+ </el-progress>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 闂璁板綍缁熻 -->
+ <el-card class="report-section">
+ <template #header>
+ <span>闂璁板綍缁熻</span>
+ </template>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <div class="issue-summary">
+ <div class="summary-item">
+ <div class="summary-label">鎬婚棶棰樻暟</div>
+ <div class="summary-value">{{ reportData.issueStats.total }}</div>
+ </div>
+ <div class="summary-item">
+ <div class="summary-label">宸茶В鍐�</div>
+ <div class="summary-value resolved">{{ reportData.issueStats.resolved }}</div>
+ </div>
+ <div class="summary-item">
+ <div class="summary-label">寰呰В鍐�</div>
+ <div class="summary-value pending">{{ reportData.issueStats.pending }}</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="16">
+ <el-table :data="reportData.issueStats.topIssues" size="small">
+ <el-table-column prop="title" label="涓昏闂" />
+ <el-table-column prop="severity" label="涓ラ噸绋嬪害" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getSeverityType(row.severity)" size="small">
+ {{ row.severity }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="80">
+ <template #default="{ row }">
+ <el-tag :type="row.status === '宸茶В鍐�' ? 'success' : 'warning'" size="small">
+ {{ row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 寤惰鍒嗘瀽 -->
+ <el-card class="report-section">
+ <template #header>
+ <span>寤惰鍒嗘瀽</span>
+ </template>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="delay-stats">
+ <div class="delay-item">
+ <div class="delay-label">寤惰浠诲姟鏁�</div>
+ <div class="delay-value">{{ reportData.delayAnalysis.delayedTasks }}</div>
+ </div>
+ <div class="delay-item">
+ <div class="delay-label">骞冲潎寤惰澶╂暟</div>
+ <div class="delay-value">{{ reportData.delayAnalysis.avgDelayDays }}</div>
+ </div>
+ <div class="delay-item">
+ <div class="delay-label">鏈�澶у欢璇ぉ鏁�</div>
+ <div class="delay-value">{{ reportData.delayAnalysis.maxDelayDays }}</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="delay-reasons">
+ <h4>涓昏寤惰鍘熷洜</h4>
+ <ul>
+ <li v-for="reason in reportData.delayAnalysis.reasons" :key="reason.reason">
+ {{ reason.reason }} ({{ reason.count }}娆�)
+ </li>
+ </ul>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 鍥㈤槦缁╂晥 -->
+ <el-card class="report-section">
+ <template #header>
+ <span>鍥㈤槦缁╂晥</span>
+ </template>
+ <el-table :data="reportData.teamPerformance" size="small">
+ <el-table-column prop="name" label="鎴愬憳濮撳悕" />
+ <el-table-column prop="completedTasks" label="瀹屾垚浠诲姟鏁�" />
+ <el-table-column prop="completionRate" label="瀹屾垚鐜�" width="100">
+ <template #default="{ row }">
+ <el-progress :percentage="row.completionRate" :show-text="false" />
+ <span style="margin-left: 10px;">{{ row.completionRate }}%</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="performance" label="缁╂晥璇勭骇" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getPerformanceType(row.performance)">
+ {{ row.performance }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 鎬荤粨涓庡缓璁� -->
+ <el-card class="report-section">
+ <template #header>
+ <span>鎬荤粨涓庡缓璁�</span>
+ </template>
+ <div class="summary-content">
+ <h4>椤圭洰鎬荤粨</h4>
+ <p>{{ reportData.summary.conclusion }}</p>
+
+ <h4>鏀硅繘寤鸿</h4>
+ <ul>
+ <li v-for="suggestion in reportData.summary.suggestions" :key="suggestion">
+ {{ suggestion }}
+ </li>
+ </ul>
+ </div>
+ </el-card>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { Document, Download, Refresh } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+
+// 鍝嶅簲寮忔暟鎹�
+const generating = ref(false)
+const reportGenerated = ref(false)
+
+// 鎶ュ憡閰嶇疆
+const reportConfig = reactive({
+ projectId: '',
+ dateRange: []
+})
+
+// 椤圭洰鍒楄〃
+const projectList = ref([
+ { id: 1, name: '浜у搧搴撳瓨绠$悊绯荤粺' },
+ { id: 2, name: '瀹㈡埛鍏崇郴绠$悊骞冲彴' },
+ { id: 3, name: '璐㈠姟绠$悊绯荤粺鍗囩骇' },
+ { id: 4, name: '绉诲姩绔簲鐢ㄥ紑鍙�' }
+])
+
+// 鎶ュ憡鏁版嵁
+const reportData = ref({})
+
+// 妯℃嫙鎶ュ憡鏁版嵁
+const mockReportData = {
+ projectInfo: {
+ name: '浜у搧搴撳瓨绠$悊绯荤粺',
+ manager: '寮犱笁',
+ startDate: '2024-01-01',
+ endDate: '2024-03-31',
+ status: '宸插畬鎴�',
+ budget: '楼500,000'
+ },
+ taskStats: {
+ total: 45,
+ completed: 38,
+ inProgress: 5,
+ pending: 2,
+ completionRate: 84
+ },
+ issueStats: {
+ total: 12,
+ resolved: 9,
+ pending: 3,
+ topIssues: [
+ { title: '鏁版嵁搴撹繛鎺ヨ秴鏃�', severity: '楂�', status: '宸茶В鍐�' },
+ { title: '鍓嶇椤甸潰鍝嶅簲鎱�', severity: '涓�', status: '宸茶В鍐�' },
+ { title: '鏉冮檺楠岃瘉寮傚父', severity: '楂�', status: '寰呰В鍐�' },
+ { title: '鎶ヨ〃瀵煎嚭鍔熻兘缂哄け', severity: '涓�', status: '寰呰В鍐�' }
+ ]
+ },
+ delayAnalysis: {
+ delayedTasks: 8,
+ avgDelayDays: 3.5,
+ maxDelayDays: 12,
+ reasons: [
+ { reason: '闇�姹傚彉鏇�', count: 3 },
+ { reason: '鎶�鏈毦棰�', count: 2 },
+ { reason: '璧勬簮涓嶈冻', count: 2 },
+ { reason: '澶栭儴渚濊禆', count: 1 }
+ ]
+ },
+ teamPerformance: [
+ { name: '鏉庡洓', completedTasks: 12, completionRate: 92, performance: '浼樼' },
+ { name: '鐜嬩簲', completedTasks: 10, completionRate: 85, performance: '鑹ソ' },
+ { name: '璧靛叚', completedTasks: 8, completionRate: 78, performance: '鑹ソ' },
+ { name: '閽变竷', completedTasks: 8, completionRate: 72, performance: '涓�鑸�' }
+ ],
+ summary: {
+ conclusion: '鏈」鐩暣浣撴墽琛屾儏鍐佃壇濂斤紝浠诲姟瀹屾垚鐜囪揪鍒�84%锛屽洟闃熷崗浣滄晥鐜囪緝楂樸�備富瑕侀棶棰橀泦涓湪鎶�鏈疄鐜板拰闇�姹傚彉鏇存柟闈紝閫氳繃鍙婃椂娌熼�氬拰鎶�鏈敾鍏筹紝澶ч儴鍒嗛棶棰樺凡寰楀埌瑙e喅銆�',
+ suggestions: [
+ '鍔犲己闇�姹傚垎鏋愰樁娈电殑宸ヤ綔锛屽噺灏戝悗鏈熼渶姹傚彉鏇�',
+ '寤虹珛鎶�鏈毦棰橀璀︽満鍒讹紝鎻愬墠璇嗗埆鍜岃В鍐虫妧鏈闄�',
+ '浼樺寲鍥㈤槦璧勬簮閰嶇疆锛屾彁楂樻暣浣撳伐浣滄晥鐜�',
+ '瀹屽杽椤圭洰绠$悊娴佺▼锛屽姞寮鸿繃绋嬬洃鎺�'
+ ]
+ }
+}
+
+// 鐢熸垚鎶ュ憡
+const generateReport = async () => {
+ if (!reportConfig.projectId) {
+ ElMessage.warning('璇烽�夋嫨椤圭洰')
+ return
+ }
+
+ generating.value = true
+
+ // 妯℃嫙鐢熸垚鎶ュ憡鐨勮繃绋�
+ setTimeout(() => {
+ reportData.value = mockReportData
+ reportGenerated.value = true
+ generating.value = false
+ ElMessage.success('鎶ュ憡鐢熸垚鎴愬姛')
+ }, 2000)
+}
+
+// 瀵煎嚭鎶ュ憡
+const exportReport = () => {
+ ElMessage.success('鎶ュ憡瀵煎嚭鍔熻兘寮�鍙戜腑...')
+}
+
+// 閲嶇疆閰嶇疆锛岀敓鎴愭柊鎶ュ憡
+const resetConfig = () => {
+ reportGenerated.value = false
+ reportData.value = {}
+ // 閲嶇疆閰嶇疆涓洪粯璁ゅ��
+ reportConfig.projectId = 1
+ const endDate = new Date()
+ const startDate = new Date()
+ startDate.setDate(startDate.getDate() - 30)
+ reportConfig.dateRange = [
+ startDate.toISOString().split('T')[0],
+ endDate.toISOString().split('T')[0]
+ ]
+ ElMessage.success('宸查噸缃厤缃紝鍙互鐢熸垚鏂版姤鍛�')
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const statusMap = {
+ '宸插畬鎴�': 'success',
+ '杩涜涓�': 'warning',
+ '鏈紑濮�': 'info',
+ '宸叉殏鍋�': 'danger'
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇涓ラ噸绋嬪害绫诲瀷
+const getSeverityType = (severity) => {
+ const severityMap = {
+ '楂�': 'danger',
+ '涓�': 'warning',
+ '浣�': 'success'
+ }
+ return severityMap[severity] || 'info'
+}
+
+// 鑾峰彇缁╂晥绫诲瀷
+const getPerformanceType = (performance) => {
+ const performanceMap = {
+ '浼樼': 'success',
+ '鑹ソ': 'primary',
+ '涓�鑸�': 'warning',
+ '寰呮敼杩�': 'danger'
+ }
+ return performanceMap[performance] || 'info'
+}
+
+// 缁勪欢鎸傝浇鏃跺垵濮嬪寲
+onMounted(() => {
+ // 璁剧疆榛樿椤圭洰
+ reportConfig.projectId = 1
+ // 璁剧疆榛樿鏃ユ湡鑼冨洿锛堟渶杩�30澶╋級
+ const endDate = new Date()
+ const startDate = new Date()
+ startDate.setDate(startDate.getDate() - 30)
+ reportConfig.dateRange = [
+ startDate.toISOString().split('T')[0],
+ endDate.toISOString().split('T')[0]
+ ]
+})
+</script>
+
+<style scoped>
+.report-generation {
+ padding: 20px;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.page-header h2 {
+ margin: 0;
+ color: #303133;
+}
+
+.header-actions {
+ display: flex;
+ gap: 10px;
+}
+
+.config-card {
+ margin-bottom: 20px;
+}
+
+.report-content {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.report-section {
+ margin-bottom: 20px;
+}
+
+.completion-stats {
+ display: flex;
+ justify-content: space-around;
+ padding: 20px 0;
+}
+
+.stat-item {
+ text-align: center;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #909399;
+ margin-bottom: 8px;
+}
+
+.stat-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.stat-value.completed {
+ color: #67c23a;
+}
+
+.stat-value.in-progress {
+ color: #e6a23c;
+}
+
+.stat-value.pending {
+ color: #909399;
+}
+
+.completion-rate {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 200px;
+}
+
+.percentage-value {
+ font-size: 20px;
+ font-weight: bold;
+ color: #409eff;
+}
+
+.percentage-label {
+ font-size: 12px;
+ color: #909399;
+ margin-top: 4px;
+}
+
+.issue-summary {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ padding: 20px 0;
+}
+
+.summary-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.summary-label {
+ font-size: 14px;
+ color: #606266;
+}
+
+.summary-value {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.summary-value.resolved {
+ color: #67c23a;
+}
+
+.summary-value.pending {
+ color: #e6a23c;
+}
+
+.delay-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ padding: 20px 0;
+}
+
+.delay-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.delay-label {
+ font-size: 14px;
+ color: #606266;
+}
+
+.delay-value {
+ font-size: 18px;
+ font-weight: bold;
+ color: #e6a23c;
+}
+
+.delay-reasons h4 {
+ margin: 0 0 15px 0;
+ color: #303133;
+}
+
+.delay-reasons ul {
+ margin: 0;
+ padding-left: 20px;
+}
+
+.delay-reasons li {
+ margin-bottom: 8px;
+ color: #606266;
+}
+
+.summary-content h4 {
+ margin: 0 0 10px 0;
+ color: #303133;
+}
+
+.summary-content p {
+ line-height: 1.6;
+ color: #606266;
+ margin-bottom: 20px;
+}
+
+.summary-content ul {
+ margin: 0;
+ padding-left: 20px;
+}
+
+.summary-content li {
+ margin-bottom: 8px;
+ color: #606266;
+ line-height: 1.5;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/rpaManagement/index.vue b/src/views/collaborativeApproval/rpaManagement/index.vue
new file mode 100644
index 0000000..c734b28
--- /dev/null
+++ b/src/views/collaborativeApproval/rpaManagement/index.vue
@@ -0,0 +1,366 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">绋嬪簭鍚嶏細</span>
+ <el-input
+ v-model="searchForm.programName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ョ▼搴忓悕鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <span class="search_title ml10">鎵ц鐘舵�侊細</span>
+ <el-select v-model="searchForm.status" clearable @change="handleQuery" style="width: 240px">
+ <el-option label="杩愯涓�" :value="'running'" />
+ <el-option label="宸插仠姝�" :value="'stopped'" />
+ <el-option label="寮傚父" :value="'error'" />
+ </el-select>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button @click="handleExport" style="margin-right: 10px">瀵煎嚭</el-button>
+ <el-button type="primary" @click="openForm('add')">鏂板</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+
+ <!-- RPA琛ㄥ崟寮圭獥 -->
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="500px"
+ :close-on-click-modal="false"
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px"
+ >
+ <el-form-item label="绋嬪簭鍚�" prop="programName">
+ <el-input
+ v-model="form.programName"
+ placeholder="璇疯緭鍏ョ▼搴忓悕"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="鎵ц鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鎵ц鐘舵��" style="width: 100%">
+ <el-option label="杩愯涓�" value="running" />
+ <el-option label="宸插仠姝�" value="stopped" />
+ <el-option label="寮傚父" value="error" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎻忚堪" prop="description">
+ <el-input
+ v-model="form.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏PA绋嬪簭鎻忚堪"
+ clearable
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import {listRpa, addRpa, updateRpa, delRpa, delRpaBatch} from "@/api/collaborativeApproval/rpaManagement.js";
+// 鍝嶅簲寮忔暟鎹�
+const data = reactive({
+ searchForm: {
+ programName: "",
+ status: "",
+ },
+ form: {
+ programName: "",
+ status: "",
+ description: ""
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+});
+
+const { searchForm, form, dialogVisible, dialogTitle, dialogType, selectedIds, tableLoading, page, tableData } = toRefs(data);
+
+// 琛ㄥ崟寮曠敤
+const formRef = ref();
+// 閫夋嫨鐨勮鏁版嵁
+const selectedRows = ref([]);
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+ programName: [
+ { required: true, message: "璇疯緭鍏ョ▼搴忓悕", trigger: "blur" }
+ ],
+ status: [
+ { required: true, message: "璇烽�夋嫨鎵ц鐘舵��", trigger: "change" }
+ ]
+};
+
+// 琛ㄦ牸鍒楅厤缃�
+const tableColumn = ref([
+ {
+ label: "绋嬪簭鍚�",
+ prop: "programName",
+ // width: 200,
+ },
+ {
+ label: "鎵ц鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ // width: 120,
+ formatData: (params) => {
+ const statusMap = {
+ running: "杩愯涓�",
+ stopped: "宸插仠姝�",
+ error: "寮傚父"
+ };
+ return statusMap[params] || params;
+ },
+ formatType: (params) => {
+ const typeMap = {
+ running: "success",
+ stopped: "info",
+ error: "danger"
+ };
+ return typeMap[params] || "info";
+ }
+ },
+ {
+ label: "鎻忚堪",
+ prop: "description",
+ // width: 300,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ // width: 180,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 150,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ }
+ },
+ // {
+ // name: "寮�濮�",
+ // type: "text",
+ // clickFun: (row) => {
+ // handleStart(row);
+ // },
+ // disabled: (row) => row.status !== 'stopped'
+ // },
+ // {
+ // name: "鍋滄",
+ // type: "text",
+ // clickFun: (row) => {
+ // handleStop(row);
+ // },
+ // disabled: (row) => row.status === 'stopped'
+ // }
+ ]
+ }
+]);
+
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ getList();
+});
+
+// 鏌ヨ鏁版嵁
+const handleQuery = () => {
+ // page.value.current = 1;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ listRpa({...page.value, ...searchForm.value})
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+
+// 鍒嗛〉澶勭悊
+const pagination = (obj) => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ handleQuery();
+};
+
+// 閫夋嫨鍙樺寲澶勭悊
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑琛ㄥ崟
+const openForm = (type, row) => {
+ dialogType.value = type;
+ dialogVisible.value = true;
+
+ if (type === "add") {
+ dialogTitle.value = "娣诲姞RPA";
+ } else {
+ dialogTitle.value = "缂栬緫RPA";
+ form.value = { ...row };
+ }
+};
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = async () => {
+ if (!formRef.value) return;
+
+ try {
+ await formRef.value.validate();
+
+ if (dialogType.value === "add") {
+ // 娣诲姞鏂癛PA
+ addRpa({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ form.value = {
+ programName: "",
+ status: "",
+ description: ""
+ },
+ dialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } else {
+ // 缂栬緫RPA
+ updateRpa({...form.value}).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+};
+
+// 寮�濮婻PA
+const handleStart = (row) => {
+ ElMessageBox.confirm(`纭畾瑕佸惎鍔≧PA绋嬪簭"${row.programName}"鍚楋紵`, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ row.status = "running";
+ ElMessage.success("RPA鍚姩鎴愬姛");
+ getList();
+ }).catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+};
+
+// 鍋滄RPA
+const handleStop = (row) => {
+ ElMessageBox.confirm(`纭畾瑕佸仠姝PA绋嬪簭"${row.programName}"鍚楋紵`, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ row.status = "stopped";
+ ElMessage.success("RPA鍋滄鎴愬姛");
+ getList();
+ }).catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+};
+
+// 鍒犻櫎RPA
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delRpa(ids).then((res) => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎嚭鍔熻兘
+const { proxy } = getCurrentInstance()
+const handleExport = () => {
+ proxy.download('/rpaProcessAutomation/export', { ...searchForm.value }, 'RPA绠$悊.xlsx')
+}
+</script>
+
+<style scoped></style>
diff --git a/src/views/collaborativeApproval/rulesRegulationsManagement/index.vue b/src/views/collaborativeApproval/rulesRegulationsManagement/index.vue
new file mode 100644
index 0000000..f7ba9d9
--- /dev/null
+++ b/src/views/collaborativeApproval/rulesRegulationsManagement/index.vue
@@ -0,0 +1,629 @@
+<template>
+ <div class="app-container">
+ <!-- 瑙勭珷鍒跺害绠$悊-->
+ <el-card class="box-card">
+ <template #header>
+ <div class="card-header">
+ <span>瑙勭珷鍒跺害鍙戝竷</span>
+ </div>
+ </template>
+ <div class="tab-content">
+ <el-row :gutter="20"
+ class="mb-20">
+ <span class="ml-10">鍒跺害鏍囬锛�</span>
+ <el-col :span="6">
+ <el-input v-model="regulationSearchForm.title"
+ placeholder="璇疯緭鍏ュ埗搴︽爣棰�"
+ clearable />
+ </el-col>
+ <span class="search_title">鍒跺害鍒嗙被锛�</span>
+ <el-col :span="4">
+ <el-select v-model="regulationSearchForm.category"
+ placeholder="鍒跺害鍒嗙被"
+ clearable>
+ <el-option label="浜轰簨鍒跺害"
+ value="hr" />
+ <el-option label="璐㈠姟鍒跺害"
+ value="finance" />
+ <el-option label="瀹夊叏鍒跺害"
+ value="safety" />
+ <el-option label="鎶�鏈埗搴�"
+ value="tech" />
+ </el-select>
+ </el-col>
+ <el-col :span="8">
+ <el-button type="primary"
+ @click="searchRegulations">鎼滅储</el-button>
+ <el-button @click="resetRegulationSearch">閲嶇疆</el-button>
+ <el-button @click="handleExport">瀵煎嚭</el-button>
+ <el-button type="success"
+ @click="handleAdd">
+ 鍙戝竷鍒跺害
+ </el-button>
+ </el-col>
+ </el-row>
+ <PIMTable
+ rowKey="id"
+ :column="regulationTableColumn"
+ :tableData="regulations"
+ :tableLoading="tableLoading"
+ :page="page"
+ :isShowPagination="true"
+ @pagination="paginationChange"
+ />
+ </div>
+ </el-card>
+ <!-- 鐢ㄥ嵃鐢宠瀵硅瘽妗嗭紙宸茬Щ闄わ級 -->
+ <!-- 瑙勭珷鍒跺害鍙戝竷瀵硅瘽妗� -->
+ <el-dialog v-model="showRegulationDialog"
+ :title="operationType === 'add' ? '鍙戝竷鍒跺害' : '缂栬緫鍒跺害'"
+ width="800px">
+ <el-form :model="regulationForm"
+ :rules="regulationRules"
+ ref="regulationFormRef"
+ label-width="100px">
+ <el-form-item label="鍒跺害缂栧彿"
+ prop="regulationNum">
+ <el-input v-model="regulationForm.regulationNum"
+ placeholder="璇疯緭鍏ュ埗搴︾紪鍙�" />
+ </el-form-item>
+ <el-form-item label="鍒跺害鏍囬"
+ prop="title">
+ <el-input v-model="regulationForm.title"
+ placeholder="璇疯緭鍏ュ埗搴︽爣棰�" />
+ </el-form-item>
+ <el-form-item label="鍒跺害鍒嗙被"
+ prop="category">
+ <el-select v-model="regulationForm.category"
+ placeholder="璇烽�夋嫨鍒跺害鍒嗙被"
+ style="width: 100%">
+ <el-option label="浜轰簨鍒跺害"
+ value="hr" />
+ <el-option label="璐㈠姟鍒跺害"
+ value="finance" />
+ <el-option label="瀹夊叏鍒跺害"
+ value="safety" />
+ <el-option label="鎶�鏈埗搴�"
+ value="tech" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒跺害鍐呭"
+ prop="content">
+ <el-input v-model="regulationForm.content"
+ type="textarea"
+ :rows="10"
+ placeholder="璇疯緭鍏ュ埗搴﹁缁嗗唴瀹�" />
+ </el-form-item>
+ <el-form-item label="鍒跺害鐗堟湰"
+ prop="version">
+ <el-input v-model="regulationForm.version"
+ placeholder="璇疯緭鍏ュ埗搴︾増鏈�" />
+ </el-form-item>
+ <el-form-item label="鐢熸晥鏃堕棿"
+ prop="effectiveTime">
+ <el-date-picker v-model="regulationForm.effectiveTime"
+ type="datetime"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ placeholder="閫夋嫨鐢熸晥鏃堕棿"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="閫傜敤鑼冨洿"
+ prop="scope">
+ <el-checkbox-group v-model="regulationForm.scope">
+ <el-checkbox label="all">鍏ㄤ綋鍛樺伐</el-checkbox>
+ <el-checkbox label="manager">绠$悊灞�</el-checkbox>
+ <el-checkbox label="hr">浜轰簨閮ㄩ棬</el-checkbox>
+ <el-checkbox label="finance">璐㈠姟閮ㄩ棬</el-checkbox>
+ <el-checkbox label="tech">鎶�鏈儴闂�</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ <el-form-item label="鏄惁闇�瑕佺‘璁�"
+ prop="requireConfirm">
+ <el-switch v-model="regulationForm.requireConfirm" />
+ <span class="ml-10">寮�鍚悗鍛樺伐闇�瑕侀槄璇荤‘璁�</span>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="submitRegulation">鍙戝竷鍒跺害</el-button>
+ <el-button @click="showRegulationDialog = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鐢ㄥ嵃璇︽儏瀵硅瘽妗嗭紙宸茬Щ闄わ級 -->
+ <!-- 瑙勭珷鍒跺害璇︽儏瀵硅瘽妗� -->
+ <el-dialog v-model="showRegulationDetailDialog"
+ title="瑙勭珷鍒跺害璇︽儏"
+ width="800px">
+ <div v-if="currentRegulationDetail">
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="鍒跺害缂栧彿">{{ currentRegulationDetail.id }}</el-descriptions-item>
+ <el-descriptions-item label="鍒跺害鏍囬">{{ currentRegulationDetail.title }}</el-descriptions-item>
+ <el-descriptions-item label="鍒嗙被">{{ getCategoryText(currentRegulationDetail.category) }}</el-descriptions-item>
+ <el-descriptions-item label="鐗堟湰">{{ currentRegulationDetail.version }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戝竷浜�">{{ currentRegulationDetail.createUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戝竷鏃堕棿">{{ currentRegulationDetail.createTime }}</el-descriptions-item>
+ </el-descriptions>
+ <div class="mt-20">
+ <h4>鍒跺害鍐呭</h4>
+ <div class="regulation-content">{{ currentRegulationDetail.content }}</div>
+ </div>
+ <!-- 濡傛灉tableData>0 鏄剧ず -->
+ <div style="margin: 10px 0;"
+ v-if="tableData && tableData.length > 0">
+ <el-button type="success"
+ @click="resetForm(currentRegulationDetail)">纭鏌ョ湅</el-button>
+ </div>
+ </div>
+ </el-dialog>
+ <!-- 鐗堟湰鍘嗗彶瀵硅瘽妗� -->
+ <el-dialog v-model="showVersionHistoryDialog"
+ title="鐗堟湰鍘嗗彶"
+ width="800px">
+ <el-table :data="versionHistory"
+ style="width: 100%;margin-bottom: 10px">
+ <el-table-column prop="version"
+ label="鐗堟湰鍙�"
+ width="100" />
+ <el-table-column prop="updateTime"
+ label="鏇存柊鏃堕棿"
+ width="180" />
+ <el-table-column prop="createUserName"
+ label="鏇存柊浜�"
+ width="120" />
+ <el-table-column prop="changeLog"
+ label="鍙樻洿璇存槑">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
+ {{ scope.row.status === 'active' ? '鐢熸晥涓�' : '宸插簾姝�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <!-- 闃呰鐘舵�佸璇濇 -->
+ <el-dialog v-model="showReadStatusDialog"
+ title="闃呰鐘舵��"
+ width="800px">
+ <el-table :data="readStatusList"
+ style="width: 100%;margin-bottom: 10px">
+ <el-table-column prop="employee"
+ label="鍛樺伐濮撳悕"
+ width="120" />
+ <el-table-column prop="department"
+ label="鎵�灞為儴闂�"
+ width="150" />
+ <el-table-column prop="createTime"
+ label="闃呰鏃堕棿"
+ width="180" />
+ <el-table-column prop="confirmTime"
+ label="纭鏃堕棿"
+ width="180" />
+ <el-table-column prop="status"
+ label="鐘舵��"
+ width="100">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'confirmed' ? 'success' : 'warning'">
+ {{ scope.row.status === 'confirmed' ? '宸茬‘璁�' : '鏈‘璁�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <FileList v-if="fileDialogVisible" v-model:visible="fileDialogVisible" record-type="rules_regulations_management" :record-id="recordId" />
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import {
+ listRuleManagement,
+ addRuleManagement,
+ updateRuleManagement,
+ delRuleManagement,
+ getReadingStatusByRuleId,
+ addReadingStatus,
+ updateReadingStatus,
+ } from "@/api/collaborativeApproval/sealManagement.js";
+ const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+ import {
+ listRuleFiles,
+ delRuleFile,
+ addRuleFile,
+ } from "@/api/collaborativeApproval/rulesRegulationsManagementFile.js";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+
+ // 鍝嶅簲寮忔暟鎹�
+ const operationType = ref("add");
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ // 鍒嗛〉鍙傛暟
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ // 闄勪欢寮圭獥
+ const currentFileRuleId = ref(null);
+ // 瑙勭珷鍒跺害鐩稿叧
+ const showRegulationDialog = ref(false);
+ const showRegulationDetailDialog = ref(false);
+ const showVersionHistoryDialog = ref(false);
+ const showReadStatusDialog = ref(false);
+ const currentRegulationDetail = ref(null);
+ const regulationFormRef = ref();
+ const regulationForm = reactive({
+ id: "",
+ regulationNum: "",
+ title: "",
+ category: "",
+ content: "",
+ version: "",
+ status: "active",
+ readCount: 0,
+ effectiveTime: "",
+ scope: [],
+ requireConfirm: false,
+ });
+
+ const readStatus = ref({
+ id: "",
+ ruleId: "",
+ employee: "",
+ department: "",
+ createTime: "",
+ confirmTime: "",
+ status: "unconfirmed",
+ });
+
+ const regulationRules = {
+ title: [{ required: true, message: "璇疯緭鍏ュ埗搴︽爣棰�", trigger: "blur" }],
+ category: [{ required: true, message: "璇烽�夋嫨鍒跺害鍒嗙被", trigger: "change" }],
+ content: [{ required: true, message: "璇疯緭鍏ュ埗搴﹀唴瀹�", trigger: "blur" }],
+ effectiveTime: [
+ { required: true, message: "璇烽�夋嫨鐢熸晥鏃堕棿", trigger: "change" },
+ ],
+ scope: [{ required: true, message: "璇烽�夋嫨閫傜敤鑼冨洿", trigger: "change" }],
+ };
+
+ const regulationSearchForm = reactive({
+ title: "",
+ category: "",
+ });
+
+ const regulations = ref([]);
+
+ // 琛ㄦ牸鍒楅厤缃�
+ const regulationTableColumn = ref([
+ { label: "鍒跺害缂栧彿", prop: "regulationNum"},
+ { label: "鍒跺害鏍囬", prop: "title" },
+ {
+ label: "鍒嗙被",
+ prop: "category",
+ dataType: "tag",
+ formatData: (v) => getCategoryText(v),
+ formatType: () => "info",
+ },
+ { label: "鐗堟湰", prop: "version", width: 120 },
+ { label: "鍙戝竷浜�", prop: "createUserName", width: 120 },
+ { label: "鍙戝竷鏃堕棿", prop: "createTime", width: 180 },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ width: 100,
+ dataType: "tag",
+ formatData: (v) => (v === "active" ? "鐢熸晥涓�" : "宸插簾姝�"),
+ formatType: (v) => (v === "active" ? "success" : "info"),
+ },
+ { label: "宸茶浜烘暟", prop: "readCount", width: 100 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ width: 320,
+ fixed: "right",
+ align: "center",
+ operation: [
+ { name: "缂栬緫", clickFun: (row) => handleEdit(row) },
+ { name: "搴熷純", clickFun: (row) => repealEdit(row) },
+ { name: "鐗堟湰鍘嗗彶", clickFun: (row) => viewVersionHistory(row) },
+ { name: "璇︽儏", clickFun: (row) => viewRegulation(row) },
+ { name: "闄勪欢", clickFun: (row) => openFileDialog(row) },
+ ],
+ },
+ ]);
+
+ const versionHistory = ref([]);
+
+ const readStatusList = ref([]);
+ // { employee: '闄堝織寮�', department: '閿�鍞儴', readTime: '2025-01-11 10:30:00', confirmTime: '2025-01-11 10:35:00', status: 'confirmed' },
+ // { employee: '鍒橀泤濠�', department: '鎶�鏈儴', readTime: '2025-01-11 14:20:00', confirmTime: '', status: 'unconfirmed' },
+ // { employee: '鐜嬪缓鍥�', department: '璐㈠姟閮�', readTime: '2025-01-12 09:15:00', confirmTime: '2025-01-12 09:20:00', status: 'confirmed' }
+
+ // 鍒跺害鍒嗙被
+ const getCategoryText = category => {
+ const categoryMap = {
+ hr: "浜轰簨鍒跺害",
+ finance: "璐㈠姟鍒跺害",
+ safety: "瀹夊叏鍒跺害",
+ tech: "鎶�鏈埗搴�",
+ };
+ return categoryMap[category] || "鏈煡";
+ };
+ // 鎼滅储鍒跺害
+ const searchRegulations = () => {
+ page.current = 1;
+ getRegulationList();
+ };
+ // 閲嶇疆鍒跺害鎼滅储
+ const resetRegulationSearch = () => {
+ regulationSearchForm.title = "";
+ regulationSearchForm.category = "";
+ searchRegulations();
+ };
+ // 鏂板
+ const handleAdd = () => {
+ operationType.value = "add";
+ resetRegulationForm();
+ showRegulationDialog.value = true;
+ };
+
+ // 缂栬緫
+ const handleEdit = row => {
+ operationType.value = "edit";
+ Object.assign(regulationForm, row);
+ showRegulationDialog.value = true;
+ };
+ // 搴熷純
+ const repealEdit = row => {
+ operationType.value = "edit";
+ Object.assign(regulationForm, row);
+ regulationForm.status = "repealed";
+ ElMessageBox.confirm("纭搴熷純璇ュ埗搴︼紵", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ updateRuleManagement(regulationForm).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒跺害搴熷純鎴愬姛");
+ // showRegulationDialog.value = false
+ getRegulationList();
+ resetRegulationForm();
+ }
+ });
+ })
+ .catch(() => {
+ ElMessage({
+ type: "info",
+ message: "宸插彇娑堝簾寮�",
+ });
+ });
+ };
+ // 鍙戝竷鍒跺害
+ const submitRegulation = async () => {
+ try {
+ await regulationFormRef.value.validate();
+ if (operationType.value == "add") {
+ addRuleManagement(regulationForm).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒跺害鍙戝竷鎴愬姛");
+ showRegulationDialog.value = false;
+ getRegulationList();
+ resetRegulationForm();
+ }
+ });
+ } else {
+ updateRuleManagement(regulationForm).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒跺害缂栬緫鎴愬姛");
+ showRegulationDialog.value = false;
+ resetRegulationForm();
+ getRegulationList();
+ }
+ });
+ }
+ } catch (err) {
+ ElMessage.error(err.msg);
+ }
+ };
+ //閲嶇疆鍒跺害琛ㄥ崟
+ const resetRegulationForm = () => {
+ Object.assign(regulationForm, {
+ id: "",
+ regulationNum: "",
+ title: "",
+ category: "",
+ content: "",
+ version: "",
+ status: "active",
+ readCount: 0,
+ effectiveTime: "",
+ scope: [],
+ requireConfirm: false,
+ });
+ };
+
+ // 鏌ョ湅鍒跺害鐗堟湰鍘嗗彶
+ const viewVersionHistory = row => {
+ showVersionHistoryDialog.value = true;
+ const params = {
+ category: row.category,
+ };
+ listRuleManagement(page, params).then(res => {
+ if (res.code == 200) {
+ versionHistory.value = res.data.records;
+ }
+ });
+ };
+ // 鏌ョ湅鍒跺害璇︽儏
+ const viewRegulation = row => {
+ currentRegulationDetail.value = row;
+ showRegulationDetailDialog.value = true;
+ getReadingStatusByRuleId(row.id).then(res => {
+ if (res.code == 200) {
+ readStatusList.value = res.data;
+ if (readStatusList.value.length == 0 && tableData.value.length > 0) {
+ const params = {
+ ruleId: row.id,
+ employee: tableData.value[0].staffName,
+ department: tableData.value[0].postJob,
+ status: "unconfirmed",
+ };
+ addReadingStatus(params).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒跺害闃呰鎴愬姛");
+ }
+ });
+ }
+ }
+ });
+ };
+ // 鏌ョ湅鍒跺害闃呰鐘舵��
+ const viewReadStatus = row => {
+ showReadStatusDialog.value = true;
+ //鏌ョ湅闃呰鐘舵�佸垪琛�
+ getReadingStatusByRuleId(row.id).then(res => {
+ if (res.code == 200) {
+ readStatusList.value = res.data;
+ }
+ });
+ };
+
+ //纭鏌ョ湅
+ const resetForm = row => {
+ console.log("row", row);
+ row.readCount = row.readCount + 1;
+
+ updateRuleManagement(row).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鏌ョ湅鏁伴噺淇敼鎴愬姛");
+ //淇敼闃呰鐘舵��
+ //鏍规嵁鍒跺害id鍜屽綋鍓嶇櫥褰曠殑鍛樺伐寰楀埌闃呰鐘舵��
+ // let item = readStatusList.value.filter(item => item.employee == tableData.value[0].staffName )
+ // if(item.length>0){
+ // item[0].status = 'confirmed',
+ // item[0].confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
+ // }
+ // 绛涢�夊綋鍓嶅憳宸ュ搴旇鍒跺害鐨勯槄璇荤姸鎬佽褰�
+ let statusItem = readStatusList.value.find(
+ item =>
+ item.employee === tableData.value[0].staffName &&
+ item.ruleId === row.id
+ );
+
+ if (statusItem) {
+ // 濡傛灉鎵惧埌璁板綍锛屾洿鏂扮姸鎬佸拰纭鏃堕棿
+ statusItem.status = "confirmed";
+ // 鏍煎紡鍖栨椂闂翠负"YYYY-MM-DD HH:mm:ss"鏍煎紡
+ const now = new Date();
+ statusItem.confirmTime = `${now.getFullYear()}-${String(
+ now.getMonth() + 1
+ ).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")} ${String(
+ now.getHours()
+ ).padStart(2, "0")}:${String(now.getMinutes()).padStart(
+ 2,
+ "0"
+ )}:${String(now.getSeconds()).padStart(2, "0")}`;
+ // statusItem.confirmTime = new Date().toISOString().replace('T', ' ').split('.')[0];
+
+ updateReadingStatus(statusItem).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒跺害闃呰鐘舵�佷慨鏀规垚鍔�");
+ }
+ });
+ }
+ }
+ });
+ };
+
+ // 瀵煎嚭瑙勭珷鍒跺害
+ const { proxy } = getCurrentInstance();
+ const handleExport = () => {
+ proxy.download(
+ "/rulesRegulationsManagement/export",
+ { ...regulationSearchForm },
+ "瑙勭珷鍒跺害.xlsx"
+ );
+ };
+
+ // 鎵撳紑闄勪欢寮圭獥
+ const recordId =ref(0)
+ const fileDialogVisible = ref(false)
+
+ // 鎵撳紑闄勪欢寮规
+ const openFileDialog = async (row) => {
+ recordId.value = row.id
+ fileDialogVisible.value = true
+ }
+
+ // 鑾峰彇瑙勭珷鍒跺害鍒楄〃鏁版嵁
+ const getRegulationList = async () => {
+ tableLoading.value = true;
+ listRuleManagement(page, regulationSearchForm)
+ .then(res => {
+ regulations.value = res.data.records;
+ // 杩囨护鎺夊凡搴熷純鐨勫埗搴�
+ // regulations.value = res.data.records.filter(item => item.status !== 'repealed')
+ page.total = res.data.total;
+ tableLoading.value = false;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ // 鍒嗛〉鍙樺寲澶勭悊
+ const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getRegulationList();
+ };
+ onMounted(() => {
+ // 鍒濆鍖�
+ getRegulationList();
+ });
+</script>
+
+<style scoped>
+ .app-container {
+ padding: 20px;
+ }
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .tab-content {
+ padding: 20px 0;
+ }
+
+ .mb-20 {
+ margin-bottom: 20px;
+ }
+
+ .mt-20 {
+ margin-top: 20px;
+ }
+
+ .ml-10 {
+ margin-left: 10px;
+ }
+
+ .regulation-content {
+ background-color: #f5f5f5;
+ padding: 15px;
+ border-radius: 4px;
+ line-height: 1.6;
+ white-space: pre-wrap;
+ height: 200px;
+ }
+
+ .dialog-footer {
+ text-align: center;
+ }
+</style>
diff --git a/src/views/collaborativeApproval/sealManagement/index.vue b/src/views/collaborativeApproval/sealManagement/index.vue
new file mode 100644
index 0000000..9d68848
--- /dev/null
+++ b/src/views/collaborativeApproval/sealManagement/index.vue
@@ -0,0 +1,494 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <template #header>
+ <div class="card-header">
+ <span>鐢ㄥ嵃绠$悊鍙戝竷</span>
+ </div>
+ </template>
+
+
+ <!-- 鐢ㄥ嵃鐢宠绠$悊 -->
+ <div class="tab-content">
+ <el-row :gutter="20" class="mb-20 ">
+ <span class="ml-10">鐢ㄥ嵃鏍囬锛�</span>
+ <el-col :span="4">
+ <el-input v-model="sealSearchForm.title" placeholder="璇疯緭鍏ョ敵璇锋爣棰�" clearable />
+ </el-col>
+ <span class="ml-10">鐢ㄥ嵃缂栧彿锛�</span>
+ <el-col :span="4">
+ <el-input v-model="sealSearchForm.applicationNum" placeholder="璇疯緭鍏ョ敤鍗扮紪鍙�" clearable />
+ </el-col>
+ <span class="search_title">瀹℃壒鐘舵�侊細</span>
+ <el-col :span="4">
+ <el-select v-model="sealSearchForm.status" placeholder="瀹℃壒鐘舵��" clearable>
+ <el-option label="寰呭鎵�" value="pending" />
+ <el-option label="宸查�氳繃" value="approved" />
+ <el-option label="宸叉嫆缁�" value="rejected" />
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="searchSealApplications">鎼滅储</el-button>
+ <el-button @click="resetSealSearch">閲嶇疆</el-button>
+ <el-button @click="handleExport">瀵煎嚭</el-button>
+ <el-button type="primary" @click="showSealApplyDialog = true">鐢宠鐢ㄥ嵃
+ </el-button>
+ </el-col>
+ </el-row>
+
+ <PIMTable
+ rowKey="id"
+ :column="sealTableColumn"
+ :tableData="sealApplications"
+ :tableLoading="tableLoading"
+ :page="page"
+ :isShowPagination="true"
+ @pagination="paginationChange"
+ />
+ </div>
+ </el-card>
+
+ <!-- 鐢ㄥ嵃鐢宠瀵硅瘽妗� -->
+ <FormDialog
+ v-model="showSealApplyDialog"
+ title="鐢宠鐢ㄥ嵃"
+ :width="'600px'"
+ @close="closeSealApplyDialog"
+ @confirm="submitSealApplication"
+ @cancel="closeSealApplyDialog"
+ >
+ <el-form :model="sealForm" :rules="sealRules" ref="sealFormRef" label-width="100px">
+ <el-form-item label="鐢宠缂栧彿" prop="applicationNum">
+ <el-input v-model="sealForm.applicationNum" placeholder="璇疯緭鍏ョ敵璇风紪鍙�" />
+ </el-form-item>
+ <el-form-item label="鐢宠鏍囬" prop="title">
+ <el-input v-model="sealForm.title" placeholder="璇疯緭鍏ョ敵璇锋爣棰�" />
+ </el-form-item>
+ <el-form-item label="鐢ㄥ嵃绫诲瀷" prop="sealType">
+ <el-select v-model="sealForm.sealType" placeholder="璇烽�夋嫨鐢ㄥ嵃绫诲瀷" style="width: 100%">
+ <el-option label="鍏珷" value="official" />
+ <el-option label="鍚堝悓涓撶敤绔�" value="contract" />
+ <el-option label="璐㈠姟涓撶敤绔�" value="finance" />
+ <el-option label="娉曚汉绔�" value="legal" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鍘熷洜" prop="reason">
+ <el-input v-model="sealForm.reason" type="textarea" :rows="4" placeholder="璇疯缁嗚鏄庣敤鍗板師鍥�" />
+ </el-form-item>
+ <el-form-item label="瀹℃壒浜�" prop="approveUserId">
+ <el-select v-model="sealForm.approveUserId" placeholder="璇烽�夋嫨瀹℃壒浜�" style="width: 100%" filterable>
+ <el-option
+ v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="绱ф�ョ▼搴�" prop="urgency">
+ <el-radio-group v-model="sealForm.urgency">
+ <el-radio value="normal">鏅��</el-radio>
+ <el-radio value="urgent">绱ф��</el-radio>
+ <el-radio value="very-urgent">鐗规��</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="闄勪欢涓婁紶">
+ <AttachmentUploadFile
+ v-model:fileList="sealForm.storageBlobDTOs"
+ :limit="10"
+ :fileSize="50"
+ buttonText="鐐瑰嚮涓婁紶闄勪欢"
+ />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+
+ <!-- 鐢ㄥ嵃璇︽儏瀵硅瘽妗� -->
+ <FormDialog
+ v-model="showSealDetailDialog"
+ title="鐢ㄥ嵃鐢宠璇︽儏"
+ :width="'700px'"
+ @close="closeSealDetailDialog"
+ @confirm="closeSealDetailDialog"
+ @cancel="closeSealDetailDialog"
+ >
+ <div v-if="currentSealDetail" class="mb10">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鐢宠缂栧彿">{{ currentSealDetail.applicationNum }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏍囬">{{ currentSealDetail.title }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜�">{{ currentSealDetail.createUserName }}</el-descriptions-item>
+ <el-descriptions-item label="鎵�灞為儴闂�">{{ currentSealDetail.department }}</el-descriptions-item>
+ <el-descriptions-item label="鐢ㄥ嵃绫诲瀷">{{ getSealTypeText(currentSealDetail.sealType) }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏃堕棿">{{ currentSealDetail.createTime }}</el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">
+ <el-tag :type="getStatusType(currentSealDetail.status)">
+ {{ getStatusText(currentSealDetail.status) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢宠鍘熷洜" :span="2">{{ currentSealDetail.reason }}</el-descriptions-item>
+ </el-descriptions>
+ <!-- 闄勪欢鍒楄〃 -->
+ <div v-if="currentSealDetail.storageBlobVOList?.length || currentSealDetail.storageBlobDTOs?.length" class="attachment-section">
+ <div class="attachment-title">闄勪欢鍒楄〃锛�</div>
+ <el-table :data="currentSealDetail.storageBlobVOList || currentSealDetail.storageBlobDTOs" border class="attachment-table">
+ <el-table-column label="闄勪欢鍚嶇О" show-overflow-tooltip>
+ <template #default="scope">
+ {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '鏈懡鍚嶆枃浠�' }}
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="previewFile(scope.row)">棰勮</el-button>
+ <el-button link type="primary" size="small" @click="downloadFile(scope.row)">涓嬭浇</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+ </FormDialog>
+ <!-- 鏂囦欢棰勮缁勪欢 -->
+ <FilePreview ref="filePreviewRef" />
+
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { listSealApplication, addSealApplication, updateSealApplication } from '@/api/collaborativeApproval/sealManagement.js'
+import { userListNoPageByTenantId } from '@/api/system/user.js'
+import useUserStore from '@/store/modules/user'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue'
+import FilePreview from '@/components/filePreview/index.vue'
+import download from '@/plugins/download.js'
+
+// 鍝嶅簲寮忔暟鎹�
+// 鐢ㄥ嵃鐢宠鐩稿叧
+const userStore = useUserStore()
+const route = useRoute()
+const showSealApplyDialog = ref(false)
+const tableLoading = ref(false)
+const showSealDetailDialog = ref(false)
+const currentSealDetail = ref(null)
+const filePreviewRef = ref(null)
+const sealFormRef = ref()
+const userList = ref([])
+const sealForm = reactive({
+ applicationNum: '',
+ title: '',
+ sealType: '',
+ reason: '',
+ approveUserId: '',
+ urgency: 'normal',
+ status: 'pending',
+ storageBlobDTOs: []
+})
+
+const sealRules = {
+ applicationNum: [{ required: true, message: '璇疯緭鍏ョ敵璇风紪鍙�', trigger: 'blur' }],
+ title: [{ required: true, message: '璇疯緭鍏ョ敵璇锋爣棰�', trigger: 'blur' }],
+ sealType: [{ required: true, message: '璇烽�夋嫨鐢ㄥ嵃绫诲瀷', trigger: 'change' }],
+ reason: [{ required: true, message: '璇疯緭鍏ョ敵璇峰師鍥�', trigger: 'blur' }],
+ approveUserId: [{ required: true, message: '璇烽�夋嫨瀹℃壒浜�', trigger: 'change' }]
+}
+
+const sealSearchForm = reactive({
+ title: '',
+ status: '',
+ applicationNum: ''
+})
+// 鍒嗛〉鍙傛暟
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0
+})
+
+const sealApplications = ref([])
+
+// 鐢ㄥ嵃鐢宠鐘舵��
+const getStatusType = (status) => {
+ const statusMap = {
+ pending: 'warning',
+ approved: 'success',
+ rejected: 'danger'
+ }
+ return statusMap[status] || 'info'
+}
+// 鐢ㄥ嵃鐢宠鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ pending: '寰呭鎵�',
+ approved: '宸查�氳繃',
+ rejected: '宸叉嫆缁�'
+ }
+ return statusMap[status] || '鏈煡'
+}
+// 鐢ㄥ嵃绫诲瀷
+const getSealTypeText = (sealType) => {
+ const sealTypeMap = {
+ official: '鍏珷',
+ contract: '鍚堝悓涓撶敤绔�',
+ finance: '璐㈠姟涓撶敤绔�',
+ legal: '娉曚汉绔�',
+ tegal: '鎶�鏈笓鐢ㄧ珷'
+ }
+ return sealTypeMap[sealType] || '鏈煡'
+}
+
+// 鐢ㄥ嵃鐢宠琛ㄦ牸鍒楅厤缃紙闇�鍦� getStatusText/getSealTypeText 绛変箣鍚庡畾涔夛級
+const sealTableColumn = ref([
+ { label: '鐢宠缂栧彿', prop: 'applicationNum',},
+ { label: '鐢宠鏍囬', prop: 'title', showOverflowTooltip: true },
+ { label: '鐢宠浜�', prop: 'createUserName', },
+ { label: '鎵�灞為儴闂�', prop: 'department', width: 150 },
+ {
+ label: '鐢ㄥ嵃绫诲瀷',
+ prop: 'sealType',
+ dataType: 'tag',
+ formatData: (v) => getSealTypeText(v),
+ formatType: () => 'info'
+ },
+ { label: '鐢宠鏃堕棿', prop: 'createTime', width: 180 },
+ {
+ label: '鐘舵��',
+ prop: 'status',
+ width: 100,
+ dataType: 'tag',
+ formatData: (v) => getStatusText(v),
+ formatType: (v) => getStatusType(v)
+ },
+ {
+ dataType: 'action',
+ label: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ align: 'center',
+ operation: [
+ {
+ name: '瀹℃壒',
+ clickFun: (row) => approveSeal(row),
+ showHide: (row) => row.status === 'pending'
+ },
+ {
+ name: '鎷掔粷',
+ clickFun: (row) => rejectSeal(row),
+ showHide: (row) => row.status === 'pending'
+ },
+ { name: '璇︽儏', clickFun: (row) => viewSealDetail(row) }
+ ]
+ }
+])
+
+// 鎼滅储鍗扮珷鐢宠
+const searchSealApplications = () => {
+ page.current=1
+ getSealApplicationList()
+
+ // ElMessage.success('鎼滅储瀹屾垚')
+}
+// 閲嶇疆鍗扮珷鐢宠鎼滅储
+const resetSealSearch = () => {
+ sealSearchForm.title = ''
+ sealSearchForm.status = ''
+ sealSearchForm.applicationNum = ''
+ searchSealApplications()
+}
+// 鎻愪氦鐢ㄥ嵃鐢宠
+const submitSealApplication = async () => {
+ try {
+ await sealFormRef.value.validate()
+ addSealApplication(sealForm).then(res => {
+ if(res.code == 200){
+ ElMessage.success('鐢宠鎻愪氦鎴愬姛')
+ closeSealApplyDialog()
+ getSealApplicationList()
+ Object.assign(sealForm, {
+ applicationNum: '',
+ title: '',
+ sealType: '',
+ reason: '',
+ approveUserId: '',
+ urgency: 'normal',
+ status: 'pending',
+ storageBlobDTOs: []
+ })
+ }
+ }).catch(err => {
+ console.log(err.msg)
+ })
+
+ } catch (error) {
+ }
+}
+// 鍏抽棴鐢ㄥ嵃鐢宠瀵硅瘽妗�
+const closeSealApplyDialog = () => {
+ // 娓呯┖琛ㄥ崟鏁版嵁
+ Object.assign(sealForm, {
+ applicationNum: '',
+ title: '',
+ sealType: '',
+ reason: '',
+ approveUserId: '',
+ urgency: 'normal',
+ status: 'pending',
+ storageBlobDTOs: []
+ })
+ // 娓呴櫎琛ㄥ崟楠岃瘉鐘舵��
+ if (sealFormRef.value) {
+ sealFormRef.value.clearValidate()
+ }
+ showSealApplyDialog.value = false
+}
+// 鍏抽棴鐢ㄥ嵃璇︽儏瀵硅瘽妗�
+const closeSealDetailDialog = () => {
+ showSealDetailDialog.value = false
+}
+
+// 鏌ョ湅鐢ㄥ嵃鐢宠璇︽儏
+const viewSealDetail = (row) => {
+ currentSealDetail.value = row
+ showSealDetailDialog.value = true
+}
+
+// 棰勮鏂囦欢
+const previewFile = (row) => {
+ const url = row.previewURL || row.previewUrl || row.url
+ if (url && filePreviewRef.value) {
+ filePreviewRef.value.open(url)
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�')
+ }
+}
+
+// 涓嬭浇鏂囦欢
+const downloadFile = (row) => {
+ const url = row.downloadURL || row.downloadUrl || row.url
+ if (url) {
+ const filename = row.originalFilename || row.name || row.fileName || 'download'
+ download.byUrl(url, filename)
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曚笅杞�')
+ }
+}
+// 瀹℃壒鐢ㄥ嵃鐢宠
+const approveSeal = (row) => {
+ ElMessageBox.confirm('纭閫氳繃璇ョ敤鍗扮敵璇凤紵', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ row.status = 'approved'
+ updateSealApplication(row).then(res => {
+ if(res.code == 200){
+ ElMessage.success('瀹℃壒閫氳繃')
+ getSealApplicationList()
+ }
+ })
+ })
+}
+// 鎷掔粷鐢ㄥ嵃鐢宠
+const rejectSeal = (row) => {
+ ElMessageBox.prompt('璇疯緭鍏ユ嫆缁濆師鍥�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ inputPattern: /\S+/,
+ inputErrorMessage: '鎷掔粷鍘熷洜涓嶈兘涓虹┖'
+ }).then(({ value }) => {
+ row.status = 'rejected'
+ updateSealApplication(row).then(res => {
+ if(res.code == 200){
+ ElMessage.success('宸叉嫆缁濈敵璇�')
+ getSealApplicationList()
+ }
+ })
+ })
+}
+
+// 瀵煎嚭鐢ㄥ嵃鐢宠
+const { proxy } = getCurrentInstance()
+const handleExport = () => {
+ proxy.download('/sealApplicationManagement/export', { ...sealSearchForm }, '鐢ㄥ嵃鐢宠.xlsx')
+}
+
+// 鑾峰彇鍗扮珷鐢宠鍒楄〃鏁版嵁
+const getSealApplicationList = async () => {
+ tableLoading.value = true
+ listSealApplication(page, sealSearchForm)
+ .then(res => {
+ sealApplications.value = res.data.records
+ page.total = res.data.total
+ tableLoading.value = false
+ }).catch(err => {
+ tableLoading.value = false
+ })
+}
+// 鍒嗛〉鍙樺寲澶勭悊
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getSealApplicationList();
+};
+
+// 鐩戝惉瀵硅瘽妗嗘墦寮�锛岃幏鍙栫敤鎴峰垪琛�
+watch(showSealApplyDialog, (newVal) => {
+ if (newVal) {
+ userListNoPageByTenantId().then((res) => {
+ userList.value = res.data;
+ });
+ }
+});
+
+onMounted(() => {
+ // 璺敱鎼哄甫 applicationNum 鏃讹紝棰勫~骞舵煡璇�
+ if (route.query.applicationNum) {
+ sealSearchForm.applicationNum = String(route.query.applicationNum)
+ page.current = 1
+ getSealApplicationList()
+ } else {
+ getSealApplicationList()
+ }
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.tab-content {
+ padding: 20px 0;
+}
+
+.mb-20 {
+ margin-bottom: 20px;
+}
+
+.ml-10 {
+ margin-left: 10px;
+}
+
+.attachment-section {
+ margin-top: 20px;
+}
+
+.attachment-title {
+ font-size: 14px;
+ color: #606266;
+ margin-bottom: 10px;
+ font-weight: 500;
+}
+
+.attachment-table {
+ border-radius: 4px;
+}
+</style>
diff --git a/src/views/collaborativeApproval/shipmentReview/fileList.vue b/src/views/collaborativeApproval/shipmentReview/fileList.vue
new file mode 100644
index 0000000..555312a
--- /dev/null
+++ b/src/views/collaborativeApproval/shipmentReview/fileList.vue
@@ -0,0 +1,42 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="闄勪欢" width="40%" :before-close="handleClose">
+ <el-table :data="tableData" border height="40vh">
+ <el-table-column label="闄勪欢鍚嶇О" prop="name" min-width="400" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" width="100" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">涓嬭浇</el-button>
+ <el-button link type="primary" size="small" @click="lookFile(scope.row)">棰勮</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import filePreview from '@/components/filePreview/index.vue'
+
+const dialogVisible = ref(false)
+const tableData = ref([])
+const { proxy } = getCurrentInstance();
+const filePreviewRef = ref()
+const handleClose = () => {
+ dialogVisible.value = false
+}
+const open = (list) => {
+ dialogVisible.value = true
+ tableData.value = list
+}
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+defineExpose({
+ open
+})
+</script>
+
+<style></style>
\ No newline at end of file
diff --git a/src/views/collaborativeApproval/shipmentReview/index.vue b/src/views/collaborativeApproval/shipmentReview/index.vue
new file mode 100644
index 0000000..ac2b790
--- /dev/null
+++ b/src/views/collaborativeApproval/shipmentReview/index.vue
@@ -0,0 +1,340 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">閿�鍞悎鍚屽彿锛�</span>
+ <el-input
+ v-model="searchForm.salesContractNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ラ攢鍞悎鍚屽彿鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <span class="search_title ml10">瀹℃壒鐘舵�侊細</span>
+ <el-select v-model="searchForm.approveStatus" clearable @change="handleQuery" style="width: 240px">
+ <el-option label="寰呭鏍�" :value="2" />
+ <el-option label="瀹℃牳鎴愬姛" :value="3" />
+ <el-option label="瀹℃牳澶辫触" :value="4" />
+ </el-select>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+<!-- <el-button type="primary" @click="openForm('add')">鏂板</el-button>-->
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+<!-- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>-->
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="approveType"></info-form-dia>
+ <approval-dia ref="approvalDia" @close="handleQuery"></approval-dia>
+ <FileList ref="fileListRef" />
+ </div>
+</template>
+
+<script setup>
+import FileList from "./fileList.vue";
+import { Search } from "@element-plus/icons-vue";
+import {onMounted, ref} from "vue";
+import {ElMessageBox} from "element-plus";
+import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
+import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
+import {getShipmentApprovalList, approveShipment} from "@/api/collaborativeApproval/shipmentReview.js";
+// import {approveProcessDelete, approveProcessListPage} from "@/api/collaborativeApproval/approvalProcess.js";
+import useUserStore from "@/store/modules/user";
+import { userListNoPage } from "@/api/system/user.js";
+
+// 瀹氫箟缁勪欢鎺ユ敹鐨刾rops
+const props = defineProps({
+ approveType: {
+ type: [Number, String],
+ default: 6
+ }
+});
+
+const userList = ref([]);
+
+const userStore = useUserStore();
+
+
+const data = reactive({
+ searchForm: {
+ approveId: "",
+ approveStatus: "",
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "瀹℃壒鐘舵��",
+ prop: "approveStatus",
+ dataType: "tag",
+ width: 100,
+ formatData: (params) => {
+ if (params === 2) {
+ return "寰呭鏍�";
+ } else if (params === 3) {
+ return "瀹℃牳瀹屾垚";
+ } else if (params === 4) {
+ return "瀹℃牳椹冲洖";
+ } else {
+ return '鏈煡鐘舵��';
+ }
+ },
+ formatType: (params) => {
+ if (params === 0) {
+ return "warning";
+ } else if (params === 2) {
+ return "info";
+ } else if (params === 3) {
+ return "success";
+ } else if (params === 4) {
+ return "danger";
+ } else {
+ return 'danger';
+ }
+ },
+ },
+ {
+ label: "閿�鍞悎鍚屽彿",
+ prop: "salesContractNo",
+ width: 170
+ },
+ {
+ label: "瀹㈡埛鍚嶇О",
+ prop: "customerName",
+ width: 200
+ },
+ {
+ label: "浜у搧澶х被",
+ prop: "productCategory",
+ width: 200
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "specificationModel",
+ width: 220
+ },
+ {
+ label: "鐢宠浜�",
+ prop: "approveUserId",
+ width: 120,
+ align: "center",
+ formatData:(params)=>{
+ const user = userList.value.find(item => item.userId === params)
+ return user ? user.nickName : '--'
+ }
+ },
+ {
+ label: "杞︾墝鍙�",
+ prop: "shippingCarNumber",
+ width: 120,
+ },
+ {
+ label: "鐢宠浜�",
+ prop: "approveUserId",
+ width: 120,
+ },
+ {
+ label: "鐢宠鏃ユ湡",
+ prop: "executionDate",
+ width: 200
+ },
+ {
+ label: "褰撳墠瀹℃壒浜�",
+ prop: "salesman",
+ width: 120
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 120,
+ operation: [
+ {
+ name: "閫氳繃",
+ type: "text",
+ clickFun: (row) => {
+ handleApproval("閫氳繃", row);
+ },
+ disabled: (row) => row.approveStatus !== 2
+ },
+ {
+ name: "椹冲洖",
+ type: "text",
+ clickFun: (row) => {
+ handleApproval("椹冲洖", row);
+ },
+ disabled: (row) => row.approveStatus !== 2
+ },
+ // {
+ // name: "缂栬緫",
+ // type: "text",
+ // clickFun: (row) => {
+ // openForm("edit", row);
+ // },
+ // disabled: (row) => row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4
+ // },
+ // {
+ // name: "瀹℃牳",
+ // type: "text",
+ // clickFun: (row) => {
+ // openApprovalDia("approval", row);
+ // },
+ // disabled: (row) => row.approveUserCurrentId == null || row.approveStatus == 2 || row.approveStatus == 3 || row.approveStatus == 4 || row.approveUserCurrentId !== userStore.id
+ // },
+ // {
+ // name: "璇︽儏",
+ // type: "text",
+ // clickFun: (row) => {
+ // openApprovalDia('view', row);
+ // },
+ // },
+ // {
+ // name: "闄勪欢",
+ // type: "text",
+ // clickFun: (row) => {
+ // downLoadFile(row);
+ // },
+ // },
+ ],
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0
+});
+const infoFormDia = ref()
+const approvalDia = ref()
+const { proxy } = getCurrentInstance()
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const fileListRef = ref(null)
+const downLoadFile = (row) => {
+ fileListRef.value.open(row.commonFileList)
+
+}
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList =async () => {
+ let userLists = await userListNoPage();
+ userList.value = userLists.data;
+ tableLoading.value = true;
+ getShipmentApprovalList({...page, ...searchForm.value,approveType:props.approveType}).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 瀵煎嚭
+const handleOut = () => {
+ const type = Number(props.approveType || 6)
+ const urlMap = {
+ 0: "/shipmentApproval/export",
+ }
+ const url = urlMap[type] || urlMap[0]
+ const nameMap = {
+ 0: "鍙戣揣瀹℃牳琛�",
+ }
+ const fileName = nameMap[type] || nameMap[0]
+ proxy.download(url, {}, `${fileName}.xlsx`)
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑鏂板銆佺紪杈戝脊妗�
+const openForm = (type, row) => {
+ nextTick(() => {
+ infoFormDia.value?.openDialog(type, row)
+ })
+};
+// 鎵撳紑鏂板妫�楠屽脊妗�
+const openApprovalDia = (type, row) => {
+ nextTick(() => {
+ approvalDia.value?.openDialog(type, row)
+ })
+};
+
+// 瀹℃牳閫氳繃/椹冲洖
+const handleApproval = (name = "瀹℃牳",row) => {
+ ElMessageBox.confirm(`閫変腑鐨勫唴瀹瑰皢琚�${name}锛屾槸鍚︾‘璁�${name}锛焋, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async()=>{
+ let res = await approveShipment({
+ id: row.id,
+ approveStatus: name === "閫氳繃" ? 3 : 4
+ });
+ if(res.code === 200){
+ proxy.$modal.msgSuccess(`${name}鎴愬姛`);
+ }else{
+ proxy.$modal.msgError(`${name}澶辫触`);
+ }
+ await getList()
+ }).catch(err=>{
+ proxy.$modal.msgError(`鏈煡閿欒,璇疯仈绯荤鐞嗗憳`);
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.approveId);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ approveProcessDelete(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/collaborativeApproval/warningSystem/index.vue b/src/views/collaborativeApproval/warningSystem/index.vue
new file mode 100644
index 0000000..ddab6c0
--- /dev/null
+++ b/src/views/collaborativeApproval/warningSystem/index.vue
@@ -0,0 +1,305 @@
+<template>
+ <div class="warning-system">
+ <h2>棰勮鑱斿姩鏈哄埗</h2>
+
+ <!-- 缁熻鍗$墖 -->
+ <div class="stats">
+ <div class="stat-card red">
+ <span class="number">2</span>
+ <span class="label">绾㈣壊棰勮</span>
+ </div>
+ <div class="stat-card orange">
+ <span class="number">1</span>
+ <span class="label">姗欒壊棰勮</span>
+ </div>
+ <div class="stat-card yellow">
+ <span class="number">1</span>
+ <span class="label">榛勮壊棰勮</span>
+ </div>
+ <div class="stat-card green">
+ <span class="number">1</span>
+ <span class="label">缁胯壊棰勮</span>
+ </div>
+ </div>
+
+ <!-- 棰勮鍒楄〃 -->
+ <div class="warning-list">
+ <h3>棰勮鍒楄〃</h3>
+ <table>
+ <thead>
+ <tr>
+ <th>缂栧彿</th>
+ <th>鏍囬</th>
+ <th>绫诲瀷</th>
+ <th>绛夌骇</th>
+ <th>鐘舵��</th>
+ <th>鎿嶄綔</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="warning in warnings" :key="warning.id">
+ <td>{{ warning.id }}</td>
+ <td>{{ warning.title }}</td>
+ <td>{{ warning.type }}</td>
+ <td>
+ <span :class="['level-tag', warning.level]">
+ {{ warning.levelText }}
+ </span>
+ </td>
+ <td>
+ <span :class="['status-tag', warning.status]">
+ {{ warning.statusText }}
+ </span>
+ </td>
+ <td>
+ <button @click="viewDetail(warning)">鏌ョ湅璇︽儏</button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+
+ <!-- 璇︽儏瀵硅瘽妗� -->
+ <div v-if="showDetail" class="modal">
+ <div class="modal-content">
+ <h3>棰勮璇︽儏</h3>
+ <div v-if="currentWarning">
+ <p><strong>缂栧彿锛�</strong>{{ currentWarning.id }}</p>
+ <p><strong>鏍囬锛�</strong>{{ currentWarning.title }}</p>
+ <p><strong>绫诲瀷锛�</strong>{{ currentWarning.type }}</p>
+ <p><strong>绛夌骇锛�</strong>{{ currentWarning.levelText }}</p>
+ <p><strong>鎻忚堪锛�</strong>{{ currentWarning.description }}</p>
+ <p><strong>褰卞搷锛�</strong>{{ currentWarning.impact }}</p>
+ <p><strong>寤鸿锛�</strong>{{ currentWarning.suggestions }}</p>
+ </div>
+ <button @click="showDetail = false">鍏抽棴</button>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ name: 'WarningSystem',
+ data() {
+ return {
+ showDetail: false,
+ currentWarning: null,
+ warnings: [
+ {
+ id: 'W001',
+ title: '椤圭洰棰勭畻瓒呮敮棰勮',
+ type: '璐㈠姟棰勮',
+ level: 'red',
+ levelText: '绾㈣壊棰勮',
+ status: 'pending',
+ statusText: '寰呭鐞�',
+ responsible: '闄堝織寮�',
+ description: 'A椤圭洰棰勭畻鎵ц鐜囧凡杈�95%锛岄璁″皢瓒呭嚭棰勭畻鑼冨洿銆�',
+ impact: '褰卞搷椤圭洰鏁翠綋璐㈠姟鎸囨爣锛屽彲鑳藉鑷撮」鐩簭鎹�',
+ suggestions: '鏆傚仠闈炲繀瑕佹敮鍑猴紝浼樺寲璧勬簮閰嶇疆锛岀敵璇烽绠楄皟鏁�'
+ },
+ {
+ id: 'W002',
+ title: '鍚堝悓鍒版湡棰勮',
+ type: '鍚堣棰勮',
+ level: 'orange',
+ levelText: '姗欒壊棰勮',
+ status: 'processing',
+ statusText: '澶勭悊涓�',
+ responsible: '鏉庝富绠�',
+ description: '涓庝緵搴斿晢B鐨勫悎鍚屽皢浜�2024骞�1鏈�25鏃ュ埌鏈熴��',
+ impact: '褰卞搷渚涘簲閾剧ǔ瀹氭�э紝鍙兘瀵艰嚧鏈嶅姟涓柇',
+ suggestions: '璇勪及渚涘簲鍟嗚〃鐜帮紝鍑嗗缁鏉愭枡锛屽埗瀹氬閫夋柟妗�'
+ },
+ {
+ id: 'W003',
+ title: '璁惧缁存姢棰勮',
+ type: '杩愯惀棰勮',
+ level: 'yellow',
+ levelText: '榛勮壊棰勮',
+ status: 'pending',
+ statusText: '寰呭鐞�',
+ responsible: '鐜嬪伐绋嬪笀',
+ description: '鐢熶骇绾胯澶嘋宸茶繍琛�8000灏忔椂锛屾帴杩戠淮鎶ゅ懆鏈熴��',
+ impact: '鍙兘褰卞搷鐢熶骇鏁堢巼鍜屼骇鍝佽川閲�',
+ suggestions: '瀹夋帓缁存姢鏃堕棿锛屽噯澶囧浠讹紝鍒跺畾缁存姢璁″垝'
+ },
+ {
+ id: 'W004',
+ title: '浜哄憳閰嶇疆棰勮',
+ type: '杩愯惀棰勮',
+ level: 'green',
+ levelText: '缁胯壊棰勮',
+ status: 'resolved',
+ statusText: '宸茶В鍐�',
+ responsible: '璧礖R',
+ description: '鎶�鏈儴闂ㄤ汉鍛橀厤缃厖瓒筹紝椤圭洰杩涘害姝e父銆�',
+ impact: '鏃犺礋闈㈠奖鍝�',
+ suggestions: '缁х画鐩戞帶浜哄憳閰嶇疆鎯呭喌'
+ },
+ {
+ id: 'W005',
+ title: '璐ㄩ噺浜嬫晠棰勮',
+ type: '杩愯惀棰勮',
+ level: 'red',
+ levelText: '绾㈣壊棰勮',
+ status: 'pending',
+ statusText: '寰呭鐞�',
+ responsible: '闄堝織寮�',
+ description: '浜у搧D鍦ㄥ鎴风幇鍦哄嚭鐜拌川閲忛棶棰樸��',
+ impact: '褰卞搷瀹㈡埛婊℃剰搴︼紝鍙兘閫犳垚缁忔祹鎹熷け',
+ suggestions: '绔嬪嵆鍙洖闂浜у搧锛屽垎鏋愬師鍥狅紝鍒跺畾鏀硅繘鎺柦'
+ }
+ ]
+ }
+ },
+ methods: {
+ viewDetail(warning) {
+ this.currentWarning = warning
+ this.showDetail = true
+ }
+ }
+}
+</script>
+
+<style scoped>
+.warning-system {
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+h2 {
+ color: #333;
+ margin-bottom: 30px;
+}
+
+.stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+.stat-card {
+ padding: 20px;
+ border-radius: 8px;
+ color: white;
+ text-align: center;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+.stat-card.red { background: linear-gradient(135deg, #ff6b6b, #ee5a52); }
+.stat-card.orange { background: linear-gradient(135deg, #ffa726, #ff9800); }
+.stat-card.yellow { background: linear-gradient(135deg, #ffd54f, #ffc107); }
+.stat-card.green { background: linear-gradient(135deg, #66bb6a, #4caf50); }
+
+.stat-card .number {
+ display: block;
+ font-size: 32px;
+ font-weight: bold;
+ margin-bottom: 8px;
+}
+
+.stat-card .label {
+ font-size: 14px;
+ opacity: 0.9;
+}
+
+.warning-list h3 {
+ margin-bottom: 20px;
+ color: #333;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+}
+
+th, td {
+ padding: 12px;
+ text-align: left;
+ border-bottom: 1px solid #eee;
+}
+
+th {
+ background: #f8f9fa;
+ font-weight: 600;
+ color: #333;
+}
+
+.level-tag, .status-tag {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ color: white;
+}
+
+.level-tag.red { background: #f56c6c; }
+.level-tag.orange { background: #e6a23c; }
+.level-tag.yellow { background: #e6a23c; }
+.level-tag.green { background: #67c23a; }
+
+.status-tag.pending { background: #f56c6c; }
+.status-tag.processing { background: #e6a23c; }
+.status-tag.resolved { background: #67c23a; }
+
+button {
+ padding: 6px 12px;
+ margin: 0 4px;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ background: #409eff;
+ color: white;
+}
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0,0,0,0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.modal-content {
+ background: white;
+ padding: 30px;
+ border-radius: 8px;
+ max-width: 600px;
+ width: 90%;
+ max-height: 80vh;
+ overflow-y: auto;
+}
+
+.modal-content h3 {
+ margin-bottom: 20px;
+ color: #333;
+}
+
+.modal-content p {
+ margin-bottom: 15px;
+ line-height: 1.6;
+}
+
+.modal-content strong {
+ color: #333;
+}
+
+.modal-content button {
+ background: #409eff;
+ color: white;
+ padding: 10px 20px;
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/customerService/expiryAfterSales/components/formDia.vue b/src/views/customerService/expiryAfterSales/components/formDia.vue
new file mode 100644
index 0000000..d899b8d
--- /dev/null
+++ b/src/views/customerService/expiryAfterSales/components/formDia.vue
@@ -0,0 +1,319 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="dialogTitle"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="涓存湡浜у搧鍚嶇О锛�" prop="productName">
+ <el-input
+ v-model="form.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ :disabled="isFieldDisabled('productName')"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧鎵瑰彿锛�" prop="batchNumber">
+ <el-input
+ v-model="form.batchNumber"
+ placeholder="璇疯緭鍏ヤ骇鍝佹壒鍙�"
+ clearable
+ :disabled="isFieldDisabled('batchNumber')"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="涓存湡鏃ユ湡锛�" prop="expiryDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.expiryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨涓存湡鏃ユ湡"
+ clearable
+ :disabled="isFieldDisabled('expiryDate')"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="搴撳瓨鏁伴噺锛�" prop="stockQuantity">
+ <el-input-number
+ v-model="form.stockQuantity"
+ :min="0"
+ placeholder="璇疯緭鍏ュ簱瀛樻暟閲�"
+ style="width: 100%"
+ :disabled="isFieldDisabled('stockQuantity')"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerName">
+ <el-input
+ v-model="form.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ :disabled="isFieldDisabled('customerName')"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽锛�" prop="contactPhone">
+ <el-input
+ v-model="form.contactPhone"
+ placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+ clearable
+ :disabled="isFieldDisabled('contactPhone')"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闂鎻忚堪锛�" prop="problemDesc">
+ <el-input
+ v-model="form.problemDesc"
+ placeholder="璇疯緭鍏ラ棶棰樻弿杩�"
+ clearable
+ :disabled="isFieldDisabled('problemDesc')"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30" v-if="operationType !== 'add'">
+ <el-col :span="12">
+ <el-form-item label="澶勭悊浜猴細" prop="handlerId">
+ <el-select
+ v-model="form.handlerId"
+ placeholder="璇烽�夋嫨澶勭悊浜�"
+ clearable
+ :disabled="isFieldDisabled('handlerId')"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊鏃ユ湡锛�" prop="handleDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.handleDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨澶勭悊鏃ユ湡"
+ clearable
+ :disabled="isFieldDisabled('handleDate')"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30" v-if="operationType !== 'add'">
+ <el-col :span="24">
+ <el-form-item label="澶勭悊缁撴灉锛�" prop="handleResult">
+ <el-input
+ v-model="form.handleResult"
+ placeholder="璇疯緭鍏ュ鐞嗙粨鏋�"
+ clearable
+ :disabled="isFieldDisabled('handleResult')"
+ type="textarea"
+ :rows="3"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm" v-if="operationType !== 'view'">纭</el-button>
+ <el-button @click="closeDia">{{ operationType === 'view' ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, computed} from "vue";
+import useUserStore from "@/store/modules/user.js";
+import { getCurrentDate } from "@/utils/index.js";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+import {expiryAfterSalesAdd, expiryAfterSalesUpdate} from "@/api/customerService/index.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const userStore = useUserStore();
+
+const dialogTitle = computed(() => {
+ switch (operationType.value) {
+ case 'add':
+ return '鏂板涓存湡鍞悗';
+ case 'edit':
+ return '缂栬緫涓存湡鍞悗';
+ case 'handle':
+ return '澶勭悊涓存湡鍞悗';
+ case 'view':
+ return '鏌ョ湅涓存湡鍞悗';
+ default:
+ return '涓存湡鍞悗绠$悊';
+ }
+});
+
+const data = reactive({
+ form: {
+ id: "",
+ productName: "",
+ batchNumber: "",
+ expiryDate: "",
+ stockQuantity: 0,
+ customerName: "",
+ contactPhone: "",
+ problemDesc: "",
+ handlerId: "",
+ handleDate: "",
+ handleResult: "",
+ status: 1
+ },
+ rules: {
+ productName: [{required: true, message: "璇疯緭鍏ヤ骇鍝佸悕绉�", trigger: "blur"}],
+ batchNumber: [{required: true, message: "璇疯緭鍏ヤ骇鍝佹壒鍙�", trigger: "blur"}],
+ expiryDate: [{required: true, message: "璇烽�夋嫨涓存湡鏃ユ湡", trigger: "change"}],
+ stockQuantity: [{required: true, message: "璇疯緭鍏ュ簱瀛樻暟閲�", trigger: "blur"}],
+ customerName: [{required: true, message: "璇疯緭鍏ュ鎴峰悕绉�", trigger: "blur"}],
+ contactPhone: [
+ {required: true, message: "璇疯緭鍏ヨ仈绯荤數璇�", trigger: "blur"},
+ {pattern: /^1[3-9]\d{9}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur"}
+ ],
+ problemDesc: [{required: true, message: "璇疯緭鍏ラ棶棰樻弿杩�", trigger: "blur"}],
+ }
+})
+const { form, rules } = toRefs(data);
+const userList = ref([])
+const handleEditableFields = ["handlerId", "handleDate", "handleResult"];
+
+const isFieldDisabled = (field) => {
+ if (operationType.value === "view") return true;
+ if (operationType.value === "handle") return !handleEditableFields.includes(field);
+ return false;
+};
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ userListNoPageByTenantId().then(res => {
+ userList.value = res.data;
+ });
+
+ if (type === 'add') {
+ // 鏂板鏃堕噸缃〃鍗�
+ form.value = {
+ id: "",
+ productName: "",
+ batchNumber: "",
+ expiryDate: "",
+ stockQuantity: 0,
+ customerName: "",
+ contactPhone: "",
+ problemDesc: "",
+ handlerId: "",
+ handleDate: "",
+ handleResult: "",
+ status: 1
+ };
+ } else {
+ // 缂栬緫鎴栨煡鐪嬫椂濉厖鏁版嵁
+ form.value = { ...row };
+ if (type === 'handle' && !form.value.handlerId) {
+ form.value.handlerId = userStore.id;
+ form.value.handleDate = getCurrentDate();
+ }
+ }
+}
+
+const submitForm = () => {
+ if (operationType.value === "handle") {
+ if (!form.value.handlerId || !form.value.handleDate || !form.value.handleResult) {
+ proxy.$modal.msgWarning("璇峰~鍐欏鐞嗕汉銆佸鐞嗘棩鏈熷拰澶勭悊缁撴灉");
+ return;
+ }
+ handleSubmit();
+ return;
+ }
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ handleSubmit();
+ }
+ });
+}
+
+const handleSubmit = () => {
+ const submitData = {
+ id: form.value.id,
+ productName: form.value.productName,
+ batchNumber: form.value.batchNumber,
+ expireDate: form.value.expiryDate,
+ stockQuantity: form.value.stockQuantity,
+ customerName: form.value.customerName,
+ contactPhone: form.value.contactPhone,
+ disRes: form.value.problemDesc,
+ status: operationType.value === "handle" ? 2 : form.value.status,
+ disposeUserId: form.value.handlerId,
+ disposeNickName: userList.value.find(item => item.userId === form.value.handlerId)?.nickName,
+ disposeResult: form.value.handleResult,
+ disDate: form.value.handleDate
+ };
+
+ const apiCall = operationType.value === 'add' ? expiryAfterSalesAdd : expiryAfterSalesUpdate;
+ apiCall(submitData).then(() => {
+ const successText = operationType.value === "add" ? "鏂板鎴愬姛" : operationType.value === "handle" ? "澶勭悊鎴愬姛" : "鏇存柊鎴愬姛";
+ proxy.$modal.msgSuccess(successText);
+ closeDia();
+ }).catch(error => {
+ console.error('鎻愪氦鏁版嵁澶辫触:', error);
+ proxy.$modal.msgError('鎻愪氦鏁版嵁澶辫触锛岃绋嶅悗閲嶈瘯');
+ });
+}
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/customerService/expiryAfterSales/index.vue b/src/views/customerService/expiryAfterSales/index.vue
new file mode 100644
index 0000000..9966785
--- /dev/null
+++ b/src/views/customerService/expiryAfterSales/index.vue
@@ -0,0 +1,273 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">涓存湡鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.expiryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="handleQuery"
+ />
+ <span class="search_title ml10">澶勭悊鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.handleDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="handleQuery"
+ />
+ <span style = "margin-left: 10px;" class="search_title">澶勭悊鐘舵�侊細</span>
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" @change="handleQuery" style="width: 140px" clearable>
+ <el-option label="寰呭鐞�" :value="1"></el-option>
+ <el-option label="宸插鐞�" :value="2"></el-option>
+ </el-select>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ <el-button @click="resetQuery" style="margin-left: 10px"
+ >閲嶇疆</el-button
+ >
+ </div>
+ <div class="table_actions">
+ <el-button type="primary" @click="openForm('add')">鏂板</el-button>
+ <el-button type="danger" @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ >
+ <!-- 琛ㄦ牸鎻掓Ы -->
+ <template #status="{ row }">
+ <el-tag :type="row.status === 1 ? 'warning' : 'success'">
+ {{ row.status === 1 ? '寰呭鐞�' : '宸插鐞�' }}
+ </el-tag>
+ </template>
+
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="openForm('view', row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="openForm('handle', row)" v-if="row.status === 1">澶勭悊</el-button>
+ </template>
+ </PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ </div>
+</template>
+
+<script setup>
+import {Search} from "@element-plus/icons-vue";
+import {onMounted, ref} from "vue";
+import FormDia from "@/views/customerService/expiryAfterSales/components/formDia.vue";
+import {ElMessageBox} from "element-plus";
+import {expiryAfterSalesDelete, expiryAfterSalesListPage} from "@/api/customerService/index.js";
+import useUserStore from "@/store/modules/user.js";
+const { proxy } = getCurrentInstance();
+const userStore = useUserStore()
+
+const data = reactive({
+ searchForm: {
+ expiryDate: "",
+ handleDate: "",
+ status: ""
+ },
+ tableData: [],
+ page: {
+ current: 1,
+ size: 10,
+ total: 0,
+ },
+ selectedRows: [],
+ tableLoading: false,
+ formDia: null,
+ tableColumn: [
+ {
+ label: "涓存湡浜у搧鍚嶇О",
+ prop: "productName",
+ width: "",
+ },
+ {
+ label: "浜у搧鎵瑰彿",
+ prop: "batchNumber",
+ width: "",
+ },
+ {
+ label: "涓存湡鏃ユ湡",
+ prop: "expiryDate",
+ width: "",
+ },
+ {
+ label: "搴撳瓨鏁伴噺",
+ prop: "stockQuantity",
+ width: "",
+ },
+ {
+ label: "瀹㈡埛鍚嶇О",
+ prop: "customerName",
+ width: "",
+ },
+ {
+ label: "闂鎻忚堪",
+ prop: "problemDesc",
+ width: "",
+ },
+ {
+ label: "澶勭悊鐘舵��",
+ prop: "status",
+ width: "",
+ dataType: "slot",
+ slot: "status",
+ },
+ {
+ label: "澶勭悊浜�",
+ prop: "handlerName",
+ width: "",
+ },
+ {
+ label: "澶勭悊鏃ユ湡",
+ prop: "handleDate",
+ width: "",
+ },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "slot",
+ slot: "operation",
+ width: "200",
+ },
+ ],
+});
+
+const {
+ searchForm,
+ tableData,
+ page,
+ selectedRows,
+ tableLoading,
+ formDia,
+ tableColumn,
+} = toRefs(data);
+
+// 鏌ヨ
+const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+};
+
+// 閫夋嫨
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 閲嶇疆
+const resetQuery = () => {
+ proxy.resetForm("queryRef");
+ searchForm.value = {
+ expiryDate: "",
+ handleDate: "",
+ status: ""
+ };
+ handleQuery();
+};
+
+// 鍒嗛〉
+const pagination = (obj) => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ getList();
+};
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = () => {
+ tableLoading.value = true;
+ // 鏋勯�犳煡璇㈠弬鏁帮紝鏄犲皠鍓嶇瀛楁鍒板悗绔瓧娈�
+ const queryParams = {
+ expireDate: searchForm.value.expiryDate,
+ disDate: searchForm.value.handleDate,
+ status: searchForm.value.status,
+ current: page.value.current,
+ size: page.value.size
+ };
+
+ expiryAfterSalesListPage(queryParams).then(res => {
+ // 鏄犲皠鍚庣杩斿洖鏁版嵁鍒板墠绔〃鏍�
+ tableData.value = res.data.records.map(item => ({
+ id: item.id,
+ productName: item.productName,
+ batchNumber: item.batchNumber,
+ expiryDate: item.expireDate,
+ stockQuantity: item.stockQuantity,
+ customerName: item.customerName,
+ contactPhone: item.contactPhone,
+ problemDesc: item.disRes,
+ status: item.status,
+ handlerId: item.disposeUserId,
+ handlerName: item.disposeNickName,
+ handleResult: item.disposeResult,
+ handleDate: item.disDate
+ }));
+ page.value.total = res.data.total;
+ tableLoading.value = false;
+ }).catch(error => {
+ console.error('鑾峰彇鍒楄〃鏁版嵁澶辫触:', error);
+ tableLoading.value = false;
+ proxy.$modal.msgError('鑾峰彇鏁版嵁澶辫触锛岃绋嶅悗閲嶈瘯');
+ });
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ expiryAfterSalesDelete(ids).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ }).finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue b/src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue
new file mode 100644
index 0000000..37e21ef
--- /dev/null
+++ b/src/views/customerService/feedbackRegistration/components/ProductSelectDialog.vue
@@ -0,0 +1,275 @@
+<template>
+ <el-dialog v-model="visible" title="閫夋嫨浜у搧" width="900px" destroy-on-close :close-on-click-modal="false">
+ <el-form :inline="true" :model="query" class="mb-2">
+ <el-form-item label="浜у搧鍒嗙被">
+ <el-input v-model="query.productCategory" placeholder="杈撳叆浜у搧鍒嗙被" clearable @keyup.enter="onSearch" />
+ </el-form-item>
+
+ <el-form-item label="鍩烘湰鍗曚綅">
+ <el-input v-model="query.unit" placeholder="杈撳叆鍩烘湰鍗曚綅" clearable @keyup.enter="onSearch" />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="columnsDialogVisible = true">鍒楄〃瀛楁</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鍒楄〃 -->
+ <el-table ref="tableRef" v-loading="loading" :data="tableData" height="420" highlight-current-row :row-key="getRowKey"
+ @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" />
+ <el-table-column type="index" label="搴忓彿" width="60" />
+ <template v-for="column in visibleColumns" :key="column.prop">
+ <el-table-column :prop="column.prop" :label="column.label" :min-width="column.minWidth" show-overflow-tooltip align="center" />
+ </template>
+ </el-table>
+
+ <div class="mt-3" style="margin-top: 10px;display: flex; justify-content: flex-end;">
+
+
+ <el-pagination background layout="total, sizes, prev, pager, next, jumper" :total="total"
+ v-model:page-size="page.pageSize" v-model:current-page="page.pageNum" :page-sizes="[10, 20, 50, 100]"
+ @size-change="onPageChange" @current-change="onPageChange" />
+ </div>
+
+ <template #footer>
+ <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm">
+ 纭畾
+ </el-button>
+ <el-button @click="close()">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+
+ <el-dialog v-model="columnsDialogVisible" title="鑷畾涔夋樉绀哄垪椤�" width="600px">
+ <el-checkbox-group v-model="selectedColumns">
+ <el-checkbox v-for="column in allColumns" :key="column.prop" :label="column.prop" :disabled="column.disabled">
+ {{ column.label }}
+ </el-checkbox>
+ </el-checkbox-group>
+ <template #footer>
+ <el-button @click="resetColumns">鎭㈠榛樿</el-button>
+ <el-button type="primary" @click="saveColumns">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed, onMounted, reactive, ref, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+
+const props = defineProps({
+ modelValue: Boolean,
+ single: Boolean, // 鏄惁鍙兘閫夋嫨涓�涓紝榛樿false锛堝彲閫夋嫨澶氫釜锛�
+ products: {
+ type: Array,
+ default: () => []
+ },
+ selectedIds: {
+ type: Array,
+ default: () => []
+ }
+});
+
+const emit = defineEmits(['update:modelValue', 'confirm']);
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: (v) => emit("update:modelValue", v),
+});
+
+const query = reactive({
+ productCategory: "",
+ unit: "",
+});
+
+const page = reactive({
+ pageNum: 1,
+ pageSize: 10,
+});
+
+const loading = ref(false);
+const tableData = ref([]);
+const total = ref(0);
+const multipleSelection = ref([]);
+const selectedRowMap = ref(new Map());
+const tableRef = ref();
+
+const columnsDialogVisible = ref(false);
+
+const allColumns = ref([
+ { prop: 'productCategory', label: '浜у搧鍒嗙被', selected: true, disabled: false },
+ { prop: 'unit', label: '鍩烘湰鍗曚綅', selected: true, disabled: false },
+]);
+
+const selectedColumns = ref(allColumns.value.filter(c => c.selected).map(c => c.prop));
+
+const visibleColumns = computed(() => {
+ return allColumns.value.filter(c => selectedColumns.value.includes(c.prop));
+});
+
+const getRowKey = (row) => {
+ return row?.id ?? row?.productModelId ?? `${row?.productCategory || ""}-${row?.specificationModel || row?.model || ""}-${row?.unit || ""}`;
+};
+
+const syncMultipleSelection = () => {
+ multipleSelection.value = Array.from(selectedRowMap.value.values());
+};
+
+const initSelectionFromProps = () => {
+ const selectedIdSet = new Set((props.selectedIds || []).map((id) => String(id)));
+ selectedRowMap.value = new Map();
+ if (!selectedIdSet.size) {
+ syncMultipleSelection();
+ return;
+ }
+ (props.products || []).forEach((row) => {
+ if (selectedIdSet.has(String(row?.id))) {
+ selectedRowMap.value.set(getRowKey(row), row);
+ }
+ });
+ syncMultipleSelection();
+};
+
+const resetColumns = () => {
+ selectedColumns.value = allColumns.value.filter(c => c.selected).map(c => c.prop);
+};
+
+const saveColumns = () => {
+ if (selectedColumns.value.length < 1) {
+ ElMessage.warning("鍒楄〃椤规樉绀轰笉寰楀皯浜�1椤�");
+ return;
+ }
+ columnsDialogVisible.value = false;
+};
+
+function close() {
+ visible.value = false;
+}
+
+const handleSelectionChange = (val) => {
+ const currentPageKeys = new Set(tableData.value.map((item) => getRowKey(item)));
+ currentPageKeys.forEach((key) => selectedRowMap.value.delete(key));
+
+ if (props.single && val.length > 1) {
+ const lastSelected = val[val.length - 1];
+ selectedRowMap.value = new Map();
+ if (lastSelected) {
+ selectedRowMap.value.set(getRowKey(lastSelected), lastSelected);
+ }
+ syncMultipleSelection();
+ nextTick(() => {
+ if (tableRef.value) {
+ tableRef.value.clearSelection();
+ tableRef.value.toggleRowSelection(lastSelected, true);
+ }
+ });
+ } else if (props.single) {
+ selectedRowMap.value = new Map();
+ if (val[0]) {
+ selectedRowMap.value.set(getRowKey(val[0]), val[0]);
+ }
+ syncMultipleSelection();
+ } else {
+ val.forEach((row) => {
+ selectedRowMap.value.set(getRowKey(row), row);
+ });
+ syncMultipleSelection();
+ }
+}
+
+function onSearch() {
+ page.pageNum = 1;
+ loadData();
+}
+
+function onReset() {
+ query.productCategory = "";
+ query.unit = "";
+ page.pageNum = 1;
+ loadData();
+}
+
+function onPageChange() {
+ loadData();
+}
+
+function onConfirm() {
+ if (multipleSelection.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�鏉′骇鍝�");
+ return;
+ }
+ if (props.single && multipleSelection.value.length > 1) {
+ ElMessage.warning("鍙兘閫夋嫨涓�涓骇鍝�");
+ return;
+ }
+ emit("confirm", props.single ? [multipleSelection.value[0]] : multipleSelection.value);
+ close();
+}
+
+async function loadData() {
+ loading.value = true;
+ try {
+ let filtered = props.products || [];
+ if (query.productCategory) {
+ filtered = filtered.filter(item => item.productCategory && item.productCategory.includes(query.productCategory));
+ }
+ if (query.unit) {
+ filtered = filtered.filter(item => item.unit && item.unit.includes(query.unit));
+ }
+
+ total.value = filtered.length;
+ const start = (page.pageNum - 1) * page.pageSize;
+ const end = start + page.pageSize;
+ tableData.value = filtered.slice(start, end);
+
+ nextTick(() => {
+ if (tableRef.value) {
+ tableRef.value.clearSelection();
+ tableData.value.forEach(row => {
+ if (selectedRowMap.value.has(getRowKey(row))) {
+ tableRef.value.toggleRowSelection(row, true);
+ }
+ });
+ }
+ syncMultipleSelection();
+ });
+ } finally {
+ loading.value = false;
+ }
+}
+
+// 鐩戝惉寮圭獥鎵撳紑锛岄噸缃�夋嫨
+watch(() => props.modelValue, (visible) => {
+ if (visible) {
+ initSelectionFromProps();
+ page.pageNum = 1;
+ loadData();
+ }
+});
+
+watch(() => props.products, () => {
+ const latestMap = new Map();
+ const currentKeys = new Set(selectedRowMap.value.keys());
+ (props.products || []).forEach((row) => {
+ const key = getRowKey(row);
+ if (currentKeys.has(key)) {
+ latestMap.set(key, row);
+ }
+ });
+ selectedRowMap.value.forEach((row, key) => {
+ if (!latestMap.has(key)) {
+ latestMap.set(key, row);
+ }
+ });
+ selectedRowMap.value = latestMap;
+ syncMultipleSelection();
+ if (props.modelValue) {
+ loadData();
+ }
+}, { deep: true });
+
+onMounted(() => {
+ loadData()
+})
+</script>
diff --git a/src/views/demo/fakePage/index.vue b/src/views/demo/fakePage/index.vue
new file mode 100644
index 0000000..42cef72
--- /dev/null
+++ b/src/views/demo/fakePage/index.vue
@@ -0,0 +1,248 @@
+<template>
+ <div class="app-container">
+ <el-card shadow="never">
+ <div class="toolbar">
+ <el-input
+ v-model="query.keyword"
+ placeholder="鎼滅储鍚嶇О/绫诲埆"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleSearch"
+ />
+ <el-select
+ v-model="query.status"
+ placeholder="鐘舵��"
+ clearable
+ style="width: 140px; margin-left: 12px"
+ >
+ <el-option label="鍚敤" value="鍚敤" />
+ <el-option label="鍋滅敤" value="鍋滅敤" />
+ </el-select>
+ <el-button type="primary" style="margin-left: 12px" @click="handleSearch">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ <el-button type="success" plain style="float: right" @click="openCreate">鏂板</el-button>
+ </div>
+
+ <el-table :data="pagedList" border style="width: 100%" height="480">
+ <el-table-column prop="id" label="缂栧彿" width="90" sortable />
+ <el-table-column prop="name" label="鍚嶇О" min-width="140" />
+ <el-table-column prop="category" label="绫诲埆" width="120" />
+ <el-table-column prop="stock" label="搴撳瓨" width="100" sortable />
+ <el-table-column prop="price" label="鍗曚环(楼)" width="120">
+ <template #default="scope">{{ formatPrice(scope.row.price) }}</template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" width="120">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === '鍚敤' ? 'success' : 'info'">{{ scope.row.status }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="updatedAt" label="鏇存柊鏃堕棿" min-width="160" />
+ <el-table-column label="鎿嶄綔" width="180" fixed="right">
+ <template #default="scope">
+ <el-button link type="primary" @click="openEdit(scope.row)">缂栬緫</el-button>
+ <el-button link type="danger" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="pagination">
+ <el-pagination
+ background
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="filteredList.length"
+ :page-sizes="[5, 10, 20, 50]"
+ :page-size="pager.pageSize"
+ :current-page="pager.pageNum"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </el-card>
+
+ <el-dialog v-model="dialogVisible" :title="isEdit ? '缂栬緫' : '鏂板'" width="520px">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="90px">
+ <el-form-item label="鍚嶇О" prop="name">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ュ悕绉�" />
+ </el-form-item>
+ <el-form-item label="绫诲埆" prop="category">
+ <el-select v-model="form.category" placeholder="璇烽�夋嫨绫诲埆" style="width: 100%">
+ <el-option label="鍘熸枡" value="鍘熸枡" />
+ <el-option label="鍗婃垚鍝�" value="鍗婃垚鍝�" />
+ <el-option label="鎴愬搧" value="鎴愬搧" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="搴撳瓨" prop="stock">
+ <el-input v-model.number="form.stock" type="number" min="0" />
+ </el-form-item>
+ <el-form-item label="鍗曚环(楼)" prop="price">
+ <el-input v-model.number="form.price" type="number" min="0" step="0.01" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio label="鍚敤">鍚敤</el-radio>
+ <el-radio label="鍋滅敤">鍋滅敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+
+</template>
+
+<script setup>
+import { ref, reactive, computed, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+defineOptions({ name: 'FakePage' })
+
+const query = reactive({
+ keyword: '',
+ status: ''
+})
+
+const pager = reactive({
+ pageNum: 1,
+ pageSize: 10
+})
+
+const allList = ref(generateMockData())
+
+const filteredList = computed(() => {
+ const keyword = (query.keyword || '').trim()
+ const status = query.status
+ return allList.value.filter(item => {
+ const hitKeyword = !keyword || item.name.includes(keyword) || item.category.includes(keyword)
+ const hitStatus = !status || item.status === status
+ return hitKeyword && hitStatus
+ })
+})
+
+const pagedList = computed(() => {
+ const start = (pager.pageNum - 1) * pager.pageSize
+ const end = start + pager.pageSize
+ return filteredList.value.slice(start, end)
+})
+
+function handleSearch() {
+ pager.pageNum = 1
+}
+
+function resetQuery() {
+ query.keyword = ''
+ query.status = ''
+ pager.pageNum = 1
+}
+
+function handleSizeChange(size) {
+ pager.pageSize = size
+ pager.pageNum = 1
+}
+
+function handleCurrentChange(page) {
+ pager.pageNum = page
+}
+
+function formatPrice(val) {
+ return Number(val || 0).toFixed(2)
+}
+
+// 鏂板/缂栬緫
+const dialogVisible = ref(false)
+const isEdit = ref(false)
+const formRef = ref()
+const form = reactive({ id: null, name: '', category: '', stock: 0, price: 0, status: '鍚敤' })
+
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }],
+ category: [{ required: true, message: '璇烽�夋嫨绫诲埆', trigger: 'change' }],
+ stock: [{ required: true, message: '璇疯緭鍏ュ簱瀛�', trigger: 'blur' }],
+ price: [{ required: true, message: '璇疯緭鍏ュ崟浠�', trigger: 'blur' }]
+}
+
+function openCreate() {
+ isEdit.value = false
+ Object.assign(form, { id: null, name: '', category: '', stock: 0, price: 0, status: '鍚敤' })
+ dialogVisible.value = true
+ nextTick(() => formRef.value?.clearValidate?.())
+}
+
+function openEdit(row) {
+ isEdit.value = true
+ Object.assign(form, JSON.parse(JSON.stringify(row)))
+ dialogVisible.value = true
+ nextTick(() => formRef.value?.clearValidate?.())
+}
+
+function submitForm() {
+ formRef.value?.validate?.((valid) => {
+ if (!valid) return
+ if (isEdit.value) {
+ const index = allList.value.findIndex(x => x.id === form.id)
+ if (index > -1) {
+ allList.value[index] = { ...form, updatedAt: nowString() }
+ ElMessage.success('宸蹭繚瀛�')
+ }
+ } else {
+ const newId = Date.now()
+ allList.value.unshift({ ...form, id: newId, updatedAt: nowString() })
+ ElMessage.success('宸叉柊澧�')
+ }
+ dialogVisible.value = false
+ })
+}
+
+function handleDelete(row) {
+ ElMessageBox.confirm(`纭鍒犻櫎銆�${row.name}銆戝悧锛焋, '鎻愮ず', { type: 'warning' })
+ .then(() => {
+ allList.value = allList.value.filter(x => x.id !== row.id)
+ ElMessage.success('宸插垹闄�')
+ })
+ .catch(() => {})
+}
+
+function generateMockData() {
+ const categories = ['鍘熸枡', '鍗婃垚鍝�', '鎴愬搧']
+ const statusOptions = ['鍚敤', '鍋滅敤']
+ const list = []
+ for (let i = 1; i <= 36; i++) {
+ list.push({
+ id: i,
+ name: `鐗╂枡-${i.toString().padStart(3, '0')}`,
+ category: categories[i % categories.length],
+ stock: Math.floor(Math.random() * 1000),
+ price: (Math.random() * 500 + 10).toFixed(2),
+ status: statusOptions[i % 2],
+ updatedAt: nowString()
+ })
+ }
+ return list
+}
+
+function nowString() {
+ const d = new Date()
+ const yyyy = d.getFullYear()
+ const MM = String(d.getMonth() + 1).padStart(2, '0')
+ const dd = String(d.getDate()).padStart(2, '0')
+ const hh = String(d.getHours()).padStart(2, '0')
+ const mm = String(d.getMinutes()).padStart(2, '0')
+ const ss = String(d.getSeconds()).padStart(2, '0')
+ return `${yyyy}-${MM}-${dd} ${hh}:${mm}:${ss}`
+}
+</script>
+
+<style scoped>
+.toolbar {
+ margin-bottom: 12px;
+}
+.pagination {
+ margin-top: 12px;
+ text-align: right;
+}
+</style>
+
+
+
diff --git a/src/views/energyManagement/carbonManagement/index.vue b/src/views/energyManagement/carbonManagement/index.vue
new file mode 100644
index 0000000..5a4be25
--- /dev/null
+++ b/src/views/energyManagement/carbonManagement/index.vue
@@ -0,0 +1,1553 @@
+<template>
+ <div class="carbon-management">
+ <!-- 椤甸潰澶撮儴 -->
+ <div class="page-header">
+ <div class="header-content">
+ <h1 class="page-title">纰虫帓鏀剧鐞嗙郴缁�</h1>
+ <p class="page-subtitle">鍩轰簬ISO 14064鏍囧噯 路 GHG Protocol鏍哥畻鏍囧噯</p>
+ </div>
+ <div class="header-stats">
+ <div class="stat-item">
+ <span class="stat-label">鎬荤⒊鎺掓斁閲�</span>
+ <span class="stat-value">{{totalEmissions}} tCO鈧俥</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">鏈湀鍑忔帓</span>
+ <span class="stat-value reduction">-{{monthlyReduction}}%</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">纰充腑鍜岃繘搴�</span>
+ <span class="stat-value">{{neutralProgress}}%</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- 涓昏鍐呭鍖哄煙 -->
+ <div class="dashboard-content">
+ <!-- 椤堕儴鏁版嵁闈㈡澘 -->
+ <div class="top-panels">
+ <div class="data-panel top-left">
+ <div class="panel-title">褰撳墠纰虫帓鏀�</div>
+ <div class="panel-value">{{carbonData.scope1}} <span class="unit">tCO鈧俥</span></div>
+ <div class="panel-subtitle">鑼冨洿1鐩存帴鎺掓斁</div>
+ </div>
+ <div class="data-panel top-center">
+ <div class="panel-title">鑳借�楃洃娴�</div>
+ <div class="panel-value">{{carbonData.scope2}} <span class="unit">tCO鈧俥</span></div>
+ <div class="panel-subtitle">鑼冨洿2闂存帴鎺掓斁</div>
+ </div>
+ <div class="data-panel top-right">
+ <div class="panel-title">渚涘簲閾炬帓鏀�</div>
+ <div class="panel-value">{{carbonData.scope3}} <span class="unit">tCO鈧俥</span></div>
+ <div class="panel-subtitle">鑼冨洿3渚涘簲閾炬帓鏀�</div>
+ </div>
+ <div class="data-panel top-far-right">
+ <div class="panel-title">鍑忔帓杩涘害</div>
+ <div class="panel-value">{{neutralProgress}} <span class="unit">%</span></div>
+ <div class="panel-subtitle">纰充腑鍜岀洰鏍�</div>
+ </div>
+ </div>
+
+ <!-- 涓績涓昏鍥惧尯鍩� -->
+ <div class="center-main-view">
+ <!-- 宸︿晶鎺у埗闈㈡澘 -->
+ <div class="left-control-panel">
+ <div class="control-section">
+ <div class="section-title">纰虫帓鏀捐寖鍥�</div>
+ <el-radio-group v-model="selectedScope" @change="updateScopeData" class="vertical-radio">
+ <el-radio-button :value="'all'">鍏ㄩ儴鑼冨洿</el-radio-button>
+ <el-radio-button :value="'scope1'">鑼冨洿1</el-radio-button>
+ <el-radio-button :value="'scope2'">鑼冨洿2</el-radio-button>
+ <el-radio-button :value="'scope3'">鑼冨洿3</el-radio-button>
+ </el-radio-group>
+ </div>
+ <div class="control-section">
+ <div class="section-title">鐩戞祴灞傜骇</div>
+ <el-radio-group v-model="heatmapLevel" @change="updateHeatmapLevel" class="vertical-radio">
+ <el-radio-button :value="'device'">璁惧绾�</el-radio-button>
+ <el-radio-button :value="'line'">浜х嚎绾�</el-radio-button>
+ <el-radio-button :value="'enterprise'">浼佷笟绾�</el-radio-button>
+ </el-radio-group>
+ </div>
+ </div>
+
+ <!-- 涓績鐑姏鍥� -->
+ <div class="main-heatmap">
+ <div class="heatmap-header">
+ <h2 class="main-title">纰宠冻杩圭儹鍔涘浘鍒嗘瀽</h2>
+ <div class="date-selector">
+ <el-date-picker
+ v-model="selectedDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ size="small"
+ @change="updateHeatmapData"
+ />
+ </div>
+ </div>
+ <div class="heatmap-view">
+ <Echarts ref="heatmapChart"
+ :series="heatmapSeries"
+ :xAxis="heatmapXAxis"
+ :yAxis="heatmapYAxis"
+ :tooltip="heatmapTooltip"
+ :visualMap="heatmapVisualMap"
+ :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
+ style="height: 450px"></Echarts>
+ </div>
+ </div>
+
+ <!-- 鍙充晶鏁版嵁闈㈡澘 -->
+ <div class="right-data-panel">
+ <div class="data-section">
+ <div class="section-title">瀹炴椂鐩戞帶</div>
+ <div class="mini-chart">
+ <Echarts ref="realtimeChart"
+ :series="realtimeSeries"
+ :xAxis="realtimeXAxis"
+ :chartStyle="chartStyle"
+ :yAxis="realtimeYAxis"
+ :tooltip="realtimeTooltip"
+ :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
+ style="height: 300px"></Echarts>
+ </div>
+ </div>
+ <div class="data-section">
+ <div class="section-title">瓒嬪娍鍒嗘瀽</div>
+ <div class="trend-controls">
+ <el-radio-group v-model="trendPeriod" size="small" @change="updateTrendData">
+ <el-radio-button :value="'week'">鍛�</el-radio-button>
+ <el-radio-button :value="'month'">鏈�</el-radio-button>
+ <el-radio-button :value="'year'">骞�</el-radio-button>
+ </el-radio-group>
+ </div>
+ <div class="mini-chart">
+ <Echarts ref="trendChart"
+ :series="trendSeries"
+ :xAxis="trendXAxis"
+ :yAxis="trendYAxis"
+ :tooltip="trendTooltip"
+ :chartStyle="chartStyle"
+ :legend="trendLegend"
+ :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
+ style="height: 200px"></Echarts>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 搴曢儴杩涘害闈㈡澘 -->
+ <div class="bottom-progress-panel">
+ <div class="progress-section">
+ <div class="progress-title">2024骞村噺鎺掔洰鏍�</div>
+ <div class="progress-data">
+ <span class="current">{{reductionTarget.current}}</span>
+ <span class="separator">/</span>
+ <span class="target">{{reductionTarget.target}} tCO鈧俥</span>
+ </div>
+ <el-progress :percentage="reductionTarget.percentage" :stroke-width="6" color="#00E676"/>
+ </div>
+ <div class="progress-section">
+ <div class="progress-title">纰充腑鍜岃繘搴�</div>
+ <div class="progress-data">
+ <span class="current">{{neutralTarget.current}}</span>
+ <span class="separator">/</span>
+ <span class="target">{{neutralTarget.target}} tCO鈧俥</span>
+ </div>
+ <el-progress :percentage="neutralTarget.percentage" :stroke-width="6" color="#00D4FF"/>
+ </div>
+ </div>
+
+ <!-- 搴曢儴鏁版嵁琛ㄦ牸 -->
+ <div class="bottom-data-table">
+ <div class="table-panel">
+ <div class="table-header">
+ <h3 class="table-title">纰虫帓鏀捐缁嗘暟鎹�</h3>
+ <div class="table-controls">
+ <el-input
+ v-model="searchKeyword"
+ placeholder="鎼滅储璁惧鎴栦骇绾�"
+ size="small"
+ style="width: 200px; margin-right: 10px;"
+ />
+ <el-button type="primary" size="small" @click="exportData">瀵煎嚭鏁版嵁</el-button>
+ </div>
+ </div>
+ <el-table :data="filteredTableData" style="width: 100%" height="180">
+ <el-table-column prop="name" label="璁惧/浜х嚎" width="150"/>
+ <el-table-column prop="type" label="绫诲瀷" width="100"/>
+ <el-table-column prop="scope1" label="鑼冨洿1鎺掓斁" width="120"/>
+ <el-table-column prop="scope2" label="鑼冨洿2鎺掓斁" width="120"/>
+ <el-table-column prop="scope3" label="鑼冨洿3鎺掓斁" width="120"/>
+ <el-table-column prop="total" label="鎬绘帓鏀鹃噺" width="120"/>
+ <el-table-column prop="efficiency" label="纰虫晥鐜�" width="100"/>
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">{{scope.row.status}}</el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
+import * as echarts from 'echarts'
+import Echarts from '@/components/Echarts/echarts.vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const selectedScope = ref('all')
+const heatmapLevel = ref('device')
+const selectedDate = ref(new Date())
+const trendPeriod = ref('week')
+const searchKeyword = ref('')
+
+// 纰虫帓鏀炬暟鎹�
+const carbonData = ref({
+ scope1: 125.6,
+ scope2: 89.3,
+ scope3: 234.7
+})
+const chartStyle = {
+ width: '96%',
+ height: '110%' // 璁剧疆鍥捐〃瀹瑰櫒鐨勯珮搴�
+}
+// 璁$畻灞炴��
+const totalEmissions = computed(() => {
+ return (carbonData.value.scope1 + carbonData.value.scope2 + carbonData.value.scope3).toFixed(1)
+})
+
+const monthlyReduction = ref(8.5)
+
+// 璁$畻纰充腑鍜岃繘搴︾櫨鍒嗘瘮
+const neutralProgress = computed(() => {
+ return Math.round(neutralTarget.value.percentage)
+})
+
+// 鍑忔帓鐩爣鏁版嵁
+const reductionTarget = ref({
+ current: 320.5,
+ target: 500,
+ percentage: 64.1
+})
+
+const neutralTarget = ref({
+ current: 1250,
+ target: 3800,
+ percentage: 32.9
+})
+
+// 瀹炴椂鐩戞帶鍥捐〃閰嶇疆
+const realtimeSeries = ref([
+ {
+ name: '瀹炴椂纰虫帓鏀�',
+ type: 'line',
+ smooth: true,
+ data: generateRealtimeData(),
+ itemStyle: {
+ color: '#FF6B6B'
+ },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(255, 107, 107, 0.3)' },
+ { offset: 1, color: 'rgba(255, 107, 107, 0.1)' }
+ ])
+ }
+ }
+])
+
+const realtimeXAxis = [{
+ type: 'category',
+ data: Array.from({length: 24}, (_, i) => `${i.toString().padStart(2, '0')}:00`),
+ axisLabel: { color: '#B8C8E0' }
+}]
+
+const realtimeYAxis = [{
+ type: 'value',
+ name: 'tCO鈧俥/h',
+ axisLabel: { color: '#B8C8E0' },
+ nameTextStyle: { color: '#B8C8E0' }
+}]
+
+const realtimeTooltip = {
+ trigger: 'axis',
+ formatter: '{b}: {c} tCO鈧俥/h'
+}
+
+// 鐑姏鍥鹃厤缃�
+const heatmapSeries = ref([
+ {
+ name: '纰虫帓鏀鹃噺',
+ type: 'heatmap',
+ data: generateHeatmapData(),
+ label: {
+ show: false
+ },
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ }
+ }
+])
+
+const heatmapXAxis = [{
+ type: 'category',
+ data: Array.from({length: 24}, (_, i) => `${i}:00`),
+ splitArea: { show: true },
+ axisLabel: { color: '#B8C8E0' }
+}]
+
+const heatmapYAxis = [{
+ type: 'category',
+ data: ['璁惧A', '璁惧B', '璁惧C', '璁惧D', '璁惧E', '璁惧F', '璁惧G'],
+ splitArea: { show: true },
+ axisLabel: { color: '#B8C8E0' }
+}]
+
+const heatmapTooltip = {
+ trigger: 'item',
+ formatter: function (params) {
+ const [hour, device] = params.data
+ const value = params.value[2]
+ return `璁惧: ${heatmapYAxis[0].data[device]}<br/>鏃堕棿: ${hour}:00<br/>纰虫帓鏀鹃噺: ${value} tCO鈧俥`
+ }
+}
+
+const heatmapVisualMap = ref({
+ min: 0,
+ max: 50,
+ calculable: true,
+ orient: 'horizontal',
+ left: 'center',
+ bottom: '5%',
+ inRange: {
+ color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']
+ },
+ textStyle: { color: '#B8C8E0' }
+})
+
+// 瓒嬪娍鍒嗘瀽鍥捐〃閰嶇疆
+const trendSeries = ref([
+ {
+ name: '鑼冨洿1',
+ type: 'line',
+ data: [120, 132, 101, 134, 90, 230, 210],
+ itemStyle: { color: '#FF6B6B' }
+ },
+ {
+ name: '鑼冨洿2',
+ type: 'line',
+ data: [220, 182, 191, 234, 290, 330, 310],
+ itemStyle: { color: '#4ECDC4' }
+ },
+ {
+ name: '鑼冨洿3',
+ type: 'line',
+ data: [150, 232, 201, 154, 190, 330, 410],
+ itemStyle: { color: '#45B7D1' }
+ }
+])
+
+const trendXAxis = [{
+ type: 'category',
+ data: ['鍛ㄤ竴', '鍛ㄤ簩', '鍛ㄤ笁', '鍛ㄥ洓', '鍛ㄤ簲', '鍛ㄥ叚', '鍛ㄦ棩'],
+ axisLabel: { color: '#B8C8E0' }
+}]
+
+const trendYAxis = [{
+ type: 'value',
+ name: 'tCO鈧俥',
+ axisLabel: { color: '#B8C8E0' },
+ nameTextStyle: { color: '#B8C8E0' }
+}]
+
+const trendTooltip = {
+ trigger: 'axis'
+}
+
+const trendLegend = {
+ data: ['鑼冨洿1', '鑼冨洿2', '鑼冨洿3'],
+ textStyle: { color: '#B8C8E0' }
+}
+
+// 琛ㄦ牸鏁版嵁
+const carbonTableData = ref([
+ { name: '鐢熶骇绾緼', type: '浜х嚎', scope1: 45.2, scope2: 32.1, scope3: 18.7, total: 96.0, efficiency: '鑹ソ', status: '姝e父' },
+ { name: '璁惧B-01', type: '璁惧', scope1: 12.5, scope2: 8.3, scope3: 5.2, total: 26.0, efficiency: '浼樼', status: '姝e父' },
+ { name: '鐢熶骇绾緾', type: '浜х嚎', scope1: 38.7, scope2: 28.9, scope3: 15.4, total: 83.0, efficiency: '鑹ソ', status: '鍛婅' },
+ { name: '璁惧D-02', type: '璁惧', scope1: 15.8, scope2: 11.2, scope3: 7.1, total: 34.1, efficiency: '涓�鑸�', status: '姝e父' },
+ { name: '鐢熶骇绾縀', type: '浜х嚎', scope1: 52.3, scope2: 39.6, scope3: 22.8, total: 114.7, efficiency: '寰呬紭鍖�', status: '鍛婅' }
+])
+
+// 鐢熸垚瀹炴椂鏁版嵁
+function generateRealtimeData() {
+ return Array.from({length: 24}, () => (Math.random() * 20 + 10).toFixed(1))
+}
+
+// 鐢熸垚鐑姏鍥炬暟鎹�
+function generateHeatmapData() {
+ const data = []
+ let yAxisLength = 7 // 榛樿璁惧绾�
+ let baseMultiplier = 1 // 鍩虹鍊嶆暟
+
+ // 鏍规嵁灞傜骇纭畾Y杞撮暱搴﹀拰鏁版嵁鑼冨洿
+ if (heatmapLevel.value === 'line') {
+ yAxisLength = 5
+ baseMultiplier = 2 // 浜х嚎绾ф暟鎹洿澶�
+ } else if (heatmapLevel.value === 'enterprise') {
+ yAxisLength = 3
+ baseMultiplier = 4 // 浼佷笟绾ф暟鎹渶澶�
+ }
+
+ for (let i = 0; i < yAxisLength; i++) {
+ for (let j = 0; j < 24; j++) {
+ let value
+ // 绠�鍖栫殑鏃堕棿娈甸�昏緫
+ if (j >= 8 && j <= 18) {
+ // 宸ヤ綔鏃堕棿鎺掓斁閲忚緝楂�
+ value = Math.random() * 30 + 20
+ } else if (j >= 19 && j <= 22) {
+ // 鏅氶棿鎺掓斁閲忎腑绛�
+ value = Math.random() * 20 + 10
+ } else {
+ // 娣卞鍜屽噷鏅ㄦ帓鏀鹃噺杈冧綆
+ value = Math.random() * 10 + 2
+ }
+
+ // 娣诲姞璁惧宸紓鍜屽眰绾у�嶆暟
+ value *= (0.8 + i * 0.1) * baseMultiplier
+
+ data.push([j, i, Math.round(value * 10) / 10])
+ }
+ }
+ return data
+}
+
+// 鏇存柊鑼冨洿鏁版嵁
+function updateScopeData() {
+ // 鏍规嵁閫夋嫨鐨勮寖鍥存洿鏂版墍鏈夌浉鍏冲浘琛ㄦ暟鎹�
+ const scopeMultiplier = {
+ 'all': 1,
+ 'scope1': 0.3,
+ 'scope2': 0.4,
+ 'scope3': 0.3
+ }
+
+ const multiplier = scopeMultiplier[selectedScope.value] || 1
+
+ // 鏇存柊纰虫帓鏀炬暟鎹樉绀�
+ if (selectedScope.value === 'all') {
+ carbonData.value = {
+ scope1: 125.6,
+ scope2: 89.3,
+ scope3: 234.7
+ }
+ } else {
+ const baseTotal = 125.6 + 89.3 + 234.7
+ carbonData.value = {
+ scope1: selectedScope.value === 'scope1' ? 125.6 : 0,
+ scope2: selectedScope.value === 'scope2' ? 89.3 : 0,
+ scope3: selectedScope.value === 'scope3' ? 234.7 : 0
+ }
+ }
+
+ // 鏇存柊鐑姏鍥炬暟鎹�
+ heatmapSeries.value[0].data = generateHeatmapData().map(item => [
+ item[0], item[1], Math.round(item[2] * multiplier * 10) / 10
+ ])
+
+ // 鏇存柊瀹炴椂鐩戞帶鏁版嵁
+ realtimeSeries.value[0].data = generateRealtimeData().map(val =>
+ Math.round(parseFloat(val) * multiplier * 10) / 10
+ )
+}
+
+// 鏇存柊鐑姏鍥惧眰绾�
+function updateHeatmapLevel() {
+ // 鏍规嵁灞傜骇鏇存柊Y杞存暟鎹拰visualMap鑼冨洿
+ if (heatmapLevel.value === 'device') {
+ heatmapYAxis[0].data = ['閿呯倝A', '鍘嬬缉鏈築', '鍐峰嵈濉擟', '椋庢満D', '娉礒', '鍙樺帇鍣‵', '鐢垫満G']
+ heatmapVisualMap.value.max = 50
+ } else if (heatmapLevel.value === 'line') {
+ heatmapYAxis[0].data = ['鐢熶骇绾�1', '鐢熶骇绾�2', '鐢熶骇绾�3', '鐢熶骇绾�4', '鐢熶骇绾�5']
+ heatmapVisualMap.value.max = 100
+ } else {
+ heatmapYAxis[0].data = ['鍘傚尯A', '鍘傚尯B', '鍘傚尯C']
+ heatmapVisualMap.value.max = 200
+ }
+
+ // 鏇存柊鐑姏鍥炬暟鎹�
+ heatmapSeries.value[0].data = generateHeatmapData()
+
+ // 鏇存柊琛ㄦ牸鏁版嵁浠ュ尮閰嶅綋鍓嶅眰绾�
+ updateTableDataForLevel()
+}
+
+// 鏍规嵁灞傜骇鏇存柊琛ㄦ牸鏁版嵁
+function updateTableDataForLevel() {
+ const levelConfigs = {
+ device: [
+ { name: '閿呯倝A', type: '璁惧', scope1: 45.2, scope2: 32.1, scope3: 18.7, total: 96.0, efficiency: '鑹ソ', status: '姝e父' },
+ { name: '鍘嬬缉鏈築', type: '璁惧', scope1: 38.5, scope2: 28.3, scope3: 15.2, total: 82.0, efficiency: '浼樼', status: '姝e父' },
+ { name: '鍐峰嵈濉擟', type: '璁惧', scope1: 22.8, scope2: 18.9, scope3: 12.3, total: 54.0, efficiency: '鑹ソ', status: '鍛婅' },
+ { name: '椋庢満D', type: '璁惧', scope1: 15.6, scope2: 12.4, scope3: 8.1, total: 36.1, efficiency: '涓�鑸�', status: '姝e父' },
+ { name: '娉礒', type: '璁惧', scope1: 12.3, scope2: 9.8, scope3: 6.4, total: 28.5, efficiency: '浼樼', status: '姝e父' }
+ ],
+ line: [
+ { name: '鐢熶骇绾�1', type: '浜х嚎', scope1: 125.6, scope2: 89.3, scope3: 56.8, total: 271.7, efficiency: '鑹ソ', status: '姝e父' },
+ { name: '鐢熶骇绾�2', type: '浜х嚎', scope1: 98.4, scope2: 72.1, scope3: 45.2, total: 215.7, efficiency: '浼樼', status: '姝e父' },
+ { name: '鐢熶骇绾�3', type: '浜х嚎', scope1: 87.2, scope2: 65.8, scope3: 41.6, total: 194.6, efficiency: '鑹ソ', status: '鍛婅' },
+ { name: '鐢熶骇绾�4', type: '浜х嚎', scope1: 76.9, scope2: 58.3, scope3: 37.1, total: 172.3, efficiency: '涓�鑸�', status: '姝e父' },
+ { name: '鐢熶骇绾�5', type: '浜х嚎', scope1: 65.7, scope2: 49.2, scope3: 31.8, total: 146.7, efficiency: '寰呬紭鍖�', status: '鍛婅' }
+ ],
+ enterprise: [
+ { name: '鍘傚尯A', type: '鍘傚尯', scope1: 456.8, scope2: 334.7, scope3: 212.5, total: 1004.0, efficiency: '鑹ソ', status: '姝e父' },
+ { name: '鍘傚尯B', type: '鍘傚尯', scope1: 387.2, scope2: 289.6, scope3: 184.3, total: 861.1, efficiency: '浼樼', status: '姝e父' },
+ { name: '鍘傚尯C', type: '鍘傚尯', scope1: 298.5, scope2: 223.8, scope3: 142.7, total: 665.0, efficiency: '鑹ソ', status: '鍛婅' }
+ ]
+ }
+
+ carbonTableData.value = levelConfigs[heatmapLevel.value] || levelConfigs.device
+}
+
+// 鏇存柊鐑姏鍥炬暟鎹紙鏃ユ湡鍙樺寲鏃讹級
+function updateHeatmapData() {
+ heatmapSeries.value[0].data = generateHeatmapData()
+
+ // 鍚屾椂鏇存柊鍏朵粬鐩稿叧鏁版嵁
+ updateScopeData()
+}
+
+// 鏇存柊瓒嬪娍鏁版嵁
+function updateTrendData() {
+ const trendDataConfigs = {
+ week: {
+ xAxisData: ['鍛ㄤ竴', '鍛ㄤ簩', '鍛ㄤ笁', '鍛ㄥ洓', '鍛ㄤ簲', '鍛ㄥ叚', '鍛ㄦ棩'],
+ scope1Data: [120, 132, 101, 134, 90, 80, 75],
+ scope2Data: [220, 182, 191, 234, 190, 150, 140],
+ scope3Data: [150, 232, 201, 154, 190, 120, 110]
+ },
+ month: {
+ xAxisData: ['1鏈�', '2鏈�', '3鏈�', '4鏈�', '5鏈�', '6鏈�', '7鏈�', '8鏈�', '9鏈�', '10鏈�', '11鏈�', '12鏈�'],
+ scope1Data: [1200, 1150, 1300, 1250, 1180, 1320, 1280, 1350, 1220, 1290, 1160, 1100],
+ scope2Data: [2200, 2100, 2350, 2280, 2150, 2400, 2320, 2450, 2180, 2380, 2120, 2050],
+ scope3Data: [1800, 1750, 1950, 1880, 1820, 2000, 1920, 2100, 1850, 1980, 1780, 1720]
+ },
+ year: {
+ xAxisData: ['2019', '2020', '2021', '2022', '2023', '2024'],
+ scope1Data: [14500, 14200, 13800, 13500, 13100, 12800],
+ scope2Data: [26800, 26200, 25600, 25000, 24400, 23800],
+ scope3Data: [22400, 21800, 21200, 20600, 20000, 19400]
+ }
+ }
+
+ const config = trendDataConfigs[trendPeriod.value] || trendDataConfigs.week
+
+ // 鏇存柊X杞存暟鎹�
+ trendXAxis[0].data = config.xAxisData
+
+ // 鏇存柊绯诲垪鏁版嵁
+ trendSeries.value = [
+ {
+ name: '鑼冨洿1',
+ type: 'line',
+ data: config.scope1Data,
+ itemStyle: { color: '#FF6B6B' },
+ smooth: true
+ },
+ {
+ name: '鑼冨洿2',
+ type: 'line',
+ data: config.scope2Data,
+ itemStyle: { color: '#4ECDC4' },
+ smooth: true
+ },
+ {
+ name: '鑼冨洿3',
+ type: 'line',
+ data: config.scope3Data,
+ itemStyle: { color: '#45B7D1' },
+ smooth: true
+ }
+ ]
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+function getStatusType(status) {
+ switch (status) {
+ case '姝e父': return 'success'
+ case '鍛婅': return 'warning'
+ case '寮傚父': return 'danger'
+ default: return 'info'
+ }
+}
+
+// 瀵煎嚭鏁版嵁
+function exportData() {
+ // 鍑嗗瀵煎嚭鏁版嵁
+ const exportDataSet = {
+ 鍩烘湰淇℃伅: {
+ 瀵煎嚭鏃堕棿: new Date().toLocaleString('zh-CN'),
+ 鏁版嵁灞傜骇: heatmapLevel.value === 'device' ? '璁惧绾�' : heatmapLevel.value === 'line' ? '浜х嚎绾�' : '浼佷笟绾�',
+ 閫夋嫨鑼冨洿: selectedScope.value === 'all' ? '鍏ㄩ儴鑼冨洿' : `鑼冨洿${selectedScope.value.slice(-1)}`,
+ 閫夋嫨鏃ユ湡: selectedDate.value ? selectedDate.value.toLocaleDateString('zh-CN') : '浠婃棩'
+ },
+ 纰虫帓鏀剧粺璁�: {
+ 鑼冨洿1鐩存帴鎺掓斁: carbonData.value.scope1 + ' tCO鈧俥',
+ 鑼冨洿2闂存帴鎺掓斁: carbonData.value.scope2 + ' tCO鈧俥',
+ 鑼冨洿3渚涘簲閾炬帓鏀�: carbonData.value.scope3 + ' tCO鈧俥',
+ 鎬绘帓鏀鹃噺: totalEmissions.value + ' tCO鈧俥'
+ },
+ 璇︾粏鏁版嵁: carbonTableData.value,
+ 鐑姏鍥炬暟鎹�: heatmapSeries.value[0].data.map(item => ({
+ 鏃堕棿: `${item[0]}:00`,
+ 璁惧搴忓彿: item[1],
+ 璁惧鍚嶇О: heatmapYAxis.data[item[1]],
+ 纰虫帓鏀鹃噺: item[2] + ' tCO鈧俥'
+ }))
+ }
+
+ // 鍒涘缓CSV鍐呭
+ let csvContent = '\uFEFF' // BOM for UTF-8
+
+ // 鍩烘湰淇℃伅
+ csvContent += '鍩烘湰淇℃伅\n'
+ Object.entries(exportDataSet.鍩烘湰淇℃伅).forEach(([key, value]) => {
+ csvContent += `${key},${value}\n`
+ })
+ csvContent += '\n'
+
+ // 纰虫帓鏀剧粺璁�
+ csvContent += '纰虫帓鏀剧粺璁n'
+ Object.entries(exportDataSet.纰虫帓鏀剧粺璁�).forEach(([key, value]) => {
+ csvContent += `${key},${value}\n`
+ })
+ csvContent += '\n'
+
+ // 璇︾粏鏁版嵁琛ㄦ牸
+ csvContent += '璇︾粏鏁版嵁\n'
+ csvContent += '鍚嶇О,绫诲瀷,鑼冨洿1鎺掓斁,鑼冨洿2鎺掓斁,鑼冨洿3鎺掓斁,鎬绘帓鏀鹃噺,纰虫晥鐜�,鐘舵�乗n'
+ exportDataSet.璇︾粏鏁版嵁.forEach(row => {
+ csvContent += `${row.name},${row.type},${row.scope1},${row.scope2},${row.scope3},${row.total},${row.efficiency},${row.status}\n`
+ })
+ csvContent += '\n'
+
+ // 鐑姏鍥炬暟鎹紙鍓�50鏉★級
+ csvContent += '鐑姏鍥炬暟鎹紙鍓�50鏉★級\n'
+ csvContent += '鏃堕棿,璁惧鍚嶇О,纰虫帓鏀鹃噺\n'
+ exportDataSet.鐑姏鍥炬暟鎹�.slice(0, 50).forEach(row => {
+ csvContent += `${row.鏃堕棿},${row.璁惧鍚嶇О},${row.纰虫帓鏀鹃噺}\n`
+ })
+
+ // 鍒涘缓涓嬭浇閾炬帴
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
+ const link = document.createElement('a')
+ const url = URL.createObjectURL(blob)
+ link.setAttribute('href', url)
+ link.setAttribute('download', `纰虫帓鏀炬暟鎹甠${new Date().toISOString().slice(0, 10)}.csv`)
+ link.style.visibility = 'hidden'
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+
+ // 鏄剧ず鎴愬姛娑堟伅
+ console.log('纰虫帓鏀炬暟鎹鍑烘垚鍔�')
+}
+
+// 鎼滅储杩囨护鍔熻兘
+const filteredTableData = computed(() => {
+ if (!searchKeyword.value) {
+ return carbonTableData.value
+ }
+ return carbonTableData.value.filter(item =>
+ item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
+ item.type.toLowerCase().includes(searchKeyword.value.toLowerCase())
+ )
+})
+
+// 鐑姏鍥剧偣鍑讳簨浠跺鐞�
+function handleHeatmapClick(params) {
+ if (params.componentType === 'series') {
+ const [hour, deviceIndex, value] = params.data
+ const deviceName = heatmapYAxis.data[deviceIndex]
+ console.log(`鐐瑰嚮浜嗚澶�: ${deviceName}, 鏃堕棿: ${hour}:00, 鎺掓斁閲�: ${value} tCO鈧俥`)
+
+ // 鍙互鍦ㄨ繖閲屾坊鍔犺缁嗕俊鎭脊绐楁垨璺宠浆鍒拌缁嗛〉闈�
+ }
+}
+
+onMounted(() => {
+ // 椤甸潰鍔犺浇瀹屾垚鍚庣殑鍒濆鍖栨搷浣�
+ console.log('纰崇鐞嗛〉闈㈠凡鍔犺浇')
+
+ // 鍒濆鍖栫儹鍔涘浘鏁版嵁
+ updateHeatmapLevel()
+
+ // 鍒濆鍖栬秼鍔挎暟鎹�
+ updateTrendData()
+
+ // 鍒濆鍖栬寖鍥存暟鎹�
+ updateScopeData()
+
+ // 璁剧疆瀹氭椂鍣紝姣�30绉掓洿鏂颁竴娆″疄鏃舵暟鎹�
+ const timer = setInterval(() => {
+ realtimeSeries.value[0].data = generateRealtimeData()
+ }, 30000)
+
+ // 娓呯悊瀹氭椂鍣�
+ onBeforeUnmount(() => {
+ clearInterval(timer)
+ })
+})
+
+// 娣诲姞鐑姏鍥剧偣鍑讳簨浠剁粦瀹�
+function bindHeatmapEvents() {
+ // 杩欎釜鍑芥暟鍙互鐢ㄦ潵缁戝畾鐑姏鍥剧殑鐐瑰嚮浜嬩欢
+ // 鍦ㄥ疄闄呬娇鐢ㄤ腑锛屽彲浠ラ�氳繃ECharts鐨勪簨浠剁郴缁熸潵瀹炵幇
+}
+</script>
+
+<style scoped>
+.carbon-management {
+ min-height: 100vh;
+ background:
+ radial-gradient(ellipse at top, rgba(29, 78, 216, 0.15), transparent 50%),
+ radial-gradient(ellipse at bottom, rgba(139, 92, 246, 0.15), transparent 50%),
+ linear-gradient(135deg, #0a0f1c 0%, #1e293b 25%, #0f172a 50%, #1e293b 75%, #0a0f1c 100%);
+ padding: 20px;
+ font-family: 'Inter', 'Microsoft YaHei', sans-serif;
+ overflow: hidden;
+ position: relative;
+}
+
+.carbon-management::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ radial-gradient(circle at 20% 80%, rgba(120, 119, 198, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 80% 20%, rgba(255, 119, 198, 0.1) 0%, transparent 50%),
+ radial-gradient(circle at 40% 40%, rgba(120, 219, 255, 0.05) 0%, transparent 50%);
+ pointer-events: none;
+
+}
+
+
+
+.page-header {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%),
+ radial-gradient(circle at top right, rgba(59, 130, 246, 0.1), transparent 50%);
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ border-radius: 20px;
+ padding: 40px;
+ margin-bottom: 30px;
+ box-shadow:
+ 0 25px 50px -12px rgba(0, 0, 0, 0.4),
+ 0 0 0 1px rgba(255, 255, 255, 0.05),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ position: relative;
+ overflow: hidden;
+ backdrop-filter: blur(20px);
+
+}
+
+.page-header:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 32px 64px -12px rgba(0, 0, 0, 0.5),
+ 0 0 0 1px rgba(255, 255, 255, 0.1),
+ inset 0 1px 0 rgba(255, 255, 255, 0.15);
+}
+
+.page-header::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background:
+ linear-gradient(45deg, rgba(59, 130, 246, 0.08) 0%, rgba(147, 51, 234, 0.08) 50%, rgba(236, 72, 153, 0.08) 100%);
+ pointer-events: none;
+
+}
+
+
+
+.header-content {
+ flex: 1;
+ position: relative;
+ z-index: 1;
+}
+
+.page-title {
+ font-size: 28px;
+ font-weight: bold;
+ color: #ffffff;
+ margin: 0 0 8px 0;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+}
+
+.page-subtitle {
+ font-size: 14px;
+ color: #B8C8E0;
+ margin: 0;
+}
+
+.header-stats {
+ display: flex;
+ gap: 40px;
+ position: relative;
+ z-index: 1;
+}
+
+.stat-item {
+ text-align: center;
+ padding: 20px;
+ background:
+ linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(147, 51, 234, 0.15) 100%),
+ radial-gradient(circle at center, rgba(255, 255, 255, 0.05), transparent 70%);
+ border-radius: 12px;
+ border: 1px solid rgba(148, 163, 184, 0.2);
+ position: relative;
+ overflow: hidden;
+
+ backdrop-filter: blur(10px);
+}
+
+.stat-item:hover {
+ transform: translateY(-2px) scale(1.05);
+ box-shadow:
+ 0 20px 25px -5px rgba(59, 130, 246, 0.3),
+ 0 10px 10px -5px rgba(59, 130, 246, 0.2);
+}
+
+.stat-item::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
+
+}
+
+.stat-item:hover::before {
+ left: 100%;
+}
+
+.stat-label {
+ display: block;
+ font-size: 12px;
+ color: #94A3B8;
+ margin-bottom: 8px;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+}
+
+.stat-value {
+ display: block;
+ font-size: 28px;
+ font-weight: 700;
+ color: #00D4FF;
+ text-shadow:
+ 0 0 20px rgba(0, 212, 255, 0.6),
+ 0 0 40px rgba(0, 212, 255, 0.3);
+ position: relative;
+ z-index: 1;
+}
+
+.stat-value.reduction {
+ color: #00E676;
+ text-shadow:
+ 0 0 20px rgba(0, 230, 118, 0.6),
+ 0 0 40px rgba(0, 230, 118, 0.3);
+}
+
+.dashboard-content {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ min-height: calc(100vh - 200px);
+}
+
+.top-panels {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+ gap: 20px;
+ height: 120px;
+}
+
+.data-panel {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.9) 0%, rgba(30, 41, 59, 0.85) 100%),
+ radial-gradient(circle at bottom left, rgba(59, 130, 246, 0.08), transparent 50%);
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.2),
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(16px);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+
+.panel-title {
+ font-size: 12px;
+ color: #94A3B8;
+ margin-bottom: 8px;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+}
+
+.panel-value {
+ font-size: 24px;
+ font-weight: 700;
+ color: #00D4FF;
+ text-shadow:
+ 0 0 20px rgba(0, 212, 255, 0.6),
+ 0 0 40px rgba(0, 212, 255, 0.3);
+ margin-bottom: 4px;
+}
+
+.panel-subtitle {
+ font-size: 11px;
+ color: #B8C8E0;
+ font-weight: 400;
+}
+
+.unit {
+ font-size: 16px;
+ color: #94A3B8;
+}
+
+.center-main-view {
+ display: grid;
+ grid-template-columns: 200px 1fr 300px;
+ gap: 20px;
+ flex: 1;
+}
+
+.left-control-panel {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.9) 0%, rgba(30, 41, 59, 0.85) 100%);
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(16px);
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.control-section {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.section-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 8px;
+}
+
+.vertical-radio {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.main-heatmap {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.9) 0%, rgba(30, 41, 59, 0.85) 100%);
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(16px);
+ display: flex;
+ flex-direction: column;
+}
+
+.heatmap-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+.main-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #ffffff;
+ margin: 0;
+}
+
+.date-selector {
+ display: flex;
+ align-items: center;
+}
+
+.heatmap-view {
+ flex: 1;
+}
+
+.right-data-panel {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.9) 0%, rgba(30, 41, 59, 0.85) 100%);
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(16px);
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.data-section {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.mini-chart {
+ width: 100%;
+}
+
+.trend-controls {
+ margin-bottom: 10px;
+}
+
+.bottom-progress-panel {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 40px;
+ height: 100px;
+}
+
+.progress-section {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.9) 0%, rgba(30, 41, 59, 0.85) 100%);
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
+ 0 0 0 1px rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(16px);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.progress-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 8px;
+}
+
+.progress-data {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+ margin-bottom: 12px;
+}
+
+.progress-data .current {
+ font-size: 20px;
+ font-weight: 700;
+ color: #00D4FF;
+}
+
+.progress-data .separator {
+ font-size: 16px;
+ color: #94A3B8;
+}
+
+.progress-data .target {
+ font-size: 14px;
+ color: #B8C8E0;
+}
+
+.bottom-data-table {
+ margin-top: 20px;
+}
+
+.table-panel {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.9) 0%, rgba(30, 41, 59, 0.85) 100%),
+ radial-gradient(circle at bottom left, rgba(59, 130, 246, 0.08), transparent 50%);
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ border-radius: 16px;
+ padding: 20px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.2),
+ 0 0 0 1px rgba(255, 255, 255, 0.05),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(16px);
+}
+
+.table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid rgba(148, 163, 184, 0.2);
+}
+
+.table-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #ffffff;
+ margin: 0;
+}
+
+.table-controls {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.panel-card {
+ background:
+ linear-gradient(135deg, rgba(15, 27, 46, 0.9) 0%, rgba(30, 41, 59, 0.85) 100%),
+ radial-gradient(circle at bottom left, rgba(59, 130, 246, 0.08), transparent 50%);
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ border-radius: 16px;
+ padding: 24px;
+ box-shadow:
+ 0 20px 25px -5px rgba(0, 0, 0, 0.3),
+ 0 10px 10px -5px rgba(0, 0, 0, 0.2),
+ 0 0 0 1px rgba(255, 255, 255, 0.05),
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ position: relative;
+ overflow: hidden;
+ backdrop-filter: blur(16px);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+
+
+.heatmap-card {
+ height: 500px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 1px solid rgba(81, 129, 219, 0.3);
+ position: relative;
+ z-index: 1;
+}
+
+.card-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #ffffff;
+ margin: 0;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+}
+
+.heatmap-controls {
+ display: flex;
+ align-items: center;
+}
+
+.scope-stats {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ position: relative;
+ z-index: 1;
+}
+
+.scope-item {
+ display: flex;
+ align-items: center;
+ padding: 20px;
+ border-radius: 12px;
+ background:
+ linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(147, 51, 234, 0.12) 100%),
+ radial-gradient(circle at top, rgba(255, 255, 255, 0.05), transparent 60%);
+ border-left: 4px solid #00D4FF;
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ backdrop-filter: blur(8px);
+}
+
+.scope-item:hover {
+ transform: translateY(-3px);
+ box-shadow:
+ 0 15px 30px -5px rgba(59, 130, 246, 0.25),
+ 0 0 0 1px rgba(255, 255, 255, 0.1);
+}
+
+.scope-item::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, #3B82F6, #8B5CF6, #EC4899);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.scope-item:hover::after {
+ opacity: 1;
+}
+
+/* 纰虫帓鏀剧粺璁℃牱寮� */
+.carbon-stats {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ gap: 15px;
+}
+
+.carbon-stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ padding: 20px;
+ background:
+ linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(147, 51, 234, 0.12) 100%),
+ radial-gradient(circle at top, rgba(255, 255, 255, 0.05), transparent 60%);
+ border-radius: 12px;
+ border: 1px solid rgba(148, 163, 184, 0.15);
+ flex: 1;
+ position: relative;
+ overflow: hidden;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ backdrop-filter: blur(8px);
+}
+
+.carbon-stat-item:hover {
+ transform: translateY(-3px);
+ box-shadow:
+ 0 15px 30px -5px rgba(59, 130, 246, 0.25),
+ 0 0 0 1px rgba(255, 255, 255, 0.1);
+}
+
+.carbon-stat-item::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, #3B82F6, #8B5CF6, #EC4899);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.carbon-stat-item:hover::after {
+ opacity: 1;
+}
+
+.carbon-label {
+ color: #94A3B8;
+ font-size: 11px;
+ text-align: center;
+ font-weight: 500;
+ letter-spacing: 0.5px;
+ text-transform: uppercase;
+}
+
+.carbon-value {
+ color: #00D4FF;
+ font-size: 18px;
+ font-weight: 700;
+ text-shadow:
+ 0 0 15px rgba(0, 212, 255, 0.6),
+ 0 0 30px rgba(0, 212, 255, 0.3);
+ position: relative;
+}
+
+.scope-item.scope1 {
+ border-left-color: #FF6B6B;
+}
+
+.scope-item.scope2 {
+ border-left-color: #FFD93D;
+}
+
+.scope-item.scope3 {
+ border-left-color: #6BCF7F;
+}
+
+.scope-icon {
+ font-size: 24px;
+ margin-right: 15px;
+}
+
+.scope-info {
+ flex: 1;
+}
+
+.scope-name {
+ display: block;
+ font-weight: bold;
+ color: #ffffff;
+ margin-bottom: 5px;
+}
+
+.scope-value {
+ display: block;
+ font-size: 20px;
+ font-weight: bold;
+ color: #00D4FF;
+ margin-bottom: 3px;
+ text-shadow: 0 0 8px rgba(0, 212, 255, 0.5);
+}
+
+.scope-desc {
+ display: block;
+ font-size: 12px;
+ color: #B8C8E0;
+}
+
+.target-progress {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ position: relative;
+ z-index: 1;
+}
+
+.progress-item {
+ padding: 15px;
+ background: rgba(81, 129, 219, 0.1);
+ border-radius: 8px;
+ border: 1px solid rgba(81, 129, 219, 0.2);
+}
+
+.progress-info {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.progress-label {
+ font-weight: bold;
+ color: #ffffff;
+}
+
+.progress-value {
+ color: #B8C8E0;
+ font-size: 14px;
+}
+
+.bottom-panel {
+ margin-top: 20px;
+}
+
+.table-controls {
+ display: flex;
+ align-items: center;
+}
+
+/* Element Plus 缁勪欢娣辫壊涓婚鏍峰紡 */
+:deep(.el-table) {
+ background: transparent !important;
+ color: #ffffff !important;
+}
+
+:deep(.el-table th) {
+ background: rgba(81, 129, 219, 0.2) !important;
+ color: #ffffff !important;
+ border-bottom: 1px solid rgba(81, 129, 219, 0.3) !important;
+}
+
+:deep(.el-table td) {
+ background: transparent !important;
+ color: #B8C8E0 !important;
+ border-bottom: 1px solid rgba(81, 129, 219, 0.1) !important;
+}
+
+:deep(.el-table tr:hover > td) {
+ background: rgba(81, 129, 219, 0.1) !important;
+}
+
+:deep(.el-input__wrapper) {
+ background: rgba(15, 27, 46, 0.8) !important;
+ border: 1px solid rgba(81, 129, 219, 0.3) !important;
+ color: #ffffff !important;
+}
+
+:deep(.el-input__inner) {
+ color: #ffffff !important;
+}
+
+:deep(.el-button--primary) {
+ background: linear-gradient(135deg, #5181DB, #D369E0) !important;
+ border: none !important;
+ box-shadow: 0 0 10px rgba(81, 129, 219, 0.5) !important;
+}
+
+/* 鍨傜洿鍗曢�夋寜閽粍鏍峰紡 */
+:deep(.vertical-radio) {
+ display: flex !important;
+ flex-direction: column !important;
+ gap: 6px !important;
+}
+
+:deep(.vertical-radio .el-radio-button) {
+ margin: 0 !important;
+ width: 100% !important;
+}
+
+:deep(.vertical-radio .el-radio-button__inner) {
+ background: rgba(59, 130, 246, 0.1) !important;
+ border: 1px solid rgba(148, 163, 184, 0.2) !important;
+ color: #B8C8E0 !important;
+ border-radius: 8px !important;
+ padding: 10px 16px !important;
+ width: 100% !important;
+ text-align: center !important;
+ font-size: 12px !important;
+ font-weight: 500 !important;
+}
+
+:deep(.vertical-radio .el-radio-button__inner:hover) {
+ background: rgba(59, 130, 246, 0.2) !important;
+ border-color: rgba(59, 130, 246, 0.4) !important;
+ color: #ffffff !important;
+}
+
+:deep(.vertical-radio .el-radio-button.is-active .el-radio-button__inner) {
+ background: linear-gradient(135deg, #3B82F6, #8B5CF6) !important;
+ border-color: #3B82F6 !important;
+ color: #ffffff !important;
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3) !important;
+}
+
+:deep(.vertical-radio .el-radio-button:first-child .el-radio-button__inner) {
+ border-left: 1px solid rgba(148, 163, 184, 0.2) !important;
+}
+
+:deep(.el-radio-group .el-radio-button__inner) {
+ background: rgba(59, 130, 246, 0.1) !important;
+ border: 1px solid rgba(148, 163, 184, 0.2) !important;
+ color: #B8C8E0 !important;
+ border-radius: 6px !important;
+ padding: 6px 12px !important;
+ margin: 0 2px !important;
+ font-size: 12px !important;
+}
+
+:deep(.el-radio-group .el-radio-button__inner:hover) {
+ background: rgba(59, 130, 246, 0.2) !important;
+ border-color: rgba(59, 130, 246, 0.4) !important;
+ color: #ffffff !important;
+}
+
+:deep(.el-radio-group .el-radio-button.is-active .el-radio-button__inner) {
+ background: linear-gradient(135deg, #3B82F6, #8B5CF6) !important;
+ border-color: #3B82F6 !important;
+ color: #ffffff !important;
+ box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3) !important;
+}
+
+:deep(.el-date-editor .el-input__wrapper) {
+ background: rgba(15, 27, 46, 0.8) !important;
+ border: 1px solid rgba(81, 129, 219, 0.3) !important;
+}
+
+:deep(.el-progress-bar__outer) {
+ background: rgba(81, 129, 219, 0.2) !important;
+}
+
+:deep(.el-tag) {
+ background: rgba(81, 129, 219, 0.2) !important;
+ border: 1px solid rgba(81, 129, 219, 0.3) !important;
+ color: #ffffff !important;
+}
+
+:deep(.el-tag.el-tag--success) {
+ background: rgba(0, 230, 118, 0.2) !important;
+ border-color: rgba(0, 230, 118, 0.3) !important;
+ color: #00E676 !important;
+}
+
+:deep(.el-tag.el-tag--warning) {
+ background: rgba(255, 193, 7, 0.2) !important;
+ border-color: rgba(255, 193, 7, 0.3) !important;
+ color: #FFC107 !important;
+}
+
+:deep(.el-tag.el-tag--danger) {
+ background: rgba(244, 67, 54, 0.2) !important;
+ border-color: rgba(244, 67, 54, 0.3) !important;
+ color: #F44336 !important;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 1200px) {
+ .main-content {
+ flex-direction: column;
+ }
+
+ .header-stats {
+ gap: 20px;
+ }
+}
+
+@media (max-width: 768px) {
+ .page-header {
+ flex-direction: column;
+ text-align: center;
+ gap: 20px;
+ }
+
+ .header-stats {
+ justify-content: center;
+ }
+
+ .carbon-management {
+ padding: 10px;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/energyManagement/dynamicEnergySaving/index.vue b/src/views/energyManagement/dynamicEnergySaving/index.vue
new file mode 100644
index 0000000..8976b22
--- /dev/null
+++ b/src/views/energyManagement/dynamicEnergySaving/index.vue
@@ -0,0 +1,657 @@
+<template>
+ <div class="app-container">
+ <!-- 杈圭紭璁$畻鐘舵�佺洃鎺� -->
+ <el-row :gutter="20" class="status-section">
+ <el-col :span="8">
+ <el-card class="status-card">
+ <div class="status-item">
+ <div class="status-icon">
+ <el-icon><Monitor /></el-icon>
+ </div>
+ <div class="status-info">
+ <div class="status-title">杈圭紭鏈嶅姟鍣ㄧ姸鎬�</div>
+ <div class="status-value" :class="edgeServerStatus.status">
+ {{ edgeServerStatus.status === 'online' ? '鍦ㄧ嚎' : '绂荤嚎' }}
+ </div>
+ <div class="status-detail">鏈�鍚庡績璺�: {{ formatTime(edgeServerStatus.lastHeartbeat) }}</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="8">
+ <el-card class="status-card">
+ <div class="status-item">
+ <div class="status-icon">
+ <el-icon><Cpu /></el-icon>
+ </div>
+ <div class="status-info">
+ <div class="status-title">妯″瀷杩愯鐘舵��</div>
+ <div class="status-value" :class="modelStatus.status">
+ {{ modelStatus.status === 'running' ? '杩愯涓�' : '宸插仠姝�' }}
+ </div>
+ <div class="status-detail">杩愯妯″瀷: {{ modelStatus.modelCount }}涓�</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="8">
+ <el-card class="status-card">
+ <div class="status-item">
+ <div class="status-icon">
+ <el-icon><TrendCharts /></el-icon>
+ </div>
+ <div class="status-info">
+ <div class="status-title">鑺傝兘鏁堟灉</div>
+ <div class="status-value success">{{ energySavingRate.toFixed(1) }}%</div>
+ <div class="status-detail">绱鑺傝兘: {{ totalEnergySaved.toFixed(1) }}kWh</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 娉ㄦ按娉甸鐜囦紭鍖栨帶鍒� -->
+ <el-card class="control-section">
+ <template #header>
+ <span>娉ㄦ按娉甸鐜囦紭鍖栨帶鍒�</span>
+ </template>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="pump-control">
+ <h4>瀹炴椂鍙傛暟鐩戞帶</h4>
+ <el-form label-width="120px">
+ <el-form-item label="鍦板眰鍘嬪姏 (MPa)">
+ <el-input v-model="pumpData.formationPressure" readonly>
+ <template #append>MPa</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="褰撳墠娉甸�� (Hz)">
+ <el-input v-model="pumpData.currentFrequency" readonly>
+ <template #append>Hz</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="浼樺寲鍚庢车閫� (Hz)">
+ <el-input v-model="pumpData.optimizedFrequency" readonly>
+ <template #append>Hz</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="鑳借�楅檷浣�">
+ <el-progress
+ :percentage="pumpData.energyReduction"
+ :color="getProgressColor"
+ :format="format => `${format}%`"
+ />
+ </el-form-item>
+ <el-form-item label="娴侀噺 (m鲁/h)">
+ <el-input v-model="pumpData.flowRate" readonly>
+ <template #append>m鲁/h</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="鍔熺巼 (kW)">
+ <el-input v-model="pumpData.power" readonly>
+ <template #append>kW</template>
+ </el-input>
+ </el-form-item>
+ </el-form>
+ </div>
+ </el-col>
+
+ <el-col :span="12">
+ <div class="pump-chart">
+ <h4>棰戠巼浼樺寲瓒嬪娍</h4>
+ <div ref="frequencyChart" style="height: 300px;"></div>
+ </div>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20" class="control-buttons">
+ <el-col :span="24">
+ <el-button
+ type="primary"
+ :disabled="!canControl"
+ @click="applyOptimization"
+ >
+ 搴旂敤浼樺寲璁剧疆
+ </el-button>
+ <el-button
+ type="warning"
+ :disabled="!canControl"
+ @click="emergencyStop"
+ >
+ 绱ф�ュ仠姝�
+ </el-button>
+ <el-button
+ type="info"
+ @click="showOptimizationHistory"
+ >
+ 浼樺寲鍘嗗彶
+ </el-button>
+ <el-button
+ type="success"
+ @click="toggleAutoRefresh"
+ >
+ {{ autoRefreshStatus ? '鍋滄鑷姩鍒锋柊' : '寮�鍚嚜鍔ㄥ埛鏂�' }}
+ </el-button>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 杈圭紭璁$畻妯″瀷閰嶇疆 -->
+ <el-card class="model-section">
+ <template #header>
+ <span>杈圭紭璁$畻妯″瀷閰嶇疆</span>
+ </template>
+
+ <el-table :data="modelConfigs" style="width: 100%">
+ <el-table-column prop="modelName" label="妯″瀷鍚嶇О" />
+ <el-table-column prop="version" label="鐗堟湰" />
+ <el-table-column prop="status" label="鐘舵��">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'active' ? 'success' : 'info'">
+ {{ scope.row.status === 'active' ? '婵�娲�' : '寰呮満' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="accuracy" label="鍑嗙‘鐜�" />
+ <el-table-column prop="lastUpdate" label="鏈�鍚庢洿鏂�" />
+ <el-table-column label="鎿嶄綔">
+ <template #default="scope">
+ <el-button
+ @click="updateModel(scope.row)"
+ >
+ 鏇存柊妯″瀷
+ </el-button>
+ <el-button
+ type="danger"
+ @click="deleteModel(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 鑳借�楀垎鏋愬浘琛� -->
+ <el-card class="analysis-section">
+ <template #header>
+ <span>鑳借�楀垎鏋�</span>
+ </template>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div ref="energyChart" style="height: 400px;"></div>
+ </el-col>
+ <el-col :span="12">
+ <div ref="savingChart" style="height: 400px;"></div>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 浼樺寲鍘嗗彶瀵硅瘽妗� -->
+ <el-dialog v-model="historyDialogVisible" title="浼樺寲鍘嗗彶璁板綍" width="80%">
+ <el-table :data="optimizationHistory" style="width: 100%">
+ <el-table-column prop="timestamp" label="鏃堕棿" />
+ <el-table-column prop="formationPressure" label="鍦板眰鍘嬪姏 (MPa)" />
+ <el-table-column prop="oldFrequency" label="鍘熼鐜� (Hz)" />
+ <el-table-column prop="newFrequency" label="鏂伴鐜� (Hz)" />
+ <el-table-column prop="energySaved" label="鑺傝兘 (kWh)" />
+ <el-table-column prop="status" label="鐘舵��">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'success' ? 'success' : 'warning'">
+ {{ scope.row.status === 'success' ? '鎴愬姛' : '澶辫触' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Monitor, Cpu, TrendCharts } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+
+// 鍝嶅簲寮忔暟鎹�
+const edgeServerStatus = ref({ status: 'online', lastHeartbeat: Date.now() })
+const modelStatus = ref({ status: 'running', modelCount: 3 })
+const energySavingRate = ref(15.8)
+const totalEnergySaved = ref(1250.5)
+const pumpData = ref({
+ formationPressure: 25.6,
+ currentFrequency: 45.2,
+ optimizedFrequency: 42.1,
+ energyReduction: 23,
+ flowRate: 180.5,
+ power: 85.3
+})
+
+const modelConfigs = ref([
+ {
+ modelName: '娉ㄦ按娉甸鐜囦紭鍖栨ā鍨�',
+ version: 'v2.1.0',
+ status: 'active',
+ accuracy: '94.2%',
+ lastUpdate: '2025-01-15 14:30:00'
+ },
+ {
+ modelName: '鍦板眰鍘嬪姏棰勬祴妯″瀷',
+ version: 'v1.8.5',
+ status: 'active',
+ accuracy: '91.7%',
+ lastUpdate: '2025-01-14 09:15:00'
+ },
+ {
+ modelName: '鑳借�楀垎鏋愭ā鍨�',
+ version: 'v2.0.3',
+ status: 'standby',
+ accuracy: '89.3%',
+ lastUpdate: '2025-01-13 16:45:00'
+ }
+])
+
+const historyDialogVisible = ref(false)
+const optimizationHistory = ref([])
+
+// 鍥捐〃寮曠敤
+const frequencyChart = ref(null)
+const energyChart = ref(null)
+const savingChart = ref(null)
+
+// 鑷姩鍒锋柊鐩稿叧
+const autoRefreshStatus = ref(true)
+const autoRefreshTimer = ref(null)
+const chartInstances = ref([])
+
+// 璁$畻灞炴��
+const canControl = computed(() => {
+ return edgeServerStatus.value.status === 'online' && modelStatus.value.status === 'running'
+})
+
+const getProgressColor = computed(() => {
+ return (percentage) => {
+ if (percentage < 20) return '#909399'
+ if (percentage < 40) return '#E6A23C'
+ if (percentage < 60) return '#409EFF'
+ return '#67C23A'
+ }
+})
+
+// 鐢熸垚妯℃嫙鏁版嵁
+const generateMockData = () => {
+ // 鐢熸垚闅忔満鍦板眰鍘嬪姏 (20-30 MPa)
+ const formationPressure = 20 + Math.random() * 10
+
+ // 鏍规嵁鍦板眰鍘嬪姏璁$畻浼樺寲棰戠巼
+ const baseFrequency = 40 + (formationPressure - 25) * 2
+ const currentFrequency = baseFrequency + (Math.random() - 0.5) * 4
+ const optimizedFrequency = Math.max(35, baseFrequency - Math.random() * 3)
+
+ // 璁$畻鑳借�楅檷浣�
+ const energyReduction = Math.round((currentFrequency - optimizedFrequency) / currentFrequency * 100)
+
+ // 璁$畻娴侀噺鍜屽姛鐜�
+ const flowRate = 150 + Math.random() * 60
+ const power = 70 + Math.random() * 30
+
+ // 鏇存柊娉垫暟鎹�
+ pumpData.value = {
+ formationPressure: parseFloat(formationPressure.toFixed(1)),
+ currentFrequency: parseFloat(currentFrequency.toFixed(1)),
+ optimizedFrequency: parseFloat(optimizedFrequency.toFixed(1)),
+ energyReduction: Math.min(energyReduction, 35),
+ flowRate: parseFloat(flowRate.toFixed(1)),
+ power: parseFloat(power.toFixed(1))
+ }
+
+ // 鏇存柊鑺傝兘鏁堟灉
+ energySavingRate.value = 12 + Math.random() * 8
+ totalEnergySaved.value += Math.random() * 2
+
+ // 鏇存柊杈圭紭鏈嶅姟鍣ㄧ姸鎬�
+ edgeServerStatus.value.lastHeartbeat = Date.now()
+
+ // 闅忔満鏇存柊妯″瀷鐘舵��
+ if (Math.random() > 0.95) {
+ modelStatus.value.modelCount = Math.max(1, modelStatus.value.modelCount + (Math.random() > 0.5 ? 1 : -1))
+ }
+
+ // 娣诲姞浼樺寲鍘嗗彶璁板綍
+ if (Math.random() > 0.7) {
+ addOptimizationHistory()
+ }
+
+ // 鏇存柊鍥捐〃鏁版嵁
+ updateCharts()
+}
+
+// 娣诲姞浼樺寲鍘嗗彶璁板綍
+const addOptimizationHistory = () => {
+ const timestamp = new Date().toLocaleString()
+ const record = {
+ timestamp,
+ formationPressure: pumpData.value.formationPressure,
+ oldFrequency: pumpData.value.currentFrequency,
+ newFrequency: pumpData.value.optimizedFrequency,
+ energySaved: parseFloat((Math.random() * 5 + 1).toFixed(2)),
+ status: Math.random() > 0.1 ? 'success' : 'failed'
+ }
+
+ optimizationHistory.value.unshift(record)
+
+ // 淇濇寔鏈�澶�100鏉¤褰�
+ if (optimizationHistory.value.length > 100) {
+ optimizationHistory.value = optimizationHistory.value.slice(0, 100)
+ }
+}
+
+// 鏇存柊鍥捐〃鏁版嵁
+const updateCharts = () => {
+ chartInstances.value.forEach(instance => {
+ if (instance && instance.setOption) {
+ // 杩欓噷鍙互鏇存柊鍥捐〃鏁版嵁
+ // 涓轰簡绠�鍖栵紝鎴戜滑鍙槸閲嶆柊鍒濆鍖栧浘琛�
+ }
+ })
+}
+
+// 鏂规硶
+const refreshData = () => {
+ generateMockData()
+ ElMessage.success('鏁版嵁鍒锋柊鎴愬姛')
+}
+
+const applyOptimization = async () => {
+ try {
+ await ElMessageBox.confirm('纭畾瑕佸簲鐢ㄥ綋鍓嶇殑浼樺寲璁剧疆鍚楋紵', '纭鎿嶄綔', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+
+ // 搴旂敤浼樺寲璁剧疆
+ pumpData.value.currentFrequency = pumpData.value.optimizedFrequency
+ ElMessage.success('浼樺寲璁剧疆搴旂敤鎴愬姛')
+ refreshData()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error('搴旂敤浼樺寲璁剧疆澶辫触')
+ }
+ }
+}
+
+const emergencyStop = async () => {
+ try {
+ await ElMessageBox.confirm('纭畾瑕佺揣鎬ュ仠姝㈡墍鏈夋敞姘存车鍚楋紵', '绱ф�ユ搷浣�', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'error'
+ })
+
+ // 鎵ц绱ф�ュ仠姝㈤�昏緫
+ pumpData.value.currentFrequency = 0
+ pumpData.value.optimizedFrequency = 0
+ ElMessage.success('绱ф�ュ仠姝㈡墽琛屾垚鍔�')
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error('绱ф�ュ仠姝㈡墽琛屽け璐�')
+ }
+ }
+}
+
+const showOptimizationHistory = () => {
+ historyDialogVisible.value = true
+}
+
+const updateModel = (model) => {
+ ElMessage.info(`鏇存柊妯″瀷: ${model.modelName}`)
+}
+
+const deleteModel = async (model) => {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄ゆā鍨� ${model.modelName} 鍚楋紵`, '纭鍒犻櫎', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+
+ const index = modelConfigs.value.findIndex(m => m.modelName === model.modelName)
+ if (index > -1) {
+ modelConfigs.value.splice(index, 1)
+ ElMessage.success('妯″瀷鍒犻櫎鎴愬姛')
+ }
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error('妯″瀷鍒犻櫎澶辫触')
+ }
+ }
+}
+
+const toggleAutoRefresh = () => {
+ autoRefreshStatus.value = !autoRefreshStatus.value
+ if (autoRefreshStatus.value) {
+ startAutoRefresh()
+ ElMessage.success('鑷姩鍒锋柊宸插紑鍚�')
+ } else {
+ stopAutoRefresh()
+ ElMessage.info('鑷姩鍒锋柊宸插叧闂�')
+ }
+}
+
+const startAutoRefresh = () => {
+ stopAutoRefresh() // 鍏堝仠姝箣鍓嶇殑瀹氭椂鍣�
+ autoRefreshTimer.value = setInterval(() => {
+ generateMockData()
+ }, 60000) // 1鍒嗛挓 = 60000姣
+}
+
+const stopAutoRefresh = () => {
+ if (autoRefreshTimer.value) {
+ clearInterval(autoRefreshTimer.value)
+ autoRefreshTimer.value = null
+ }
+}
+
+const formatTime = (timestamp) => {
+ return new Date(timestamp).toLocaleTimeString()
+}
+
+// 鍒濆鍖栧浘琛�
+const initCharts = () => {
+ // 棰戠巼浼樺寲瓒嬪娍鍥�
+ const frequencyChartInstance = echarts.init(frequencyChart.value)
+ const frequencyOption = {
+ title: { text: '娉甸鐜囦紭鍖栬秼鍔�' },
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['褰撳墠棰戠巼', '浼樺寲棰戠巼', '鍦板眰鍘嬪姏'] },
+ xAxis: { type: 'category', data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00'] },
+ yAxis: [
+ { type: 'value', name: '棰戠巼 (Hz)' },
+ { type: 'value', name: '鍘嬪姏 (MPa)' }
+ ],
+ series: [
+ {
+ name: '褰撳墠棰戠巼',
+ type: 'line',
+ data: [45.2, 44.8, 45.5, 45.1, 44.9, 45.2]
+ },
+ {
+ name: '浼樺寲棰戠巼',
+ type: 'line',
+ data: [42.1, 41.8, 42.3, 41.9, 41.7, 42.1]
+ },
+ {
+ name: '鍦板眰鍘嬪姏',
+ type: 'line',
+ yAxisIndex: 1,
+ data: [25.6, 25.8, 26.1, 25.9, 25.7, 25.6]
+ }
+ ]
+ }
+ frequencyChartInstance.setOption(frequencyOption)
+ chartInstances.value.push(frequencyChartInstance)
+
+ // 鑳借�楀垎鏋愬浘
+ const energyChartInstance = echarts.init(energyChart.value)
+ const energyOption = {
+ title: { text: '鏃ヨ兘鑰楀姣�' },
+ tooltip: { trigger: 'item' },
+ legend: { orient: 'vertical', left: 'left',top: 'center' },
+ series: [
+ {
+ name: '鑳借�楀垎甯�',
+ type: 'pie',
+ radius: '50%',
+ data: [
+ { value: 45, name: '娉ㄦ按娉�' },
+ { value: 25, name: '鐓ф槑绯荤粺' },
+ { value: 20, name: '閫氶绯荤粺' },
+ { value: 10, name: '鍏朵粬璁惧' }
+ ]
+ }
+ ]
+ }
+ energyChartInstance.setOption(energyOption)
+ chartInstances.value.push(energyChartInstance)
+
+ // 鑺傝兘鏁堟灉鍥�
+ const savingChartInstance = echarts.init(savingChart.value)
+ const savingOption = {
+ title: { text: '鑺傝兘鏁堟灉瓒嬪娍' },
+ tooltip: { trigger: 'axis' },
+ xAxis: { type: 'category', data: ['鍛ㄤ竴', '鍛ㄤ簩', '鍛ㄤ笁', '鍛ㄥ洓', '鍛ㄤ簲', '鍛ㄥ叚', '鍛ㄦ棩'] },
+ yAxis: { type: 'value', name: '鑺傝兘鐜� (%)' },
+ series: [
+ {
+ name: '鑺傝兘鐜�',
+ type: 'bar',
+ data: [12.5, 15.2, 18.7, 16.3, 19.1, 17.8, 15.8]
+ }
+ ]
+ }
+ savingChartInstance.setOption(savingOption)
+ chartInstances.value.push(savingChartInstance)
+}
+
+// 鐢熸垚鍒濆鍘嗗彶鏁版嵁
+const generateInitialHistory = () => {
+ for (let i = 0; i < 20; i++) {
+ const timestamp = new Date(Date.now() - i * 3600000).toLocaleString()
+ const record = {
+ timestamp,
+ formationPressure: parseFloat((20 + Math.random() * 10).toFixed(1)),
+ oldFrequency: parseFloat((40 + Math.random() * 10).toFixed(1)),
+ newFrequency: parseFloat((35 + Math.random() * 8).toFixed(1)),
+ energySaved: parseFloat((Math.random() * 5 + 1).toFixed(2)),
+ status: Math.random() > 0.1 ? 'success' : 'failed'
+ }
+ optimizationHistory.value.push(record)
+ }
+}
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ initCharts()
+ generateInitialHistory()
+ refreshData()
+ if (autoRefreshStatus.value) {
+ startAutoRefresh()
+ }
+})
+
+onUnmounted(() => {
+ stopAutoRefresh()
+ chartInstances.value.forEach(instance => {
+ if (instance && instance.dispose) {
+ instance.dispose()
+ }
+ })
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+
+
+.status-section {
+ margin-bottom: 20px;
+}
+
+.status-card {
+ height: 140px;
+}
+
+.status-item {
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
+.status-icon {
+ font-size: 48px;
+ margin-right: 20px;
+ color: #409EFF;
+}
+
+.status-info {
+ flex: 1;
+}
+
+.status-title {
+ font-size: 14px;
+ color: #909399;
+ margin-bottom: 8px;
+}
+
+.status-value {
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 8px;
+}
+
+.status-detail {
+ font-size: 12px;
+ color: #909399;
+}
+
+.status-value.online,
+.status-value.running {
+ color: #67C23A;
+}
+
+.status-value.offline,
+.status-value.stopped {
+ color: #F56C6C;
+}
+
+.status-value.success {
+ color: #67C23A;
+}
+
+.control-section,
+.model-section,
+.analysis-section {
+ margin-bottom: 20px;
+}
+
+.pump-control h4,
+.pump-chart h4 {
+ margin-bottom: 20px;
+ color: #303133;
+}
+
+.control-buttons {
+ margin-top: 20px;
+ text-align: center;
+}
+
+.control-buttons .el-button {
+ margin: 0 10px;
+}
+</style>
diff --git a/src/views/energyManagement/energyArea/index.vue b/src/views/energyManagement/energyArea/index.vue
new file mode 100644
index 0000000..63feff8
--- /dev/null
+++ b/src/views/energyManagement/energyArea/index.vue
@@ -0,0 +1,511 @@
+<template>
+ <div class="app-container product-view">
+ <div class="left">
+ <div>
+ <el-input
+ v-model="search"
+ style="width: 210px"
+ placeholder="杈撳叆鍏抽敭瀛楄繘琛屾悳绱�"
+ @change="searchFilter"
+ @clear="searchFilter"
+ clearable
+ prefix-icon="Search"
+ />
+ <el-button
+ type="primary"
+ @click="openProDia('addOne')"
+ style="margin-left: 10px"
+ >鏂板鐖跺尯鍩�</el-button
+ >
+ </div>
+ <div ref="containerRef">
+ <el-tree
+ ref="tree"
+ v-loading="treeLoad"
+ :data="list"
+ @node-click="handleNodeClick"
+ :expand-on-click-node="false"
+ default-expand-all
+ :default-expanded-keys="expandedKeys"
+ :draggable="true"
+ :filter-node-method="filterNode"
+ :props="{ children: 'children', label: 'label' }"
+ highlight-current
+ node-key="id"
+ style="
+ height: calc(100vh - 190px);
+ overflow-y: scroll;
+ scrollbar-width: none;
+ margin-top: 10px;
+ "
+ >
+ <template #default="{ node, data }">
+ <div class="custom-tree-node">
+ <span class="tree-node-content">
+ <el-icon class="orange-icon">
+ <component :is="data.children && data.children.length > 0
+ ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
+ </el-icon>
+ {{ data.label }}
+ </span>
+ <div>
+ <el-button
+ type="primary"
+ link
+ @click="openProDia('edit', data)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button type="primary" link @click="openModelDia('add','', data.id)">
+ 娣诲姞瀛愬尯鍩�
+ </el-button>
+ <el-button
+ v-if="!node.childNodes.length"
+ style="margin-left: 4px"
+ type="danger"
+ link
+ @click="remove(node, data)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ </template>
+ </el-tree>
+ </div>
+ </div>
+ <div class="right">
+ <div style="margin-bottom: 10px" v-if="isShowButton">
+ <el-button type="primary" @click="openModelDia('add')">
+ 鏂板瀛愬尯鍩�
+ </el-button>
+ <el-button
+ type="danger"
+ @click="handleDelete"
+ style="margin-left: 10px"
+ plain
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ <el-dialog v-model="productDia" title="鍖哄煙" width="400px" @keydown.enter.prevent>
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="鍖哄煙鍚嶇О锛�" prop="areaName">
+ <el-input
+ v-model="form.areaName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @keydown.enter.prevent
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeProDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <el-dialog
+ v-model="modelDia"
+ title="瀛愬尯鍩�"
+ width="400px"
+ @close="closeModelDia"
+ @keydown.enter.prevent
+ >
+ <el-form
+ :model="modelForm"
+ label-width="140px"
+ label-position="top"
+ :rules="modelRules"
+ ref="modelFormRef"
+ >
+ <el-form-item label="鐖跺尯鍩燂細" prop="fuId">
+ <el-cascader v-model="modelForm.fuId" :options="list" :props="{
+ value: 'id',
+ label: 'label',
+ children: 'children',
+ checkStrictly: true,
+ }" />
+ </el-form-item>
+ <el-form-item label="鍖哄煙绫诲瀷锛�" prop="areaType">
+ <el-select v-model="modelForm.areaType" placeholder="璇烽�夋嫨">
+ <el-option v-for="item in area_type" :key="item.value" :label="item.label" :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍖哄煙鍚嶇О锛�" prop="areaName">
+ <el-input
+ v-model="modelForm.areaName"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable
+ @keydown.enter.prevent
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitModelForm">纭</el-button>
+ <el-button @click="closeModelDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { ElMessageBox } from "element-plus";
+import {
+ areaAdd,
+ areaDelete,
+ areaListPage,
+ areaListTree,
+} from "@/api/energyManagement/index.js";
+
+const { proxy } = getCurrentInstance();
+const tree = ref(null);
+const containerRef = ref(null);
+
+const productDia = ref(false);
+const modelDia = ref(false);
+const modelOperationType = ref("");
+const search = ref("");
+const currentId = ref("");
+const currentParentId = ref("");
+const operationType = ref("");
+const treeLoad = ref(false);
+const list = ref([]);
+const expandedKeys = ref([]);
+const {area_type} = proxy.useDict("area_type")
+const tableColumn = ref([
+ {
+ label: "鍖哄煙鍚嶇О",
+ prop: "areaName",
+ },
+ {
+ label: "鍖哄煙绫诲瀷",
+ prop: "areaType",
+ dataType: "tag",
+ formatData: (row) => {
+ return area_type.value.find(item => item.value == row)?.label;
+ }
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openModelDia("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+const isShowButton = ref(false);
+const selectedRows = ref([]);
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+const data = reactive({
+ form: {
+ areaName: "",
+ },
+ rules: {
+ areaName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ },
+ modelForm: {
+ areaName: "",
+ fuId: "",
+ },
+ modelRules: {
+ areaName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ fuId: [{ required: true, message: "璇疯緭鍏�", trigger: "change" }],
+ },
+});
+const { form, rules, modelForm, modelRules } = toRefs(data);
+
+// 鏌ヨ浜у搧鏍�
+const getProductTreeList = () => {
+ treeLoad.value = true;
+ areaListTree()
+ .then((res) => {
+ list.value = res;
+ list.value.forEach((a) => {
+ expandedKeys.value.push(a.label);
+ });
+ treeLoad.value = false;
+ })
+ .catch((err) => {
+ treeLoad.value = false;
+ });
+};
+// 杩囨护浜у搧鏍�
+const searchFilter = () => {
+ proxy.$refs.tree.filter(search.value);
+};
+// 鎵撳紑浜у搧寮规
+const openProDia = (type, data) => {
+ operationType.value = type;
+ productDia.value = true;
+ form.value.areaName = "";
+ if (type === "edit") {
+ form.value.areaName = data.areaName;
+ }
+};
+// 鎵撳紑瑙勬牸鍨嬪彿寮规
+const openModelDia = (type, data,fatherId) => {
+ modelOperationType.value = type;
+ modelDia.value = true;
+ modelForm.value.fuId = "";
+ modelForm.value.areaType = "";
+ modelForm.value.areaName = "";
+ modelForm.value.id = "";
+ modelForm.value.fuId = fatherId;
+ if (type === "edit") {
+ modelForm.value = { ...data };
+ }
+};
+// 鎻愪氦浜у搧鍚嶇О淇敼
+const submitForm = () => {
+ proxy.$refs.formRef.validate((valid) => {
+ if (valid) {
+ if (operationType.value === "add") {
+ form.value.parentId = currentId.value;
+ form.value.id = "";
+ } else if (operationType.value === "addOne") {
+ form.value.id = "";
+ form.value.parentId = "";
+ } else {
+ form.value.id = currentId.value;
+ form.value.parentId = "";
+ }
+ areaAdd(form.value).then((res) => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeProDia();
+ getProductTreeList();
+ });
+ }
+ });
+};
+// 鍏抽棴浜у搧寮规
+const closeProDia = () => {
+ proxy.$refs.formRef.resetFields();
+ productDia.value = false;
+};
+
+// 鍒犻櫎浜у搧
+const remove = (node, data) => {
+ let ids = [];
+ ids.push(data.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ areaDelete(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getProductTreeList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 閫夋嫨浜у搧
+const handleNodeClick = (val, node, el) => {
+ // 鍒ゆ柇鏄惁涓哄彾瀛愯妭鐐�
+ isShowButton.value = !(val.children && val.children.length > 0);
+ // 鍙湁鍙跺瓙鑺傜偣鎵嶆墽琛屼互涓嬮�昏緫
+ currentId.value = val.id;
+ currentParentId.value = val.parentId;
+ getModelList(true);
+};
+
+// 鎻愪氦瑙勬牸鍨嬪彿淇敼
+const submitModelForm = () => {
+ proxy.$refs.modelFormRef.validate((valid) => {
+ if (valid) {
+ modelForm.value.fuId = currentId.value;
+ areaAdd(modelForm.value).then((res) => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeModelDia();
+ getModelList();
+ getProductTreeList();
+ });
+ }
+ });
+};
+// 鍏抽棴鍨嬪彿寮规
+const closeModelDia = () => {
+ proxy.$refs.modelFormRef.resetFields();
+ modelDia.value = false;
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鏌ヨ瑙勬牸鍨嬪彿
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getModelList();
+};
+const getModelList = (val = false) => {
+ tableLoading.value = true;
+ let obj = {
+ id: currentId.value,
+ fuId:currentId.value,
+ current: page.current,
+ size: page.size
+ }
+ if(val){
+ delete obj.id;
+ }else{
+ delete obj.fuId
+ }
+ areaListPage(obj).then((res) => {
+ console.log("res", res);
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ tableLoading.value = false;
+ });
+};
+// 鍒犻櫎瑙勬牸鍨嬪彿
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ areaDelete(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getModelList();
+ getProductTreeList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 璋冪敤tree杩囨护鏂规硶 涓枃鑻辫繃婊�
+const filterNode = (value, data, node) => {
+ if (!value) {
+ //濡傛灉鏁版嵁涓虹┖锛屽垯杩斿洖true,鏄剧ず鎵�鏈夌殑鏁版嵁椤�
+ return true;
+ }
+ // 鏌ヨ鍒楄〃鏄惁鏈夊尮閰嶆暟鎹紝灏嗗�煎皬鍐欙紝鍖归厤鑻辨枃鏁版嵁
+ let val = value.toLowerCase();
+ return chooseNode(val, data, node); // 璋冪敤杩囨护浜屽眰鏂规硶
+};
+// 杩囨护鐖惰妭鐐� / 瀛愯妭鐐� (濡傛灉杈撳叆鐨勫弬鏁版槸鐖惰妭鐐逛笖鑳藉尮閰嶏紝鍒欒繑鍥炶鑺傜偣浠ュ強鍏朵笅鐨勬墍鏈夊瓙鑺傜偣锛涘鏋滃弬鏁版槸瀛愯妭鐐癸紝鍒欒繑鍥炶鑺傜偣鐨勭埗鑺傜偣銆俷ame鏄腑鏂囧瓧绗︼紝enName鏄嫳鏂囧瓧绗�.
+const chooseNode = (value, data, node) => {
+ if (data.label.indexOf(value) !== -1) {
+ return true;
+ }
+ const level = node.level;
+ // 濡傛灉浼犲叆鐨勮妭鐐规湰韬氨鏄竴绾ц妭鐐瑰氨涓嶇敤鏍¢獙浜�
+ if (level === 1) {
+ return false;
+ }
+ // 鍏堝彇褰撳墠鑺傜偣鐨勭埗鑺傜偣
+ let parentData = node.parent;
+ // 閬嶅巻褰撳墠鑺傜偣鐨勭埗鑺傜偣
+ let index = 0;
+ while (index < level - 1) {
+ // 濡傛灉鍖归厤鍒扮洿鎺ヨ繑鍥烇紝姝ゅname鍊兼槸涓枃瀛楃锛宔nName鏄嫳鏂囧瓧绗︺�傚垽鏂尮閰嶄腑鑻辨枃杩囨护
+ if (parentData.data.label.indexOf(value) !== -1) {
+ return true;
+ }
+ // 鍚﹀垯鐨勮瘽鍐嶅線涓婁竴灞傚仛鍖归厤
+ parentData = parentData.parent;
+ index++;
+ }
+ // 娌″尮閰嶅埌杩斿洖false
+ return false;
+};
+getProductTreeList();
+</script>
+
+<style scoped>
+.product-view {
+ display: flex;
+}
+.left {
+ width: 380px;
+ padding: 16px;
+ background: #ffffff;
+}
+.right {
+ width: calc(100% - 380px);
+ padding: 16px;
+ margin-left: 20px;
+ background: #ffffff;
+}
+.custom-tree-node {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ padding-right: 8px;
+}
+.tree-node-content {
+ display: flex;
+ align-items: center; /* 鍨傜洿灞呬腑 */
+ height: 100%;
+}
+.orange-icon {
+ color: orange;
+ font-size: 18px;
+ margin-right: 8px; /* 鍥炬爣涓庢枃瀛椾箣闂村姞鐐归棿璺� */
+}
+</style>
diff --git a/src/views/energyManagement/energyCockpit/index.vue b/src/views/energyManagement/energyCockpit/index.vue
new file mode 100644
index 0000000..9281e37
--- /dev/null
+++ b/src/views/energyManagement/energyCockpit/index.vue
@@ -0,0 +1,1380 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <h2>鑳芥簮椹鹃┒鑸�</h2>
+ <div class="header-info">
+ <span class="update-time">鏈�鍚庢洿鏂帮細{{ lastUpdateTime }}</span>
+ <el-button type="primary" size="small" @click="refreshData">
+ <el-icon><Refresh /></el-icon>
+ 鍒锋柊鏁版嵁
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 瀹炴椂鑳借�楃洃鎺� -->
+ <div class="real-time-monitor">
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-card class="monitor-card">
+ <template #header>
+ <div class="card-header">
+ <span>鐢靛姏娑堣��</span>
+ <el-tag type="success" size="small">瀹炴椂</el-tag>
+ </div>
+ </template>
+ <div class="monitor-content">
+ <div class="monitor-value">
+ <span class="value">{{ electricityConsumption }}</span>
+ <span class="unit">kW路h</span>
+ </div>
+ <div class="monitor-trend">
+ <span class="trend-label">瓒嬪娍锛�</span>
+ <el-tag :type="getTrendType(electricityTrend)" size="small">
+ {{ electricityTrend > 0 ? '鈫�' : '鈫�' }} {{ Math.abs(electricityTrend) }}%
+ </el-tag>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="8">
+ <el-card class="monitor-card">
+ <template #header>
+ <div class="card-header">
+ <span>姘存秷鑰�</span>
+ <el-tag type="primary" size="small">瀹炴椂</el-tag>
+ </div>
+ </template>
+ <div class="monitor-content">
+ <div class="monitor-value">
+ <span class="value">{{ waterConsumption }}</span>
+ <span class="unit">m鲁</span>
+ </div>
+ <div class="monitor-trend">
+ <span class="trend-label">瓒嬪娍锛�</span>
+ <el-tag :type="getTrendType(waterTrend)" size="small">
+ {{ waterTrend > 0 ? '鈫�' : '鈫�' }} {{ Math.abs(waterTrend) }}%
+ </el-tag>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="8">
+ <el-card class="monitor-card">
+ <template #header>
+ <div class="card-header">
+ <span>姘斾綋娑堣��</span>
+ <el-tag type="warning" size="small">瀹炴椂</el-tag>
+ </div>
+ </template>
+ <div class="monitor-content">
+ <div class="monitor-value">
+ <span class="value">{{ gasConsumption }}</span>
+ <span class="unit">m鲁</span>
+ </div>
+ <div class="monitor-trend">
+ <span class="trend-label">瓒嬪娍锛�</span>
+ <el-tag :type="getTrendType(gasTrend)" size="small">
+ {{ gasTrend > 0 ? '鈫�' : '鈫�' }} {{ Math.abs(gasTrend) }}%
+ </el-tag>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 鑳借�楄秼鍔垮垎鏋� -->
+ <div class="trend-analysis">
+ <el-card>
+ <template #header>
+ <div class="card-header">
+ <span>鑳借�楄秼鍔垮垎鏋�</span>
+ <div class="time-selector">
+ <el-radio-group v-model="trendTimeUnit" @change="handleTrendTimeChange">
+ <el-radio value="hour">灏忔椂</el-radio>
+ <el-radio value="day">鏃�</el-radio>
+ <el-radio value="week">鍛�</el-radio>
+ <el-radio value="month">鏈�</el-radio>
+ <el-radio value="year">骞�</el-radio>
+ </el-radio-group>
+ </div>
+ </div>
+ </template>
+ <div class="chart-container">
+ <div ref="trendChart" style="width: 100%; height: 400px;"></div>
+ </div>
+ </el-card>
+ </div>
+
+ <!-- 鑳借�楃粺璁′笌鎺掑悕 -->
+ <div class="statistics-ranking">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-card class="statistics-card">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">鑳借�楃粺璁℃姤琛�</span>
+ <div class="header-actions">
+ <el-select v-model="statisticsPeriod" @change="handleStatisticsChange" size="small" style="width: 100px;">
+ <el-option label="鏃ョ粺璁�" value="day" />
+ <el-option label="鍛ㄧ粺璁�" value="week" />
+ <el-option label="鏈堢粺璁�" value="month" />
+ <el-option label="骞寸粺璁�" value="year" />
+ </el-select>
+ </div>
+ </div>
+ </template>
+ <div class="statistics-content">
+ <div class="statistics-item">
+ <span class="label">鎬昏兘鑰楋細</span>
+ <span class="value">{{ totalEnergyConsumption }} kW路h</span>
+ </div>
+ <div class="statistics-item">
+ <span class="label">鍚屾瘮锛�</span>
+ <span class="value" :class="getComparisonClass(yearOverYear)">
+ {{ yearOverYear > 0 ? '+' : '' }}{{ yearOverYear }}%
+ </span>
+ </div>
+ <div class="statistics-item">
+ <span class="label">鐜瘮锛�</span>
+ <span class="value" :class="getComparisonClass(monthOverMonth)">
+ {{ monthOverMonth > 0 ? '+' : '' }}{{ monthOverMonth }}%
+ </span>
+ </div>
+ <div class="statistics-item">
+ <span class="label">鑺傝兘鐜囷細</span>
+ <span class="value success">{{ energySavingRate }}%</span>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="12">
+ <el-card class="ranking-card">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">鑳借�楁帓鍚�</span>
+ <el-select v-model="rankingType" @change="handleRankingChange" size="small" style="width: 120px;">
+ <el-option label="閮ㄩ棬鎺掑悕" value="department" />
+ <el-option label="杞﹂棿鎺掑悕" value="workshop" />
+ <el-option label="璁惧鎺掑悕" value="equipment" />
+ </el-select>
+ </div>
+ </template>
+ <div class="ranking-list">
+ <div v-for="(item, index) in rankingList" :key="index" class="ranking-item">
+ <div class="ranking-number" :class="getRankingClass(index + 1)">{{ index + 1 }}</div>
+ <div class="ranking-info">
+ <div class="ranking-name">{{ item.name }}</div>
+ <div class="ranking-value">{{ item.value }} kW路h</div>
+ </div>
+ <div class="ranking-trend">
+ <el-tag :type="getTrendType(item.trend)" size="small">
+ {{ item.trend > 0 ? '鈫�' : '鈫�' }} {{ Math.abs(item.trend) }}%
+ </el-tag>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 寮傚父鍒嗘瀽涓庢櫤鑳芥帶鍒� -->
+ <div class="analysis-control">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-card class="abnormal-card">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">寮傚父鍒嗘瀽</span>
+ <el-tag type="danger" size="small">{{ abnormalCount }}涓紓甯�</el-tag>
+ </div>
+ </template>
+ <div class="abnormal-list">
+ <div v-for="(item, index) in abnormalList" :key="index" class="abnormal-item">
+ <div class="abnormal-icon">
+ <el-icon :color="getAbnormalColor(item.level)">
+ <Warning v-if="item.level === 'warning'" />
+ <CircleClose v-else />
+ </el-icon>
+ </div>
+ <div class="abnormal-content">
+ <div class="abnormal-title">{{ item.title }}</div>
+ <div class="abnormal-desc">{{ item.description }}</div>
+ <div class="abnormal-time">{{ item.time }}</div>
+ </div>
+ <div class="abnormal-action">
+ <el-button link size="small" @click="handleAbnormal(item)">澶勭悊</el-button>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="12">
+ <el-card class="control-card">
+ <template #header>
+ <div class="card-header">
+ <span class="card-title">鏅鸿兘鎺у埗绯荤粺</span>
+ <el-switch v-model="autoControlEnabled" @change="handleAutoControlChange" />
+ </div>
+ </template>
+ <div class="control-content">
+ <div class="control-item">
+ <span class="label">宄拌胺骞崇數浠风鐞嗭細</span>
+ <el-tag :type="getPriceType(currentPriceType)" size="small">
+ {{ getPriceTypeText(currentPriceType) }}
+ </el-tag>
+ </div>
+ <div class="control-item">
+ <span class="label">璐熻嵎棰勬祴锛�</span>
+ <span class="value">{{ loadForecast }} kW</span>
+ </div>
+ <div class="control-item">
+ <span class="label">鑷姩鍚仠锛�</span>
+ <el-tag :type="autoStartStop ? 'success' : 'info'" size="small">
+ {{ autoStartStop ? '宸插惎鐢�' : '宸茬鐢�' }}
+ </el-tag>
+ </div>
+ <div class="control-item">
+ <span class="label">鏅鸿兘璋冭妭锛�</span>
+ <el-progress :percentage="intelligentAdjustment" :color="getProgressColor" />
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 鐜繚鎸囨爣 -->
+ <div class="environmental-indicators">
+ <el-card>
+ <template #header>
+ <div class="card-header">
+ <span>鐜繚鎸囨爣鐩戞帶</span>
+ </div>
+ </template>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <div class="indicator-item">
+ <div class="indicator-title">纰虫帓鏀鹃噺</div>
+ <div class="indicator-value">{{ carbonEmission }} kg</div>
+ <div class="indicator-trend">
+ <span>鍚屾瘮锛�</span>
+ <span :class="getComparisonClass(carbonEmissionTrend)">
+ {{ carbonEmissionTrend > 0 ? '+' : '' }}{{ carbonEmissionTrend }}%
+ </span>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="indicator-item">
+ <div class="indicator-title">鐜繚杈炬爣鐜�</div>
+ <div class="indicator-value">{{ environmentalCompliance }}%</div>
+ <div class="indicator-trend">
+ <span>鐩爣锛�</span>
+ <span class="success">95%</span>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="indicator-item">
+ <div class="indicator-title">缁胯壊鑳芥簮鍗犳瘮</div>
+ <div class="indicator-value">{{ greenEnergyRatio }}%</div>
+ <div class="indicator-trend">
+ <span>鐩爣锛�</span>
+ <span class="success">30%</span>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+ </div>
+
+ <!-- 澶氱淮搴︽姤琛� -->
+ <div class="multi-dimensional-reports">
+ <el-card>
+ <template #header>
+ <div class="card-header">
+ <span>澶氱淮搴︽姤琛�</span>
+ </div>
+ </template>
+ <div class="report-filters">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鏃堕棿缁村害">
+ <el-select v-model="reportTimeDimension" placeholder="閫夋嫨鏃堕棿缁村害">
+ <el-option label="灏忔椂" value="hour" />
+ <el-option label="鏃�" value="day" />
+ <el-option label="鍛�" value="week" />
+ <el-option label="鏈�" value="month" />
+ <el-option label="骞�" value="year" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閮ㄩ棬缁村害">
+ <el-select v-model="reportDepartmentDimension" placeholder="閫夋嫨閮ㄩ棬">
+ <el-option label="鍏ㄩ儴閮ㄩ棬" value="all" />
+ <el-option label="鐢熶骇閮�" value="production" />
+ <el-option label="鎶�鏈儴" value="technology" />
+ <el-option label="琛屾斂閮�" value="administration" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁惧缁村害">
+ <el-select v-model="reportEquipmentDimension" placeholder="閫夋嫨璁惧绫诲瀷">
+ <el-option label="鍏ㄩ儴璁惧" value="all" />
+ <el-option label="鐢靛姏璁惧" value="electricity" />
+ <el-option label="姘村鐞嗚澶�" value="water" />
+ <el-option label="姘斾綋璁惧" value="gas" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item>
+ <el-button type="primary" @click="generateReport">鐢熸垚鎶ヨ〃</el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ <div class="report-preview">
+ <div class="report-data">
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <div class="data-card">
+ <div class="data-title">鐢靛姏娑堣��</div>
+ <div class="data-value">{{ reportData.electricity }} kW路h</div>
+ <div class="data-trend">
+ <span :class="getTrendClass(reportData.electricityTrend)">
+ {{ reportData.electricityTrend > 0 ? '鈫�' : '鈫�' }} {{ Math.abs(reportData.electricityTrend) }}%
+ </span>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="data-card">
+ <div class="data-title">姘存秷鑰�</div>
+ <div class="data-value">{{ reportData.water }} m鲁</div>
+ <div class="data-trend">
+ <span :class="getTrendClass(reportData.waterTrend)">
+ {{ reportData.waterTrend > 0 ? '鈫�' : '鈫�' }} {{ Math.abs(reportData.waterTrend) }}%
+ </span>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="data-card">
+ <div class="data-title">姘斾綋娑堣��</div>
+ <div class="data-value">{{ reportData.gas }} m鲁</div>
+ <div class="data-trend">
+ <span :class="getTrendClass(reportData.gasTrend)">
+ {{ reportData.gasTrend > 0 ? '鈫�' : '鈫�' }} {{ Math.abs(reportData.gasTrend) }}%
+ </span>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+
+ <div class="report-chart">
+ <div class="chart-title">鑳借�楄秼鍔垮浘</div>
+ <div class="chart-bars">
+ <div v-for="(item, index) in reportData.chartData" :key="index" class="chart-bar">
+ <div class="bar-label">{{ item.label }}</div>
+ <div class="bar-container">
+ <div class="bar-fill" :style="{ height: item.percentage + '%', backgroundColor: item.color }"></div>
+ </div>
+ <div class="bar-value">{{ item.value }}</div>
+ </div>
+ </div>
+ </div>
+
+ <div class="report-summary">
+ <div class="summary-item">
+ <span class="summary-label">鎬昏兘鑰楋細</span>
+ <span class="summary-value">{{ reportData.totalEnergy }} kW路h</span>
+ </div>
+ <div class="summary-item">
+ <span class="summary-label">骞冲潎鑳借�楋細</span>
+ <span class="summary-value">{{ reportData.averageEnergy }} kW路h</span>
+ </div>
+ <div class="summary-item">
+ <span class="summary-label">鑳借�楁晥鐜囷細</span>
+ <span class="summary-value">{{ reportData.efficiency }}%</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import * as echarts from 'echarts'
+import {
+ Refresh,
+ Download,
+ Warning,
+ CircleClose,
+ Document,
+ Edit,
+ Bell
+} from '@element-plus/icons-vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const lastUpdateTime = ref('')
+const electricityConsumption = ref(0)
+const waterConsumption = ref(0)
+const gasConsumption = ref(0)
+const electricityTrend = ref(0)
+const waterTrend = ref(0)
+const gasTrend = ref(0)
+
+// 瓒嬪娍鍒嗘瀽
+const trendTimeUnit = ref('day')
+const trendChart = ref(null)
+let chartInstance = null
+
+// 缁熻鎶ヨ〃
+const statisticsPeriod = ref('month')
+const totalEnergyConsumption = ref(0)
+const yearOverYear = ref(0)
+const monthOverMonth = ref(0)
+const energySavingRate = ref(0)
+
+// 鑳借�楁帓鍚�
+const rankingType = ref('department')
+const rankingList = ref([])
+
+// 寮傚父鍒嗘瀽
+const abnormalCount = ref(0)
+const abnormalList = ref([])
+
+// 鏅鸿兘鎺у埗
+const autoControlEnabled = ref(true)
+const currentPriceType = ref('peak')
+const loadForecast = ref(0)
+const autoStartStop = ref(true)
+const intelligentAdjustment = ref(0)
+
+// 鐜繚鎸囨爣
+const carbonEmission = ref(0)
+const carbonEmissionTrend = ref(0)
+const environmentalCompliance = ref(0)
+const greenEnergyRatio = ref(0)
+
+// 澶氱淮搴︽姤琛�
+const reportTimeDimension = ref('month')
+const reportDepartmentDimension = ref('all')
+const reportEquipmentDimension = ref('all')
+const reportData = ref({
+ electricity: 0,
+ water: 0,
+ gas: 0,
+ electricityTrend: 0,
+ waterTrend: 0,
+ gasTrend: 0,
+ totalEnergy: 0,
+ averageEnergy: 0,
+ efficiency: 0,
+ chartData: []
+})
+
+// 瀹氭椂鍣�
+let updateTimer = null
+
+// 鑾峰彇瓒嬪娍绫诲瀷鏍峰紡
+const getTrendType = (trend) => {
+ if (trend > 0) return 'danger'
+ if (trend < 0) return 'success'
+ return 'info'
+}
+
+// 鑾峰彇瀵规瘮绫诲瀷鏍峰紡
+const getComparisonClass = (value) => {
+ if (value > 0) return 'danger'
+ if (value < 0) return 'success'
+ return 'info'
+}
+
+// 鑾峰彇鎺掑悕鏍峰紡
+const getRankingClass = (rank) => {
+ if (rank === 1) return 'ranking-first'
+ if (rank === 2) return 'ranking-second'
+ if (rank === 3) return 'ranking-third'
+ return 'ranking-normal'
+}
+
+// 鑾峰彇寮傚父棰滆壊
+const getAbnormalColor = (level) => {
+ return level === 'warning' ? '#E6A23C' : '#F56C6C'
+}
+
+// 鑾峰彇鐢典环绫诲瀷鏍峰紡
+const getPriceType = (type) => {
+ const typeMap = {
+ peak: 'danger',
+ normal: 'warning',
+ valley: 'success'
+ }
+ return typeMap[type] || 'info'
+}
+
+// 鑾峰彇鐢典环绫诲瀷鏂囨湰
+const getPriceTypeText = (type) => {
+ const typeMap = {
+ peak: '宄版椂',
+ normal: '骞虫椂',
+ valley: '璋锋椂'
+ }
+ return typeMap[type] || '鏈煡'
+}
+
+// 鑾峰彇杩涘害鏉¢鑹�
+const getProgressColor = (percentage) => {
+ if (percentage < 50) return '#67C23A'
+ if (percentage < 80) return '#E6A23C'
+ return '#F56C6C'
+}
+
+// 鑾峰彇瓒嬪娍鏍峰紡
+const getTrendClass = (trend) => {
+ if (trend > 0) return 'trend-up'
+ if (trend < 0) return 'trend-down'
+ return 'trend-stable'
+}
+
+// 妯℃嫙鏁版嵁鐢熸垚
+const generateMockData = () => {
+ // 瀹炴椂鑳借�楁暟鎹�
+ electricityConsumption.value = Math.floor(Math.random() * 1000) + 2000
+ waterConsumption.value = Math.floor(Math.random() * 100) + 150
+ gasConsumption.value = Math.floor(Math.random() * 50) + 80
+
+ // 瓒嬪娍鏁版嵁
+ electricityTrend.value = (Math.random() * 20 - 10).toFixed(1)
+ waterTrend.value = (Math.random() * 15 - 7.5).toFixed(1)
+ gasTrend.value = (Math.random() * 12 - 6).toFixed(1)
+
+ // 缁熻鏁版嵁
+ totalEnergyConsumption.value = Math.floor(Math.random() * 50000) + 100000
+ yearOverYear.value = (Math.random() * 20 - 10).toFixed(1)
+ monthOverMonth.value = (Math.random() * 15 - 7.5).toFixed(1)
+ energySavingRate.value = (Math.random() * 10 + 5).toFixed(1)
+
+ // 鎺掑悕鏁版嵁
+ rankingList.value = [
+ { name: '鐢熶骇杞﹂棿A', value: Math.floor(Math.random() * 5000) + 10000, trend: (Math.random() * 20 - 10).toFixed(1) },
+ { name: '鐢熶骇杞﹂棿B', value: Math.floor(Math.random() * 4000) + 8000, trend: (Math.random() * 20 - 10).toFixed(1) },
+ { name: '鎶�鏈爺鍙戦儴', value: Math.floor(Math.random() * 3000) + 6000, trend: (Math.random() * 20 - 10).toFixed(1) },
+ { name: '琛屾斂鍔炲叕鍖�', value: Math.floor(Math.random() * 2000) + 4000, trend: (Math.random() * 20 - 10).toFixed(1) },
+ { name: '鍚庡嫟淇濋殰鍖�', value: Math.floor(Math.random() * 1500) + 3000, trend: (Math.random() * 20 - 10).toFixed(1) }
+ ].sort((a, b) => b.value - a.value)
+
+ // 寮傚父鏁版嵁
+ abnormalCount.value = Math.floor(Math.random() * 5) + 1
+ abnormalList.value = [
+ { level: 'warning', title: '鐢靛姏璐熻嵎杩囬珮', description: '鐢熶骇杞﹂棿A鐢靛姏璐熻嵎杈惧埌85%锛屽缓璁鏌ヨ澶囪繍琛岀姸鎬�', time: '2鍒嗛挓鍓�' },
+ { level: 'error', title: '姘村帇寮傚父', description: '姘村鐞嗚澶囧帇鍔涘紓甯革紝褰撳墠鍘嬪姏0.3MPa锛屼綆浜庢甯歌寖鍥�', time: '5鍒嗛挓鍓�' }
+ ]
+
+ // 鏅鸿兘鎺у埗鏁版嵁
+ loadForecast.value = Math.floor(Math.random() * 500) + 1500
+ intelligentAdjustment.value = Math.floor(Math.random() * 30) + 60
+
+ // 鐜繚鎸囨爣
+ carbonEmission.value = Math.floor(Math.random() * 1000) + 5000
+ carbonEmissionTrend.value = (Math.random() * 15 - 7.5).toFixed(1)
+ environmentalCompliance.value = (Math.random() * 5 + 95).toFixed(1)
+ greenEnergyRatio.value = (Math.random() * 10 + 25).toFixed(1)
+
+ // 鏇存柊鏈�鍚庢洿鏂版椂闂�
+ lastUpdateTime.value = new Date().toLocaleString()
+
+ // 鍚屾椂鏇存柊鎶ヨ〃鏁版嵁
+ generateReportData()
+}
+
+// 鍒濆鍖栬秼鍔垮浘琛�
+const initTrendChart = () => {
+ if (chartInstance) {
+ chartInstance.dispose()
+ }
+
+ chartInstance = echarts.init(trendChart.value)
+
+ const option = {
+ title: {
+ text: '鑳借�楄秼鍔垮垎鏋�',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis'
+ },
+ legend: {
+ data: ['鐢靛姏', '姘�', '姘斾綋'],
+ bottom: 10
+ },
+ xAxis: {
+ type: 'category',
+ data: generateTimeData()
+ },
+ yAxis: {
+ type: 'value',
+ name: '娑堣�楅噺'
+ },
+ series: [
+ {
+ name: '鐢靛姏',
+ type: 'line',
+ data: generateSeriesData(),
+ smooth: true
+ },
+ {
+ name: '姘�',
+ type: 'line',
+ data: generateSeriesData(),
+ smooth: true
+ },
+ {
+ name: '姘斾綋',
+ type: 'line',
+ data: generateSeriesData(),
+ smooth: true
+ }
+ ]
+ }
+
+ chartInstance.setOption(option)
+}
+
+// 鐢熸垚鏃堕棿鏁版嵁
+const generateTimeData = () => {
+ const data = []
+ const now = new Date()
+
+ switch (trendTimeUnit.value) {
+ case 'hour':
+ for (let i = 23; i >= 0; i--) {
+ const time = new Date(now.getTime() - i * 60 * 60 * 1000)
+ data.unshift(time.getHours() + ':00')
+ }
+ break
+ case 'day':
+ for (let i = 29; i >= 0; i--) {
+ const time = new Date(now.getTime() - i * 24 * 60 * 60 * 1000)
+ data.unshift(time.getDate() + '鏃�')
+ }
+ break
+ case 'week':
+ for (let i = 11; i >= 0; i--) {
+ data.unshift(`绗�${12 - i}鍛╜)
+ }
+ break
+ case 'month':
+ for (let i = 11; i >= 0; i--) {
+ const month = (12 - i) % 12 || 12
+ data.unshift(`${month}鏈坄)
+ }
+ break
+ case 'year':
+ for (let i = 4; i >= 0; i--) {
+ const year = new Date().getFullYear() - i
+ data.unshift(`${year}骞碻)
+ }
+ break
+ }
+
+ return data
+}
+
+// 鐢熸垚绯诲垪鏁版嵁
+const generateSeriesData = () => {
+ const data = []
+ const count = trendTimeUnit.value === 'hour' ? 24 :
+ trendTimeUnit.value === 'day' ? 30 :
+ trendTimeUnit.value === 'week' ? 12 :
+ trendTimeUnit.value === 'month' ? 12 : 5
+
+ for (let i = 0; i < count; i++) {
+ data.push(Math.floor(Math.random() * 1000) + 500)
+ }
+
+ return data
+}
+
+// 澶勭悊瓒嬪娍鏃堕棿鍙樺寲
+const handleTrendTimeChange = () => {
+ nextTick(() => {
+ initTrendChart()
+ })
+}
+
+// 澶勭悊缁熻鍛ㄦ湡鍙樺寲
+const handleStatisticsChange = () => {
+ generateMockData()
+}
+
+// 澶勭悊鎺掑悕绫诲瀷鍙樺寲
+const handleRankingChange = () => {
+ // 鏍规嵁绫诲瀷閲嶆柊鐢熸垚鎺掑悕鏁版嵁
+ generateMockData()
+}
+
+// 澶勭悊鑷姩鎺у埗鍙樺寲
+const handleAutoControlChange = (value) => {
+ ElMessage.success(`鏅鸿兘鎺у埗绯荤粺宸�${value ? '鍚敤' : '绂佺敤'}`)
+}
+
+// 澶勭悊寮傚父
+const handleAbnormal = (item) => {
+ ElMessage.info(`姝e湪澶勭悊寮傚父锛�${item.title}`)
+}
+
+// 鍒锋柊鏁版嵁
+const refreshData = () => {
+ generateMockData()
+ if (chartInstance) {
+ initTrendChart()
+ }
+ ElMessage.success('鏁版嵁宸插埛鏂�')
+}
+
+// 瀵煎嚭缁熻
+const exportStatistics = () => {
+ ElMessage.success('缁熻鏁版嵁瀵煎嚭鎴愬姛')
+}
+
+// 瀵煎嚭鐜繚鎶ュ憡
+const exportEnvironmentalReport = () => {
+ ElMessage.success('鐜繚鎶ュ憡瀵煎嚭鎴愬姛')
+}
+
+// 鐢熸垚鑷畾涔夋姤琛�
+const generateCustomReport = () => {
+ ElMessage.info('鑷畾涔夋姤琛ㄥ姛鑳藉紑鍙戜腑...')
+}
+
+// 璁㈤槄鎶ヨ〃
+const subscribeReport = () => {
+ ElMessage.info('鎶ヨ〃璁㈤槄鍔熻兘寮�鍙戜腑...')
+}
+
+// 鐢熸垚鎶ヨ〃鏁版嵁
+const generateReportData = () => {
+ // 鐢熸垚鍩虹鏁版嵁
+ reportData.value.electricity = Math.floor(Math.random() * 5000) + 8000
+ reportData.value.water = Math.floor(Math.random() * 200) + 300
+ reportData.value.gas = Math.floor(Math.random() * 100) + 150
+
+ // 鐢熸垚瓒嬪娍鏁版嵁
+ reportData.value.electricityTrend = (Math.random() * 20 - 10).toFixed(1)
+ reportData.value.waterTrend = (Math.random() * 15 - 7.5).toFixed(1)
+ reportData.value.gasTrend = (Math.random() * 12 - 6).toFixed(1)
+
+ // 璁$畻鎬昏兘鑰楀拰骞冲潎鑳借��
+ reportData.value.totalEnergy = reportData.value.electricity + reportData.value.water * 0.1 + reportData.value.gas * 0.05
+ reportData.value.averageEnergy = Math.floor(reportData.value.totalEnergy / 3)
+ reportData.value.efficiency = (Math.random() * 20 + 80).toFixed(1)
+
+ // 鐢熸垚鍥捐〃鏁版嵁
+ const labels = ['鍛ㄤ竴', '鍛ㄤ簩', '鍛ㄤ笁', '鍛ㄥ洓', '鍛ㄤ簲', '鍛ㄥ叚', '鍛ㄦ棩']
+ const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399', '#9c27b0', '#ff9800']
+
+ reportData.value.chartData = labels.map((label, index) => ({
+ label,
+ value: Math.floor(Math.random() * 1000) + 500,
+ percentage: Math.floor(Math.random() * 40) + 30,
+ color: colors[index]
+ }))
+}
+
+// 鐢熸垚鎶ヨ〃
+const generateReport = () => {
+ generateReportData()
+ ElMessage.success('鎶ヨ〃鐢熸垚鎴愬姛')
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+// 鍚姩瀹氭椂鏇存柊
+const startAutoUpdate = () => {
+ updateTimer = setInterval(() => {
+ generateMockData()
+ if (chartInstance) {
+ initTrendChart()
+ }
+ }, 60000) // 姣忓垎閽熸洿鏂颁竴娆�
+}
+
+// 鍋滄瀹氭椂鏇存柊
+const stopAutoUpdate = () => {
+ if (updateTimer) {
+ clearInterval(updateTimer)
+ updateTimer = null
+ }
+}
+
+// 缁勪欢鎸傝浇
+onMounted(() => {
+ generateMockData()
+ nextTick(() => {
+ initTrendChart()
+ })
+ startAutoUpdate()
+})
+
+// 缁勪欢鍗歌浇
+onUnmounted(() => {
+ stopAutoUpdate()
+ if (chartInstance) {
+ chartInstance.dispose()
+ }
+})
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+ padding: 12px;
+ background: #f5f5f5;
+ min-height: 100vh;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ padding: 16px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ h2 {
+ margin: 0;
+ color: #303133;
+ font-size: 22px;
+ }
+
+ .header-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .update-time {
+ color: #909399;
+ font-size: 14px;
+ }
+ }
+}
+
+.real-time-monitor {
+ margin-bottom: 12px;
+
+ .monitor-card {
+ .monitor-content {
+ text-align: center;
+ padding: 16px 0;
+
+ .monitor-value {
+ margin-bottom: 12px;
+
+ .value {
+ font-size: 28px;
+ font-weight: bold;
+ color: #409eff;
+ }
+
+ .unit {
+ font-size: 14px;
+ color: #909399;
+ margin-left: 4px;
+ }
+ }
+
+ .monitor-trend {
+ .trend-label {
+ font-size: 14px;
+ color: #606266;
+ margin-right: 6px;
+ }
+ }
+ }
+ }
+}
+
+.trend-analysis {
+ margin-bottom: 12px;
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .time-selector {
+ .el-radio-group {
+ .el-radio {
+ margin-right: 4px;
+ }
+ }
+ }
+ }
+
+ .chart-container {
+ padding: 16px 0;
+ }
+}
+
+.statistics-ranking {
+ margin-bottom: 12px;
+
+ .statistics-card, .ranking-card {
+ height: 100%;
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+ border-radius: 8px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
+ }
+ }
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid #f0f0f0;
+ background: #fafafa;
+
+ .card-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .header-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ }
+ }
+
+ .statistics-content {
+ padding: 16px;
+
+ .statistics-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ padding: 10px 12px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ transition: background-color 0.3s ease;
+
+ &:hover {
+ background: #e9ecef;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .label {
+ color: #606266;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .value {
+ font-weight: bold;
+ font-size: 15px;
+
+ &.success {
+ color: #67c23a;
+ }
+ }
+ }
+ }
+
+ .ranking-list {
+ padding: 16px;
+
+ .ranking-item {
+ display: flex;
+ align-items: center;
+ padding: 12px;
+ margin-bottom: 6px;
+ background: #f8f9fa;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: #e9ecef;
+ transform: translateX(4px);
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .ranking-number {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: bold;
+ font-size: 14px;
+ margin-right: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ &.ranking-first {
+ background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
+ color: #fff;
+ }
+
+ &.ranking-second {
+ background: linear-gradient(135deg, #c0c0c0 0%, #d4d4d4 100%);
+ color: #fff;
+ }
+
+ &.ranking-third {
+ background: linear-gradient(135deg, #cd7f32 0%, #daa520 100%);
+ color: #fff;
+ }
+
+ &.ranking-normal {
+ background: linear-gradient(135deg, #f5f5f5 0%, #e9ecef 100%);
+ color: #909399;
+ }
+ }
+
+ .ranking-info {
+ flex: 1;
+
+ .ranking-name {
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 4px;
+ font-size: 14px;
+ }
+
+ .ranking-value {
+ color: #606266;
+ font-size: 13px;
+ font-weight: 500;
+ }
+ }
+
+ .ranking-trend {
+ margin-left: 12px;
+ }
+ }
+ }
+}
+
+.analysis-control {
+ margin-bottom: 20px;
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .abnormal-list {
+ .abnormal-item {
+ display: flex;
+ align-items: flex-start;
+ padding: 15px 0;
+ border-bottom: 1px solid #f0f0f0;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ .abnormal-icon {
+ margin-right: 15px;
+ margin-top: 2px;
+ }
+
+ .abnormal-content {
+ flex: 1;
+
+ .abnormal-title {
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 5px;
+ }
+
+ .abnormal-desc {
+ color: #606266;
+ font-size: 14px;
+ margin-bottom: 5px;
+ }
+
+ .abnormal-time {
+ color: #909399;
+ font-size: 12px;
+ }
+ }
+
+ .abnormal-action {
+ margin-left: 15px;
+ }
+ }
+ }
+
+ .control-content {
+ .control-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .label {
+ color: #606266;
+ font-size: 14px;
+ }
+
+ .value {
+ font-weight: bold;
+ color: #303133;
+ }
+ }
+ }
+}
+
+.environmental-indicators {
+ margin-bottom: 20px;
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .header-actions {
+ display: flex;
+ gap: 10px;
+ }
+ }
+
+ .indicator-item {
+ text-align: center;
+ padding: 20px 0;
+
+ .indicator-title {
+ color: #606266;
+ font-size: 14px;
+ margin-bottom: 10px;
+ }
+
+ .indicator-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 10px;
+ }
+
+ .indicator-trend {
+ font-size: 12px;
+ color: #909399;
+
+ .success {
+ color: #67c23a;
+ }
+ }
+ }
+}
+
+.multi-dimensional-reports {
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .header-actions {
+ display: flex;
+ gap: 10px;
+ }
+ }
+
+ .report-filters {
+ padding: 20px 0;
+ border-bottom: 1px solid #f0f0f0;
+ margin-bottom: 20px;
+ }
+
+ .report-preview {
+ .report-data {
+ padding: 20px 0;
+
+ .data-card {
+ text-align: center;
+ padding: 16px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ margin-bottom: 16px;
+
+ .data-title {
+ color: #606266;
+ font-size: 14px;
+ margin-bottom: 8px;
+ }
+
+ .data-value {
+ font-size: 20px;
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 8px;
+ }
+
+ .data-trend {
+ font-size: 12px;
+
+ .trend-up {
+ color: #f56c6c;
+ }
+
+ .trend-down {
+ color: #67c23a;
+ }
+
+ .trend-stable {
+ color: #909399;
+ }
+ }
+ }
+
+ .report-chart {
+ margin: 20px 0;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+
+ .chart-title {
+ text-align: center;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 16px;
+ }
+
+ .chart-bars {
+ display: flex;
+ justify-content: space-around;
+ align-items: flex-end;
+ height: 120px;
+
+ .chart-bar {
+ text-align: center;
+ flex: 1;
+ margin: 0 8px;
+
+ .bar-label {
+ font-size: 12px;
+ color: #606266;
+ margin-bottom: 8px;
+ }
+
+ .bar-container {
+ height: 80px;
+ background: #e9ecef;
+ border-radius: 4px;
+ position: relative;
+ margin-bottom: 8px;
+ }
+
+ .bar-fill {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ border-radius: 4px;
+ transition: height 0.3s ease;
+ }
+
+ .bar-value {
+ font-size: 12px;
+ color: #303133;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+
+ .report-summary {
+ display: flex;
+ justify-content: space-around;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+
+ .summary-item {
+ text-align: center;
+
+ .summary-label {
+ display: block;
+ color: #606266;
+ font-size: 14px;
+ margin-bottom: 8px;
+ }
+
+ .summary-value {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+ }
+ }
+ }
+ }
+ }
+}
+
+// 閫氱敤鏍峰紡
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.success {
+ color: #67c23a;
+}
+
+.danger {
+ color: #f56c6c;
+}
+
+.warning {
+ color: #e6a23c;
+}
+
+.info {
+ color: #909399;
+}
+</style>
diff --git a/src/views/energyManagement/energyPeriodTime/index.vue b/src/views/energyManagement/energyPeriodTime/index.vue
new file mode 100644
index 0000000..49bb226
--- /dev/null
+++ b/src/views/energyManagement/energyPeriodTime/index.vue
@@ -0,0 +1,462 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">鏃ユ湡锛�</span>
+ <!-- <el-time-picker
+ style="width: 240px;margin-right: 10px"
+ v-model="searchForm.startTime"
+ value-format="HH:mm:ss"
+ format="HH:mm:ss"
+ type="time"
+ placeholder="璇烽�夋嫨寮�濮嬫椂闂�"
+ clearable
+ /> -->
+ <el-date-picker
+ v-model="searchForm.date"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ />
+ <!-- <el-time-picker
+ v-model="searchForm.timeRange"
+ is-range
+ arrow-control
+ range-separator="To"
+ start-placeholder="閫夋嫨缁撴潫鏃堕棿"
+ end-placeholder="閫夋嫨缁撴潫鏃堕棿"
+ /> -->
+ <span class="search_title">鐢典环锛堝厓/搴︼級锛�</span>
+ <el-input
+ v-model="searchForm.price"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ョ數浠�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="鐢ㄧ數鏃舵绠$悊"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鏃ユ湡锛�" prop="date">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢典环锛堝厓/搴︼級锛�" prop="price">
+ <el-input
+ v-model="form.price"
+ placeholder="璇疯緭鍏ョ數浠�"
+ clearable
+ type="number"
+ step="0.01"
+ min="0"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="宄版锛�" prop="peak">
+ <el-input
+ v-model="form.peak"
+ placeholder="璇疯緭鍏ュ嘲娈�"
+ clearable
+ type="number"
+ step="0.01"
+ min="0"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璋锋锛�" prop="valley">
+ <el-input
+ v-model="form.valley"
+ placeholder="璇疯緭鍏ヨ胺娈�"
+ clearable
+ type="number"
+ step="0.01"
+ min="0"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="骞虫锛�" prop="flat">
+ <el-input
+ v-model="form.flat"
+ placeholder="璇疯緭鍏ュ钩娈�"
+ clearable
+ type="number"
+ step="0.01"
+ min="0"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="灏栨锛�" prop="sharp">
+ <el-input
+ v-model="form.sharp"
+ placeholder="璇疯緭鍏ュ皷娈�"
+ clearable
+ type="number"
+ step="0.01"
+ min="0"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+<script setup>
+import {Search} from "@element-plus/icons-vue";
+import {onMounted, ref, getCurrentInstance} from "vue";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import {periodListPage,periodDelete,periodAdd,periodUpdate} from "@/api/energyManagement/index.js";
+const { proxy } = getCurrentInstance();
+
+const data = reactive({
+ searchForm: {
+ date: "",
+ price: ""
+ },
+ form: {
+ date: "",
+ price: "",
+ peak: "",
+ valley: "",
+ flat: "",
+ sharp: ""
+ }
+});
+const { searchForm,form } = toRefs(data);
+const page = ref({
+ current: 1,
+ size: 100,
+ total: 0
+});
+const dialogFormVisible = ref(false);
+const selectedRows = ref([]);
+const operationType = ref('');
+const tableData = ref([]);
+const emit = defineEmits(['close'])
+const tableLoading = ref(false);
+const tableColumn = ref([
+ // {
+ // label: "鏃舵鍚嶇О",
+ // prop: "timeName",
+ // width: 200,
+ // },
+ {
+ label: "鏃ユ湡",
+ prop: "date",
+ width: 200,
+ },
+ {
+ label: "鐢典环锛堝厓/搴︼級",
+ prop: "price",
+ width: 200,
+ },
+ {
+ label: "宄版",
+ prop: "peak",
+ },
+ {
+ label: "璋锋",
+ prop: "valley",
+ },
+ {
+ label: "骞虫",
+ prop: "flat",
+ },
+ {
+ label: "灏栨",
+ prop: "sharp",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ ],
+ },
+]);
+
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+const formDia = ref()
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙瀹㈡埛瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙瀹㈡埛瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/equipmentEnergyConsumption/importData",
+ // 鏂囦欢涓婁紶鍓嶇殑鍥炶皟
+ beforeUpload: (file) => {
+ console.log('鏂囦欢鍗冲皢涓婁紶', file);
+ // 鍙互鍦ㄦ澶勫仛鏂囦欢绫诲瀷鎴栧ぇ灏忔牎楠�
+ const isValid = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
+ if (!isValid) {
+ proxy.$modal.msgError("鍙兘涓婁紶 Excel 鏂囦欢");
+ }
+ return isValid;
+ },
+ // 鏂囦欢鐘舵�佹敼鍙樻椂鐨勫洖璋�
+ onChange: (file, fileList) => {
+ console.log('鏂囦欢鐘舵�佹敼鍙�', file, fileList);
+ },
+ // 鏂囦欢涓婁紶鎴愬姛鏃剁殑鍥炶皟
+ onSuccess: (response, file, fileList) => {
+ console.log('涓婁紶鎴愬姛', response, file, fileList);
+ if(response.code === 200){
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ }else if(response.code === 500){
+ ElMessageBox.error(response.msg);
+ }else{
+ ElMessageBox.warning(response.msg);
+ }
+ },
+ // 鏂囦欢涓婁紶澶辫触鏃剁殑鍥炶皟
+ onError: (error, file, fileList) => {
+ console.error('涓婁紶澶辫触', error, file, fileList);
+ ElMessageBox.error("鏂囦欢涓婁紶澶辫触");
+ },
+ // 鏂囦欢涓婁紶杩涘害鍥炶皟
+ onProgress: (event, file, fileList) => {
+ console.log('涓婁紶涓�...', event.percent);
+ }
+});
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+//閲嶇疆
+const resetFilters = () => {
+ searchForm.value = {
+ date: "",
+ price: ""
+ };
+ getList();
+
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ periodListPage({ ...searchForm.value, ...page.value }).then((res) => {
+ tableLoading.value = false;
+ if (res && res.data) {
+ tableData.value = res.data.records || [];
+ page.value.total = res.data.total || 0;
+ } else {
+ tableData.value = [];
+ page.value.total = 0;
+ ElMessageBox.warning('鏈幏鍙栧埌鏁版嵁');
+ }
+ })
+ .catch((err) => {
+ tableLoading.value = false;
+ console.error('鏁版嵁鍔犺浇澶辫触:', err);
+ ElMessageBox.error('鏁版嵁鍔犺浇澶辫触锛岃閲嶈瘯');
+ });
+};
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ // form.value.maintainer = userStore.nickName;
+ // form.value.maintenanceTime = getCurrentDate();
+ form.value = {}
+ proxy.resetForm("formRef");
+ periodListPage().then((res) => {
+ codeList.value = res.data;
+ });
+ if (type === "edit") {
+ form.value = {...row}
+ }
+}
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ openDialog(type, row)
+};
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ periodAdd(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ closeDia()
+ getList()
+ })
+ } else {
+ periodUpdate(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ closeDia()
+ getList()
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImport() {
+ upload.title = "璁惧鑳借��";
+ upload.open = true;
+ // 娓呯┖涓婃涓婁紶鐨勬枃浠跺垪琛�
+ nextTick(() => {
+ proxy.$refs["uploadRef"]?.clearFiles();
+ });
+}
+function importTemplate() {
+ proxy.download(
+ "/equipmentEnergyConsumption/export",
+ {},
+ '璁惧鑳借�楀鍏ユā鐗�.xlsx'
+ );
+}
+/** 鎻愪氦涓婁紶鏂囦欢 */
+function submitFileForm() {
+ proxy.$refs["uploadRef"].submit();
+}
+
+/** 寮规鍏抽棴鏃舵竻绌烘枃浠跺垪琛� */
+function handleDialogClose() {
+ nextTick(() => {
+ proxy.$refs["uploadRef"]?.clearFiles();
+ });
+}
+
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ periodDelete(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/energyPeriod/export", {}, "鐢ㄧ數鏃舵绠$悊.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/energyManagement/energyPower/components/formDia.vue b/src/views/energyManagement/energyPower/components/formDia.vue
new file mode 100644
index 0000000..3cb4455
--- /dev/null
+++ b/src/views/energyManagement/energyPower/components/formDia.vue
@@ -0,0 +1,228 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="璁惧鑳借��"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="璁惧锛�" prop="code">
+ <el-select
+ v-model="form.code"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="setName"
+ :disabled="operationType !== 'add'"
+ >
+ <el-option
+ v-for="item in codeList"
+ :key="item.deviceModel"
+ :label="item.deviceName"
+ :value="item.deviceModel"
+ >
+ {{item.deviceName + '--' + item.deviceModel}}
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄧ數娑堣�楀尯鍩燂細" prop="electricityConsumptionAreaId">
+ <el-cascader
+ v-model="form.electricityConsumptionAreaId"
+ :options="areaList"
+ :props="{
+ value: 'id',
+ label: 'label',
+ children: 'children',
+ checkStrictly: true,
+ }"
+ placeholder="璇烽�夋嫨鍖哄煙"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="姣忔棩闄愬埗鐢甸噺锛�" prop="everyNum">
+ <el-input
+ v-model="form.everyNum"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="棰濆畾鍔熺巼锛�" prop="powerRating">
+ <el-input
+ v-model="form.powerRating"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瀹為檯鍔熺巼锛�" prop="powerActual">
+ <el-input
+ v-model="form.powerActual"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杩愯鏃堕棿锛�" prop="runDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.runDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="褰撴棩鐢ㄧ數閲忥細" prop="dayNum">
+ <el-input
+ v-model="form.dayNum"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import useUserStore from "@/store/modules/user.js";
+import {deviceList, equipmentEnergyAdd, equipmentEnergyUpdate, areaListTree} from "@/api/energyManagement/index.js";
+import { getCurrentDate } from "@/utils/index.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const userStore = useUserStore();
+
+const data = reactive({
+ form: {
+ name: "",
+ code: "",
+ everyNum: "",
+ powerRating: "",
+ powerActual: "",
+ runDate: "",
+ dayNum: "",
+ electricityConsumptionAreaId: "",
+ },
+ rules: {
+ code: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ runDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ everyNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ powerRating: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ powerActual: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ dayNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ electricityConsumptionAreaId: [{ required: true, message: "璇烽�夋嫨鍖哄煙", trigger: "change" }],
+ },
+})
+const { form, rules } = toRefs(data);
+const codeList = ref([])
+const areaList = ref([])
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ // form.value.maintainer = userStore.nickName;
+ // form.value.maintenanceTime = getCurrentDate();
+ form.value = {}
+ proxy.resetForm("formRef");
+
+ // 鑾峰彇璁惧鍒楄〃
+ deviceList().then((res) => {
+ codeList.value = res.data;
+ });
+
+ // 鑾峰彇鍖哄煙鍒楄〃
+ areaListTree().then((res) => {
+ areaList.value = res;
+ console.log("areaList", res);
+ });
+
+ if (type === "edit") {
+ form.value = {...row}
+ // 缂栬緫鏃讹紝灏嗗崟涓狪D杞崲涓烘暟缁勬牸寮忕敤浜庡洖鏄�
+ if (row.electricityConsumptionAreaId) {
+ form.value.electricityConsumptionAreaId = [row.electricityConsumptionAreaId];
+ }
+ }
+}
+const setName = (code) => {
+ const index = codeList.value.findIndex(item => item.deviceModel === code);
+ if (index > -1) {
+ console.log(codeList)
+ form.value.name = codeList.value[index].deviceName;
+ }
+}
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ // 鎻愪氦鍓嶅鐞� electricityConsumptionAreaId锛屽彇鏁扮粍鐨勬渶鍚庝竴涓��
+ const submitData = { ...form.value };
+ if (Array.isArray(submitData.electricityConsumptionAreaId) && submitData.electricityConsumptionAreaId.length > 0) {
+ submitData.electricityConsumptionAreaId = submitData.electricityConsumptionAreaId[submitData.electricityConsumptionAreaId.length - 1];
+ }
+
+ if (operationType.value === "add") {
+ equipmentEnergyAdd(submitData).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ closeDia()
+ })
+ } else {
+ equipmentEnergyUpdate(submitData).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ closeDia()
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/energyManagement/energyPower/index.vue b/src/views/energyManagement/energyPower/index.vue
new file mode 100644
index 0000000..6311019
--- /dev/null
+++ b/src/views/energyManagement/energyPower/index.vue
@@ -0,0 +1,322 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">璁惧鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板</el-button>
+ <el-button type="info" plain icon="Upload" @click="handleImport">瀵煎叆</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ <el-dialog
+ :title="upload.title"
+ v-model="upload.open"
+ width="400px"
+ append-to-body
+ @close="handleDialogClose"
+ >
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url"
+ :disabled="upload.isUploading"
+ :before-upload="upload.beforeUpload"
+ :on-progress="upload.onProgress"
+ :on-success="upload.onSuccess"
+ :on-error="upload.onError"
+ :on-change="upload.onChange"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link
+ type="primary"
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ @click="importTemplate"
+ >涓嬭浇妯℃澘</el-link
+ >
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {Search} from "@element-plus/icons-vue";
+import {onMounted, ref, getCurrentInstance} from "vue";
+import FormDia from "@/views/energyManagement/energyPower/components/formDia.vue";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import {equipmentEnergyDelete, equipmentEnergyListPage} from "@/api/energyManagement/index.js";
+const { proxy } = getCurrentInstance();
+
+const data = reactive({
+ searchForm: {
+ name: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "璁惧鍚嶇О",
+ prop: "name",
+ width: 200,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "code",
+ width: 200,
+ },
+ {
+ label: "棰濆畾鍔熺巼",
+ prop: "powerRating",
+ },
+ {
+ label: "瀹為檯鍔熺巼",
+ prop: "powerActual",
+ },
+ {
+ label: "杩愯鏃堕棿",
+ prop: "runDate",
+ width:150
+ },
+ {
+ label: "褰撴棩鐢ㄧ數閲�",
+ prop: "dayNum",
+ width: 150,
+ },
+ // {
+ // label: "绱鐢ㄧ數閲�",
+ // prop: "sumNum",
+ // width: 150,
+ // },
+ {
+ label: "姣忔棩闄愬埗鐢甸噺",
+ prop: "everyNum",
+ width:220
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+const formDia = ref()
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙瀹㈡埛瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙瀹㈡埛瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/equipmentEnergyConsumption/importData",
+ // 鏂囦欢涓婁紶鍓嶇殑鍥炶皟
+ beforeUpload: (file) => {
+ console.log('鏂囦欢鍗冲皢涓婁紶', file);
+ // 鍙互鍦ㄦ澶勫仛鏂囦欢绫诲瀷鎴栧ぇ灏忔牎楠�
+ const isValid = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
+ if (!isValid) {
+ proxy.$modal.msgError("鍙兘涓婁紶 Excel 鏂囦欢");
+ }
+ return isValid;
+ },
+ // 鏂囦欢鐘舵�佹敼鍙樻椂鐨勫洖璋�
+ onChange: (file, fileList) => {
+ console.log('鏂囦欢鐘舵�佹敼鍙�', file, fileList);
+ },
+ // 鏂囦欢涓婁紶鎴愬姛鏃剁殑鍥炶皟
+ onSuccess: (response, file, fileList) => {
+ console.log('涓婁紶鎴愬姛', response, file, fileList);
+ if(response.code === 200){
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ }else if(response.code === 500){
+ proxy.$modal.msgError(response.msg);
+ }else{
+ proxy.$modal.msgWarning(response.msg);
+ }
+ },
+ // 鏂囦欢涓婁紶澶辫触鏃剁殑鍥炶皟
+ onError: (error, file, fileList) => {
+ console.error('涓婁紶澶辫触', error, file, fileList);
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ },
+ // 鏂囦欢涓婁紶杩涘害鍥炶皟
+ onProgress: (event, file, fileList) => {
+ console.log('涓婁紶涓�...', event.percent);
+ }
+});
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ equipmentEnergyListPage({ ...searchForm.value, ...page }).then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ });
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImport() {
+ upload.title = "璁惧鑳借��";
+ upload.open = true;
+ // 娓呯┖涓婃涓婁紶鐨勬枃浠跺垪琛�
+ nextTick(() => {
+ proxy.$refs["uploadRef"]?.clearFiles();
+ });
+}
+function importTemplate() {
+ proxy.download(
+ "/equipmentEnergyConsumption/export",
+ {},
+ '璁惧鑳借�楀鍏ユā鐗�.xlsx'
+ );
+}
+/** 鎻愪氦涓婁紶鏂囦欢 */
+function submitFileForm() {
+ proxy.$refs["uploadRef"].submit();
+}
+
+/** 寮规鍏抽棴鏃舵竻绌烘枃浠跺垪琛� */
+function handleDialogClose() {
+ nextTick(() => {
+ proxy.$refs["uploadRef"]?.clearFiles();
+ });
+}
+
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ equipmentEnergyDelete(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/equipmentEnergyConsumption/export", {}, "鑳芥簮鍔熺巼.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/energyManagement/energyTrends/index.vue b/src/views/energyManagement/energyTrends/index.vue
new file mode 100644
index 0000000..7f67be7
--- /dev/null
+++ b/src/views/energyManagement/energyTrends/index.vue
@@ -0,0 +1,137 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">璁惧鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ <el-button @click="handleOut" style="margin-left: 10px">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+
+ </div>
+</template>
+
+<script setup>
+import {Search} from "@element-plus/icons-vue";
+import {onMounted, ref, getCurrentInstance} from "vue";
+import {listPageByTrend} from "@/api/energyManagement/index.js";
+import { ElMessageBox } from "element-plus";
+
+const { proxy } = getCurrentInstance();
+
+const data = reactive({
+ searchForm: {
+ name: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const tableColumn = ref([
+ {
+ label: "璁惧鍚嶇О",
+ prop: "name",
+ width: 220,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "code",
+ width: 220,
+ },
+ {
+ label: "杩愯鏃堕棿",
+ prop: "runDate",
+ width: 250,
+ },
+ {
+ label: "鏄ㄦ棩鐢ㄧ數閲�",
+ prop: "toDayNum",
+ },
+ {
+ label: "鏈湀骞冲潎鐢甸噺",
+ prop: "avgNum",
+ width:150
+ },
+ {
+ label: "瓒嬪娍",
+ prop: "trend",
+ width: 220,
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ listPageByTrend({ ...searchForm.value, ...page }).then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/equipmentEnergyConsumption/exportTwo", {}, "鑳芥簮瓒嬪娍.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/energyManagement/gasManagement/index.vue b/src/views/energyManagement/gasManagement/index.vue
new file mode 100644
index 0000000..a9b9abe
--- /dev/null
+++ b/src/views/energyManagement/gasManagement/index.vue
@@ -0,0 +1,624 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <h2>鐢ㄦ皵绠$悊绯荤粺</h2>
+ <div class="header-info">
+ <span class="update-time">鏈�鍚庢洿鏂帮細{{ lastUpdateTime }}</span>
+ <el-button type="primary" size="small" @click="refreshData">
+ <el-icon><Refresh /></el-icon>
+ 鍒锋柊鏁版嵁
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 缁熻鍗$墖鍖哄煙 -->
+ <div class="stats-cards">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-icon gas-device">
+ <el-icon size="32"><Box /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-value">{{ totalDevices }}</div>
+ <div class="stat-label">鍦ㄧ敤璁惧</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-icon daily-consumption">
+ <el-icon size="32"><TrendCharts /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-value">{{ dailyConsumption }} m鲁</div>
+ <div class="stat-label">鏃ヨ�楅噺</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-icon monthly-consumption">
+ <el-icon size="32"><DataLine /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-value">{{ monthlyConsumption }} m鲁</div>
+ <div class="stat-label">鏈堣�楅噺</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card">
+ <div class="stat-content">
+ <div class="stat-icon gas-price">
+ <el-icon size="32"><Money /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-value">楼{{ gasUnitPrice }}</div>
+ <div class="stat-label">姘斾綋鍗曚环</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 璐圭敤缁熻鍖哄煙 -->
+ <div class="cost-stats">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-card class="cost-card">
+ <template #header>
+ <div class="card-header">
+ <span>鏃ヨ垂鐢ㄧ粺璁�</span>
+ <el-tag type="success" size="small">浠婃棩</el-tag>
+ </div>
+ </template>
+ <div class="cost-content">
+ <div class="cost-main">
+ <span class="cost-amount">楼{{ dailyTotalCost.toFixed(2) }}</span>
+ <span class="cost-unit">鍏�</span>
+ </div>
+ <div class="cost-details">
+ <div class="cost-item">
+ <span>娑堣�楅噺锛�</span>
+ <span>{{ dailyConsumption }} m鲁</span>
+ </div>
+ <div class="cost-item">
+ <span>鍗曚环锛�</span>
+ <span>楼{{ gasUnitPrice }}/m鲁</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="12">
+ <el-card class="cost-card">
+ <template #header>
+ <div class="card-header">
+ <span>鏈堣垂鐢ㄧ粺璁�</span>
+ <el-tag type="primary" size="small">鏈湀</el-tag>
+ </div>
+ </template>
+ <div class="cost-content">
+ <div class="cost-main">
+ <span class="cost-amount">楼{{ monthlyTotalCost.toFixed(2) }}</span>
+ <span class="cost-unit">鍏�</span>
+ </div>
+ <div class="cost-details">
+ <div class="cost-item">
+ <span>娑堣�楅噺锛�</span>
+ <span>{{ monthlyConsumption }} m鲁</span>
+ </div>
+ <div class="cost-item">
+ <span>骞冲潎鍗曚环锛�</span>
+ <span>楼{{ gasUnitPrice }}/m鲁</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 璁惧鍒楄〃鍖哄煙 -->
+ <div class="device-section">
+ <el-card>
+ <template #header>
+ <div class="card-header">
+ <span>璁惧鐩戞帶</span>
+ <div class="header-actions">
+ <el-button type="primary" size="small" @click="addDevice">
+ <el-icon><Plus /></el-icon>
+ 娣诲姞璁惧
+ </el-button>
+ </div>
+ </div>
+ </template>
+
+ <el-table :data="deviceList" border style="width: 100%" v-loading="tableLoading">
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="璁惧缂栧彿" prop="deviceCode" width="120" show-overflow-tooltip />
+ <el-table-column label="璁惧鍚嶇О" prop="deviceName" width="150" show-overflow-tooltip />
+ <el-table-column label="璁惧绫诲瀷" prop="deviceType" width="120" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specification" width="150" show-overflow-tooltip />
+ <el-table-column label="褰撳墠鍘嬪姏(MPa)" prop="currentPressure" width="130" show-overflow-tooltip>
+ <template #default="scope">
+ <span :class="getPressureClass(scope.row.currentPressure)">
+ {{ scope.row.currentPressure }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="褰撳墠娓╁害(鈩�)" prop="currentTemperature" width="130" show-overflow-tooltip>
+ <template #default="scope">
+ <span :class="getTemperatureClass(scope.row.currentTemperature)">
+ {{ scope.row.currentTemperature }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="姘斾綋娴撳害(ppm)" prop="gasConcentration" width="140" show-overflow-tooltip>
+ <template #default="scope">
+ <span :class="getConcentrationClass(scope.row.gasConcentration)">
+ {{ scope.row.gasConcentration }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="杩愯鐘舵��" prop="status" width="100" show-overflow-tooltip>
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)" size="small">
+ {{ getStatusText(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏈�鍚庢洿鏂�" prop="lastUpdate" width="160" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" align="center" width="100" fixed="right">
+ <template #default="scope">
+ <el-button link size="small" @click="editDevice(scope.row)">
+ 缂栬緫
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+ </div>
+
+ <!-- 娣诲姞/缂栬緫璁惧寮圭獥 -->
+ <el-dialog v-model="deviceDialogVisible" :title="dialogTitle" width="600px">
+ <el-form :model="deviceForm" :rules="deviceRules" ref="deviceFormRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璁惧缂栧彿" prop="deviceCode">
+ <el-input v-model="deviceForm.deviceCode" placeholder="璇疯緭鍏ヨ澶囩紪鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁惧鍚嶇О" prop="deviceName">
+ <el-input v-model="deviceForm.deviceName" placeholder="璇疯緭鍏ヨ澶囧悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璁惧绫诲瀷" prop="deviceType">
+ <el-select v-model="deviceForm.deviceType" placeholder="璇烽�夋嫨璁惧绫诲瀷" style="width: 100%">
+ <el-option label="娑插寲姘斿偍缃�" value="娑插寲姘斿偍缃�" />
+ <el-option label="鍘嬬缉姘斿偍缃�" value="鍘嬬缉姘斿偍缃�" />
+ <el-option label="澶╃劧姘斿偍缃�" value="澶╃劧姘斿偍缃�" />
+ <el-option label="姘ф皵鍌ㄧ綈" value="姘ф皵鍌ㄧ綈" />
+ <el-option label="鍏朵粬" value="鍏朵粬" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿" prop="specification">
+ <el-input v-model="deviceForm.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璁捐鍘嬪姏(MPa)" prop="designPressure">
+ <el-input-number v-model="deviceForm.designPressure" :min="0" :precision="2" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹圭Н(m鲁)" prop="volume">
+ <el-input-number v-model="deviceForm.volume" :min="0" :precision="2" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <el-button @click="deviceDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="saveDevice">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+ Refresh,
+ Box,
+ TrendCharts,
+ DataLine,
+ Money,
+ Plus
+} from '@element-plus/icons-vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const lastUpdateTime = ref('')
+const totalDevices = ref(0)
+const dailyConsumption = ref(0)
+const monthlyConsumption = ref(0)
+const gasUnitPrice = ref(0)
+const dailyTotalCost = ref(0)
+const monthlyTotalCost = ref(0)
+const deviceList = ref([])
+const tableLoading = ref(false)
+const deviceDialogVisible = ref(false)
+const dialogTitle = ref('')
+const deviceFormRef = ref()
+
+// 璁惧琛ㄥ崟鏁版嵁
+const deviceForm = reactive({
+ deviceCode: '',
+ deviceName: '',
+ deviceType: '',
+ specification: '',
+ designPressure: 0,
+ volume: 0
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const deviceRules = {
+ deviceCode: [{ required: true, message: '璇疯緭鍏ヨ澶囩紪鍙�', trigger: 'blur' }],
+ deviceName: [{ required: true, message: '璇疯緭鍏ヨ澶囧悕绉�', trigger: 'blur' }],
+ deviceType: [{ required: true, message: '璇烽�夋嫨璁惧绫诲瀷', trigger: 'change' }],
+ specification: [{ required: true, message: '璇疯緭鍏ヨ鏍煎瀷鍙�', trigger: 'blur' }],
+ designPressure: [{ required: true, message: '璇疯緭鍏ヨ璁″帇鍔�', trigger: 'blur' }],
+ volume: [{ required: true, message: '璇疯緭鍏ュ绉�', trigger: 'blur' }]
+}
+
+// 瀹氭椂鍣�
+let updateTimer = null
+
+// 妯℃嫙鏁版嵁鐢熸垚
+const generateMockData = () => {
+ // 鏇存柊缁熻鏁版嵁
+ totalDevices.value = Math.floor(Math.random() * 8) // 0-7鍙拌澶�
+ dailyConsumption.value = Math.floor(Math.random() * 100) + 200 // 200-300 m鲁
+ monthlyConsumption.value = Math.floor(Math.random() * 2000) + 5000 // 5000-7000 m鲁
+ gasUnitPrice.value = (Math.random() * 2 + 3).toFixed(2) // 3-5鍏�/m鲁
+
+ // 璁$畻璐圭敤
+ dailyTotalCost.value = dailyConsumption.value * gasUnitPrice.value
+ monthlyTotalCost.value = monthlyConsumption.value * gasUnitPrice.value
+
+ // 鏇存柊璁惧鍒楄〃鏁版嵁
+ deviceList.value = Array.from({ length: totalDevices.value }, (_, index) => ({
+ id: index + 1,
+ deviceCode: `GT${String(index + 1).padStart(3, '0')}`,
+ deviceName: `鍌ㄦ皵缃�${index + 1}`,
+ deviceType: ['娑插寲姘斿偍缃�', '鍘嬬缉姘斿偍缃�', '澶╃劧姘斿偍缃�', '姘ф皵鍌ㄧ綈'][Math.floor(Math.random() * 4)],
+ specification: `${Math.floor(Math.random() * 50) + 50}m鲁`,
+ currentPressure: (Math.random() * 2 + 0.5).toFixed(2),
+ currentTemperature: (Math.random() * 20 + 15).toFixed(1),
+ gasConcentration: (Math.random() * 10).toFixed(2),
+ status: ['running', 'stopped', 'warning', 'error'][Math.floor(Math.random() * 4)],
+ lastUpdate: new Date().toLocaleString()
+ }))
+
+ // 鏇存柊鏈�鍚庢洿鏂版椂闂�
+ lastUpdateTime.value = new Date().toLocaleString()
+}
+
+// 鑾峰彇鍘嬪姏鐘舵�佹牱寮�
+const getPressureClass = (pressure) => {
+ const p = parseFloat(pressure)
+ if (p < 0.8) return 'pressure-low'
+ if (p > 1.5) return 'pressure-high'
+ return 'pressure-normal'
+}
+
+// 鑾峰彇娓╁害鐘舵�佹牱寮�
+const getTemperatureClass = (temperature) => {
+ const t = parseFloat(temperature)
+ if (t < 10 || t > 35) return 'temperature-warning'
+ return 'temperature-normal'
+}
+
+// 鑾峰彇娴撳害鐘舵�佹牱寮�
+const getConcentrationClass = (concentration) => {
+ const c = parseFloat(concentration)
+ if (c > 5) return 'concentration-warning'
+ return 'concentration-normal'
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const statusMap = {
+ running: 'success',
+ stopped: 'info',
+ warning: 'warning',
+ error: 'danger'
+ }
+ return statusMap[status] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const statusMap = {
+ running: '杩愯涓�',
+ stopped: '宸插仠姝�',
+ warning: '璀﹀憡',
+ error: '鏁呴殰'
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+// 鍒锋柊鏁版嵁
+const refreshData = () => {
+ generateMockData()
+ ElMessage.success('鏁版嵁宸插埛鏂�')
+}
+
+// 娣诲姞璁惧
+const addDevice = () => {
+ dialogTitle.value = '娣诲姞璁惧'
+ Object.keys(deviceForm).forEach(key => {
+ deviceForm[key] = key === 'designPressure' || key === 'volume' ? 0 : ''
+ })
+ deviceDialogVisible.value = true
+}
+
+// 缂栬緫璁惧
+const editDevice = (row) => {
+ dialogTitle.value = '缂栬緫璁惧'
+ Object.keys(deviceForm).forEach(key => {
+ if (row[key] !== undefined) {
+ deviceForm[key] = row[key]
+ }
+ })
+ deviceDialogVisible.value = true
+}
+
+
+
+// 淇濆瓨璁惧
+const saveDevice = () => {
+ deviceFormRef.value.validate((valid) => {
+ if (valid) {
+ ElMessage.success('淇濆瓨鎴愬姛')
+ deviceDialogVisible.value = false
+ refreshData()
+ }
+ })
+}
+
+
+
+// 鍚姩瀹氭椂鏇存柊
+const startAutoUpdate = () => {
+ updateTimer = setInterval(() => {
+ generateMockData()
+ }, 60000) // 姣忓垎閽熸洿鏂颁竴娆�
+}
+
+// 鍋滄瀹氭椂鏇存柊
+const stopAutoUpdate = () => {
+ if (updateTimer) {
+ clearInterval(updateTimer)
+ updateTimer = null
+ }
+}
+
+// 缁勪欢鎸傝浇
+onMounted(() => {
+ generateMockData()
+ startAutoUpdate()
+})
+
+// 缁勪欢鍗歌浇
+onUnmounted(() => {
+ stopAutoUpdate()
+})
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+ padding: 20px;
+ background: #f5f5f5;
+ min-height: 100vh;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding: 20px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ h2 {
+ margin: 0;
+ color: #303133;
+ font-size: 24px;
+ }
+
+ .header-info {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+
+ .update-time {
+ color: #909399;
+ font-size: 14px;
+ }
+ }
+}
+
+.stats-cards {
+ margin-bottom: 20px;
+
+ .stat-card {
+ .stat-content {
+ display: flex;
+ align-items: center;
+ padding: 10px;
+
+ .stat-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 15px;
+
+ &.gas-device {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ }
+
+ &.daily-consumption {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ color: white;
+ }
+
+ &.monthly-consumption {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+ color: white;
+ }
+
+ &.gas-price {
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+ color: white;
+ }
+ }
+
+ .stat-info {
+ .stat-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #303133;
+ line-height: 1;
+ }
+
+ .stat-label {
+ font-size: 14px;
+ color: #909399;
+ margin-top: 5px;
+ }
+ }
+ }
+ }
+}
+
+.cost-stats {
+ margin-bottom: 20px;
+
+ .cost-card {
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .cost-content {
+ text-align: center;
+ padding: 20px 0;
+
+ .cost-main {
+ margin-bottom: 15px;
+
+ .cost-amount {
+ font-size: 36px;
+ font-weight: bold;
+ color: #409eff;
+ }
+
+ .cost-unit {
+ font-size: 16px;
+ color: #909399;
+ margin-left: 5px;
+ }
+ }
+
+ .cost-details {
+ .cost-item {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 8px;
+ font-size: 14px;
+ color: #606266;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+ }
+}
+
+.device-section {
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .header-actions {
+ display: flex;
+ gap: 10px;
+ }
+ }
+}
+
+// 鐘舵�佹牱寮�
+.pressure-low {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.pressure-normal {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.pressure-high {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.temperature-normal {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.temperature-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.concentration-normal {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.concentration-warning {
+ color: #f56c6c;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/energyManagement/meterCollection/index.vue b/src/views/energyManagement/meterCollection/index.vue
new file mode 100644
index 0000000..dfa5617
--- /dev/null
+++ b/src/views/energyManagement/meterCollection/index.vue
@@ -0,0 +1,556 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span>鐢佃〃閲囬泦绠$悊</span>
+ <el-button style="float: right; padding: 3px 0" link @click="refreshData">
+ <i class="el-icon-refresh"></i> 鍒锋柊
+ </el-button>
+ </div>
+
+ <!-- 娴嬭瘯鎸夐挳 -->
+ <el-row :gutter="20" style="margin-bottom: 15px;">
+ <el-col :span="24">
+ <el-button @click="addTestData" type="primary" size="small">娣诲姞娴嬭瘯鏁版嵁</el-button>
+ <el-button @click="clearData" type="danger" size="small">娓呯┖鏁版嵁</el-button>
+ <el-button @click="testChart" type="success" size="small">娴嬭瘯鍥捐〃</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-input
+ v-model="searchForm.meterNo"
+ placeholder="璇疯緭鍏ョ數琛ㄧ紪鍙�"
+ clearable
+ @keyup.enter.native="handleSearch"
+ >
+ <i slot="prefix" class="el-input__icon el-icon-search"></i>
+ </el-input>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.location" placeholder="璇烽�夋嫨浣嶇疆" clearable>
+ <el-option label="鐢熶骇杞﹂棿A" value="杞﹂棿A"></el-option>
+ <el-option label="鐢熶骇杞﹂棿B" value="杞﹂棿B"></el-option>
+ <el-option label="鍔炲叕鍖哄煙" value="鍔炲叕鍖�"></el-option>
+ <el-option label="閰嶇數瀹�" value="閰嶇數瀹�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-date-picker
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="yyyy-MM-dd"
+ value-format="yyyy-MM-dd"
+ />
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鐢佃〃鍒楄〃 -->
+ <el-table
+ :data="meterList"
+ style="width: 100%"
+ v-loading="loading"
+ border
+ stripe
+ height="calc(100vh - 22em)"
+ >
+ <el-table-column prop="meterNo" label="鐢佃〃缂栧彿" width="120" />
+ <el-table-column prop="location" label="瀹夎浣嶇疆" width="120" />
+ <el-table-column prop="meterType" label="鐢佃〃绫诲瀷" width="120" />
+ <el-table-column prop="voltage" label="鐢靛帇绛夌骇" width="100" />
+ <el-table-column prop="currentReading" label="褰撳墠璇绘暟(kWh)" width="140" />
+ <el-table-column prop="lastReading" label="涓婃璇绘暟(kWh)" width="140" />
+ <el-table-column prop="consumption" label="鐢ㄧ數閲�(kWh)" width="120" />
+ <el-table-column prop="power" label="鍔熺巼(kW)" width="100" />
+ <el-table-column prop="powerFactor" label="鍔熺巼鍥犳暟" width="100" />
+ <el-table-column prop="status" label="鐘舵��" width="80">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === '姝e父' ? 'success' : 'danger'">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="lastUpdateTime" label="鏈�鍚庢洿鏂版椂闂�" width="160" />
+ <el-table-column label="鎿嶄綔" width="180" fixed="right" align="center">
+ <template #default="scope">
+ <el-button link @click="viewDetails(scope.row)">
+ 鏌ョ湅璇︽儏
+ </el-button>
+ <el-button link @click="manualCollection(scope.row)">
+ 鎵嬪姩閲囬泦
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.currentPage"
+ :limit="pagination.pageSize"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <!-- 璇︽儏瀵硅瘽妗� -->
+ <el-dialog
+ title="鐢佃〃璇︽儏"
+ v-model="detailDialogVisible"
+ width="60%"
+ @opened="onDialogOpened"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鐢佃〃缂栧彿:</label>
+ <span>{{ currentMeter.meterNo }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>瀹夎浣嶇疆:</label>
+ <span>{{ currentMeter.location }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鐢佃〃绫诲瀷:</label>
+ <span>{{ currentMeter.meterType }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鐢靛帇绛夌骇:</label>
+ <span>{{ currentMeter.voltage }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>褰撳墠璇绘暟:</label>
+ <span>{{ currentMeter.currentReading }} kWh</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>涓婃璇绘暟:</label>
+ <span>{{ currentMeter.lastReading }} kWh</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鐢ㄧ數閲�:</label>
+ <span>{{ currentMeter.consumption }} kWh</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鍔熺巼:</label>
+ <span>{{ currentMeter.power }} kW</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鍔熺巼鍥犳暟:</label>
+ <span>{{ currentMeter.powerFactor }}</span>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鐘舵��:</label>
+ <el-tag :type="currentMeter.status === '姝e父' ? 'success' : 'danger'">
+ {{ currentMeter.status }}
+ </el-tag>
+ </div>
+ </el-col>
+ <el-col :span="12">
+ <div class="detail-item">
+ <label>鏈�鍚庢洿鏂版椂闂�:</label>
+ <span>{{ currentMeter.lastUpdateTime }}</span>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 鐢ㄧ數瓒嬪娍鍥� -->
+ <div style="margin-top: 20px;">
+ <h4>24灏忔椂鐢ㄧ數瓒嬪娍</h4>
+ <div ref="chartContainer" style="height: 300px;"></div>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+export default {
+ name: 'MeterCollection',
+ data() {
+ return {
+ loading: false,
+ searchForm: {
+ meterNo: '',
+ location: '',
+ dateRange: []
+ },
+ meterList: [],
+ pagination: {
+ currentPage: 1,
+ pageSize: 10,
+ total: 0
+ },
+ detailDialogVisible: false,
+ currentMeter: {},
+ chart: null
+ }
+ },
+ created() {
+ // 绔嬪嵆鐢熸垚涓�浜涙祴璇曟暟鎹�
+ this.meterList = [
+ {
+ id: 1,
+ meterNo: 'M001',
+ location: '杞﹂棿A',
+ meterType: '鏅鸿兘鐢佃〃',
+ voltage: '380V',
+ currentReading: 8500,
+ lastReading: 8400,
+ consumption: 100,
+ power: '75.5',
+ powerFactor: '0.85',
+ status: '姝e父',
+ lastUpdateTime: '2025-01-15 10:30:00'
+ },
+ {
+ id: 2,
+ meterNo: 'M002',
+ location: '杞﹂棿B',
+ meterType: '澶氬姛鑳界數琛�',
+ voltage: '220V',
+ currentReading: 6200,
+ lastReading: 6100,
+ consumption: 100,
+ power: '45.2',
+ powerFactor: '0.92',
+ status: '姝e父',
+ lastUpdateTime: '2025-01-15 10:25:00'
+ }
+ ]
+ this.pagination.total = this.meterList.length
+ },
+ mounted() {
+ // 寤惰繜涓�鐐规椂闂村啀璋冪敤锛岀‘淇滵OM宸茬粡娓叉煋
+ this.$nextTick(() => {
+ this.getMeterList()
+ })
+ },
+ watch: {
+ meterList: {
+ handler(newVal) {
+ console.log('meterList鏁版嵁鍙樺寲:', newVal)
+ },
+ deep: true,
+ immediate: true
+ }
+ },
+ methods: {
+ // 鑾峰彇鐢佃〃鍒楄〃
+ getMeterList() {
+ this.loading = true
+ // 妯℃嫙API璋冪敤
+ setTimeout(() => {
+ const mockData = this.generateMockData()
+ this.meterList = mockData
+ this.pagination.total = this.meterList.length
+ this.loading = false
+ }, 500)
+ },
+
+ // 鐢熸垚妯℃嫙鏁版嵁
+ generateMockData() {
+ const locations = ['杞﹂棿A', '杞﹂棿B', '鍔炲叕鍖�', '閰嶇數瀹�']
+ const meterTypes = ['鏅鸿兘鐢佃〃', '澶氬姛鑳界數琛�', '鏅�氱數琛�']
+ const voltages = ['220V', '380V', '10kV']
+ const statuses = ['姝e父', '寮傚父']
+
+ const data = []
+ for (let i = 1; i <= 25; i++) {
+ const currentReading = Math.floor(Math.random() * 10000) + 5000
+ const lastReading = currentReading - Math.floor(Math.random() * 100) - 10
+ const consumption = currentReading - lastReading
+ const power = Math.random() * 100 + 20
+ const powerFactor = (Math.random() * 0.3 + 0.7).toFixed(2)
+
+ data.push({
+ id: i,
+ meterNo: `M${String(i).padStart(3, '0')}`,
+ location: locations[Math.floor(Math.random() * locations.length)],
+ meterType: meterTypes[Math.floor(Math.random() * meterTypes.length)],
+ voltage: voltages[Math.floor(Math.random() * voltages.length)],
+ currentReading: currentReading,
+ lastReading: lastReading,
+ consumption: consumption,
+ power: power.toFixed(2),
+ powerFactor: powerFactor,
+ status: statuses[Math.floor(Math.random() * statuses.length)],
+ lastUpdateTime: this.formatDate(new Date(Date.now() - Math.random() * 86400000))
+ })
+ }
+ return data
+ },
+
+ // 鏍煎紡鍖栨棩鏈�
+ formatDate(date) {
+ const year = date.getFullYear()
+ const month = String(date.getMonth() + 1).padStart(2, '0')
+ const day = String(date.getDate()).padStart(2, '0')
+ const hours = String(date.getHours()).padStart(2, '0')
+ const minutes = String(date.getMinutes()).padStart(2, '0')
+ return `${year}-${month}-${day} ${hours}:${minutes}`
+ },
+
+ // 鎼滅储
+ handleSearch() {
+ this.pagination.currentPage = 1
+ this.getMeterList()
+ },
+
+ // 閲嶇疆鎼滅储
+ resetSearch() {
+ this.searchForm = {
+ meterNo: '',
+ location: '',
+ dateRange: []
+ }
+ this.handleSearch()
+ },
+
+ // 鏌ョ湅璇︽儏
+ viewDetails(row) {
+ this.currentMeter = row
+ this.detailDialogVisible = true
+ },
+
+ // 瀵硅瘽妗嗘墦寮�鍚庡垵濮嬪寲鍥捐〃
+ onDialogOpened() {
+ this.$nextTick(() => {
+ setTimeout(() => {
+ this.initChart()
+ }, 100)
+ })
+ },
+
+ // 鎵嬪姩閲囬泦
+ manualCollection(row) {
+ this.$message.success(`姝e湪閲囬泦鐢佃〃 ${row.meterNo} 鐨勬暟鎹�...`)
+ // 妯℃嫙閲囬泦杩囩▼
+ setTimeout(() => {
+ row.currentReading = Math.floor(Math.random() * 100) + row.currentReading
+ row.lastUpdateTime = this.formatDate(new Date())
+ this.$message.success('鏁版嵁閲囬泦瀹屾垚')
+ }, 1000)
+ },
+
+ // 鍒锋柊鏁版嵁
+ refreshData() {
+ this.getMeterList()
+ this.$message.success('鏁版嵁宸插埛鏂�')
+ },
+
+ // 娣诲姞娴嬭瘯鏁版嵁
+ addTestData() {
+ const testData = {
+ id: Date.now(),
+ meterNo: `M${String(this.meterList.length + 1).padStart(3, '0')}`,
+ location: '娴嬭瘯浣嶇疆',
+ meterType: '娴嬭瘯鐢佃〃',
+ voltage: '220V',
+ currentReading: Math.floor(Math.random() * 10000) + 1000,
+ lastReading: Math.floor(Math.random() * 5000) + 500,
+ consumption: Math.floor(Math.random() * 100) + 10,
+ power: (Math.random() * 100 + 10).toFixed(2),
+ powerFactor: (Math.random() * 0.3 + 0.7).toFixed(2),
+ status: '姝e父',
+ lastUpdateTime: this.formatDate(new Date())
+ }
+ this.meterList.push(testData)
+ this.pagination.total = this.meterList.length
+ this.$message.success('娴嬭瘯鏁版嵁宸叉坊鍔�')
+ },
+
+ // 娓呯┖鏁版嵁
+ clearData() {
+ this.meterList = []
+ this.pagination.total = 0
+ this.$message.success('鏁版嵁宸叉竻绌�')
+ },
+
+ // 娴嬭瘯鍥捐〃
+ testChart() {
+ this.$message.info('鍥捐〃娴嬭瘯鍔熻兘')
+ // 鍒涘缓涓�涓祴璇曞璇濇鏉ユ祴璇曞浘琛�
+ this.currentMeter = {
+ meterNo: 'TEST001',
+ location: '娴嬭瘯浣嶇疆',
+ meterType: '娴嬭瘯鐢佃〃',
+ voltage: '220V',
+ currentReading: 1000,
+ lastReading: 900,
+ consumption: 100,
+ power: '50.0',
+ powerFactor: '0.85',
+ status: '姝e父',
+ lastUpdateTime: '2025-01-15 12:00:00'
+ }
+ this.detailDialogVisible = true
+ },
+
+ // 鍒嗛〉澶у皬鏀瑰彉
+ handleSizeChange(val) {
+ this.pagination.pageSize = val
+ this.getMeterList()
+ },
+
+ // 褰撳墠椤垫敼鍙�
+ handleCurrentChange(val) {
+ this.pagination.pageSize = val.limit
+ this.pagination.currentPage = val.page
+ this.getMeterList()
+ },
+
+ // 鍒濆鍖栧浘琛�
+ initChart() {
+ try {
+ if (this.chart) {
+ this.chart.dispose()
+ this.chart = null
+ }
+
+ // 纭繚DOM鍏冪礌瀛樺湪
+ if (!this.$refs.chartContainer) {
+ console.error('鍥捐〃瀹瑰櫒涓嶅瓨鍦紝绛夊緟DOM鏇存柊...')
+ // 濡傛灉瀹瑰櫒涓嶅瓨鍦紝绛夊緟涓�涓嬪啀璇�
+ setTimeout(() => {
+ this.initChart()
+ }, 100)
+ return
+ }
+
+ // 妫�鏌ュ鍣ㄥ昂瀵�
+ const container = this.$refs.chartContainer
+ if (container.offsetWidth === 0 || container.offsetHeight === 0) {
+ setTimeout(() => {
+ this.initChart()
+ }, 100)
+ return
+ }
+ this.chart = echarts.init(container)
+
+ // 鐢熸垚24灏忔椂妯℃嫙鏁版嵁
+ const hours = []
+ const consumption = []
+ for (let i = 0; i < 24; i++) {
+ hours.push(`${i}:00`)
+ consumption.push(Math.floor(Math.random() * 50) + 20)
+ }
+
+ const option = {
+ title: {
+ text: '24灏忔椂鐢ㄧ數閲忚秼鍔�',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis',
+ formatter: '{b}<br/>鐢ㄧ數閲�: {c} kWh'
+ },
+ xAxis: {
+ type: 'category',
+ data: hours,
+ axisLabel: {
+ rotate: 45
+ }
+ },
+ yAxis: {
+ type: 'value',
+ name: '鐢ㄧ數閲� (kWh)'
+ },
+ series: [{
+ data: consumption,
+ type: 'line',
+ smooth: true,
+ areaStyle: {
+ opacity: 0.3
+ },
+ itemStyle: {
+ color: '#409EFF'
+ }
+ }]
+ }
+
+ this.chart.setOption(option)
+ } catch (error) {
+ console.error('鍥捐〃鍒濆鍖栧け璐�:', error)
+ this.$message.error('鍥捐〃鍒濆鍖栧け璐�: ' + error.message)
+ }
+ }
+ },
+
+ beforeUnmount() {
+ if (this.chart) {
+ try {
+ this.chart.dispose()
+ this.chart = null
+ } catch (error) {
+ console.error('娓呯悊鍥捐〃澶辫触:', error)
+ }
+ }
+ }
+}
+</script>
+
+<style scoped>
+.search-row {
+ margin-bottom: 20px;
+}
+
+.pagination {
+ margin-top: 20px;
+ text-align: right;
+}
+
+.el-table {
+ margin-top: 20px;
+}
+
+.detail-item {
+ margin-bottom: 15px;
+ padding: 10px;
+ border: 1px solid #ebeef5;
+ border-radius: 4px;
+ background-color: #fafafa;
+}
+
+.detail-item label {
+ font-weight: bold;
+ color: #606266;
+ margin-right: 10px;
+ min-width: 100px;
+ display: inline-block;
+}
+
+.detail-item span {
+ color: #303133;
+}
+
+.detail-item .el-tag {
+ margin-left: 0;
+}
+</style>
diff --git a/src/views/energyManagement/waterManagement/components/formDia.vue b/src/views/energyManagement/waterManagement/components/formDia.vue
new file mode 100644
index 0000000..bf605ca
--- /dev/null
+++ b/src/views/energyManagement/waterManagement/components/formDia.vue
@@ -0,0 +1,214 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="鐢ㄦ按璁惧"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="璁惧锛�" prop="deviceModel">
+ <el-select
+ v-model="form.deviceModel"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="setName"
+ :disabled="operationType !== 'add'"
+ >
+ <el-option
+ v-for="item in codeList"
+ :key="item.deviceModel"
+ :label="item.deviceName"
+ :value="item.deviceModel"
+ >
+ {{item.deviceName + '--' + item.deviceModel}}
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="姣忔棩闄愬埗姘撮噺锛�" prop="waterDayLimit">
+ <el-input
+ v-model="form.waterDayLimit"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="棰濆畾娴侀噺锛�" prop="ratedRate">
+ <el-input
+ v-model="form.ratedRate"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹為檯娴侀噺锛�" prop="actualTraffic">
+ <el-input
+ v-model="form.actualTraffic"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="杩愯鏃堕棿锛�" prop="runTime">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.runTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰撴棩鐢ㄦ按閲忥細" prop="waterDay">
+ <el-input
+ v-model="form.waterDay"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="姘磋垂鍗曚环锛�" prop="waterPrice">
+ <el-input
+ v-model="form.waterPrice"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ按绫诲瀷锛�" prop="type">
+ <el-select
+ v-model="form.type"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option label="宸ヤ笟鐢ㄦ按" value="industrial" />
+ <el-option label="鐢熸椿鐢ㄦ按" value="domestic" />
+ <el-option label="娑堥槻鐢ㄦ按" value="fire" />
+ <el-option label="缁垮寲鐢ㄦ按" value="greening" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, nextTick} from "vue";
+import useUserStore from "@/store/modules/user.js";
+import {waterDeviceList, waterEquipmentAdd, waterEquipmentUpdate} from "@/api/energyManagement/waterManagement.js";
+import { getCurrentDate } from "@/utils/index.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const userStore = useUserStore();
+
+const data = reactive({
+ form: {
+ deviceName: "",
+ deviceModel: "",
+ waterDayLimit: "",
+ ratedRate: "",
+ actualTraffic: "",
+ runTime: "",
+ waterDay: "",
+ waterPrice: "",
+ type: "",
+ },
+ rules: {
+ deviceModel: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ runTime: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ waterDayLimit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ ratedRate: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ actualTraffic: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ waterDay: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ waterPrice: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ type: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+})
+const { form, rules } = toRefs(data);
+const codeList = ref([])
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ form.value = {}
+ proxy.resetForm("formRef");
+ waterDeviceList({size: -1}).then((res) => {
+ codeList.value = res.data.records;
+ });
+ if (type === "edit") {
+ form.value = {...row}
+ }
+}
+const setName = (code) => {
+ const index = codeList.value.findIndex(item => item.deviceModel === code);
+ if (index > -1) {
+ console.log(codeList)
+ form.value.name = codeList.value[index].deviceName;
+ }
+}
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ waterEquipmentAdd(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ closeDia()
+ })
+ } else {
+ waterEquipmentUpdate(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ closeDia()
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/energyManagement/waterManagement/components/waterBillForm.vue b/src/views/energyManagement/waterManagement/components/waterBillForm.vue
new file mode 100644
index 0000000..667fe10
--- /dev/null
+++ b/src/views/energyManagement/waterManagement/components/waterBillForm.vue
@@ -0,0 +1,203 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="姘磋垂绠$悊"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="璁惧锛�" prop="code">
+ <el-select
+ v-model="form.code"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="setName"
+ :disabled="operationType !== 'add'"
+ >
+ <el-option
+ v-for="item in codeList"
+ :key="item.deviceModel"
+ :label="item.deviceName"
+ :value="item.deviceModel"
+ >
+ {{item.deviceName + '--' + item.deviceModel}}
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ按閲忥細" prop="waterConsumption">
+ <el-input
+ v-model="form.waterConsumption"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="姘磋垂鍗曚环锛�" prop="waterPrice">
+ <el-input
+ v-model="form.waterPrice"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="姘磋垂閲戦锛�" prop="waterBill">
+ <el-input
+ v-model="form.waterBill"
+ placeholder="鑷姩璁$畻"
+ clearable
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="璁¤垂鏃ユ湡锛�" prop="billDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.billDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ按绫诲瀷锛�" prop="waterType">
+ <el-select
+ v-model="form.waterType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option label="宸ヤ笟鐢ㄦ按" value="industrial" />
+ <el-option label="鐢熸椿鐢ㄦ按" value="domestic" />
+ <el-option label="娑堥槻鐢ㄦ按" value="fire" />
+ <el-option label="缁垮寲鐢ㄦ按" value="greening" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, nextTick, watch} from "vue";
+import useUserStore from "@/store/modules/user.js";
+import {waterDeviceList, waterBillAdd, waterBillUpdate} from "@/api/energyManagement/waterManagement.js";
+import { getCurrentDate } from "@/utils/index.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const userStore = useUserStore();
+
+const data = reactive({
+ form: {
+ name: "",
+ code: "",
+ waterConsumption: "",
+ waterPrice: "",
+ waterBill: "",
+ billDate: "",
+ waterType: "",
+ },
+ rules: {
+ code: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ waterConsumption: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ waterPrice: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ billDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ waterType: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+})
+const { form, rules } = toRefs(data);
+const codeList = ref([])
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ form.value = {}
+ proxy.resetForm("formRef");
+ waterDeviceList().then((res) => {
+ codeList.value = res.data;
+ });
+ if (type === "edit") {
+ form.value = {...row}
+ }
+}
+const setName = (code) => {
+ const index = codeList.value.findIndex(item => item.deviceModel === code);
+ if (index > -1) {
+ console.log(codeList)
+ form.value.name = codeList.value[index].deviceName;
+ }
+}
+
+// 璁$畻姘磋垂閲戦
+const calculateWaterBill = () => {
+ if (form.value.waterConsumption && form.value.waterPrice) {
+ form.value.waterBill = (parseFloat(form.value.waterConsumption) * parseFloat(form.value.waterPrice)).toFixed(2);
+ }
+}
+
+// 鐩戝惉鐢ㄦ按閲忓拰姘磋垂鍗曚环鍙樺寲
+watch([() => form.value.waterConsumption, () => form.value.waterPrice], () => {
+ calculateWaterBill();
+});
+
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ waterBillAdd(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ closeDia()
+ })
+ } else {
+ waterBillUpdate(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ closeDia()
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/energyManagement/waterManagement/index.vue b/src/views/energyManagement/waterManagement/index.vue
new file mode 100644
index 0000000..4ada029
--- /dev/null
+++ b/src/views/energyManagement/waterManagement/index.vue
@@ -0,0 +1,329 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">璁惧鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.deviceName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板</el-button>
+ <el-button type="info" plain icon="Upload" @click="handleImport">瀵煎叆</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ <el-dialog
+ :title="upload.title"
+ v-model="upload.open"
+ width="400px"
+ append-to-body
+ @close="handleDialogClose"
+ >
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url"
+ :disabled="upload.isUploading"
+ :before-upload="upload.beforeUpload"
+ :on-progress="upload.onProgress"
+ :on-success="upload.onSuccess"
+ :on-error="upload.onError"
+ :on-change="upload.onChange"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link
+ type="primary"
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ @click="importTemplate"
+ >涓嬭浇妯℃澘</el-link
+ >
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {Search} from "@element-plus/icons-vue";
+import {onMounted, ref, reactive, nextTick, getCurrentInstance} from "vue";
+import FormDia from "@/views/energyManagement/waterManagement/components/formDia.vue";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import {waterEquipmentDelete, waterEquipmentListPage} from "@/api/energyManagement/waterManagement.js";
+const { proxy } = getCurrentInstance();
+
+const data = reactive({
+ searchForm: {
+ name: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "璁惧鍚嶇О",
+ prop: "deviceName",
+ width: 200,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "deviceModel",
+ width: 200,
+ },
+ {
+ label: "棰濆畾娴侀噺",
+ prop: "ratedRate",
+ },
+ {
+ label: "瀹為檯娴侀噺",
+ prop: "actualTraffic",
+ },
+ {
+ label: "杩愯鏃堕棿",
+ prop: "runTime",
+ width:150
+ },
+ {
+ label: "褰撴棩鐢ㄦ按閲�",
+ prop: "waterDay",
+ width: 150,
+ },
+ {
+ label: "姣忔棩闄愬埗姘撮噺",
+ prop: "waterDayLimit",
+ width:220
+ },
+ {
+ label: "姘磋垂鍗曚环",
+ prop: "waterPrice",
+ width: 120,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+const formDia = ref()
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙瀹㈡埛瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙瀹㈡埛瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/waterRecord/importData",
+ // 鏂囦欢涓婁紶鍓嶇殑鍥炶皟
+ beforeUpload: (file) => {
+ console.log('鏂囦欢鍗冲皢涓婁紶', file);
+ // 鍙互鍦ㄦ澶勫仛鏂囦欢绫诲瀷鎴栧ぇ灏忔牎楠�
+ const isValid = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || file.name.endsWith('.xlsx') || file.name.endsWith('.xls');
+ if (!isValid) {
+ proxy.$modal.msgError("鍙兘涓婁紶 Excel 鏂囦欢");
+ }
+ return isValid;
+ },
+ // 鏂囦欢鐘舵�佹敼鍙樻椂鐨勫洖璋�
+ onChange: (file, fileList) => {
+ console.log('鏂囦欢鐘舵�佹敼鍙�', file, fileList);
+ },
+ // 鏂囦欢涓婁紶鎴愬姛鏃剁殑鍥炶皟
+ onSuccess: (response, file, fileList) => {
+ console.log('涓婁紶鎴愬姛', response, file, fileList);
+ if(response.code === 200){
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ }else if(response.code === 500){
+ proxy.$modal.msgError(response.msg);
+ }else{
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+ upload.open = false;
+ getList();
+ },
+ // 鏂囦欢涓婁紶澶辫触鏃剁殑鍥炶皟
+ onError: (error, file, fileList) => {
+ console.log('涓婁紶澶辫触', error, file, fileList);
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ upload.open = false;
+ },
+ // 鏂囦欢涓婁紶杩涘害鏀瑰彉鏃剁殑鍥炶皟
+ onProgress: (event, file, fileList) => {
+ console.log('涓婁紶杩涘害', event, file, fileList);
+ upload.isUploading = true;
+ },
+});
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ waterEquipmentListPage({ ...searchForm.value, ...page }).then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ }).catch(() => {
+ tableLoading.value = false;
+ })
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImport() {
+ upload.title = "鐢ㄦ按璁惧";
+ upload.open = true;
+ // 娓呯┖涓婃涓婁紶鐨勬枃浠跺垪琛�
+ nextTick(() => {
+ proxy.$refs["uploadRef"]?.clearFiles();
+ });
+}
+function importTemplate() {
+ proxy.download(
+ "/waterRecord/export",
+ {},
+ '鐢ㄦ按璁惧瀵煎叆妯$増.xlsx'
+ );
+}
+/** 鎻愪氦涓婁紶鏂囦欢 */
+function submitFileForm() {
+ proxy.$refs["uploadRef"].submit();
+}
+
+/** 寮规鍏抽棴鏃舵竻绌烘枃浠跺垪琛� */
+function handleDialogClose() {
+ nextTick(() => {
+ proxy.$refs["uploadRef"]?.clearFiles();
+ });
+}
+
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ waterEquipmentDelete(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/waterRecord/export", {}, "鐢ㄦ按绠$悊.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/energyManagement/waterManagement/waterBill.vue b/src/views/energyManagement/waterManagement/waterBill.vue
new file mode 100644
index 0000000..ea382f0
--- /dev/null
+++ b/src/views/energyManagement/waterManagement/waterBill.vue
@@ -0,0 +1,181 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">璁惧鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ </div>
+</template>
+
+<script setup>
+import {Search} from "@element-plus/icons-vue";
+import {onMounted, ref, reactive, nextTick} from "vue";
+import FormDia from "@/views/energyManagement/waterManagement/components/waterBillForm.vue";
+import {ElMessageBox} from "element-plus";
+import {waterBillDelete, waterBillListPage} from "@/api/energyManagement/waterManagement.js";
+const { proxy } = getCurrentInstance();
+
+const data = reactive({
+ searchForm: {
+ name: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "璁惧鍚嶇О",
+ prop: "name",
+ width: 200,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "code",
+ width: 200,
+ },
+ {
+ label: "鐢ㄦ按閲�",
+ prop: "waterConsumption",
+ },
+ {
+ label: "姘磋垂鍗曚环",
+ prop: "waterPrice",
+ },
+ {
+ label: "姘磋垂閲戦",
+ prop: "waterBill",
+ width:150
+ },
+ {
+ label: "璁¤垂鏃ユ湡",
+ prop: "billDate",
+ width: 150,
+ },
+ {
+ label: "鐢ㄦ按绫诲瀷",
+ prop: "waterType",
+ width:120
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+const formDia = ref()
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ waterBillListPage({ ...searchForm.value, ...page }).then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ });
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ waterBillDelete(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/energyManagement/waterManagement/waterTrends.vue b/src/views/energyManagement/waterManagement/waterTrends.vue
new file mode 100644
index 0000000..12e45fc
--- /dev/null
+++ b/src/views/energyManagement/waterManagement/waterTrends.vue
@@ -0,0 +1,118 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">璁惧鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import {Search} from "@element-plus/icons-vue";
+import {onMounted, ref, reactive} from "vue";
+import {listPageByWaterTrend} from "@/api/energyManagement/waterManagement.js";
+
+const data = reactive({
+ searchForm: {
+ name: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "璁惧鍚嶇О",
+ prop: "name",
+ width: 220,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "code",
+ width: 220,
+ },
+ {
+ label: "杩愯鏃堕棿",
+ prop: "runDate",
+ width: 250,
+ },
+ {
+ label: "鏄ㄦ棩鐢ㄦ按閲�",
+ prop: "toDayNum",
+ },
+ {
+ label: "鏈湀骞冲潎姘撮噺",
+ prop: "avgNum",
+ width:150
+ },
+ {
+ label: "瓒嬪娍",
+ prop: "trend",
+ width: 220,
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ listPageByWaterTrend({ ...searchForm.value, ...page }).then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/equipmentManagement/attendanceManagement/index.vue b/src/views/equipmentManagement/attendanceManagement/index.vue
new file mode 100644
index 0000000..a346d0b
--- /dev/null
+++ b/src/views/equipmentManagement/attendanceManagement/index.vue
@@ -0,0 +1,403 @@
+<template>
+ <div class="app-container">
+ <!-- 鏈嶅姟璇勪环姒傝锛氭ā鎷熷憳宸ヤ笟缁╄瘎鍒� -->
+ <el-row :gutter="16" class="mb16">
+ <el-col :span="8">
+ <el-card shadow="never">
+ <div class="kpi-title">鏈湀骞冲潎璇勫垎</div>
+ <div class="kpi-value">
+ {{ overallAvgScore.toFixed(1) }}
+ <span class="kpi-unit">鍒�</span>
+ </div>
+ <el-rate v-model="overallAvgScore" disabled show-score score-template="{value} / 5" />
+ </el-card>
+ </el-col>
+ <el-col :span="8">
+ <el-card shadow="never">
+ <div class="kpi-title">宸茶瘎浠风淮淇伐鍗�</div>
+ <div class="kpi-value">
+ {{ ratedCount }}
+ <span class="kpi-unit">鍗�</span>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="8">
+ <el-card shadow="never">
+ <div class="kpi-title">寰呰瘎浠风淮淇伐鍗�</div>
+ <div class="kpi-value kpi-warning">
+ {{ pendingCount }}
+ <span class="kpi-unit">鍗�</span>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 鏌ヨ鏉′欢锛氱鐞嗗憳鎸夊伐绋嬪笀 / 瀹㈡埛 / 鏃堕棿杩芥函璇勪环 -->
+ <div class="search_form">
+ <div>
+ <span class="search_title">缁翠慨宸ョ▼甯堬細</span>
+ <el-input
+ v-model="searchForm.engineerName"
+ placeholder="璇疯緭鍏ュ伐绋嬪笀濮撳悕"
+ style="width: 180px"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+
+ <span class="search_title ml10">瀹㈡埛鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ style="width: 180px"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+
+ <span class="search_title ml10">瀹屾垚鏃堕棿锛�</span>
+ <el-date-picker
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ />
+
+ <span class="search_title ml10">璇勪环鐘舵�侊細</span>
+ <el-select
+ v-model="searchForm.status"
+ placeholder="璇烽�夋嫨"
+ style="width: 140px"
+ clearable
+ >
+ <el-option label="寰呰瘎浠�" value="pending" />
+ <el-option label="宸茶瘎浠�" value="rated" />
+ </el-select>
+
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div>
+ <el-button icon="Download" @click="handleExport">
+ 瀵煎嚭璇勪环缁熻
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 缁翠慨璇勪环鍒楄〃锛氭ā鎷熲�滅淮淇畬鎴愬悗瑙﹀彂璇勪环鈥濆満鏅� -->
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ style="width: 100%"
+ height="calc(100vh - 24em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ >
+ <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+ <el-table-column prop="orderNo" label="缁翠慨宸ュ崟鍙�" width="160" show-overflow-tooltip />
+ <el-table-column prop="deviceName" label="璁惧鍚嶇О" width="160" show-overflow-tooltip />
+ <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" width="180" show-overflow-tooltip />
+ <el-table-column prop="engineerName" label="缁翠慨宸ョ▼甯�" width="120" />
+ <el-table-column prop="completeTime" label="缁翠慨瀹屾垚鏃堕棿" width="180" />
+ <el-table-column prop="score" label="鏄熺骇璇勫垎" width="140" align="center">
+ <template #default="scope">
+ <el-rate v-if="scope.row.score" v-model="scope.row.score" disabled />
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="璇勪环鐘舵��" width="100" align="center">
+ <template #default="scope">
+ <el-tag
+ :type="scope.row.status === 'rated' ? 'success' : 'warning'"
+ size="small"
+ >
+ {{ scope.row.status === 'rated' ? '宸茶瘎浠�' : '寰呰瘎浠�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="feedback" label="瀹㈡埛鍙嶉" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="160" align="center" fixed="right">
+ <template #default="scope">
+ <el-button
+ v-if="scope.row.status === 'pending'"
+ type="primary"
+ link
+ size="small"
+ @click="openEvaluate(scope.row)"
+ >
+ 鍘昏瘎浠�
+ </el-button>
+ <el-button
+ v-else
+ type="primary"
+ link
+ size="small"
+ @click="openEvaluate(scope.row)"
+ >
+ 鏌ョ湅 / 淇敼璇勪环
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 璇勪环寮规锛氭ā鎷熲�滅淮淇畬鎴愬悗寮瑰嚭瀹㈡埛绔瘎浠封�� -->
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="520px"
+ destroy-on-close
+ >
+ <div class="dialog-order-info" v-if="currentOrder">
+ <div>缁翠慨宸ュ崟锛歿{ currentOrder.orderNo }}</div>
+ <div>璁惧鍚嶇О锛歿{ currentOrder.deviceName }}</div>
+ <div>缁翠慨宸ョ▼甯堬細{{ currentOrder.engineerName }}</div>
+ </div>
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px"
+ >
+ <el-form-item label="鏄熺骇璇勫垎锛�" prop="score">
+ <el-rate v-model="form.score" :max="5" />
+ </el-form-item>
+ <el-form-item label="鏂囧瓧鍙嶉锛�" prop="feedback">
+ <el-input
+ v-model="form.feedback"
+ type="textarea"
+ :rows="4"
+ placeholder="璇峰~鍐欏鏈缁翠慨鏈嶅姟鐨勮瘎浠凤紝濡傚搷搴旈�熷害銆佷笓涓氱▼搴︺�佹矡閫氫綋楠岀瓑"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleSubmit">鎻� 浜� 璇� 浠�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from "vue";
+import { ElMessage } from "element-plus";
+
+// 妯℃嫙缁翠慨宸ュ崟 + 瀹㈡埛璇勪环鏁版嵁
+const rawOrders = ref([
+ {
+ id: 1,
+ orderNo: "WX-2024-1201-001",
+ deviceName: "绌哄帇鏈� A1 鍙�",
+ customerName: "鍗庡崡鐢靛瓙绉戞妧鏈夐檺鍏徃",
+ engineerName: "鐜嬪笀鍌�",
+ completeTime: "2024-12-01 10:30:00",
+ completeDate: "2024-12-01",
+ status: "rated",
+ score: 5,
+ feedback: "缁翠慨闈炲父涓撲笟锛屽搷搴旈�熷害蹇紝鐜板満瑙i噴涔熷緢娓呮櫚锛屾弧鎰忋��",
+ },
+ {
+ id: 2,
+ orderNo: "WX-2024-1201-002",
+ deviceName: "娉ㄥ鏈� B3 鍙�",
+ customerName: "鍗庝笢绮惧瘑鍒堕�犳湁闄愬叕鍙�",
+ engineerName: "鏉庡笀鍌�",
+ completeTime: "2024-12-01 15:20:00",
+ completeDate: "2024-12-01",
+ status: "rated",
+ score: 4,
+ feedback: "鏁翠綋杩樹笉閿欙紝灏辨槸鍒板満鏃堕棿绋嶅井闀夸簡涓�鐐癸紝甯屾湜鍚庨潰鑳藉啀蹇竴浜涖��",
+ },
+ {
+ id: 3,
+ orderNo: "WX-2024-1202-003",
+ deviceName: "鐒婃帴鏈哄櫒浜� C2 鍙�",
+ customerName: "瑗垮崡鏂拌兘婧愮鎶�鑲′唤",
+ engineerName: "寮犲笀鍌�",
+ completeTime: "2024-12-02 11:05:00",
+ completeDate: "2024-12-02",
+ status: "pending",
+ score: null,
+ feedback: "",
+ },
+ {
+ id: 4,
+ orderNo: "WX-2024-1203-005",
+ deviceName: "娴嬭瘯鍙� D1 鍙�",
+ customerName: "鍖楁柟姹借溅闆堕儴浠舵湁闄愬叕鍙�",
+ engineerName: "鐜嬪笀鍌�",
+ completeTime: "2024-12-03 09:50:00",
+ completeDate: "2024-12-03",
+ status: "pending",
+ score: null,
+ feedback: "",
+ },
+]);
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ engineerName: "",
+ customerName: "",
+ dateRange: [],
+ status: "",
+});
+
+// 鍒楄〃鏁版嵁
+const tableData = ref([...rawOrders.value]);
+
+// 缁熻锛氭暣浣撹瘎鍒嗐�佸凡璇勪环 / 寰呰瘎浠锋暟閲�
+const ratedOrders = computed(() =>
+ rawOrders.value.filter((o) => o.status === "rated" && o.score)
+);
+
+const overallAvgScore = computed(() => {
+ if (!ratedOrders.value.length) return 0;
+ const sum = ratedOrders.value.reduce((acc, cur) => acc + (cur.score || 0), 0);
+ return sum / ratedOrders.value.length;
+});
+
+const ratedCount = computed(() => ratedOrders.value.length);
+const pendingCount = computed(
+ () => rawOrders.value.filter((o) => o.status === "pending").length
+);
+
+// 鏌ヨ / 閲嶇疆
+const recomputeTable = () => {
+ const list = rawOrders.value.filter((item) => {
+ if (
+ searchForm.engineerName &&
+ !item.engineerName.includes(searchForm.engineerName.trim())
+ ) {
+ return false;
+ }
+ if (
+ searchForm.customerName &&
+ !item.customerName.includes(searchForm.customerName.trim())
+ ) {
+ return false;
+ }
+ if (searchForm.status && item.status !== searchForm.status) {
+ return false;
+ }
+ if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
+ const [start, end] = searchForm.dateRange;
+ if (item.completeDate < start || item.completeDate > end) {
+ return false;
+ }
+ }
+ return true;
+ });
+ tableData.value = list;
+};
+
+const handleQuery = () => {
+ recomputeTable();
+};
+
+const resetSearch = () => {
+ searchForm.engineerName = "";
+ searchForm.customerName = "";
+ searchForm.dateRange = [];
+ searchForm.status = "";
+ recomputeTable();
+};
+
+// 瀵煎嚭锛堟紨绀猴級
+const handleExport = () => {
+ ElMessage.success("褰撳墠涓烘紨绀洪〉闈紝璇勪环瀵煎嚭鍔熻兘鏈鎺ュ疄闄呮帴鍙�");
+};
+
+// 璇勪环寮规
+const dialogVisible = ref(false);
+const dialogTitle = ref("缁翠慨鏈嶅姟璇勪环");
+const currentOrder = ref(null);
+const formRef = ref(null);
+const form = reactive({
+ score: 0,
+ feedback: "",
+});
+
+const rules = {
+ score: [{ required: true, message: "璇烽�夋嫨鏄熺骇璇勫垎", trigger: "change" }],
+ feedback: [{ required: true, message: "璇峰~鍐欐枃瀛楀弽棣�", trigger: "blur" }],
+};
+
+// 鎵撳紑璇勪环锛氭ā鎷熲�滅淮淇畬鎴愮‘璁ゅ悗寮瑰嚭璇勪环寮规鈥�
+const openEvaluate = (row) => {
+ currentOrder.value = row;
+ dialogTitle.value =
+ row.status === "pending" ? "缁翠慨鏈嶅姟璇勪环" : "鏌ョ湅 / 淇敼璇勪环";
+ form.score = row.score || 0;
+ form.feedback = row.feedback || "";
+ dialogVisible.value = true;
+};
+
+// 鎻愪氦璇勪环锛氬悓姝ュ埌鏈湴鈥滃憳宸ヤ笟缁╃粺璁♀��
+const handleSubmit = () => {
+ if (!formRef.value) return;
+ formRef.value.validate((valid) => {
+ if (!valid || !currentOrder.value) return;
+
+ const target = rawOrders.value.find((o) => o.id === currentOrder.value.id);
+ if (target) {
+ target.score = form.score;
+ target.feedback = form.feedback;
+ target.status = "rated";
+ }
+
+ ElMessage.success("璇勪环鎻愪氦鎴愬姛锛屽凡鍚屾鑷冲憳宸ヤ笟缁╃粺璁�");
+ dialogVisible.value = false;
+ recomputeTable();
+ });
+};
+
+// 鍒濆鍖栧垪琛�
+recomputeTable();
+</script>
+
+<style scoped lang="scss">
+.mb16 {
+ margin-bottom: 16px;
+}
+
+.kpi-title {
+ font-size: 13px;
+ color: #909399;
+}
+
+.kpi-value {
+ margin-top: 6px;
+ font-size: 24px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.kpi-unit {
+ font-size: 12px;
+ margin-left: 4px;
+ color: #909399;
+}
+
+.kpi-warning {
+ color: #e6a23c;
+}
+
+.dialog-order-info {
+ margin-bottom: 12px;
+ font-size: 13px;
+ color: #606266;
+ line-height: 1.8;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+</style>
+
diff --git a/src/views/equipmentManagement/brand/index.vue b/src/views/equipmentManagement/brand/index.vue
new file mode 100644
index 0000000..f93518a
--- /dev/null
+++ b/src/views/equipmentManagement/brand/index.vue
@@ -0,0 +1,217 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍝佺墝鍚嶇О/鍥藉">
+ <el-input
+ v-model="filters.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ叧閿瘝"
+ clearable
+ prefix-icon="Search"
+ @change="getTableData"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="openAdd" icon="Plus"> 鏂板 </el-button>
+ <el-button
+ type="danger"
+ icon="Delete"
+ :disabled="multipleSelection.length <= 0"
+ @click="handleBatchDelete"
+ >鎵归噺鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <PIMTable
+ rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ </PIMTable>
+ </div>
+
+ <el-dialog v-model="visible" :title="dialogTitle" width="520px" destroy-on-close>
+ <el-form :model="form" ref="formRef" :rules="rules" label-width="90px">
+ <el-form-item label="鍝佺墝鍚嶇О" prop="name">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ュ搧鐗屽悕绉�" />
+ </el-form-item>
+ <el-form-item label="鎵�灞炲浗瀹�" prop="country">
+ <el-input v-model="form.country" placeholder="璇疯緭鍏ュ浗瀹�/鍦板尯" />
+ </el-form-item>
+ <el-form-item label="鎻忚堪" prop="description">
+ <el-input v-model="form.description" type="textarea" :rows="3" placeholder="鍙~鍐欏搧鐗岀畝浠�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" @click="handleSubmit">纭畾</el-button>
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+
+</template>
+
+<script setup>
+import { ref, getCurrentInstance, onMounted } from 'vue'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { usePaginationApi } from '@/hooks/usePaginationApi'
+import { getBrandPage, addBrand, editBrand, delBrand } from '@/api/equipmentManagement/brand'
+
+defineOptions({ name: '璁惧鍝佺墝绠$悊' })
+
+const { proxy } = getCurrentInstance()
+
+const multipleSelection = ref([])
+const formRef = ref()
+const visible = ref(false)
+const dialogTitle = ref('鏂板鍝佺墝')
+const form = ref({ id: undefined, name: '', country: '', description: '' })
+
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ュ搧鐗屽悕绉�', trigger: 'blur' }],
+ country: [{ required: true, message: '璇疯緭鍏ユ墍灞炲浗瀹�', trigger: 'blur' }]
+}
+
+const {
+ filters,
+ columns,
+ dataList,
+ pagination,
+ getTableData,
+ resetFilters,
+ onCurrentChange,
+} = usePaginationApi(
+ getBrandPage,
+ { name: undefined },
+ [
+ { label: '鍝佺墝鍚嶇О', align: 'center', prop: 'name' },
+ { label: '鎵�灞炲浗瀹�', align: 'center', prop: 'country' },
+ { label: '鎻忚堪', align: 'center', prop: 'description' },
+ { label: '鍒涘缓鏃堕棿', align: 'center', prop: 'createdAt' },
+ {
+ dataType: 'action',
+ label: '鎿嶄綔',
+ align: 'center',
+ fixed: 'right',
+ width: 140,
+ operation: [
+ {
+ name: '缂栬緫',
+ type: 'text',
+ clickFun: (row) => openEdit(row),
+ },
+ {
+ name: '鍒犻櫎',
+ type: 'text',
+ clickFun: (row) => handleDelete(row.id),
+ }
+ ]
+ }
+ ]
+)
+
+const handleSelectionChange = (list) => {
+ multipleSelection.value = list
+}
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page
+ pagination.pageSize = limit
+ onCurrentChange(page)
+}
+
+function resetForm() {
+ form.value = { id: undefined, name: '', country: '', description: '' }
+}
+
+function openAdd() {
+ resetForm()
+ dialogTitle.value = '鏂板鍝佺墝'
+ visible.value = true
+}
+
+function openEdit(row) {
+ form.value = { id: row.id, name: row.name, country: row.country, description: row.description }
+ dialogTitle.value = '缂栬緫鍝佺墝'
+ visible.value = true
+}
+
+function handleSubmit() {
+ formRef.value.validate(async (valid) => {
+ if (!valid) return
+ const isEdit = Boolean(form.value.id)
+ const api = isEdit ? editBrand : addBrand
+ const { code, msg } = await api({ ...form.value })
+ if (code === 200) {
+ ElMessage.success(isEdit ? '淇敼鎴愬姛' : '鏂板鎴愬姛')
+ visible.value = false
+ getTableData()
+ } else {
+ ElMessage.error(msg || '鎿嶄綔澶辫触')
+ }
+ })
+}
+
+function handleDelete(id) {
+ ElMessageBox.confirm('姝ゆ搷浣滃皢姘镐箙鍒犻櫎璇ュ搧鐗�, 鏄惁缁х画?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }).then(async () => {
+ const { code } = await delBrand(id)
+ if (code === 200) {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getTableData()
+ }
+ })
+}
+
+function handleBatchDelete() {
+ if (multipleSelection.value.length === 0) return
+ ElMessageBox.confirm('灏嗗垹闄ら�変腑鐨勫搧鐗岋紝鏄惁缁х画锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }).then(async () => {
+ const ids = multipleSelection.value.map((i) => i.id)
+ const { code } = await delBrand(ids)
+ if (code === 200) {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getTableData()
+ }
+ })
+}
+
+onMounted(() => {
+ getTableData()
+})
+
+</script>
+
+<style scoped lang="scss">
+.table_list { margin-top: unset; }
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+</style>
+
+
diff --git a/src/views/equipmentManagement/calibration/index.vue b/src/views/equipmentManagement/calibration/index.vue
new file mode 100644
index 0000000..b04da2e
--- /dev/null
+++ b/src/views/equipmentManagement/calibration/index.vue
@@ -0,0 +1,256 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">妫�瀹氭棩鏈燂細</span>
+ <el-date-picker
+ v-model="searchForm.recordDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 160px"
+ @change="handleQuery"
+ />
+ <span class="search_title ml10">褰曞叆鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.entryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 160px"
+ @change="handleQuery"
+ />
+ <span class="search_title ml10">璁¢噺鍣ㄥ叿缂栧彿锛�</span>
+ <el-input v-model="searchForm.code" placeholder="璇疯緭鍏ョ紪鍙�" clearable style="width: 240px" @change="handleQuery"/>
+<!-- <span class="search_title ml10">鐘舵�侊細</span>-->
+<!-- <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" @change="handleQuery" style="width: 160px" clearable>-->
+<!-- <el-option label="鏈夋晥" :value="1"></el-option>-->
+<!-- <el-option label="閫炬湡" :value="2"></el-option>-->
+<!-- </el-select>-->
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ <el-button @click="handleReset" style="margin-left: 10px">閲嶇疆</el-button>
+ </div>
+ <div>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ </div>
+ <calibration-dia ref="calibrationDia" @close="handleQuery"></calibration-dia>
+ </div>
+</template>
+
+<script setup>
+import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
+import {ElMessageBox, ElMessage} from "element-plus";
+import useUserStore from "@/store/modules/user.js";
+import CalibrationDia from "@/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue";
+import {ledgerRecordListPage, ledgerRecordDelete} from "@/api/equipmentManagement/calibration.js";
+const { proxy } = getCurrentInstance();
+const userStore = useUserStore()
+
+const data = reactive({
+ searchForm: {
+ recordDate: "",
+ code: "",
+ entryDate: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const tableColumn = ref([
+ // {
+ // label: "鐘舵��",
+ // prop: "status",
+ // dataType: "tag",
+ // formatData: (params) => {
+ // if (params == 1) {
+ // return "鏈夋晥";
+ // } else if (params == 2) {
+ // return "閫炬湡";
+ // } else {
+ // return null;
+ // }
+ // },
+ // formatType: (params) => {
+ // if (params == 1) {
+ // return "success";
+ // } else if (params == 2) {
+ // return "danger";
+ // } else {
+ // return null;
+ // }
+ // },
+ // },
+ {
+ label: "妫�瀹氭棩鏈�",
+ prop: "recordDate",
+ width: 130,
+ },
+ {
+ label: "璁¢噺鍣ㄥ叿缂栧彿",
+ prop: "code",
+ width: 150,
+ },
+ {
+ label: "璁¢噺鍣ㄥ叿鍚嶇О",
+ prop: "name",
+ width: 200,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ width:200
+ },
+ {
+ label: "鏈夋晥鏈�",
+ prop: "valid",
+ width: 100,
+ },
+ {
+ label: "褰曞叆浜�",
+ prop: "userName",
+ },
+ {
+ label: "褰曞叆鏃ユ湡",
+ prop: "entryDate",
+ width: 130,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ width: 140,
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openCalibrationDia("edit", row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "text",
+ style: {
+ color: "#F56C6C"
+ },
+ clickFun: (row) => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const selectedRows = ref([]);
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+const calibrationDia = ref()
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+// 閲嶇疆鎼滅储鏉′欢
+const handleReset = () => {
+ searchForm.value.recordDate = "";
+ searchForm.value.entryDate = "";
+ searchForm.value.code = "";
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ ledgerRecordListPage({ ...searchForm.value, ...page }).then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ }).catch((err) => {
+ tableLoading.value = false;
+ })
+};
+
+// 鎵撳紑妫�瀹氭牎鍑嗗脊妗�
+const openCalibrationDia = (type, row) => {
+ nextTick(() => {
+ calibrationDia.value?.openDialog(type, row)
+ })
+}
+
+// 鍒犻櫎璁板綍
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎璁¢噺鍣ㄥ叿缂栧彿涓�"${row.code}"鐨勬瀹氳褰曞悧锛焋, "鍒犻櫎纭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ ledgerRecordDelete([row.id]).then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ }).catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑堝垹闄�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/measuringInstrumentLedgerRecord/export", {}, "妫�瀹氭牎鍑嗚褰�.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/defectManagement/index.vue b/src/views/equipmentManagement/defectManagement/index.vue
new file mode 100644
index 0000000..8673000
--- /dev/null
+++ b/src/views/equipmentManagement/defectManagement/index.vue
@@ -0,0 +1,221 @@
+<template>
+ <div class="defect-management">
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="actions">
+ <el-button type="primary" @click="showRegisterDialog = true">鐧昏缂洪櫡</el-button>
+ </div>
+
+ <!-- 缂洪櫡鍒楄〃 -->
+ <el-table :data="defectList" style="width: 100%; margin-top: 10px;" border>
+ <el-table-column prop="deviceName" label="璁惧鍚嶇О" width="180"></el-table-column>
+ <el-table-column prop="defectDescription" label="缂洪櫡鎻忚堪" win-width="300"></el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="220">
+ <template #default="{ row }">
+ <el-tag :type="row.status === '涓ラ噸缂洪櫡' ? 'danger' : 'success'">
+ {{ row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="220">
+ <template #default="{ row }">
+ <el-button
+ v-if="row.status === '涓ラ噸缂洪櫡' || row.status === '涓�鑸己闄�'"
+ type="text"
+ @click="eliminateDefect(row)"
+ >
+ 娑堥櫎缂洪櫡
+ </el-button>
+ <!-- <el-button
+ v-if="row.status === '涓ラ噸缂洪櫡'"
+ type="text"
+ @click="transferToRepairOrder(row.id)"
+ >
+ 杞淮淇崟
+ </el-button> -->
+ <el-button type="text" @click="getLedger(row.deviceLedgerId)">
+ 鏌ョ湅鍙拌处
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 缂洪櫡鐧昏瀵硅瘽妗� -->
+ <el-dialog title="鐧昏璁惧缂洪櫡" v-model="showRegisterDialog" width="50%">
+ <el-form :model="defectForm" :rules="defectRules" ref="defectFormRef" label-width="100px">
+ <el-form-item label="璁惧鍚嶇О" prop="deviceName">
+ <el-select v-model="defectForm.deviceLedgerId" @change="setDeviceModel">
+ <el-option
+ v-for="(item, index) in deviceOptions"
+ :key="index"
+ :label="item.deviceName"
+ :value="item.id"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缂洪櫡鎻忚堪" prop="defectDescription">
+ <el-input type="textarea" v-model="defectForm.defectDescription"></el-input>
+ </el-form-item>
+ <el-form-item label="璁惧鐘舵��" prop="status">
+ <el-radio-group v-model="defectForm.status">
+ <el-radio label="姝e父">姝e父</el-radio>
+ <el-radio label="涓�鑸己闄�">涓�鑸己闄�</el-radio>
+ <el-radio label="涓ラ噸缂洪櫡">涓ラ噸缂洪櫡</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="submitDefectForm">纭畾</el-button>
+ <el-button @click="showRegisterDialog = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
+ <!-- 缂洪櫡璁惧鍙拌处瀵硅瘽妗� -->
+ <el-dialog title="缂洪櫡璁惧鍙拌处" v-model="showLedgerDialog" width="80%">
+ <el-table :data="ledgerList" style="width: 100%; margin-top: 10px;" border>
+ <el-table-column prop="deviceName" label="璁惧鍚嶇О"></el-table-column>
+ <el-table-column prop="defectDescription" label="缂洪櫡鎻忚堪"></el-table-column>
+ <el-table-column prop="status" label="鐘舵��"></el-table-column>
+ <el-table-column prop="eliminateTime" label="娑堢己鏃堕棿"></el-table-column>
+ </el-table>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue';
+import { ElMessage } from 'element-plus';
+import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+// 鍋囪浠ヤ笅鏄悗绔帴鍙�
+import {
+ registerDefect,
+ getDefectList,
+ eliminateDefect as apiEliminateDefect,
+ getDefectLedger,
+ deleteDefect
+} from '@/api/equipmentManagement/defectManagement';
+
+// 缂洪櫡鍒楄〃
+const defectList = ref([]);
+// 鐧昏瀵硅瘽妗嗘樉绀虹姸鎬�
+const showRegisterDialog = ref(false);
+// 鍙拌处瀵硅瘽妗嗘樉绀虹姸鎬�
+const showLedgerDialog = ref(false);
+// 缂洪櫡琛ㄥ崟
+const defectForm = reactive({
+ deviceLedgerId: '',
+ defectDescription: '',
+ status: '',
+});
+const deviceOptions = ref([]);
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const defectRules = reactive({
+ deviceLedgerId: [{ required: true, message: '璇疯緭鍏ヨ澶囧悕绉�', trigger: 'blur' }],
+ defectDescription: [{ required: true, message: '璇疯緭鍏ョ己闄锋弿杩�', trigger: 'blur' }]
+});
+// 琛ㄥ崟寮曠敤
+const defectFormRef = ref(null);
+// 鍙拌处鍒楄〃
+const ledgerList = ref([]);
+
+const loadDeviceName = async () => {
+ const { data } = await getDeviceLedger();
+ // console.log(data);
+ deviceOptions.value = data;
+};
+
+// 鑾峰彇缂洪櫡鍒楄〃
+const fetchDefectList = async () => {
+ try {
+ const res = await getDefectList();
+ if (res.code === 200) {
+ defectList.value = res.data.records;
+ } else {
+ ElMessage.error(res.message || '鑾峰彇缂洪櫡鍒楄〃澶辫触');
+ }
+ } catch (error) {
+ ElMessage.error('鑾峰彇缂洪櫡鍒楄〃澶辫触');
+ }
+};
+
+// 鎻愪氦缂洪櫡鐧昏琛ㄥ崟
+const submitDefectForm = async () => {
+ if (!defectFormRef.value) return;
+ try {
+ await defectFormRef.value.validate();
+ const res = await registerDefect(defectForm);
+ if (res.code === 200) {
+ ElMessage.success('缂洪櫡鐧昏鎴愬姛');
+ showRegisterDialog.value = false;
+ fetchDefectList();
+ } else {
+ ElMessage.error(res.message || '缂洪櫡鐧昏澶辫触');
+ }
+ } catch (error) {
+ ElMessage.error('璇峰~鍐欏畬鏁磋〃鍗曚俊鎭�');
+ }
+};
+
+// 娑堥櫎缂洪櫡
+const eliminateDefect = async (row) => {
+
+ try {
+ const res = await apiEliminateDefect(row);
+ if (res.code === 200) {
+ ElMessage.success('缂洪櫡娑堥櫎鎴愬姛');
+ fetchDefectList();
+ } else {
+ ElMessage.error(res.message || '缂洪櫡娑堥櫎澶辫触');
+ }
+ } catch (error) {
+ ElMessage.error('缂洪櫡娑堥櫎澶辫触');
+ }
+};
+
+// // 杞淮淇伐鍗�
+// const transferToRepairOrder = async (id) => {
+// try {
+// const res = await transferToRepair(id);
+// if (res.code === 200) {
+// ElMessage.success('杞淮淇伐鍗曟垚鍔�');
+// } else {
+// ElMessage.error(res.message || '杞淮淇伐鍗曞け璐�');
+// }
+// } catch (error) {
+// ElMessage.error('杞淮淇伐鍗曞け璐�');
+// }
+// };
+
+// 鑾峰彇缂洪櫡璁惧鍙拌处
+const getLedger = async (deviceLedgerId) => {
+ try {
+ const res = await getDefectLedger(deviceLedgerId);
+ if (res.code === 200) {
+ ledgerList.value = res.data.records;
+ showLedgerDialog.value = true;
+ } else {
+ ElMessage.error(res.message || '鑾峰彇缂洪櫡璁惧鍙拌处澶辫触');
+ }
+ } catch (error) {
+ ElMessage.error('鑾峰彇缂洪櫡璁惧鍙拌处澶辫触');
+ }
+};
+
+// 缁勪欢鎸傝浇鏃惰幏鍙栫己闄峰垪琛�
+const onMounted = () => {
+ fetchDefectList();
+ loadDeviceName();
+};
+onMounted();
+</script>
+
+<style scoped>
+.defect-management {
+ padding: 20px;
+}
+
+.actions {
+ margin-bottom: 10px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/deviceInfo/index.vue b/src/views/equipmentManagement/deviceInfo/index.vue
new file mode 100644
index 0000000..de162cc
--- /dev/null
+++ b/src/views/equipmentManagement/deviceInfo/index.vue
@@ -0,0 +1,190 @@
+<template>
+ <div class="device-info-container">
+ <div class="page-header">
+ <h1>璁惧淇℃伅</h1>
+ <div class="device-status" :class="deviceStatusClass">
+ {{ deviceStatusText }}
+ </div>
+ </div>
+
+ <div class="info-card">
+ <div class="card-header">鍩烘湰淇℃伅</div>
+ <div class="card-content">
+ <div class="info-row">
+ <span class="label">璁惧鍚嶇О锛�</span>
+ <span class="value">{{ deviceInfo.deviceName }}</span>
+ </div>
+ <div class="info-row">
+ <span class="label">瑙勬牸鍨嬪彿锛�</span>
+ <span class="value">{{ deviceInfo.deviceModel }}</span>
+ </div>
+ <div class="info-row">
+ <span class="label">鐢熶骇鍘傚锛�</span>
+ <span class="value">{{ deviceInfo.supplierName }}</span>
+ </div>
+ <div class="info-row">
+ <span class="label">鍗曚綅锛�</span>
+ <span class="value">{{ deviceInfo.unit }}</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="info-card">
+ <div class="card-header">缁存姢淇℃伅</div>
+ <div class="card-content">
+ <div class="maintenance-info">
+ <div class="maintenance-item">
+ <span class="label">鏈�鍚庣淮鎶わ細</span>
+ <span class="value">{{ deviceInfo.updateTime }}</span>
+ </div>
+ <div class="maintenance-item">
+ <span class="label">涓嬫缁存姢锛�</span>
+ <span class="value">{{ deviceInfo.createTime }}</span>
+ </div>
+ <div class="maintenance-item">
+ <span class="label">缁存姢鐘舵�侊細</span>
+ <span class="value status-normal">{{ deviceInfo.statusText }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import {
+ getDeviceInfo,
+} from '@/api/equipmentManagement/deviceInfo'
+
+const route = useRoute()
+
+const deviceInfo = reactive({
+ deviceName: '',
+ deviceModel: '',
+ supplierName: '',
+ unit: '',
+ statusText:'姝e父',
+ updateTime:'',
+ createTime:''
+})
+
+const deviceStatusClass = computed(() => {
+ return 'status-normal'
+})
+
+const deviceStatusText = computed(() => {
+ return '姝e父'
+})
+
+const fetchDeviceInfo = async (deviceId) => {
+ try {
+ // 鑾峰彇璁惧淇℃伅
+ const deviceResponse = await getDeviceInfo({id:deviceId})
+ if (deviceResponse.code === 200) {
+ Object.assign(deviceInfo, deviceResponse.data)
+ }
+
+
+ } catch (error) {
+
+ ElMessage.warning('浣跨敤妯℃嫙鏁版嵁锛屽疄闄匒PI璋冪敤澶辫触')
+ }
+}
+
+onMounted(() => {
+ const deviceId = route.query.deviceId || route.params.deviceId || ''
+ fetchDeviceInfo(deviceId)
+})
+</script>
+
+<style scoped>
+.device-info-container {
+ min-height: 100vh;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ padding: 20px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+.page-header {
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ padding: 20px;
+ margin-bottom: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.page-header h1 {
+ margin: 0;
+ color: #2c3e50;
+ font-size: 24px;
+}
+
+.device-status {
+ padding: 8px 16px;
+ border-radius: 20px;
+ font-size: 14px;
+ color: white;
+ background: #52c41a;
+}
+
+.info-card {
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 16px;
+ margin-bottom: 20px;
+ overflow: hidden;
+}
+
+.card-header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 16px 20px;
+ font-weight: 500;
+}
+
+.card-content {
+ padding: 20px;
+}
+
+.info-row, .maintenance-item {
+ display: flex;
+ margin-bottom: 12px;
+ align-items: center;
+}
+
+.label {
+ width: 100px;
+ color: #666;
+ font-size: 14px;
+}
+
+.value {
+ flex: 1;
+ color: #2c3e50;
+ font-weight: 500;
+}
+
+.status-normal {
+ color: #52c41a;
+}
+
+
+
+@media (max-width: 768px) {
+ .device-info-container {
+ padding: 16px;
+ }
+
+ .page-header h1 {
+ font-size: 20px;
+ }
+
+ .label {
+ width: 80px;
+ }
+}
+</style>
diff --git a/src/views/equipmentManagement/gasTank/simple.vue b/src/views/equipmentManagement/gasTank/simple.vue
new file mode 100644
index 0000000..4cb2d76
--- /dev/null
+++ b/src/views/equipmentManagement/gasTank/simple.vue
@@ -0,0 +1,566 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <h2>閲嶅瀷缃愬紡璐ц溅鐩戞帶</h2>
+ <div class="header-actions">
+<!-- <el-button type="primary" @click="addTank">鏂板鍌ㄧ綈</el-button>-->
+<!-- <el-button @click="exportData">瀵煎嚭鏁版嵁</el-button>-->
+ </div>
+ </div>
+
+ <!-- 鍥涗釜涓昏妯″潡 -->
+ <div class="modules-container">
+ <!-- 1. 鍩烘湰淇℃伅妯″潡 -->
+ <el-card class="module-card">
+ <template #header>
+ <div class="card-header">
+ <span>1. 鍩烘湰淇℃伅</span>
+ <el-button type="text" @click="handleEditBasicInfo">缂栬緫</el-button>
+ </div>
+ </template>
+ <div class="info-grid">
+ <div class="info-item">
+ <label>鍌ㄧ綈缂栧彿锛�</label>
+ <span>{{ basicInfo.tankCode }}</span>
+ </div>
+ <div class="info-item">
+ <label>鍌ㄧ綈鍚嶇О锛�</label>
+ <span>{{ basicInfo.tankName }}</span>
+ </div>
+ <div class="info-item">
+ <label>鍌ㄧ綈绫诲瀷锛�</label>
+ <span>{{ basicInfo.tankType }}</span>
+ </div>
+ <div class="info-item">
+ <label>璁捐鍘嬪姏锛�</label>
+ <span>{{ basicInfo.designPressure }} MPa</span>
+ </div>
+ <div class="info-item">
+ <label>宸ヤ綔鍘嬪姏锛�</label>
+ <span>{{ basicInfo.workingPressure }} MPa</span>
+ </div>
+ <div class="info-item">
+ <label>瀹圭Н锛�</label>
+ <span>{{ basicInfo.volume }} m鲁</span>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 2. 鐩戞祴鍙傛暟妯″潡 -->
+ <el-card class="module-card">
+ <template #header>
+ <div class="card-header">
+ <span>2. 鐩戞祴鍙傛暟</span>
+ <el-button type="text" @click="refreshMonitoring">鍒锋柊</el-button>
+ </div>
+ </template>
+ <div class="monitoring-grid">
+ <div class="monitor-item">
+ <div class="monitor-label">鍘嬪姏</div>
+ <div class="monitor-value" :class="getStatusClass(monitoringData.pressureStatus)">
+ {{ monitoringData.pressure }} MPa
+ </div>
+ <div class="monitor-status">{{ monitoringData.pressureStatus === 'normal' ? '姝e父' : '寮傚父' }}</div>
+ </div>
+ <div class="monitor-item">
+ <div class="monitor-label">娓╁害</div>
+ <div class="monitor-value" :class="getStatusClass(monitoringData.temperatureStatus)">
+ {{ monitoringData.temperature }} 鈩�
+ </div>
+ <div class="monitor-status">{{ monitoringData.temperatureStatus === 'normal' ? '姝e父' : '寮傚父' }}</div>
+ </div>
+ <div class="monitor-item">
+ <div class="monitor-label">姘斾綋娴撳害</div>
+ <div class="monitor-value" :class="getStatusClass(monitoringData.gasStatus)">
+ {{ monitoringData.gasConcentration }} ppm
+ </div>
+ <div class="monitor-status">{{ monitoringData.gasStatus === 'normal' ? '姝e父' : '寮傚父' }}</div>
+ </div>
+ <div class="monitor-item">
+ <div class="monitor-label">娴侀噺</div>
+ <div class="monitor-value" :class="getStatusClass(monitoringData.flowStatus)">
+ {{ monitoringData.flow }} m鲁/h
+ </div>
+ <div class="monitor-status">{{ monitoringData.flowStatus === 'normal' ? '姝e父' : '寮傚父' }}</div>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 3. 瀹夊叏瑁呯疆妯″潡 -->
+ <el-card class="module-card">
+ <template #header>
+ <div class="card-header">
+ <span>3. 瀹夊叏瑁呯疆</span>
+ <el-button type="text" @click="checkSafetyDevices">妫�鏌�</el-button>
+ </div>
+ </template>
+ <div class="safety-grid">
+ <div class="safety-item" v-for="device in safetyDevices" :key="device.name">
+
+ <div class="device-info">
+ <div class="device-name">{{ device.name }}</div>
+ <div class="device-status" :class="device.status">
+ {{ device.status === 'normal' ? '姝e父' : '寮傚父' }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 4. 缁存姢璁板綍妯″潡 -->
+ <el-card class="module-card">
+ <template #header>
+ <div class="card-header">
+ <span>4. 缁存姢璁板綍</span>
+ <el-button type="text" @click="addMaintenanceRecord">娣诲姞璁板綍</el-button>
+ </div>
+ </template>
+ <div class="maintenance-list">
+ <div class="maintenance-item" v-for="record in maintenanceRecords" :key="record.id">
+ <div class="record-header">
+ <span class="record-date">{{ record.date }}</span>
+ <el-tag :type="record.type === 'inspection' ? 'primary' : 'success'" size="small">
+ {{ record.type === 'inspection' ? '妫�楠�' : '缁存姢' }}
+ </el-tag>
+ </div>
+ <div class="record-content">
+ <div class="record-title">{{ record.title }}</div>
+ <div class="record-desc">{{ record.description }}</div>
+ <div class="record-operator">鎿嶄綔浜猴細{{ record.operator }}</div>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </div>
+
+ <!-- 缂栬緫鍩烘湰淇℃伅寮圭獥 -->
+ <el-dialog v-model="basicInfoDialogVisible" title="缂栬緫鍩烘湰淇℃伅" width="600px">
+ <el-form :model="editBasicInfo" label-width="120px">
+ <el-form-item label="鍌ㄧ綈缂栧彿">
+ <el-input v-model="editBasicInfo.tankCode" />
+ </el-form-item>
+ <el-form-item label="鍌ㄧ綈鍚嶇О">
+ <el-input v-model="editBasicInfo.tankName" />
+ </el-form-item>
+ <el-form-item label="鍌ㄧ綈绫诲瀷">
+ <el-select v-model="editBasicInfo.tankType" style="width: 100%">
+ <el-option label="娑插寲姘斾綋鍌ㄧ綈" value="娑插寲姘斾綋鍌ㄧ綈" />
+ <el-option label="鍘嬪姏瀹瑰櫒" value="鍘嬪姏瀹瑰櫒" />
+ <el-option label="甯稿帇鍌ㄧ綈" value="甯稿帇鍌ㄧ綈" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁捐鍘嬪姏">
+ <el-input-number v-model="editBasicInfo.designPressure" :precision="2" style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="宸ヤ綔鍘嬪姏">
+ <el-input-number v-model="editBasicInfo.workingPressure" :precision="2" style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="瀹圭Н">
+ <el-input-number v-model="editBasicInfo.volume" :precision="2" style="width: 100%" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="basicInfoDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="saveBasicInfo">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 娣诲姞缁存姢璁板綍寮圭獥 -->
+ <el-dialog v-model="maintenanceDialogVisible" title="娣诲姞缁存姢璁板綍" width="600px">
+ <el-form :model="newMaintenanceRecord" label-width="120px">
+ <el-form-item label="璁板綍绫诲瀷">
+ <el-select v-model="newMaintenanceRecord.type" style="width: 100%">
+ <el-option label="妫�楠�" value="inspection" />
+ <el-option label="缁存姢" value="maintenance" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏍囬">
+ <el-input v-model="newMaintenanceRecord.title" />
+ </el-form-item>
+ <el-form-item label="鎻忚堪">
+ <el-input type="textarea" v-model="newMaintenanceRecord.description" :rows="3" />
+ </el-form-item>
+ <el-form-item label="鎿嶄綔浜�">
+ <el-input v-model="newMaintenanceRecord.operator" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="maintenanceDialogVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="saveMaintenanceRecord">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+
+// 鍩烘湰淇℃伅
+const basicInfo = reactive({
+ tankCode: 'GT001',
+ tankName: '娑插寲姘斿偍缃怉',
+ tankType: '娑插寲姘斾綋鍌ㄧ綈',
+ designPressure: 1.6,
+ workingPressure: 0.8,
+ volume: 100.5
+})
+
+// 鐩戞祴鍙傛暟
+const monitoringData = reactive({
+ pressure: 0.8,
+ pressureStatus: 'normal',
+ temperature: 25.5,
+ temperatureStatus: 'normal',
+ gasConcentration: 0.1,
+ gasStatus: 'normal',
+ flow: 15.2,
+ flowStatus: 'normal'
+})
+
+// 瀹夊叏瑁呯疆
+const safetyDevices = ref([
+ { name: '瀹夊叏闃�', status: 'normal' },
+ { name: '鍘嬪姏浼犳劅鍣�', status: 'normal' },
+ { name: '娓╁害浼犳劅鍣�', status: 'normal' },
+ { name: '姘斾綋妫�娴嬪櫒', status: 'normal' },
+ { name: '鐖嗙牬鐗�', status: 'normal' },
+ { name: '娉勫帇瑁呯疆', status: 'normal' }
+])
+
+// 缁存姢璁板綍
+const maintenanceRecords = ref([
+ {
+ id: 1,
+ date: '2025-01-15',
+ type: 'inspection',
+ title: '骞村害妫�楠�',
+ description: '鎸夌収TSG 21-2016鏍囧噯杩涜骞村害妫�楠岋紝璁惧鐘舵�佽壇濂�',
+ operator: '寮犲伐绋嬪笀'
+ },
+ {
+ id: 2,
+ date: '2025-02-20',
+ type: 'maintenance',
+ title: '瀹夊叏闃�缁存姢',
+ description: '鏇存崲瀹夊叏闃�瀵嗗皝鍦堬紝鏍″噯鍘嬪姏璁惧畾鍊�',
+ operator: '鏉庢妧甯�'
+ },
+ {
+ id: 3,
+ date: '2025-03-10',
+ type: 'inspection',
+ title: '鍘嬪姏娴嬭瘯',
+ description: '杩涜鍘嬪姏瀹瑰櫒姘村帇璇曢獙锛岀鍚堣璁¤姹�',
+ operator: '鐜嬫楠屽憳'
+ }
+])
+
+// 寮圭獥鎺у埗
+const basicInfoDialogVisible = ref(false)
+const maintenanceDialogVisible = ref(false)
+
+// 缂栬緫琛ㄥ崟鏁版嵁
+const editBasicInfo = reactive({ ...basicInfo })
+const newMaintenanceRecord = reactive({
+ type: 'inspection',
+ title: '',
+ description: '',
+ operator: ''
+})
+
+// 鑾峰彇鐘舵�佹牱寮忕被
+const getStatusClass = (status) => {
+ return status === 'normal' ? 'status-normal' : 'status-warning'
+}
+
+// 鏂板鍌ㄧ綈
+const addTank = () => {
+ ElMessage.success('鏂板鍌ㄧ綈鍔熻兘')
+}
+
+// 瀵煎嚭鏁版嵁
+const exportData = () => {
+ ElMessage.success('瀵煎嚭鎴愬姛')
+}
+
+// 缂栬緫鍩烘湰淇℃伅
+const handleEditBasicInfo = () => {
+ Object.assign(editBasicInfo, basicInfo)
+ basicInfoDialogVisible.value = true
+}
+
+// 淇濆瓨鍩烘湰淇℃伅
+const saveBasicInfo = () => {
+ Object.assign(basicInfo, editBasicInfo)
+ basicInfoDialogVisible.value = false
+ ElMessage.success('淇濆瓨鎴愬姛')
+}
+
+// 鍒锋柊鐩戞祴鏁版嵁
+const refreshMonitoring = () => {
+ // 妯℃嫙鏁版嵁鏇存柊
+ monitoringData.pressure = (Math.random() * 0.5 + 0.6).toFixed(2)
+ monitoringData.temperature = (Math.random() * 10 + 20).toFixed(1)
+ monitoringData.gasConcentration = (Math.random() * 0.2).toFixed(2)
+ monitoringData.flow = (Math.random() * 10 + 10).toFixed(1)
+ ElMessage.success('鏁版嵁宸插埛鏂�')
+}
+
+// 妫�鏌ュ畨鍏ㄨ缃�
+const checkSafetyDevices = () => {
+ // 妯℃嫙妫�鏌ヨ繃绋�
+ safetyDevices.value.forEach(device => {
+ device.status = Math.random() > 0.1 ? 'normal' : 'warning'
+ })
+ ElMessage.success('瀹夊叏瑁呯疆妫�鏌ュ畬鎴�')
+}
+
+// 娣诲姞缁存姢璁板綍
+const addMaintenanceRecord = () => {
+ newMaintenanceRecord.type = 'inspection'
+ newMaintenanceRecord.title = ''
+ newMaintenanceRecord.description = ''
+ newMaintenanceRecord.operator = ''
+ maintenanceDialogVisible.value = true
+}
+
+// 淇濆瓨缁存姢璁板綍
+const saveMaintenanceRecord = () => {
+ const record = {
+ id: Date.now(),
+ date: new Date().toISOString().split('T')[0],
+ ...newMaintenanceRecord
+ }
+ maintenanceRecords.value.unshift(record)
+ maintenanceDialogVisible.value = false
+ ElMessage.success('璁板綍娣诲姞鎴愬姛')
+}
+
+// 妯℃嫙瀹炴椂鏁版嵁鏇存柊
+onMounted(() => {
+ setInterval(() => {
+ monitoringData.pressure = (Math.random() * 0.5 + 0.6).toFixed(2)
+ monitoringData.temperature = (Math.random() * 10 + 20).toFixed(1)
+ monitoringData.gasConcentration = (Math.random() * 0.2).toFixed(2)
+ monitoringData.flow = (Math.random() * 10 + 10).toFixed(1)
+ }, 5000)
+})
+</script>
+
+<style lang="scss" scoped>
+.app-container {
+ padding: 20px;
+ background: #f5f5f5;
+ min-height: 100vh;
+}
+
+.page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding: 20px;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+
+ h2 {
+ margin: 0;
+ color: #303133;
+ }
+
+ .header-actions {
+ display: flex;
+ gap: 10px;
+ }
+}
+
+.modules-container {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 20px;
+}
+
+.module-card {
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: bold;
+ color: #303133;
+ }
+}
+
+.info-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+
+ .info-item {
+ display: flex;
+ justify-content: space-between;
+ padding: 10px;
+ background: #f8f9fa;
+ border-radius: 4px;
+
+ label {
+ font-weight: bold;
+ color: #606266;
+ }
+
+ span {
+ color: #303133;
+ }
+ }
+}
+
+.monitoring-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+
+ .monitor-item {
+ text-align: center;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ border: 2px solid transparent;
+
+ .monitor-label {
+ font-size: 14px;
+ color: #606266;
+ margin-bottom: 8px;
+ }
+
+ .monitor-value {
+ font-size: 20px;
+ font-weight: bold;
+ margin-bottom: 5px;
+
+ &.status-normal {
+ color: #67c23a;
+ }
+
+ &.status-warning {
+ color: #e6a23c;
+ }
+ }
+
+ .monitor-status {
+ font-size: 12px;
+ color: #909399;
+ }
+ }
+}
+
+.safety-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 15px;
+
+ .safety-item {
+ display: flex;
+ align-items: center;
+ padding: 15px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ border: 2px solid transparent;
+
+ .device-icon {
+ margin-right: 15px;
+ }
+
+ .device-info {
+ flex: 1;
+
+ .device-name {
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 5px;
+ }
+
+ .device-status {
+ font-size: 12px;
+ padding: 2px 8px;
+ border-radius: 10px;
+ display: inline-block;
+
+ &.normal {
+ background: #f0f9ff;
+ color: #409eff;
+ }
+
+ &.warning {
+ background: #fef7e0;
+ color: #e6a23c;
+ }
+ }
+ }
+ }
+}
+
+.maintenance-list {
+ max-height: 300px;
+ overflow-y: auto;
+
+ .maintenance-item {
+ padding: 15px;
+ border-bottom: 1px solid #ebeef5;
+ margin-bottom: 10px;
+
+ &:last-child {
+ border-bottom: none;
+ margin-bottom: 0;
+ }
+
+ .record-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+
+ .record-date {
+ font-size: 14px;
+ color: #909399;
+ }
+ }
+
+ .record-content {
+ .record-title {
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 5px;
+ }
+
+ .record-desc {
+ font-size: 14px;
+ color: #606266;
+ margin-bottom: 5px;
+ line-height: 1.4;
+ }
+
+ .record-operator {
+ font-size: 12px;
+ color: #909399;
+ }
+ }
+ }
+}
+
+// 鍝嶅簲寮忚璁�
+@media (max-width: 1200px) {
+ .modules-container {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 768px) {
+ .info-grid,
+ .monitoring-grid,
+ .safety-grid {
+ grid-template-columns: 1fr;
+ }
+}
+</style>
diff --git a/src/views/equipmentManagement/inspectionManagement/components/qrCodeDia.vue b/src/views/equipmentManagement/inspectionManagement/components/qrCodeDia.vue
new file mode 100644
index 0000000..136c18c
--- /dev/null
+++ b/src/views/equipmentManagement/inspectionManagement/components/qrCodeDia.vue
@@ -0,0 +1,132 @@
+<template>
+ <div>
+ <el-dialog :title="operationType === 'add' ? '鏂板浜岀淮鐮�' : '缂栬緫浜岀淮鐮�'"
+ v-model="dialogVisitable" width="500px" @close="cancel">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="璁惧鍚嶇О" prop="deviceName">
+ <el-input v-model="form.deviceName" placeholder="璇疯緭鍏ヨ澶囧悕绉�" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鎵�鍦ㄤ綅缃弿杩�" prop="location">
+ <el-input v-model="form.location" placeholder="璇疯緭鍏ユ墍鍦ㄤ綅缃弿杩�" maxlength="30"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <div>
+ <el-button type="primary" @click="submitForm">鐢熸垚骞舵墦鍗颁簩缁寸爜</el-button>
+ </div>
+ <div v-if="isShowQrCode" class="print-section" ref="qrCodeContainer" id="qrCodeContainer">
+ <vue-qrcode :value="qrCodeValue" :width="qrCodeSize"></vue-qrcode>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import useUserStore from "@/store/modules/user.js";
+import {reactive, ref} from "vue";
+import printJS from 'print-js';
+import {addOrEditQrCode} from "@/api/inspectionUpload/index.js";
+
+const { proxy } = getCurrentInstance()
+const emit = defineEmits()
+const userStore = useUserStore()
+const dialogVisitable = ref(false);
+const isShowQrCode = ref(false);
+const operationType = ref('add');
+
+const qrCodeValue = ref('');
+const qrCodeSize = ref(100);
+const data = reactive({
+ form: {
+ deviceName: '',
+ location: '',
+ qrCodeId: '',
+ id: ''
+ },
+ rules: {
+ deviceName: [{ required: true, message: '璇疯緭鍏ヨ澶囧悕绉�', trigger: 'blur' }],
+ location: [{ required: true, message: '璇疯緭鍏ュ湴鐐�', trigger: 'blur' }]
+ }
+})
+const { form, rules } = toRefs(data)
+
+
+// 鎵撳紑寮规
+const openDialog = async (type, row) => {
+ dialogVisitable.value = true
+ qrCodeValue.value = ''
+ isShowQrCode.value = false;
+ if (type === 'edit') {
+ form.value.id = row.id
+ form.value.qrCodeId = row.id
+ form.value.deviceName = row.deviceName
+ form.value.location = row.location
+ // 灏嗚〃鍗曟暟鎹浆涓� JSON 瀛楃涓蹭綔涓轰簩缁寸爜鍐呭
+ qrCodeValue.value = JSON.stringify(form.value);
+ isShowQrCode.value = true;
+ }
+}
+// 鎻愪氦鍚堝苟琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ addOrEditQrCode(form.value).then((res) => {
+ form.value.qrCodeId = res.data
+ })
+ // 灏嗚〃鍗曟暟鎹浆涓� JSON 瀛楃涓蹭綔涓轰簩缁寸爜鍐呭
+ qrCodeValue.value = JSON.stringify(form.value);
+ isShowQrCode.value = true;
+ showQrCode()
+ }
+ })
+}
+const showQrCode = () => {
+ // 寤惰繜鎵ц鎵撳嵃锛岄伩鍏� DOM 鏇存柊鍓嶅氨璋冪敤鎵撳嵃
+ setTimeout(() => {
+ printJS({
+ printable: 'qrCodeContainer',//椤甸潰
+ type: "html",//鏂囨。绫诲瀷
+ maxWidth: 360,
+ style: `@page {
+ margin:0;
+ size: 400px 75px collapse;
+ margin-top:3px;
+ &:first-of-type{
+ margin-top:0 !important;
+ }
+ }
+ html{
+ zoom:100%;
+ }
+ @media print{
+ width: 400px;
+ height: 75px;
+ margin:0;
+ }`,
+ targetStyles: ["*"], // 浣跨敤dom鐨勬墍鏈夋牱寮忥紝寰堥噸瑕�
+ font_size: '0.20cm',
+ });
+ }, 300);
+}
+// 鍏抽棴鍚堝苟琛ㄥ崟
+const cancel = () => {
+ proxy.resetForm("formRef")
+ dialogVisitable.value = false
+ emit('closeDia')
+}
+defineExpose({ openDialog })
+</script>
+
+<style scoped>
+.print-section {
+ text-align: center;
+ margin-top: 30px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/inspectionManagement/components/viewQrCodeFiles.vue b/src/views/equipmentManagement/inspectionManagement/components/viewQrCodeFiles.vue
new file mode 100644
index 0000000..f8e923a
--- /dev/null
+++ b/src/views/equipmentManagement/inspectionManagement/components/viewQrCodeFiles.vue
@@ -0,0 +1,169 @@
+<template>
+ <div>
+ <el-dialog title="鏌ョ湅闄勪欢"
+ v-model="dialogVisitable" width="800px" @close="cancel">
+ <div class="upload-container">
+ <div class="form-container">
+ <div class="title">宸℃闄勪欢</div>
+ <!-- 鍥剧墖鍒楄〃 -->
+ <div style="display: flex; flex-wrap: wrap;">
+ <img v-for="(item, index) in beforeProductionImgs" :key="index"
+ @click="showMedia(beforeProductionImgs, index, 'image')"
+ :src="item" style="max-width: 100px; height: 100px; margin: 5px;" alt="">
+ </div>
+
+ <!-- 瑙嗛鍒楄〃 -->
+ <div style="display: flex; flex-wrap: wrap;">
+ <div
+ v-for="(videoUrl, index) in beforeProductionVideos"
+ :key="index"
+ @click="showMedia(beforeProductionVideos, index, 'video')"
+ style="position: relative; margin: 10px; cursor: pointer;"
+ >
+ <div style="width: 160px; height: 90px; background-color: #333; display: flex; align-items: center; justify-content: center;">
+ <img src="@/assets/images/video.png" alt="鎾斁" style="width: 30px; height: 30px; opacity: 0.8;" />
+ </div>
+ <div style="text-align: center; font-size: 12px; color: #666;">鐐瑰嚮鎾斁</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-dialog>
+ <!-- 缁熶竴濯掍綋鏌ョ湅鍣� -->
+ <div v-if="isMediaViewerVisible" class="media-viewer-overlay" @click.self="closeMediaViewer">
+ <div class="media-viewer-content" @click.stop>
+ <!-- 鍥剧墖 -->
+ <vue-easy-lightbox
+ v-if="mediaType === 'image'"
+ :visible="isMediaViewerVisible"
+ :imgs="mediaList"
+ :index="currentMediaIndex"
+ @hide="closeMediaViewer"
+ ></vue-easy-lightbox>
+
+ <!-- 瑙嗛 -->
+ <div v-else-if="mediaType === 'video'" style="position: relative;">
+ <Video
+ :src="mediaList[currentMediaIndex]"
+ autoplay
+ controls
+ style="max-width: 90vw; max-height: 80vh;"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+// 鎺у埗寮圭獥鏄剧ず
+import VueEasyLightbox from "vue-easy-lightbox";
+
+const dialogVisitable = ref(false);
+// 鍥剧墖鏁扮粍
+const beforeProductionImgs = ref([]);
+// 瑙嗛鏁扮粍
+const beforeProductionVideos = ref([]);
+// 濯掍綋鏌ョ湅鍣ㄧ姸鎬�
+const isMediaViewerVisible = ref(false);
+const currentMediaIndex = ref(0);
+const mediaList = ref([]); // 瀛樺偍褰撳墠瑕佹煡鐪嬬殑濯掍綋鍒楄〃锛堝惈鍥剧墖鍜岃棰戝璞★級
+const mediaType = ref('image'); // image | video
+
+// 鎵撳紑寮圭獥骞跺姞杞芥暟鎹�
+const openDialog = async (row) => {
+ const { images: beforeImgs, videos: beforeVids } = processItems(row.storageBlobDTO);
+
+ beforeProductionImgs.value = beforeImgs;
+ beforeProductionVideos.value = beforeVids;
+ dialogVisitable.value = true;
+};
+// 鏄剧ず濯掍綋锛堝浘鐗� or 瑙嗛锛�
+function showMedia(mediaArray, index, type) {
+ mediaList.value = mediaArray;
+ currentMediaIndex.value = index;
+ mediaType.value = type;
+ isMediaViewerVisible.value = true;
+}
+// 鍏抽棴濯掍綋鏌ョ湅鍣�
+function closeMediaViewer() {
+ isMediaViewerVisible.value = false;
+ mediaList.value = [];
+ mediaType.value = 'image';
+}
+// 琛ㄥ崟鍏抽棴鏂规硶
+const cancel = () => {
+ dialogVisitable.value = false;
+};
+// 澶勭悊姣忎竴绫绘暟鎹細鍒嗙鍥剧墖鍜岃棰�
+function processItems(items) {
+ const images = [];
+ const videos = [];
+ items.forEach(item => {
+ if (item.contentType?.startsWith('image/')) {
+ images.push(item.url);
+ } else if (item.contentType?.startsWith('video/')) {
+ videos.push(item.url);
+ }
+ });
+ return { images, videos };
+}
+defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+.upload-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 20px;
+ border: 1px solid #dcdfe6;
+ box-sizing: border-box;
+
+ .form-container {
+ flex: 1;
+ width: 100%;
+ margin-bottom: 20px;
+ }
+}
+
+.title {
+ font-size: 14px;
+ color: #165dff;
+ line-height: 20px;
+ font-weight: 600;
+ padding-left: 10px;
+ position: relative;
+ margin: 6px 0;
+
+ &::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 3px;
+ width: 4px;
+ height: 14px;
+ background-color: #165dff;
+ }
+}
+
+.media-viewer-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: rgba(0, 0, 0, 0.8);
+ z-index: 9999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.media-viewer-content {
+ position: relative;
+ max-width: 90vw;
+ max-height: 90vh;
+ overflow: hidden;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/iotMonitor/index.vue b/src/views/equipmentManagement/iotMonitor/index.vue
new file mode 100644
index 0000000..de62866
--- /dev/null
+++ b/src/views/equipmentManagement/iotMonitor/index.vue
@@ -0,0 +1,317 @@
+<template>
+ <div class="app-container iot-monitor">
+ <div class="header">
+ <div class="title">瀹炴椂宸ュ喌鐩戞帶锛圛oT锛�</div>
+ <div class="actions">
+ <el-button type="primary" @click="toggleCollecting">{{ collecting ? '鏆傚仠閲囬泦' : '鍚姩閲囬泦' }}</el-button>
+ <el-button @click="resetAll">閲嶇疆</el-button>
+ <span class="ts">涓婃鏇存柊鏃堕棿锛歿{ lastUpdatedDisplay }}</span>
+ </div>
+ </div>
+
+<!-- <el-alert-->
+<!-- title="杈圭紭棰勮瑙勫垯锛氳酱鎵跨(鎹�-鎸姩鍊煎亸绂诲熀绾柯�5%瑙﹀彂鍛婅锛涙俯搴�/鍘嬪姏瓒婄晫瑙﹀彂鎻愰啋"-->
+<!-- type="info"-->
+<!-- :closable="false"-->
+<!-- show-icon-->
+<!-- class="rule-alert"-->
+<!-- />-->
+
+ <el-row :gutter="16">
+ <el-col v-for="dev in devices" :key="dev.id" :span="12">
+ <el-card :class="['device-card', dev.hasAlert ? 'is-alert' : '']">
+ <template #header>
+ <div class="card-header">
+ <div class="card-title">
+ <span class="device-name">{{ dev.name }}</span>
+ <el-tag :type="dev.hasAlert ? 'danger' : 'success'" size="small">{{ dev.hasAlert ? '鍛婅' : '姝e父' }}</el-tag>
+ </div>
+ <div class="meta">绫诲瀷锛歿{ dev.type }}锝滃熀绾挎尟鍔細{{ dev.baseline.vibration.toFixed(2) }} mm/s</div>
+ </div>
+ </template>
+
+ <div class="metrics">
+ <div class="metric" :class="{ 'metric-alert': dev.alerts.vibration }">
+ <div class="metric-head">
+ <span>鎸姩(mm/s)</span>
+ <el-tag :type="dev.alerts.vibration ? 'danger' : 'info'" size="small">{{ dev.alerts.vibration ? '卤5%瓒婄晫' : '鍩虹嚎卤5%' }}</el-tag>
+ </div>
+ <div class="metric-value">{{ currentValue(dev.series.vibration).toFixed(2) }}</div>
+ <Echarts
+ :xAxis="[{ type: 'category', data: xAxisLabels }]"
+ :yAxis="[{ type: 'value', name: 'mm/s' }]"
+ :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.vibration }]"
+ :tooltip="{ trigger: 'axis' }"
+ :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
+ :chartStyle="{ height: '160px', width: '100%' }"
+ :lineColors="['#409EFF']"
+ />
+ </div>
+
+ <div class="metric" :class="{ 'metric-alert': dev.alerts.temperature }">
+ <div class="metric-head">
+ <span>娓╁害(掳C)</span>
+ <el-tag :type="dev.alerts.temperature ? 'warning' : 'info'" size="small">{{ dev.alerts.temperature ? '瓒婄晫' : '20~80' }}</el-tag>
+ </div>
+ <div class="metric-value">{{ currentValue(dev.series.temperature).toFixed(1) }}</div>
+ <Echarts
+ :xAxis="[{ type: 'category', data: xAxisLabels }]"
+ :yAxis="[{ type: 'value', name: '掳C' }]"
+ :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.temperature }]"
+ :tooltip="{ trigger: 'axis' }"
+ :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
+ :chartStyle="{ height: '160px', width: '100%' }"
+ :lineColors="['#E6A23C']"
+ />
+ </div>
+
+ <div class="metric" :class="{ 'metric-alert': dev.alerts.pressure }">
+ <div class="metric-head">
+ <span>鍘嬪姏(MPa)</span>
+ <el-tag :type="dev.alerts.pressure ? 'warning' : 'info'" size="small">{{ dev.alerts.pressure ? '瓒婄晫' : '0.2~1.5' }}</el-tag>
+ </div>
+ <div class="metric-value">{{ currentValue(dev.series.pressure).toFixed(2) }}</div>
+ <Echarts
+ :xAxis="[{ type: 'category', data: xAxisLabels }]"
+ :yAxis="[{ type: 'value', name: 'MPa' }]"
+ :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.pressure }]"
+ :tooltip="{ trigger: 'axis' }"
+ :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
+ :chartStyle="{ height: '160px', width: '100%' }"
+ :lineColors="['#67C23A']"
+ />
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
+import { ElNotification } from 'element-plus'
+import Echarts from '@/components/Echarts/echarts.vue'
+
+defineOptions({ name: 'IoTMonitor' })
+
+const windowSize = 30
+const collecting = ref(true)
+const lastUpdated = ref(Date.now())
+const lastUpdatedDisplay = computed(() => new Date(lastUpdated.value).toLocaleTimeString())
+
+const xAxisLabels = ref(Array.from({ length: windowSize }, (_, i) => i - (windowSize - 1)).map(n => `${n}s`))
+
+function makeSeries(fill, decimals = 2) {
+ return Array.from({ length: windowSize }, () => Number(fill.toFixed(decimals)))
+}
+
+const devices = reactive([
+ {
+ id: 'water-pump',
+ name: '璁惧1',
+ type: '绉诲姩瑁呭',
+ baseline: { vibration: 9 },
+ initial: { temperature: 40, pressure: 0.70 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(9),
+ temperature: makeSeries(40, 1),
+ pressure: makeSeries(0.7, 2),
+ },
+ },
+ {
+ id: 'fluid-supply-truck',
+ name: '璁惧2',
+ type: '绉诲姩瑁呭',
+ baseline: { vibration: 7 },
+ initial: { temperature: 30, pressure: 0.60 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(7),
+ temperature: makeSeries(30, 1),
+ pressure: makeSeries(0.6, 2),
+ },
+ },
+ {
+ id: 'fracturing-truck',
+ name: '璁惧3',
+ type: '绉诲姩瑁呭',
+ baseline: { vibration: 12 },
+ initial: { temperature: 65, pressure: 1.40 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(12),
+ temperature: makeSeries(65, 1),
+ pressure: makeSeries(1.4, 2),
+ },
+ },
+ {
+ id: 'oil-tank-truck',
+ name: '璁惧4',
+ type: '绉诲姩瑁呭',
+ baseline: { vibration: 6 },
+ initial: { temperature: 28, pressure: 0.50 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(6),
+ temperature: makeSeries(28, 1),
+ pressure: makeSeries(0.5, 2),
+ },
+ },
+])
+
+function currentValue(arr) {
+ return arr[arr.length - 1] ?? 0
+}
+
+function pushWindow(arr, val) {
+ if (arr.length >= windowSize) arr.shift()
+ arr.push(val)
+}
+
+function clamp(val, min, max) { return Math.max(min, Math.min(max, val)) }
+
+function tickDevice(dev) {
+ const vibBase = dev.baseline.vibration
+ // 鎸姩锛氬熀绾柯�2%闅忔満娉㈠姩锛�5%姒傜巼瑙﹀彂8%~12%灏栧嘲妯℃嫙鍛婅
+ const spike = Math.random() < 0.05
+ const vibNoise = vibBase * (spike ? (1 + (Math.random() * 0.08 + 0.04) * (Math.random() < 0.5 ? -1 : 1)) : (1 + (Math.random() - 0.5) * 0.04))
+ const vibVal = Number(vibNoise.toFixed(2))
+ pushWindow(dev.series.vibration, vibVal)
+
+ // 娓╁害锛氱紦鎱㈤殢鏈烘父璧帮紝骞舵坊鍔犲伓鍙戦珮娓╁亸绉�
+ const tPrev = currentValue(dev.series.temperature)
+ const tDrift = tPrev + (Math.random() - 0.5) * 0.8 + (Math.random() < 0.02 ? 6 : 0)
+ const tVal = Number(clamp(tDrift, 15, 95).toFixed(1))
+ pushWindow(dev.series.temperature, tVal)
+
+ // 鍘嬪姏锛氬皬骞呮尝鍔紝鍋跺彂浣庡帇/楂樺帇
+ const pPrev = currentValue(dev.series.pressure)
+ const pDrift = pPrev + (Math.random() - 0.5) * 0.05 + (Math.random() < 0.02 ? (Math.random() < 0.5 ? -0.3 : 0.3) : 0)
+ const pVal = Number(clamp(pDrift, 0.05, 2.0).toFixed(2))
+ pushWindow(dev.series.pressure, pVal)
+
+ // 杈圭紭璁$畻闃堝�煎垽鏂�
+ const vibDelta = Math.abs(vibVal - vibBase) / vibBase
+ const vibAlert = vibDelta > 0.05
+ const tAlert = tVal < 20 || tVal > 80
+ const pAlert = pVal < 0.2 || pVal > 1.5
+
+ const prevHasAlert = dev.hasAlert
+ dev.alerts.vibration = vibAlert
+ dev.alerts.temperature = tAlert
+ dev.alerts.pressure = pAlert
+ dev.hasAlert = vibAlert || tAlert || pAlert
+
+ if (dev.hasAlert && !prevHasAlert) {
+ const reasons = []
+ if (vibAlert) reasons.push(`鎸姩鍋忕卤5% (褰撳墠 ${vibVal} / 鍩虹嚎 ${vibBase})`)
+ if (tAlert) reasons.push(`娓╁害瓒婄晫 (褰撳墠 ${tVal}掳C, 鏈熸湜 20~80掳C) `)
+ if (pAlert) reasons.push(`鍘嬪姏瓒婄晫 (褰撳墠 ${pVal}MPa, 鏈熸湜 0.2~1.5MPa) `)
+ ElNotification({
+ title: `${dev.name} 鍛婅`,
+ message: reasons.join('锛�'),
+ type: vibAlert ? 'error' : 'warning',
+ duration: 5000,
+ })
+ }
+}
+
+let timer = null
+function start() {
+ if (timer) return
+ timer = setInterval(() => {
+ if (!collecting.value) return
+ devices.forEach(tickDevice)
+ lastUpdated.value = Date.now()
+ }, 10000)
+}
+
+function stop() {
+ if (timer) {
+ clearInterval(timer)
+ timer = null
+ }
+}
+
+function toggleCollecting() { collecting.value = !collecting.value }
+
+function resetAll() {
+ devices.forEach(dev => {
+ dev.series.vibration = makeSeries(dev.baseline.vibration)
+ const t0 = dev.initial?.temperature ?? 45
+ const p0 = dev.initial?.pressure ?? 0.8
+ dev.series.temperature = makeSeries(t0, 1)
+ dev.series.pressure = makeSeries(p0, 2)
+ dev.alerts.vibration = false
+ dev.alerts.temperature = false
+ dev.alerts.pressure = false
+ dev.hasAlert = false
+ })
+ lastUpdated.value = Date.now()
+}
+
+onMounted(() => {
+ start()
+})
+
+onBeforeUnmount(() => {
+ stop()
+})
+</script>
+
+<style lang="scss" scoped>
+.iot-monitor {
+ .header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ .title { font-size: 18px; font-weight: 600; }
+ .actions { display: flex; align-items: center; gap: 8px; }
+ .ts { color: #909399; font-size: 12px; }
+ }
+ .rule-alert { margin-bottom: 12px; }
+}
+
+.device-card {
+ margin-bottom: 16px;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ &.is-alert { border-color: #F56C6C; box-shadow: 0 0 0 2px rgba(245,108,108,0.2) inset; }
+ .card-header {
+ display: flex; flex-direction: column; gap: 4px;
+ .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
+ .meta { color: #909399; font-size: 12px; }
+ }
+ .metrics {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+}
+
+.metric {
+ border: 1px solid #ebeef5;
+ border-radius: 6px;
+ padding: 8px 8px 0 8px;
+ &-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-size: 13px; color: #606266; }
+ &-value { font-size: 20px; font-weight: 600; margin: 2px 0 6px 0; }
+}
+
+.metric-alert {
+ border-color: #F56C6C;
+ background: #FFF6F6;
+}
+
+@media (min-width: 1200px) {
+ .device-card .metrics { grid-template-columns: 1fr 1fr 1fr; }
+}
+</style>
+
+
diff --git a/src/views/equipmentManagement/iotMonitor/indexWD.vue b/src/views/equipmentManagement/iotMonitor/indexWD.vue
new file mode 100644
index 0000000..d98afe2
--- /dev/null
+++ b/src/views/equipmentManagement/iotMonitor/indexWD.vue
@@ -0,0 +1,317 @@
+<template>
+ <div class="app-container iot-monitor">
+ <div class="header">
+ <div class="title">瀹炴椂宸ュ喌鐩戞帶锛圛oT锛�</div>
+ <div class="actions">
+ <el-button type="primary" @click="toggleCollecting">{{ collecting ? '鏆傚仠閲囬泦' : '鍚姩閲囬泦' }}</el-button>
+ <el-button @click="resetAll">閲嶇疆</el-button>
+ <span class="ts">涓婃鏇存柊鏃堕棿锛歿{ lastUpdatedDisplay }}</span>
+ </div>
+ </div>
+
+<!-- <el-alert-->
+<!-- title="杈圭紭棰勮瑙勫垯锛氳酱鎵跨(鎹�-鎸姩鍊煎亸绂诲熀绾柯�5%瑙﹀彂鍛婅锛涙俯搴�/鍘嬪姏瓒婄晫瑙﹀彂鎻愰啋"-->
+<!-- type="info"-->
+<!-- :closable="false"-->
+<!-- show-icon-->
+<!-- class="rule-alert"-->
+<!-- />-->
+
+ <el-row :gutter="16">
+ <el-col v-for="dev in devices" :key="dev.id" :span="12">
+ <el-card :class="['device-card', dev.hasAlert ? 'is-alert' : '']">
+ <template #header>
+ <div class="card-header">
+ <div class="card-title">
+ <span class="device-name">{{ dev.name }}</span>
+ <el-tag :type="dev.hasAlert ? 'danger' : 'success'" size="small">{{ dev.hasAlert ? '鍛婅' : '姝e父' }}</el-tag>
+ </div>
+ <div class="meta">绫诲瀷锛歿{ dev.type }}锝滃熀绾挎尟鍔細{{ dev.baseline.vibration.toFixed(2) }} mm/s</div>
+ </div>
+ </template>
+
+ <div class="metrics">
+ <div class="metric" :class="{ 'metric-alert': dev.alerts.vibration }">
+ <div class="metric-head">
+ <span>鎸姩(mm/s)</span>
+ <el-tag :type="dev.alerts.vibration ? 'danger' : 'info'" size="small">{{ dev.alerts.vibration ? '卤5%瓒婄晫' : '鍩虹嚎卤5%' }}</el-tag>
+ </div>
+ <div class="metric-value">{{ currentValue(dev.series.vibration).toFixed(2) }}</div>
+ <Echarts
+ :xAxis="[{ type: 'category', data: xAxisLabels }]"
+ :yAxis="[{ type: 'value', name: 'mm/s' }]"
+ :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.vibration }]"
+ :tooltip="{ trigger: 'axis' }"
+ :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
+ :chartStyle="{ height: '160px', width: '100%' }"
+ :lineColors="['#409EFF']"
+ />
+ </div>
+
+ <div class="metric" :class="{ 'metric-alert': dev.alerts.temperature }">
+ <div class="metric-head">
+ <span>娓╁害(掳C)</span>
+ <el-tag :type="dev.alerts.temperature ? 'warning' : 'info'" size="small">{{ dev.alerts.temperature ? '瓒婄晫' : '20~80' }}</el-tag>
+ </div>
+ <div class="metric-value">{{ currentValue(dev.series.temperature).toFixed(1) }}</div>
+ <Echarts
+ :xAxis="[{ type: 'category', data: xAxisLabels }]"
+ :yAxis="[{ type: 'value', name: '掳C' }]"
+ :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.temperature }]"
+ :tooltip="{ trigger: 'axis' }"
+ :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
+ :chartStyle="{ height: '160px', width: '100%' }"
+ :lineColors="['#E6A23C']"
+ />
+ </div>
+
+ <div class="metric" :class="{ 'metric-alert': dev.alerts.pressure }">
+ <div class="metric-head">
+ <span>鍘嬪姏(MPa)</span>
+ <el-tag :type="dev.alerts.pressure ? 'warning' : 'info'" size="small">{{ dev.alerts.pressure ? '瓒婄晫' : '0.2~1.5' }}</el-tag>
+ </div>
+ <div class="metric-value">{{ currentValue(dev.series.pressure).toFixed(2) }}</div>
+ <Echarts
+ :xAxis="[{ type: 'category', data: xAxisLabels }]"
+ :yAxis="[{ type: 'value', name: 'MPa' }]"
+ :series="[{ type: 'line', smooth: true, showSymbol: false, data: dev.series.pressure }]"
+ :tooltip="{ trigger: 'axis' }"
+ :grid="{ left: 40, right: 10, top: 10, bottom: 20 }"
+ :chartStyle="{ height: '160px', width: '100%' }"
+ :lineColors="['#67C23A']"
+ />
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onBeforeUnmount, computed } from 'vue'
+import { ElNotification } from 'element-plus'
+import Echarts from '@/components/Echarts/echarts.vue'
+
+defineOptions({ name: 'IoTMonitor' })
+
+const windowSize = 30
+const collecting = ref(true)
+const lastUpdated = ref(Date.now())
+const lastUpdatedDisplay = computed(() => new Date(lastUpdated.value).toLocaleTimeString())
+
+const xAxisLabels = ref(Array.from({ length: windowSize }, (_, i) => i - (windowSize - 1)).map(n => `${n}s`))
+
+function makeSeries(fill, decimals = 2) {
+ return Array.from({ length: windowSize }, () => Number(fill.toFixed(decimals)))
+}
+
+const devices = reactive([
+ {
+ id: 'hydrocyclone-desander',
+ name: '璁惧1',
+ type: '鍒嗙璁惧',
+ baseline: { vibration: 8 },
+ initial: { temperature: 35, pressure: 0.85 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(8),
+ temperature: makeSeries(35, 1),
+ pressure: makeSeries(0.85, 2),
+ },
+ },
+ {
+ id: 'high-pressure-separator',
+ name: '璁惧2',
+ type: '鍒嗙璁惧',
+ baseline: { vibration: 6 },
+ initial: { temperature: 45, pressure: 1.20 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(6),
+ temperature: makeSeries(45, 1),
+ pressure: makeSeries(1.2, 2),
+ },
+ },
+ {
+ id: 'heating-throttle-pressure',
+ name: '璁惧3',
+ type: '璋冨帇璁惧',
+ baseline: { vibration: 10 },
+ initial: { temperature: 75, pressure: 1.80 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(10),
+ temperature: makeSeries(75, 1),
+ pressure: makeSeries(1.8, 2),
+ },
+ },
+ {
+ id: 'three-phase-separator',
+ name: '璁惧4',
+ type: '鍒嗙璁惧',
+ baseline: { vibration: 7 },
+ initial: { temperature: 38, pressure: 0.95 },
+ alerts: { vibration: false, temperature: false, pressure: false },
+ hasAlert: false,
+ series: {
+ vibration: makeSeries(7),
+ temperature: makeSeries(38, 1),
+ pressure: makeSeries(0.95, 2),
+ },
+ },
+])
+
+function currentValue(arr) {
+ return arr[arr.length - 1] ?? 0
+}
+
+function pushWindow(arr, val) {
+ if (arr.length >= windowSize) arr.shift()
+ arr.push(val)
+}
+
+function clamp(val, min, max) { return Math.max(min, Math.min(max, val)) }
+
+function tickDevice(dev) {
+ const vibBase = dev.baseline.vibration
+ // 鎸姩锛氬熀绾柯�2%闅忔満娉㈠姩锛�5%姒傜巼瑙﹀彂8%~12%灏栧嘲妯℃嫙鍛婅
+ const spike = Math.random() < 0.05
+ const vibNoise = vibBase * (spike ? (1 + (Math.random() * 0.08 + 0.04) * (Math.random() < 0.5 ? -1 : 1)) : (1 + (Math.random() - 0.5) * 0.04))
+ const vibVal = Number(vibNoise.toFixed(2))
+ pushWindow(dev.series.vibration, vibVal)
+
+ // 娓╁害锛氱紦鎱㈤殢鏈烘父璧帮紝骞舵坊鍔犲伓鍙戦珮娓╁亸绉�
+ const tPrev = currentValue(dev.series.temperature)
+ const tDrift = tPrev + (Math.random() - 0.5) * 0.8 + (Math.random() < 0.02 ? 6 : 0)
+ const tVal = Number(clamp(tDrift, 15, 95).toFixed(1))
+ pushWindow(dev.series.temperature, tVal)
+
+ // 鍘嬪姏锛氬皬骞呮尝鍔紝鍋跺彂浣庡帇/楂樺帇
+ const pPrev = currentValue(dev.series.pressure)
+ const pDrift = pPrev + (Math.random() - 0.5) * 0.05 + (Math.random() < 0.02 ? (Math.random() < 0.5 ? -0.3 : 0.3) : 0)
+ const pVal = Number(clamp(pDrift, 0.05, 2.0).toFixed(2))
+ pushWindow(dev.series.pressure, pVal)
+
+ // 杈圭紭璁$畻闃堝�煎垽鏂�
+ const vibDelta = Math.abs(vibVal - vibBase) / vibBase
+ const vibAlert = vibDelta > 0.05
+ const tAlert = tVal < 20 || tVal > 80
+ const pAlert = pVal < 0.2 || pVal > 1.5
+
+ const prevHasAlert = dev.hasAlert
+ dev.alerts.vibration = vibAlert
+ dev.alerts.temperature = tAlert
+ dev.alerts.pressure = pAlert
+ dev.hasAlert = vibAlert || tAlert || pAlert
+
+ if (dev.hasAlert && !prevHasAlert) {
+ const reasons = []
+ if (vibAlert) reasons.push(`鎸姩鍋忕卤5% (褰撳墠 ${vibVal} / 鍩虹嚎 ${vibBase})`)
+ if (tAlert) reasons.push(`娓╁害瓒婄晫 (褰撳墠 ${tVal}掳C, 鏈熸湜 20~80掳C) `)
+ if (pAlert) reasons.push(`鍘嬪姏瓒婄晫 (褰撳墠 ${pVal}MPa, 鏈熸湜 0.2~1.5MPa) `)
+ ElNotification({
+ title: `${dev.name} 鍛婅`,
+ message: reasons.join('锛�'),
+ type: vibAlert ? 'error' : 'warning',
+ duration: 5000,
+ })
+ }
+}
+
+let timer = null
+function start() {
+ if (timer) return
+ timer = setInterval(() => {
+ if (!collecting.value) return
+ devices.forEach(tickDevice)
+ lastUpdated.value = Date.now()
+ }, 10000)
+}
+
+function stop() {
+ if (timer) {
+ clearInterval(timer)
+ timer = null
+ }
+}
+
+function toggleCollecting() { collecting.value = !collecting.value }
+
+function resetAll() {
+ devices.forEach(dev => {
+ dev.series.vibration = makeSeries(dev.baseline.vibration)
+ const t0 = dev.initial?.temperature ?? 45
+ const p0 = dev.initial?.pressure ?? 0.8
+ dev.series.temperature = makeSeries(t0, 1)
+ dev.series.pressure = makeSeries(p0, 2)
+ dev.alerts.vibration = false
+ dev.alerts.temperature = false
+ dev.alerts.pressure = false
+ dev.hasAlert = false
+ })
+ lastUpdated.value = Date.now()
+}
+
+onMounted(() => {
+ start()
+})
+
+onBeforeUnmount(() => {
+ stop()
+})
+</script>
+
+<style lang="scss" scoped>
+.iot-monitor {
+ .header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ .title { font-size: 18px; font-weight: 600; }
+ .actions { display: flex; align-items: center; gap: 8px; }
+ .ts { color: #909399; font-size: 12px; }
+ }
+ .rule-alert { margin-bottom: 12px; }
+}
+
+.device-card {
+ margin-bottom: 16px;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ &.is-alert { border-color: #F56C6C; box-shadow: 0 0 0 2px rgba(245,108,108,0.2) inset; }
+ .card-header {
+ display: flex; flex-direction: column; gap: 4px;
+ .card-title { display: flex; align-items: center; gap: 8px; font-weight: 600; }
+ .meta { color: #909399; font-size: 12px; }
+ }
+ .metrics {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+}
+
+.metric {
+ border: 1px solid #ebeef5;
+ border-radius: 6px;
+ padding: 8px 8px 0 8px;
+ &-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-size: 13px; color: #606266; }
+ &-value { font-size: 20px; font-weight: 600; margin: 2px 0 6px 0; }
+}
+
+.metric-alert {
+ border-color: #F56C6C;
+ background: #FFF6F6;
+}
+
+@media (min-width: 1200px) {
+ .device-card .metrics { grid-template-columns: 1fr 1fr 1fr; }
+}
+</style>
+
+
diff --git a/src/views/equipmentManagement/kplMonitor/index.vue b/src/views/equipmentManagement/kplMonitor/index.vue
new file mode 100644
index 0000000..178b658
--- /dev/null
+++ b/src/views/equipmentManagement/kplMonitor/index.vue
@@ -0,0 +1,714 @@
+<template>
+ <div class="kpl-monitor-container">
+ <!-- 椤甸潰澶撮儴 -->
+ <div class="page-header">
+ <div class="header-content">
+ <h1>KPL鐩戞帶鍒嗘瀽</h1>
+ <p>璁惧鍏抽敭鎬ц兘鎸囨爣鐩戞帶涓庣淮淇濈瓥鐣ヤ紭鍖�</p>
+ </div>
+ <div class="time-range">
+ <el-date-picker
+ v-model="timeRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ @change="fetchKPLData"
+ />
+ </div>
+ </div>
+
+ <!-- 鍏抽敭鎸囨爣姒傝 -->
+ <div class="metrics-overview">
+ <div class="metric-card mtbf-card">
+ <div class="metric-icon">鈴憋笍</div>
+ <div class="metric-content">
+ <div class="metric-title">MTBF</div>
+ <div class="metric-subtitle">骞冲潎鏃犳晠闅滄椂闂�</div>
+ <div class="metric-value">{{ currentMTBF }}<span class="unit">灏忔椂</span></div>
+ <div class="metric-trend" :class="mtbfTrendClass">
+ <span class="trend-icon">{{ mtbfTrendText }}</span>
+ <span class="trend-text">{{ Math.abs(mtbfChange) }}%</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="metric-card mttr-card">
+ <div class="metric-icon">馃敡</div>
+ <div class="metric-content">
+ <div class="metric-title">MTTR</div>
+ <div class="metric-subtitle">骞冲潎淇鏃堕棿</div>
+ <div class="metric-value">{{ currentMTTR }}<span class="unit">灏忔椂</span></div>
+ <div class="metric-trend" :class="mttrTrendClass">
+ <span class="trend-icon">{{ mttrTrendText }}</span>
+ <span class="trend-text">{{ Math.abs(mttrChange) }}%</span>
+ </div>
+ </div>
+ </div>
+
+ <div class="metric-card availability-card">
+ <div class="metric-icon">馃搳</div>
+ <div class="metric-content">
+ <div class="metric-title">璁惧鍙敤鐜�</div>
+ <div class="metric-subtitle">杩愯鏁堢巼鎸囨爣</div>
+ <div class="metric-value">{{ currentAvailability }}<span class="unit">%</span></div>
+ <div class="metric-trend" :class="availabilityTrendClass">
+ <span class="trend-icon">{{ availabilityTrendText }}</span>
+ <span class="trend-text">{{ Math.abs(availabilityChange) }}%</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 瓒嬪娍鍒嗘瀽鍥捐〃 -->
+ <div class="charts-section">
+ <div class="chart-container">
+ <div class="chart-header">
+ <h3>MTBF & MTTR 瓒嬪娍鍒嗘瀽</h3>
+ <div class="chart-legend">
+ <span class="legend-item mtbf-legend">
+ <span class="legend-color"></span>
+ MTBF (骞冲潎鏃犳晠闅滄椂闂�)
+ </span>
+ <span class="legend-item mttr-legend">
+ <span class="legend-color"></span>
+ MTTR (骞冲潎淇鏃堕棿)
+ </span>
+ </div>
+ </div>
+ <div class="chart-wrapper">
+ <div ref="trendChart" class="chart"></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 缁翠繚绛栫暐寤鸿 -->
+ <div class="recommendations-section">
+ <div class="section-header">
+ <h3>缁翠繚绛栫暐浼樺寲寤鸿</h3>
+ <p>鍩轰簬褰撳墠鎸囨爣鍒嗘瀽锛屼负鎮ㄦ彁渚涢拡瀵规�х殑浼樺寲寤鸿</p>
+ </div>
+ <div class="recommendations-grid">
+ <div
+ v-for="(recommendation, index) in recommendations"
+ :key="index"
+ class="recommendation-card"
+ >
+ <div class="recommendation-icon">{{ recommendation.icon }}</div>
+ <div class="recommendation-content">
+ <h4>{{ recommendation.title }}</h4>
+ <p>{{ recommendation.description }}</p>
+ <div class="recommendation-priority" :class="recommendation.priority">
+ {{ recommendation.priorityText }}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick } from 'vue'
+import * as echarts from 'echarts'
+
+// 鐢熸垚妯℃嫙鏁版嵁
+const generateMockData = () => {
+ const months = ['1鏈�', '2鏈�', '3鏈�', '4鏈�', '5鏈�', '6鏈�', '7鏈�', '8鏈�', '9鏈�', '10鏈�', '11鏈�', '12鏈�']
+ const mtbfData = months.map(() => Math.floor(Math.random() * 200 + 300)) // 300-500灏忔椂
+ const mttrData = months.map(() => Math.floor(Math.random() * 8 + 4)) // 4-12灏忔椂
+
+ return {
+ months,
+ mtbfData,
+ mttrData
+ }
+}
+
+const timeRange = ref([
+ new Date(new Date().getFullYear(), 0, 1).toISOString().split('T')[0],
+ new Date().toISOString().split('T')[0]
+])
+
+const mockData = reactive(generateMockData())
+
+// 璁$畻褰撳墠鎸囨爣鍊�
+const currentMTBF = computed(() => mockData.mtbfData[mockData.mtbfData.length - 1])
+const currentMTTR = computed(() => mockData.mttrData[mockData.mttrData.length - 1])
+const currentAvailability = computed(() =>
+ Math.round((currentMTBF.value / (currentMTBF.value + currentMTTR.value)) * 100)
+)
+
+// 璁$畻鍙樺寲瓒嬪娍
+const mtbfChange = computed(() => {
+ const current = currentMTBF.value
+ const previous = mockData.mtbfData[mockData.mtbfData.length - 2] || current
+ return Math.round(((current - previous) / previous) * 100)
+})
+
+const mttrChange = computed(() => {
+ const current = currentMTTR.value
+ const previous = mockData.mttrData[mockData.mttrData.length - 2] || current
+ return Math.round(((current - previous) / previous) * 100)
+})
+
+const availabilityChange = computed(() => {
+ const current = currentAvailability.value
+ const previous = Math.round(
+ (mockData.mtbfData[mockData.mtbfData.length - 2] /
+ (mockData.mtbfData[mockData.mtbfData.length - 2] + mockData.mttrData[mockData.mttrData.length - 2])) * 100
+ ) || current
+ return Math.round(((current - previous) / previous) * 100)
+})
+
+// 瓒嬪娍鏍峰紡鍜屾枃鏈�
+const mtbfTrendClass = computed(() => mtbfChange.value >= 0 ? 'trend-up' : 'trend-down')
+const mttrTrendClass = computed(() => mttrChange.value <= 0 ? 'trend-up' : 'trend-down')
+const availabilityTrendClass = computed(() => availabilityChange.value >= 0 ? 'trend-up' : 'trend-down')
+
+const mtbfTrendText = computed(() => mtbfChange.value >= 0 ? '鈫�' : '鈫�')
+const mttrTrendText = computed(() => mttrChange.value <= 0 ? '鈫�' : '鈫�')
+const availabilityTrendText = computed(() => availabilityChange.value >= 0 ? '鈫�' : '鈫�')
+
+// 鏅鸿兘缁翠繚寤鸿
+const recommendations = computed(() => {
+ const suggestions = []
+
+ // MTBF鐩稿叧寤鸿
+ if (currentMTBF.value < 400) {
+ suggestions.push({
+ icon: '馃敡',
+ title: '鎻愬崌MTBF',
+ description: '褰撳墠MTBF杈冧綆锛屽缓璁姞寮洪闃叉�х淮鎶わ紝瀹氭湡妫�鏌ュ叧閿儴浠讹紝寤堕暱璁惧鏃犳晠闅滆繍琛屾椂闂�',
+ priority: 'high',
+ priorityText: '楂樹紭鍏堢骇'
+ })
+ }
+
+ // MTTR鐩稿叧寤鸿
+ if (currentMTTR.value > 8) {
+ suggestions.push({
+ icon: '鈿�',
+ title: '浼樺寲MTTR',
+ description: '褰撳墠MTTR杈冮珮锛屽缓璁紭鍖栫淮淇祦绋嬶紝鎻愰珮缁翠慨浜哄憳鎶�鑳斤紝缂╃煭鏁呴殰淇鏃堕棿',
+ priority: 'high',
+ priorityText: '楂樹紭鍏堢骇'
+ })
+ }
+
+ // 鍙敤鐜囩浉鍏冲缓璁�
+ if (currentAvailability.value < 95) {
+ suggestions.push({
+ icon: '馃搱',
+ title: '鎻愬崌鍙敤鐜�',
+ description: '璁惧鍙敤鐜囨湁寰呮彁鍗囷紝寤鸿浼樺寲缁翠繚璁″垝瀹夋帓锛屽噺灏戣鍒掑鍋滄満鏃堕棿',
+ priority: 'medium',
+ priorityText: '涓紭鍏堢骇'
+ })
+ }
+
+ // 缁煎悎寤鸿
+ if (currentMTBF.value >= 400 && currentMTTR.value <= 8 && currentAvailability.value >= 95) {
+ suggestions.push({
+ icon: '鉁�',
+ title: '杩愯鐘跺喌鑹ソ',
+ description: '褰撳墠璁惧杩愯鐘跺喌鑹ソ锛屽悇椤规寚鏍囧潎杈惧埌棰勬湡锛屽缓璁户缁繚鎸佺幇鏈夌淮淇濈瓥鐣�',
+ priority: 'low',
+ priorityText: '浣庝紭鍏堢骇'
+ })
+ }
+
+ // 棰勯槻鎬у缓璁�
+ suggestions.push({
+ icon: '馃搵',
+ title: '棰勯槻鎬х淮鎶�',
+ description: '寤鸿寤虹珛璁惧鍋ュ悍妗f锛屽畾鏈熷垎鏋愯澶囪繍琛屾暟鎹紝鎻愬墠璇嗗埆娼滃湪鏁呴殰椋庨櫓',
+ priority: 'medium',
+ priorityText: '涓紭鍏堢骇'
+ })
+
+ return suggestions
+})
+
+// 鍥捐〃瀹炰緥
+let trendChart = null
+
+const initChart = () => {
+ nextTick(() => {
+ trendChart = echarts.init(document.querySelector('.chart'))
+ trendChart.setOption({
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ }
+ },
+ legend: {
+ data: ['MTBF', 'MTTR'],
+ top: 10
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: mockData.months,
+ axisLine: {
+ lineStyle: {
+ color: '#e0e0e0'
+ }
+ }
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: 'MTBF (灏忔椂)',
+ position: 'left',
+ axisLine: {
+ lineStyle: {
+ color: '#1890ff'
+ }
+ },
+ axisLabel: {
+ formatter: '{value}h'
+ }
+ },
+ {
+ type: 'value',
+ name: 'MTTR (灏忔椂)',
+ position: 'right',
+ axisLine: {
+ lineStyle: {
+ color: '#52c41a'
+ }
+ },
+ axisLabel: {
+ formatter: '{value}h'
+ }
+ }
+ ],
+ series: [
+ {
+ name: 'MTBF',
+ type: 'line',
+ yAxisIndex: 0,
+ data: mockData.mtbfData,
+ smooth: true,
+ lineStyle: {
+ color: '#1890ff',
+ width: 3
+ },
+ itemStyle: {
+ color: '#1890ff'
+ },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(24, 144, 255, 0.3)' },
+ { offset: 1, color: 'rgba(24, 144, 255, 0.1)' }
+ ])
+ }
+ },
+ {
+ name: 'MTTR',
+ type: 'line',
+ yAxisIndex: 1,
+ data: mockData.mttrData,
+ smooth: true,
+ lineStyle: {
+ color: '#52c41a',
+ width: 3
+ },
+ itemStyle: {
+ color: '#52c41a'
+ },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(82, 196, 26, 0.3)' },
+ { offset: 1, color: 'rgba(82, 196, 26, 0.1)' }
+ ])
+ }
+ }
+ ]
+ })
+ })
+}
+
+const fetchKPLData = () => {
+ // 妯℃嫙鏁版嵁鍒锋柊
+ Object.assign(mockData, generateMockData())
+
+ // 閲嶆柊娓叉煋鍥捐〃
+ if (trendChart) {
+ trendChart.setOption({
+ xAxis: {
+ data: mockData.months
+ },
+ series: [
+ {
+ data: mockData.mtbfData
+ },
+ {
+ data: mockData.mttrData
+ }
+ ]
+ })
+ }
+}
+
+onMounted(() => {
+ initChart()
+
+ // 鐩戝惉绐楀彛澶у皬鍙樺寲锛岄噸鏂拌皟鏁村浘琛ㄥぇ灏�
+ window.addEventListener('resize', () => {
+ if (trendChart) trendChart.resize()
+ })
+})
+</script>
+
+<style scoped>
+.kpl-monitor-container {
+ min-height: 100vh;
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+ padding: 24px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+/* 椤甸潰澶撮儴 */
+.page-header {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ margin-bottom: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.header-content h1 {
+ margin: 0 0 8px 0;
+ color: #1f2937;
+ font-size: 28px;
+ font-weight: 700;
+}
+
+.header-content p {
+ margin: 0;
+ color: #6b7280;
+ font-size: 16px;
+}
+
+.time-range {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+/* 鎸囨爣姒傝 */
+.metrics-overview {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ margin-bottom: 24px;
+}
+
+.metric-card {
+ background: white;
+ border-radius: 12px;
+ padding: 24px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.metric-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+}
+
+.metric-icon {
+ font-size: 32px;
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.mtbf-card .metric-icon {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.mttr-card .metric-icon {
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+}
+
+.availability-card .metric-icon {
+ background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
+}
+
+.metric-content {
+ flex: 1;
+}
+
+.metric-title {
+ font-size: 18px;
+ font-weight: 600;
+ color: #1f2937;
+ margin-bottom: 4px;
+}
+
+.metric-subtitle {
+ font-size: 14px;
+ color: #6b7280;
+ margin-bottom: 12px;
+}
+
+.metric-value {
+ font-size: 32px;
+ font-weight: 700;
+ color: #1f2937;
+ margin-bottom: 8px;
+}
+
+.unit {
+ font-size: 16px;
+ font-weight: 500;
+ color: #6b7280;
+ margin-left: 4px;
+}
+
+.metric-trend {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 14px;
+ font-weight: 600;
+}
+
+.trend-up {
+ color: #10b981;
+}
+
+.trend-down {
+ color: #ef4444;
+}
+
+.trend-icon {
+ font-size: 16px;
+}
+
+/* 鍥捐〃鍖哄煙 */
+.charts-section {
+ margin-bottom: 24px;
+}
+
+.chart-container {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.chart-header {
+ padding: 20px 24px;
+ border-bottom: 1px solid #e5e7eb;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.chart-header h3 {
+ margin: 0;
+ color: #1f2937;
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.chart-legend {
+ display: flex;
+ gap: 20px;
+}
+
+.legend-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 14px;
+ color: #6b7280;
+}
+
+.legend-color {
+ width: 12px;
+ height: 12px;
+ border-radius: 2px;
+}
+
+.mtbf-legend .legend-color {
+ background: #1890ff;
+}
+
+.mttr-legend .legend-color {
+ background: #52c41a;
+}
+
+.chart-wrapper {
+ padding: 20px;
+ height: 400px;
+}
+
+.chart {
+ width: 100%;
+ height: 100%;
+}
+
+/* 寤鸿鍖哄煙 */
+.recommendations-section {
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.section-header {
+ padding: 24px;
+ border-bottom: 1px solid #e5e7eb;
+}
+
+.section-header h3 {
+ margin: 0 0 8px 0;
+ color: #1f2937;
+ font-size: 20px;
+ font-weight: 600;
+}
+
+.section-header p {
+ margin: 0;
+ color: #6b7280;
+ font-size: 14px;
+}
+
+.recommendations-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
+ gap: 20px;
+ padding: 24px;
+}
+
+.recommendation-card {
+ background: #f8fafc;
+ border-radius: 8px;
+ padding: 20px;
+ display: flex;
+ gap: 16px;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.recommendation-card:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.recommendation-icon {
+ font-size: 24px;
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ background: white;
+ flex-shrink: 0;
+}
+
+.recommendation-content {
+ flex: 1;
+}
+
+.recommendation-content h4 {
+ margin: 0 0 8px 0;
+ color: #1f2937;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.recommendation-content p {
+ margin: 0 0 12px 0;
+ color: #6b7280;
+ font-size: 14px;
+ line-height: 1.5;
+}
+
+.recommendation-priority {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.priority.high {
+ background: #fef2f2;
+ color: #dc2626;
+}
+
+.priority.medium {
+ background: #fffbeb;
+ color: #d97706;
+}
+
+.priority.low {
+ background: #f0fdf4;
+ color: #16a34a;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .kpl-monitor-container {
+ padding: 16px;
+ }
+
+ .page-header {
+ flex-direction: column;
+ gap: 16px;
+ align-items: stretch;
+ }
+
+ .metrics-overview {
+ grid-template-columns: 1fr;
+ }
+
+ .recommendations-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .chart-wrapper {
+ height: 300px;
+ }
+
+ .chart-legend {
+ flex-direction: column;
+ gap: 8px;
+ }
+}
+
+@media (max-width: 480px) {
+ .metric-card {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .recommendation-card {
+ flex-direction: column;
+ text-align: center;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/ledger/Form.vue b/src/views/equipmentManagement/ledger/Form.vue
new file mode 100644
index 0000000..f1d21f2
--- /dev/null
+++ b/src/views/equipmentManagement/ledger/Form.vue
@@ -0,0 +1,333 @@
+<template>
+ <el-form :model="form" label-width="120px" :rules="formRules" ref="formRef">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璁惧鍚嶇О" prop="deviceName">
+ <el-input v-model="form.deviceName" placeholder="璇疯緭鍏ヨ澶囧悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿" prop="deviceModel">
+ <el-input v-model="form.deviceModel" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁惧鍝佺墝" prop="deviceBrand">
+ <el-input v-model="form.deviceBrand" placeholder="璇疯緭鍏ヨ澶囧搧鐗�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁惧绫诲瀷" prop="type">
+ <el-select
+ v-model="form.type"
+ placeholder="璇烽�夋嫨鎴栬緭鍏ヨ澶囩被鍨�"
+ clearable
+ filterable
+ allow-create
+ default-first-option
+ style="width: 100%"
+ @change="handleDeviceTypeChange"
+ >
+ <el-option
+ v-for="item in deviceTypeOptions"
+ :key="item"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�" prop="supplierName">
+ <el-input v-model="form.supplierName" placeholder="璇疯緭鍏ヤ緵搴斿晢" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀛樻斁浣嶇疆" prop="storageLocation">
+ <el-input v-model="form.storageLocation" placeholder="璇疯緭鍏ュ瓨鏀句綅缃�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅" prop="unit">
+ <el-input v-model="form.unit" placeholder="璇疯緭鍏ュ崟浣�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍚敤鎶樻棫" prop="isDepr">
+ <el-switch v-model="form.isDepr" :active-value="1" :inactive-value="2" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.isDepr === 1">
+ <el-form-item label="姣忓勾鎶樻棫閲戦" prop="annualDepreciationAmount">
+ <el-input-number
+ :step="0.01"
+ :min="0"
+ style="width: 100%"
+ v-model="form.annualDepreciationAmount"
+ placeholder="璇疯緭鍏ユ瘡骞存姌鏃ч噾棰�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺" prop="number">
+ <el-input-number :min="1" style="width: 100%"
+ v-model="form.number"
+ disabled
+ placeholder="璇疯緭鍏ユ暟閲�"
+ @change="mathNum"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍚◣鍗曚环" prop="taxIncludingPriceUnit">
+ <el-input-number :step="0.01" :min="0" style="width: 100%"
+ v-model="form.taxIncludingPriceUnit"
+ placeholder="璇疯緭鍏ュ惈绋庡崟浠�"
+ maxlength="10"
+ @change="mathNum"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍚◣鎬讳环" prop="taxIncludingPriceTotal">
+ <el-input
+ v-model="form.taxIncludingPriceTotal"
+ placeholder="鑷姩鐢熸垚"
+ type="number"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)" prop="taxRate">
+ <el-select
+ v-model="form.taxRate"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="mathNum"
+ >
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="Number(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓嶅惈绋庢�讳环" prop="unTaxIncludingPriceTotal">
+ <el-input
+ v-model="form.unTaxIncludingPriceTotal"
+ placeholder="鑷姩鐢熸垚"
+ type="number"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ <!-- <el-col :span="12">
+ <el-form-item label="褰曞叆浜�" prop="createUser">
+ <el-input v-model="form.createUser" placeholder="璇疯緭鍏ュ綍鍏ヤ汉" />
+ </el-form-item>
+ </el-col> -->
+ <el-col :span="12">
+ <el-form-item label="褰曞叆鏃ユ湡" prop="createTime">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.createTime"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="date"
+ placeholder="璇烽�夋嫨褰曞叆鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="棰勮杩愯鏃堕棿" prop="planRuntimeTime">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.planRuntimeTime"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨褰曞叆鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="璁惧鍥剧墖" prop="storageBlobDTOs">
+ <AttachmentUploadImage
+ v-model:fileList="fileList"
+ :limit="20"
+ :fileSize="5"
+ :buttonText="'涓婁紶鍥剧墖'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+
+<script setup>
+import useFormData from "@/hooks/useFormData";
+// import useUserStore from "@/store/modules/user";
+import { getLedgerById } from "@/api/equipmentManagement/ledger";
+import dayjs from "dayjs";
+import {
+ calculateTaxIncludeTotalPrice,
+ calculateTaxExclusiveTotalPrice,
+} from "@/utils/summarizeTable";
+import { ElMessage } from "element-plus";
+import { ref, getCurrentInstance, computed } from "vue";
+import AttachmentUploadImage from '@/components/AttachmentUpload/image/index.vue';
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+defineOptions({
+ name: "璁惧鍙拌处琛ㄥ崟",
+});
+const formRef = ref(null);
+const operationType = ref('');
+// 璁惧绫诲瀷鍥哄畾閫夐」
+const deviceTypeOptions = ref([
+ '鐢熶骇璁惧',
+ '鍔炲叕璁惧',
+ '妫�娴嬭澶�',
+ '杩愯緭璁惧',
+ '鍏朵粬璁惧'
+]);
+const formRules = {
+ deviceName: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ deviceModel: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ type: [{ required: true, trigger: "change", message: "璇烽�夋嫨鎴栬緭鍏ヨ澶囩被鍨�" }],
+ supplierName: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ unit: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ number: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ taxIncludingPriceUnit: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ taxRate: [{ required: true, trigger: "change", message: "璇疯緭鍏�" }],
+ planRuntimeTime: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+ annualDepreciationAmount: [
+ {
+ validator: (rule, value, callback) => {
+ if (form.isDepr === 1 && (value === undefined || value === null || value === '')) {
+ callback(new Error('鍚敤鎶樻棫鏃讹紝璇疯緭鍏ユ瘡骞存姌鏃ч噾棰�'));
+ } else {
+ callback();
+ }
+ },
+ trigger: "blur"
+ }
+ ],
+}
+
+const { form, resetForm } = useFormData({
+ deviceName: undefined, // 璁惧鍚嶇О
+ deviceModel: undefined, // 瑙勬牸鍨嬪彿
+ deviceBrand: undefined, // 璁惧鍝佺墝
+ type: undefined, // 璁惧绫诲瀷
+ supplierName: undefined, // 渚涘簲鍟�
+ storageLocation: undefined, // 瀛樻斁浣嶇疆
+ isDepr: 2, // 鏄惁鍚敤鎶樻棫 1-鏄� 2-鍚�
+ annualDepreciationAmount: undefined, // 姣忓勾鎶樻棫閲戦
+ unit: undefined, // 鍗曚綅
+ number: 1, // 鏁伴噺
+ taxIncludingPriceUnit: undefined, // 鍚◣鍗曚环
+ taxIncludingPriceTotal: undefined, // 鍚◣鎬讳环
+ taxRate: undefined, // 绋庣巼
+ unTaxIncludingPriceTotal: undefined, // 涓嶅惈绋庢�讳环
+ // createUser: useUserStore().nickName, // 褰曞叆浜�
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 褰曞叆鏃ユ湡
+ planRuntimeTime: dayjs().format("YYYY-MM-DD"), // 褰曞叆鏃ユ湡
+ storageBlobDTOs: undefined, // 璁惧鍥剧墖鎻愪氦
+ storageBlobVOs: undefined, // 璁惧鍥剧墖灞曠ず
+});
+
+const fileList = computed({
+ get() {
+ return form.storageBlobVOs || [];
+ },
+ set(val) {
+ form.storageBlobDTOs = val;
+ form.storageBlobVOs = val;
+ }
+});
+
+const loadForm = async (id) => {
+ if (id) {
+ operationType.value = 'edit'
+ }
+ const { code, data } = await getLedgerById(id);
+ if (code == 200) {
+ form.deviceName = data.deviceName;
+ form.deviceModel = data.deviceModel;
+ form.deviceBrand = data.deviceBrand;
+ form.type = data.type;
+ form.supplierName = data.supplierName;
+ form.storageLocation = data.storageLocation;
+ form.isDepr = data.isDepr;
+ form.annualDepreciationAmount = data.annualDepreciationAmount;
+ form.unit = data.unit;
+ form.number = 1;
+ form.taxIncludingPriceUnit = data.taxIncludingPriceUnit;
+ form.taxIncludingPriceTotal = data.taxIncludingPriceTotal;
+ form.taxRate = data.taxRate;
+ form.unTaxIncludingPriceTotal = data.unTaxIncludingPriceTotal;
+ form.createTime = data.createTime;
+ // 棰勮杩愯鏃堕棿锛氬悗绔繑鍥炲悗杞负 YYYY-MM-DD 浠ヤ究鏃ユ湡閫夋嫨鍣ㄦ纭睍绀�
+ if (data.planRuntimeTime) {
+ form.planRuntimeTime = dayjs(data.planRuntimeTime).format('YYYY-MM-DD');
+ } else {
+ form.planRuntimeTime = undefined;
+ }
+ form.storageBlobVOs = data.storageBlobVOs;
+ form.storageBlobDTOs = data.storageBlobVOs;
+ }
+};
+
+const handleDeviceTypeChange = (value) => {
+ // 濡傛灉杈撳叆鐨勬柊鍊间笉鍦ㄥ浐瀹氶�夐」涓紝鍒欐坊鍔犲埌閫夐」鍒楄〃
+ if (value && !deviceTypeOptions.value.includes(value)) {
+ deviceTypeOptions.value.push(value);
+ }
+};
+
+const mathNum = () => {
+ if (!form.taxIncludingPriceUnit) {
+ ElMessage.error("璇疯緭鍏ュ崟浠�");
+ return;
+ }
+ form.taxIncludingPriceTotal = calculateTaxIncludeTotalPrice(
+ form.taxIncludingPriceUnit,
+ form.number
+ );
+ if (form.taxRate) {
+ form.unTaxIncludingPriceTotal = calculateTaxExclusiveTotalPrice(
+ form.taxIncludingPriceTotal,
+ form.taxRate
+ );
+ }
+};
+
+// 娓呴櫎琛ㄥ崟鏍¢獙鐘舵��
+const clearValidate = () => {
+ formRef.value?.clearValidate();
+};
+
+// 閲嶇疆琛ㄥ崟鏁版嵁鍜屾牎楠岀姸鎬�
+const resetFormAndValidate = () => {
+ resetForm();
+ clearValidate();
+};
+
+defineExpose({
+ form,
+ loadForm,
+ resetForm,
+ clearValidate,
+ resetFormAndValidate,
+ formRef,
+});
+</script>
diff --git a/src/views/equipmentManagement/ledger/Modal.vue b/src/views/equipmentManagement/ledger/Modal.vue
new file mode 100644
index 0000000..16166c6
--- /dev/null
+++ b/src/views/equipmentManagement/ledger/Modal.vue
@@ -0,0 +1,69 @@
+<template>
+ <el-dialog :title="modalOptions.title" v-model="visible" @close="close" draggable>
+ <Form ref="formRef"></Form>
+ <template #footer>
+ <el-button type="primary" @click="sendForm" :loading="loading">
+ {{ modalOptions.confirmText }}
+ </el-button>
+ <el-button @click="closeModal">{{ modalOptions.cancelText }}</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { useModal } from "@/hooks/useModal";
+import { addLedger, editLedger } from "@/api/equipmentManagement/ledger";
+import Form from "./Form.vue";
+import { ElMessage } from "element-plus";
+const { proxy } = getCurrentInstance()
+
+defineOptions({
+ name: "璁惧鍙拌处鏂板缂栬緫",
+});
+
+const emits = defineEmits(["success"]);
+
+const formRef = ref();
+const {
+ id,
+ visible,
+ loading,
+ openModal,
+ modalOptions,
+ handleConfirm,
+ closeModal,
+} = useModal({ title: "璁惧鍙拌处" });
+
+const sendForm = () => {
+ proxy.$refs.formRef.$refs.formRef.validate(async valid => {
+ if (valid) {
+ const {code} = id.value
+ ? await editLedger({id: id.value, ...formRef.value.form})
+ : await addLedger(formRef.value.form);
+ if (code == 200) {
+ emits("success");
+ ElMessage({message: "鎿嶄綔鎴愬姛", type: "success"});
+ close();
+ } else {
+ loading.value = false;
+ }
+ }
+ })
+};
+
+const close = () => {
+ formRef.value.resetFormAndValidate();
+ closeModal();
+};
+
+const loadForm = async (id) => {
+ openModal(id);
+ await nextTick();
+ formRef.value.loadForm(id);
+};
+
+defineExpose({
+ openModal,
+ loadForm,
+});
+</script>
diff --git a/src/views/equipmentManagement/ledger/index.vue b/src/views/equipmentManagement/ledger/index.vue
new file mode 100644
index 0000000..af10532
--- /dev/null
+++ b/src/views/equipmentManagement/ledger/index.vue
@@ -0,0 +1,439 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="璁惧鍚嶇О">
+ <el-input
+ v-model="filters.deviceName"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ヨ澶囧悕绉�"
+ clearable
+ @change="getTableData"
+ />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿">
+ <el-input
+ v-model="filters.deviceModel"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ clearable
+ @change="getTableData"
+ />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�">
+ <el-input
+ v-model="filters.supplierName"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ヤ緵搴斿晢"
+ clearable
+ @change="getTableData"
+ />
+ </el-form-item>
+ <el-form-item label="褰曞叆鏃ユ湡:">
+ <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+ placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus"> 鏂板 </el-button>
+ <el-button type="info" @click="handleImport" icon="Upload">瀵煎叆</el-button>
+ <el-button @click="handleOut" icon="download">瀵煎嚭</el-button>
+ <el-button
+ type="danger"
+ icon="Delete"
+ :disabled="multipleList.length <= 0"
+ @click="deleteRow(multipleList.map((item) => item.id))"
+ >
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ </PIMTable>
+ </div>
+ <Modal ref="modalRef" @success="getTableData"></Modal>
+ <el-dialog v-model="qrDialogVisible" title="浜岀淮鐮�" width="300px" draggable>
+ <div style="text-align:center;">
+ <img :src="qrCodeUrl" alt="浜岀淮鐮�" style="width:200px;height:200px;" />
+ <div style="margin:10px 0;">
+ <el-button type="primary" @click="downloadQRCode">涓嬭浇浜岀淮鐮佸浘鐗�</el-button>
+ </div>
+ </div>
+ </el-dialog>
+
+ <!-- 瀵煎叆瀵硅瘽妗� -->
+ <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url"
+ :disabled="upload.isUploading"
+ :on-progress="handleFileUploadProgress"
+ :on-success="handleFileSuccess"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">涓嬭浇妯℃澘</el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 璇︽儏瀵硅瘽妗� -->
+ <el-dialog v-model="detailDialogVisible" title="璁惧鍙拌处璇︽儏" width="60%" draggable>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="璁惧鍚嶇О">{{ detailData.deviceName }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{ detailData.deviceModel }}</el-descriptions-item>
+ <el-descriptions-item label="璁惧鍝佺墝">{{ detailData.deviceBrand }}</el-descriptions-item>
+ <el-descriptions-item label="璁惧绫诲瀷">{{ detailData.type }}</el-descriptions-item>
+ <el-descriptions-item label="渚涘簲鍟�">{{ detailData.supplierName }}</el-descriptions-item>
+ <el-descriptions-item label="瀛樻斁浣嶇疆">{{ detailData.storageLocation }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曚綅">{{ detailData.unit }}</el-descriptions-item>
+ <el-descriptions-item label="鏁伴噺">{{ detailData.number }}</el-descriptions-item>
+ <el-descriptions-item label="鍚敤鎶樻棫">{{ detailData.isDepr === 1 ? '鏄�' : '鍚�' }}</el-descriptions-item>
+ <el-descriptions-item label="姣忓勾鎶樻棫閲戦">{{ detailData.annualDepreciationAmount }}</el-descriptions-item>
+ <el-descriptions-item label="鍚◣鍗曚环">{{ detailData.taxIncludingPriceUnit }}</el-descriptions-item>
+ <el-descriptions-item label="鍚◣鎬讳环">{{ detailData.taxIncludingPriceTotal }}</el-descriptions-item>
+ <el-descriptions-item label="绋庣巼(%)">{{ detailData.taxRate }}</el-descriptions-item>
+ <el-descriptions-item label="涓嶅惈绋庢�讳环">{{ detailData.unTaxIncludingPriceTotal }}</el-descriptions-item>
+ <el-descriptions-item label="褰曞叆鏃ユ湡">{{ detailData.createTime }}</el-descriptions-item>
+ <el-descriptions-item label="棰勮杩愯鏃堕棿">{{ detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : '' }}</el-descriptions-item>
+ <el-descriptions-item label="璁惧鍥剧墖" :span="2">
+ <div v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0" style="display: flex; gap: 10px; flex-wrap: wrap;">
+ <el-image
+ v-for="(file, index) in detailData.storageBlobVOs"
+ :key="index"
+ :src="file.previewURL || file.url"
+ :preview-src-list="detailData.storageBlobVOs.map(u => u.previewURL || u.url)"
+ :initial-index="index"
+ style="width: 100px; height: 100px"
+ fit="cover"
+ />
+ </div>
+ <span v-else>鏃犲浘鐗�</span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <template #footer>
+ <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { usePaginationApi } from "@/hooks/usePaginationApi";
+// import { Search } from "@element-plus/icons-vue";
+import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger";
+import { onMounted, getCurrentInstance, ref, reactive } from "vue";
+import Modal from "./Modal.vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import { UploadFilled } from "@element-plus/icons-vue";
+import { getToken } from "@/utils/auth";
+import dayjs from "dayjs";
+import QRCode from "qrcode";
+
+defineOptions({
+ name: "璁惧鍙拌处",
+});
+
+// 琛ㄦ牸澶氶�夋閫変腑椤�
+const multipleList = ref([]);
+const { proxy } = getCurrentInstance();
+const modalRef = ref();
+const qrDialogVisible = ref(false);
+const qrCodeUrl = ref("");
+const qrRowData = ref(null);
+
+const detailDialogVisible = ref(false);
+const detailData = ref({});
+
+// 瀵煎叆鐩稿叧
+const uploadRef = ref(null)
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞�
+ open: false,
+ // 寮瑰嚭灞傛爣棰�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import"
+})
+
+const {
+ filters,
+ columns,
+ dataList,
+ pagination,
+ getTableData,
+ resetFilters,
+ onCurrentChange,
+} = usePaginationApi(
+ getLedgerPage,
+ {
+ deviceName: undefined,
+ deviceModel: undefined,
+ supplierName: undefined,
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ [
+ {
+ label: "璁惧鍚嶇О",
+ prop: "deviceName",
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "deviceModel",
+ },
+ {
+ label: "璁惧鍝佺墝",
+ prop: "deviceBrand",
+ },
+ {
+ label: "璁惧绫诲瀷",
+ prop: "type",
+ },
+ {
+ label: "渚涘簲鍟�",
+ prop: "supplierName",
+ },
+ {
+ label: "瀛樻斁浣嶇疆",
+ prop: "storageLocation",
+ },
+ {
+ label: "鏁伴噺",
+ prop: "number",
+ },
+ {
+ label: "褰曞叆浜�",
+ prop: "createUser",
+ },
+ {
+ label: "褰曞叆鏃ユ湡",
+ prop: "createTime",
+ formatData: (v) => {
+ if (!v) return '';
+ // 濡傛灉鍖呭惈鏃跺垎绉掞紝鍙彇鏃ユ湡閮ㄥ垎
+ if (v.includes(' ')) {
+ return v.split(' ')[0];
+ }
+ return v;
+ },
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: 180,
+ operation: [
+ {
+ name: "璇︽儏",
+ clickFun: (row) => {
+ handleDetail(row);
+ },
+ },
+ {
+ name: "缂栬緫",
+ clickFun: (row) => {
+ edit(row.id)
+ },
+ },
+ {
+ name: "浜岀淮鐮�",
+ clickFun: (row) => {
+ showQRCode(row)
+ },
+ },
+ ],
+ },
+ ]
+);
+
+// 澶氶�夊悗鍋氫粈涔�
+const handleSelectionChange = (selectionList) => {
+ multipleList.value = selectionList;
+};
+
+const add = () => {
+ modalRef.value.openModal();
+};
+const edit = (id) => {
+ modalRef.value.loadForm(id);
+};
+const handleDetail = async (row) => {
+ const { code, data } = await getLedgerById(row.id);
+ if (code == 200) {
+ detailData.value = data;
+ detailDialogVisible.value = true;
+ }
+};
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ onCurrentChange(page);
+};
+const deleteRow = (id) => {
+ ElMessageBox.confirm("姝ゆ搷浣滃皢姘镐箙鍒犻櫎璇ユ枃浠�, 鏄惁缁х画?", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ const { code } = await delLedger(id);
+ if (code == 200) {
+ ElMessage({
+ type: "success",
+ message: "鍒犻櫎鎴愬姛",
+ });
+ getTableData();
+ }
+ });
+};
+
+const changeDaterange = (value) => {
+ if (value) {
+ filters.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ filters.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ } else {
+ filters.entryDateStart = undefined;
+ filters.entryDateEnd = undefined;
+ }
+ getTableData();
+};
+
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(`/device/ledger/export`, {}, "璁惧鍙拌处妗f.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+const showQRCode = async (row) => {
+ // 鐩存帴浣跨敤URL锛屼笉瑕佺敤JSON.stringify鍖呰
+ const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id;
+ const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 200, margin: 2 });
+
+ // 鍒涘缓canvas鍚堟垚甯﹀悕绉扮殑浜岀淮鐮佸浘鐗�
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ const qrSize = 200;
+ const textHeight = 30;
+ const padding = 10;
+ canvas.width = qrSize + padding * 2;
+ canvas.height = qrSize + textHeight + padding * 2;
+
+ // 濉厖鐧借壊鑳屾櫙
+ ctx.fillStyle = '#ffffff';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // 缁樺埗浜岀淮鐮�
+ const qrImg = new Image();
+ qrImg.src = qrDataUrl;
+ await new Promise((resolve) => { qrImg.onload = resolve; });
+ ctx.drawImage(qrImg, padding, padding, qrSize, qrSize);
+
+ // 缁樺埗璁惧鍚嶇О
+ ctx.fillStyle = '#333333';
+ ctx.font = 'bold 14px Arial';
+ ctx.textAlign = 'center';
+ ctx.fillText(row.deviceName || '', canvas.width / 2, qrSize + padding + 20);
+
+ qrCodeUrl.value = canvas.toDataURL('image/png');
+ qrRowData.value = row;
+ qrDialogVisible.value = true;
+};
+
+const downloadQRCode = () => {
+ const a = document.createElement("a");
+ a.href = qrCodeUrl.value;
+ a.download = `${qrRowData.value.deviceName || "浜岀淮鐮�"}.png`;
+ a.click();
+};
+
+// 瀵煎叆鎸夐挳鎿嶄綔
+const handleImport = () => {
+ upload.title = "璁惧鍙拌处瀵煎叆"
+ upload.open = true
+}
+
+// 涓嬭浇妯℃澘鎿嶄綔
+const importTemplate = () => {
+ proxy.download("/device/ledger/downloadTemplate", {}, `璁惧鍙拌处瀵煎叆妯℃澘_${new Date().getTime()}.xlsx`)
+}
+
+// 鏂囦欢涓婁紶涓鐞�
+const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true
+}
+
+// 鏂囦欢涓婁紶鎴愬姛澶勭悊
+const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false
+ upload.isUploading = false
+ proxy.$refs["uploadRef"].handleRemove(file)
+ proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "瀵煎叆缁撴灉", { dangerouslyUseHTMLString: true })
+ getTableData()
+}
+
+// 鎻愪氦涓婁紶鏂囦欢
+const submitFileForm = () => {
+ proxy.$refs["uploadRef"].submit()
+}
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.table_list {
+ margin-top: unset;
+}
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+</style>
diff --git a/src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue b/src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
new file mode 100644
index 0000000..923dd2c
--- /dev/null
+++ b/src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
@@ -0,0 +1,282 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="璁¢噺鍣ㄥ叿"
+ width="50%"
+ draggable
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="璁¢噺鍣ㄥ叿缂栧彿锛�" prop="customerName">
+ <el-input
+ v-model="form.code"
+ placeholder="璇疯緭鍏�"
+ clearable
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁¢噺鍣ㄥ叿鍚嶇О锛�" prop="proDesc">
+ <el-input
+ v-model="form.name"
+ placeholder="璇疯緭鍏�"
+ clearable
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="妫�瀹氭棩鏈燂細" prop="recordDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.recordDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏈夋晥鏃ユ湡(澶�)锛�" prop="valid">
+ <el-input
+ v-model="form.valid"
+ type="number"
+ placeholder="璇疯緭鍏ユ湁鏁堟湡澶╂暟"
+ clearable
+ :min="1"
+ @input="handleValidInput"
+ >
+ <template #append>鏃�</template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="褰曞叆浜猴細" prop="userId">
+ <el-select
+ v-model="form.userId"
+ placeholder="璇烽�夋嫨"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ clearable
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰曞叆鏃ユ湡锛�" prop="entryDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.entryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+<!-- <el-row :gutter="30">-->
+<!-- <el-col :span="24">-->
+<!-- <el-form-item label="闄勪欢鏉愭枡锛�" prop="remark">-->
+<!-- <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload-->
+<!-- :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"-->
+<!-- :on-success="handleUploadSuccess" :on-remove="handleRemove">-->
+<!-- <el-button type="primary" v-if="operationType !== 'view'">涓婁紶</el-button>-->
+<!-- <template #tip v-if="operationType !== 'view'">-->
+<!-- <div class="el-upload__tip">-->
+<!-- 鏂囦欢鏍煎紡鏀寔-->
+<!-- doc锛宒ocx锛寈ls锛寈lsx锛宲pt锛宲ptx锛宲df锛宼xt锛寈ml锛宩pg锛宩peg锛宲ng锛実if锛宐mp锛宺ar锛寊ip锛�7z-->
+<!-- </div>-->
+<!-- </template>-->
+<!-- </el-upload>-->
+<!-- </el-form-item>-->
+<!-- </el-col>-->
+<!-- </el-row>-->
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, toRefs, getCurrentInstance} from "vue";
+import useUserStore from "@/store/modules/user.js";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+import {afterSalesServiceAdd, afterSalesServiceUpdate} from "@/api/customerService/index.js";
+import {getToken} from "@/utils/auth.js";
+import {ledgerRecordUpdate, ledgerRecordVerifying} from "@/api/equipmentManagement/calibration.js";
+import {delLedgerFile} from "@/api/salesManagement/salesLedger.js";
+import { getCurrentDate } from "@/utils/index.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const userStore = useUserStore();
+
+const data = reactive({
+ form: {
+ code: "",
+ name: "",
+ valid: "",
+ recordDate: "",
+ userId: "",
+ entryDate: "",
+ tempFileIds: []
+ },
+ rules: {
+ code: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
+ name: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
+ valid: [
+ {required: true, message: "璇疯緭鍏�", trigger: "blur"},
+ {
+ validator: (rule, value, callback) => {
+ if (value === '' || value === null || value === undefined) {
+ callback();
+ return;
+ }
+ const numValue = Number(value);
+ if (isNaN(numValue)) {
+ callback(new Error('璇疯緭鍏ユ湁鏁堢殑鏁板瓧'));
+ return;
+ }
+ if (numValue <= 0) {
+ callback(new Error('鍙兘杈撳叆姝f暟'));
+ return;
+ }
+ if (!Number.isInteger(numValue)) {
+ callback(new Error('璇疯緭鍏ユ暣鏁�'));
+ return;
+ }
+ callback();
+ },
+ trigger: 'blur'
+ }
+ ],
+ recordDate: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
+ userId: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
+ entryDate: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
+ }
+})
+const { form, rules } = toRefs(data);
+const userList = ref([])
+const fileList = ref([]);
+const upload = reactive({
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+});
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ console.log(row)
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ userListNoPageByTenantId().then((res) => {
+ userList.value = res.data;
+ });
+ fileList.value = []
+ if(type !== "add"){
+ form.value.tempFileIds = [];
+ }
+ if (type === "edit") {
+ form.value.valid = row.valid;
+ form.value.recordDate = row.recordDate;
+ fileList.value = row.commonFiles;
+ }
+ if(type === "add"){
+ fileList.value = row.commonFiles;
+ }
+ if(type === "verifying"){
+ form.value.valid = row.valid;
+ form.value.recordDate = row.mostDate;
+ }
+
+ form.value.id = row.id;
+ form.value.code = row.code;
+ form.value.name = row.name;
+ form.value.userId = userStore.id;
+ form.value.entryDate = getCurrentDate();
+}
+
+// 澶勭悊鏈夋晥鏃ユ湡杈撳叆锛屽彧鍏佽姝f暣鏁�
+const handleValidInput = (value) => {
+ if (value === '' || value === null || value === undefined) {
+ form.value.valid = '';
+ return;
+ }
+ // 杞崲涓哄瓧绗︿覆骞剁Щ闄ゆ墍鏈夐潪鏁板瓧瀛楃锛堝寘鎷礋鍙枫�佸皬鏁扮偣绛夛級
+ const numStr = String(value).replace(/[^0-9]/g, '');
+ if (numStr === '') {
+ form.value.valid = '';
+ return;
+ }
+ const numValue = parseInt(numStr, 10);
+ // 纭繚鏄鏁存暟锛堝ぇ浜�0锛�
+ if (numValue > 0 && !isNaN(numValue)) {
+ form.value.valid = numValue;
+ } else {
+ form.value.valid = '';
+ }
+}
+
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "verifying") {
+ ledgerRecordVerifying(form.value).then(response => {
+ proxy.$modal.msgSuccess("妫�瀹氭牎鍑嗘垚鍔�")
+ closeDia()
+ })
+ } else {
+ ledgerRecordUpdate(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ closeDia()
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/measurementEquipment/components/dialogForm.vue b/src/views/equipmentManagement/measurementEquipment/components/dialogForm.vue
new file mode 100644
index 0000000..c6aa70e
--- /dev/null
+++ b/src/views/equipmentManagement/measurementEquipment/components/dialogForm.vue
@@ -0,0 +1,7 @@
+<template>
+
+</template>
+
+<script setup>
+
+</script>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/measurementEquipment/components/formDia.vue b/src/views/equipmentManagement/measurementEquipment/components/formDia.vue
new file mode 100644
index 0000000..16ac41f
--- /dev/null
+++ b/src/views/equipmentManagement/measurementEquipment/components/formDia.vue
@@ -0,0 +1,325 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="璁¢噺鍣ㄥ叿"
+ width="50%"
+ draggable
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍑哄巶缂栧彿锛�" prop="code">
+ <el-input
+ v-model="form.code"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁¢噺鍣ㄥ叿鍚嶇О锛�" prop="name">
+ <el-input
+ v-model="form.name"
+ placeholder="璇疯緭鍏ヨ閲忓櫒鍏峰悕绉�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="妫�瀹氬崟浣嶏細" prop="unit">
+ <el-input
+ v-model="form.unit"
+ placeholder="璇疯緭鍏ユ瀹氬崟浣�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇佷功缂栧彿锛�" prop="model">
+ <el-input
+ v-model="form.model"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鏈�鏂伴壌瀹氭棩鏈燂細" prop="mostDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.mostDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏈夋晥鏃ユ湡(澶�)锛�" prop="valid">
+ <el-input
+ v-model="form.valid"
+ type="number"
+ placeholder="璇疯緭鍏ユ湁鏁堟湡澶╂暟"
+ clearable
+ :min="1"
+ @input="handleValidInput"
+ >
+ <template #append>鏃�</template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="褰曞叆浜猴細" prop="userId">
+ <el-select
+ v-model="form.userId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰曞叆鏃ユ湡锛�" prop="recordDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.recordDate"
+ value-format="YYYY-MM-DD"
+ disabled
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+<!-- <el-row :gutter="30">-->
+<!-- <el-col :span="24">-->
+<!-- <el-form-item label="闄勪欢鏉愭枡锛�" prop="remark">-->
+<!-- <el-upload v-model:file-list="fileList" :action="upload.url" multiple ref="fileUpload" auto-upload-->
+<!-- :headers="upload.headers" :before-upload="handleBeforeUpload" :on-error="handleUploadError"-->
+<!-- :on-success="handleUploadSuccess" :on-remove="handleRemove">-->
+<!-- <el-button type="primary" v-if="operationType !== 'view'">涓婁紶</el-button>-->
+<!-- <template #tip v-if="operationType !== 'view'">-->
+<!-- <div class="el-upload__tip">-->
+<!-- 鏂囦欢鏍煎紡鏀寔-->
+<!-- doc锛宒ocx锛寈ls锛寈lsx锛宲pt锛宲ptx锛宲df锛宼xt锛寈ml锛宩pg锛宩peg锛宲ng锛実if锛宐mp锛宺ar锛寊ip锛�7z-->
+<!-- </div>-->
+<!-- </template>-->
+<!-- </el-upload>-->
+<!-- </el-form-item>-->
+<!-- </el-col>-->
+<!-- </el-row>-->
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import useUserStore from "@/store/modules/user.js";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+import {afterSalesServiceAdd, afterSalesServiceUpdate} from "@/api/customerService/index.js";
+import {getToken} from "@/utils/auth.js";
+import {addMeasuringInstrumentLedger, updateMeasuringInstrumentLedger} from "@/api/equipmentManagement/measurementEquipment.js";
+import { getCurrentDate } from "@/utils/index.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const userStore = useUserStore();
+
+const data = reactive({
+ form: {
+ code: "",
+ name: "",
+ mostDate:"",
+ model: "",
+ validDate: "",
+ nextDate: "",
+ userId: "",
+ recordDate: "",
+ unit:"",
+ tempFileIds: []
+ },
+ rules: {
+ code: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
+ name: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ model: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
+ validDate: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
+ nextDate: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
+ userId: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
+ recordDate: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
+ mostDate: [{required: true, message: "璇烽�夋嫨", trigger: "change"}],
+ valid: [
+ {required: true, message: "璇疯緭鍏�", trigger: "blur"},
+ {
+ validator: (rule, value, callback) => {
+ if (value === '' || value === null || value === undefined) {
+ callback();
+ return;
+ }
+ const numValue = Number(value);
+ if (isNaN(numValue)) {
+ callback(new Error('璇疯緭鍏ユ湁鏁堢殑鏁板瓧'));
+ return;
+ }
+ if (numValue <= 0) {
+ callback(new Error('鍙兘杈撳叆姝f暟'));
+ return;
+ }
+ if (!Number.isInteger(numValue)) {
+ callback(new Error('璇疯緭鍏ユ暣鏁�'));
+ return;
+ }
+ callback();
+ },
+ trigger: 'blur'
+ }
+ ],
+ unit: [{required: true, message: "璇疯緭鍏�", trigger: "blur"}],
+ }
+})
+const { form, rules } = toRefs(data);
+const userList = ref([])
+const fileList = ref([]);
+const upload = reactive({
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+});
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ fileList.value = []
+ form.value.userId = userStore.id;
+ form.value.recordDate = getCurrentDate();
+ userListNoPageByTenantId().then((res) => {
+ userList.value = res.data;
+ });
+ if (type === "edit") {
+ form.value = {...row}
+ }
+}
+
+// 涓婁紶鍓嶆牎妫�
+function handleBeforeUpload(file) {
+ proxy.$modal.loading("姝e湪涓婁紶鏂囦欢锛岃绋嶅��...");
+ return true;
+}
+// 涓婁紶澶辫触
+function handleUploadError(err) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶辫触");
+ proxy.$modal.closeLoading();
+}
+// 涓婁紶鎴愬姛鍥炶皟
+function handleUploadSuccess(res, file, uploadFiles) {
+ proxy.$modal.closeLoading();
+ if (res.code === 200) {
+ file.tempId = res.data.tempId;
+ form.value.tempFileIds.push(res.data.tempId)
+ proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ proxy.$refs.fileUpload.handleRemove(file);
+ }
+}
+// 绉婚櫎鏂囦欢
+function handleRemove(file) {
+ if (operationType.value === "edit") {
+ let ids = [];
+ ids.push(file.id);
+ delLedgerFile(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ });
+ }
+}
+
+// 澶勭悊鏈夋晥鏃ユ湡杈撳叆锛屽彧鍏佽姝f暣鏁�
+const handleValidInput = (value) => {
+ if (value === '' || value === null || value === undefined) {
+ form.value.valid = '';
+ return;
+ }
+ // 杞崲涓哄瓧绗︿覆骞剁Щ闄ゆ墍鏈夐潪鏁板瓧瀛楃锛堝寘鎷礋鍙枫�佸皬鏁扮偣绛夛級
+ const numStr = String(value).replace(/[^0-9]/g, '');
+ if (numStr === '') {
+ form.value.valid = '';
+ return;
+ }
+ const numValue = parseInt(numStr, 10);
+ // 纭繚鏄鏁存暟锛堝ぇ浜�0锛�
+ if (numValue > 0 && !isNaN(numValue)) {
+ form.value.valid = numValue;
+ } else {
+ form.value.valid = '';
+ }
+}
+
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ addMeasuringInstrumentLedger(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ form.value.tempFileIds = []
+ closeDia()
+ })
+ } else {
+ updateMeasuringInstrumentLedger(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ form.value.tempFileIds = []
+ closeDia()
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/measurementEquipment/components/rowClickData.vue b/src/views/equipmentManagement/measurementEquipment/components/rowClickData.vue
new file mode 100644
index 0000000..6604587
--- /dev/null
+++ b/src/views/equipmentManagement/measurementEquipment/components/rowClickData.vue
@@ -0,0 +1,128 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="妫�瀹氭牎鍑嗚褰�"
+ width="50%"
+ @close="closeDia"
+ >
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ @selection-change="handleSelectionChange"
+ height="500"
+ :isPagination="false"
+ >
+ </PIMTable>
+ <pagination
+ style="margin: 10px 0"
+ v-show="total > 0"
+ @pagination="paginationSearch"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import filePreview from '@/components/filePreview/index.vue'
+import {ledgerRecordListPage} from "@/api/equipmentManagement/calibration.js";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const filePreviewRef = ref()
+const tableColumn = ref([
+ {
+ label: "妫�瀹氭棩鏈�",
+ prop: "recordDate",
+ width: 130,
+ },
+ {
+ label: "璁¢噺鍣ㄥ叿缂栧彿",
+ prop: "code",
+ width: 150,
+ },
+ {
+ label: "璁¢噺鍣ㄥ叿鍚嶇О",
+ prop: "name",
+ width: 200,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ width:200
+ },
+ {
+ label: "鏈夋晥鏈�",
+ prop: "valid",
+ width: 100,
+ },
+ {
+ label: "褰曞叆浜�",
+ prop: "userName",
+ },
+ {
+ label: "褰曞叆鏃ユ湡",
+ prop: "entryDate",
+ width: 130,
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+// 鎵撳紑寮规
+const openDialog = (row,type) => {
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ let query = {
+ measuringInstrumentLedgerId:currentId.value,
+ current : page.current,
+ size : page.size
+ }
+ ledgerRecordListPage(query).then(res => {
+ tableData.value = res?.data?.records || [];
+ total.value = res?.data?.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
diff --git a/src/views/equipmentManagement/measurementEquipment/filesDia.vue b/src/views/equipmentManagement/measurementEquipment/filesDia.vue
new file mode 100644
index 0000000..045ebc0
--- /dev/null
+++ b/src/views/equipmentManagement/measurementEquipment/filesDia.vue
@@ -0,0 +1,176 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible" title="涓婁紶闄勪欢" width="50%" @close="closeDia">
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload v-model:file-list="fileList" class="upload-demo" :action="uploadUrl"
+ :on-success="handleUploadSuccess" :on-error="handleUploadError" name="files" :show-file-list="false"
+ :headers="headers" style="display: inline;margin-right: 10px">
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :tableLoading="tableLoading"
+ :isSelection="true" @selection-change="handleSelectionChange" @pagination="paginationSearch" height="500">
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { ElMessageBox } from "element-plus";
+import { getToken } from "@/utils/auth.js";
+import filePreview from '@/components/filePreview/index.vue'
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import {
+ addStorageAttachment,
+ delStorageAttachment,
+ getStorageAttachmentList
+} from "@/api/equipmentManagement/measurementEquipment.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const filePreviewRef = ref()
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: (row) => {
+ lookFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const accountType = ref('')
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 涓婁紶鐨勬湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row, type) => {
+ accountType.value = type;
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ // 鍓嶇鍒嗛〉鏆備笉澶勭悊锛岀洿鎺ヨ皟鐢ㄨ幏鍙栧叏閲忓垪琛�
+ getList();
+};
+const getList = () => {
+ getStorageAttachmentList({ recordId: currentId.value, recordType: accountType.value }).then(res => {
+ tableData.value = res.data;
+ page.total = res.data ? res.data.length : 0;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200 && res.data && res.data.length > 0) {
+ const newFiles = res.data.map(item => ({
+ ...item,
+ name: item.originalFilename || item.name
+ }));
+ const mergedFiles = [...(tableData.value || []), ...newFiles];
+ const storageAttachmentDTO = {
+ recordType: accountType.value,
+ recordId: currentId.value,
+ application: "file",
+ storageBlobDTOs: mergedFiles
+ };
+ addStorageAttachment(storageAttachmentDTO).then(r => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.storageAttachmentId);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ delStorageAttachment(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 棰勮闄勪欢
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped></style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/measurementEquipment/index.vue b/src/views/equipmentManagement/measurementEquipment/index.vue
new file mode 100644
index 0000000..007eef6
--- /dev/null
+++ b/src/views/equipmentManagement/measurementEquipment/index.vue
@@ -0,0 +1,354 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">褰曞叆鏃ユ湡锛�</span>
+ <el-date-picker v-model="searchForm.recordDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 160px"
+ @change="handleQuery" />
+ <span class="search_title ml10">璁¢噺鍣ㄥ叿缂栧彿锛�</span>
+ <el-input v-model="searchForm.code"
+ placeholder="璇疯緭鍏ョ紪鍙�"
+ clearable
+ style="width: 240px"
+ @change="handleQuery" />
+ <span class="search_title ml10">鐘舵�侊細</span>
+ <el-select v-model="searchForm.status"
+ placeholder="璇烽�夋嫨鐘舵��"
+ @change="handleQuery"
+ style="width: 160px"
+ clearable>
+ <el-option label="鏈夋晥"
+ :value="1"></el-option>
+ <el-option label="閫炬湡"
+ :value="2"></el-option>
+ </el-select>
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">鎼滅储</el-button>
+ <el-button @click="handleReset"
+ style="margin-left: 10px">閲嶇疆</el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板璁¢噺鍣ㄥ叿</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :dbRowClick="dbRowClick"
+ :rowClassName="rowClassName"></PIMTable>
+ </div>
+ <form-dia ref="formDia"
+ @close="handleQuery"></form-dia>
+ <calibration-dia ref="calibrationDia"
+ @close="handleQuery"></calibration-dia>
+ <files-dia ref="filesDia"></files-dia>
+ <rowClickDataForm ref="rowClickData"></rowClickDataForm>
+ </div>
+</template>
+
+<script setup>
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+ } from "vue";
+ import FormDia from "@/views/equipmentManagement/measurementEquipment/components/formDia.vue";
+ import { ElMessageBox } from "element-plus";
+ import useUserStore from "@/store/modules/user.js";
+ import CalibrationDia from "@/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue";
+ import {
+ measuringInstrumentDelete,
+ measuringInstrumentListPage,
+ } from "@/api/equipmentManagement/measurementEquipment.js";
+ import FilesDia from "./filesDia.vue";
+ import rowClickDataForm from "./components/rowClickData.vue";
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
+
+ const data = reactive({
+ searchForm: {
+ recordDate: "",
+ code: "",
+ status: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ const tableColumn = ref([
+ {
+ label: "鍑哄巶缂栧彿",
+ prop: "code",
+ minWidth: 150,
+ align: "center",
+ },
+ {
+ label: "璁¢噺鍣ㄥ叿鍚嶇О",
+ prop: "name",
+ width: "160px",
+ align: "center",
+ },
+ {
+ label: "妫�瀹氬崟浣�",
+ prop: "unit",
+ width: 200,
+ align: "center",
+ },
+ {
+ label: "璇佷功缂栧彿",
+ prop: "model",
+ width: 200,
+ align: "center",
+ },
+ {
+ label: "鏈�鏂伴壌瀹氭棩鏈�",
+ prop: "mostDate",
+ width: 130,
+ align: "center",
+ },
+ {
+ label: "褰曞叆浜�",
+ prop: "userName",
+ width: 130,
+ align: "center",
+ },
+ {
+ label: "褰曞叆鏃ユ湡",
+ prop: "recordDate",
+ align: "center",
+ minWidth: 130,
+ },
+ {
+ label: "鏈夋晥鏃ユ湡",
+ prop: "valid",
+ width: 130,
+ align: "center",
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ width: 130,
+ align: "center",
+ formatData: params => {
+ if (params === 1) {
+ return "鏈夋晥";
+ } else if (params === 2) {
+ return "閫炬湡";
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ width: "130",
+ fixed: "right",
+ operation: [
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ openFilesFormDia(row);
+ },
+ },
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openCalibrationDia("verifying", row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const rowClickData = ref([]);
+ const filesDia = ref();
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const selectedRows = ref([]);
+
+ // 鎵撳紑闄勪欢寮规
+ const openFilesFormDia = row => {
+ filesDia.value?.openDialog(row, "measuring_instrument_ledger");
+ };
+
+ const dbRowClick = row => {
+ rowClickData.value?.openDialog(row);
+ };
+
+ // 琛屾牱寮忥細蹇埌鏈燂紙7澶╁唴锛夋垨閫炬湡鏍囩孩
+ const rowClassName = ({ row }) => {
+ console.log("rowClassName called:", row);
+ // valid 鏄湁鏁堝ぉ鏁帮紝mostDate 鏄渶鏂版瀹氭棩鏈�
+ if (row.valid && row.mostDate) {
+ const mostDate = new Date(row.mostDate);
+ // 璁$畻鍒版湡鏃ユ湡 = 妫�瀹氭棩鏈� + 鏈夋晥澶╂暟
+ const validDays = parseInt(row.valid) || 0;
+ const expireDate = new Date(mostDate);
+ expireDate.setDate(expireDate.getDate() + validDays);
+
+ const now = new Date();
+ const diffDays = Math.ceil((expireDate - now) / (1000 * 60 * 60 * 24));
+ console.log(
+ "row:",
+ row.code,
+ "validDays:",
+ validDays,
+ "expireDate:",
+ expireDate,
+ "diffDays:",
+ diffDays
+ );
+ // 7澶╁唴鍒版湡鎴栧凡閫炬湡閮芥爣绾�
+ if (diffDays <= 7) {
+ console.log("return warning-row");
+ return "warning-row";
+ }
+ } else {
+ console.log("row missing valid or mostDate:", row.valid, row.mostDate);
+ }
+ return "";
+ };
+
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+ const formDia = ref();
+ const calibrationDia = ref();
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ // 閲嶇疆鎼滅储鏉′欢
+ const handleReset = () => {
+ searchForm.value.recordDate = "";
+ searchForm.value.code = "";
+ searchForm.value.status = "";
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ measuringInstrumentListPage({ ...searchForm.value, ...page })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎵撳紑妫�瀹氭牎鍑嗗脊妗�
+ const openCalibrationDia = (type, row) => {
+ nextTick(() => {
+ calibrationDia.value?.openDialog(type, row);
+ });
+ };
+
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ measuringInstrumentDelete(ids)
+ .then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(
+ "/measuringInstrumentLedger/export",
+ {},
+ "璁¢噺鍣ㄥ叿鍙拌处.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped>
+ :deep(.el-table .warning-row) {
+ background-color: #fef0f0 !important;
+ }
+ :deep(.el-table .warning-row:hover > td) {
+ background-color: #f9d5d5 !important;
+ }
+ :deep(.el-table .el-table__body tr.warning-row td) {
+ background-color: #fef0f0 !important;
+ }
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/operationManagement/index.vue b/src/views/equipmentManagement/operationManagement/index.vue
new file mode 100644
index 0000000..99c3bd1
--- /dev/null
+++ b/src/views/equipmentManagement/operationManagement/index.vue
@@ -0,0 +1,484 @@
+<template>
+ <div class="app-container">
+
+ <!-- 绛涢�夋潯浠� -->
+ <div class="filter-section">
+ <el-select v-model="deviceFilter" placeholder="璁惧鐘舵�佺瓫閫�" clearable style="width: 200px; margin-right: 10px;">
+ <el-option label="鍏ㄩ儴" value="all" />
+ <el-option label="杩愯涓�" value="start" />
+ <el-option label="鍋滄杩愯" value="stop" />
+ </el-select>
+ </div>
+
+ <!-- 璁惧鍚仠璁板綍琛ㄦ牸 -->
+ <el-card class="table-card">
+ <template #header>
+ <span>璁惧杩愯璁板綍</span>
+ </template>
+ <el-table
+ :data="filteredDeviceRecords"
+ style="width: 100%"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ :row-class-name="getRowClassName"
+ v-loading="loading"
+ >
+ <el-table-column
+ align="center"
+ label="搴忓彿"
+ type="index"
+ width="60"
+ />
+ <el-table-column
+ label="璁惧鍚嶇О"
+ prop="deviceName"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="瑙勬牸鍨嬪彿"
+ prop="deviceModel"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="璁惧鐘舵��"
+ prop="status"
+ width="150"
+ align="center"
+ >
+ <template #default="scope">
+ <!-- 瓒呮椂鏈惎鍔ㄦ椂鏄剧ず璀﹀憡 -->
+ <el-tag
+ v-if="isOverdue(scope.row)"
+ type="warning"
+ size="small"
+ effect="dark"
+ >
+ <el-icon><Warning /></el-icon>
+ 瓒呮椂鏈惎鍔�
+ </el-tag>
+ <!-- 姝e父鐘舵�佹椂鏄剧ず璁惧鐘舵�� -->
+ <el-tag
+ v-else
+ :type="getDeviceStatusType(scope.row.status)"
+ size="small"
+ >
+ <el-icon v-if="scope.row.status === '杩愯涓�'"><VideoPlay /></el-icon>
+ <el-icon v-else><VideoPause /></el-icon>
+ {{ scope.row.status || '鏈煡' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="璁″垝杩愯鏃堕棿"
+ prop="planRuntimeTime"
+ width="150"
+ align="center"
+ >
+ <template #default="scope">
+ {{ scope.row.planRuntimeTime || '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="寮�濮嬭繍琛屾椂闂�"
+ prop="startRuntimeTime"
+ width="180"
+ align="center"
+ >
+ <template #default="scope">
+ {{ scope.row.startRuntimeTime || '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="缁撴潫杩愯鏃堕棿"
+ prop="endRuntimeTime"
+ width="180"
+ align="center"
+ >
+ <template #default="scope">
+ {{ scope.row.endRuntimeTime || '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="杩愯鏃堕暱"
+ prop="runtimeDuration"
+ width="120"
+ align="center"
+ >
+ <template #default="scope">
+ {{ getRuntimeDurationDisplay(scope.row) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鎿嶄綔"
+ width="120"
+ align="center"
+ >
+ <template #default="scope">
+ <!-- 瓒呮椂鏈惎鍔ㄦ椂鏄剧ず鍚姩鎸夐挳 -->
+ <el-button
+ v-if="isOverdue(scope.row)"
+ type="warning"
+ size="small"
+ @click="changeDeviceStatus(scope.row, '鍚姩杩愯')"
+ >
+ <el-icon><VideoPlay /></el-icon>
+ 绔嬪嵆鍚姩
+ </el-button>
+ <!-- 姝e父鐘舵�佹椂鏄剧ず瀵瑰簲鐨勬搷浣滄寜閽� -->
+ <template v-else>
+ <el-button
+ v-if="scope.row.status === '杩愯涓�'"
+ type="danger"
+ size="small"
+ @click="changeDeviceStatus(scope.row, '鍋滄杩愯')"
+ >
+ <el-icon><VideoPause /></el-icon>
+ 鍋滄杩愯
+ </el-button>
+ <el-button
+ v-else
+ type="success"
+ size="small"
+ @click="changeDeviceStatus(scope.row, '鍚姩杩愯')"
+ >
+ <el-icon><VideoPlay /></el-icon>
+ 鍚姩杩愯
+ </el-button>
+ </template>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, computed } from 'vue'
+import dayjs from 'dayjs'
+import { ElMessage } from 'element-plus'
+import {
+ VideoPlay,
+ VideoPause,
+ Warning
+} from '@element-plus/icons-vue'
+import {editLedger, getLedgerPage} from "@/api/equipmentManagement/ledger.js";
+
+// 鍝嶅簲寮忔暟鎹�
+const deviceFilter = ref('all')
+const loading = ref(false)
+const total = ref(0)
+const queryParams = ref({
+ current: -1,
+ size: -1
+})
+
+// 绉婚櫎姒傝鏁版嵁锛屽洜涓虹幇鍦ㄤ娇鐢ㄨ〃鏍煎睍绀�
+
+// 璁惧鍚仠璁板綍鏁版嵁
+const deviceRecords = ref([])
+const allDeviceRecords = ref([]) // 瀛樺偍鎵�鏈夊師濮嬫暟鎹�
+
+// 鏍规嵁绛涢�夋潯浠惰繃婊ゆ暟鎹�
+const filteredDeviceRecords = computed(() => {
+ let filtered = allDeviceRecords.value
+
+ // 鏍规嵁璁惧鐘舵�佺瓫閫�
+ if (deviceFilter.value !== 'all') {
+ if (deviceFilter.value === 'start') {
+ filtered = filtered.filter(device => device.status === '杩愯涓�')
+ } else if (deviceFilter.value === 'stop') {
+ filtered = filtered.filter(device => device.status === '鍋滄杩愯')
+ }
+ }
+
+ return filtered
+})
+
+// 杩愯涓棤缁撴潫鏃堕棿鏃讹紝杩愯鏃堕暱闇�闅忓綋鍓嶆椂闂村彉鍖栵紝鐢� tick 瑙﹀彂妯℃澘閲嶇畻
+const runtimeDisplayTick = ref(0)
+
+/** 鍙栧悗绔彲鑳戒娇鐢ㄧ殑寮�濮�/缁撴潫鏃堕棿瀛楁 */
+const pickStartTime = (row) => row?.startRuntimeTime ?? row?.startTime ?? row?.start_time
+const pickEndTime = (row) => row?.endRuntimeTime ?? row?.endTime ?? row?.end_time
+
+/**
+ * 瑙f瀽鎺ュ彛/鍓嶇鍐欏叆鐨勫悇绫绘椂闂达細鏃堕棿鎴炽�両SO 瀛楃涓层�亂yyy-MM-dd HH:mm:ss銆丣ackson 鏁扮粍 [y,M,d,h,m,s]銆佸惈涓枃鐨� toLocaleString 绛�
+ */
+const parseDeviceTime = (input) => {
+ if (input === null || input === undefined || input === '') return null
+ if (typeof input === 'number' && !Number.isNaN(input)) {
+ const d = dayjs(input)
+ return d.isValid() ? d.toDate() : null
+ }
+ if (Array.isArray(input)) {
+ const [y, mo, day, h = 0, mi = 0, se = 0] = input
+ if (y == null || y === '') return null
+ const d = dayjs()
+ .year(Number(y))
+ .month(Number(mo || 1) - 1)
+ .date(Number(day || 1))
+ .hour(Number(h) || 0)
+ .minute(Number(mi) || 0)
+ .second(Number(se) || 0)
+ return d.isValid() ? d.toDate() : null
+ }
+ const s = String(input).trim()
+ if (!s || s === '-') return null
+ let d = dayjs(s)
+ if (d.isValid()) return d.toDate()
+ d = dayjs(s.replace(/-/g, '/'))
+ if (d.isValid()) return d.toDate()
+ d = dayjs(s.replace(/\//g, '-'))
+ if (d.isValid()) return d.toDate()
+ return null
+}
+
+const formatDurationMs = (durationMs) => {
+ if (durationMs == null || Number.isNaN(durationMs) || durationMs < 0) return '-'
+ const hours = Math.floor(durationMs / (1000 * 60 * 60))
+ const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60))
+ if (hours === 0 && minutes === 0) return '涓嶈冻1鍒嗛挓'
+ return `${hours}灏忔椂${minutes}鍒嗛挓`
+}
+
+const hasMeaningfulEnd = (endRaw) =>
+ endRaw !== null &&
+ endRaw !== undefined &&
+ String(endRaw).trim() !== '' &&
+ String(endRaw).trim() !== '-'
+
+const formatStoredDuration = (row) => {
+ const rd = row?.runtimeDuration
+ if (rd === null || rd === undefined) return ''
+ const t = String(rd).trim()
+ return t === '' || t === '-' ? '' : String(rd)
+}
+
+/** 杩愯涓細濮嬬粓鐢ㄣ�屽綋鍓嶆椂闂� - 寮�濮嬫椂闂淬�嶏紱宸插仠姝細浼樺厛鎺ュ彛 runtimeDuration锛屽惁鍒欑敤缁撴潫-寮�濮嬶紱鏃犵粨鏉熷彲鐪嬪凡瀛樻椂闀挎垨鍔ㄦ�佹帹绠� */
+const getRuntimeDurationDisplay = (row) => {
+ void runtimeDisplayTick.value
+ const start = parseDeviceTime(pickStartTime(row))
+ if (!start) {
+ return formatStoredDuration(row) || '-'
+ }
+
+ const statusStr = String(row?.status ?? '').trim()
+ const isRunning = statusStr === '杩愯涓�' || statusStr === '1'
+ const endRaw = pickEndTime(row)
+ const hasEnd = hasMeaningfulEnd(endRaw)
+
+ // 鏃犵粨鏉熸椂闂达細杩愯涓竴瀹氬姩鎬佺畻锛涘凡鍋滄鍒欎紭鍏堝睍绀哄悗绔凡瀛樻椂闀匡紝娌℃湁鍐嶆寜褰撳墠鏃堕棿鎺ㄧ畻
+ if (!hasEnd) {
+ if (isRunning) return formatDurationMs(Date.now() - start.getTime())
+ const stored = formatStoredDuration(row)
+ if (stored) return stored
+ return formatDurationMs(Date.now() - start.getTime())
+ }
+
+ if (isRunning) {
+ return formatDurationMs(Date.now() - start.getTime())
+ }
+
+ const end = parseDeviceTime(endRaw)
+ const stored = formatStoredDuration(row)
+ if (stored) return stored
+ if (end) return formatDurationMs(end.getTime() - start.getTime())
+ return '-'
+}
+
+// 妫�鏌ヨ澶囨槸鍚﹁秴鏃舵湭鍚姩
+const isOverdue = (device) => {
+ if (!device.planRuntimeTime || device.status === '杩愯涓�' || device.startRuntimeTime) {
+ return false
+ }
+
+ const planTime = new Date(device.planRuntimeTime)
+ const currentTime = new Date()
+
+ return currentTime > planTime
+}
+
+// 鏂规硶
+const getList = async () => {
+ loading.value = true
+ try {
+ const response = await getLedgerPage(queryParams.value)
+ if (response.code === 200) {
+ allDeviceRecords.value = response.data.records || []
+ total.value = response.data.total || 0
+ }
+ } catch (error) {
+ console.error('鑾峰彇璁惧鍒楄〃澶辫触:', error)
+ ElMessage.error('鑾峰彇璁惧鍒楄〃澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+const changeDeviceStatus = async (device, status) => {
+ try {
+ const currentTime = new Date().toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false
+ }).replace(/\//g, '-')
+
+ // 鏇存柊璁惧鐘舵�佸拰鐩稿叧鏃堕棿瀛楁
+ if (status === '鍚姩杩愯') {
+ device.status = '杩愯涓�'
+ device.startRuntimeTime = currentTime
+ device.endRuntimeTime = null // 娓呯┖缁撴潫鏃堕棿
+ device.runtimeDuration = null // 娓呯┖杩愯鏃堕暱
+ } else {
+ device.status = '鍋滄杩愯'
+ device.endRuntimeTime = currentTime
+ // 璁$畻杩愯鏃堕暱
+ if (device.startRuntimeTime) {
+ const startTime = parseDeviceTime(device.startRuntimeTime)
+ const endTime = parseDeviceTime(currentTime)
+ if (startTime && endTime) {
+ device.runtimeDuration = formatDurationMs(endTime.getTime() - startTime.getTime())
+ }
+ }
+ }
+ const params = {
+ id: device.id,
+ status: device.status,
+ planRuntimeTime: device.planRuntimeTime,
+ startRuntimeTime: device.startRuntimeTime,
+ endRuntimeTime: device.endRuntimeTime,
+ runtimeDuration: device.runtimeDuration,
+ }
+ // 璋冪敤API鏇存柊璁惧鐘舵��
+ const response = await editLedger(params)
+ if (response.code === 200) {
+ ElMessage.success(`${device.deviceName} ${status}鎴愬姛`)
+ // 鍒锋柊鍒楄〃
+ await getList()
+ } else {
+ ElMessage.error(response.msg || '鎿嶄綔澶辫触')
+ }
+ } catch (error) {
+ console.error('鏇存柊璁惧鐘舵�佸け璐�:', error)
+ ElMessage.error('鎿嶄綔澶辫触')
+ }
+}
+
+const getDeviceStatusType = (status) => {
+ if (status === '杩愯涓�') {
+ return 'success'
+ } else if (status === '鍋滄杩愯') {
+ return 'danger'
+ } else {
+ return 'info'
+ }
+}
+
+// 鑾峰彇琛ㄦ牸琛岀殑绫诲悕
+const getRowClassName = ({ row }) => {
+ if (isOverdue(row)) {
+ return 'overdue-row'
+ }
+ return ''
+}
+
+
+
+const POLL_MS = 60 * 1000
+const RUNTIME_TICK_MS = 30 * 1000
+let listPollTimer = null
+let runtimeTickTimer = null
+
+// 缁勪欢鎸傝浇鏃舵媺鍙栨暟鎹紝骞舵瘡鍒嗛挓鍒锋柊涓�娆″垪琛紱杩愯涓椂闀挎瘡 30 绉掑埛鏂版樉绀�
+onMounted(() => {
+ getList()
+ listPollTimer = setInterval(() => {
+ getList()
+ }, POLL_MS)
+ runtimeTickTimer = setInterval(() => {
+ runtimeDisplayTick.value++
+ }, RUNTIME_TICK_MS)
+})
+
+onUnmounted(() => {
+ if (listPollTimer != null) {
+ clearInterval(listPollTimer)
+ listPollTimer = null
+ }
+ if (runtimeTickTimer != null) {
+ clearInterval(runtimeTickTimer)
+ runtimeTickTimer = null
+ }
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+ background: #f5f7fa;
+ min-height: 100vh;
+}
+
+
+.filter-section {
+ margin-bottom: 20px;
+ padding: 15px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ display: flex;
+ justify-content: flex-start;
+}
+
+.table-card {
+ margin-bottom: 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+:deep(.el-card__header) {
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ font-weight: 500;
+ font-size: 16px;
+}
+
+:deep(.el-table .el-table__header-wrapper th) {
+ background-color: #F0F1F5 !important;
+ color: #333333;
+ font-weight: 600;
+}
+
+:deep(.el-table .el-table__body-wrapper td) {
+ padding: 12px 0;
+}
+
+:deep(.el-select) {
+ width: 100%;
+}
+
+:deep(.el-tag) {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+
+/* 瓒呮椂鏈惎鍔ㄨ鐨勬牱寮� */
+:deep(.overdue-row) {
+ background-color: #fef0f0 !important;
+ border-left: 4px solid #f56c6c;
+}
+
+:deep(.overdue-row:hover) {
+ background-color: #fde2e2 !important;
+}
+
+:deep(.overdue-row td) {
+ background-color: transparent !important;
+}
+</style>
diff --git a/src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue b/src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
new file mode 100644
index 0000000..6d61a9f
--- /dev/null
+++ b/src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
@@ -0,0 +1,144 @@
+<template>
+ <FormDialog
+ v-model="visible"
+ title="楠屾敹瀹℃壒"
+ width="500px"
+ @confirm="submitForm"
+ @cancel="handleCancel"
+ @close="handleCancel"
+ >
+ <el-form :model="form" :rules="rules" label-width="100px">
+ <el-form-item label="楠屾敹浜�" prop="acceptanceName">
+ <el-select
+ v-model="form.acceptanceName"
+ placeholder="璇烽�夋嫨楠屾敹浜�"
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.nickName"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="楠屾敹鏃堕棿" prop="acceptanceTime">
+ <el-date-picker
+ v-model="form.acceptanceTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨楠屾敹鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ />
+ </el-form-item>
+ <el-form-item label="楠屾敹澶囨敞" prop="acceptanceRemark">
+ <el-input
+ v-model="form.acceptanceRemark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ラ獙鏀跺娉�"
+ />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { ref, reactive } from "vue";
+import { ElMessage } from "element-plus";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { repairAcceptance } from "@/api/equipmentManagement/repair";
+import dayjs from "dayjs";
+
+defineOptions({
+ name: "楠屾敹瀹℃壒寮圭獥",
+});
+
+const emits = defineEmits(["ok"]);
+
+const visible = ref(false);
+const loading = ref(false);
+const repairId = ref(null);
+const userList = ref([]);
+
+const form = reactive({
+ acceptanceName: undefined,
+ acceptanceTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ acceptanceRemark: undefined,
+});
+
+const rules = {
+ acceptanceName: [
+ { required: true, message: "璇烽�夋嫨楠屾敹浜�", trigger: "change" },
+ ],
+ acceptanceTime: [
+ { required: true, message: "璇烽�夋嫨楠屾敹鏃堕棿", trigger: "change" },
+ ],
+ acceptanceRemark: [
+ { required: true, message: "璇疯緭鍏ラ獙鏀跺娉�", trigger: "blur" },
+ ],
+};
+
+// 鍔犺浇鐢ㄦ埛鍒楄〃
+const loadUserList = async () => {
+ const { data } = await userListNoPageByTenantId();
+ userList.value = data;
+};
+
+// 鎵撳紑寮圭獥
+const open = async (row) => {
+ repairId.value = row.id;
+ visible.value = true;
+ // 閲嶇疆琛ㄥ崟
+ form.acceptanceName = undefined;
+ form.acceptanceTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ form.acceptanceRemark = undefined;
+ await loadUserList();
+};
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = async () => {
+ if (!form.acceptanceName) {
+ ElMessage.warning("璇烽�夋嫨楠屾敹浜�");
+ return;
+ }
+ if (!form.acceptanceTime) {
+ ElMessage.warning("璇烽�夋嫨楠屾敹鏃堕棿");
+ return;
+ }
+ if (!form.acceptanceRemark) {
+ ElMessage.warning("璇疯緭鍏ラ獙鏀跺娉�");
+ return;
+ }
+
+ loading.value = true;
+ try {
+ const { code } = await repairAcceptance({
+ id: repairId.value,
+ acceptanceName: form.acceptanceName,
+ acceptanceTime: form.acceptanceTime,
+ acceptanceRemark: form.acceptanceRemark,
+ });
+ if (code === 200) {
+ ElMessage.success("楠屾敹閫氳繃");
+ visible.value = false;
+ emits("ok");
+ }
+ } finally {
+ loading.value = false;
+ }
+};
+
+const handleCancel = () => {
+ visible.value = false;
+};
+
+defineExpose({
+ open,
+});
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/equipmentManagement/repair/Modal/MaintainModal.vue b/src/views/equipmentManagement/repair/Modal/MaintainModal.vue
new file mode 100644
index 0000000..b0b09f0
--- /dev/null
+++ b/src/views/equipmentManagement/repair/Modal/MaintainModal.vue
@@ -0,0 +1,226 @@
+<template>
+ <FormDialog
+ v-model="visible"
+ :title="'璁惧缁翠慨'"
+ width="500px"
+ @confirm="sendForm"
+ @cancel="handleCancel"
+ @close="handleClose"
+ >
+ <el-form :model="form" label-width="80px">
+ <el-form-item label="缁翠慨浜�">
+ <el-input v-model="form.maintenanceName" placeholder="璇疯緭鍏ョ淮淇汉" />
+ </el-form-item>
+ <el-form-item label="缁翠慨缁撴灉">
+ <el-input v-model="form.maintenanceResult" placeholder="璇疯緭鍏ョ淮淇粨鏋�" />
+ </el-form-item>
+ <el-form-item label="缁翠慨鐘舵��">
+ <el-select v-model="form.status">
+ <el-option label="寰呮姤淇�" :value="0"></el-option>
+ <el-option label="瀹岀粨" :value="1"></el-option>
+ <el-option label="澶辫触" :value="2"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缁翠慨鏃ユ湡">
+ <el-date-picker
+ v-model="form.maintenanceTime"
+ placeholder="璇烽�夋嫨缁翠慨鏃ユ湡"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="datetime"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ <el-form-item label="璁惧澶囦欢">
+ <el-select v-model="form.sparePartsIds" :loading="loadingSparePartOptions" placeholder="璇烽�夋嫨璁惧澶囦欢" multiple filterable>
+ <el-option
+ v-for="item in sparePartOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item v-if="selectedSpareParts.length" label="棰嗙敤鏁伴噺">
+ <div style="width: 100%">
+ <div
+ v-for="item in selectedSpareParts"
+ :key="item.id"
+ style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;"
+ >
+ <div style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
+ {{ item.name }}
+ <span v-if="item.quantity !== null && item.quantity !== undefined" style="color: #909399;">
+ 锛堝簱瀛橈細{{ item.quantity }}锛�
+ </span>
+ </div>
+ <el-input-number
+ v-model="sparePartQtyMap[item.id]"
+ :min="1"
+ :max="item.quantity !== null && item.quantity !== undefined ? Number(item.quantity) : undefined"
+ :step="1"
+ controls-position="right"
+ style="width: 180px"
+ />
+ </div>
+ </div>
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, nextTick, ref } from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { addMaintain } from "@/api/equipmentManagement/repair";
+import useFormData from "@/hooks/useFormData";
+import useUserStore from "@/store/modules/user";
+import dayjs from "dayjs";
+import { ElMessage } from "element-plus";
+import { getSparePartsList } from "@/api/equipmentManagement/spareParts";
+
+defineOptions({
+ name: "缁翠慨妯℃�佹",
+});
+
+const emits = defineEmits(["ok"]);
+const { proxy } = getCurrentInstance();
+
+// 淇濆瓨鎶ヤ慨璁板綍鐨刬d
+const repairId = ref();
+const visible = ref(false);
+const loading = ref(false);
+
+const userStore = useUserStore();
+const { form, resetForm } = useFormData({
+ maintenanceName: undefined, // 缁翠慨鍚嶇О
+ maintenanceResult: undefined, // 缁翠慨缁撴灉
+ maintenanceTime: undefined, // 缁翠慨鏃ユ湡
+ status: 0,
+ sparePartsIds: [],
+});
+const sparePartOptions = ref([])
+const loadingSparePartOptions = ref(true)
+const sparePartQtyMap = ref({})
+
+const selectedSpareParts = computed(() => {
+ const ids = Array.isArray(form.sparePartsIds) ? form.sparePartsIds : [];
+ const set = new Set(ids.map((i) => String(i)));
+ return (sparePartOptions.value || []).filter((p) => set.has(String(p.id)));
+});
+
+const setForm = (data) => {
+ form.maintenanceName = data.maintenanceName ?? userStore.nickName;
+ form.maintenanceResult = data.maintenanceResult;
+ form.maintenanceTime =
+ data.maintenanceTime
+ ? dayjs(data.maintenanceTime).format("YYYY-MM-DD HH:mm:ss")
+ : dayjs().format("YYYY-MM-DD HH:mm:ss");
+ form.status = 1; // 榛樿鐘舵�佷负瀹岀粨
+ // multiple 閫夋嫨鍣ㄨ姹傛暟缁勶紱鍚庣甯歌繑鍥� "1,2,3"
+ if (Array.isArray(data?.sparePartsIds)) {
+ form.sparePartsIds = data.sparePartsIds.map((v) => Number(v)).filter((v) => Number.isFinite(v));
+ } else if (typeof data?.sparePartsIds === "string") {
+ form.sparePartsIds = data.sparePartsIds
+ .split(",")
+ .map((s) => Number(String(s).trim()))
+ .filter((v) => Number.isFinite(v));
+ } else if (typeof data?.sparePartsIds === "number") {
+ form.sparePartsIds = [data.sparePartsIds];
+ } else {
+ form.sparePartsIds = [];
+ }
+};
+
+const sendForm = async () => {
+ loading.value = true;
+ try {
+ // 棰嗙敤鏁伴噺鏍¢獙
+ if (Array.isArray(form.sparePartsIds) && form.sparePartsIds.length > 0) {
+ for (const partId of form.sparePartsIds) {
+ const qty = Number(sparePartQtyMap.value?.[partId]);
+ if (!Number.isFinite(qty) || qty <= 0) {
+ proxy?.$modal?.msgError?.("璇峰~鍐欏浠堕鐢ㄦ暟閲�");
+ return;
+ }
+ const part = sparePartOptions.value.find((p) => String(p.id) === String(partId));
+ const stock = part?.quantity;
+ if (stock !== null && stock !== undefined && Number.isFinite(Number(stock))) {
+ if (qty > Number(stock)) {
+ proxy?.$modal?.msgError?.(`澶囦欢銆�${part?.name || ""}銆嶉鐢ㄦ暟閲忎笉鑳借秴杩囧簱瀛橈紙${stock}锛塦);
+ return;
+ }
+ }
+ }
+ }
+ const data = {
+ id: repairId.value,
+ ...form,
+ sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
+ sparePartsQty: form.sparePartsIds
+ ? form.sparePartsIds.map((id) => sparePartQtyMap.value?.[id] ?? 1).join(",")
+ : "",
+ sparePartsUseList: form.sparePartsIds
+ ? form.sparePartsIds.map((id) => ({ id, quantity: sparePartQtyMap.value?.[id] ?? 1 }))
+ : [],
+ }
+ const { code } = await addMaintain(data);
+ if (code == 200) {
+ ElMessage.success("缁翠慨鎴愬姛");
+ emits("ok");
+ resetForm();
+ sparePartQtyMap.value = {};
+ visible.value = false;
+ }
+ } finally {
+ loading.value = false;
+ }
+};
+
+const fetchSparePartOptions = () => {
+ loadingSparePartOptions.value = true;
+ // 鍜屽浠剁鐞嗛〉涓�鑷达細/spareParts/listPage 鈫� res.data.records
+ getSparePartsList({ current: 1, size: 1000 })
+ .then((res) => {
+ if (res.code === 200) {
+ sparePartOptions.value = res?.data?.records || [];
+ } else {
+ sparePartOptions.value = [];
+ }
+ })
+ .catch(() => {
+ sparePartOptions.value = [];
+ })
+ .finally(() => {
+ loadingSparePartOptions.value = false;
+ });
+}
+
+const handleCancel = () => {
+ resetForm();
+ sparePartQtyMap.value = {};
+ visible.value = false;
+};
+
+const handleClose = () => {
+ resetForm();
+ sparePartQtyMap.value = {};
+ visible.value = false;
+};
+
+const open = async (id, row) => {
+ repairId.value = id; // 淇濆瓨鎶ヤ慨璁板綍鐨刬d
+ visible.value = true;
+ await nextTick();
+ setForm(row);
+ fetchSparePartOptions()
+};
+
+defineExpose({
+ open,
+});
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/equipmentManagement/repair/Modal/RepairModal.vue b/src/views/equipmentManagement/repair/Modal/RepairModal.vue
new file mode 100644
index 0000000..4a10071
--- /dev/null
+++ b/src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -0,0 +1,268 @@
+<template>
+ <FormDialog v-model="visible"
+ :title="computedTitle"
+ :operation-type="operationType"
+ width="800px"
+ @confirm="sendForm"
+ @cancel="handleCancel"
+ @close="handleClose">
+ <el-form :model="form"
+ label-width="100px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="璁惧鍚嶇О">
+ <el-select v-model="form.deviceLedgerId"
+ @change="setDeviceModel"
+ filterable
+ :disabled="operationType === 'view'">
+ <el-option v-for="(item, index) in deviceOptions"
+ :key="index"
+ :label="item.deviceName"
+ :value="item.id"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿">
+ <el-input v-model="form.deviceModel"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎶ヤ慨鏃ユ湡">
+ <el-date-picker v-model="form.repairTime"
+ placeholder="璇烽�夋嫨鎶ヤ慨鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ type="date"
+ clearable
+ style="width: 100%"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎶ヤ慨浜�">
+ <el-input v-model="form.repairName"
+ placeholder="璇疯緭鍏ユ姤淇汉"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎶ヤ慨鎶ヤ慨椤圭洰">
+ <el-input v-model="form.machineryCategory"
+ placeholder="璇疯緭鍏ユ姤淇姤淇」鐩�"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="缁翠慨浜�">
+ <el-input v-model="form.maintenanceName"
+ placeholder="璇疯緭鍏ョ淮淇汉濮撳悕"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="id">
+ <el-col :span="12">
+ <el-form-item label="鎶ヤ慨鐘舵��">
+ <el-select v-model="form.status"
+ disabled>
+ <el-option label="寰呯淮淇�"
+ :value="0"></el-option>
+ <el-option label="宸查獙鏀�"
+ :value="1"></el-option>
+ <el-option label="澶辫触"
+ :value="2"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 楠屾敹淇℃伅灞曠ず -->
+ <el-row v-if="id && (form.status === 1 || form.status === 3)">
+ <el-col :span="12">
+ <el-form-item label="楠屾敹浜�">
+ <el-input v-model="form.acceptanceName"
+ disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="楠屾敹鏃堕棿">
+ <el-input v-model="form.acceptanceTime"
+ disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="楠屾敹澶囨敞">
+ <el-input v-model="form.acceptanceRemark"
+ type="textarea"
+ :rows="2"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鏁呴殰鐜拌薄">
+ <el-input v-model="form.remark"
+ :rows="2"
+ type="textarea"
+ placeholder="璇疯緭鍏ユ晠闅滅幇璞�"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="operationType !== 'view'"
+ :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢"
+ prop="attachmentIds">
+ <FileUpload v-model:file-list="form.storageBlobDTOs"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+ import {
+ addRepair,
+ editRepair,
+ getRepairById,
+ } from "@/api/equipmentManagement/repair";
+ import { ElMessage } from "element-plus";
+ import dayjs from "dayjs";
+ import useFormData from "@/hooks/useFormData";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+ import useUserStore from "@/store/modules/user";
+
+ defineOptions({
+ name: "璁惧鎶ヤ慨寮圭獥",
+ });
+
+ const emits = defineEmits(["ok"]);
+
+ const id = ref();
+ const visible = ref(false);
+ const loading = ref(false);
+ const operationType = ref(""); // add, edit, view
+
+ const computedTitle = computed(() => {
+ if (operationType.value === "add") return "鏂板璁惧鎶ヤ慨";
+ if (operationType.value === "edit") return "缂栬緫璁惧鎶ヤ慨";
+ if (operationType.value === "view") return "璁惧鎶ヤ慨璇︽儏";
+ return "";
+ });
+
+ const userStore = useUserStore();
+ const deviceOptions = ref([]);
+ const fileList = ref([]);
+
+ const loadDeviceName = async () => {
+ const { data } = await getDeviceLedger();
+ deviceOptions.value = data;
+ };
+
+ const { form, resetForm } = useFormData({
+ deviceLedgerId: undefined, // 璁惧Id
+ deviceName: undefined, // 璁惧鍚嶇О
+ deviceModel: undefined, // 瑙勬牸鍨嬪彿
+ repairTime: dayjs().format("YYYY-MM-DD"), // 鎶ヤ慨鏃ユ湡锛岄粯璁ゅ綋澶�
+ repairName: userStore.nickName, // 鎶ヤ慨浜�
+ remark: undefined, // 鏁呴殰鐜拌薄
+ status: 0, // 鎶ヤ慨鐘舵��
+ machineryCategory: undefined,
+ storageBlobDTOs: [],
+ maintenanceName: undefined, // 缁翠慨浜�
+ });
+
+ const setDeviceModel = deviceId => {
+ const option = deviceOptions.value.find(item => item.id === deviceId);
+ form.deviceModel = option.deviceModel;
+ };
+
+ const setForm = data => {
+ form.deviceLedgerId = data.deviceLedgerId;
+ form.deviceName = data.deviceName;
+ form.deviceModel = data.deviceModel;
+ form.repairTime = data.repairTime;
+ form.repairName = data.repairName;
+ form.remark = data.remark;
+ form.status = data.status;
+ form.machineryCategory = data.machineryCategory;
+ form.storageBlobDTOs = data.storageBlobVOs || [];
+ form.maintenanceName = data.maintenanceName;
+ form.acceptanceName = data.acceptanceName;
+ form.acceptanceTime = data.acceptanceTime;
+ form.acceptanceRemark = data.acceptanceRemark;
+ };
+
+ const sendForm = async () => {
+ loading.value = true;
+ try {
+ const { code } = id.value
+ ? await editRepair({ id: unref(id), ...form })
+ : await addRepair(form);
+ if (code == 200) {
+ ElMessage.success(`${id.value ? "缂栬緫" : "鏂板"}鎶ヤ慨鎴愬姛`);
+ visible.value = false;
+ emits("ok");
+ }
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const handleCancel = () => {
+ resetForm();
+ visible.value = false;
+ };
+
+ const handleClose = () => {
+ resetForm();
+ visible.value = false;
+ };
+
+ const openAdd = async () => {
+ id.value = undefined;
+ operationType.value = "add";
+ visible.value = true;
+ fileList.value = [];
+ await nextTick();
+ await loadDeviceName();
+ };
+
+ const openEdit = async editId => {
+ const { data } = await getRepairById(editId);
+ id.value = editId;
+ operationType.value = "edit";
+ visible.value = true;
+ await nextTick();
+ await loadDeviceName();
+ setForm(data);
+ };
+
+ const openView = async viewId => {
+ const { data } = await getRepairById(viewId);
+ id.value = viewId;
+ operationType.value = "view";
+ visible.value = true;
+ await nextTick();
+ await loadDeviceName();
+ setForm(data);
+ };
+
+ defineExpose({
+ openAdd,
+ openEdit,
+ openView,
+ });
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/equipmentManagement/repair/index.vue b/src/views/equipmentManagement/repair/index.vue
new file mode 100644
index 0000000..f1573cb
--- /dev/null
+++ b/src/views/equipmentManagement/repair/index.vue
@@ -0,0 +1,365 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="璁惧鍚嶇О">
+ <el-input v-model="filters.deviceName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヨ澶囧悕绉�"
+ clearable
+ :prefix-icon="Search"
+ @change="getTableData" />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿">
+ <el-input v-model="filters.deviceModel"
+ style="width: 240px"
+ placeholder="璇烽�夋嫨瑙勬牸鍨嬪彿"
+ clearable
+ :prefix-icon="Search"
+ @change="getTableData" />
+ </el-form-item>
+ <el-form-item label="鏁呴殰鐜拌薄">
+ <el-input v-model="filters.remark"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ユ晠闅滅幇璞�"
+ clearable
+ :prefix-icon="Search"
+ @change="getTableData" />
+ </el-form-item>
+ <el-form-item label="缁翠慨浜�">
+ <el-input v-model="filters.maintenanceName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ョ淮淇汉"
+ clearable
+ :prefix-icon="Search"
+ @change="getTableData" />
+ </el-form-item>
+ <el-form-item label="鎶ヤ慨鏃ユ湡">
+ <el-date-picker v-model="filters.repairTimeStr"
+ type="date"
+ placeholder="璇烽�夋嫨鎶ヤ慨鏃ユ湡"
+ size="default"
+ @change="(date) => handleDateChange(date,2)" />
+ </el-form-item>
+ <el-form-item label="缁翠慨鏃ユ湡">
+ <el-date-picker v-model="filters.maintenanceTimeStr"
+ type="date"
+ placeholder="璇烽�夋嫨缁翠慨鏃ユ湡"
+ size="default"
+ @change="(date) => handleDateChange(date,1)" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <el-text class="mx-1"
+ size="large">璁惧鎶ヤ慨</el-text>
+ <div>
+ <el-button type="success"
+ icon="Van"
+ @click="addRepair">
+ 鏂板鎶ヤ慨
+ </el-button>
+ <el-button @click="handleOut">
+ 瀵煎嚭
+ </el-button>
+ <el-button type="danger"
+ icon="Delete"
+ :disabled="multipleList.length <= 0 || hasFinishedStatus"
+ @click="delRepairByIds(multipleList.map((item) => item.id))">
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage">
+ <template #statusRef="{ row }">
+ <el-tag v-if="row.status === 2"
+ type="danger">澶辫触</el-tag>
+ <el-tag v-if="row.status === 1"
+ type="success">瀹岀粨</el-tag>
+ <el-tag v-if="row.status === 3"
+ type="info">寰呴獙鏀�</el-tag>
+ <el-tag v-if="row.status === 0"
+ type="warning">寰呯淮淇�</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary"
+ link
+ @click="viewRepair(row.id)">
+ 璇︽儏
+ </el-button>
+ <el-button type="primary"
+ link
+ :disabled="row.status === 1 || row.status === 3"
+ @click="editRepair(row.id)">
+ 缂栬緫
+ </el-button>
+ <el-button type="success"
+ link
+ :disabled="row.status !== 0"
+ @click="addMaintain(row)">
+ 缁翠慨
+ </el-button>
+ <el-button type="warning"
+ link
+ :disabled="row.status !== 3"
+ @click="openAcceptance(row)">
+ 楠屾敹
+ </el-button>
+ <el-button type="danger"
+ link
+ :disabled="row.status === 1 || row.status === 3"
+ @click="delRepairByIds(row.id)">
+ 鍒犻櫎
+ </el-button>
+ <el-button type="primary"
+ link
+ @click="openFileDialog(row)">
+ 闄勪欢
+ </el-button>
+ </template>
+ </PIMTable>
+ </div>
+ <RepairModal ref="repairModalRef"
+ @ok="getTableData" />
+ <MaintainModal ref="maintainModalRef"
+ @ok="getTableData" />
+ <AcceptanceModal ref="acceptanceModalRef"
+ @ok="getTableData" />
+ <FileList v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ :record-type="'device_repair'"
+ :record-id="recordId" />
+ </div>
+</template>
+
+<script setup>
+ import {
+ onMounted,
+ getCurrentInstance,
+ computed,
+ ref,
+ defineAsyncComponent,
+ } from "vue";
+ import { usePaginationApi } from "@/hooks/usePaginationApi";
+ import { getRepairPage, delRepair } from "@/api/equipmentManagement/repair";
+ import RepairModal from "./Modal/RepairModal.vue";
+ import { ElMessageBox, ElMessage } from "element-plus";
+ import dayjs from "dayjs";
+ import MaintainModal from "./Modal/MaintainModal.vue";
+ import AcceptanceModal from "./Modal/AcceptanceModal.vue";
+ const FileList = defineAsyncComponent(() =>
+ import("@/components/Dialog/FileList.vue")
+ );
+
+ defineOptions({
+ name: "璁惧鎶ヤ慨",
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ // 妯℃�佹瀹炰緥
+ const repairModalRef = ref();
+ const maintainModalRef = ref();
+ const acceptanceModalRef = ref();
+
+ // 琛ㄦ牸澶氶�夋閫変腑椤�
+ const multipleList = ref([]);
+
+ // 琛ㄦ牸閽╁瓙
+ const {
+ filters,
+ columns,
+ dataList,
+ pagination,
+ getTableData,
+ resetFilters,
+ onCurrentChange,
+ } = usePaginationApi(
+ getRepairPage,
+ {
+ deviceName: undefined,
+ deviceModel: undefined,
+ remark: undefined,
+ maintenanceName: undefined,
+ repairTimeStr: undefined,
+ maintenanceTimeStr: undefined,
+ },
+ [
+ {
+ label: "璁惧鍚嶇О",
+ align: "center",
+ prop: "deviceName",
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ align: "center",
+ prop: "deviceModel",
+ },
+ {
+ label: "鎶ヤ慨鏃ユ湡",
+ align: "center",
+ prop: "repairTime",
+ formatData: cell => dayjs(cell).format("YYYY-MM-DD"),
+ },
+ {
+ label: "鎶ヤ慨浜�",
+ align: "center",
+ prop: "repairName",
+ },
+ {
+ label: "鐘舵��",
+ align: "center",
+ prop: "status",
+ dataType: "slot",
+ slot: "statusRef",
+ },
+ {
+ fixed: "right",
+ label: "鎿嶄綔",
+ dataType: "slot",
+ slot: "operation",
+ align: "center",
+ width: "320px",
+ },
+ ]
+ );
+
+ // type === 1 缁翠慨 2鎶ヤ慨闂�
+ const handleDateChange = (value, type) => {
+ filters.maintenanceTimeStr = null;
+ filters.c = null;
+ if (type === 1) {
+ if (value) {
+ filters.maintenanceTimeStr = dayjs(value).format("YYYY-MM-DD");
+ }
+ } else {
+ if (value) {
+ filters.repairTimeStr = dayjs(value).format("YYYY-MM-DD");
+ }
+ }
+ getTableData();
+ };
+
+ // 鎵撳紑闄勪欢寮圭獥
+ const recordId = ref(0);
+ const fileDialogVisible = ref(false);
+
+ const openFileDialog = async row => {
+ recordId.value = row.id;
+ fileDialogVisible.value = true;
+ };
+
+ // 澶氶�夊悗鍋氫粈涔�
+ const handleSelectionChange = selectionList => {
+ multipleList.value = selectionList;
+ };
+
+ // 妫�鏌ラ�変腑鐨勮褰曚腑鏄惁鏈夊畬缁撶姸鎬佺殑
+ const hasFinishedStatus = computed(() => {
+ return multipleList.value.some(item => item.status === 1);
+ });
+
+ // 鏂板鎶ヤ慨
+ const addRepair = () => {
+ repairModalRef.value.openAdd();
+ };
+
+ // 璇︽儏鏌ョ湅
+ const viewRepair = id => {
+ repairModalRef.value.openView(id);
+ };
+
+ // 缂栬緫鎶ヤ慨
+ const editRepair = id => {
+ repairModalRef.value.openEdit(id);
+ };
+
+ // 鏂板缁翠慨
+ const addMaintain = row => {
+ maintainModalRef.value.open(row.id, row);
+ };
+
+ // 鎵撳紑楠屾敹寮圭獥
+ const openAcceptance = row => {
+ acceptanceModalRef.value.open(row);
+ };
+
+ const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ onCurrentChange(page);
+ };
+
+ // 鍗曡鍒犻櫎
+ const delRepairByIds = async ids => {
+ // 妫�鏌ユ槸鍚︽湁瀹岀粨鐘舵�佺殑璁板綍
+ const idsArray = Array.isArray(ids) ? ids : [ids];
+ const hasFinished = idsArray.some(id => {
+ const record = dataList.value.find(item => item.id === id);
+ return record && record.status === 1;
+ });
+
+ if (hasFinished) {
+ ElMessage.warning("涓嶈兘鍒犻櫎鐘舵�佷负瀹岀粨鐨勮褰�");
+ return;
+ }
+
+ ElMessageBox.confirm("纭鍒犻櫎鎶ヤ慨鏁版嵁, 姝ゆ搷浣滀笉鍙��?", "璀﹀憡", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ const { code } = await delRepair(ids);
+ if (code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ }
+ });
+ };
+
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/device/repair/export", {}, "璁惧鎶ヤ慨.xlsx");
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑�");
+ });
+ };
+
+ onMounted(() => {
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .table_list {
+ margin-top: unset;
+ }
+
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+ }
+</style>
diff --git a/src/views/equipmentManagement/spareParts/index.vue b/src/views/equipmentManagement/spareParts/index.vue
new file mode 100644
index 0000000..8abe35d
--- /dev/null
+++ b/src/views/equipmentManagement/spareParts/index.vue
@@ -0,0 +1,570 @@
+<template>
+ <div class="spare-part-category">
+ <el-tabs v-model="activeTab" @tab-change="handleTabChange">
+ <el-tab-pane label="澶囦欢鍒楄〃" name="list">
+ <div class="search_form">
+ <el-form :inline="true" :model="queryParams" class="search-form">
+ <el-form-item label="澶囦欢鍚嶇О">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ュ浠跺悕绉�"
+ clearable
+ style="width: 240px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鏌ヨ</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div>
+ <el-button type="primary" @click="addCategory">鏂板</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="renderTableData"
+ :tableLoading="loading"
+ :page="pagination"
+ :isShowPagination="true"
+ @pagination="handleSizeChange"
+ >
+ <template #status="{ row }">
+ <el-tag type="success" size="small">{{ row.status }}</el-tag>
+ </template>
+ </PIMTable>
+ </div>
+
+ <el-dialog title="鍒嗙被绠$悊" v-model="dialogVisible" width="60%">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+ <el-form-item label="璁惧" prop="deviceLedgerIds">
+ <el-select
+ v-model="form.deviceLedgerIds"
+ placeholder="璇烽�夋嫨璁惧"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ multiple
+ style="width: 100%"
+ >
+ <el-option
+ v-for="(item, index) in deviceOptions"
+ :key="index"
+ :label="item.deviceName"
+ :value="item.id"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囦欢鍚嶇О" prop="name">
+ <el-input v-model="form.name"></el-input>
+ </el-form-item>
+ <el-form-item label="澶囦欢缂栧彿" prop="sparePartsNo">
+ <el-input v-model="form.sparePartsNo"></el-input>
+ </el-form-item>
+ <el-form-item label="鏁伴噺" prop="quantity">
+ <el-input type="number" v-model="form.quantity"></el-input>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option label="姝e父" value="姝e父"></el-option>
+ <el-option label="绂佺敤" value="绂佺敤"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎻忚堪" prop="description">
+ <el-input v-model="form.description"></el-input>
+ </el-form-item>
+ <el-form-item label="浠锋牸" prop="price">
+ <el-input-number
+ v-model="form.price"
+ placeholder="璇疯緭鍏ヤ环鏍�"
+ :min="0"
+ :step="0.01"
+ :precision="2"
+ style="width: 100%"
+ ></el-input-number>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="submitForm" :loading="formLoading">纭畾</el-button>
+ <el-button @click="dialogVisible = false" :disabled="formLoading">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </el-tab-pane>
+
+ <el-tab-pane label="澶囦欢棰嗙敤璁板綍" name="usage">
+ <div class="search_form">
+ <el-form :inline="true" :model="usageQuery" class="search-form">
+ <el-form-item label="澶囦欢鍚嶇О">
+ <el-input v-model="usageQuery.sparePartsName" placeholder="璇疯緭鍏ュ浠跺悕绉�" clearable style="width: 240px" />
+ </el-form-item>
+ <el-form-item label="鏉ユ簮">
+ <el-select v-model="usageQuery.sourceType" placeholder="璇烽�夋嫨" clearable style="width: 200px">
+ <el-option label="缁翠慨" :value="0" />
+ <el-option label="淇濆吇" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleUsageQuery">鏌ヨ</el-button>
+ <el-button @click="resetUsageQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="rowKey"
+ :column="usageColumns"
+ :tableData="usageTableData"
+ :tableLoading="usageLoading"
+ :page="usagePagination"
+ :isShowPagination="true"
+ @pagination="handleUsagePageChange"
+ />
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, reactive } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { getSparePartsList, addSparePart, editSparePart, delSparePart } from "@/api/equipmentManagement/spareParts";
+import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import { getSparePartsUsagePage } from "@/api/equipmentManagement/sparePartsUsage";
+
+// 鍔犺浇鐘舵��
+const loading = ref(false);
+const formLoading = ref(false);
+const activeTab = ref("list");
+// 瀵硅瘽妗嗘樉绀虹姸鎬�
+const dialogVisible = ref(false);
+// 缂栬緫 ID
+const editId = ref(null);
+// 琛ㄦ牸鏁版嵁
+const categories = ref([]);
+// 娓叉煋鐢ㄧ殑琛ㄦ牸鏁版嵁
+// const renderTableData = computed(() => buildTree(categories.value));
+const renderTableData = ref([]);
+const operationType = ref('add')
+// 璁惧閫夐」
+const deviceOptions = ref([]);
+// 琛ㄥ崟寮曠敤
+const formRef = ref(null);
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ name: ''
+});
+// 鍒嗛〉鍙傛暟
+const pagination = reactive({
+ current: 1,
+ size: 10,
+ total: 0
+});
+
+// 澶囦欢棰嗙敤璁板綍
+const usageLoading = ref(false);
+const usageQuery = reactive({
+ sparePartsName: "",
+ sourceType: "",
+});
+const usagePagination = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+const usageTableData = ref([]);
+const usageColumns = ref([
+ { label: "鏉ユ簮", prop: "sourceText" },
+ { label: "鍗曟嵁/璁板綍ID", prop: "sourceId" },
+ { label: "璁惧鍚嶇О", prop: "deviceName" },
+ { label: "澶囦欢鍚嶇О", prop: "sparePartsName" },
+ { label: "棰嗙敤鏁伴噺", prop: "quantity" },
+ { label: "鎿嶄綔浜�", prop: "operator" },
+ { label: "鏃堕棿", prop: "createTime" },
+]);
+
+const handleTabChange = async (name) => {
+ if (name === "usage") {
+ usagePagination.current = 1;
+ await fetchUsageData();
+ }
+};
+const columns = ref([
+ {
+ label: "璁惧鍚嶇О",
+ prop: "deviceNameStr",
+ },
+ {
+ label: "澶囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ label: "澶囦欢缂栧彿",
+ prop: "sparePartsNo",
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ slot: "status",
+ dataType: "slot",
+ },
+ {
+ label: "浠锋牸",
+ prop: "price",
+ },
+ {
+ label: "鏁伴噺",
+ prop: "quantity",
+ },
+ {
+ label: "鎻忚堪",
+ prop: "description",
+ },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ width: 150,
+ fixed: 'right',
+ align: "center",
+ dataType: "action",
+ operation: [
+ {
+ name: "缂栬緫",
+ clickFun: (row) => {
+ editCategory(row)
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ clickFun: (row) => {
+ deleteCategory(row.id)
+ },
+ },
+ ],
+ },
+]);
+// 琛ㄥ崟鏁版嵁
+const form = reactive({
+ id:'',
+ name: '',
+ sparePartsNo: '',
+ status: '',
+ description: '',
+ deviceLedgerIds: [],
+ price: null
+});
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = reactive({
+ name: [
+ { required: true, message: '璇疯緭鍏ュ浠跺悕绉�', trigger: 'blur' }
+ ],
+ sparePartsNo: [
+ { required: true, message: '璇疯緭鍏ュ浠剁紪鍙�', trigger: 'blur' }
+ ],
+ quantity:[
+ { required: true, message: '璇疯緭鍏ユ暟閲�', trigger: 'blur' }
+ ],
+ status: [
+ { required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }
+ ],
+ deviceLedgerIds: [
+ {
+ required: true,
+ message: '璇烽�夋嫨璁惧',
+ trigger: 'change',
+ validator: (rule, value, callback) => {
+ if (operationType.value === 'add' && (!value || value.length === 0)) {
+ callback(new Error('璇烽�夋嫨璁惧'));
+ } else {
+ callback();
+ }
+ }
+ }
+ ]
+});
+// 鑾峰彇缂╄繘閲�
+const getIndentation = (row) => {
+ // 杩欓噷绠�鍗曡繑鍥� 20锛屽彲鏍规嵁瀹為檯闇�姹傚疄鐜板眰绾х缉杩涢�昏緫
+ return 20;
+};
+// 瀹氫箟 buildTree 鍑芥暟
+const buildTree = (flatData) => {
+ const map = {};
+ const result = [];
+ if(flatData){
+ return result;
+ }
+ flatData.forEach(item => {
+ map[item.id] = { ...item, children: [] };
+ });
+ flatData.forEach(item => {
+ if (item.parentId === null || !map[item.parentId]) {
+ result.push(map[item.id]);
+ } else {
+ map[item.parentId].children.push(map[item.id]);
+ }
+ });
+ return result;
+};
+// 鑾峰彇鍒楄〃鏁版嵁
+const fetchListData = async () => {
+ loading.value = true;
+ try {
+ const params = {
+ current: pagination.current,
+ size: pagination.size
+ };
+ if (queryParams.name) {
+ params.name = queryParams.name;
+ }
+ const res = await getSparePartsList(params);
+ if (res.code === 200) {
+ renderTableData.value = res.data.records || [];
+ categories.value = res.data.records || [];
+ pagination.total = res.data.total || 0;
+ }
+ } catch (error) {
+ loading.value = false;
+ } finally {
+ loading.value = false;
+ }
+}
+
+const fetchUsageData = async () => {
+ usageLoading.value = true;
+ try {
+ const res = await getSparePartsUsagePage({
+ current: usagePagination.current,
+ size: usagePagination.size,
+ sparePartsName: usageQuery.sparePartsName || undefined,
+ sourceType: usageQuery.sourceType || undefined,
+ });
+ if (res?.code === 200) {
+ const records = res?.data?.records || [];
+ usagePagination.total = res?.data?.total || 0;
+ usageTableData.value = records.map((r, idx) => ({
+ rowKey: r.id ?? `${usagePagination.current}-${idx}`,
+ ...r,
+ sourceText: r.sourceText === "" ? "-" : r.sourceText,
+ }));
+ } else {
+ usagePagination.total = 0;
+ usageTableData.value = [];
+ }
+ } finally {
+ usageLoading.value = false;
+ }
+};
+
+const handleUsageQuery = () => {
+ usagePagination.current = 1;
+ fetchUsageData();
+};
+const resetUsageQuery = () => {
+ usageQuery.sparePartsName = "";
+ usageQuery.sourceType = "";
+ usagePagination.current = 1;
+ fetchUsageData();
+};
+const handleUsagePageChange = (obj) => {
+ usagePagination.current = obj.page;
+ usagePagination.size = obj.limit;
+ fetchUsageData();
+};
+
+// 鏌ヨ
+const handleQuery = () => {
+ pagination.current = 1;
+ fetchListData();
+}
+
+// 閲嶇疆鏌ヨ
+const resetQuery = () => {
+ queryParams.name = '';
+ pagination.current = 1;
+ fetchListData();
+}
+
+// 鍒嗛〉澶у皬鏀瑰彉
+const handleSizeChange = (size) => {
+ pagination.size = size;
+ pagination.current = 1;
+ fetchListData();
+}
+
+// 褰撳墠椤垫敼鍙�
+const handleCurrentChange = (current) => {
+ pagination.current = current;
+ fetchListData();
+}
+
+// 鍔犺浇璁惧鍒楄〃锛堝湪鎵撳紑寮规鏃惰皟鐢級
+const loadDeviceName = async () => {
+ try {
+ const { data } = await getDeviceLedger();
+ deviceOptions.value = data || [];
+ } catch (error) {
+ ElMessage.error('鑾峰彇璁惧鍒楄〃澶辫触');
+ }
+};
+
+// 鏂板鍒嗙被
+const addCategory = async () => {
+ await loadDeviceName();
+ form.id = '';
+ form.name = '';
+ form.sparePartsNo = '';
+ form.status = '';
+ form.description = '';
+ form.deviceLedgerIds = [];
+ form.quantity = undefined;
+ form.price = null;
+ operationType.value = 'add'
+ dialogVisible.value = true;
+};
+
+// 缂栬緫鍒嗙被
+const editCategory = async (row) => {
+ await loadDeviceName();
+ Object.assign(form, row);
+ // 濡傛灉鍚庣杩斿洖鐨勬槸 deviceIds 瀛楃涓诧紝闇�瑕佽浆鎹负鏁扮粍
+ if (row.deviceIds && typeof row.deviceIds === 'string') {
+ // 纭繚ID绫诲瀷涓庤澶囬�夐」涓殑ID绫诲瀷涓�鑷�
+ const deviceIdsArray = row.deviceIds.split(',').map(id => id.trim()).filter(id => id);
+ // 濡傛灉璁惧閫夐」涓殑ID鏄暟瀛楃被鍨嬶紝鍒欒浆鎹负鏁板瓧
+ if (deviceOptions.value.length > 0 && typeof deviceOptions.value[0].id === 'number') {
+ form.deviceLedgerIds = deviceIdsArray.map(id => Number(id)).filter(id => !isNaN(id));
+ } else {
+ form.deviceLedgerIds = deviceIdsArray;
+ }
+ } else if (row.deviceIds && Array.isArray(row.deviceIds)) {
+ form.deviceLedgerIds = row.deviceIds;
+ } else {
+ form.deviceLedgerIds = [];
+ }
+ operationType.value = 'edit'
+ dialogVisible.value = true;
+};
+
+// 鍒犻櫎鍒嗙被
+const deleteCategory = async (id) => {
+ try {
+ await ElMessageBox.confirm('姝ゆ搷浣滃皢姘镐箙鍒犻櫎璇ュ垎绫伙紝鏄惁缁х画?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ });
+ loading.value = true;
+ const res = await delSparePart(id);
+ if (res.code === 200) {
+ ElMessage.success('鍒犻櫎鎴愬姛');
+ fetchListData();
+ } else {
+ ElMessage.error(res.message || '鍒犻櫎澶辫触');
+ }
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error('鍒犻櫎澶辫触');
+ }
+ } finally {
+ loading.value = false;
+ }
+};
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = async () => {
+ if (!formRef.value) return;
+ try {
+ await formRef.value.validate();
+ formLoading.value = true;
+
+ // 鏋勫缓鎻愪氦鏁版嵁
+ const submitData = {
+ ...form,
+ deviceIds: form.deviceLedgerIds && form.deviceLedgerIds.length > 0
+ ? form.deviceLedgerIds.join(',')
+ : ''
+ };
+
+ // 鍒犻櫎涓嶉渶瑕佺殑瀛楁
+ delete submitData.deviceLedgerIds;
+
+ if (operationType.value === 'edit') {
+ let res = await editSparePart(submitData);
+ if (res.code === 200) {
+ ElMessage.success('缂栬緫鎴愬姛');
+ dialogVisible.value = false;
+ fetchListData();
+ }
+ } else {
+ let res = await addSparePart(submitData);
+ if (res.code === 200) {
+ ElMessage.success('鏂板鎴愬姛');
+ dialogVisible.value = false;
+ fetchListData();
+ }
+ }
+ } catch (error) {
+ ElMessage.error('璇峰~鍐欏畬鏁磋〃鍗曚俊鎭�');
+ } finally {
+ formLoading.value = false;
+ }
+};
+
+// 缁勪欢鎸傝浇鏃惰幏鍙栧垪琛ㄦ暟鎹�
+onMounted(() => {
+ fetchListData();
+});
+</script>
+
+<style scoped>
+.spare-part-category {
+ padding: 20px;
+}
+.search_form {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+}
+.table_list {
+ margin-top: unset;
+}
+
+.pagination-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+.el-table__header-wrapper th {
+ background-color: #f5f7fa;
+ font-weight: 600;
+}
+
+.el-table__row:hover > td {
+ background-color: #fafafa;
+}
+
+/* 鎸夐挳缁勬牱寮� */
+.actions > div {
+ display: flex;
+ gap: 10px;
+}
+
+/* 纭繚琛ㄦ牸涓殑鎿嶄綔鎸夐挳涓嶄細琚埅鏂� */
+.el-table-column--fixed-right .el-button {
+ margin: 0 2px;
+}
+
+/* 鏍戝舰鑺傜偣鍐呭鏍峰紡 */
+.nested-tree .el-tree-node__expand-icon {
+ font-size: 12px;
+ margin-right: 4px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue b/src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue
new file mode 100644
index 0000000..0fcccb2
--- /dev/null
+++ b/src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue
@@ -0,0 +1,253 @@
+<template>
+ <FormDialog v-model="visible"
+ :title="'璁惧淇濆吇'"
+ width="500px"
+ @confirm="sendForm"
+ @cancel="handleCancel"
+ @close="handleClose">
+ <el-form :model="form"
+ label-width="100px">
+ <el-form-item label="瀹為檯淇濆吇浜�">
+ <el-input v-model="form.maintenanceActuallyName"
+ placeholder="璇疯緭鍏ュ疄闄呬繚鍏讳汉"></el-input>
+ </el-form-item>
+ <el-form-item label="瀹為檯淇濆吇鏃ユ湡">
+ <el-date-picker v-model="form.maintenanceActuallyTime"
+ placeholder="璇烽�夋嫨瀹為檯淇濆吇鏃ユ湡"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="datetime"
+ clearable
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="淇濆吇鐘舵��">
+ <el-select v-model="form.status">
+ <el-option label="寰呬繚鍏�"
+ :value="0"></el-option>
+ <el-option label="瀹岀粨"
+ :value="1"></el-option>
+ <el-option label="澶辫触"
+ :value="2"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="淇濆吇缁撴灉">
+ <el-input v-model="form.maintenanceResult"
+ placeholder="璇疯緭鍏ヤ繚鍏荤粨鏋�"
+ type="text" />
+ </el-form-item>
+ <el-form-item label="璁惧澶囦欢">
+ <el-select v-model="form.sparePartsIds"
+ :loading="loadingSparePartOptions"
+ placeholder="璇烽�夋嫨璁惧澶囦欢"
+ multiple
+ filterable>
+ <el-option v-for="item in sparePartOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="selectedSpareParts.length"
+ label="棰嗙敤鏁伴噺">
+ <div style="width: 100%">
+ <div v-for="item in selectedSpareParts"
+ :key="item.id"
+ style="display: flex; align-items: center; gap: 10px; margin-bottom: 10px;">
+ <div style="flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
+ {{ item.name }}
+ <span v-if="item.quantity !== null && item.quantity !== undefined"
+ style="color: #909399;">
+ 锛堝簱瀛橈細{{ item.quantity }}锛�
+ </span>
+ </div>
+ <el-input-number v-model="sparePartQtyMap[item.id]"
+ :min="1"
+ :max="item.quantity !== null && item.quantity !== undefined ? Number(item.quantity) : undefined"
+ :step="1"
+ controls-position="right"
+ style="width: 180px" />
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="闄勪欢">
+ <FileUpload v-model:file-list="form.storageBlobDTOs" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+ import { addMaintenance } from "@/api/equipmentManagement/upkeep";
+ import useFormData from "@/hooks/useFormData";
+ import dayjs from "dayjs";
+ import useUserStore from "@/store/modules/user";
+ import { ElMessage } from "element-plus";
+ import { computed, ref, nextTick, getCurrentInstance } from "vue";
+ import { getSparePartsList } from "@/api/equipmentManagement/spareParts.js";
+
+ defineOptions({
+ name: "淇濆吇妯℃�佹",
+ });
+
+ const emits = defineEmits(["ok"]);
+
+ const { proxy } = getCurrentInstance();
+ // 淇濆瓨璁″垝淇濆吇璁板綍鐨刬d
+ const planId = ref();
+ const visible = ref(false);
+ const loading = ref(false);
+ const userStore = useUserStore();
+
+ const { form, resetForm } = useFormData({
+ maintenanceActuallyName: undefined, // 瀹為檯淇濆吇浜�
+ maintenanceActuallyTime: undefined, // 瀹為檯淇濆吇鏃ユ湡
+ maintenanceResult: undefined, // 淇濆吇缁撴灉
+ status: 0, // 淇濆吇鐘舵��
+ sparePartsIds: [],
+ storageBlobDTOs: [],
+ });
+
+ const sparePartOptions = ref([]);
+ const loadingSparePartOptions = ref(true);
+ const sparePartQtyMap = ref({});
+
+ const selectedSpareParts = computed(() => {
+ const ids = Array.isArray(form.sparePartsIds) ? form.sparePartsIds : [];
+ const set = new Set(ids.map(i => String(i)));
+ return (sparePartOptions.value || []).filter(p => set.has(String(p.id)));
+ });
+
+ const setForm = data => {
+ form.maintenanceActuallyName =
+ data.maintenanceActuallyName ?? userStore.nickName;
+ form.maintenanceActuallyTime = data.maintenanceActuallyTime
+ ? dayjs(data.maintenanceActuallyTime).format("YYYY-MM-DD HH:mm:ss")
+ : dayjs().format("YYYY-MM-DD HH:mm:ss");
+ form.maintenanceResult = data.maintenanceResult;
+ form.status = 1; // 榛樿鐘舵�佷负瀹岀粨
+ // multiple 閫夋嫨鍣ㄨ姹傛暟缁勶紱鍚庣甯歌繑鍥� "1,2,3"
+ if (Array.isArray(data?.sparePartsIds)) {
+ form.sparePartsIds = data.sparePartsIds
+ .map(v => Number(v))
+ .filter(v => Number.isFinite(v));
+ } else if (typeof data?.sparePartsIds === "string") {
+ form.sparePartsIds = data.sparePartsIds
+ .split(",")
+ .map(s => Number(String(s).trim()))
+ .filter(v => Number.isFinite(v));
+ } else if (typeof data?.sparePartsIds === "number") {
+ form.sparePartsIds = [data.sparePartsIds];
+ } else {
+ form.sparePartsIds = [];
+ }
+ form.storageBlobDTOs = data.storageBlobVOs || [];
+ };
+
+ /**
+ * @desc 淇濆瓨淇濆吇
+ */
+ const sendForm = async () => {
+ loading.value = true;
+ try {
+ // 棰嗙敤鏁伴噺鏍¢獙
+ if (Array.isArray(form.sparePartsIds) && form.sparePartsIds.length > 0) {
+ for (const partId of form.sparePartsIds) {
+ const qty = Number(sparePartQtyMap.value?.[partId]);
+ if (!Number.isFinite(qty) || qty <= 0) {
+ proxy?.$modal?.msgError?.("璇峰~鍐欏浠堕鐢ㄦ暟閲�");
+ return;
+ }
+ const part = sparePartOptions.value.find(
+ p => String(p.id) === String(partId)
+ );
+ const stock = part?.quantity;
+ if (
+ stock !== null &&
+ stock !== undefined &&
+ Number.isFinite(Number(stock))
+ ) {
+ if (qty > Number(stock)) {
+ proxy?.$modal?.msgError?.(
+ `澶囦欢銆�${part?.name || ""}銆嶉鐢ㄦ暟閲忎笉鑳借秴杩囧簱瀛橈紙${stock}锛塦
+ );
+ return;
+ }
+ }
+ }
+ }
+ const data = {
+ id: planId.value,
+ ...form,
+ sparePartsIds: form.sparePartsIds ? form.sparePartsIds.join(",") : "",
+ sparePartsQty: form.sparePartsIds
+ ? form.sparePartsIds
+ .map(id => sparePartQtyMap.value?.[id] ?? 1)
+ .join(",")
+ : "",
+ sparePartsUseList: form.sparePartsIds
+ ? form.sparePartsIds.map(id => ({
+ id,
+ quantity: sparePartQtyMap.value?.[id] ?? 1,
+ }))
+ : [],
+ };
+ const { code } = await addMaintenance(data);
+ if (code == 200) {
+ ElMessage.success("淇濆吇鎴愬姛");
+ emits("ok");
+ resetForm();
+ sparePartQtyMap.value = {};
+ visible.value = false;
+ }
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ const fetchSparePartOptions = () => {
+ loadingSparePartOptions.value = true;
+ // 鍜屽浠剁鐞嗛〉涓�鑷达細/spareParts/listPage 鈫� res.data.records
+ getSparePartsList({ current: 1, size: 1000 })
+ .then(res => {
+ if (res.code === 200) {
+ sparePartOptions.value = res?.data?.records || [];
+ } else {
+ sparePartOptions.value = [];
+ }
+ })
+ .catch(() => {
+ sparePartOptions.value = [];
+ })
+ .finally(() => {
+ loadingSparePartOptions.value = false;
+ });
+ };
+
+ const handleCancel = () => {
+ resetForm();
+ sparePartQtyMap.value = {};
+ visible.value = false;
+ };
+
+ const handleClose = () => {
+ resetForm();
+ sparePartQtyMap.value = {};
+ visible.value = false;
+ };
+
+ const open = async (id, row) => {
+ planId.value = id; // 淇濆瓨璁″垝淇濆吇璁板綍鐨刬d
+ visible.value = true;
+ await nextTick();
+ fetchSparePartOptions();
+ setForm(row);
+ };
+
+ defineExpose({
+ open,
+ });
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/equipmentManagement/upkeep/Form/PlanModal.vue b/src/views/equipmentManagement/upkeep/Form/PlanModal.vue
new file mode 100644
index 0000000..8a9cd98
--- /dev/null
+++ b/src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -0,0 +1,217 @@
+<template>
+ <FormDialog
+ v-model="visible"
+ :title="id ? '缂栬緫璁惧淇濆吇璁″垝' : '鏂板璁惧淇濆吇璁″垝'"
+ width="500px"
+ @confirm="sendForm"
+ @cancel="handleCancel"
+ @close="handleClose"
+ >
+ <el-form :model="form" label-width="100px">
+ <el-form-item label="璁惧鍚嶇О">
+ <el-select
+ v-model="form.deviceLedgerId"
+ @change="setDeviceModel"
+ placeholder="璇烽�夋嫨璁惧"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ >
+ <el-option
+ v-for="(item, index) in deviceOptions"
+ :key="index"
+ :label="item.deviceName"
+ :value="item.id"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿">
+ <el-input
+ v-model="form.deviceModel"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+ disabled
+ />
+ </el-form-item>
+ <el-form-item label="淇濆吇椤圭洰">
+ <el-input
+ v-model="form.machineryCategory"
+ placeholder="璇疯緭鍏ヤ繚鍏婚」鐩�"
+ />
+ </el-form-item>
+ <el-form-item label="褰曞叆浜�">
+ <el-select
+ v-model="form.createUser"
+ placeholder="璇烽�夋嫨"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ clearable
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="id" label="淇濅慨鐘舵��">
+ <el-select v-model="form.status">
+ <el-option label="寰呬繚淇�" :value="0"></el-option>
+ <el-option label="瀹岀粨" :value="1"></el-option>
+ <el-option label="澶辫触" :value="2"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="淇濆吇浜�">
+ <el-input
+ v-model="form.maintenancePerson"
+ placeholder="璇疯緭鍏ヤ繚鍏讳汉濮撳悕"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="璁″垝淇濆吇鏃ユ湡">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.maintenancePlanTime"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="date"
+ placeholder="璇烽�夋嫨璁″垝淇濆吇鏃ユ湡鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢" prop="attachmentIds">
+ <FileUpload v-model:file-list="form.storageBlobDTOs" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import {
+ addUpkeep,
+ editUpkeep,
+ getUpkeepById,
+} from "@/api/equipmentManagement/upkeep";
+import { ElMessage } from "element-plus";
+import useFormData from "@/hooks/useFormData";
+import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+import { onMounted } from "vue";
+import dayjs from "dayjs";
+import { userListNoPage } from "@/api/system/user.js";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+
+defineOptions({
+ name: "璁惧淇濆吇鏂板璁″垝",
+});
+
+const emits = defineEmits(["ok"]);
+
+const id = ref();
+const visible = ref(false);
+const loading = ref(false);
+
+const deviceOptions = ref([]);
+const loadDeviceName = async () => {
+ const { data } = await getDeviceLedger();
+ deviceOptions.value = data;
+};
+
+const { form, resetForm } = useFormData({
+ deviceLedgerId: undefined, // 璁惧Id
+ deviceName: undefined, // 璁惧鍚嶇О
+ deviceModel: undefined, // 瑙勬牸鍨嬪彿
+ maintenancePlanTime: undefined, // 璁″垝淇濆吇鏃ユ湡
+ createUser: undefined, // 褰曞叆浜�
+ status: 0, //淇濅慨鐘舵��
+ machineryCategory: undefined,
+ storageBlobDTOs: [],
+ maintenancePerson: undefined, // 淇濆吇浜�
+});
+
+const setDeviceModel = (deviceId) => {
+ const option = deviceOptions.value.find((item) => item.id === deviceId);
+ form.deviceModel = option.deviceModel;
+};
+
+/**
+ * @desc 璁剧疆琛ㄥ崟鍐呭
+ * @param data 璁惧淇℃伅
+ */
+const setForm = (data) => {
+ form.deviceLedgerId = data.deviceLedgerId;
+ form.deviceName = data.deviceName;
+ form.deviceModel = data.deviceModel;
+ form.createUser = Number(data.createUser);
+ form.status = data.status;
+ form.machineryCategory = data.machineryCategory;
+ form.maintenancePerson = data.maintenancePerson;
+ if (data.maintenancePlanTime) {
+ form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
+ "YYYY-MM-DD HH:mm:ss"
+ );
+ }
+ form.storageBlobDTOs = data.storageBlobVOs || [];
+};
+
+// 鐢ㄦ埛鍒楄〃
+const userList = ref([]);
+
+onMounted(() => {
+ loadDeviceName();
+ userListNoPage().then((res) => {
+ userList.value = res.data;
+ });
+});
+
+const openEdit = async (editId) => {
+ const { data } = await getUpkeepById(editId);
+ id.value = editId;
+ visible.value = true;
+ await nextTick();
+ setForm(data);
+};
+
+const sendForm = async () => {
+ loading.value = true;
+ try {
+ const { code } = id.value
+ ? await editUpkeep({ id: unref(id), ...form })
+ : await addUpkeep(form);
+ if (code == 200) {
+ ElMessage.success(`${id.value ? "缂栬緫" : "鏂板"}璁″垝鎴愬姛`);
+ visible.value = false;
+ emits("ok");
+ }
+ } finally {
+ loading.value = false;
+ }
+};
+
+const handleCancel = () => {
+ resetForm();
+ visible.value = false;
+};
+
+const handleClose = () => {
+ resetForm();
+ visible.value = false;
+};
+
+const openModal = () => {
+ id.value = undefined;
+ visible.value = true;
+};
+
+defineExpose({
+ openModal,
+ openEdit,
+});
+</script>
+
+<style lang="scss" scoped></style>
diff --git a/src/views/error/401.vue b/src/views/error/401.vue
new file mode 100644
index 0000000..2bc8922
--- /dev/null
+++ b/src/views/error/401.vue
@@ -0,0 +1,82 @@
+<template>
+ <div class="errPage-container">
+ <el-button icon="arrow-left" class="pan-back-btn" @click="back">
+ 杩斿洖
+ </el-button>
+ <el-row>
+ <el-col :span="12">
+ <h1 class="text-jumbo text-ginormous">
+ 401閿欒!
+ </h1>
+ <h2>鎮ㄦ病鏈夎闂潈闄愶紒</h2>
+ <h6>瀵逛笉璧凤紝鎮ㄦ病鏈夎闂潈闄愶紝璇蜂笉瑕佽繘琛岄潪娉曟搷浣滐紒鎮ㄥ彲浠ヨ繑鍥炰富椤甸潰</h6>
+ <ul class="list-unstyled">
+ <li class="link-type">
+ <router-link to="/">
+ 鍥為椤�
+ </router-link>
+ </li>
+ </ul>
+ </el-col>
+ <el-col :span="12">
+ <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import errImage from "@/assets/401_images/401.gif"
+
+let { proxy } = getCurrentInstance()
+
+const errGif = ref(errImage + "?" + +new Date())
+
+function back() {
+ if (proxy.$route.query.noGoBack) {
+ proxy.$router.push({ path: "/" })
+ } else {
+ proxy.$router.go(-1)
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+.errPage-container {
+ width: 800px;
+ max-width: 100%;
+ margin: 100px auto;
+ .pan-back-btn {
+ background: #008489;
+ color: #fff;
+ border: none !important;
+ }
+ .pan-gif {
+ margin: 0 auto;
+ display: block;
+ }
+ .pan-img {
+ display: block;
+ margin: 0 auto;
+ width: 100%;
+ }
+ .text-jumbo {
+ font-size: 60px;
+ font-weight: 700;
+ color: #484848;
+ }
+ .list-unstyled {
+ font-size: 14px;
+ li {
+ padding-bottom: 5px;
+ }
+ a {
+ color: #008489;
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/error/404.vue b/src/views/error/404.vue
new file mode 100644
index 0000000..08a617a
--- /dev/null
+++ b/src/views/error/404.vue
@@ -0,0 +1,227 @@
+<template>
+ <div class="wscn-http404-container">
+ <div class="wscn-http404">
+ <div class="pic-404">
+ <img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
+ <img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
+ <img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
+ <img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
+ </div>
+ <div class="bullshit">
+ <div class="bullshit__oops">
+ 404閿欒!
+ </div>
+ <div class="bullshit__headline">
+ {{ message }}
+ </div>
+ <div class="bullshit__info">
+ 瀵逛笉璧凤紝鎮ㄦ鍦ㄥ鎵剧殑椤甸潰涓嶅瓨鍦ㄣ�傚皾璇曟鏌RL鐨勯敊璇紝鐒跺悗鎸夋祻瑙堝櫒涓婄殑鍒锋柊鎸夐挳鎴栧皾璇曞湪鎴戜滑鐨勫簲鐢ㄧ▼搴忎腑鎵惧埌鍏朵粬鍐呭銆�
+ </div>
+ <router-link to="/index" class="bullshit__return-home">
+ 杩斿洖棣栭〉
+ </router-link>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+let message = computed(() => {
+ return '鎵句笉鍒扮綉椤碉紒'
+})
+</script>
+
+<style lang="scss" scoped>
+.wscn-http404-container{
+ transform: translate(-50%,-50%);
+ position: absolute;
+ top: 40%;
+ left: 50%;
+}
+.wscn-http404 {
+ position: relative;
+ width: 1200px;
+ padding: 0 50px;
+ overflow: hidden;
+ .pic-404 {
+ position: relative;
+ float: left;
+ width: 600px;
+ overflow: hidden;
+ &__parent {
+ width: 100%;
+ }
+ &__child {
+ position: absolute;
+ &.left {
+ width: 80px;
+ top: 17px;
+ left: 220px;
+ opacity: 0;
+ animation-name: cloudLeft;
+ animation-duration: 2s;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+ animation-delay: 1s;
+ }
+ &.mid {
+ width: 46px;
+ top: 10px;
+ left: 420px;
+ opacity: 0;
+ animation-name: cloudMid;
+ animation-duration: 2s;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+ animation-delay: 1.2s;
+ }
+ &.right {
+ width: 62px;
+ top: 100px;
+ left: 500px;
+ opacity: 0;
+ animation-name: cloudRight;
+ animation-duration: 2s;
+ animation-timing-function: linear;
+ animation-fill-mode: forwards;
+ animation-delay: 1s;
+ }
+ @keyframes cloudLeft {
+ 0% {
+ top: 17px;
+ left: 220px;
+ opacity: 0;
+ }
+ 20% {
+ top: 33px;
+ left: 188px;
+ opacity: 1;
+ }
+ 80% {
+ top: 81px;
+ left: 92px;
+ opacity: 1;
+ }
+ 100% {
+ top: 97px;
+ left: 60px;
+ opacity: 0;
+ }
+ }
+ @keyframes cloudMid {
+ 0% {
+ top: 10px;
+ left: 420px;
+ opacity: 0;
+ }
+ 20% {
+ top: 40px;
+ left: 360px;
+ opacity: 1;
+ }
+ 70% {
+ top: 130px;
+ left: 180px;
+ opacity: 1;
+ }
+ 100% {
+ top: 160px;
+ left: 120px;
+ opacity: 0;
+ }
+ }
+ @keyframes cloudRight {
+ 0% {
+ top: 100px;
+ left: 500px;
+ opacity: 0;
+ }
+ 20% {
+ top: 120px;
+ left: 460px;
+ opacity: 1;
+ }
+ 80% {
+ top: 180px;
+ left: 340px;
+ opacity: 1;
+ }
+ 100% {
+ top: 200px;
+ left: 300px;
+ opacity: 0;
+ }
+ }
+ }
+ }
+ .bullshit {
+ position: relative;
+ float: left;
+ width: 300px;
+ padding: 30px 0;
+ overflow: hidden;
+ &__oops {
+ font-size: 32px;
+ font-weight: bold;
+ line-height: 40px;
+ color: #1482f0;
+ opacity: 0;
+ margin-bottom: 20px;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-fill-mode: forwards;
+ }
+ &__headline {
+ font-size: 20px;
+ line-height: 24px;
+ color: #222;
+ font-weight: bold;
+ opacity: 0;
+ margin-bottom: 10px;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-delay: 0.1s;
+ animation-fill-mode: forwards;
+ }
+ &__info {
+ font-size: 13px;
+ line-height: 21px;
+ color: grey;
+ opacity: 0;
+ margin-bottom: 30px;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-delay: 0.2s;
+ animation-fill-mode: forwards;
+ }
+ &__return-home {
+ display: block;
+ float: left;
+ width: 110px;
+ height: 36px;
+ background: #1482f0;
+ border-radius: 100px;
+ text-align: center;
+ color: #ffffff;
+ opacity: 0;
+ font-size: 14px;
+ line-height: 36px;
+ cursor: pointer;
+ animation-name: slideUp;
+ animation-duration: 0.5s;
+ animation-delay: 0.3s;
+ animation-fill-mode: forwards;
+ }
+ @keyframes slideUp {
+ 0% {
+ transform: translateY(60px);
+ opacity: 0;
+ }
+ 100% {
+ transform: translateY(0);
+ opacity: 1;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/example/DynamicTableExample.vue b/src/views/example/DynamicTableExample.vue
new file mode 100644
index 0000000..14dfc92
--- /dev/null
+++ b/src/views/example/DynamicTableExample.vue
@@ -0,0 +1,354 @@
+<template>
+ <div class="app-container">
+ <div class="search-form">
+ <el-form :inline="true" :model="searchForm">
+ <el-form-item label="閮ㄩ棬">
+ <el-input
+ v-model="searchForm.department"
+ placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�"
+ clearable
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item label="濮撳悕">
+ <el-input
+ v-model="searchForm.name"
+ placeholder="璇疯緭鍏ュ鍚�"
+ clearable
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ <el-button type="success" @click="handleAdd">鏂板</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <div class="table-container">
+ <DynamicTable
+ ref="dynamicTableRef"
+ :data="tableData"
+ :dict-types="dictTypes"
+ :loading="loading"
+ :show-selection="true"
+ :show-actions="true"
+ :show-pagination="true"
+ :pagination="pagination"
+ height="calc(100vh - 280px)"
+ @selection-change="handleSelectionChange"
+ @edit="handleEdit"
+ @delete="handleDelete"
+ @select-change="handleSelectChange"
+ @input-change="handleInputChange"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="600px"
+ append-to-body
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px"
+ >
+ <el-form-item label="閮ㄩ棬" prop="department">
+ <el-input v-model="form.department" placeholder="璇疯緭鍏ラ儴闂�" />
+ </el-form-item>
+ <el-form-item label="濮撳悕" prop="name">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ュ鍚�" />
+ </el-form-item>
+ <el-form-item label="宸ュ彿" prop="employeeId">
+ <el-input v-model="form.employeeId" placeholder="璇疯緭鍏ュ伐鍙�" />
+ </el-form-item>
+
+ <!-- 鍔ㄦ�佽〃鍗曢」锛氭牴鎹瓧鍏哥敓鎴� -->
+ <el-form-item
+ v-for="dictItem in dynamicFormItems"
+ :key="dictItem.value"
+ :label="dictItem.label"
+ :prop="dictItem.value"
+ >
+ <el-select
+ v-model="form[dictItem.value]"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="option in dictItem.options"
+ :key="option.value"
+ :label="option.label"
+ :value="option.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import DynamicTable from '@/components/DynamicTable/index.vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const editIndex = ref(-1)
+const selectedRows = ref([])
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ department: '',
+ name: ''
+})
+
+// 琛ㄦ牸鏁版嵁
+const tableData = ref([
+ {
+ id: 1,
+ department: '鎶�鏈儴',
+ name: '寮犱笁',
+ employeeId: 'EMP001',
+ status: '1',
+ level: '2',
+ position: '1'
+ },
+ {
+ id: 2,
+ department: '浜轰簨閮�',
+ name: '鏉庡洓',
+ employeeId: 'EMP002',
+ status: '0',
+ level: '1',
+ position: '2'
+ },
+ {
+ id: 3,
+ department: '璐㈠姟閮�',
+ name: '鐜嬩簲',
+ employeeId: 'EMP003',
+ status: '1',
+ level: '3',
+ position: '1'
+ }
+])
+
+// 瀛楀吀绫诲瀷閰嶇疆
+const dictTypes = ref([
+ 'sys_normal_disable', // 鐘舵�佸瓧鍏�
+ 'sys_user_level', // 绾у埆瀛楀吀
+ 'sys_user_position' // 鑱屼綅瀛楀吀
+])
+
+// 鍒嗛〉閰嶇疆
+const pagination = reactive({
+ current: 1,
+ size: 10,
+ total: 0
+})
+
+// 琛ㄥ崟鏁版嵁
+const form = reactive({
+ department: '',
+ name: '',
+ employeeId: '',
+ status: '',
+ level: '',
+ position: ''
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+ department: [
+ { required: true, message: '璇疯緭鍏ラ儴闂�', trigger: 'blur' }
+ ],
+ name: [
+ { required: true, message: '璇疯緭鍏ュ鍚�', trigger: 'blur' }
+ ],
+ employeeId: [
+ { required: true, message: '璇疯緭鍏ュ伐鍙�', trigger: 'blur' }
+ ]
+}
+
+// 鍔ㄦ�佽〃鍗曢」
+const dynamicFormItems = computed(() => {
+ // 杩欓噷鍙互鏍规嵁瀛楀吀鏁版嵁鍔ㄦ�佺敓鎴愯〃鍗曢」
+ return [
+ {
+ label: '鐘舵��',
+ value: 'status',
+ options: [
+ { label: '鍚敤', value: '1' },
+ { label: '绂佺敤', value: '0' }
+ ]
+ },
+ {
+ label: '绾у埆',
+ value: 'level',
+ options: [
+ { label: '鍒濈骇', value: '1' },
+ { label: '涓骇', value: '2' },
+ { label: '楂樼骇', value: '3' }
+ ]
+ },
+ {
+ label: '鑱屼綅',
+ value: 'position',
+ options: [
+ { label: '鍛樺伐', value: '1' },
+ { label: '涓荤', value: '2' },
+ { label: '缁忕悊', value: '3' }
+ ]
+ }
+ ]
+})
+
+// 缁勪欢寮曠敤
+const dynamicTableRef = ref(null)
+const formRef = ref(null)
+
+// 浜嬩欢澶勭悊鍑芥暟
+const handleSearch = () => {
+ // 瀹炵幇鎼滅储閫昏緫
+ console.log('鎼滅储鏉′欢:', searchForm)
+ ElMessage.success('鎼滅储鍔熻兘寰呭疄鐜�')
+}
+
+const handleReset = () => {
+ searchForm.department = ''
+ searchForm.name = ''
+}
+
+const handleAdd = () => {
+ dialogTitle.value = '鏂板鍛樺伐'
+ editIndex.value = -1
+ resetForm()
+ dialogVisible.value = true
+}
+
+const handleEdit = (row, index) => {
+ dialogTitle.value = '缂栬緫鍛樺伐'
+ editIndex.value = index
+ Object.assign(form, row)
+ dialogVisible.value = true
+}
+
+const handleDelete = async (row, index) => {
+ try {
+ await ElMessageBox.confirm('纭畾瑕佸垹闄よ繖鏉¤褰曞悧锛�', '鎻愮ず', {
+ type: 'warning'
+ })
+
+ tableData.value.splice(index, 1)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ } catch (error) {
+ // 鐢ㄦ埛鍙栨秷鍒犻櫎
+ }
+}
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection
+}
+
+const handleSelectChange = (row, prop, value) => {
+ console.log('閫夋嫨鍙樺寲:', row, prop, value)
+ // 鍙互鍦ㄨ繖閲屽鐞嗘暟鎹洿鏂伴�昏緫
+}
+
+const handleInputChange = (row, prop, value) => {
+ console.log('杈撳叆鍙樺寲:', row, prop, value)
+ // 鍙互鍦ㄨ繖閲屽鐞嗘暟鎹洿鏂伴�昏緫
+}
+
+const handleSizeChange = (size) => {
+ pagination.size = size
+ // 閲嶆柊鍔犺浇鏁版嵁
+}
+
+const handleCurrentChange = (current) => {
+ pagination.current = current
+ // 閲嶆柊鍔犺浇鏁版嵁
+}
+
+const handleSubmit = async () => {
+ try {
+ await formRef.value.validate()
+
+ if (editIndex.value === -1) {
+ // 鏂板
+ const newRow = {
+ id: Date.now(),
+ ...form
+ }
+ tableData.value.push(newRow)
+ ElMessage.success('鏂板鎴愬姛')
+ } else {
+ // 缂栬緫
+ Object.assign(tableData.value[editIndex.value], form)
+ ElMessage.success('缂栬緫鎴愬姛')
+ }
+
+ dialogVisible.value = false
+ } catch (error) {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', error)
+ }
+}
+
+const resetForm = () => {
+ Object.assign(form, {
+ department: '',
+ name: '',
+ employeeId: '',
+ status: '',
+ level: '',
+ position: ''
+ })
+ formRef.value?.resetFields()
+}
+
+// 缁勪欢鎸傝浇鏃跺垵濮嬪寲鏁版嵁
+onMounted(() => {
+ pagination.total = tableData.value.length
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.search-form {
+ margin-bottom: 20px;
+ padding: 20px;
+ background-color: #f5f5f5;
+ border-radius: 4px;
+}
+
+.table-container {
+ background-color: #fff;
+ border-radius: 4px;
+ padding: 20px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+</style>
diff --git a/src/views/example/SimpleExample.vue b/src/views/example/SimpleExample.vue
new file mode 100644
index 0000000..fb528eb
--- /dev/null
+++ b/src/views/example/SimpleExample.vue
@@ -0,0 +1,135 @@
+<template>
+ <div class="app-container">
+ <!-- 绠�鍗曠殑鎼滅储鍖哄煙 -->
+ <el-card class="search-card">
+ <el-form :inline="true">
+ <el-form-item label="閮ㄩ棬">
+ <el-input v-model="searchForm.department" placeholder="璇疯緭鍏ラ儴闂�" clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 鍔ㄦ�佽〃鏍� -->
+ <el-card class="table-card">
+ <template #header>
+ <div class="card-header">
+ <span>鍛樺伐淇℃伅琛�</span>
+ <el-button type="primary" size="small" @click="handleAdd">鏂板鍛樺伐</el-button>
+ </div>
+ </template>
+
+ <DynamicTable
+ :data="tableData"
+ :dict-types="dictTypes"
+ :loading="loading"
+ :show-selection="true"
+ :show-actions="true"
+ height="400px"
+ @selection-change="handleSelectionChange"
+ @edit="handleEdit"
+ @delete="handleDelete"
+ />
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import DynamicTable from '@/components/DynamicTable/index.vue'
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ department: ''
+})
+
+// 琛ㄦ牸鏁版嵁
+const tableData = ref([
+ {
+ id: 1,
+ department: '鎶�鏈儴',
+ name: '寮犱笁',
+ employeeId: 'EMP001',
+ sys_normal_disable: '1', // 鐘舵��
+ sys_user_level: '2', // 绾у埆
+ sys_user_position: '1' // 鑱屼綅
+ },
+ {
+ id: 2,
+ department: '浜轰簨閮�',
+ name: '鏉庡洓',
+ employeeId: 'EMP002',
+ sys_normal_disable: '0', // 鐘舵��
+ sys_user_level: '1', // 绾у埆
+ sys_user_position: '2' // 鑱屼綅
+ }
+])
+
+// 瀛楀吀绫诲瀷
+const dictTypes = ref([
+ 'sys_normal_disable', // 鐘舵�侊細鍚敤/绂佺敤
+ 'sys_user_level', // 绾у埆锛氬垵绾�/涓骇/楂樼骇
+ 'sys_user_position' // 鑱屼綅锛氬憳宸�/涓荤/缁忕悊
+])
+
+// 鍔犺浇鐘舵��
+const loading = ref(false)
+
+// 浜嬩欢澶勭悊
+const handleSearch = () => {
+ loading.value = true
+ // 妯℃嫙鎼滅储
+ setTimeout(() => {
+ loading.value = false
+ ElMessage.success('鎼滅储瀹屾垚')
+ }, 1000)
+}
+
+const handleReset = () => {
+ searchForm.department = ''
+}
+
+const handleAdd = () => {
+ ElMessage.info('鏂板鍔熻兘寰呭疄鐜�')
+}
+
+const handleSelectionChange = (selection) => {
+ console.log('閫変腑鐨勮:', selection)
+}
+
+const handleEdit = (row, index) => {
+ ElMessage.info(`缂栬緫绗�${index + 1}琛屾暟鎹甡)
+}
+
+const handleDelete = (row, index) => {
+ ElMessage.warning(`鍒犻櫎绗�${index + 1}琛屾暟鎹甡)
+}
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.search-card {
+ margin-bottom: 20px;
+}
+
+.table-card {
+ margin-bottom: 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+:deep(.el-form-item) {
+ margin-bottom: 0;
+}
+</style>
diff --git a/src/views/fileManagement/bookshelf/detail.vue b/src/views/fileManagement/bookshelf/detail.vue
new file mode 100644
index 0000000..5d7d3ac
--- /dev/null
+++ b/src/views/fileManagement/bookshelf/detail.vue
@@ -0,0 +1,110 @@
+<template>
+ <div class="detail-container">
+ <div class="header">
+ <el-button @click="handleBack" type="primary" size="small">杩斿洖</el-button>
+ <h2>鍥句功璇︽儏</h2>
+ </div>
+
+ <div class="content" v-loading="loading">
+ <el-card v-if="current">
+ <template #header>
+ <div class="card-header">
+ <span>鍩烘湰淇℃伅</span>
+ </div>
+ </template>
+
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鍥句功缂栧彿">{{ current.docNumber }}</el-descriptions-item>
+ <el-descriptions-item label="鍥句功鍚嶇О">{{ current.docName }}</el-descriptions-item>
+ <el-descriptions-item label="鍏ュ簱鏃堕棿">{{ current.createTime }}</el-descriptions-item>
+ <!-- <el-descriptions-item label="褰撳墠浣嶇疆">{{ current.currentLocation }}</el-descriptions-item> -->
+ <el-descriptions-item label="鐘舵��">{{ current.docStatus }}</el-descriptions-item>
+ </el-descriptions>
+
+ <!-- <div class="additional-info" v-if="current.description">
+ <h4>鍥句功绠�浠�</h4>
+ <p>{{ current.description }}</p>
+ </div> -->
+ </el-card>
+
+ <el-empty v-else description="鏆傛棤鏁版嵁" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+
+// 瀹氫箟props
+const props = defineProps({
+ current: {
+ type: Object,
+ required: true
+ }
+})
+
+// 瀹氫箟emits
+const emit = defineEmits(['hanldeBack'])
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+// const bookInfo = ref(null)
+
+// 鏂规硶
+const handleBack = () => {
+ emit('hanldeBack')
+}
+
+</script>
+
+<style scoped>
+.detail-container {
+ padding: 20px;
+ height: 100%;
+ background-color: #f5f5f5;
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+ background-color: #fff;
+ padding: 15px 20px;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.header h2 {
+ margin: 0 0 0 20px;
+ color: #333;
+}
+
+.content {
+ background-color: #fff;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.card-header {
+ font-weight: bold;
+ color: #333;
+}
+
+.additional-info {
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid #ebeef5;
+}
+
+.additional-info h4 {
+ margin: 0 0 10px 0;
+ color: #333;
+ font-size: 16px;
+}
+
+.additional-info p {
+ margin: 0;
+ color: #666;
+ line-height: 1.6;
+}
+</style>
diff --git a/src/views/fileManagement/bookshelf/index.vue b/src/views/fileManagement/bookshelf/index.vue
new file mode 100644
index 0000000..c73ae1c
--- /dev/null
+++ b/src/views/fileManagement/bookshelf/index.vue
@@ -0,0 +1,695 @@
+<template>
+ <div class="sample">
+ <div class="main-content" v-if="!isDetail">
+ <div class="search">
+ <div class="search_thing">
+ <div class="search_label">浠撳簱鍚嶇О锛�</div>
+ <div class="search_input">
+ <el-select v-model="entity.warehouseId" placeholder="閫夋嫨浠撳簱" size="small" @change="warehouseChange">
+ <el-option v-for="item in warehouse" :key="item.id" :label="item.label" :value="item.id">
+ </el-option>
+ </el-select>
+ </div>
+ </div>
+ <div class="search_thing">
+ <div class="search_label">璐ф灦锛�</div>
+ <div class="search_input">
+ <el-select v-model="entity.shelfId" placeholder="閫夋嫨璐ф灦" size="small" @change="handleShelf">
+ <el-option v-for="item in shelf" :key="item.id" :label="item.label" :value="item.id">
+ </el-option>
+ </el-select>
+ </div>
+ </div>
+ <!-- <div class="search_thing">
+ <el-button size="small" @click="handleShelf(entity.shelfId,'')">閲嶇疆</el-button>
+ <el-button size="small" type="primary" @click="handleShelf(entity.shelfId)">鏌ヨ</el-button>
+ </div> -->
+ <div class="btns">
+ <el-button size="small" style="color:#3A7BFA" @click="keepVisible=true">缁存姢</el-button>
+ <el-button size="small" style="color:#3A7BFA" @click="warehouseVisible=true,isEdit=false">娣诲姞浠撳簱</el-button>
+ <el-button size="small" style="color:#3A7BFA" @click="shelvesVisible=true,isEdit=false"
+ :disabled="entity.warehouseId==null">娣诲姞璐ф灦</el-button>
+ </div>
+ </div>
+ <div class="table" v-loading="tableLoading">
+ <table class="tables" style="table-layout:fixed;" v-if="tableList.length>0">
+ <tbody>
+ <tr v-for="(item,index) in tableList" :key="index">
+ <td v-for="(m,i) in item" :key="i" class="content">
+ <h4 v-if="m.row!=undefined">{{ m.row }} - {{ m.col }}</h4>
+ <ul>
+ <el-tooltip
+ effect="dark"
+ placement="top"
+ v-for="(n,j) in m.documentationDtoList"
+ :key="j">
+ <template #content><span>{{ n.docName }}</span>
+ <span> [{{ n.docNumber }}]</span></template>
+ <li class="green"
+ @click="handelDetail(n)">
+ <i></i>
+ <span>{{ n.docName }}</span>
+ <span> [{{ n.docNumber }}] <span :style="{ color: getStatusColor(n.docStatus) }">锛坽{ n.docStatus }}锛�</span></span>
+ </li>
+ </el-tooltip>
+ </ul>
+ </td>
+ </tr>
+ <tr>
+ <td v-for="(item,index) in rowList" :key="index" style="background: ghostwhite;height: 20px;">{{ item }}
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ <span v-else style="color: rgb(144, 147, 153);display: inline-block;position: absolute;top: 60%;left: 50%;transform: translate(-50%,-50%);">鏆傛棤鏁版嵁</span>
+ </div>
+ </div>
+ <Detail v-else @hanldeBack="isDetail=false" :current="current" />
+
+ <!-- 搴撲綅缁存姢瀵硅瘽妗� -->
+ <el-dialog v-model="keepVisible" title="搴撲綅缁存姢" width="350px" :append-to-body="true">
+ <el-tree :data="warehouse" ref="tree" node-key="id"
+ highlight-current v-if="keepVisible"
+ empty-text="鏆傛棤鏁版嵁">
+ <template #default="{ node, data }">
+ <div class="custom-tree-node" style="width: 100%;">
+ <el-row style="width: 100%;display: flex;align-items: center;">
+ <el-col :span="14">
+ <span>
+ <el-icon v-if="node.level < 2" class="folder-icon">
+ <FolderOpened />
+ </el-icon>
+ <el-icon v-else class="file-icon">
+ <Document />
+ </el-icon>
+ {{ data.label }}
+ </span>
+ </el-col>
+ <el-col :span="10" v-if="node.level<3">
+ <el-button type="link" size="small" :icon="Edit" @click.stop="handleEdit(data,node.level)">
+ </el-button>
+ <el-button type="danger" size="small" :icon="Delete" @click.stop="handleDelete(data,node.level)">
+ </el-button>
+ </el-col>
+ </el-row>
+ </div>
+ </template>
+ </el-tree>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="keepVisible = false" >纭� 瀹�</el-button>
+ <el-button @click="keepVisible = false">鍙� 娑�</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
+ <!-- 浠撳簱鏂板/淇敼瀵硅瘽妗� -->
+ <el-dialog v-model="warehouseVisible" :title="isEdit?'浠撳簱淇敼':'浠撳簱鏂板'" width="350px">
+ <el-row>
+ <el-col class="search_thing" :span="24">
+ <div class="search_label"><span class="required-span">* </span>浠撳簱鍚嶇О锛�</div>
+ <div class="search_input">
+ <el-input v-model="name" size="small" @keyup.enter="confirmWarehouse"></el-input>
+ </div>
+ </el-col>
+ </el-row>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="confirmWarehouse" :loading="upLoadWarehouse">纭� 瀹�</el-button>
+ <el-button @click="warehouseVisible = false">鍙� 娑�</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
+ <!-- 璐ф灦鏂板/淇敼瀵硅瘽妗� -->
+ <el-dialog v-model="shelvesVisible" :title="isEdit?'璐ф灦淇敼':'璐ф灦鏂板'" width="350px">
+ <el-row>
+ <el-col class="search_thing" :span="24">
+ <div class="search_label"><span class="required-span">* </span>璐ф灦鍚嶇О锛�</div>
+ <div class="search_input">
+ <el-input v-model="shelves.name" size="small"></el-input>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col class="search_thing" :span="24">
+ <div class="search_label"><span class="required-span">* </span>璐ф灦灞傛暟锛�</div>
+ <div class="search_input">
+ <el-input-number v-model="shelves.row" size="small" :min="1" :max="10" :precision="0" :step="1" controls-position="right" style="width: 100%"></el-input-number>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col class="search_thing" :span="24">
+ <div class="search_label"><span class="required-span">* </span>璐ф灦鍒楁暟锛�</div>
+ <div class="search_input">
+ <el-input-number v-model="shelves.col" size="small" :min="1" :max="10" :precision="0" :step="1" controls-position="right" style="width: 100%"></el-input-number>
+ </div>
+ </el-col>
+ </el-row>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="confirmShelves" :loading="upLoadShelves">纭� 瀹�</el-button>
+ <el-button @click="shelvesVisible = false">鍙� 娑�</el-button>
+ </span>
+ </template>
+ </el-dialog>
+
+
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Edit, Delete, FolderOpened, Document } from '@element-plus/icons-vue'
+import { getWarehouseList, addWarehouse, updateWarehouse, deleteWarehouse, getWarehouseStructure, addShelf, updateShelf, deleteShelf } from '@/api/fileManagement/bookshelf'
+import Detail from './detail.vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const entity = reactive({
+ warehouseId: null,
+ shelfId: null
+})
+
+const warehouse = ref([])
+const shelf = ref([])
+const keepVisible = ref(false)
+const warehouseVisible = ref(false)
+const shelvesVisible = ref(false)
+const upLoadWarehouse = ref(false)
+const upLoadShelves = ref(false)
+const tableList = ref([])
+const rowList = ref([])
+const value = ref('')
+const name = ref('')
+const shelves = reactive({})
+const isEdit = ref(false)
+const isDetail = ref(false)
+const currentEdit = ref(null)
+const tableLoading = ref(false)
+const current = ref({})
+
+// 妯℃澘寮曠敤
+const organization = ref(null)
+
+// 鐩戝惉鍣�
+watch(isEdit, (newVal) => {
+ if (!newVal) {
+ Object.keys(shelves).forEach(key => delete shelves[key])
+ }
+})
+
+// 鏂规硶
+
+const selectList = async () => {
+ // 杩欓噷闇�瑕佹浛鎹负瀹為檯鐨凙PI璋冪敤
+ const res = await getWarehouseList()
+ warehouse.value = res.data
+
+ if (warehouse.value.length == 0) {
+ entity.warehouseId = ''
+ entity.shelfId = ''
+ tableList.value = []
+ }
+
+
+
+ if (!entity.warehouseId && warehouse.value.length > 0) {
+ entity.warehouseId = warehouse.value[0].id
+ warehouseChange(entity.warehouseId)
+ if (shelf.value.length > 0) {
+ entity.shelfId = shelf.value[0].id
+ handleShelf(entity.shelfId)
+ } else {
+ tableList.value = []
+ }
+ } else if (warehouse.value.length > 0) {
+ warehouseChange(entity.warehouseId)
+ if (shelf.value.length > 0) {
+ entity.shelfId = shelf.value[0].id
+ handleShelf(entity.shelfId)
+ } else {
+ tableList.value = []
+ }
+ }
+}
+
+const confirmWarehouse = () => {
+ if (!name.value) {
+ ElMessage.error('璇峰~鍐欎粨搴撳悕绉�')
+ return
+ }
+ upLoadWarehouse.value = true
+
+ if (currentEdit.value && currentEdit.value.id) {
+ // 淇敼浠撳簱
+ // 杩欓噷闇�瑕佹浛鎹负瀹為檯鐨凙PI璋冪敤
+ updateWarehouse({
+ id: currentEdit.value.id,
+ warehouseName: name.value
+ }).then(res => {
+ upLoadWarehouse.value = false
+ warehouseVisible.value = false
+ currentEdit.value = null
+ ElMessage.success('淇敼鎴愬姛')
+ selectList()
+ name.value = ''
+ warehouseChange(entity.warehouseId)
+ })
+
+ } else {
+ // 鏂板浠撳簱
+ // 杩欓噷闇�瑕佹浛鎹负瀹為檯鐨凙PI璋冪敤
+ addWarehouse({
+ warehouseName: name.value
+ }).then(res => {
+ upLoadWarehouse.value = false
+ warehouseVisible.value = false
+ ElMessage.success('娣诲姞鎴愬姛')
+ selectList()
+ name.value = ''
+ warehouseChange(entity.warehouseId)
+ })
+ }
+}
+
+const confirmShelves = () => {
+ if (!shelves.name) {
+ ElMessage.error('璇峰~鍐欒揣鏋跺悕绉�')
+ return
+ }
+ if (!shelves.row) {
+ ElMessage.error('璇峰~鍐欒揣鏋跺眰鏁�')
+ return
+ }
+ if (!shelves.col) {
+ ElMessage.error('璇峰~鍐欒揣鏋跺垪鏁�')
+ return
+ }
+ const rowNum = Number(shelves.row)
+ const colNum = Number(shelves.col)
+ if (rowNum < 1 || colNum < 1 || rowNum > 10 || colNum > 10) {
+ ElMessage.error('璐ф灦灞傛暟鍜屽垪鏁伴渶涓�1-10鐨勬暣鏁�')
+ return
+ }
+ if (!Number.isInteger(rowNum) || !Number.isInteger(colNum)) {
+ ElMessage.error('璐ф灦灞傛暟鍜屽垪鏁颁笉鑳戒负灏忔暟')
+ return
+ }
+ upLoadShelves.value = true
+
+ if (currentEdit.value && currentEdit.value.id) {
+ // 淇敼
+ updateShelf({
+ id: currentEdit.value.id,
+ name: shelves.name,
+ row: rowNum,
+ col: colNum,
+ warehouseId: entity.warehouseId
+ }).then(res => {
+ upLoadShelves.value = false
+ shelvesVisible.value = false
+ ElMessage.success('淇敼鎴愬姛')
+ selectList()
+ currentEdit.value = {}
+ }).catch(err => {
+ upLoadShelves.value = false
+ shelvesVisible.value = false
+ ElMessage.error('淇敼澶辫触')
+ })
+
+ } else {
+ // 鏂板
+ addShelf({
+ name: shelves.name,
+ row: rowNum,
+ col: colNum,
+ warehouseId: entity.warehouseId
+ }).then(res => {
+ upLoadShelves.value = false
+ shelvesVisible.value = false
+ ElMessage.success('娣诲姞鎴愬姛')
+ selectList()
+ Object.keys(shelves).forEach(key => delete shelves[key])
+ }).catch(err => {
+ upLoadShelves.value = false
+ shelvesVisible.value = false
+ ElMessage.error('娣诲姞澶辫触')
+ })
+ }
+ warehouseChange(entity.warehouseId)
+}
+
+
+
+const handleDelete = (row, level) => {
+ ElMessageBox.confirm('鏄惁鍒犻櫎褰撳墠鏁版嵁?', "璀﹀憡", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }).then(() => {
+ if (level == 1) {
+ // 鍒犻櫎浠撳簱锛堟帴鍙h姹備紶 ID 鏁扮粍锛�
+ deleteWarehouse([row.id]).then(res => {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ selectList()
+ })
+ } else {
+ // 鍒犻櫎璐ф灦锛堟帴鍙e悓鏍疯姹備紶 ID 鏁扮粍锛�
+ deleteShelf([row.id]).then(res => {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ selectList()
+ })
+ }
+ warehouseChange(entity.warehouseId)
+ }).catch(() => {})
+}
+
+const handleEdit = (data, level) => {
+ isEdit.value = true
+ if (level == 1) {
+ warehouseVisible.value = true
+ currentEdit.value = data
+ name.value = data.label
+ } else {
+ shelvesVisible.value = true
+ currentEdit.value = data
+ Object.assign(shelves, {
+ name: data.label,
+ row: data.row,
+ col: data.col,
+ warehouseId: data.warehouseId
+ })
+ }
+}
+
+const handelDetail = (row) => {
+ current.value = row
+ isDetail.value = true
+}
+
+// 鏍规嵁鏂囨。鐘舵�佽繑鍥炲搴旂殑棰滆壊
+const getStatusColor = (status) => {
+ if (status === '姝e父') {
+ return '#34BD66' // 缁胯壊
+ } else if (status === '鍊熷嚭') {
+ return '#F56C6C' // 绾㈣壊
+ }
+ return '#606266' // 榛樿棰滆壊
+}
+
+const warehouseChange = (val) => {
+tableList.value = []
+let map = warehouse.value.find(a => {
+ return a && a.id === val ? a : null
+})
+if (map && map.children) {
+ shelf.value = map.children
+ entity.shelfId = ''
+} else {
+ shelf.value = []
+}
+currentEdit.value = null
+}
+
+const handleShelf = async(e) => {
+ if (e) {
+ tableLoading.value = true
+ let data = []
+ const res = await getWarehouseStructure({warehouseGoodsShelvesId:e})
+ if(res.code == 200){
+ data = res.data.map(m=>{
+ m.books = m.documentationDtoList|[]
+ return m
+ })
+ }else{
+ ElMessage.error(res.message)
+ }
+ setTimeout(() => {
+ tableLoading.value = false
+ let set = new Set()
+ tableList.value = []
+ let arr = []
+
+ if (data && data.length > 0) {
+ data.forEach(m => {
+ if (m && m.row && m.col) {
+ set.add(m.col)
+ if (arr.length > 0) {
+ if (arr.find(n => n.row == m.row)) {
+ arr.push(m)
+ } else {
+ tableList.value.push(arr)
+ arr = []
+ arr.push(m)
+ }
+ } else {
+ arr.push(m)
+ }
+ }
+ })
+
+ if (arr.length > 0) {
+ tableList.value.push(arr)
+ }
+ }
+
+ rowList.value = []
+ for (let i = 0; i < set.size; i++) {
+ rowList.value.push(`${i + 1} 鍒梎)
+ }
+ console.log(6666, tableList.value,rowList.value,data)
+ }, 1000)
+ }
+}
+
+
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ selectList()
+})
+</script>
+
+<style scoped>
+ .main-content {
+ width: 100%;
+ height: 100%;
+ padding: 20px;
+ box-sizing: border-box;
+ }
+
+ .title {
+ height: 20px;
+ line-height: 20px;
+ margin-bottom: 20px;
+ }
+
+ .search {
+ background-color: #fff;
+ height: 80px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ margin-bottom: 20px;
+ }
+
+ .search_thing {
+ display: flex;
+ align-items: center;
+ height: 50px;
+ margin-right: 20px;
+ }
+
+ .search_label {
+ width: 90px;
+ font-size: 14px;
+ text-align: right;
+ color: #606266;
+ font-weight: 500;
+ margin-right: 10px;
+ }
+
+ .search_input {
+ width: 200px;
+ }
+
+ .table {
+ background-color: #fff;
+ width: 100%;
+ height: calc(100% - 100px);
+ padding: 20px;
+ overflow-y: auto;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ .el-form-item {
+ margin-bottom: 16px;
+ }
+
+ .btns {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+
+ .tables {
+ width: 100%;
+ height: 100%;
+ border-collapse: collapse;
+ border: 1px solid #e4e7ed;
+ }
+
+ .tables th {
+ font-size: 14px;
+ border: 1px solid #e4e7ed;
+ background-color: #fafafa;
+ padding: 8px;
+ font-weight: 500;
+ }
+
+ .tables td {
+ font-size: 12px;
+ text-align: center;
+ vertical-align: top;
+ border: 1px solid #e4e7ed;
+ padding: 8px;
+ box-sizing: border-box;
+ height: 120px;
+ background-color: #fff;
+ }
+
+ .tables ul {
+ list-style-type: none;
+ }
+
+ .tables ul li {
+ border-radius: 3px;
+ padding: 4px 10px;
+ box-sizing: border-box;
+ margin-bottom: 5px;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: start;
+ color: #333333;
+ cursor: pointer;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .tables h4 {
+ color: #999999;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 6px 0;
+ }
+
+ .tables i {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ margin-right: 6px;
+ }
+
+ li:hover {
+ background: rgba(58, 123, 250, 0.18);
+ }
+
+ li:hover i {
+ background: #3A7BFA;
+ }
+
+ li:hover .num {
+ color: #3A7BFA;
+ }
+
+ .green {
+ background: #E0F6EA;
+ }
+
+ .green i {
+ background: #34BD66;
+ }
+
+ .green .num {
+ color: #34BD66;
+ }
+
+ .el-dialog {
+ position: relative;
+ }
+
+ .shaoma {
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ color: #3A7BFA;
+ position: absolute;
+ top: 23px;
+ right: 54px;
+ cursor: pointer;
+ }
+
+ .folder-icon {
+ color: #409eff;
+ font-size: 16px;
+ margin-right: 6px;
+ }
+
+ .file-icon {
+ color: #67c23a;
+ font-size: 16px;
+ margin-right: 6px;
+ }
+
+ .node_i {
+ color: orange;
+ font-size: 18px;
+ }
+
+ .custom-tree-node .el-button {
+ opacity: 0;
+ }
+
+ .custom-tree-node:hover .el-button {
+ opacity: 1;
+ }
+
+ :deep(.el-loading-mask) {
+ z-index: 10;
+ }
+
+ .required-span {
+ color: #f56c6c;
+ }
+
+ .table-row {
+ border-bottom: 1px solid #e4e7ed;
+ }
+
+ .table-row:last-child {
+ border-bottom: none;
+ }
+
+ .column-header {
+ background-color: #fafafa !important;
+ font-weight: 500;
+ color: #606266;
+ }
+
+ .content {
+ transition: background-color 0.2s ease;
+ }
+
+ .content:hover {
+ background-color: #f5f7fa;
+ }
+</style>
diff --git a/src/views/fileManagement/borrow/index.vue b/src/views/fileManagement/borrow/index.vue
new file mode 100644
index 0000000..c7d8e0d
--- /dev/null
+++ b/src/views/fileManagement/borrow/index.vue
@@ -0,0 +1,658 @@
+<template>
+ <div class="app-container borrow-view">
+ <!-- 鏌ヨ鍖哄煙 -->
+ <div class="search-container">
+ <el-form :model="searchForm" :inline="true" class="search-form">
+ <el-form-item label="鍊熼槄鐘舵�侊細">
+ <el-select v-model="searchForm.borrowStatus" placeholder="璇烽�夋嫨鍊熼槄鐘舵��" clearable style="width: 150px">
+ <el-option label="鍊熼槄" value="鍊熼槄" />
+ <el-option label="褰掕繕" value="褰掕繕" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍊熼槄浜猴細">
+ <el-input
+ v-model="searchForm.borrower"
+ placeholder="璇疯緭鍏ュ�熼槄浜�"
+ clearable
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item label="鍊熼槄鏃ユ湡鑼冨洿锛�">
+ <el-date-picker
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 300px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">
+ <el-icon><Search /></el-icon>
+ 鏌ヨ
+ </el-button>
+ <el-button @click="handleReset">
+ <el-icon><Refresh /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ <el-form-item style="margin-left: auto;">
+ <el-button type="primary" @click="openBorrowDia('add')">
+ <el-icon><Plus /></el-icon>
+ 鏂板鍊熼槄
+ </el-button>
+ <el-button @click="handleOut">
+ 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ @click="handleBatchDelete"
+ :disabled="selectedRows.length === 0"
+ >
+ <el-icon><Delete /></el-icon>
+ 鎵归噺鍒犻櫎 ({{ selectedRows.length }})
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <!-- 琛ㄦ牸鍖哄煙 -->
+ <div class="table-container">
+ <PIMTable
+ :table-data="borrowList"
+ :column="tableColumns"
+ :is-selection="true"
+ :border="true"
+ :table-loading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ layout: 'total, sizes, prev, pager, next, jumper'
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="handlePagination"
+ />
+ </div>
+
+ <!-- 鍊熼槄鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog
+ v-model="borrowDia"
+ :title="borrowOperationType === 'add' ? '鏂板鍊熼槄' : '缂栬緫鍊熼槄'"
+ width="800px"
+ @close="closeBorrowDia"
+ @keydown.enter.prevent
+ >
+ <el-form
+ :model="borrowForm"
+ label-width="140px"
+ :rules="borrowRules"
+ ref="borrowFormRef"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍊熼槄浜猴細" prop="borrower">
+ <el-input v-model="borrowForm.borrower" placeholder="璇疯緭鍏ュ�熼槄浜�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍊熼槄涔︾睄锛�" prop="documentationId">
+ <div style="display: flex; gap: 10px;">
+ <el-select
+ v-if="borrowOperationType !== 'edit'"
+ v-model="borrowForm.documentationId"
+ placeholder="璇烽�夋嫨鍊熼槄涔︾睄"
+ style="flex: 1;width: 100px;"
+ @change="handleSelectChange"
+ >
+ <el-option
+ v-for="item in documentList"
+ :key="item.id"
+ :label="item.docName || item.name"
+ :value="item.id"
+ />
+ </el-select>
+ <el-input
+ v-else
+ v-model="currentEditDocName"
+ style="flex: 1;width: 100px;"
+ disabled
+ />
+ <el-input
+ v-if="borrowOperationType !== 'edit'"
+ v-model="scanContent"
+ placeholder="鎵爜杈撳叆"
+ style="width: 100px;"
+ @input="handleScanContent"
+ clearable
+ />
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍊熼槄鏃ユ湡锛�" prop="borrowDate">
+ <el-date-picker
+ v-model="borrowForm.borrowDate"
+ type="date"
+ placeholder="閫夋嫨鍊熼槄鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="搴斿綊杩樻棩鏈燂細" prop="dueReturnDate">
+ <el-date-picker
+ v-model="borrowForm.dueReturnDate"
+ type="date"
+ placeholder="閫夋嫨搴斿綊杩樻棩鏈�"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鍊熼槄鐩殑锛�" prop="borrowPurpose">
+ <el-input v-model="borrowForm.borrowPurpose" placeholder="璇疯緭鍏ュ�熼槄鐩殑" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞锛�" prop="remark">
+ <el-input
+ v-model="borrowForm.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitBorrowForm">纭</el-button>
+ <el-button @click="closeBorrowDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import { Search, Refresh, Plus, Delete } from '@element-plus/icons-vue';
+import PIMTable from '@/components/PIMTable/PIMTable.vue';
+import { getBorrowList, addBorrow, updateBorrow, deleteBorrow, getDocumentList } from '@/api/fileManagement/borrow';
+
+const { proxy } = getCurrentInstance();
+
+// 鍝嶅簲寮忔暟鎹�
+const borrowDia = ref(false);
+const borrowOperationType = ref("");
+const tableLoading = ref(false);
+const borrowList = ref([]);
+const selectedRows = ref([]);
+const documentList = ref([]); // 鏂囨。鍒楄〃锛岀敤浜庡�熼槄涔︾睄閫夋嫨
+const scanContent = ref() // 鎵爜鍐呭
+const currentEditDocName = ref(''); // 缂栬緫鏃跺瓨鍌ㄧ殑鏂囨。鍚嶇О
+// 鍒嗛〉鐩稿叧
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ documentationId: "",
+ borrowStatus: "",
+ borrower: "",
+ returnerId: "",
+ dateRange: []
+});
+
+// 鍊熼槄琛ㄥ崟
+const borrowForm = reactive({
+ id: "",
+ documentationId: "",
+ borrower: "",
+ returnerId: "",
+ borrowPurpose: "",
+ borrowDate: "",
+ dueReturnDate: "",
+ returnDate: "",
+ borrowStatus: "",
+ remark: ""
+});
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const borrowRules = reactive({
+
+ borrower: [{ required: true, message: "璇疯緭鍏ュ�熼槄浜�", trigger: "blur" }],
+ borrowPurpose: [{ required: true, message: "璇疯緭鍏ュ�熼槄鐩殑", trigger: "blur" }],
+ borrowDate: [{ required: true, message: "璇烽�夋嫨鍊熼槄鏃ユ湡", trigger: "change" }],
+ dueReturnDate: [{ required: true, message: "璇烽�夋嫨搴斿綊杩樻棩鏈�", trigger: "change" }],
+ borrowStatus: [{ required: true, message: "璇烽�夋嫨鍊熼槄鐘舵��", trigger: "change" }]
+});
+
+// 琛ㄦ牸鍒楅厤缃�
+const tableColumns = ref([
+ {
+ label: '鏂囨。鍚嶇О',
+ prop: 'docName',
+ width: '200',
+ },
+ { label: '鍊熼槄浜�', prop: 'borrower' },
+ { label: '鍊熼槄鐩殑', prop: 'borrowPurpose' },
+ { label: '鍊熼槄鏃ユ湡', prop: 'borrowDate' },
+ { label: '搴斿綊杩樻棩鏈�', prop: 'dueReturnDate' },
+ {
+ label: '鍊熼槄鐘舵��',
+ prop: 'borrowStatus',
+ width: '100',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ return params;
+ },
+ formatType: (params) => {
+ if (params === '褰掕繕') return 'success';
+ if (params === '鍊熼槄') return 'warning';
+ return 'info';
+ }
+ },
+ { label: '澶囨敞', prop: 'remark', width: '150' },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: '150',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: (row) => row.borrowStatus === '褰掕繕',
+ clickFun: (row) => {
+ openBorrowDia('edit', row)
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "text",
+ clickFun: (row) => {
+ handleDelete(row)
+ },
+ },
+ ],
+ }
+]);
+
+// 鍒濆鍖栨暟鎹�
+const initData = async () => {
+ await Promise.all([
+ loadDocumentList(),
+ loadBorrowList()
+ ]);
+};
+
+// 鍔犺浇鏂囨。鍒楄〃
+const loadDocumentList = async () => {
+ try {
+ const res = await getDocumentList();
+ if (res.code === 200) {
+ documentList.value = res.data || [];
+ console.log("shuju",documentList.value)
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇鏂囨。鍒楄〃澶辫触");
+ documentList.value = [];
+ }
+ } catch (error) {
+ ElMessage.error("鑾峰彇鏂囨。鍒楄〃澶辫触锛岃閲嶈瘯");
+ documentList.value = [];
+ }
+};
+
+// 鍔犺浇鍊熼槄鍒楄〃
+const loadBorrowList = async () => {
+ try {
+ tableLoading.value = true;
+
+ // 鏋勫缓鏌ヨ鍙傛暟
+ const query = {
+ page: pagination.currentPage,
+ size: pagination.pageSize,
+ documentationId: searchForm.documentationId || undefined,
+ borrowStatus: searchForm.borrowStatus || undefined,
+ borrower: searchForm.borrower || undefined,
+ returnerId: searchForm.returnerId || undefined,
+ entryDateStart: searchForm.dateRange && searchForm.dateRange.length > 0 ? searchForm.dateRange[0] : undefined,
+ entryDateEnd: searchForm.dateRange && searchForm.dateRange.length > 1 ? searchForm.dateRange[1] : undefined
+ };
+
+ // 绉婚櫎undefined鐨勫弬鏁�
+ Object.keys(query).forEach(key => {
+ if (query[key] === undefined) {
+ delete query[key];
+ }
+ });
+
+ const res = await getBorrowList(query);
+ if (res.code === 200) {
+ borrowList.value = res.data.records || [];
+ pagination.total = res.data.total || 0;
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇鍊熼槄鍒楄〃澶辫触");
+ borrowList.value = [];
+ pagination.total = 0;
+ }
+
+ // 閲嶇疆閫夋嫨鐘舵��
+ selectedRows.value = [];
+ } catch (error) {
+ ElMessage.error("鑾峰彇鍊熼槄鍒楄〃澶辫触锛岃閲嶈瘯");
+ borrowList.value = [];
+ pagination.total = 0;
+ } finally {
+ tableLoading.value = false;
+ }
+};
+
+// 鏌ヨ
+const handleSearch = () => {
+ pagination.currentPage = 1;
+ loadBorrowList();
+};
+
+// 閲嶇疆鏌ヨ
+const handleReset = () => {
+ searchForm.documentationId = "";
+ searchForm.borrowStatus = "";
+ searchForm.borrower = "";
+ searchForm.returnerId = "";
+ searchForm.dateRange = [];
+ pagination.currentPage = 1;
+ loadBorrowList();
+ ElMessage.success("鏌ヨ鏉′欢宸查噸缃�");
+};
+
+// 澶勭悊涓嬫媺閫夋嫨鍙樺寲
+const handleSelectChange = (value) => {
+ // 褰撲笅鎷夋閫夋嫨鏃讹紝娓呯┖鎵爜杈撳叆妗�
+ scanContent.value = '';
+};
+
+// 澶勭悊鎵爜鍐呭
+const handleScanContent = async (value) => {
+ if (!value) return;
+ try {
+ // 鏌ユ壘鎵弿鍐呭瀵瑰簲鐨勬枃妗�
+ const matchedDoc = documentList.value.find(item =>
+ item.id == value
+ );
+ console.log("matchedDoc", matchedDoc);
+
+
+ if (matchedDoc) {
+
+ // 鎵惧埌鍖归厤鐨勬枃妗o紝璁剧疆琛ㄥ崟鍊�
+ borrowForm.documentationId = matchedDoc.id;
+ ElMessage.success(`宸查�夋嫨: ${matchedDoc.docName || matchedDoc.name}`);
+ } else {
+ // 鏈壘鍒板尮閰嶇殑鏂囨。锛屾彁绀虹敤鎴�
+ ElMessage.warning('鏈壘鍒板搴旂殑涔︾睄锛岃妫�鏌ユ壂鐮佸唴瀹规垨鎵嬪姩閫夋嫨');
+ }
+ } catch (error) {
+ ElMessage.error('鎵爜澶勭悊澶辫触锛岃閲嶈瘯');
+ console.error('鎵爜澶勭悊閿欒:', error);
+ }
+}
+// 鎵撳紑鍊熼槄寮规
+const openBorrowDia = async (type, data) => {
+ // 鍏堝埛鏂版枃妗e垪琛�
+ await loadDocumentList();
+
+ borrowOperationType.value = type;
+ borrowDia.value = true;
+ scanContent.value = ''; // 娓呯┖鎵爜鍐呭
+
+ if (type === "edit") {
+ // 缂栬緫妯″紡锛屽姞杞界幇鏈夋暟鎹�
+ Object.assign(borrowForm, data);
+ // 瀛樺偍鏂囨。鍚嶇О鐢ㄤ簬鏄剧ず
+ currentEditDocName.value = data.docName || '';
+ } else {
+ // 鏂板妯″紡锛屾竻绌鸿〃鍗�
+ Object.keys(borrowForm).forEach(key => {
+ borrowForm[key] = "";
+ });
+ currentEditDocName.value = ''; // 娓呯┖缂栬緫鏃剁殑鏂囨。鍚嶇О
+ // 璁剧疆榛樿鐘舵��
+ borrowForm.borrowStatus = "鍊熼槄";
+ // 璁剧疆褰撳墠鏃ユ湡涓哄�熼槄鏃ユ湡
+ borrowForm.borrowDate = new Date().toISOString().split('T')[0];
+ }
+};
+
+// 鍏抽棴鍊熼槄寮规
+const closeBorrowDia = () => {
+ proxy.$refs.borrowFormRef.resetFields();
+ borrowDia.value = false;
+ scanContent.value = ''; // 娓呯┖鎵爜鍐呭
+ currentEditDocName.value = ''; // 娓呯┖缂栬緫鏃剁殑鏂囨。鍚嶇О
+};
+
+// 鎻愪氦鍊熼槄琛ㄥ崟
+const submitBorrowForm = () => {
+ proxy.$refs.borrowFormRef.validate(async (valid) => {
+ if (valid) {
+ try {
+ if (borrowOperationType.value === "edit") {
+ // 缂栬緫妯″紡锛屾洿鏂扮幇鏈夋暟鎹�
+ const res = await updateBorrow({
+ borrower:borrowForm.borrower,
+ id: borrowForm.id,
+ borrowPurpose: borrowForm.borrowPurpose,
+ borrowDate: borrowForm.borrowDate,
+ dueReturnDate: borrowForm.dueReturnDate,
+ returnDate: borrowForm.returnDate,
+ remark: borrowForm.remark
+ });
+
+ if (res.code === 200) {
+ ElMessage.success("缂栬緫鎴愬姛");
+ await loadBorrowList();
+ closeBorrowDia();
+ } else {
+ ElMessage.error(res.msg || "缂栬緫澶辫触");
+ }
+ } else {
+ // 鏂板妯″紡锛屾坊鍔犳柊鏁版嵁
+ const res = await addBorrow({
+ documentationId: borrowForm.documentationId,
+ borrower: borrowForm.borrower,
+ returnerId: borrowForm.returnerId,
+ borrowPurpose: borrowForm.borrowPurpose,
+ borrowDate: borrowForm.borrowDate,
+ dueReturnDate: borrowForm.dueReturnDate,
+ returnDate: borrowForm.returnDate,
+ borrowStatus: borrowForm.borrowStatus,
+ remark: borrowForm.remark
+ });
+
+ if (res.code === 200) {
+ ElMessage.success("鏂板鎴愬姛");
+ await loadBorrowList();
+ closeBorrowDia();
+ } else {
+ ElMessage.error(res.msg || "鏂板澶辫触");
+ }
+ }
+ } catch (error) {
+ ElMessage.error("鎿嶄綔澶辫触锛岃閲嶈瘯");
+ }
+ }
+ });
+};
+
+// 鍒犻櫎鍊熼槄璁板綍
+const handleDelete = (row) => {
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄よ繖鏉″�熼槄璁板綍鍚楋紵`,
+ "鍒犻櫎鎻愮ず",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ ).then(async () => {
+ try {
+ const res = await deleteBorrow([row.id]);
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await loadBorrowList();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("鍒犻櫎澶辫触锛岃閲嶈瘯");
+ }
+ }).catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+// 鎵归噺鍒犻櫎
+const handleBatchDelete = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑璁板綍");
+ return;
+ }
+
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉″�熼槄璁板綍鍚楋紵`,
+ "鎵归噺鍒犻櫎鎻愮ず",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ ).then(async () => {
+ try {
+ const selectedIds = selectedRows.value.map(row => row.id);
+ const res = await deleteBorrow(selectedIds);
+ if (res.code === 200) {
+ ElMessage.success("鎵归噺鍒犻櫎鎴愬姛");
+ await loadBorrowList();
+ } else {
+ ElMessage.error(res.msg || "鎵归噺鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("鎵归噺鍒犻櫎澶辫触锛岃閲嶈瘯");
+ }
+ }).catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/documentationBorrowManagement/export", {}, "鍊熼槄鐧昏.xlsx");
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑�");
+ });
+};
+
+// 閫夋嫨鍙樺寲浜嬩欢
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 澶勭悊鍒嗛〉鍙樺寲
+const handlePagination = (current, size) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ loadBorrowList();
+};
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ initData();
+});
+</script>
+
+<style scoped>
+.borrow-view {
+ padding: 20px;
+}
+
+.search-container {
+ background: #ffffff;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.search-form {
+ margin: 0;
+}
+
+.table-container {
+ background: #ffffff;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.empty-data {
+ text-align: center;
+ color: #909399;
+ padding: 40px;
+ font-size: 14px;
+}
+
+.dialog-footer {
+ text-align: center;
+}
+
+:deep(.el-form-item__label) {
+ font-weight: 500;
+ color: #303133;
+}
+
+:deep(.el-input__wrapper) {
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
+}
+
+:deep(.el-input__wrapper:hover) {
+ box-shadow: 0 0 0 1px #c0c4cc inset;
+}
+
+:deep(.el-input__wrapper.is-focus) {
+ box-shadow: 0 0 0 1px #409eff inset;
+}
+</style>
diff --git a/src/views/fileManagement/document/attachmentManager.vue b/src/views/fileManagement/document/attachmentManager.vue
new file mode 100644
index 0000000..f3c0504
--- /dev/null
+++ b/src/views/fileManagement/document/attachmentManager.vue
@@ -0,0 +1,425 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="闄勪欢绠$悊" width="60%" :before-close="handleClose">
+ <div class="attachment-manager">
+ <!-- 涓婁紶鍖哄煙 -->
+ <div class="upload-section">
+ <el-upload
+ ref="uploadRef"
+ :action="uploadUrl"
+ :headers="uploadHeaders"
+ :before-upload="handleBeforeUpload"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :on-remove="handleRemove"
+ :file-list="fileList"
+ multiple
+ :show-file-list="false"
+ :data="{documentId: currentDocumentId}"
+ accept=".doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.txt,.xml,.jpg,.jpeg,.png,.gif,.bmp,.rar,.zip,.7z"
+ >
+ <el-button type="primary" :icon="Plus">涓婁紶闄勪欢</el-button>
+ <template #tip>
+ <div class="el-upload__tip">
+ 鏀寔鏍煎紡锛歞oc锛宒ocx锛寈ls锛寈lsx锛宲pt锛宲ptx锛宲df锛宼xt锛寈ml锛宩pg锛宩peg锛宲ng锛実if锛宐mp锛宺ar锛寊ip锛�7z
+ <br>鍗曚釜鏂囦欢澶у皬涓嶈秴杩�50MB
+ </div>
+ </template>
+ </el-upload>
+ </div>
+
+ <!-- 闄勪欢鍒楄〃 -->
+ <div class="attachment-list">
+ <el-table :data="fileList" border max-height="400px" v-loading="loading">
+ <el-table-column label="搴忓彿" type="index" width="60" align="center" />
+ <el-table-column label="闄勪欢鍚嶇О" prop="name" min-width="200" show-overflow-tooltip />
+ <el-table-column label="鏂囦欢澶у皬" prop="size" width="100" align="center">
+ <template #default="scope">
+ {{ formatFileSize(scope.row.size) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="涓婁紶鏃堕棿" prop="uploadTime" width="160" align="center">
+ <template #default="scope">
+ {{ formatDate(scope.row.uploadTime) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" prop="status" width="80" align="center">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === 'success' ? 'success' : 'danger'" size="small">
+ {{ scope.row.status === 'success' ? '鎴愬姛' : '澶辫触' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" width="200" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="previewFile(scope.row)">
+ 棰勮
+ </el-button>
+ <el-button link type="primary" size="small" @click="downloadFile(scope.row)">
+ 涓嬭浇
+ </el-button>
+ <el-button link type="danger" size="small" @click="removeFile(scope.row)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <!-- 鏂囦欢棰勮缁勪欢 -->
+ <filePreview ref="filePreviewRef" />
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import { getToken } from "@/utils/auth"
+import { addDocumentationFile, getDocumentationFileList, deleteDocumentationFile } from '@/api/fileManagement/document'
+import filePreview from '@/components/filePreview/index.vue'
+
+const props = defineProps({
+ // documentId 閫氳繃 open 浜嬩欢浼犲叆锛屼笉闇�瑕佷綔涓� props
+})
+
+const emit = defineEmits(['update:attachments'])
+
+const dialogVisible = ref(false)
+const loading = ref(false)
+const fileList = ref([])
+const uploadRef = ref()
+const filePreviewRef = ref()
+const currentDocumentId = ref('') // 鍐呴儴绠$悊褰撳墠鏂囨。ID
+
+// 涓婁紶閰嶇疆
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/file/upload"
+const uploadHeaders = computed(() => ({
+ Authorization: "Bearer " + getToken()
+}))
+
+// 鎵撳紑寮规
+const open = (attachments = [], documentId = '') => {
+ dialogVisible.value = true
+ currentDocumentId.value = documentId // 璁剧疆褰撳墠鏂囨。ID
+ // 濡傛灉鏈夋枃妗D锛屽垯鍔犺浇闄勪欢鍒楄〃
+ if (documentId) {
+ loadAttachmentList(documentId)
+ } else {
+ fileList.value = attachments || []
+ // total.value = fileList.value.length // Removed total.value
+ }
+ // currentPage.value = 1 // Removed currentPage.value
+}
+
+// 鍔犺浇闄勪欢鍒楄〃
+const loadAttachmentList = async (documentId) => {
+ try {
+ loading.value = true
+ const params = {
+ page: 1, // Always load from page 1
+ size: 1000, // Load all for now
+ documentationId: documentId
+ }
+
+ const res = await getDocumentationFileList(params)
+ if (res.code === 200) {
+ const records = res.data
+
+ // 杞崲鏁版嵁鏍煎紡
+ fileList.value = records.map(item => ({
+ id: item.id,
+ name: item.name,
+ size: item.fileSize,
+ url: item.url,
+ uploadTime: item.createTime || item.uploadTime,
+ status: 'success',
+ uid: item.id
+ }))
+
+ // total.value = totalCount // Removed total.value
+ } else {
+ ElMessage.error(res.msg || '鑾峰彇闄勪欢鍒楄〃澶辫触')
+ fileList.value = []
+ // total.value = 0 // Removed total.value
+ }
+ } catch (error) {
+ console.error('鑾峰彇闄勪欢鍒楄〃澶辫触:', error)
+ ElMessage.error('鑾峰彇闄勪欢鍒楄〃澶辫触')
+ fileList.value = []
+ // total.value = 0 // Removed total.value
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鍏抽棴寮规
+const handleClose = () => {
+ dialogVisible.value = false
+ emit('update:attachments', fileList.value)
+}
+
+// 鏂囦欢涓婁紶鍓嶆牎楠�
+const handleBeforeUpload = (file) => {
+ // 妫�鏌ユ枃浠跺ぇ灏忥紙50MB锛�
+ const isLt50M = file.size / 1024 / 1024 < 50
+ if (!isLt50M) {
+ ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃50MB!')
+ return false
+ }
+
+ // 妫�鏌ユ枃浠剁被鍨�
+ const allowedTypes = [
+ 'application/msword',
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+ 'application/vnd.ms-excel',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-powerpoint',
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
+ 'application/pdf',
+ 'text/plain',
+ 'text/xml',
+ 'image/jpeg',
+ 'image/png',
+ 'image/gif',
+ 'image/bmp',
+ 'application/x-rar-compressed',
+ 'application/zip',
+ 'application/x-7z-compressed'
+ ]
+
+ if (!allowedTypes.includes(file.type)) {
+ ElMessage.error('涓嶆敮鎸佺殑鏂囦欢绫诲瀷!')
+ return false
+ }
+
+ return true
+}
+
+// 鏂囦欢涓婁紶鎴愬姛
+const handleUploadSuccess = (response, file, fileList) => {
+ console.log('鏂囦欢涓婁紶鎴愬姛鍝嶅簲:', response);
+ console.log('鏂囦欢淇℃伅:', file);
+
+ if (response.code === 200) {
+ // 鏋勫缓闄勪欢鏁版嵁 - 纭繚姝g‘鑾峰彇URL
+ const attachmentData = {
+ name: file.name,
+ url: response.data.url || response.data.path || response.data.tempPath || file.url,
+ fileSize: file.size,
+ documentationId: currentDocumentId.value
+ };
+
+ console.log('鏋勫缓鐨勯檮浠舵暟鎹�:', attachmentData);
+
+ // 璋冪敤淇濆瓨闄勪欢鎺ュ彛
+ saveAttachment(attachmentData, file, fileList);
+ } else {
+ ElMessage.error(response.msg || '鏂囦欢涓婁紶澶辫触')
+ }
+}
+
+// 淇濆瓨闄勪欢淇℃伅
+const saveAttachment = async (attachmentData, file, fileList) => {
+ try {
+ console.log('寮�濮嬩繚瀛橀檮浠讹紝鏁版嵁:', attachmentData);
+
+ // 纭繚URL瀛楁瀛樺湪涓旀湁鏁�
+ if (!attachmentData.url) {
+ console.error('闄勪欢URL涓虹┖锛屾棤娉曚繚瀛�');
+ ElMessage.error('鏂囦欢URL鑾峰彇澶辫触锛屾棤娉曚繚瀛橀檮浠�');
+ return;
+ }
+
+ const res = await addDocumentationFile(attachmentData);
+ console.log('淇濆瓨闄勪欢鎺ュ彛鍝嶅簲:', res);
+
+ if (res.code === 200) {
+ const newFile = {
+ id: res.data.id || Date.now(),
+ name: attachmentData.name,
+ size: attachmentData.fileSize,
+ url: attachmentData.url,
+ uploadTime: new Date().toISOString(),
+ status: 'success',
+ uid: file.uid
+ }
+
+ console.log('鍒涘缓鐨勬柊鏂囦欢瀵硅薄:', newFile);
+ fileList.push(newFile)
+ ElMessage.success('鏂囦欢涓婁紶骞朵繚瀛樻垚鍔�')
+
+ // 淇濆瓨鎴愬姛鍚庡埛鏂伴檮浠跺垪琛�
+ if (currentDocumentId.value) {
+ await loadAttachmentList(currentDocumentId.value);
+ }
+ } else {
+ ElMessage.error(res.msg || '淇濆瓨闄勪欢淇℃伅澶辫触')
+ // 淇濆瓨澶辫触鏃剁Щ闄ゆ枃浠�
+ const index = fileList.findIndex(item => item.uid === file.uid)
+ if (index > -1) {
+ fileList.splice(index, 1)
+ }
+ }
+ } catch (error) {
+ console.error('淇濆瓨闄勪欢澶辫触:', error)
+ ElMessage.error('淇濆瓨闄勪欢淇℃伅澶辫触')
+ // 淇濆瓨澶辫触鏃剁Щ闄ゆ枃浠�
+ const index = fileList.findIndex(item => item.uid === file.uid)
+ if (index > -1) {
+ fileList.splice(index, 1)
+ }
+ }
+}
+
+// 鏂囦欢涓婁紶澶辫触
+const handleUploadError = (error, file, fileList) => {
+ console.error('鏂囦欢涓婁紶澶辫触:', error);
+ console.error('澶辫触鐨勬枃浠�:', file);
+ console.error('褰撳墠鏂囦欢鍒楄〃:', fileList);
+
+ ElMessage.error('鏂囦欢涓婁紶澶辫触锛岃妫�鏌ョ綉缁滆繛鎺ユ垨鏂囦欢鏍煎紡')
+}
+
+// 绉婚櫎鏂囦欢
+const handleRemove = (file, fileList) => {
+ const index = fileList.findIndex(item => item.uid === file.uid)
+ if (index > -1) {
+ fileList.splice(index, 1)
+ // total.value = fileList.length // Removed total.value
+ }
+}
+
+// 鍒犻櫎鏂囦欢
+const removeFile = (file) => {
+ ElMessageBox.confirm(`纭畾瑕佸垹闄ゆ枃浠� "${file.name}" 鍚楋紵`, '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(async () => {
+ try {
+ // 璋冪敤鍒犻櫎鎺ュ彛
+ const res = await deleteDocumentationFile([file.id]);
+ if (res.code === 200) {
+ // 浠庢湰鍦板垪琛ㄤ腑绉婚櫎
+ const index = fileList.value.findIndex(item => item.id === file.id);
+ if (index > -1) {
+ fileList.value.splice(index, 1);
+ }
+ ElMessage.success('鍒犻櫎鎴愬姛');
+
+ // 濡傛灉鏈夋枃妗D锛屽埛鏂伴檮浠跺垪琛�
+ if (currentDocumentId.value) {
+ await loadAttachmentList(currentDocumentId.value);
+ }
+ } else {
+ ElMessage.error(res.msg || '鍒犻櫎澶辫触');
+ }
+ } catch (error) {
+ console.error('鍒犻櫎闄勪欢澶辫触:', error);
+ ElMessage.error('鍒犻櫎闄勪欢澶辫触');
+ }
+ }).catch(() => {
+ // 鍙栨秷鍒犻櫎
+ })
+}
+
+// 棰勮鏂囦欢
+const previewFile = (file) => {
+ if (file.url) {
+ filePreviewRef.value.open(file.url)
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�')
+ }
+}
+
+// 涓嬭浇鏂囦欢
+const downloadFile = (file) => {
+ if (file.url) {
+ // 鍒涘缓涓嬭浇閾炬帴
+ const link = document.createElement('a')
+ link.href = file.url
+ link.download = file.name
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ ElMessage.success('寮�濮嬩笅杞芥枃浠�')
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曚笅杞�')
+ }
+}
+
+// 鏍煎紡鍖栨枃浠跺ぇ灏�
+const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 B'
+ const k = 1024
+ const sizes = ['B', 'KB', 'MB', 'GB']
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+}
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (dateString) => {
+ if (!dateString) return ''
+ const date = new Date(dateString)
+ return date.toLocaleString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ })
+}
+
+// 娴嬭瘯鏂囦欢涓婁紶
+const testUpload = () => {
+ console.log('褰撳墠鏂囨。ID:', currentDocumentId.value);
+ console.log('涓婁紶URL:', uploadUrl);
+ console.log('涓婁紶Headers:', uploadHeaders.value);
+}
+
+// 鏆撮湶鏂规硶
+defineExpose({
+ open,
+ loadAttachmentList,
+ testUpload
+})
+</script>
+
+<style scoped>
+.attachment-manager {
+ padding: 20px;
+}
+
+.upload-section {
+ margin-bottom: 20px;
+ padding: 20px;
+ background-color: #f8f9fa;
+ border-radius: 8px;
+ border: 2px dashed #d9d9d9;
+}
+
+.upload-section:hover {
+ border-color: #409eff;
+}
+
+.attachment-list {
+ margin-bottom: 20px;
+}
+
+.el-upload__tip {
+ margin-top: 10px;
+ color: #666;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+:deep(.el-upload) {
+ width: 100%;
+}
+
+:deep(.el-upload-dragger) {
+ width: 100%;
+ height: 120px;
+}
+</style>
diff --git a/src/views/fileManagement/document/index.vue b/src/views/fileManagement/document/index.vue
new file mode 100644
index 0000000..f4eac2d
--- /dev/null
+++ b/src/views/fileManagement/document/index.vue
@@ -0,0 +1,1418 @@
+<template>
+ <div class="app-container document-view">
+ <div class="left">
+ <div>
+ <el-input
+ v-model="search"
+ style="width: 210px"
+ placeholder="杈撳叆鍏抽敭瀛楄繘琛屾悳绱�"
+ @change="searchFilter"
+ @clear="searchFilter"
+ clearable
+ prefix-icon="Search"
+ />
+ <el-button
+ type="primary"
+ @click="openCategoryDia('addOne')"
+ style="margin-left: 10px"
+ >鏂板鍒嗙被</el-button
+ >
+ </div>
+ <div ref="containerRef">
+ <el-tree
+ ref="tree"
+ v-loading="treeLoad"
+ :data="categoryList"
+ @node-click="handleNodeClick"
+ :expand-on-click-node="false"
+ default-expand-all
+ :default-expanded-keys="expandedKeys"
+ :draggable="true"
+ :filter-node-method="filterNode"
+ :props="{ children: 'children', label: 'category' }"
+ highlight-current
+ node-key="id"
+ style="
+ height: calc(100vh - 190px);
+ overflow-y: scroll;
+ scrollbar-width: none;
+ margin-top: 10px;
+ "
+ >
+ <template #default="{ node, data }">
+ <div class="custom-tree-node">
+ <span class="tree-node-content">
+ <el-icon class="orange-icon">
+ <component :is="data.children && data.children.length > 0
+ ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
+ </el-icon>
+ {{ data.category }}
+ </span>
+ <div>
+ <el-button
+ type="primary"
+ link
+ @click="openCategoryDia('edit', data)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="primary"
+ link
+ @click="openCategoryDia('addSub', data)"
+ v-if="node.level < 2"
+ >
+ 娣诲姞瀛愬垎绫�
+ </el-button>
+ <el-button
+ v-if="!node.childNodes.length"
+ style="margin-left: 4px"
+ type="danger"
+ link
+ @click="removeCategory(node, data)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ </template>
+ </el-tree>
+ </div>
+ </div>
+ <div class="right">
+ <div style="margin-bottom: 10px" v-if="isShowButton">
+ <el-button type="primary" @click="openDocumentDia('add')">
+ 鏂板鏂囨。
+ </el-button>
+ <el-button
+ type="danger"
+ @click="handleDelete"
+ style="margin-left: 10px"
+ plain
+ :disabled="selectedRows.length === 0"
+ >
+ 鍒犻櫎 ({{ selectedRows.length }})
+ </el-button>
+ </div>
+ <div class="table-container">
+
+ <!-- PIMTable 缁勪欢 -->
+ <PIMTable
+ :table-data="documentList"
+ :column="tableColumns"
+ :is-selection="true"
+ :border="true"
+ :table-loading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="handlePagination"
+ />
+ </div>
+ </div>
+
+ <!-- 鍒嗙被鏂板/淇敼瀵硅瘽妗� -->
+ <el-dialog v-model="categoryDia" title="鍒嗙被" width="400px" @keydown.enter.prevent>
+ <el-form
+ :model="categoryForm"
+ label-width="140px"
+ label-position="top"
+ :rules="categoryRules"
+ ref="categoryFormRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="24" v-if="categoryOperationType === 'addSub'">
+ <el-form-item label="鐖跺垎绫伙細" prop="parentName">
+ <el-input
+ v-model="categoryForm.parentName"
+ placeholder="鐖跺垎绫诲悕绉�"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鍒嗙被鍚嶇О锛�" prop="category">
+ <el-input
+ v-model="categoryForm.category"
+ placeholder="璇疯緭鍏ュ垎绫诲悕绉�"
+ clearable
+ @keydown.enter.prevent
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitCategoryForm">纭</el-button>
+ <el-button @click="closeCategoryDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+<el-dialog
+ v-model="qrCodeDialogVisible"
+ title="鏂囨。浜岀淮鐮�"
+ width="400px"
+ @close="closeQrCodeDialog"
+ >
+ <div class="qr-code-container">
+ <div v-if="qrCodeUrl" class="qr-code-image">
+ <img :src="qrCodeUrl" alt="鏂囨。浜岀淮鐮�" class="qr-image" />
+ <div class="qr-info">
+ <p><strong>鏂囨。鍚嶇О锛�</strong>{{ currentDocument.docName }}</p>
+ <p><strong>鏂囨。缂栧彿锛�</strong>{{ currentDocument.docNumber }}</p>
+ </div>
+ </div>
+ <div v-else class="qr-loading">
+ <el-icon class="is-loading"><Loading /></el-icon>
+ <p>姝e湪鐢熸垚浜岀淮鐮�...</p>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeQrCodeDialog">鍏抽棴</el-button>
+ <el-button
+ v-if="qrCodeUrl"
+ type="primary"
+ @click="downloadQRCode"
+ icon="Download"
+ >
+ 涓嬭浇浜岀淮鐮�
+ </el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 鏂囨。鏂板/淇敼瀵硅瘽妗� -->
+ <el-dialog
+ v-model="documentDia"
+ :title="documentOperationType === 'add' ? '鏂板鏂囨。' : '缂栬緫鏂囨。'"
+ width="600px"
+ @close="closeDocumentDia"
+ @keydown.enter.prevent
+ >
+ <el-form
+ :model="documentForm"
+ label-width="140px"
+ label-position="top"
+ :rules="documentRules"
+ ref="documentFormRef"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂囨。鍚嶇О锛�" prop="docName">
+ <el-input v-model="documentForm.docName" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="骞村害锛�" prop="year">
+ <el-date-picker
+ v-model="documentForm.year"
+ type="year"
+ value-format="YYYY"
+ format="YYYY"
+ placeholder="閫夋嫨骞村害"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂囨。缂栧彿锛�" prop="docNumber">
+ <el-input v-model="documentForm.docNumber" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐d换浜猴細" prop="responsiblePerson">
+ <el-input v-model="documentForm.responsiblePerson" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂囨。鍒嗙被锛�" prop="documentClassificationId">
+ <el-select v-model="documentForm.documentClassificationId" placeholder="璇烽�夋嫨鏂囨。鍒嗙被" style="width: 100%">
+ <el-option
+ v-for="item in categoryList"
+ :key="item.id"
+ :label="item.category"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏂囨。鏀剧疆浣嶇疆锛�" prop="warehouseGoodsShelvesRowcolId">
+ <el-tree-select
+ v-model="documentForm.warehouseGoodsShelvesRowcolId"
+ :data="locationTree"
+ placeholder="璇烽�夋嫨鏂囦欢鏀剧疆浣嶇疆"
+ clearable
+ check-strictly
+ :render-after-expand="false"
+ :props="{ children: 'children', label: 'label', value: 'value' }"
+ style="width: 100%"
+ @change="handleLocationChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂囨。鏃ユ湡锛�" prop="docData">
+ <el-date-picker
+ v-model="documentForm.docData"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="閫夋嫨鏃ユ湡"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="淇濈鏈熼檺锛�" prop="retentionPeriod">
+ <el-select v-model="documentForm.retentionPeriod" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in retention_period"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="淇濆瘑绾у埆锛�" prop="securityLevel">
+ <el-select v-model="documentForm.securityLevel" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in confidentiality_level"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍒嗘暟锛�" prop="copyCount">
+ <el-input v-model="documentForm.copyCount" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="椤垫暟锛�" prop="pageCount">
+ <el-input v-model="documentForm.pageCount" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏂囨。绫诲埆锛�" prop="docCategory">
+ <el-select v-model="documentForm.docCategory" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in document_type"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂囨。绉嶇被锛�" prop="docType">
+ <el-select v-model="documentForm.docType" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in document_categories"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绱ф�ョ▼搴︼細" prop="urgencyLevel">
+ <el-select v-model="documentForm.urgencyLevel" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in document_urgency"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂囨。鐘舵�侊細" prop="docStatus">
+ <el-select v-model="documentForm.docStatus" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="item in document_status"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞锛�" prop="remark">
+ <el-input
+ v-model="documentForm.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitDocumentForm">纭</el-button>
+ <el-button @click="closeDocumentDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <AttachmentManager ref="attachmentManagerRef" />
+ </div>
+ </template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance, toRefs, watch } from "vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import { ArrowRight, Folder, FolderOpened, Tickets, Document } from '@element-plus/icons-vue';
+import PIMTable from '@/components/PIMTable/PIMTable.vue';
+import { getToken } from "@/utils/auth";
+import { getCategoryTree, addCategory, updateCategory, deleteCategory, getDocumentList, addDocument, updateDocument, deleteDocument, getDocumentDetail, searchDocument, getWarehouseStructure } from '@/api/fileManagement/document'
+import { getWarehouseList } from '@/api/fileManagement/bookshelf'
+import AttachmentManager from './attachmentManager.vue'
+import { useDict } from '@/utils/dict'
+
+const { proxy } = getCurrentInstance();
+const tree = ref(null);
+const containerRef = ref(null);
+// 瀵煎叆qrcode搴�
+import QRCode from 'qrcode'
+import { Loading, Download } from '@element-plus/icons-vue'
+// 浣跨敤瀛楀吀鏁版嵁
+const { confidentiality_level, document_urgency, document_status, document_type, document_categories, retention_period } = useDict('confidentiality_level', 'document_urgency', 'document_status', 'document_type', 'document_categories', 'retention_period')
+
+// 鐩戝惉瀛楀吀鏁版嵁鍙樺寲
+watch([confidentiality_level, document_urgency, document_status, document_type, document_categories, retention_period], () => {
+ // 瀛楀吀鏁版嵁宸叉洿鏂�
+}, { immediate: true, deep: true });
+
+const categoryDia = ref(false);
+const documentDia = ref(false);
+const categoryOperationType = ref("");
+const documentOperationType = ref("");
+const search = ref("");
+const currentId = ref("");
+const currentParentId = ref("");
+const treeLoad = ref(false);
+const categoryList = ref([]);
+const expandedKeys = ref([]);
+const documentList = ref([]);
+const isShowButton = ref(false);
+const selectedRows = ref([]);
+const selectAll = ref(false);
+const isIndeterminate = ref(false);
+const tableLoading = ref(false);
+const attachmentManagerRef = ref(null);
+
+// 鏂囦欢涓婁紶閰嶇疆
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
+ headers: { Authorization: "Bearer " + getToken() },
+});
+
+// 浣嶇疆鏍戞暟鎹�
+const locationTree = ref([]);
+
+// 浜岀淮鐮佺浉鍏冲彉閲�
+const qrCodeDialogVisible = ref(false)
+const qrCodeUrl = ref('')
+const currentDocument = ref({})
+// 琛ㄦ牸鍒楅厤缃�
+const tableColumns = ref([
+ { label: '鏂囨。鍚嶇О', prop: 'docName', width: '200' },
+ { label: '鏂囨。缂栧彿', prop: 'docNumber', width: '120' },
+ { label: '骞村害', prop: 'year', width: '80' },
+ { label: '璐d换浜�', prop: 'responsiblePerson', width: '100' },
+ {
+ label: '鏂囨。鏀剧疆浣嶇疆',
+ prop: 'warehouseGoodsShelvesRowcolId',
+ width: '150',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ return getLocationName(params);
+ }
+ },
+ { label: '鏂囨。鏃ユ湡', prop: 'docData', width: '120' },
+ {
+ label: '淇濈鏈熼檺',
+ prop: 'retentionPeriod',
+ width: '100',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ if (!retention_period.value || retention_period.value.length === 0) {
+ return params;
+ }
+ const item = retention_period.value.find(item => item.value == params);
+ return item ? item.label : params;
+ },
+ formatType: (params) => {
+ if (params === null || params === undefined || params === '') return 'info';
+ if (!retention_period.value || retention_period.value.length === 0) {
+ return 'info';
+ }
+ const item = retention_period.value.find(item => item.value == params);
+ const validTypes = ['success', 'warning', 'danger', 'info'];
+ return item && validTypes.includes(item.elTagType) ? item.elTagType : 'info';
+ }
+ },
+ {
+ label: '淇濆瘑绾у埆',
+ prop: 'securityLevel',
+ width: '80',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ if (!confidentiality_level.value || confidentiality_level.value.length === 0) {
+ return params;
+ }
+ const item = confidentiality_level.value.find(item => item.value == params);
+ return item ? item.label : params;
+ },
+ formatType: (params) => {
+ if (params === null || params === undefined || params === '') return 'info';
+ if (!confidentiality_level.value || confidentiality_level.value.length === 0) {
+ return 'info';
+ }
+ const item = confidentiality_level.value.find(item => item.value == params);
+ const validTypes = ['success', 'warning', 'danger', 'info'];
+ return item && validTypes.includes(item.elTagType) ? item.elTagType : 'info';
+ }
+ },
+ { label: '鍒嗘暟', prop: 'copyCount', width: '80' },
+ { label: '椤垫暟', prop: 'pageCount', width: '80' },
+ {
+ label: '鏂囨。绫诲埆',
+ prop: 'docCategory',
+ width: '100',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ if (!document_type.value || document_type.value.length === 0) {
+ return params;
+ }
+ const item = document_type.value.find(item => item.value == params);
+ return item ? item.label : params;
+ },
+ formatType: (params) => {
+ if (params === null || params === undefined || params === '') return 'info';
+ if (!document_type.value || document_type.value.length === 0) {
+ return 'info';
+ }
+ const item = document_type.value.find(item => item.value == params);
+ const validTypes = ['success', 'warning', 'danger', 'info'];
+ return item && validTypes.includes(item.elTagType) ? item.elTagType : 'info';
+ }
+ },
+ {
+ label: '鏂囨。绉嶇被',
+ prop: 'docType',
+ width: '100',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ if (!document_categories.value || document_categories.value.length === 0) {
+ return params;
+ }
+ const item = document_categories.value.find(item => item.value == params);
+ return item ? item.label : params;
+ },
+ formatType: (params) => {
+ if (params === null || params === undefined || params === '') return 'info';
+ if (!document_categories.value || document_categories.value.length === 0) {
+ return 'info';
+ }
+ const item = document_categories.value.find(item => item.value == params);
+ const validTypes = ['success', 'warning', 'danger', 'info'];
+ return item && validTypes.includes(item.elTagType) ? item.elTagType : 'info';
+ }
+ },
+ {
+ label: '绱ф�ョ▼搴�',
+ prop: 'urgencyLevel',
+ width: '100',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ if (!document_urgency.value || document_urgency.value.length === 0) {
+ return params;
+ }
+ const item = document_urgency.value.find(item => item.value == params);
+ return item ? item.label : params;
+ },
+ formatType: (params) => {
+ if (params === null || params === undefined || params === '') return 'info';
+ if (!document_urgency.value || document_urgency.value.length === 0) {
+ return 'info';
+ }
+ const item = document_urgency.value.find(item => item.value == params);
+ const validTypes = ['success', 'warning', 'danger', 'info'];
+ return item && validTypes.includes(item.elTagType) ? item.elTagType : 'info';
+ }
+ },
+ {
+ label: '鏂囨。鐘舵��',
+ prop: 'docStatus',
+ width: '100',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ if (!document_status.value || document_status.value.length === 0) {
+ return params;
+ }
+ const item = document_status.value.find(item => item.value == params);
+ return item ? item.label : params;
+ },
+ formatType: (params) => {
+ if (params === null || params === undefined || params === '') return 'info';
+ if (!document_status.value || document_status.value.length === 0) {
+ return 'info';
+ }
+ const item = document_status.value.find(item => item.value == params);
+ const validTypes = ['success', 'warning', 'danger', 'info'];
+ return item && validTypes.includes(item.elTagType) ? item.elTagType : 'info';
+ }
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: '200',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openDocumentDia('edit', row)
+ },
+ },
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: (row) => {
+ openAttachment(row)
+ },
+ },
+ {
+ name: "鐢熸垚浜岀淮鐮�",
+ type: "text",
+ clickFun: (row) => {
+ generateQRCode(row)
+ },
+ },
+ ],
+ }
+]);
+// 鐢熸垚浜岀淮鐮�
+const generateQRCode = async (row) => {
+ try {
+ // 妫�鏌ュ繀瑕佸瓧娈�
+ if (!row.docName || !row.docNumber) {
+ ElMessage.warning('鏂囨。淇℃伅涓嶅畬鏁达紝鏃犳硶鐢熸垚浜岀淮鐮�')
+ return
+ }
+
+ currentDocument.value = row
+ qrCodeUrl.value = ''
+ qrCodeDialogVisible.value = true
+
+ // 鏋勫缓浜岀淮鐮佸唴瀹�
+ // const qrContent = `${row.id}|${row.docName}|${row.docNumber}`
+ const qrContent = `${row.id}`
+ // 鐢熸垚浜岀淮鐮�
+ qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
+ width: 256,
+ margin: 2,
+ color: {
+ dark: '#000000',
+ light: '#FFFFFF'
+ },
+ errorCorrectionLevel: 'M'
+ })
+
+ // ElMessage.success('浜岀淮鐮佺敓鎴愭垚鍔燂紒')
+
+ } catch (error) {
+ console.error('鐢熸垚浜岀淮鐮佸け璐�:', error)
+ ElMessage.error('鐢熸垚浜岀淮鐮佸け璐ワ細' + error.message)
+ qrCodeDialogVisible.value = false
+ }
+}
+
+// 涓嬭浇浜岀淮鐮�
+const downloadQRCode = () => {
+ if (!qrCodeUrl.value) {
+ ElMessage.warning('璇峰厛鐢熸垚浜岀淮鐮�')
+ return
+ }
+
+ const a = document.createElement('a')
+ a.href = qrCodeUrl.value
+ a.download = `${currentDocument.value.docName}_浜岀淮鐮乢${new Date().getTime()}.png`
+ document.body.appendChild(a)
+ a.click()
+ document.body.removeChild(a)
+ ElMessage.success('涓嬭浇鎴愬姛锛�')
+}
+
+// 鍏抽棴浜岀淮鐮佸脊绐�
+const closeQrCodeDialog = () => {
+ qrCodeDialogVisible.value = false
+ qrCodeUrl.value = ''
+ currentDocument.value = {}
+}
+// 鍒嗙被琛ㄥ崟
+const categoryForm = reactive({
+ category: "",
+ parentId: "",
+ parentName: "",
+});
+
+const categoryRules = reactive({
+ category: [{ required: true, message: "璇疯緭鍏ュ垎绫诲悕绉�", trigger: "blur" }],
+});
+
+// 鏂囨。琛ㄥ崟
+const documentForm = reactive({
+ id: "",
+ documentClassificationId: "",
+ docName: "",
+ docNumber: "",
+ year: "",
+ responsiblePerson: "",
+ warehouseGoodsShelvesRowcolId: "",
+ docData: "",
+ retentionPeriod: "",
+ securityLevel: "",
+ copyCount: "",
+ pageCount: "",
+ docCategory: "",
+ docType: "",
+ urgencyLevel: "",
+ docStatus: "",
+ remark: "",
+ attachments: [], // 鏂板闄勪欢鏁扮粍
+});
+
+const documentRules = reactive({
+ docName: [{ required: true, message: "璇疯緭鍏ユ枃妗e悕绉�", trigger: "blur" }],
+ docNumber: [{ required: true, message: "璇疯緭鍏ユ枃妗g紪鍙�", trigger: "blur" }],
+ year: [{ required: true, message: "璇烽�夋嫨骞村害", trigger: "change" }],
+ documentClassificationId: [{ required: true, message: "璇烽�夋嫨鏂囨。鍒嗙被", trigger: "change" }],
+ warehouseGoodsShelvesRowcolId: [{ required: true, message: "璇烽�夋嫨鏂囨。鏀剧疆浣嶇疆", trigger: "change" }],
+});
+
+// 鍒嗛〉鐩稿叧
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+// 鍒濆鍖栧垎绫绘爲鏁版嵁
+const initCategoryTree = async() => {
+ try {
+ treeLoad.value = true;
+ const res = await getCategoryTree();
+ if (res.code === 200) {
+ categoryList.value = res.data || [];
+
+ // 璁剧疆灞曞紑鐨勮妭鐐�
+ expandedKeys.value = [];
+ categoryList.value.forEach((item) => {
+ if (item.id) {
+ expandedKeys.value.push(item.id);
+ }
+ });
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇鍒嗙被鏍戝け璐�");
+ }
+ } catch (error) {
+ ElMessage.error("鑾峰彇鍒嗙被鏍戝け璐ワ紝璇烽噸璇�");
+ } finally {
+ treeLoad.value = false;
+ }
+};
+
+// 鍒濆鍖栦粨搴撲綅缃暟鎹�
+const initLocationTree = async() => {
+ try {
+ const res = await getWarehouseList();
+ if (res.code === 200) {
+ // 杞崲鏁版嵁鏍煎紡锛岄�傞厤el-tree-select缁勪欢
+ locationTree.value = transformWarehouseData(res.data || []);
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇浠撳簱浣嶇疆澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("鑾峰彇浠撳簱浣嶇疆澶辫触锛岃閲嶈瘯");
+ }
+};
+
+// 杞崲浠撳簱鏁版嵁鏍煎紡
+const transformWarehouseData = (data) => {
+ return data.map(item => ({
+ id: item.id,
+ label: item.name || item.warehouseName || item.label,
+ value: item.id,
+ children: item.children ? transformWarehouseData(item.children) : []
+ }));
+};
+
+// 鏍规嵁ID鑾峰彇浣嶇疆鍚嶇О
+const getLocationName = (locationId) => {
+ if (!locationId || !locationTree.value || locationTree.value.length === 0) {
+ return locationId || '-';
+ }
+
+ const findLocation = (tree, id) => {
+ for (let item of tree) {
+ if (item.value === locationId || item.id === locationId) {
+ return item.label;
+ }
+ if (item.children && item.children.length > 0) {
+ const result = findLocation(item.children, id);
+ if (result) return result;
+ }
+ }
+ return null;
+ };
+
+ const locationName = findLocation(locationTree.value, locationId);
+ return locationName || locationId;
+};
+
+// 杩囨护鍒嗙被鏍�
+const searchFilter = () => {
+ if (proxy.$refs.tree) {
+ proxy.$refs.tree.filter(search.value);
+ }
+};
+
+// 鎵撳紑鍒嗙被寮规
+const openCategoryDia = (type, data) => {
+ categoryOperationType.value = type;
+ categoryDia.value = true;
+ categoryForm.category = "";
+ categoryForm.parentId ="";
+ categoryForm.parentName = "";
+
+ if (type === "edit") {
+ categoryForm.category = data.category;
+ // 淇濆瓨褰撳墠缂栬緫鐨勫垎绫籌D
+ currentId.value = data.id;
+ } else if (type === "addSub") {
+ categoryForm.parentId = data.id;
+ categoryForm.parentName = data.category;
+ }
+};
+
+// 鎵撳紑鏂囨。寮规
+const openDocumentDia = (type, data) => {
+ documentOperationType.value = type;
+ documentDia.value = true;
+
+ if (type === "edit") {
+ // 缂栬緫妯″紡锛屽姞杞界幇鏈夋暟鎹�
+ Object.assign(documentForm, data);
+ documentForm.retentionPeriod = String(documentForm.retentionPeriod)
+ documentForm.securityLevel = String(documentForm.securityLevel)
+ documentForm.docCategory = String(documentForm.docCategory)
+ documentForm.docType = String(documentForm.docType)
+ documentForm.urgencyLevel = String(documentForm.urgencyLevel)
+ documentForm.docStatus = String(documentForm.docStatus)
+
+ // 鍔犺浇闄勪欢淇℃伅
+ if (data.attachments) {
+ documentForm.attachments = [...data.attachments];
+ } else {
+ documentForm.attachments = [];
+ }
+ } else {
+ // 鏂板妯″紡锛屾竻绌鸿〃鍗�
+ Object.keys(documentForm).forEach(key => {
+ documentForm[key] = "";
+ });
+ documentForm.attachments = []; // 鏂板妯″紡涓嬩篃娓呯┖闄勪欢
+ // 璁剧疆榛樿鍊� - 鏂囨。鐘舵�侀粯璁よ缃负"姝e父"
+ if (document_status.value && document_status.value.length > 0) {
+ const normalStatus = document_status.value.find(item => item.label === '姝e父');
+ documentForm.docStatus = normalStatus ? normalStatus.value : document_status.value[0].value;
+ }
+ if (document_urgency.value && document_urgency.value.length > 0) {
+ const normalUrgency = document_urgency.value.find(item => item.label === '鏅��');
+ documentForm.urgencyLevel = normalUrgency ? normalUrgency.value : document_urgency.value[0].value;
+ }
+ }
+};
+
+// 鎻愪氦鍒嗙被琛ㄥ崟
+const submitCategoryForm = () => {
+ proxy.$refs.categoryFormRef.validate(async (valid) => {
+ if (valid) {
+ try {
+ if (categoryOperationType.value === "addSub") {
+ // 娣诲姞瀛愬垎绫�
+ const res = await addCategory({
+ category: categoryForm.category,
+ parentId: categoryForm.parentId
+ });
+ if (res.code === 200) {
+ ElMessage.success("娣诲姞瀛愬垎绫绘垚鍔�");
+ // 閲嶆柊鍔犺浇鍒嗙被鏍�
+ await initCategoryTree();
+ } else {
+ ElMessage.error(res.msg || "娣诲姞瀛愬垎绫诲け璐�");
+ }
+ } else if (categoryOperationType.value === "edit") {
+ // 缂栬緫鍒嗙被
+ const res = await updateCategory({
+ id: currentId.value,
+ category: categoryForm.category
+ });
+ if (res.code === 200) {
+ ElMessage.success("缂栬緫鍒嗙被鎴愬姛");
+ // 閲嶆柊鍔犺浇鍒嗙被鏍�
+ await initCategoryTree();
+ } else {
+ ElMessage.error(res.msg || "缂栬緫鍒嗙被澶辫触");
+ }
+ } else {
+ // 鏂板椤剁骇鍒嗙被
+ const res = await addCategory({
+ category: categoryForm.category,
+ parentId: null
+ });
+ if (res.code === 200) {
+ ElMessage.success("鏂板鍒嗙被鎴愬姛");
+ // 閲嶆柊鍔犺浇鍒嗙被鏍�
+ await initCategoryTree();
+ } else {
+ ElMessage.error(res.msg || "鏂板鍒嗙被澶辫触");
+ }
+ }
+
+ closeCategoryDia();
+ } catch (error) {
+ ElMessage.error("鎿嶄綔澶辫触锛岃閲嶈瘯");
+ }
+ }
+ });
+};
+
+// 鍏抽棴鍒嗙被寮规
+const closeCategoryDia = () => {
+ proxy.$refs.categoryFormRef.resetFields();
+ categoryForm.parentId = "";
+ categoryForm.parentName = "";
+ categoryDia.value = false;
+};
+
+// 鍒犻櫎鍒嗙被
+const removeCategory = (node, data) => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(async () => {
+ try {
+ const res = await deleteCategory([data.id]);
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ // 閲嶆柊鍔犺浇鍒嗙被鏍�
+ await initCategoryTree();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("鍒犻櫎澶辫触锛岃閲嶈瘯");
+ }
+ })
+ .catch(() => {
+ ElMessage("宸插彇娑�");
+ });
+};
+
+// 閫夋嫨鍒嗙被
+const handleNodeClick = (val, node, el) => {
+ // 鍒ゆ柇鏄惁涓哄彾瀛愯妭鐐�
+ isShowButton.value = true;
+ // 鍙湁鍙跺瓙鑺傜偣鎵嶆墽琛屼互涓嬮�昏緫
+ currentId.value = val.id;
+ currentParentId.value = val.parentId;
+
+ // 娓呯┖閫夋嫨鐘舵��
+ selectedRows.value = [];
+ selectAll.value = false;
+ isIndeterminate.value = false;
+
+ // 閲嶇疆鍒嗛〉
+ pagination.currentPage = 1;
+ pagination.total = 0;
+
+ // 鍔犺浇鏂囨。鍒楄〃
+ if (isShowButton.value) {
+ loadDocumentList();
+ } else {
+ // 濡傛灉涓嶆槸鍙跺瓙鑺傜偣锛屾竻绌烘枃妗e垪琛�
+ documentList.value = [];
+ }
+};
+
+// 鎻愪氦鏂囨。琛ㄥ崟
+const submitDocumentForm = () => {
+ proxy.$refs.documentFormRef.validate(async (valid) => {
+ if (valid) {
+ try {
+ // 鏋勫缓鎻愪氦鏁版嵁
+ const submitData = {
+ ...documentForm,
+ // 璁剧疆褰撳墠閫変腑鐨勫垎绫籌D
+ documentClassificationId: currentId.value || documentForm.documentClassificationId,
+ // 娣诲姞闄勪欢淇℃伅
+ // attachments: documentForm.attachments
+ };
+
+ if (documentOperationType.value === "edit") {
+ // 缂栬緫妯″紡锛屾洿鏂扮幇鏈夋暟鎹�
+ const res = await updateDocument(submitData);
+ if (res.code === 200) {
+ ElMessage.success("缂栬緫鎴愬姛");
+ // 閲嶆柊鍔犺浇鏂囨。鍒楄〃
+ await loadDocumentList();
+ // 鍒锋柊闄勪欢鍒楄〃
+ if (attachmentManagerRef.value && documentForm.id) {
+ attachmentManagerRef.value.loadAttachmentList(documentForm.id);
+ }
+ } else {
+ ElMessage.error(res.msg || "缂栬緫澶辫触");
+ }
+ } else {
+ // 鏂板妯″紡锛屾坊鍔犳柊鏁版嵁
+ const res = await addDocument(submitData);
+ if (res.code === 200) {
+ ElMessage.success("鏂板鎴愬姛");
+ // 閲嶆柊鍔犺浇鏂囨。鍒楄〃
+ await loadDocumentList();
+ // 鍒锋柊闄勪欢鍒楄〃
+ if (attachmentManagerRef.value && res.data && res.data.id) {
+ attachmentManagerRef.value.loadAttachmentList(res.data.id);
+ }
+ } else {
+ ElMessage.error(res.msg || "鏂板澶辫触");
+ }
+ }
+ closeDocumentDia();
+ } catch (error) {
+ ElMessage.error("鎿嶄綔澶辫触锛岃閲嶈瘯");
+ }
+ }
+ });
+};
+
+// 鍏抽棴鏂囨。寮规
+const closeDocumentDia = () => {
+ proxy.$refs.documentFormRef.resetFields();
+ documentDia.value = false;
+ // 娓呯┖琛ㄥ崟鏁版嵁
+ Object.keys(documentForm).forEach(key => {
+ documentForm[key] = "";
+ });
+ documentForm.attachments = []; // 鍏抽棴寮规鏃朵篃娓呯┖闄勪欢
+};
+
+// 澶勭悊浣嶇疆閫夋嫨鍙樺寲
+const handleLocationChange = (value) => {
+ if (value) {
+ // 妫�鏌ラ�夋嫨鐨勬槸鍚︿负鍙跺瓙鑺傜偣
+ const isLeafNode = checkIfLeafNode(locationTree.value, value);
+ if (!isLeafNode) {
+ ElMessage.warning("璇烽�夋嫨鏈�搴曞眰鐨勪綅缃紙濡傦細鏌滃眰锛�");
+ documentForm.warehouseGoodsShelvesRowcolId = "";
+ return;
+ }
+ }
+};
+
+// 妫�鏌ユ槸鍚︿负鍙跺瓙鑺傜偣
+const checkIfLeafNode = (tree, value) => {
+ for (let item of tree) {
+ if (item.value === value || item.id === value) {
+ // 濡傛灉娌℃湁瀛愯妭鐐癸紝鍒欎负鍙跺瓙鑺傜偣
+ return !item.children || item.children.length === 0;
+ }
+ if (item.children && item.children.length > 0) {
+ const result = checkIfLeafNode(item.children, value);
+ if (result !== null) {
+ return result;
+ }
+ }
+ }
+ return null;
+};
+
+// 鍒犻櫎鏂囨。
+const handleDelete = () => {
+ if (selectedRows.value.length > 0) {
+ ElMessageBox.confirm(`纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉¤褰曞悧锛焋, "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(async () => {
+ try {
+ const selectedIds = selectedRows.value.map(row => row.id);
+ const res = await deleteDocument(selectedIds);
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ // 閲嶆柊鍔犺浇鏂囨。鍒楄〃
+ await loadDocumentList();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("鍒犻櫎澶辫触锛岃閲嶈瘯");
+ }
+ })
+ .catch(() => {
+ ElMessage("宸插彇娑�");
+ });
+ } else {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁");
+ }
+};
+
+// PIMTable 閫夋嫨鍙樺寲浜嬩欢
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+
+ // 鏇存柊鍏ㄩ�夌姸鎬�
+ const selectedCount = selection.length;
+ const totalCount = documentList.value.length;
+
+ if (selectedCount === 0) {
+ selectAll.value = false;
+ isIndeterminate.value = false;
+ } else if (selectedCount === totalCount) {
+ selectAll.value = true;
+ isIndeterminate.value = false;
+ } else {
+ selectAll.value = false;
+ isIndeterminate.value = true;
+ }
+};
+
+// 鍔犺浇鏂囨。鍒楄〃
+const loadDocumentList = async () => {
+ try {
+ tableLoading.value = true;
+
+ // 鏋勫缓鏌ヨ鍙傛暟
+ const query = {
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ documentClassificationId: currentId.value
+ };
+
+ const res = await getDocumentList(query);
+ if (res.code === 200) {
+ documentList.value = res.data.records || [];
+ pagination.total = res.data.total || 0;
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇鏂囨。鍒楄〃澶辫触");
+ documentList.value = [];
+ pagination.total = 0;
+ }
+
+ // 閲嶇疆閫夋嫨鐘舵��
+ selectedRows.value = [];
+ selectAll.value = false;
+ isIndeterminate.value = false;
+ } catch (error) {
+ ElMessage.error("鑾峰彇鏂囨。鍒楄〃澶辫触锛岃閲嶈瘯");
+ documentList.value = [];
+ pagination.total = 0;
+ } finally {
+ tableLoading.value = false;
+ }
+};
+
+// 澶勭悊鍒嗛〉鍙樺寲
+const handlePagination = (payload) => {
+ // PIMTable emit: { page, limit }
+ pagination.currentPage = payload?.page || 1;
+ pagination.pageSize = payload?.limit || pagination.pageSize;
+ loadDocumentList();
+};
+
+// 璋冪敤tree杩囨护鏂规硶
+const filterNode = (value, data, node) => {
+ if (!value) {
+ return true;
+ }
+ let val = value.toLowerCase();
+ return chooseNode(val, data, node);
+};
+
+// 杩囨护鐖惰妭鐐� / 瀛愯妭鐐�
+const chooseNode = (value, data, node) => {
+ if (data.category && data.category.toLowerCase().indexOf(value) !== -1) {
+ return true;
+ }
+ const level = node.level;
+ if (level === 1) {
+ return false;
+ }
+ let parentData = node.parent;
+ let index = 0;
+ while (index < level - 1) {
+ if (parentData.data.category && parentData.data.category.toLowerCase().indexOf(value) !== -1) {
+ return true;
+ }
+ parentData = parentData.parent;
+ index++;
+ }
+ return false;
+};
+
+// 鎵撳紑闄勪欢
+const openAttachment = (row) => {
+ attachmentManagerRef.value.open([], row.id);
+};
+
+onMounted(() => {
+ initCategoryTree();
+ initLocationTree();
+
+ // 涓嶅湪鍒濆鍖栨椂鍔犺浇鏂囨。鍒楄〃锛岀瓑寰呯敤鎴烽�夋嫨鍒嗙被鍚庡啀鍔犺浇
+});
+</script>
+
+<style scoped>
+.document-view {
+ display: flex;
+ height: 100%;
+}
+
+.left {
+ width: 380px;
+ padding: 16px;
+ background: #ffffff;
+ border-right: 1px solid #e4e7ed;
+}
+
+.right {
+ width: calc(100% - 380px);
+ padding: 16px;
+ background: #ffffff;
+}
+
+.custom-tree-node {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ padding-right: 8px;
+}
+
+.tree-node-content {
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
+.orange-icon {
+ color: orange;
+ font-size: 18px;
+ margin-right: 8px;
+}
+
+.table-container {
+ background: #ffffff;
+ border-radius: 8px;
+ overflow: hidden;
+ position: relative;
+}
+
+.add-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background-color: #f5f7fa;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ padding: 12px 16px;
+ margin-bottom: 16px;
+ border-radius: 6px;
+ border: 1px dashed #d9d9d9;
+}
+
+.add-row:hover {
+ background-color: #e4e7ed;
+ border-color: #c0c4cc;
+}
+
+.add-icon {
+ color: #909399;
+ font-size: 16px;
+}
+
+.add-row span {
+ color: #606266;
+ font-size: 14px;
+}
+
+.empty-data {
+ text-align: center;
+ color: #909399;
+ padding: 40px;
+ font-size: 14px;
+}
+
+.dialog-footer {
+ text-align: center;
+}
+
+.operation-column {
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 120px;
+ background: #ffffff;
+ border-left: 1px solid #e4e7ed;
+ z-index: 1;
+ box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1);
+}
+
+.operation-header {
+ height: 40px;
+ line-height: 40px;
+ text-align: center;
+ background: #fafafa;
+ border-bottom: 1px solid #e4e7ed;
+ font-weight: 500;
+ color: #606266;
+}
+
+.operation-cell {
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-bottom: 1px solid #e4e7ed;
+}
+
+.operation-cell:last-child {
+ border-bottom: none;
+}
+
+.attachment-section {
+ width: 100%;
+}
+
+.attachment-list {
+ margin-bottom: 10px;
+}
+
+.attachment-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 12px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ margin-bottom: 8px;
+}
+
+.file-icon {
+ margin-right: 8px;
+ color: #409eff;
+}
+
+.file-name {
+ flex: 1;
+ color: #606266;
+ font-size: 14px;
+}
+/* 浜岀淮鐮侀瑙堟牱寮� */
+.qr-code-container {
+ text-align: center;
+ padding: 20px;
+}
+
+.qr-code-image {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+}
+
+.qr-image {
+ max-width: 100%;
+ height: auto;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.qr-info {
+ text-align: left;
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 8px;
+ min-width: 300px;
+}
+
+.qr-info p {
+ margin: 8px 0;
+ color: #666;
+ font-size: 14px;
+}
+
+.qr-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 15px;
+ padding: 40px 0;
+}
+
+.qr-loading .el-icon {
+ font-size: 32px;
+ color: #409EFF;
+}
+
+.qr-loading p {
+ color: #666;
+ margin: 0;
+}
+</style>
diff --git a/src/views/fileManagement/return/index.vue b/src/views/fileManagement/return/index.vue
new file mode 100644
index 0000000..097ab29
--- /dev/null
+++ b/src/views/fileManagement/return/index.vue
@@ -0,0 +1,706 @@
+<template>
+ <div class="app-container return-view">
+ <!-- 鏌ヨ鍖哄煙 -->
+ <div class="search-container">
+ <el-form :model="searchForm" :inline="true" class="search-form">
+ <!-- <el-form-item label="鍊熼槄鐘舵�侊細">
+ <el-select v-model="searchForm.borrowStatus" placeholder="璇烽�夋嫨鍊熼槄鐘舵��" clearable style="width: 150px">
+ <el-option label="鍊熼槄" value="鍊熼槄" />
+ <el-option label="褰掕繕" value="褰掕繕" />
+ </el-select>
+ </el-form-item> -->
+ <el-form-item label="鍊熼槄浜猴細">
+ <el-input
+ v-model="searchForm.borrower"
+ placeholder="璇疯緭鍏ュ�熼槄浜�"
+ clearable
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item label="褰掕繕浜猴細">
+ <el-input
+ v-model="searchForm.returner"
+ placeholder="璇疯緭鍏ュ綊杩樹汉"
+ clearable
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item label="褰掕繕鏃ユ湡鑼冨洿锛�">
+ <el-date-picker
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 300px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">
+ <el-icon><Search /></el-icon>
+ 鏌ヨ
+ </el-button>
+ <el-button @click="handleReset">
+ <el-icon><Refresh /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ <el-form-item style="margin-left: auto;">
+ <el-button type="primary" @click="openReturnDia('add')">
+ <el-icon><Plus /></el-icon>
+ 鏂板褰掕繕
+ </el-button>
+ <el-button @click="handleOut">
+ 瀵煎嚭
+ </el-button>
+ <el-button
+ type="danger"
+ @click="handleBatchDelete"
+ :disabled="selectedRows.length === 0"
+ >
+ <el-icon><Delete /></el-icon>
+ 鎵归噺鍒犻櫎 ({{ selectedRows.length }})
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <!-- 琛ㄦ牸鍖哄煙 -->
+ <div class="table-container">
+ <PIMTable
+ :table-data="returnList"
+ :column="tableColumns"
+ :is-selection="true"
+ :border="true"
+ :table-loading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ layout: 'total, sizes, prev, pager, next, jumper'
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="handlePagination"
+ />
+ </div>
+
+ <!-- 褰掕繕鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog
+ v-model="returnDia"
+ :title="returnOperationType === 'add' ? '鏂板褰掕繕' : '缂栬緫褰掕繕'"
+ width="800px"
+ @close="closeReturnDia"
+ @keydown.enter.prevent
+ >
+ <el-form
+ :model="returnForm"
+ label-width="140px"
+ :rules="returnRules"
+ ref="returnFormRef"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂囨。锛�" prop="borrowId">
+ <div style="display: flex; gap: 10px;">
+ <el-select
+ v-if="returnOperationType !== 'edit'"
+ v-model="returnForm.borrowId"
+ placeholder="璇烽�夋嫨鏂囨。"
+ style="width: 120px;"
+ @change="handleDocumentChange"
+ >
+ <el-option
+ v-for="item in documentList"
+ :key="item.id"
+ :label="item.docName || item.name"
+ :value="item.id"
+ />
+ </el-select>
+ <el-input
+ v-else
+ v-model="currentEditDocName"
+ style="width: 120px;"
+ disabled
+ />
+ <el-input
+ v-if="returnOperationType !== 'edit'"
+ v-model="scanContent"
+ placeholder="鎵爜杈撳叆"
+ style="flex: 1;"
+ @input="handleScanContent"
+ clearable
+ />
+ </div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍊熼槄浜猴細" prop="borrower">
+ <el-input v-model="returnForm.borrower" placeholder="鍊熼槄浜哄皢鏍规嵁鏂囨。閫夋嫨鑷姩甯﹀嚭" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="褰掕繕浜猴細" prop="returner">
+ <el-input v-model="returnForm.returner" placeholder="璇疯緭鍏ュ綊杩樹汉" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰掕繕鏃ユ湡锛�" prop="returnDate">
+ <el-date-picker
+ v-model="returnForm.returnDate"
+ type="date"
+ placeholder="閫夋嫨褰掕繕鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="搴斿綊杩樻棩鏈燂細" prop="dueReturnDate">
+ <el-date-picker
+ v-model="returnForm.dueReturnDate"
+ type="date"
+ placeholder="搴斿綊杩樻棩鏈熷皢鏍规嵁鏂囨。閫夋嫨鑷姩甯﹀嚭"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞璇存槑锛�" prop="remark">
+ <el-input
+ v-model="returnForm.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉ㄨ鏄�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitReturnForm">纭</el-button>
+ <el-button @click="closeReturnDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import { Search, Refresh, Plus, Delete } from '@element-plus/icons-vue';
+import PIMTable from '@/components/PIMTable/PIMTable.vue';
+import { getReturnListPage, returnDocument, deleteReturn, getDocumentList, updateBorrow, reventUpdate,getBorrowListByDocumentationId } from '@/api/fileManagement/return';
+
+const { proxy } = getCurrentInstance();
+
+// 鍝嶅簲寮忔暟鎹�
+const returnDia = ref(false);
+const returnOperationType = ref("");
+const tableLoading = ref(false);
+const returnList = ref([]);
+const selectedRows = ref([]);
+const documentList = ref([]); // 鏂囨。鍒楄〃
+const borrowInfoList = ref([]); // 鍊熼槄淇℃伅鍒楄〃
+const scanContent = ref(); // 鎵爜鍐呭
+const currentEditDocName = ref(''); // 缂栬緫鏃跺瓨鍌ㄧ殑鏂囨。鍚嶇О
+
+// 鍒嗛〉鐩稿叧
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ borrowStatus: "",
+ borrower: "",
+ returner: "",
+ dateRange: []
+});
+
+// 褰掕繕琛ㄥ崟
+const returnForm = reactive({
+ id: "",
+ borrowId: "",
+ borrower: "",
+ returner: "",
+ borrowStatus: "",
+ returnDate: "",
+ dueReturnDate: "",
+ remark: ""
+});
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const returnRules = reactive({
+ borrowId: [{ required: true, message: "璇烽�夋嫨鏂囨。", trigger: "change" }],
+ returner: [{ required: true, message: "璇疯緭鍏ュ綊杩樹汉", trigger: "blur" }],
+ returnDate: [{ required: true, message: "璇烽�夋嫨褰掕繕鏃ユ湡", trigger: "change" }]
+});
+
+// 琛ㄦ牸鍒楅厤缃�
+const tableColumns = ref([
+ {
+ label: '鏂囨。鍚嶇О',
+ prop: 'docName',
+ width: '200',
+ },
+ { label: '鍊熼槄浜�', prop: 'borrower' },
+ { label: '褰掕繕浜�', prop: 'returner' },
+ {
+ label: '鍊熼槄鐘舵��',
+ prop: 'borrowStatus',
+ dataType: 'tag',
+ formatData: (params) => {
+ if (params === null || params === undefined || params === '') return '-';
+ return params;
+ },
+ formatType: (params) => {
+ if (params === '褰掕繕') return 'success';
+ if (params === '鍊熼槄') return 'warning';
+ return 'info';
+ }
+ },
+ { label: '褰掕繕鏃ユ湡', prop: 'returnDate' },
+ { label: '搴斿綊杩樻棩鏈�', prop: 'dueReturnDate' },
+ { label: '澶囨敞', prop: 'remark', width: '150' },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: '150',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: (row) => row.borrowStatus === '褰掕繕',
+ clickFun: (row) => {
+ openReturnDia('edit', row)
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "text",
+ clickFun: (row) => {
+ handleDelete(row)
+ },
+ },
+ ],
+ }
+]);
+
+// 鍒濆鍖栨暟鎹�
+const initData = async () => {
+ await Promise.all([
+ loadDocumentList(),
+ loadReturnList()
+ ]);
+};
+
+// 鍔犺浇鏂囨。鍒楄〃
+const loadDocumentList = async () => {
+ try {
+ const res = await getDocumentList();
+ if (res.code === 200) {
+ documentList.value = res.data || [];
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇鏂囨。鍒楄〃澶辫触");
+ documentList.value = [];
+ }
+ } catch (error) {
+ ElMessage.error("鑾峰彇鏂囨。鍒楄〃澶辫触锛岃閲嶈瘯");
+ documentList.value = [];
+ }
+};
+
+// 鍔犺浇褰掕繕鍒楄〃
+const loadReturnList = async () => {
+ try {
+ tableLoading.value = true;
+
+ // 鏋勫缓鏌ヨ鍙傛暟
+ const query = {
+ page: pagination.currentPage,
+ size: pagination.pageSize,
+ borrowStatus: searchForm.borrowStatus || undefined,
+ borrower: searchForm.borrower || undefined,
+ returner: searchForm.returner || undefined,
+ entryDateStart: searchForm.dateRange && searchForm.dateRange.length > 0 ? searchForm.dateRange[0] : undefined,
+ entryDateEnd: searchForm.dateRange && searchForm.dateRange.length > 1 ? searchForm.dateRange[1] : undefined
+ };
+
+ // 绉婚櫎undefined鐨勫弬鏁�
+ Object.keys(query).forEach(key => {
+ if (query[key] === undefined) {
+ delete query[key];
+ }
+ });
+
+ const res = await getReturnListPage(query);
+ if (res.code === 200) {
+ returnList.value = res.data.records || [];
+ pagination.total = res.data.total || 0;
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇褰掕繕鍒楄〃澶辫触");
+ returnList.value = [];
+ pagination.total = 0;
+ }
+
+ // 閲嶇疆閫夋嫨鐘舵��
+ selectedRows.value = [];
+ } catch (error) {
+ ElMessage.error("鑾峰彇褰掕繕鍒楄〃澶辫触锛岃閲嶈瘯");
+ returnList.value = [];
+ pagination.total = 0;
+ } finally {
+ tableLoading.value = false;
+ }
+};
+
+// 鏌ヨ
+const handleSearch = () => {
+ pagination.currentPage = 1;
+ loadReturnList();
+};
+
+// 閲嶇疆鏌ヨ
+const handleReset = () => {
+ searchForm.borrowStatus = "";
+ searchForm.borrower = "";
+ searchForm.returner = "";
+ searchForm.dateRange = [];
+ pagination.currentPage = 1;
+ loadReturnList();
+ ElMessage.success("鏌ヨ鏉′欢宸查噸缃�");
+};
+
+// 鎵撳紑褰掕繕寮规
+const openReturnDia = (type, data) => {
+ returnOperationType.value = type;
+ returnDia.value = true;
+ scanContent.value = ''; // 娓呯┖鎵爜鍐呭
+ borrowInfoList.value = []; // 娓呯┖鍊熼槄淇℃伅鍒楄〃
+
+ if (type === "edit") {
+ // 缂栬緫妯″紡锛屽姞杞界幇鏈夋暟鎹�
+ Object.assign(returnForm, data);
+ // 瀛樺偍鏂囨。鍚嶇О鐢ㄤ簬鏄剧ず
+ currentEditDocName.value = data.docName || '';
+ } else {
+ // 鏂板妯″紡锛屾竻绌鸿〃鍗�
+ Object.keys(returnForm).forEach(key => {
+ returnForm[key] = "";
+ });
+ currentEditDocName.value = ''; // 娓呯┖缂栬緫鏃剁殑鏂囨。鍚嶇О
+ // 璁剧疆榛樿鐘舵��
+ returnForm.borrowStatus = "褰掕繕";
+ // 璁剧疆褰撳墠鏃ユ湡涓哄綊杩樻棩鏈�
+ returnForm.returnDate = new Date().toISOString().split('T')[0];
+ }
+};
+
+// 鍏抽棴褰掕繕寮规
+const closeReturnDia = () => {
+ proxy.$refs.returnFormRef.resetFields();
+ returnDia.value = false;
+ scanContent.value = ''; // 娓呯┖鎵爜鍐呭
+ borrowInfoList.value = []; // 娓呯┖鍊熼槄淇℃伅鍒楄〃
+ currentEditDocName.value = ''; // 娓呯┖缂栬緫鏃剁殑鏂囨。鍚嶇О
+};
+
+// 鎻愪氦褰掕繕琛ㄥ崟
+const submitReturnForm = () => {
+ proxy.$refs.returnFormRef.validate(async (valid) => {
+ if (valid) {
+ try {
+ if (returnOperationType.value === "edit") {
+ // 缂栬緫妯″紡锛岃皟鐢ㄥ綊杩樻洿鏂版帴鍙�
+ const res = await reventUpdate({
+ id: returnForm.id,
+ documentationId: returnForm.documentationId,
+ borrower: returnForm.borrower,
+ returner: returnForm.returner,
+ borrowStatus: returnForm.borrowStatus,
+ returnDate: returnForm.returnDate,
+ dueReturnDate: returnForm.dueReturnDate,
+ remark: returnForm.remark
+ });
+
+ if (res.code === 200) {
+ ElMessage.success("缂栬緫鎴愬姛");
+ await loadReturnList();
+ closeReturnDia();
+ } else {
+ ElMessage.error(res.msg || "缂栬緫澶辫触");
+ }
+ } else {
+ // 鏂板妯″紡锛岃皟鐢ㄥ綊杩樻帴鍙�
+ const res = await returnDocument({
+ borrowId: returnForm.borrowId,
+ borrower: returnForm.borrower,
+ returner: returnForm.returner,
+ borrowStatus: returnForm.borrowStatus,
+ returnDate: returnForm.returnDate,
+ dueReturnDate: returnForm.dueReturnDate,
+ remark: returnForm.remark
+ });
+
+ if (res.code === 200) {
+ ElMessage.success("鏂板鎴愬姛");
+ await loadReturnList();
+ closeReturnDia();
+ } else {
+ ElMessage.error(res.msg || "鏂板澶辫触");
+ }
+ }
+ } catch (error) {
+ ElMessage.error("鎿嶄綔澶辫触锛岃閲嶈瘯");
+ }
+ }
+ });
+};
+
+// 鍒犻櫎褰掕繕璁板綍
+const handleDelete = (row) => {
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄よ繖鏉″綊杩樿褰曞悧锛焋,
+ "鍒犻櫎鎻愮ず",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ ).then(async () => {
+ try {
+ const res = await deleteReturn([row.id]);
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await loadReturnList();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("鍒犻櫎澶辫触锛岃閲嶈瘯");
+ }
+ }).catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+// 鎵归噺鍒犻櫎
+const handleBatchDelete = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑璁板綍");
+ return;
+ }
+
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉″綊杩樿褰曞悧锛焋,
+ "鎵归噺鍒犻櫎鎻愮ず",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ ).then(async () => {
+ try {
+ const selectedIds = selectedRows.value.map(row => row.id);
+ const res = await deleteReturn(selectedIds);
+ if (res.code === 200) {
+ ElMessage.success("鎵归噺鍒犻櫎鎴愬姛");
+ await loadReturnList();
+ } else {
+ ElMessage.error(res.msg || "鎵归噺鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ ElMessage.error("鎵归噺鍒犻櫎澶辫触锛岃閲嶈瘯");
+ }
+ }).catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/documentationBorrowManagement/exportrevent", {}, "褰掕繕鐧昏.xlsx");
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑�");
+ });
+};
+
+// 閫夋嫨鍙樺寲浜嬩欢
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 澶勭悊鍒嗛〉鍙樺寲
+const handlePagination = (current, size) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ loadReturnList();
+};
+// 澶勭悊鎵爜鍐呭
+const handleScanContent = async (value) => {
+ if (!value) return;
+
+ try {
+ // 璋冪敤API鏍规嵁涔︾睄ID鑾峰彇鍊熼槄淇℃伅
+ const res = await getBorrowListByDocumentationId(value);
+
+ if (res.code === 200 && res.data && res.data.length > 0) {
+ // 淇濆瓨鑾峰彇鍒扮殑鍊熼槄淇℃伅鍒楄〃
+ borrowInfoList.value = res.data;
+
+ // 濡傛灉鍙湁涓�鏉¤褰曪紝鐩存帴閫夋嫨
+ if (res.data.length === 1) {
+ const borrowInfo = res.data[0];
+ returnForm.borrowId = borrowInfo.id;
+ returnForm.borrower = borrowInfo.borrower || borrowInfo.borrowerName || '';
+ returnForm.dueReturnDate = borrowInfo.dueReturnDate || borrowInfo.expectedReturnDate || '';
+ ElMessage.success(`宸查�夋嫨: ${borrowInfo.docName || borrowInfo.name}`);
+ } else {
+ // 濡傛灉鏈夊鏉¤褰曪紝鏄剧ず閫夋嫨鎻愮ず
+ ElMessage.success(`鎵惧埌 ${res.data.length} 鏉$浉鍏冲�熼槄璁板綍锛岃浠庝笅鎷夊垪琛ㄤ腑閫夋嫨`);
+ // 閲嶆柊鍔犺浇鏂囨。鍒楄〃锛屽寘鍚渶鏂扮殑鍊熼槄淇℃伅
+ await loadDocumentList();
+ }
+ } else {
+ // 鏈壘鍒板尮閰嶇殑鍊熼槄璁板綍
+ ElMessage.warning('鏈壘鍒板搴旂殑鍊熼槄璁板綍锛岃妫�鏌ユ壂鐮佸唴瀹规垨鎵嬪姩閫夋嫨');
+ }
+ } catch (error) {
+ ElMessage.error('鎵爜澶勭悊澶辫触锛岃閲嶈瘯');
+ console.error('鎵爜澶勭悊閿欒:', error);
+ }
+};
+// 澶勭悊鏂囨。閫夋嫨鍙樺寲
+// 澶勭悊鏂囨。閫夋嫨鍙樺寲
+const handleDocumentChange = (borrowId) => {
+ // 褰撲笅鎷夋閫夋嫨鏃讹紝娓呯┖鎵爜杈撳叆妗�
+ scanContent.value = '';
+
+ if (borrowId) {
+ // 浼樺厛浠庡�熼槄淇℃伅鍒楄〃涓煡鎵�
+ let selectedInfo;
+ if (borrowInfoList.value.length > 0) {
+ selectedInfo = borrowInfoList.value.find(info => info.id === borrowId);
+ }
+
+ // 濡傛灉鍊熼槄淇℃伅鍒楄〃涓病鏈夋壘鍒帮紝浠庢枃妗e垪琛ㄤ腑鏌ユ壘
+ if (!selectedInfo) {
+ selectedInfo = documentList.value.find(doc => doc.id === borrowId);
+ }
+
+ if (selectedInfo) {
+ // 鑷姩濉厖鍊熼槄浜哄拰搴斿綊杩樻棩鏈�
+ returnForm.borrower = selectedInfo.borrower || selectedInfo.borrowerName || '';
+ returnForm.dueReturnDate = selectedInfo.dueReturnDate || selectedInfo.expectedReturnDate || '';
+ }
+ } else {
+ // 娓呯┖鐩稿叧瀛楁
+ returnForm.borrower = '';
+ returnForm.dueReturnDate = '';
+ }
+};
+// const handleDocumentChange = (documentId) => {
+// // 褰撲笅鎷夋閫夋嫨鏃讹紝娓呯┖鎵爜杈撳叆妗�
+// scanContent.value = '';
+// if (documentId) {
+// // 鏍规嵁閫夋嫨鐨勬枃妗D锛屼粠鏂囨。鍒楄〃涓煡鎵惧搴旂殑鏂囨。淇℃伅
+// const selectedDoc = documentList.value.find(doc => doc.id === documentId);
+// if (selectedDoc) {
+// // 鑷姩濉厖鍊熼槄浜哄拰搴斿綊杩樻棩鏈�
+// returnForm.borrower = selectedDoc.borrower || selectedDoc.borrowerName || '';
+// returnForm.dueReturnDate = selectedDoc.dueReturnDate || selectedDoc.expectedReturnDate || '';
+// }
+// } else {
+// // 娓呯┖鐩稿叧瀛楁
+// returnForm.borrower = '';
+// returnForm.dueReturnDate = '';
+// }
+// };
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ initData();
+});
+</script>
+
+<style scoped>
+.return-view {
+ padding: 20px;
+}
+
+.search-container {
+ background: #ffffff;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.search-form {
+ margin: 0;
+}
+
+.table-container {
+ background: #ffffff;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.empty-data {
+ text-align: center;
+ color: #909399;
+ padding: 40px;
+ font-size: 14px;
+}
+
+.dialog-footer {
+ text-align: center;
+}
+
+:deep(.el-form-item__label) {
+ font-weight: 500;
+ color: #303133;
+}
+
+:deep(.el-input__wrapper) {
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
+}
+
+:deep(.el-input__wrapper:hover) {
+ box-shadow: 0 0 0 1px #c0c4cc inset;
+}
+
+:deep(.el-input__wrapper.is-focus) {
+ box-shadow: 0 0 0 1px #409eff inset;
+}
+</style>
diff --git a/src/views/fileManagement/statistics/index.vue b/src/views/fileManagement/statistics/index.vue
new file mode 100644
index 0000000..42b81e4
--- /dev/null
+++ b/src/views/fileManagement/statistics/index.vue
@@ -0,0 +1,539 @@
+<template>
+ <div class="app-container statistics-container">
+
+ <!-- 鎬讳綋缁熻鍗$墖 -->
+ <el-row :gutter="20" class="statistics-cards">
+ <el-col :span="6" v-for="(item, index) in overviewData" :key="index">
+ <el-card class="statistics-card" :class="item.type">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon :size="32">
+ <component :is="item.icon" />
+ </el-icon>
+ </div>
+ <div class="card-info">
+ <div class="card-number">
+ <el-skeleton-item v-if="loading" variant="text" style="width: 60px; height: 32px;" />
+ <span v-else>{{ item.value }}</span>
+ </div>
+ <div class="card-label">{{ item.label }}</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 鍥捐〃鍖哄煙 -->
+ <el-row :gutter="20" class="charts-section">
+ <el-col :span="12">
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span>妗f鍒嗙被缁熻</span>
+ </div>
+ </template>
+ <div class="chart-container">
+ <div ref="categoryChartRef" class="chart"></div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12">
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span>妗f鐘舵�佺粺璁�</span>
+ </div>
+ </template>
+ <div class="chart-container">
+ <div ref="statusChartRef" class="chart"></div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, nextTick, onUnmounted } from "vue";
+import { ElMessage } from "element-plus";
+import { Refresh } from "@element-plus/icons-vue";
+import * as echarts from "echarts";
+import {
+ getDocumentationOverview,
+ getDocumentationCategoryStats,
+ getDocumentationStatusStats
+} from "@/api/fileManagement/document";
+import {
+ Document,
+ Folder,
+ Tickets,
+ Calendar
+} from "@element-plus/icons-vue";
+
+// 鍝嶅簲寮忔暟鎹�
+const overviewData = ref([
+ {
+ label: "鎬绘。妗堟暟",
+ value: 0,
+ icon: "Document",
+ type: "primary",
+ },
+ {
+ label: "鍒嗙被鏁伴噺",
+ value: 0,
+ icon: "Folder",
+ type: "success",
+ },
+ {
+ label: "鍊熷嚭妗f",
+ value: 0,
+ icon: "Tickets",
+ type: "warning",
+ },
+ {
+ label: "鏈湀鏂板",
+ value: 0,
+ icon: "Calendar",
+ type: "info",
+ },
+]);
+
+const categoryChartRef = ref(null);
+const statusChartRef = ref(null);
+
+// 鍥捐〃瀹炰緥
+let categoryChart = null;
+let statusChart = null;
+
+// 鍔犺浇鐘舵��
+const loading = ref(false);
+const autoRefreshInterval = ref(null);
+
+// 鑷姩鍒锋柊寮�鍏�
+const autoRefreshEnabled = ref(true);
+
+// 鑷姩鍒锋柊闂撮殧锛�5鍒嗛挓锛�
+const AUTO_REFRESH_INTERVAL = 5 * 60 * 1000;
+
+// 鍚姩鑷姩鍒锋柊
+const startAutoRefresh = () => {
+ if (autoRefreshInterval.value) {
+ clearInterval(autoRefreshInterval.value);
+ }
+ if (autoRefreshEnabled.value) {
+ autoRefreshInterval.value = setInterval(() => {
+ refreshData();
+ }, AUTO_REFRESH_INTERVAL);
+ }
+};
+
+// 鍋滄鑷姩鍒锋柊
+const stopAutoRefresh = () => {
+ if (autoRefreshInterval.value) {
+ clearInterval(autoRefreshInterval.value);
+ autoRefreshInterval.value = null;
+ }
+};
+
+// 鍒囨崲鑷姩鍒锋柊鐘舵��
+const toggleAutoRefresh = (value) => {
+ if (value) {
+ startAutoRefresh();
+ } else {
+ stopAutoRefresh();
+ }
+};
+
+// 鍔犺浇鎬讳綋缁熻鏁版嵁
+const loadOverviewData = async () => {
+ try {
+ const response = await getDocumentationOverview();
+ if (response.code === 200) {
+ const data = response.data;
+ overviewData.value[0].value = data.totalDocsCount || 0;
+ overviewData.value[1].value = data.categoryNumCount || 0;
+ overviewData.value[2].value = data.borrowedDocsCount || 0;
+ overviewData.value[3].value = data.monthlyAddedDocsCount || 0;
+ }
+ } catch (error) {
+ console.error('鍔犺浇鎬讳綋缁熻鏁版嵁澶辫触:', error);
+ ElMessage.error('鍔犺浇鎬讳綋缁熻鏁版嵁澶辫触');
+ }
+};
+
+// 鍔犺浇鍒嗙被缁熻鏁版嵁
+const loadCategoryData = async () => {
+ try {
+ const response = await getDocumentationCategoryStats();
+ if (response.code === 200) {
+ renderCategoryChart(response.data);
+ }
+ } catch (error) {
+ console.error('鍔犺浇鍒嗙被缁熻鏁版嵁澶辫触:', error);
+ ElMessage.error('鍔犺浇鍒嗙被缁熻鏁版嵁澶辫触');
+ }
+};
+
+// 鍔犺浇鐘舵�佺粺璁℃暟鎹�
+const loadStatusData = async () => {
+ try {
+ const response = await getDocumentationStatusStats();
+ if (response.code === 200) {
+ renderStatusChart(response.data);
+ }
+ } catch (error) {
+ console.error('鍔犺浇鐘舵�佺粺璁℃暟鎹け璐�:', error);
+ ElMessage.error('鍔犺浇鐘舵�佺粺璁℃暟鎹け璐�');
+ }
+};
+
+// 鍒锋柊鏁版嵁
+const refreshData = async () => {
+ loading.value = true;
+ try {
+ await Promise.all([
+ loadOverviewData(),
+ loadCategoryData(),
+ loadStatusData()
+ ]);
+ ElMessage.success('鏁版嵁鍒锋柊鎴愬姛');
+ } catch (error) {
+ console.error('鍒锋柊鏁版嵁澶辫触:', error);
+ ElMessage.error('鍒锋柊鏁版嵁澶辫触');
+ } finally {
+ loading.value = false;
+ }
+};
+
+// 鍒濆鍖栧浘琛�
+const initCharts = () => {
+ // 寤惰繜鍒濆鍖栵紝纭繚DOM鍏冪礌宸茬粡娓叉煋
+ setTimeout(() => {
+ if (categoryChartRef.value) {
+ categoryChart = echarts.init(categoryChartRef.value);
+ }
+
+ if (statusChartRef.value) {
+ statusChart = echarts.init(statusChartRef.value);
+ }
+
+ // 鍒濆鍖栧畬鎴愬悗鍔犺浇鏁版嵁
+ loadCategoryData();
+ loadStatusData();
+ }, 300);
+};
+
+// 娓叉煋鍒嗙被缁熻鍥捐〃
+const renderCategoryChart = (data) => {
+ if (!categoryChart) return;
+ let newData = data.map(item => {
+ return {
+ name: item.category,
+ value: item.count
+ }
+ })
+
+ const option = {
+ title: {
+ text: "妗f鍒嗙被鍒嗗竷",
+ left: "center",
+ textStyle: {
+ fontSize: 16,
+ fontWeight: "normal",
+ },
+ },
+ tooltip: {
+ trigger: "item",
+ formatter: "{a} <br/>{b}: {c} ({d}%)",
+ },
+ legend: {
+ orient: "vertical",
+ left: "left",
+ top: "middle",
+ },
+ series: [
+ {
+ name: "妗f鏁伴噺",
+ type: "pie",
+ radius: ["40%", "70%"],
+ center: ["60%", "50%"],
+ data: newData || [
+ { name: "鎶�鏈枃妗�", value: 450 },
+ { name: "绠$悊鏂囨。", value: 320 },
+ { name: "璐㈠姟鏂囨。", value: 280 },
+ { name: "浜轰簨鏂囨。", value: 200 },
+ ],
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: "rgba(0, 0, 0, 0.5)",
+ },
+ },
+ },
+ ],
+ };
+
+ try {
+ categoryChart.setOption(option);
+ } catch (error) {
+ console.error('鍒嗙被鍥捐〃娓叉煋澶辫触:', error);
+ }
+};
+
+// 娓叉煋鐘舵�佺粺璁″浘琛�
+const renderStatusChart = (data) => {
+ if (!statusChart) return;
+ let newData = data.map(item => {
+ return {
+ name: item.docStatus,
+ value: item.count
+ }
+ })
+ const option = {
+ title: {
+ text: "妗f鐘舵�佸垎甯�",
+ left: "center",
+ textStyle: {
+ fontSize: 16,
+ fontWeight: "normal",
+ },
+ },
+ tooltip: {
+ trigger: "item",
+ formatter: "{a} <br/>{b}: {c} ({d}%)",
+ },
+ legend: {
+ orient: "vertical",
+ left: "left",
+ top: "middle",
+ },
+ series: [
+ {
+ name: "妗f鏁伴噺",
+ type: "pie",
+ radius: ["40%", "70%"],
+ center: ["60%", "50%"],
+ roseType: false,
+ data: newData || [
+ { name: "姝e父", value: 1150, itemStyle: { color: "#67C23A" } },
+ { name: "鍊熷嚭", value: 89, itemStyle: { color: "#E6A23C" } },
+ { name: "涓㈠け", value: 8, itemStyle: { color: "#F56C6C" } },
+ { name: "鎹熷潖", value: 4, itemStyle: { color: "#909399" } },
+ ],
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: "rgba(0, 0, 0, 0.5)",
+ },
+ },
+ },
+ ],
+ };
+
+ try {
+ statusChart.setOption(option);
+ } catch (error) {
+ console.error('鐘舵�佸浘琛ㄦ覆鏌撳け璐�:', error);
+ }
+};
+
+onMounted(() => {
+ loadOverviewData();
+ initCharts();
+ startAutoRefresh();
+});
+
+// 缁勪欢鍗歌浇鏃舵竻鐞嗗畾鏃跺櫒
+onUnmounted(() => {
+ stopAutoRefresh();
+});
+</script>
+
+<style scoped>
+.statistics-container {
+ padding: 20px;
+ background-color: #f5f7fa;
+ min-height: 100vh;
+}
+
+.page-header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 12px;
+ color: white;
+}
+
+.page-header h2 {
+ color: white;
+ margin-bottom: 10px;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.page-header p {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 14px;
+ margin: 0 0 15px 0;
+}
+
+.header-controls {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 10px;
+ gap: 20px;
+}
+
+.refresh-btn {
+ margin-left: 20px;
+}
+
+.statistics-cards {
+ margin-bottom: 30px;
+}
+
+.statistics-card {
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ border: none;
+ overflow: hidden;
+}
+
+.statistics-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+}
+
+.statistics-card.primary {
+ border-left: 4px solid #409EFF;
+ background: linear-gradient(135deg, #409EFF 0%, #36a3f7 100%);
+}
+
+.statistics-card.success {
+ border-left: 4px solid #67C23A;
+ background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
+}
+
+.statistics-card.warning {
+ border-left: 4px solid #E6A23C;
+ background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
+}
+
+.statistics-card.info {
+ border-left: 4px solid #909399;
+ background: linear-gradient(135deg, #909399 0%, #a6a9ad 100%);
+}
+
+.card-content {
+ display: flex;
+ align-items: center;
+ padding: 20px;
+}
+
+.card-icon {
+ margin-right: 20px;
+ color: white;
+}
+
+.card-info {
+ flex: 1;
+}
+
+.card-number {
+ font-size: 32px;
+ font-weight: 600;
+ color: white;
+ margin-bottom: 5px;
+}
+
+.card-label {
+ font-size: 14px;
+ color: rgba(255, 255, 255, 0.9);
+}
+
+.charts-section {
+ margin-bottom: 30px;
+}
+
+.chart-card {
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ border: none;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ color: #303133;
+ padding: 15px 20px;
+ border-bottom: 1px solid #ebeef5;
+}
+
+.chart-container {
+ height: 400px;
+ padding: 20px;
+}
+
+.chart {
+ width: 100%;
+ height: 100%;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .statistics-container {
+ padding: 10px;
+ }
+
+ .page-header {
+ padding: 15px;
+ }
+
+ .page-header h2 {
+ font-size: 24px;
+ }
+
+ .header-controls {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .refresh-btn {
+ margin-left: 0;
+ }
+
+ .statistics-cards .el-col {
+ margin-bottom: 15px;
+ }
+
+ .charts-section .el-col {
+ margin-bottom: 20px;
+ }
+
+ .chart-container {
+ height: 300px;
+ }
+}
+
+@media (max-width: 480px) {
+ .page-header h2 {
+ font-size: 20px;
+ }
+
+ .card-number {
+ font-size: 24px;
+ }
+
+ .chart-container {
+ height: 250px;
+ }
+}
+</style>
diff --git a/src/views/financialManagement/accounting/index.vue b/src/views/financialManagement/accounting/index.vue
new file mode 100644
index 0000000..ea858e1
--- /dev/null
+++ b/src/views/financialManagement/accounting/index.vue
@@ -0,0 +1,740 @@
+<template>
+ <div style="padding: 20px;">
+ <!-- 椤甸潰鏍囬鍜岀瓫閫夋潯浠� -->
+ <div class="w-full md:w-auto flex items-center gap-3">
+ <el-form :inline="true">
+ <el-form-item label="骞翠唤">
+ <el-date-picker
+ v-model="selectedYear"
+ type="year"
+ placeholder="璇烽�夋嫨骞翠唤"
+ format="YYYY"
+ value-format="YYYY"
+ clearable
+ @change="fetchData()"
+ style="width: 200px"
+ :disabled-date="(date) => date.getFullYear() > new Date().getFullYear()"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button
+ type="primary"
+ icon="Refresh"
+ @click="resetFilters"
+ size="default"
+ >
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <main class="container mx-auto px-4 pb-10">
+ <!-- 鍥哄畾璧勪骇鎸囨爣鍗$墖 -->
+ <div class="kpi-grid">
+ <!-- 璁惧鎬绘暟 -->
+ <div class="kpi-card">
+ <div class="kpi-left">
+ <span class="kpi-dot kpi-dot-blue"></span>
+ <span class="kpi-title">璁惧鎬绘暟</span>
+ <div class="kpi-value">{{ assetInfo.totalEquipment }}涓�</div>
+ </div>
+ <div class="kpi-icon-wrap kpi-icon-blue">
+ <img :src="iconBlue" alt="" class="kpi-icon" />
+ </div>
+ </div>
+
+ <!-- 璧勪骇鍘熷�� -->
+ <div class="kpi-card">
+ <div class="kpi-left">
+ <span class="kpi-dot kpi-dot-orange"></span>
+ <span class="kpi-title">璧勪骇鍘熷��</span>
+ <div class="kpi-value">楼{{ formatCurrency(assetInfo.totalOriginalValue) }}</div>
+ </div>
+ <div class="kpi-icon-wrap kpi-icon-orange">
+ <img :src="iconWalletOrange" alt="" class="kpi-icon" />
+ </div>
+ </div>
+
+ <!-- 绱鎶樻棫 -->
+ <div class="kpi-card">
+ <div class="kpi-left">
+ <span class="kpi-dot kpi-dot-green"></span>
+ <span class="kpi-title">绱鎶樻棫</span>
+ <div class="kpi-value">楼{{ formatCurrency(assetInfo.totalDepreciation) }}</div>
+ </div>
+ <div class="kpi-icon-wrap kpi-icon-green">
+ <img :src="iconGreen" alt="" class="kpi-icon" />
+ </div>
+ </div>
+
+ <!-- 搴撳瓨璧勪骇 -->
+ <div class="kpi-card">
+ <div class="kpi-left">
+ <span class="kpi-dot kpi-dot-pink"></span>
+ <span class="kpi-title">搴撳瓨璧勪骇</span>
+ <div class="kpi-value">楼{{ formatCurrency(assetInfo.inventoryValue) }}</div>
+ </div>
+ <div class="kpi-icon-wrap kpi-icon-pink">
+ <img :src="iconPink" alt="" class="kpi-icon" />
+ </div>
+ </div>
+
+ <!-- 鍑�鍊� -->
+ <div class="kpi-card">
+ <div class="kpi-left">
+ <span class="kpi-dot kpi-dot-yellow"></span>
+ <span class="kpi-title">鍑�鍊�</span>
+ <div class="kpi-value">楼{{ formatCurrency(assetInfo.totalNetValue) }}</div>
+ </div>
+ <div class="kpi-icon-wrap kpi-icon-yellow">
+ <img :src="iconYellow" alt="" class="kpi-icon" />
+ </div>
+ </div>
+
+ <!-- 璐熷�� -->
+ <div class="kpi-card">
+ <div class="kpi-left">
+ <span class="kpi-dot kpi-dot-red"></span>
+ <span class="kpi-title">璐熷��</span>
+ <div class="kpi-value">楼{{ formatCurrency(assetInfo.debt) }}</div>
+ </div>
+ <div class="kpi-icon-wrap kpi-icon-red">
+ <img :src="iconWalletRed" alt="" class="kpi-icon" />
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍥哄畾璧勪骇缁熻鍥捐〃 -->
+ <div class="chart-row">
+ <!-- 璁惧绫诲瀷鍒嗗竷 -->
+ <el-card class="chart-card">
+ <h2 class="section-title">璁惧绫诲瀷鍒嗗竷</h2>
+ <div class="chart-content">
+ <div class="pie-wrap">
+ <Echarts
+ :legend="typeDistributionLegend"
+ :chartStyle="chartStylePie"
+ :series="typeDistributionSeries"
+ :tooltip="pieTooltip"
+ style="height: 260px; width: 100%;"
+ />
+ </div>
+ <div class="type-cards">
+ <div class="type-card" v-for="(item, index) in typeDistributionData" :key="index">
+ <span class="type-name">{{ item.name }}</span>
+ <span class="type-count">{{ item.count }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ <!-- 璁惧閲戦鍒嗘瀽 -->
+ <el-card class="chart-card">
+ <h2 class="section-title">璁惧閲戦鍒嗘瀽</h2>
+ <div class="bar-chart-wrap">
+ <Echarts
+ ref="barChart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="lineLegend"
+ :series="typeDistributionBarSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis"
+ :yAxis="yAxisBar"
+ style="height: 260px; width: 100%;"
+ />
+ </div>
+ </el-card>
+ </div>
+ <!-- 璁惧鏁版嵁琛� -->
+ <el-card class="table-card">
+ <h2 class="section-title">璁惧鏁版嵁琛�</h2>
+ <el-table
+ :data="equipmentList"
+ stripe
+ style="width: 100%"
+ :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
+ >
+ <el-table-column type="index" label="搴忓彿" width="60" :index="(index) => (pagination.currentPage - 1) * pagination.pageSize + index + 1" />
+ <el-table-column prop="deviceName" label="璁惧鍚嶇О" width="250" />
+ <el-table-column prop="deviceModel" label="瑙勬牸鍨嬪彿" min-width="150" />
+ <el-table-column prop="supplierName" label="渚涘簲鍟�" min-width="120" />
+ <el-table-column prop="unit" label="鍗曚綅" width="120" />
+ <el-table-column prop="number" label="鏁伴噺" width="120" />
+ <el-table-column prop="originalValue" label="鍘熷��(鍏�)" width="120">
+ <template #default="{ row }">
+ {{ formatCurrency(row.taxIncludingPriceTotal) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="depreciation" label="绱鎶樻棫(鍏�)" width="140">
+ <template #default="{ row }">
+ {{ formatCurrency(row.deprAmount) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="netValue" label="鍑�鍊�(鍏�)" width="120">
+ <template #default="{ row }">
+ {{ formatCurrency(row.netValue) }}
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <div class="pagination-container">
+ <el-pagination
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ :current-page="pagination.currentPage"
+ :page-sizes="[10, 20, 50, 100]"
+ :page-size="pagination.pageSize"
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="pagination.total"
+ />
+ </div>
+ </el-card>
+ </main>
+
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, reactive } from 'vue';
+import 'element-plus/dist/index.css';
+import Echarts from "@/components/Echarts/echarts.vue";
+import { getAccountingTotal, getDeviceTypeDistribution, getCalculateDepreciation } from "@/api/financialManagement/accounting";
+import dayjs from "dayjs";
+import iconBlue from '@/assets/icons/png/blue@2x.png';
+import iconWalletOrange from '@/assets/icons/png/walletOrange@2x.png';
+import iconGreen from '@/assets/icons/png/green@2x.png';
+import iconPink from '@/assets/icons/png/pink@2x.png';
+import iconYellow from '@/assets/icons/png/yellow@2x.png';
+import iconWalletRed from '@/assets/icons/png/walletRed@2x.png';
+
+// 绛涢�夋潯浠�
+const dateRange = ref(null);
+const equipmentType = ref('');
+const selectedYear = ref(dayjs().format('YYYY')); // 榛樿褰撳墠骞翠唤
+
+
+// 鍥哄畾璧勪骇淇℃伅
+const assetInfo = ref({
+ totalEquipment: 0, // deviceTotal
+ totalOriginalValue: 0, // deviceAmount
+ totalDepreciation: 0, // deprAmount
+ totalNetValue: 0, // netValue
+ debt: 0, // 璐熷��
+ inventoryValue: 0 // 搴撳瓨璧勪骇
+});
+
+// 璁惧绫诲瀷鎬绘暟锛堢敤浜庡浘琛ㄦ樉绀猴級
+const deviceTypeTotalCount = ref(0);
+
+// 璁惧鍒楄〃
+const equipmentList = ref([]);
+const pagination = ref({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0
+});
+
+// 鍥捐〃閰嶇疆
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+ position: 'relative',
+};
+
+const grid = {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
+};
+
+const lineLegend = {
+ show: false,
+};
+
+// 鎶樼嚎鍥炬彁绀烘
+const tooltip = reactive({
+ trigger: 'axis',
+ axisPointer: {
+ type: 'line',
+ lineStyle: { color: '#aaa' }
+ },
+ // 鑷畾涔夊唴瀹�
+ formatter: function (params) {
+ if (!params || !params.length) return '';
+ const axisLabel = params[0].axisValueLabel || params[0].axisValue || '';
+ const rows = params
+ .map(p => {
+ const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>`;
+ return `${colorDot}${p.seriesName}: ${p.value}`;
+ })
+ .join('<br/>');
+ return `<div>${axisLabel}</div><div>${rows}</div>`;
+ }
+});
+
+const xAxis = ref([
+ {
+ type: 'category',
+ axisTick: { show: true, alignWithLabel: true },
+ data: [],
+ },
+]);
+
+const yAxis = [
+ {
+ type: 'value',
+ name: '鏁伴噺/閲戦', // 宸︿晶y杞�
+ position: 'left',
+ min: 0,
+ // 鍧愭爣杞村悕绉版牱寮�
+ nameTextStyle: {
+ color: '#000',
+ fontSize: 14,
+ },
+ }
+];
+
+const chartStylePie = {
+ width: '100%',
+ height: '100%' // 璁剧疆鍥捐〃瀹瑰櫒鐨勯珮搴�
+};
+
+const pieColors = ['#165DFF', '#14C9C9', '#8543E0', '#1890FF', '#13C2C2', '#2FC25B']; // 鍙牴鎹疄闄呰皟鏁�
+
+// 楗煎浘鏁版嵁
+const typeDistributionData = ref([]);
+const departmentDistributionData = ref([]);
+
+// 楗煎浘鍥句緥锛堟偓鍋滄樉绀哄悕绉�+鍗犳瘮锛屽浘渚嬫斁涓嬫柟鍗$墖灞曠ず锛�
+const typeDistributionLegend = computed(() => ({
+ show: false,
+ data: typeDistributionData.value.map(item => item.name)
+}));
+
+
+// 楗煎浘绯诲垪
+const typeDistributionSeries = computed(() => [
+ {
+ type: 'pie',
+ radius: ['0%', '65%'],
+ center: ['50%', '45%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: { show: false },
+ data: typeDistributionData.value,
+ color: pieColors
+ }
+]);
+
+// 鎶樼嚎鍥炬暟鎹�
+const typeDistributionLineSeries = ref([]);
+// 鏌辩姸鍥炬暟鎹紙璁惧閲戦鍒嗘瀽锛�
+const typeDistributionBarSeries = computed(() => [
+ {
+ name: '閿�鍞',
+ type: 'bar',
+ data: typeDistributionData.value.map(item => (item.amountNum != null ? item.amountNum : 0)),
+ itemStyle: { color: '#13C2C2' }
+ }
+]);
+// 鏌辩姸鍥� Y 杞�
+const yAxisBar = [
+ {
+ type: 'value',
+ name: '閿�鍞(涓囧厓)',
+ position: 'left',
+ min: 0,
+ nameTextStyle: { color: '#000', fontSize: 14 },
+ splitLine: { lineStyle: { color: '#f0f0f0' } }
+ }
+];
+
+
+// 楗煎浘鎻愮ず妗嗭紙鍥惧唴鏍峰紡锛氬悕绉� + 鍗犳瘮锛�
+const pieTooltip = reactive({
+ trigger: 'item',
+ formatter: function(params) {
+ if (!params.data) return params.name;
+ const pct = params.percent != null ? params.percent.toFixed(0) : 0;
+ return `${params.name} ${pct}%`;
+ }
+});
+
+// 閫夐」鏁版嵁
+const equipmentTypeOptions = ref([]);
+
+// 鑾峰彇鏁版嵁
+const fetchData = async () => {
+ try {
+ // 鑾峰彇鍥哄畾璧勪骇姹囨�讳俊鎭�
+ const assetInfoRes = await getAccountingTotal({
+ startDate: dateRange.value ? dateRange.value[0] : null,
+ endDate: dateRange.value ? dateRange.value[1] : null,
+ equipmentType: equipmentType.value,
+ year: selectedYear.value
+ });
+
+ if (assetInfoRes.code === 200) {
+ // 鏄犲皠鍚庣瀛楁鍒板墠绔瓧娈�
+ const data = assetInfoRes.data;
+ assetInfo.value = {
+ totalEquipment: data.deviceTotal || 0, // 璁惧鎬绘暟
+ totalOriginalValue: data.deviceAmount || 0, // 璧勪骇鍘熷��
+ totalDepreciation: data.deprAmount || 0, // 绱鎶樻棫
+ totalNetValue: data.netValue || 0, // 鍑�鍊�
+ debt: data.debt || 0, // 璐熷��
+ inventoryValue: data.inventoryValue || 0 // 搴撳瓨璧勪骇
+ };
+ }
+
+ // 鑾峰彇璁惧绫诲瀷鍒嗗竷鏁版嵁锛堥ゼ鍥惧拰鎶樼嚎鍥撅級
+ const distributionRes = await getDeviceTypeDistribution({
+ startDate: dateRange.value ? dateRange.value[0] : null,
+ endDate: dateRange.value ? dateRange.value[1] : null,
+ equipmentType: equipmentType.value,
+ year: selectedYear.value
+ });
+
+ if (distributionRes.code === 200) {
+ const data = distributionRes.data;
+
+ // 鏇存柊璁惧绫诲瀷鎬绘暟
+ deviceTypeTotalCount.value = data.totalCount || 0;
+
+ // 杞崲楗煎浘鏁版嵁鏍煎紡
+ if (data.details && data.details.length > 0) {
+ typeDistributionData.value = data.details.map(item => ({
+ name: item.type || '',
+ value: Number(item.count || 0),
+ count: Number(item.count || 0),
+ amount: `楼${formatCurrency(item.amount || 0)}`,
+ amountNum: Number(item.amount || 0)
+ }));
+ } else if (data.categories && data.categories.length > 0) {
+ // 濡傛灉娌℃湁 details锛屼娇鐢� categories銆乧ountData 鍜� amountData 鏋勫缓
+ typeDistributionData.value = data.categories.map((category, index) => ({
+ name: category,
+ value: Number(data.countData[index] || 0),
+ count: Number(data.countData[index] || 0),
+ amount: `楼${formatCurrency(data.amountData[index] || 0)}`,
+ amountNum: Number(data.amountData[index] || 0)
+ }));
+ } else {
+ typeDistributionData.value = [];
+ }
+
+ // 鏇存柊x杞存暟鎹�
+ xAxis.value[0].data = data.categories || typeDistributionData.value.map(item => item.name);
+
+ // 鏋勫缓鎶樼嚎鍥炬暟鎹�
+ typeDistributionLineSeries.value = [
+ {
+ name: '璁惧鏁伴噺',
+ type: 'line',
+ data: data.countData || typeDistributionData.value.map(item => item.count)
+ }
+ ];
+ }
+
+ // 鑾峰彇璁惧鍒楄〃锛堟姌鏃ц绠楁暟鎹級
+ const equipmentListRes = await getCalculateDepreciation({
+ current: pagination.value.currentPage,
+ size: pagination.value.pageSize,
+ startDate: dateRange.value ? dateRange.value[0] : null,
+ endDate: dateRange.value ? dateRange.value[1] : null,
+ equipmentType: equipmentType.value,
+ year: selectedYear.value
+ });
+
+ if (equipmentListRes.code === 200) {
+ // 濡傛灉杩斿洖鐨勬槸鍒嗛〉鏁版嵁
+ if (equipmentListRes.data.records) {
+ equipmentList.value = equipmentListRes.data.records;
+ pagination.value.total = equipmentListRes.data.total;
+ } else if (Array.isArray(equipmentListRes.data)) {
+ // 濡傛灉杩斿洖鐨勬槸鏁扮粍
+ equipmentList.value = equipmentListRes.data;
+ pagination.value.total = equipmentListRes.data.length;
+ } else {
+ equipmentList.value = [];
+ pagination.value.total = 0;
+ }
+ }
+ } catch (error) {
+ console.error('鑾峰彇鍥哄畾璧勪骇鏁版嵁澶辫触锛�', error);
+ }
+};
+
+// 鍒濆鍖�
+onMounted(() => {
+ // 鑾峰彇鍒楄〃鏁版嵁
+ fetchData();
+});
+
+// 鏍煎紡鍖栬揣甯�
+const formatCurrency = (value) => {
+ if (!value) return '0.00';
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+};
+
+// 鑾峰彇鐘舵�佹爣绛剧被鍨�
+const getStatusTagType = (status) => {
+ switch (status) {
+ case '鍦ㄧ敤':
+ return 'success';
+ case '闂茬疆':
+ return 'info';
+ case '缁翠慨涓�':
+ return 'warning';
+ case '鎶ュ簾':
+ return 'danger';
+ default:
+ return 'info';
+ }
+};
+
+// 閲嶇疆绛涢�夋潯浠�
+const resetFilters = () => {
+ dateRange.value = null;
+ equipmentType.value = '';
+ selectedYear.value = dayjs().format('YYYY'); // 閲嶇疆涓哄綋鍓嶅勾浠�
+ fetchData();
+};
+
+// 鍒嗛〉澶勭悊
+const handleSizeChange = (size) => {
+ pagination.value.pageSize = size;
+ fetchData();
+};
+
+const handleCurrentChange = (page) => {
+ pagination.value.currentPage = page;
+ fetchData();
+};
+</script>
+
+<style scoped lang="scss">
+:root {
+ --el-color-primary: #1890ff;
+}
+
+/* 椤甸潰鑳屾櫙 */
+main {
+ padding: 0;
+ margin: 0 -20px;
+ padding: 0 20px 20px;
+}
+
+/* KPI 鍗$墖缃戞牸 */
+.kpi-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ margin-bottom: 20px;
+}
+
+@media (max-width: 1024px) {
+ .kpi-grid {
+ grid-template-columns: repeat(2, 1fr);
+ }
+}
+
+@media (max-width: 640px) {
+ .kpi-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* KPI 鍗$墖 - 鐧藉簳銆佸渾瑙掋�侀槾褰� */
+.kpi-card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ background: #fff;
+ border-radius: 8px;
+ padding: 16px 20px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+ min-height: 100px;
+}
+
+.kpi-left {
+ flex: 1;
+ min-width: 0;
+}
+
+.kpi-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ margin-right: 6px;
+ vertical-align: middle;
+}
+
+.kpi-dot-blue { background: #1890ff; }
+.kpi-dot-orange { background: #fa8c16; }
+.kpi-dot-green { background: #52c41a; }
+.kpi-dot-pink { background: #eb2f96; }
+.kpi-dot-yellow { background: #facc14; }
+.kpi-dot-red { background: #f5222d; }
+
+.kpi-title {
+ font-size: 14px;
+ color: #333;
+ vertical-align: middle;
+}
+
+.kpi-value {
+ font-size: 24px;
+ font-weight: 600;
+ color: #333;
+ margin-top: 8px;
+ line-height: 1.2;
+}
+
+/* 鍙充晶鍥炬爣鏂瑰潡 */
+.kpi-icon-wrap {
+ width: 48px;
+ height: 48px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.kpi-icon-blue { background: #e6f7ff; }
+.kpi-icon-orange { background: #fff7e6; }
+.kpi-icon-green { background: #f6ffed; }
+.kpi-icon-pink { background: #fff0f6; }
+.kpi-icon-yellow { background: #fffbe6; }
+.kpi-icon-red { background: #fff1f0; }
+
+.kpi-icon {
+ width: 28px;
+ height: 28px;
+ object-fit: contain;
+}
+
+/* 鍥捐〃鍖哄煙涓ゅ垪 */
+.chart-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ margin-bottom: 20px;
+}
+
+@media (max-width: 1024px) {
+ .chart-row {
+ grid-template-columns: 1fr;
+ }
+}
+
+.chart-card {
+ border-radius: 8px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+
+ :deep(.el-card__body) {
+ padding: 16px 20px;
+ }
+}
+
+.chart-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.pie-wrap {
+ position: relative;
+ width: 100%;
+ height: 260px;
+}
+
+.type-cards {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ justify-content: center;
+ margin-top: 12px;
+}
+
+.type-card {
+ background: #fafafa;
+ border-radius: 6px;
+ padding: 8px 16px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 80px;
+}
+
+.type-name {
+ font-size: 12px;
+ color: #666;
+}
+
+.type-count {
+ font-size: 16px;
+ font-weight: 600;
+ color: #333;
+ margin-top: 4px;
+}
+
+.bar-chart-wrap {
+ width: 100%;
+ height: 260px;
+}
+
+/* 鍖哄潡鏍囬 - 宸︿晶钃濊壊绔栫嚎 */
+.section-title {
+ position: relative;
+ font-size: 18px;
+ color: #333;
+ padding-left: 12px;
+ margin-bottom: 16px;
+ font-weight: 700;
+}
+
+.section-title::before {
+ position: absolute;
+ left: 0;
+ top: 2px;
+ content: '';
+ width: 4px;
+ height: 18px;
+ background: #1890ff;
+ border-radius: 2px;
+}
+
+/* 琛ㄦ牸鍗$墖 */
+.table-card {
+ border-radius: 8px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+
+ :deep(.el-card__body) {
+ padding: 16px 20px;
+ }
+}
+
+.pagination-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+}
+
+:deep(.el-pagination) {
+ --el-pagination-button-bg-color: #fff;
+}
+
+:deep(.el-pager li.is-active) {
+ background: #1890ff;
+}
+</style>
diff --git a/src/views/financialManagement/assets/fixedAssets.vue b/src/views/financialManagement/assets/fixedAssets.vue
new file mode 100644
index 0000000..95eb017
--- /dev/null
+++ b/src/views/financialManagement/assets/fixedAssets.vue
@@ -0,0 +1,495 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="璧勪骇缂栧彿:">
+ <el-input v-model="filters.assetCode" placeholder="璇疯緭鍏ヨ祫浜х紪鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇鍚嶇О:">
+ <el-input v-model="filters.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇绫诲埆:">
+ <el-select v-model="filters.category" placeholder="璇烽�夋嫨绫诲埆" clearable style="width: 150px;">
+ <el-option label="鎴垮眿寤虹瓚" value="building" />
+ <el-option label="鏈哄櫒璁惧" value="machine" />
+ <el-option label="杩愯緭宸ュ叿" value="vehicle" />
+ <el-option label="鐢靛瓙璁惧" value="electronic" />
+ <el-option label="鍔炲叕瀹跺叿" value="furniture" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="鎶ュ簾" value="scrapped" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="璧勪骇鍘熷�煎悎璁�" :value="totalOriginalValue" precision="2" prefix="楼" />
+ <el-statistic title="绱鎶樻棫鍚堣" :value="totalDepreciation" precision="2" prefix="楼" style="margin-left: 30px;" />
+ <el-statistic title="鍑�鍊煎悎璁�" :value="totalNetValue" precision="2" prefix="楼" style="margin-left: 30px;" />
+ </div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板璧勪骇</el-button>
+ <el-button type="warning" @click="handleDepreciation" icon="Money">鎶樻棫璁℃彁</el-button>
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ <template #originalValue="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.originalValue) }}</span>
+ </template>
+ <template #accumulatedDepreciation="{ row }">
+ <span class="text-warning">楼{{ formatMoney(row.accumulatedDepreciation) }}</span>
+ </template>
+ <template #netValue="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.netValue) }}</span>
+ </template>
+ <template #category="{ row }">
+ <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)">缂栬緫</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
+ <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇缂栧彿" prop="assetCode">
+ <el-input v-model="form.assetCode" placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍚嶇О" prop="assetName">
+ <el-input v-model="form.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇绫诲埆" prop="category">
+ <el-select v-model="form.category" placeholder="璇烽�夋嫨璧勪骇绫诲埆" style="width: 100%;">
+ <el-option label="鎴垮眿寤虹瓚" value="building" />
+ <el-option label="鏈哄櫒璁惧" value="machine" />
+ <el-option label="杩愯緭宸ュ叿" value="vehicle" />
+ <el-option label="鐢靛瓙璁惧" value="electronic" />
+ <el-option label="鍔炲叕瀹跺叿" value="furniture" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿" prop="specification">
+ <el-input v-model="form.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璐疆鏃ユ湡" prop="purchaseDate">
+ <el-date-picker v-model="form.purchaseDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍘熷��" prop="originalValue">
+ <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浣跨敤骞撮檺" prop="usefulLife">
+ <el-input-number v-model="form.usefulLife" :min="1" :max="50" style="width: 100%;" />
+ <span style="margin-left: 10px;">骞�</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="娈嬪�肩巼" prop="residualRate">
+ <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
+ <span style="margin-left: 10px;">%</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绱鎶樻棫">
+ <el-input v-model="form.accumulatedDepreciation" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍑�鍊�">
+ <el-input v-model="form.netValue" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀛樻斁鍦扮偣" prop="location">
+ <el-input v-model="form.location" placeholder="璇疯緭鍏ュ瓨鏀惧湴鐐�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浣跨敤閮ㄩ棬" prop="department">
+ <el-input v-model="form.department" placeholder="璇疯緭鍏ヤ娇鐢ㄩ儴闂�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="淇濈浜�" prop="keeper">
+ <el-input v-model="form.keeper" placeholder="璇疯緭鍏ヤ繚绠′汉" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="鎶ュ簾" value="scrapped" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker v-model="createTimeDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from "vue";
+import dayjs from "dayjs";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import {
+ listFixedAssetPage,
+ addFixedAsset,
+ updateFixedAsset,
+ deleteFixedAsset,
+ depreciateFixedAsset,
+} from "@/api/financialManagement/fixedAsset";
+
+defineOptions({
+ name: "鍥哄畾璧勪骇",
+});
+
+const filters = reactive({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "璧勪骇缂栧彿", prop: "assetCode", width: "130" },
+ { label: "璧勪骇鍚嶇О", prop: "assetName", width: "150" },
+ { label: "璧勪骇绫诲埆", prop: "category", dataType: "slot", slot: "category" },
+ { label: "瑙勬牸鍨嬪彿", prop: "specification", width: "120" },
+ { label: "璧勪骇鍘熷��", prop: "originalValue", dataType: "slot", slot: "originalValue" },
+ { label: "绱鎶樻棫", prop: "accumulatedDepreciation", dataType: "slot", slot: "accumulatedDepreciation" },
+ { label: "璧勪骇鍑�鍊�", prop: "netValue", dataType: "slot", slot: "netValue" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
+];
+
+const dataList = ref([]);
+const multipleList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+const selectedIds = computed(() =>
+ multipleList.value
+ .map(item => item?.id)
+ .filter(id => id !== undefined && id !== null && id !== "")
+);
+
+const createDefaultForm = () => ({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ specification: "",
+ purchaseDate: "",
+ originalValue: 0,
+ usefulLife: 5,
+ residualRate: 5,
+ accumulatedDepreciation: 0,
+ netValue: 0,
+ location: "",
+ department: "",
+ keeper: "",
+ status: "in_use",
+ remark: "",
+ createTime: "",
+});
+
+const form = reactive({
+ ...createDefaultForm(),
+});
+const createTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
+
+const rules = {
+ assetName: [{ required: true, message: "璇疯緭鍏ヨ祫浜у悕绉�", trigger: "blur" }],
+ category: [{ required: true, message: "璇烽�夋嫨璧勪骇绫诲埆", trigger: "change" }],
+ purchaseDate: [{ required: true, message: "璇烽�夋嫨璐疆鏃ユ湡", trigger: "change" }],
+ originalValue: [{ required: true, message: "璇疯緭鍏ヨ祫浜у師鍊�", trigger: "blur" }],
+ usefulLife: [{ required: true, message: "璇疯緭鍏ヤ娇鐢ㄥ勾闄�", trigger: "blur" }],
+};
+
+const totalOriginalValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
+});
+
+const totalDepreciation = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedDepreciation), 0);
+});
+
+const totalNetValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getCategoryLabel = (category) => {
+ const map = {
+ building: "鎴垮眿寤虹瓚",
+ machine: "鏈哄櫒璁惧",
+ vehicle: "杩愯緭宸ュ叿",
+ electronic: "鐢靛瓙璁惧",
+ furniture: "鍔炲叕瀹跺叿",
+ };
+ return map[category] || category;
+};
+
+const getStatusLabel = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = { in_use: "鍦ㄧ敤", idle: "闂茬疆", repair: "缁翠慨涓�", scrapped: "鎶ュ簾" };
+ return map[key] || status;
+};
+
+const getStatusType = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = { in_use: "success", idle: "warning", repair: "warning", scrapped: "info" };
+ return map[key] || "";
+};
+
+const calculateNetValue = () => {
+ const originalValue = Number(form.originalValue || 0);
+ const accumulatedDepreciation = Number(form.accumulatedDepreciation || 0);
+ form.netValue = Number((originalValue - accumulatedDepreciation).toFixed(2));
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁板浐瀹氫负 current/size锛岃繑鍥� data.records/data.total
+const getTableData = async () => {
+ try {
+ const { data } = await listFixedAssetPage({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ assetCode: filters.assetCode,
+ assetName: filters.assetName,
+ category: filters.category,
+ status: filters.status,
+ });
+ dataList.value = data?.records || [];
+ multipleList.value = [];
+ pagination.total = Number(data?.total || 0);
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const handleSelectionChange = (selectionList) => {
+ multipleList.value = selectionList;
+};
+
+const resetFilters = () => {
+ filters.assetCode = "";
+ filters.assetName = "";
+ filters.category = "";
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ currentId.value = null;
+ dialogTitle.value = "鏂板鍥哄畾璧勪骇";
+ Object.assign(form, createDefaultForm(), {
+ purchaseDate: new Date().toISOString().split('T')[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ });
+ dialogVisible.value = true;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫鍥哄畾璧勪骇";
+ Object.assign(form, createDefaultForm(), row);
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ edit(row);
+ isView.value = true;
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm("纭鍒犻櫎璇ュ浐瀹氳祫浜у悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ // 鑱旇皟绾﹀畾锛氬垹闄ゆ帴鍙d娇鐢� ids=1&ids=2
+ await deleteFixedAsset([row.id]);
+ if (dataList.value.length === 1 && pagination.currentPage > 1) {
+ pagination.currentPage -= 1;
+ }
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await getTableData();
+ });
+};
+
+const handleDepreciation = () => {
+ const ids = selectedIds.value;
+ const confirmText = ids.length
+ ? `纭瀵归�変腑鐨� ${ids.length} 鏉¤祫浜ц繘琛屾湰鏈堟姌鏃ц鎻愬悧锛焋
+ : "纭杩涜鏈湀鎶樻棫璁℃彁鍚楋紵";
+ ElMessageBox.confirm(confirmText, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(async () => {
+ await depreciateFixedAsset({ ids });
+ ElMessage.success("鎶樻棫璁℃彁瀹屾垚");
+ await getTableData();
+ });
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+ if (isView.value) {
+ dialogVisible.value = false;
+ return;
+ }
+ formRef.value.validate(async valid => {
+ if (valid) {
+ try {
+ calculateNetValue();
+ const payload = { ...form };
+ if (isEdit.value) {
+ payload.id = currentId.value;
+ await updateFixedAsset(payload);
+ ElMessage.success("缂栬緫鎴愬姛");
+ } else {
+ await addFixedAsset(payload);
+ ElMessage.success("鏂板鎴愬姛");
+ }
+ dialogVisible.value = false;
+ await getTableData();
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+ }
+ });
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+
+ > div:first-child {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/assets/intangibleAssets.vue b/src/views/financialManagement/assets/intangibleAssets.vue
new file mode 100644
index 0000000..9aef2bf
--- /dev/null
+++ b/src/views/financialManagement/assets/intangibleAssets.vue
@@ -0,0 +1,493 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="璧勪骇缂栧彿:">
+ <el-input v-model="filters.assetCode" placeholder="璇疯緭鍏ヨ祫浜х紪鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇鍚嶇О:">
+ <el-input v-model="filters.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="璧勪骇绫诲埆:">
+ <el-select v-model="filters.category" placeholder="璇烽�夋嫨绫诲埆" clearable style="width: 150px;">
+ <el-option label="涓撳埄鏉�" value="patent" />
+ <el-option label="鍟嗘爣鏉�" value="trademark" />
+ <el-option label="钁椾綔鏉�" value="copyright" />
+ <el-option label="杞欢" value="software" />
+ <el-option label="鍦熷湴浣跨敤鏉�" value="land" />
+ <el-option label="鍏朵粬" value="other" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="宸叉憡閿�瀹屾瘯" value="amortized" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="璧勪骇鍘熷�煎悎璁�" :value="totalOriginalValue" precision="2" prefix="楼" />
+ <el-statistic title="绱鎽婇攢鍚堣" :value="totalAmortization" precision="2" prefix="楼" style="margin-left: 30px;" />
+ <el-statistic title="鍑�鍊煎悎璁�" :value="totalNetValue" precision="2" prefix="楼" style="margin-left: 30px;" />
+ </div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板璧勪骇</el-button>
+ <el-button type="warning" @click="handleAmortization" icon="Money">鎽婇攢璁℃彁</el-button>
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ <template #originalValue="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.originalValue) }}</span>
+ </template>
+ <template #accumulatedAmortization="{ row }">
+ <span class="text-warning">楼{{ formatMoney(row.accumulatedAmortization) }}</span>
+ </template>
+ <template #netValue="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.netValue) }}</span>
+ </template>
+ <template #category="{ row }">
+ <el-tag>{{ getCategoryLabel(row.category) }}</el-tag>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)">缂栬緫</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
+ <el-form :model="form" :rules="rules" :disabled="isView" ref="formRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇缂栧彿" prop="assetCode">
+ <el-input v-model="form.assetCode" placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍚嶇О" prop="assetName">
+ <el-input v-model="form.assetName" placeholder="璇疯緭鍏ヨ祫浜у悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧勪骇绫诲埆" prop="category">
+ <el-select v-model="form.category" placeholder="璇烽�夋嫨璧勪骇绫诲埆" style="width: 100%;">
+ <el-option label="涓撳埄鏉�" value="patent" />
+ <el-option label="鍟嗘爣鏉�" value="trademark" />
+ <el-option label="钁椾綔鏉�" value="copyright" />
+ <el-option label="杞欢" value="software" />
+ <el-option label="鍦熷湴浣跨敤鏉�" value="land" />
+ <el-option label="鍏朵粬" value="other" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇佷功缂栧彿" prop="certificateNo">
+ <el-input v-model="form.certificateNo" placeholder="璇疯緭鍏ヨ瘉涔︾紪鍙�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙栧緱鏃ユ湡" prop="acquisitionDate">
+ <el-date-picker v-model="form.acquisitionDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍘熷��" prop="originalValue">
+ <el-input-number v-model="form.originalValue" :min="0" :precision="2" style="width: 100%;" @change="calculateNetValue" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎽婇攢骞撮檺" prop="amortizationPeriod">
+ <el-input-number v-model="form.amortizationPeriod" :min="1" :max="50" style="width: 100%;" />
+ <span style="margin-left: 10px;">骞�</span>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="娈嬪�肩巼" prop="residualRate">
+ <el-input-number v-model="form.residualRate" :min="0" :max="10" :precision="2" style="width: 100%;" />
+ <span style="margin-left: 10px;">%</span>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绱鎽婇攢">
+ <el-input v-model="form.accumulatedAmortization" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璧勪骇鍑�鍊�">
+ <el-input v-model="form.netValue" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏈夋晥鏈熻嚦" prop="validityDate">
+ <el-date-picker v-model="form.validityDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%;">
+ <el-option label="鍦ㄧ敤" value="in_use" />
+ <el-option label="闂茬疆" value="idle" />
+ <el-option label="宸叉憡閿�瀹屾瘯" value="amortized" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker v-model="createTimeDate" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%;" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="璧勪骇鎻忚堪" prop="description">
+ <el-input v-model="form.description" type="textarea" :rows="3" placeholder="璇疯緭鍏ヨ祫浜ф弿杩�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed } from "vue";
+import dayjs from "dayjs";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import {
+ listIntangibleAssetPage,
+ addIntangibleAsset,
+ updateIntangibleAsset,
+ deleteIntangibleAsset,
+ amortizeIntangibleAsset,
+} from "@/api/financialManagement/intangibleAsset";
+
+defineOptions({
+ name: "鏃犲舰璧勪骇",
+});
+
+const filters = reactive({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "璧勪骇缂栧彿", prop: "assetCode", width: "130" },
+ { label: "璧勪骇鍚嶇О", prop: "assetName", width: "150" },
+ { label: "璧勪骇绫诲埆", prop: "category", dataType: "slot", slot: "category" },
+ { label: "璇佷功缂栧彿", prop: "certificateNo", width: "150" },
+ { label: "璧勪骇鍘熷��", prop: "originalValue", dataType: "slot", slot: "originalValue" },
+ { label: "绱鎽婇攢", prop: "accumulatedAmortization", dataType: "slot", slot: "accumulatedAmortization" },
+ { label: "璧勪骇鍑�鍊�", prop: "netValue", dataType: "slot", slot: "netValue" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "180", fixed: "right" },
+];
+
+const dataList = ref([]);
+const multipleList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+const selectedIds = computed(() =>
+ multipleList.value
+ .map(item => item?.id)
+ .filter(id => id !== undefined && id !== null && id !== "")
+);
+
+const createDefaultForm = () => ({
+ assetCode: "",
+ assetName: "",
+ category: "",
+ certificateNo: "",
+ acquisitionDate: "",
+ originalValue: 0,
+ amortizationPeriod: 10,
+ residualRate: 0,
+ accumulatedAmortization: 0,
+ netValue: 0,
+ validityDate: "",
+ status: "in_use",
+ description: "",
+ remark: "",
+ createTime: "",
+});
+
+const form = reactive({
+ ...createDefaultForm(),
+});
+const createTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
+
+const rules = {
+ assetName: [{ required: true, message: "璇疯緭鍏ヨ祫浜у悕绉�", trigger: "blur" }],
+ category: [{ required: true, message: "璇烽�夋嫨璧勪骇绫诲埆", trigger: "change" }],
+ acquisitionDate: [{ required: true, message: "璇烽�夋嫨鍙栧緱鏃ユ湡", trigger: "change" }],
+ originalValue: [{ required: true, message: "璇疯緭鍏ヨ祫浜у師鍊�", trigger: "blur" }],
+ amortizationPeriod: [{ required: true, message: "璇疯緭鍏ユ憡閿�骞撮檺", trigger: "blur" }],
+};
+
+const totalOriginalValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.originalValue), 0);
+});
+
+const totalAmortization = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.accumulatedAmortization), 0);
+});
+
+const totalNetValue = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.netValue), 0);
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getCategoryLabel = (category) => {
+ const map = {
+ patent: "涓撳埄鏉�",
+ trademark: "鍟嗘爣鏉�",
+ copyright: "钁椾綔鏉�",
+ software: "杞欢",
+ land: "鍦熷湴浣跨敤鏉�",
+ other: "鍏朵粬",
+ };
+ return map[category] || category;
+};
+
+const getStatusLabel = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = {
+ in_use: "鍦ㄧ敤",
+ idle: "闂茬疆",
+ expired: "宸插埌鏈�",
+ amortized: "宸叉憡閿�瀹屾瘯",
+ };
+ return map[key] || status;
+};
+
+const getStatusType = (status) => {
+ const key = String(status || "").toLowerCase();
+ const map = { in_use: "success", idle: "warning", expired: "warning", amortized: "info" };
+ return map[key] || "";
+};
+
+const calculateNetValue = () => {
+ const originalValue = Number(form.originalValue || 0);
+ const accumulatedAmortization = Number(form.accumulatedAmortization || 0);
+ form.netValue = Number((originalValue - accumulatedAmortization).toFixed(2));
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁板浐瀹氫负 current/size锛岃繑鍥� data.records/data.total
+const getTableData = async () => {
+ try {
+ const { data } = await listIntangibleAssetPage({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ assetCode: filters.assetCode,
+ assetName: filters.assetName,
+ category: filters.category,
+ status: filters.status,
+ });
+ dataList.value = data?.records || [];
+ multipleList.value = [];
+ pagination.total = Number(data?.total || 0);
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const handleSelectionChange = (selectionList) => {
+ multipleList.value = selectionList;
+};
+
+const resetFilters = () => {
+ filters.assetCode = "";
+ filters.assetName = "";
+ filters.category = "";
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ currentId.value = null;
+ dialogTitle.value = "鏂板鏃犲舰璧勪骇";
+ Object.assign(form, createDefaultForm(), {
+ acquisitionDate: new Date().toISOString().split('T')[0],
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ });
+ dialogVisible.value = true;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫鏃犲舰璧勪骇";
+ Object.assign(form, createDefaultForm(), row);
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ edit(row);
+ isView.value = true;
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm("纭鍒犻櫎璇ユ棤褰㈣祫浜у悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ // 鑱旇皟绾﹀畾锛氬垹闄ゆ帴鍙d娇鐢� ids=1&ids=2
+ await deleteIntangibleAsset([row.id]);
+ if (dataList.value.length === 1 && pagination.currentPage > 1) {
+ pagination.currentPage -= 1;
+ }
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await getTableData();
+ });
+};
+
+const handleAmortization = () => {
+ const ids = selectedIds.value;
+ const confirmText = ids.length
+ ? `纭瀵归�変腑鐨� ${ids.length} 鏉¤祫浜ц繘琛屾湰鏈堟憡閿�璁℃彁鍚楋紵`
+ : "纭杩涜鏈湀鎽婇攢璁℃彁鍚楋紵";
+ ElMessageBox.confirm(confirmText, "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(async () => {
+ await amortizeIntangibleAsset({ ids });
+ ElMessage.success("鎽婇攢璁℃彁瀹屾垚");
+ await getTableData();
+ });
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+ if (isView.value) {
+ dialogVisible.value = false;
+ return;
+ }
+ formRef.value.validate(async valid => {
+ if (valid) {
+ try {
+ calculateNetValue();
+ const payload = { ...form };
+ if (isEdit.value) {
+ payload.id = currentId.value;
+ await updateIntangibleAsset(payload);
+ ElMessage.success("缂栬緫鎴愬姛");
+ } else {
+ await addIntangibleAsset(payload);
+ ElMessage.success("鏂板鎴愬姛");
+ }
+ dialogVisible.value = false;
+ await getTableData();
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+ }
+ });
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+
+ > div:first-child {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/financialStatements/index.vue b/src/views/financialManagement/financialStatements/index.vue
new file mode 100644
index 0000000..5dbce9b
--- /dev/null
+++ b/src/views/financialManagement/financialStatements/index.vue
@@ -0,0 +1,648 @@
+<template>
+ <div style="padding: 20px;">
+ <!-- 椤甸潰鏍囬鍜屾湀浠界瓫閫� -->
+ <div class="w-full md:w-auto flex items-center gap-3"
+ style="margin-bottom: 20px;">
+ <el-date-picker v-model="dateRange"
+ type="monthrange"
+ format="YYYY-MM"
+ value-format="YYYY-MM"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫湀浠�"
+ end-placeholder="缁撴潫鏈堜唤"
+ :disabled-date="disabledDate"
+ @change="handleDateChange"
+ class="w-full md:w-auto"
+ style="margin-right: 30px;" />
+ <el-button type="primary"
+ icon="Refresh"
+ @click="resetDateRange"
+ size="default">
+ 閲嶇疆
+ </el-button>
+ </div>
+ <main class="container mx-auto px-4 pb-10">
+ <!-- 璐㈠姟鎸囨爣鍗$墖 -->
+ <div class="stats-cards">
+ <div class="stat-card stat-card-blue">
+ <div class="stat-icon"><img src="@/assets/icons/png/walletBlue@2x.png"
+ alt="鎬昏惀鏀�" /></div>
+ <div class="stat-content">
+ <div class="stat-label">鎬昏惀鏀�</div>
+ <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' 鍏�' : '' }}</div>
+ </div>
+ </div>
+ <div class="stat-card stat-card-orange">
+ <div class="stat-icon"><img src="@/assets/icons/png/walletOrange@2x.png"
+ alt="鎬绘敮鍑�" /></div>
+ <div class="stat-content">
+ <div class="stat-label">鎬绘敮鍑�</div>
+ <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' 鍏�' : '' }}</div>
+ </div>
+ </div>
+ <div class="stat-card stat-card-green">
+ <div class="stat-icon"><img src="@/assets/icons/png/walletGreen@2x.png"
+ alt="搴旀敹璐︽" /></div>
+ <div class="stat-content">
+ <div class="stat-label">搴旀敹璐︽</div>
+ <div class="stat-value">{{ formatMoney(pageInfo.totalReceivable || 0) }}{{ Math.abs(pageInfo.totalReceivable) < 10000 ? ' 鍏�' : '' }}</div>
+ </div>
+ </div>
+ <div class="stat-card stat-card-red">
+ <div class="stat-icon"><img src="@/assets/icons/png/walletRed@2x.png"
+ alt="搴斾粯璐︽" /></div>
+ <div class="stat-content">
+ <div class="stat-label">搴斾粯璐︽</div>
+ <div class="stat-value">{{ formatMoney(pageInfo.totalPayable || 0) }}{{ Math.abs(pageInfo.totalPayable) < 10000 ? ' 鍏�' : '' }}</div>
+ </div>
+ </div>
+ <div class="stat-card stat-card-yellow">
+ <div class="stat-icon"><img src="@/assets/icons/png/walletYellow@2x.png"
+ alt="鍑�鍒╂鼎" /></div>
+ <div class="stat-content">
+ <div class="stat-label">鍑�鍒╂鼎</div>
+ <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }}{{ Math.abs(pageInfo.netRevenue) < 10000 ? ' 鍏�' : '' }}</div>
+ </div>
+ </div>
+ </div>
+ <!-- 鍥捐〃鍖哄煙 -->
+ <div class="charts-row">
+ <!-- 1. 鏀舵敮鏋勬垚鍒嗘瀽 (鍙岀幆褰㈠浘 + 鍑�鍒╀腑蹇�) -->
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span class="header-title">鏀舵敮鏋勬垚鍙婂噣鍒╁垎鏋�</span>
+ <el-tooltip content="宸︿晶涓烘敹鍏ユ瀯鎴愶紝鍙充晶涓烘敮鍑烘瀯鎴愶紝涓棿灞曠ず鐩堜簭鍑�棰�"
+ placement="top">
+ <el-icon>
+ <QuestionFilled />
+ </el-icon>
+ </el-tooltip>
+ </div>
+ </template>
+ <div class="financial-overview-container">
+ <!-- 鏀跺叆灞曠ず (宸︿晶) -->
+ <div style="width:60%">
+ <div class="overview-item income"
+ style="margin-bottom: 20px;">
+ <div class="overview-box">
+ <div class="icon-circle">
+ <el-icon>
+ <TrendCharts />
+ </el-icon>
+ </div>
+ <div class="data-content">
+ <div class="label">鏈湡鎬绘敹鍏�</div>
+ <div class="value">{{ formatMoney(pageInfo.totalIncome) }}</div>
+ <div class="unit">RMB{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' / 鍏�' : '' }}</div>
+ </div>
+ <div class="bg-decoration">INCOME</div>
+ </div>
+ </div>
+ <div class="overview-item expense">
+ <div class="overview-box">
+ <div class="icon-circle">
+ <el-icon>
+ <Sell />
+ </el-icon>
+ </div>
+ <div class="data-content">
+ <div class="label">鏈湡鎬绘敮鍑�</div>
+ <div class="value">{{ formatMoney(pageInfo.totalExpense) }}</div>
+ <div class="unit">RMB{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' / 鍏�' : '' }}</div>
+ </div>
+ <div class="bg-decoration">EXPENSE</div>
+ </div>
+ </div>
+ </div>
+ <!-- 鍑�鍒╂鼎鏍稿績鎸囩ず (涓棿) -->
+ <div class="profit-indicator">
+ <div class="profit-gauge-wrapper">
+ <Echarts :chartStyle="chartStylePie"
+ :series="profitGaugeSeries"
+ :tooltip="gaugeTooltip"
+ style="height: 200px; width: 100%; max-width: 200px;">
+ </Echarts>
+ <div class="profit-center-text">
+ <div class="label">鍑�鍒╂鼎</div>
+ <div class="value"
+ :class="pageInfo.netRevenue >= 0 ? 'plus' : 'minus'">
+ {{ pageInfo.netRevenue >= 0 ? '+' : '' }}{{ formatMoney(pageInfo.netRevenue) }}
+ </div>
+ <div class="rate">鍒╂鼎鐜�: {{ pageInfo.totalIncome > 0 ? ((pageInfo.netRevenue / pageInfo.totalIncome) * 100).toFixed(1) : 0 }}%</div>
+ </div>
+ </div>
+ </div>
+ <!-- 鏀嚭灞曠ず (鍙充晶) -->
+ </div>
+ </el-card>
+ <!-- 2. 搴旀敹/搴斾粯瀵瑰啿鍒嗘瀽 (鏌辩姸鍥�) -->
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span class="header-title">搴旀敹/搴斾粯姒傝</span>
+ <el-tooltip content="瀵规瘮褰撳墠鍚勬湀浠界殑搴旀敹璐︽涓庡簲浠樿处娆�"
+ placement="top">
+ <el-icon>
+ <QuestionFilled />
+ </el-icon>
+ </el-tooltip>
+ </div>
+ </template>
+ <Echarts :chartStyle="chartStyle"
+ :grid="barGrid"
+ :legend="barLegend"
+ :series="barSeries"
+ :tooltip="barTooltip"
+ :xAxis="barXAxis"
+ :yAxis="barYAxis"
+ style="height: 270px; width: 100%;">
+ </Echarts>
+ </el-card>
+ </div>
+ <!-- 3. 璐㈠姟缁煎悎瓒嬪娍鍒嗘瀽 (鎶樼嚎鍥�) -->
+ <el-card class="trend-chart-card">
+ <template #header>
+ <div class="card-header">
+ <span class="header-title">璐㈠姟缁╂晥缁煎悎瓒嬪娍</span>
+ <el-tooltip content="灞曠ず鏀跺叆銆佹敮鍑哄強鍑�鍒╂鼎鐨勬湀搴﹀彉鍖栬秼鍔�"
+ placement="top">
+ <el-icon>
+ <QuestionFilled />
+ </el-icon>
+ </el-tooltip>
+ </div>
+ </template>
+ <Echarts :chartStyle="chartStyle"
+ :grid="trendGrid"
+ :legend="trendLegend"
+ :series="trendSeries"
+ :tooltip="trendTooltip"
+ :xAxis="trendXAxis"
+ :yAxis="trendYAxis"
+ style="height: 350px; width: 100%;">
+ </Echarts>
+ </el-card>
+ </main>
+ </div>
+</template>
+
+<script setup>
+ import {
+ ref,
+ computed,
+ onMounted,
+ reactive,
+ nextTick,
+ getCurrentInstance,
+ } from "vue";
+ import { QuestionFilled, TrendCharts, Sell } from "@element-plus/icons-vue";
+ import Echarts from "@/components/Echarts/echarts.vue";
+ import { accountStatementDetailsByMonth } from "@/api/financialManagement/financialStatements";
+ import dayjs from "dayjs";
+
+ const { proxy } = getCurrentInstance();
+ const dateRange = ref(null);
+ const pageInfo = reactive({
+ totalIncome: 0,
+ totalExpense: 0,
+ totalReceivable: 0,
+ totalPayable: 0,
+ netRevenue: 0,
+ });
+
+ const chartStyle = { width: "100%", height: "100%", position: "relative" };
+ const chartStylePie = { width: "100%", height: "100%" };
+
+ const monthlyTrendList = ref([]);
+ const receivablePayableList = ref([]);
+
+ // --- 1. 鏀舵敮鏋勬垚鍒嗘瀽 (绠�鍖栫増閫昏緫) ---
+ const gaugeTooltip = { show: false };
+
+ const profitGaugeSeries = computed(() => {
+ const rate =
+ pageInfo.totalIncome > 0
+ ? (pageInfo.netRevenue / pageInfo.totalIncome) * 100
+ : 0;
+ return [
+ {
+ type: "gauge",
+ startAngle: 210,
+ endAngle: -30,
+ min: 0,
+ max: 100,
+ splitNumber: 10,
+ radius: "100%",
+ progress: {
+ show: true,
+ width: 14,
+ itemStyle: { color: pageInfo.netRevenue >= 0 ? "#10b981" : "#f43f5e" },
+ },
+ pointer: { show: false },
+ axisLine: { lineStyle: { width: 14, color: [[1, "#f1f5f9"]] } },
+ axisTick: { show: false },
+ splitLine: { show: false },
+ axisLabel: { show: false },
+ anchor: { show: false },
+ title: { show: false },
+ detail: { show: false },
+ data: [{ value: Math.max(0, Math.min(100, rate)) }],
+ },
+ ];
+ });
+
+ // --- 2. 搴旀敹/搴斾粯姒傝 (鏌辩姸鍥�) ---
+ const barGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true };
+ const barLegend = { top: "0", right: "center" };
+ const barXAxis = computed(() => [
+ {
+ type: "category",
+ data: receivablePayableList.value.map(item => item.month || ""),
+ axisTick: { alignWithLabel: true },
+ },
+ ]);
+ const barYAxis = [{ type: "value", name: "閲戦 (鍏�)" }];
+ const barTooltip = { trigger: "axis", axisPointer: { type: "shadow" } };
+ const barSeries = computed(() => [
+ {
+ name: "搴旀敹璐︽",
+ type: "bar",
+ barWidth: "30%",
+ data: receivablePayableList.value.map(item => item.receivable || 0),
+ itemStyle: { color: "#10b981" },
+ },
+ {
+ name: "搴斾粯璐︽",
+ type: "bar",
+ barWidth: "30%",
+ data: receivablePayableList.value.map(item => item.payable || 0),
+ itemStyle: { color: "#ef4444" },
+ },
+ ]);
+
+ // --- 3. 璐㈠姟缁煎悎瓒嬪娍鍒嗘瀽 (鎶樼嚎鍥�) ---
+ const trendGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true };
+ const trendLegend = { top: "0", right: "center" };
+ const trendXAxis = computed(() => [
+ {
+ type: "category",
+ boundaryGap: false,
+ data: monthlyTrendList.value.map(item => item.month || ""),
+ },
+ ]);
+ const trendYAxis = [{ type: "value", name: "閲戦 (鍏�)" }];
+ const trendTooltip = { trigger: "axis" };
+ const trendSeries = computed(() => [
+ {
+ name: "鎬昏惀鏀�",
+ type: "line",
+ smooth: true,
+ data: monthlyTrendList.value.map(item => item.income || 0),
+ itemStyle: { color: "#4f46e5" },
+ areaStyle: { opacity: 0.1 },
+ },
+ {
+ name: "鎬绘敮鍑�",
+ type: "line",
+ smooth: true,
+ data: monthlyTrendList.value.map(item => item.expense || 0),
+ itemStyle: { color: "#f97316" },
+ },
+ {
+ name: "鍑�鍒╂鼎",
+ type: "line",
+ smooth: true,
+ data: monthlyTrendList.value.map(item => item.profit || 0),
+ lineStyle: { width: 4, type: "dashed" },
+ itemStyle: { color: "#10b981" },
+ },
+ ]);
+
+ // --- 鍏敤閫昏緫 ---
+ const formatMoney = val => {
+ return val;
+ };
+
+ const handleDateChange = val => {
+ if (val) getData();
+ };
+
+ const resetDateRange = () => {
+ dateRange.value = [
+ dayjs().subtract(5, "month").format("YYYY-MM"),
+ dayjs().format("YYYY-MM"),
+ ];
+ getData();
+ };
+
+ const disabledDate = time => dayjs(time).isAfter(dayjs(), "month");
+
+ const getData = async () => {
+ if (!dateRange.value || dateRange.value.length !== 2) return;
+
+ const params = {
+ entryDateStart: dayjs(dateRange.value[0])
+ .startOf("month")
+ .format("YYYY-MM-DD"),
+ entryDateEnd: dayjs(dateRange.value[1]).endOf("month").format("YYYY-MM-DD"),
+ };
+
+ try {
+ const res = await accountStatementDetailsByMonth(params);
+ if (res.code === 200 && res.data) {
+ const data = res.data;
+ // 鏇存柊椤堕儴姹囨�诲崱鐗囨暟鎹�
+ pageInfo.totalIncome = data.totalIncome || 0;
+ pageInfo.totalExpense = data.totalExpense || 0;
+ pageInfo.totalReceivable = data.accountsReceivable || 0;
+ pageInfo.totalPayable = data.accountsPayable || 0;
+ pageInfo.netRevenue = data.netRevenue || 0;
+
+ // 鏇存柊鍥捐〃鏁版嵁
+ monthlyTrendList.value = data.monthlyTrendList || [];
+ receivablePayableList.value = data.receivablePayableList || [];
+ }
+ } catch (error) {
+ console.error("鑾峰彇璐㈠姟鎶ヨ〃鏁版嵁澶辫触锛�", error);
+ }
+ };
+
+ onMounted(() => {
+ resetDateRange();
+ });
+</script>
+
+<style scoped lang="scss">
+ .stats-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
+ gap: 20px;
+ margin-bottom: 24px;
+ }
+
+ .stat-card {
+ background: #fff;
+ border: 1px solid #edf2f7;
+ border-radius: 12px;
+ padding: 24px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+
+ &:hover {
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ transform: translateY(-4px);
+ }
+
+ .stat-icon {
+ width: 56px;
+ height: 56px;
+ background: #f7fafc;
+ border-radius: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ img {
+ width: 32px;
+ height: 32px;
+ }
+ }
+
+ .stat-content {
+ .stat-label {
+ font-size: 14px;
+ color: #718096;
+ margin-bottom: 4px;
+ }
+ .stat-value {
+ font-size: 20px;
+ font-weight: 700;
+ color: #2d3748;
+ }
+ }
+ }
+
+ .charts-row {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 24px;
+ margin-bottom: 24px;
+ }
+
+ @media (min-width: 1200px) {
+ .charts-row {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ }
+
+ .chart-card,
+ .trend-chart-card {
+ border-radius: 16px;
+ border: none;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
+
+ .card-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ .header-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #1a202c;
+ }
+ .el-icon {
+ color: #a0aec0;
+ cursor: help;
+ }
+ }
+ }
+
+ .financial-overview-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: nowrap;
+ gap: 10px;
+ padding: 20px 0;
+ width: 100%;
+ overflow: hidden;
+
+ .overview-item {
+ flex: 1;
+ min-width: 0; // 鍏佽鍦� flex 瀹瑰櫒涓缉鍐欙紝闃叉鍐呭鎾戝紑
+ display: flex;
+ justify-content: center;
+
+ .overview-box {
+ position: relative;
+ width: 100%;
+ max-width: 320px;
+ height: 110px;
+ background: #f8fafc;
+ border-radius: 12px;
+ padding: 12px 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ overflow: hidden;
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
+ }
+
+ .icon-circle {
+ flex-shrink: 0;
+ width: 42px;
+ height: 42px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 20px;
+ z-index: 2;
+ }
+
+ .data-content {
+ z-index: 2;
+ min-width: 0;
+ .label {
+ font-size: 13px;
+ color: #718096;
+ margin-bottom: 2px;
+ font-weight: 500;
+ white-space: nowrap;
+ }
+ .value {
+ font-size: 18px;
+ font-weight: 800;
+ color: #1a202c;
+ line-height: 1.2;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .unit {
+ font-size: 11px;
+ color: #a0aec0;
+ }
+ }
+
+ .bg-decoration {
+ position: absolute;
+ right: -5px;
+ bottom: -5px;
+ font-size: 32px;
+ font-weight: 950;
+ color: rgba(0, 0, 0, 0.03);
+ font-style: italic;
+ user-select: none;
+ z-index: 1;
+ }
+ }
+
+ &.income {
+ .icon-circle {
+ background: #eef2ff;
+ color: #4f46e5;
+ }
+ .overview-box {
+ border-left: 5px solid #4f46e5;
+ }
+ }
+
+ &.expense {
+ .icon-circle {
+ background: #fff7ed;
+ color: #f97316;
+ }
+ .overview-box {
+ border-left: 5px solid #f97316;
+ }
+ }
+ }
+
+ .profit-indicator {
+ flex: 0 40%; // 鍥哄畾瀹藉害锛屼笉鍙備笌寮规�х缉鏀句互淇濊瘉浠〃鐩樺畬鏁�
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .profit-gauge-wrapper {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ // max-width: 180px;
+
+ .profit-center-text {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ text-align: center;
+ width: 100%;
+
+ .label {
+ font-size: 12px;
+ color: #718096;
+ font-weight: 500;
+ }
+
+ .value {
+ font-size: 20px;
+ font-weight: 800;
+ margin: 2px 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+
+ &.plus {
+ color: #10b981;
+ }
+
+ &.minus {
+ color: #f43f5e;
+ }
+ }
+
+ .rate {
+ font-size: 11px;
+ color: #a0aec0;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+
+ // 閽堝闈炲父绐勭殑灞忓箷杩涜鏁翠綋缂╂斁
+ @media (max-width: 1400px) {
+ transform-origin: center;
+ // 濡傛灉瀹瑰櫒澶獎锛岄�氳繃缂╁皬鍐呴儴鍏冪礌鏉ラ�傚簲
+ // 杩欓噷涓嶄娇鐢� transform: scale 鍥犱负浼氬奖鍝嶅竷灞�娴侊紝鏀圭敤鍐呴儴灏哄寰皟
+ .overview-item .overview-box {
+ padding: 10px;
+ gap: 8px;
+ .value {
+ font-size: 16px;
+ }
+ .icon-circle {
+ width: 36px;
+ height: 36px;
+ font-size: 18px;
+ }
+ }
+ .profit-indicator {
+ flex: 0 40%;
+ .profit-gauge-wrapper .value {
+ font-size: 18px;
+ }
+ }
+ }
+ }
+</style>
diff --git a/src/views/financialManagement/generalLedger/index.vue b/src/views/financialManagement/generalLedger/index.vue
new file mode 100644
index 0000000..a7b1d30
--- /dev/null
+++ b/src/views/financialManagement/generalLedger/index.vue
@@ -0,0 +1,498 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="绉戠洰缂栫爜:">
+ <el-input v-model="filters.subjectCode"
+ placeholder="璇疯緭鍏ョ鐩紪鐮�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="绉戠洰鍚嶇О:">
+ <el-input v-model="filters.subjectName"
+ placeholder="璇疯緭鍏ョ鐩悕绉�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="绉戠洰绫诲瀷:">
+ <el-select v-model="filters.subjectType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 200px;">
+ <el-option label="璧勪骇绫�"
+ value="璧勪骇绫�" />
+ <el-option label="璐熷�虹被"
+ value="璐熷�虹被" />
+ <el-option label="鏉冪泭绫�"
+ value="鏉冪泭绫�" />
+ <el-option label="鎴愭湰绫�"
+ value="鎴愭湰绫�" />
+ <el-option label="鎹熺泭绫�"
+ value="鎹熺泭绫�" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary"
+ @click="add"
+ icon="Plus">鏂板</el-button>
+ <!-- <el-button @click="handleOut"
+ icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <el-table ref="tableRef"
+ v-loading="loading"
+ :data="dataList"
+ row-key="id"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ height="calc(100vh - 280px)"
+ border
+ stripe
+ highlight-current-row
+ class="subject-table">
+ <el-table-column label="绉戠洰缂栫爜" prop="subjectCode" width="140">
+ <template #default="scope">
+ <span class="subject-code">{{ scope.row.subjectCode }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="绉戠洰鍚嶇О" prop="subjectName" min-width="180">
+ <template #default="scope">
+ <span class="subject-name" :class="{ 'is-parent': scope.row.children?.length > 0 }">
+ {{ scope.row.subjectName }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="绉戠洰绫诲瀷" prop="subjectType" width="100" align="center">
+ <template #default="scope">
+ <el-tag size="small" :type="getSubjectTypeType(scope.row.subjectType)">
+ {{ scope.row.subjectType }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浣欓鏂瑰悜" prop="balanceDirection" width="100" align="center">
+ <template #default="scope">
+ <el-tag size="small" :type="scope.row.balanceDirection === '鍊熸柟' ? 'primary' : 'danger'">
+ {{ scope.row.balanceDirection }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" prop="status" width="80" align="center">
+ <template #default="scope">
+ <el-tag size="small" :type="scope.row.status === 0 || scope.row.status === '0' ? 'success' : 'info'">
+ {{ scope.row.status === 0 || scope.row.status === '0' ? '鍚敤' : '绂佺敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" prop="remark" show-overflow-tooltip min-width="150" />
+ <el-table-column label="鎿嶄綔" align="center" fixed="right" width="240">
+ <template #default="scope">
+ <el-button link type="primary" icon="Plus" @click="addChild(scope.row)">鏂板</el-button>
+ <el-button link type="primary" icon="Edit" @click="edit(scope.row)">缂栬緫</el-button>
+ <el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <FormDialog :title="dialogTitle"
+ v-model="dialogVisible"
+ width="600px"
+ @confirm="submitForm"
+ @cancel="dialogVisible = false">
+ <el-form :model="form"
+ :rules="rules"
+ ref="formRef"
+ label-width="100px">
+ <el-form-item label="鐖剁骇绉戠洰">
+ <el-input :model-value="parentSubjectLabel"
+ disabled />
+ </el-form-item>
+ <el-form-item label="绉戠洰缂栫爜"
+ prop="subjectCode">
+ <el-input v-model="form.subjectCode"
+ placeholder="璇疯緭鍏ョ鐩紪鐮�" />
+ </el-form-item>
+ <el-form-item label="绉戠洰鍚嶇О"
+ prop="subjectName">
+ <el-input v-model="form.subjectName"
+ placeholder="璇疯緭鍏ョ鐩悕绉�" />
+ </el-form-item>
+ <el-form-item label="绉戠洰绫诲瀷"
+ prop="subjectType">
+ <el-select v-model="form.subjectType"
+ placeholder="璇烽�夋嫨绉戠洰绫诲瀷"
+ style="width: 100%;">
+ <el-option label="璧勪骇绫�"
+ value="璧勪骇绫�" />
+ <el-option label="璐熷�虹被"
+ value="璐熷�虹被" />
+ <el-option label="鏉冪泭绫�"
+ value="鏉冪泭绫�" />
+ <el-option label="鎴愭湰绫�"
+ value="鎴愭湰绫�" />
+ <el-option label="鎹熺泭绫�"
+ value="鎹熺泭绫�" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浣欓鏂瑰悜"
+ prop="balanceDirection">
+ <el-radio-group v-model="form.balanceDirection">
+ <el-radio label="鍊熸柟">鍊熸柟</el-radio>
+ <el-radio label="璐锋柟">璐锋柟</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鐘舵��"
+ prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio :label="0">鍚敤</el-radio>
+ <el-radio :label="1">绂佺敤</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance, nextTick } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import {
+ listAccountSubject,
+ addAccountSubject,
+ updateAccountSubject,
+ delAccountSubject,
+ exportAccountSubject,
+ } from "@/api/financialManagement/accountSubject";
+
+ defineOptions({
+ name: "鎬诲笎绉戠洰",
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ const filters = reactive({
+ subjectCode: "",
+ subjectName: "",
+ subjectType: "",
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "绉戠洰缂栫爜", prop: "subjectCode", width: "120" },
+ { label: "绉戠洰鍚嶇О", prop: "subjectName", width: "150" },
+ { label: "绉戠洰绫诲瀷", prop: "subjectType" },
+ {
+ label: "浣欓鏂瑰悜",
+ prop: "balanceDirection",
+ dataType: "tag",
+ formatData: value => {
+ if (value === "鍊熸柟") {
+ return "鍊熸柟";
+ }
+ return "璐锋柟";
+ },
+ formatType: value => {
+ if (value === "鍊熸柟") {
+ return "primary";
+ }
+ return "danger";
+ },
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ formatData: value => {
+ if (value === 0 || value === "0") {
+ return "鍚敤";
+ }
+ return "绂佺敤";
+ },
+ formatType: value => {
+ if (value === 0 || value === "0") {
+ return "success";
+ }
+ return "info";
+ },
+ },
+
+ { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: "220",
+ operation: [
+ {
+ name: "鏂板",
+ type: "primary",
+ clickFun: row => {
+ addChild(row);
+ },
+ },
+ {
+ name: "缂栬緫",
+ type: "primary",
+ clickFun: row => {
+ edit(row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ];
+
+ const dataList = ref([]);
+ const dialogVisible = ref(false);
+ const dialogTitle = ref("");
+ const parentSubjectLabel = ref("椤剁骇绉戠洰");
+ const formRef = ref(null);
+ const tableRef = ref(null);
+ const isEdit = ref(false);
+ const loading = ref(false);
+
+ const form = reactive({
+ id: undefined,
+ parentId: null,
+ subjectCode: "",
+ subjectName: "",
+ subjectType: "",
+ balanceDirection: "鍊熸柟",
+ status: 0,
+ remark: "",
+ });
+
+ const rules = {
+ subjectCode: [{ required: true, message: "璇疯緭鍏ョ鐩紪鐮�", trigger: "blur" }],
+ subjectName: [{ required: true, message: "璇疯緭鍏ョ鐩悕绉�", trigger: "blur" }],
+ subjectType: [
+ { required: true, message: "璇烽�夋嫨绉戠洰绫诲瀷", trigger: "change" },
+ ],
+ };
+
+ const getSubjectTypeType = type => {
+ const map = {
+ 璧勪骇绫�: "success",
+ 璐熷�虹被: "danger",
+ 鏉冪泭绫�: "warning",
+ 鎴愭湰绫�: "info",
+ 鎹熺泭绫�: "primary",
+ };
+ return map[type] || "";
+ };
+
+ const getTableData = () => {
+ loading.value = true;
+ const query = {
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ ...filters,
+ };
+ listAccountSubject(query).then(response => {
+ dataList.value = response.data.records || [];
+ loading.value = false;
+ }).catch(() => {
+ loading.value = false;
+ });
+ };
+
+ const resetFilters = () => {
+ filters.subjectCode = "";
+ filters.subjectName = "";
+ filters.subjectType = "";
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = obj => {
+ pagination.currentPage = obj.page;
+ pagination.pageSize = obj.limit;
+ getTableData();
+ };
+
+ const buildParentSubjectLabel = parentRow => {
+ if (!parentRow) {
+ return "椤剁骇绉戠洰";
+ }
+ const code = parentRow.subjectCode || "";
+ const name = parentRow.subjectName || "";
+ return `${code} ${name}`.trim();
+ };
+
+ const resetForm = ({ parentId = null, parentRow = null } = {}) => {
+ Object.assign(form, {
+ id: undefined,
+ parentId,
+ subjectCode: "",
+ subjectName: "",
+ subjectType: "",
+ balanceDirection: "鍊熸柟",
+ status: 0,
+ remark: "",
+ });
+ parentSubjectLabel.value = buildParentSubjectLabel(parentRow);
+ };
+
+ const add = () => {
+ isEdit.value = false;
+ dialogTitle.value = "鏂板绉戠洰";
+ resetForm({ parentId: null, parentRow: null });
+ dialogVisible.value = true;
+ };
+
+ const addChild = row => {
+ isEdit.value = false;
+ dialogTitle.value = "鏂板瀛愮鐩�";
+ resetForm({ parentId: row.id, parentRow: row });
+ form.subjectType = row.subjectType || "";
+ form.balanceDirection = row.balanceDirection || "鍊熸柟";
+ dialogVisible.value = true;
+ };
+
+ const findSubjectById = (nodes, id) => {
+ for (const item of nodes || []) {
+ if (item.id === id) {
+ return item;
+ }
+ if (item.children && item.children.length > 0) {
+ const found = findSubjectById(item.children, id);
+ if (found) {
+ return found;
+ }
+ }
+ }
+ return null;
+ };
+
+ const edit = row => {
+ isEdit.value = true;
+ dialogTitle.value = "缂栬緫绉戠洰";
+ Object.assign(form, row);
+ form.parentId = row.parentId ?? null;
+ const parentRow =
+ row.parentId === null || row.parentId === undefined
+ ? null
+ : findSubjectById(dataList.value, row.parentId);
+ parentSubjectLabel.value = parentRow
+ ? buildParentSubjectLabel(parentRow)
+ : row.parentId
+ ? `涓婄骇ID: ${row.parentId}`
+ : buildParentSubjectLabel(null);
+ dialogVisible.value = true;
+ };
+
+ const submitForm = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ if (isEdit.value) {
+ updateAccountSubject(form).then(() => {
+ ElMessage.success("缂栬緫鎴愬姛");
+ dialogVisible.value = false;
+ getTableData();
+ });
+ } else {
+ addAccountSubject(form).then(() => {
+ ElMessage.success("鏂板鎴愬姛");
+ dialogVisible.value = false;
+ getTableData();
+ });
+ }
+ }
+ });
+ };
+
+ const handleDelete = row => {
+ const ids = row.id;
+ ElMessageBox.confirm("纭鍒犻櫎璇ョ鐩悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ return delAccountSubject(ids);
+ })
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ });
+ };
+
+ const handleOut = () => {
+ proxy.download(
+ "accountSubject/export",
+ {
+ ...filters,
+ },
+ `account_subject_${new Date().getTime()}.xlsx`
+ );
+ };
+
+ onMounted(() => {
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ }
+
+ .subject-table {
+ border-radius: 8px;
+ overflow: hidden;
+
+ :deep(.el-table__row) {
+ transition: background-color 0.3s;
+ }
+
+ :deep(.el-table__row:hover) {
+ background-color: #f5f7fa;
+ }
+
+ .subject-code {
+ color: #606266;
+ }
+
+ .subject-name {
+ font-weight: 500;
+
+ &.is-parent {
+ color: #409eff;
+ }
+ }
+ }
+</style>
diff --git a/src/views/financialManagement/inventoryAccounting/index.vue b/src/views/financialManagement/inventoryAccounting/index.vue
new file mode 100644
index 0000000..3acf5d9
--- /dev/null
+++ b/src/views/financialManagement/inventoryAccounting/index.vue
@@ -0,0 +1,390 @@
+<template>
+ <div class="inventory-statistics">
+ <!-- 绛涢�夎〃鍗� -->
+ <div class="filter-form">
+ <el-form :model="filterForm" inline>
+<!-- <el-form-item label="鏃堕棿鑼冨洿">-->
+<!-- <el-date-picker-->
+<!-- v-model="filterForm.dateRange"-->
+<!-- type="daterange"-->
+<!-- range-separator="鑷�"-->
+<!-- start-placeholder="寮�濮嬫棩鏈�"-->
+<!-- end-placeholder="缁撴潫鏃ユ湡"-->
+<!-- />-->
+<!-- </el-form-item>-->
+<!-- <el-form-item label="渚涘簲鍟嗗悕绉�">-->
+<!-- <el-input v-model="filterForm.supplierName" style="width: 240px" placeholder="璇疯緭鍏�" clearable prefix-icon="Search" />-->
+<!-- </el-form-item>-->
+<!-- <el-form-item label="浜у搧鍚嶇О">-->
+<!-- <el-input v-model="filterForm.productCategory" style="width: 240px" placeholder="璇疯緭鍏�" clearable prefix-icon="Search" />-->
+<!-- </el-form-item>-->
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鏌ヨ</el-button>
+<!-- <el-button @click="handleReset">閲嶇疆</el-button>-->
+<!-- <el-button type="success" @click="handleExport">瀵煎嚭</el-button>-->
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <!-- 缁熻姹囨�诲崱鐗� -->
+ <div class="summary-cards">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-card class="summary-card">
+ <div class="summary-item">
+ <p class="summary-title">鎬诲簱瀛樻暟閲�</p>
+ <p class="summary-value">{{ summaryData.totalInventoryCount }}</p>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="summary-card">
+ <div class="summary-item">
+ <p class="summary-title">鎬诲簱瀛橀噾棰�</p>
+ <p class="summary-value">楼{{ summaryData.totalInventoryValue }}</p>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="summary-card">
+ <div class="summary-item">
+ <p class="summary-title">搴撳瓨鍙樺姩鏁伴噺</p>
+ <p class="summary-value">{{ summaryData.inventoryChangeCount }}</p>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="summary-card">
+ <div class="summary-item">
+ <p class="summary-title">搴撳瓨鍙樺姩閲戦</p>
+ <p class="summary-value">楼{{ summaryData.inventoryChangeValue }}</p>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 鍥捐〃鍖哄煙 -->
+ <div class="chart-section">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span>搴撳瓨鍒嗙被鍗犳瘮</span>
+ </div>
+ </template>
+ <div id="category-pie-chart" style="height: 400px;"></div>
+ </el-card>
+ </el-col>
+ <el-col :span="12">
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span>搴撳瓨閲戦瓒嬪娍</span>
+ </div>
+ </template>
+ <div id="amount-trend-chart" style="height: 400px;"></div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ v-loading="loading"
+ border
+ style="width: 100%"
+ :header-cell-style="{ background: '#f5f7fa', color: '#606266' }"
+ >
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" width="240" show-overflow-tooltip />
+ <el-table-column label="浜у搧" prop="productCategory" min-width="100" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" min-width="200" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="70" show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱鏁伴噺" prop="inboundNum" width="90" show-overflow-tooltip />
+ <el-table-column label="搴撳瓨鏁伴噺" prop="inboundNum0" width="90" show-overflow-tooltip />
+ <el-table-column label="鍚◣鍗曚环" prop="taxInclusiveUnitPrice" width="100" show-overflow-tooltip />
+ <el-table-column label="鍚◣鎬讳环" prop="taxInclusiveTotalPrice" width="100" show-overflow-tooltip />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" width="80" show-overflow-tooltip />
+ <el-table-column label="涓嶅惈绋庢�讳环" prop="taxExclusiveTotalPrice" width="100" show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱浜�" prop="createBy" width="100" show-overflow-tooltip />
+ </el-table>
+ <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper" :page="page.current" :limit="page.size" @pagination="paginationChange" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick } from 'vue'
+import * as echarts from 'echarts'
+import {getStockInChartData, getStockInPage} from "@/api/inventoryManagement/stockIn.js";
+
+// 鐘舵�佸彉閲�
+const loading = ref(false)
+const total = ref(0)
+const tableData = ref([])
+const summaryData = ref({})
+const page = reactive({
+ current: 1,
+ size: 100,
+})
+
+// 鍥捐〃瀹炰緥
+const categoryPieChart = ref(null)
+const amountTrendChart = ref(null)
+
+// 绛涢�夎〃鍗�
+const filterForm = reactive({
+ dateRange: [],
+ supplierName: '',
+ productCategory: ''
+})
+
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ loadData()
+}
+
+// 鍒濆鍖栨暟鎹�
+onMounted(() => {
+ loadSummaryData()
+ loadData()
+})
+
+// 鍔犺浇缁熻姹囨�绘暟鎹�
+const loadSummaryData = () => {
+ getStockInChartData().then(res => {
+ summaryData.value = res.data
+ })
+}
+
+// 鍔犺浇搴撳瓨鏁版嵁
+const loadData = () => {
+ loading.value = true
+ getStockInPage({ ...filterForm, ...page }).then(res => {
+ loading.value = false
+ tableData.value = res.data.records
+ total.value = res.data.total
+ console.log('res', res.data.records)
+
+ // 鏁版嵁鍔犺浇瀹屾垚鍚庢覆鏌撳浘琛�
+ nextTick(() => {
+ renderCategoryPieChart()
+ renderAmountTrendChart()
+ })
+ }).catch(() => {
+ loading.value = false
+ })
+}
+
+// 娓叉煋鍒嗙被鍗犳瘮楗煎浘
+const renderCategoryPieChart = () => {
+ if (!categoryPieChart.value) {
+ categoryPieChart.value = echarts.init(document.getElementById('category-pie-chart'))
+ }
+ // 鏍规嵁 tableData 鎸� productCategory 鍒嗙被骞惰绠� inboundNum0 鏁伴噺鎬诲拰
+ const categoryMap = tableData.value.reduce((acc, cur) => {
+ acc[cur.productCategory] = (acc[cur.productCategory] || 0) + cur.inboundNum0
+ return acc
+ }, {})
+
+ // 灏嗗垎绫荤粨鏋滆浆鎹负 ECharts 楗煎浘鎵�闇�鐨勬暟鎹牸寮�
+ const categoryData = Object.entries(categoryMap).map(([name, value]) => ({
+ name: name,
+ value: value
+ }))
+ const option = {
+ title: {
+ text: '搴撳瓨鍒嗙被鍗犳瘮',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{a} <br/>{b}: {c} ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ series: [
+ {
+ name: '搴撳瓨鍒嗙被',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: '#fff',
+ borderWidth: 2
+ },
+ label: {
+ show: true,
+ formatter: '{b}: {d}%'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: '16',
+ fontWeight: 'bold'
+ }
+ },
+ data: categoryData
+ }
+ ]
+ }
+
+ categoryPieChart.value.setOption(option)
+}
+// 娓叉煋閲戦瓒嬪娍鎶樼嚎鍥�
+const renderAmountTrendChart = () => {
+ if (!amountTrendChart.value) {
+ amountTrendChart.value = echarts.init(document.getElementById('amount-trend-chart'))
+ }
+ // 鎸夋湀浠藉垎缁勫苟璁$畻taxInclusiveTotalPrice鎬诲拰
+ const monthlyAmounts = tableData.value.reduce((acc, cur) => {
+ const date = new Date(cur.createTime);
+ const month = date.getMonth() + 1;
+
+ // 纭繚month鍦�1-12鑼冨洿鍐�
+ if (month >= 1 && month <= 12) {
+ acc[month] = (acc[month] || 0) + cur.taxInclusiveTotalPrice;
+ }
+ return acc;
+ }, {});
+
+ // 鐢熸垚12涓湀鐨勬暟鎹紝缂哄け鏈堜唤鐢�0浠f浛
+ const amounts = [];
+ for (let i = 1; i <= 12; i++) {
+ amounts.push(monthlyAmounts[i] || 0);
+ }
+ const dates = ['1鏈�', '2鏈�', '3鏈�', '4鏈�', '5鏈�', '6鏈�', '7鏈�', '8鏈�', '9鏈�', '10鏈�', '11鏈�', '12鏈�']
+
+ const option = {
+ title: {
+ text: '搴撳瓨閲戦瓒嬪娍',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis',
+ formatter: '{b}: 楼{c}'
+ },
+ xAxis: {
+ type: 'category',
+ data: dates
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: {
+ formatter: '楼{value}'
+ }
+ },
+ series: [
+ {
+ name: '搴撳瓨閲戦',
+ type: 'line',
+ data: amounts,
+ smooth: true,
+ areaStyle: {}
+ }
+ ]
+ }
+
+ amountTrendChart.value.setOption(option)
+}
+
+// 鏌ヨ鎿嶄綔
+const handleSearch = () => {
+ loadData()
+}
+
+// 閲嶇疆鎿嶄綔
+const handleReset = () => {
+ filterForm.dateRange = []
+ filterForm.supplierName = ''
+ filterForm.productCategory = ''
+ loadData()
+}
+
+// 瀵煎嚭鎿嶄綔
+const handleExport = () => {
+ console.log('瀵煎嚭鏁版嵁')
+}
+
+// 绐楀彛澶у皬鏀瑰彉鏃讹紝閲嶆柊璋冩暣鍥捐〃澶у皬
+window.addEventListener('resize', () => {
+ if (categoryPieChart.value) categoryPieChart.value.resize()
+ if (amountTrendChart.value) amountTrendChart.value.resize()
+})
+</script>
+
+<style scoped>
+.inventory-statistics {
+ padding: 20px;
+}
+
+.filter-form {
+ margin-bottom: 20px;
+}
+
+.summary-cards {
+ margin-bottom: 20px;
+}
+
+.summary-card {
+ text-align: center;
+ height: 100px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.summary-item {
+ width: 100%;
+}
+
+.summary-title {
+ font-size: 14px;
+ color: #606266;
+ margin-bottom: 5px;
+}
+
+.summary-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #303133;
+}
+
+.summary-value.warning {
+ color: #e6a23c;
+}
+
+.summary-value.danger {
+ color: #f56c6c;
+}
+
+.chart-section {
+ margin-bottom: 20px;
+}
+
+.chart-card {
+ height: 460px;
+}
+
+.card-header {
+ font-weight: bold;
+}
+
+.table_list {
+ margin-top: 20px;
+}
+
+.pagination {
+ text-align: right;
+ margin-top: 20px;
+}
+</style>
diff --git a/src/views/financialManagement/payable/input-invoice.vue b/src/views/financialManagement/payable/input-invoice.vue
new file mode 100644
index 0000000..86ebd09
--- /dev/null
+++ b/src/views/financialManagement/payable/input-invoice.vue
@@ -0,0 +1,945 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍙戠エ鍙风爜:">
+ <el-input v-model="filters.invoiceNumber" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�绁ㄦ棩鏈�:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="姝e父" :value="0" />
+ <el-option label="浣滃簾" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">褰曞叆鍙戠エ</el-button>
+ <el-button @click="handleExport" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #taxAmount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.taxAmount) }}</span>
+ </template>
+ <template #totalAmount="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.totalAmount) }}</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)" effect="light" round>
+ {{ getStatusLabel(row.status) }}
+ </el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="warning" link @click="handleCancel(row)" v-if="isNormalStatus(row.status)">浣滃簾</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐘舵��">
+ <el-tag :type="getStatusType(form.status)" effect="light" round>
+ {{ getStatusLabel(form.status) }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ鍙风爜" prop="invoiceNo">
+ <el-input v-model="form.invoiceNo" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" :disabled="isView" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="form.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 100%;"
+ filterable
+ :disabled="isView"
+ @change="handleSupplierChange"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鍏ュ簱鍗�" prop="stockInRecordIds">
+ <el-input
+ :model-value="inboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨渚涘簲鍟�"
+ readonly
+ :disabled="!form.supplierId || isView"
+ class="inbound-batch-input"
+ @click="handleInboundInputClick"
+ >
+ <template v-if="!isView" #append>
+ <el-button
+ :disabled="!form.supplierId"
+ :loading="inboundBatchLoading"
+ @click.stop="openInboundSelectDialog"
+ >
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�绁ㄦ棩鏈�" prop="invoiceDate">
+ <el-date-picker
+ v-model="form.invoiceDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+ <el-select
+ v-model="form.invoiceType"
+ placeholder="璇烽�夋嫨鍙戠エ绫诲瀷"
+ style="width: 100%;"
+ :disabled="isView"
+ >
+ <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+ <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+ <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼" prop="taxRate">
+ <el-select
+ v-model="form.taxRate"
+ placeholder="璇烽�夋嫨绋庣巼"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="handleTaxRateChange"
+ >
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="Number(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閲戦(涓嶅惈绋�)" prop="amount">
+ <el-input-number
+ v-model="form.amount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ placeholder="鏍规嵁鍏ュ簱鍗曞惈绋庨噾棰濊嚜鍔ㄦ崲绠楋紝鍙慨鏀�"
+ @change="calculateTaxFromExclusive"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="绋庨">
+ <el-input-number
+ v-model="form.taxAmount"
+ :min="0"
+ :precision="2"
+ :controls="false"
+ style="width: 100%;"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浠风◣鍚堣">
+ <el-input-number
+ v-model="form.totalAmount"
+ :min="0"
+ :precision="2"
+ :controls="false"
+ style="width: 100%;"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍙戠エ鍐呭" prop="content">
+ <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <el-dialog
+ v-model="inboundSelectVisible"
+ title="閫夋嫨鍏ュ簱鍗曞彿"
+ width="1100px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleInboundDialogClosed"
+ >
+ <el-table
+ ref="inboundTableRef"
+ v-loading="inboundBatchLoading"
+ :data="inboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleInboundDialogSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="inboundBatches" label="鍏ュ簱鍗曞彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="supplierName" label="渚涘簲鍟�" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="purchaseContractNumber" label="閲囪喘璁㈠崟鍙�" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="inboundDate" label="鍏ュ簱鏃ユ湡" width="110" align="center" />
+ <el-table-column prop="inboundAmount" label="鍏ュ簱閲戦(鍚◣)" width="120" align="right">
+ <template #default="{ row }">楼{{ formatMoney(getInboundRowTaxInclusiveAmount(row)) }}</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="confirmInboundSelection">纭畾</el-button>
+ <el-button @click="inboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+ getInboundBatchesBySupplier,
+ addAccountPurchaseInvoice,
+ listPageAccountPurchaseInvoice,
+ cancelAccountPurchaseInvoice,
+ deleteAccountPurchaseInvoice,
+} from "@/api/financialManagement/accountPurchaseInvoice.js";
+
+defineOptions({
+ name: "杩涢」鍙戠エ",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+ invoiceNumber: "",
+ supplierId: "",
+ dateRange: [],
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鍙戠エ鍙风爜", prop: "invoiceNo", width: "140" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { label: "寮�绁ㄦ棩鏈�", prop: "invoiceDate", width: "120" },
+ { label: "閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "绋庨", prop: "taxAmount", dataType: "slot", slot: "taxAmount" },
+ { label: "浠风◣鍚堣", prop: "totalAmount", dataType: "slot", slot: "totalAmount" },
+ { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "90", align: "center" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isView = ref(false);
+const submitLoading = ref(false);
+const supplierList = ref([]);
+
+const inboundBatchList = ref([]);
+const inboundBatchOptions = ref([]);
+const inboundBatchLoading = ref(false);
+const inboundSelectVisible = ref(false);
+const inboundTableRef = ref(null);
+const dialogInboundSelection = ref([]);
+
+const STATUS_LABEL_MAP = { 0: "姝e父", 1: "浣滃簾" };
+const STATUS_TYPE_MAP = { 0: "success", 1: "info" };
+
+const form = reactive({
+ invoiceNo: "",
+ supplierId: "",
+ invoiceDate: "",
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ storageAttachmentId: undefined,
+ status: 0,
+});
+
+const rules = {
+ invoiceNo: [{ required: true, message: "璇疯緭鍏ュ彂绁ㄥ彿鐮�", trigger: "blur" }],
+ supplierId: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟�", trigger: "change" }],
+ stockInRecordIds: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍏宠仈鍏ュ簱鍗�", trigger: "change" }],
+ invoiceDate: [{ required: true, message: "璇烽�夋嫨寮�绁ㄦ棩鏈�", trigger: "change" }],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+ taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return 0;
+ const num = Number(status);
+ return Number.isNaN(num) ? 0 : num;
+};
+
+const isNormalStatus = (status) => normalizeStatus(status) === 0;
+
+const getStatusLabel = (status) => STATUS_LABEL_MAP[normalizeStatus(status)] ?? "姝e父";
+
+const getStatusType = (status) => STATUS_TYPE_MAP[normalizeStatus(status)] ?? "success";
+
+const parseStockInRecordIds = (value) => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const formatInboundBatches = (value) => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+};
+
+const isSameInboundId = (a, b) => String(a) === String(b);
+
+const getInboundRowId = (row) => row?.id ?? row?.stockInRecordId;
+
+/** 鍏ュ簱鍗曢噾棰濅负鍚◣浠� */
+const getInboundRowTaxInclusiveAmount = (row) =>
+ Number(row?.inboundAmount ?? row?.taxInclusivePrice ?? row?.totalAmount ?? 0);
+
+const normalizeInboundBatchOptions = (data) => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ if (typeof item === "string" || typeof item === "number") {
+ const text = String(item);
+ return { label: text, value: text, inboundAmount: 0 };
+ }
+ const label =
+ item.inboundBatches ?? item.batchNo ?? item.inboundNo ?? item.label ?? `鍏ュ簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockInRecordId ?? label;
+ return {
+ label: String(label),
+ value,
+ inboundAmount: getInboundRowTaxInclusiveAmount(item),
+ };
+ });
+};
+
+/** 涓嶅惈绋庨噾棰濆彉鏇达細绋庨銆佷环绋庡悎璁℃鍚戣绠� */
+const calculateTaxFromExclusive = () => {
+ form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2));
+ form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2));
+};
+
+/** 浠风◣鍚堣鍙樻洿锛氭寜绋庣巼鍙嶇畻涓嶅惈绋庨噾棰濄�佺◣棰� */
+const calculateTaxFromInclusive = (inclusiveTotal) => {
+ const total = Number(inclusiveTotal ?? form.totalAmount ?? 0);
+ if (total <= 0) {
+ form.amount = 0;
+ form.taxAmount = 0;
+ form.totalAmount = 0;
+ return;
+ }
+ const rate = Number(form.taxRate) / 100;
+ form.totalAmount = Number(total.toFixed(2));
+ form.amount = Number((form.totalAmount / (1 + rate)).toFixed(2));
+ form.taxAmount = Number((form.totalAmount - form.amount).toFixed(2));
+};
+
+const handleTaxRateChange = () => {
+ if (form.totalAmount > 0) {
+ calculateTaxFromInclusive(form.totalAmount);
+ } else {
+ calculateTaxFromExclusive();
+ }
+};
+
+/** 鏍规嵁宸查�夊叆搴撳崟姹囨�诲惈绋庨噾棰濓紝鍙嶇畻涓嶅惈绋庨噾棰濅笌绋庨 */
+const syncInvoiceAmount = () => {
+ const selected = form.stockInRecordIds || [];
+ const sumFromOptions = inboundBatchOptions.value
+ .filter((opt) => selected.some((id) => isSameInboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.inboundAmount) || 0), 0);
+
+ let taxInclusiveSum = sumFromOptions;
+ if (taxInclusiveSum <= 0 && selected.length) {
+ taxInclusiveSum = inboundBatchList.value
+ .filter((row) => selected.some((id) => isSameInboundId(id, getInboundRowId(row))))
+ .reduce((acc, row) => acc + getInboundRowTaxInclusiveAmount(row), 0);
+ }
+
+ calculateTaxFromInclusive(taxInclusiveSum > 0 ? Number(taxInclusiveSum.toFixed(2)) : 0);
+};
+
+const inboundBatchDisplayText = computed(() => {
+ if (form.inboundBatches) return form.inboundBatches;
+ const ids = form.stockInRecordIds || [];
+ if (!ids.length) return "";
+ const labels = inboundBatchOptions.value
+ .filter((opt) => ids.some((id) => isSameInboundId(id, opt.value)))
+ .map((opt) => opt.label);
+ if (labels.length) return labels.join("銆�");
+ return ids.join("銆�");
+});
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ invoiceNo: row.invoiceNumber ?? row.invoiceNo,
+ invoiceDate: row.issueDate ?? row.invoiceDate,
+ amount: row.taxExclusivelPrice ?? row.amount,
+ taxAmount: row.taxPrice ?? row.taxAmount,
+ totalAmount: row.taxInclusivePrice ?? row.totalAmount,
+ content: row.invoiceContent ?? row.content,
+ status: normalizeStatus(row.status),
+ stockInRecordIds: row.stockInRecordIds ?? "",
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+});
+
+const toFormNumber = (val) => {
+ const n = Number(val);
+ return Number.isFinite(n) ? n : 0;
+};
+
+const resolveFormAmounts = (row) => {
+ let amount = toFormNumber(row.taxExclusivelPrice ?? row.amount);
+ let taxAmount = toFormNumber(row.taxPrice ?? row.taxAmount);
+ let totalAmount = toFormNumber(row.taxInclusivePrice ?? row.totalAmount);
+ const taxRate = toFormNumber(row.taxRate) || 13;
+
+ if (totalAmount > 0 && amount === 0 && taxAmount === 0) {
+ amount = Number((totalAmount / (1 + taxRate / 100)).toFixed(2));
+ taxAmount = Number((totalAmount - amount).toFixed(2));
+ } else if (totalAmount > 0 && amount > 0 && taxAmount === 0) {
+ taxAmount = Number((totalAmount - amount).toFixed(2));
+ } else if (amount > 0 && taxAmount === 0 && totalAmount === 0) {
+ taxAmount = Number((amount * taxRate / 100).toFixed(2));
+ totalAmount = Number((amount + taxAmount).toFixed(2));
+ } else if (amount > 0 && taxAmount > 0 && totalAmount === 0) {
+ totalAmount = Number((amount + taxAmount).toFixed(2));
+ }
+
+ return { amount, taxAmount, totalAmount };
+};
+
+const fillFormFromRow = (row) => {
+ const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds);
+ const { amount, taxAmount, totalAmount } = resolveFormAmounts(row);
+ Object.assign(form, {
+ invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "",
+ supplierId: row.supplierId,
+ invoiceDate: row.invoiceDate ?? row.issueDate ?? "",
+ invoiceType: row.invoiceType ?? "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: row.taxRate ?? 13,
+ amount,
+ taxAmount,
+ totalAmount,
+ content: row.content ?? row.invoiceContent ?? "",
+ remark: row.remark ?? "",
+ stockInRecordIds,
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+ storageAttachmentId: row.storageAttachmentId,
+ status: normalizeStatus(row.status),
+ });
+};
+
+const buildCancelPayload = (row) => ({
+ id: row.id,
+ invoiceNumber: row.invoiceNumber ?? row.invoiceNo,
+ taxRate: row.taxRate,
+ invoiceType: row.invoiceType,
+ issueDate: row.issueDate ?? row.invoiceDate,
+ taxExclusivelPrice: row.taxExclusivelPrice ?? row.amount,
+ taxPrice: row.taxPrice ?? row.taxAmount,
+ taxInclusivePrice: row.taxInclusivePrice ?? row.totalAmount,
+ remark: row.remark ?? "",
+ invoiceContent: row.invoiceContent ?? row.content,
+ supplierId: row.supplierId,
+ storageAttachmentId: row.storageAttachmentId,
+ stockInRecordIds: row.stockInRecordIds ?? "",
+ status: 1,
+});
+
+const buildSubmitPayload = () => ({
+ invoiceNumber: form.invoiceNo,
+ supplierId: form.supplierId,
+ issueDate: form.invoiceDate,
+ invoiceType: form.invoiceType,
+ taxRate: form.taxRate,
+ taxExclusivelPrice: form.amount,
+ taxPrice: form.taxAmount,
+ taxInclusivePrice: form.totalAmount,
+ invoiceContent: form.content,
+ remark: form.remark || "",
+ stockInRecordIds: (form.stockInRecordIds || []).join(","),
+ status: 0,
+ storageAttachmentId: form.storageAttachmentId,
+});
+
+const getSupplierList = () => {
+ getOptions().then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+};
+
+const appendFilterParams = (params) => {
+ if (filters.invoiceNumber) {
+ params.invoiceNumber = filters.invoiceNumber;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ return params;
+};
+
+const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+ proxy.download(
+ "/accountPurchaseInvoice/exportAccountPurchaseInvoice",
+ buildExportParams(),
+ `杩涢」鍙戠エ_${Date.now()}.xlsx`
+ );
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchaseInvoice(buildListParams())
+ .then((res) => {
+ if (res.code === 200) {
+ const records = res.data?.records ?? [];
+ dataList.value = records.map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.invoiceNumber = "";
+ filters.supplierId = "";
+ filters.dateRange = [];
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ isView.value = false;
+ inboundSelectVisible.value = false;
+};
+
+const resetForm = () => {
+ Object.assign(form, {
+ invoiceNo: "",
+ supplierId: "",
+ invoiceDate: new Date().toISOString().split("T")[0],
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ storageAttachmentId: undefined,
+ status: 0,
+ });
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+};
+
+const add = () => {
+ isView.value = false;
+ dialogTitle.value = "褰曞叆鍙戠エ";
+ resetForm();
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ isView.value = true;
+ dialogTitle.value = "鏌ョ湅鍙戠エ";
+ fillFormFromRow(row);
+ if (row.supplierId) {
+ loadInboundBatches(row.supplierId, true, false);
+ }
+ dialogVisible.value = true;
+};
+
+const handleCancel = (row) => {
+ ElMessageBox.confirm(`纭浣滃簾鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "浣滃簾纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ cancelAccountPurchaseInvoice(buildCancelPayload(row))
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("浣滃簾鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "浣滃簾澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("浣滃簾澶辫触");
+ });
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "鍒犻櫎纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountPurchaseInvoice([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const submitForm = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ submitLoading.value = true;
+ addAccountPurchaseInvoice(buildSubmitPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("褰曞叆鎴愬姛");
+ closeDialog();
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "褰曞叆澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("褰曞叆澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+const ensureInboundOptionsForSelected = () => {
+ const ids = form.stockInRecordIds || [];
+ ids.forEach((id) => {
+ const exists = inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, id));
+ if (exists) return;
+ const fromList = inboundBatchList.value.find((row) => isSameInboundId(getInboundRowId(row), id));
+ if (fromList) {
+ const [option] = normalizeInboundBatchOptions([fromList]);
+ if (option) inboundBatchOptions.value.push(option);
+ return;
+ }
+ inboundBatchOptions.value.push({
+ label: String(id),
+ value: id,
+ inboundAmount: 0,
+ });
+ });
+};
+
+const restoreInboundTableSelection = () => {
+ nextTick(() => {
+ const table = inboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set((form.stockInRecordIds || []).map((id) => String(id)));
+ inboundBatchList.value.forEach((row) => {
+ const rowId = getInboundRowId(row);
+ if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+};
+
+const loadInboundBatches = (supplierId, keepSelected = false, syncAmount = true) => {
+ if (!supplierId) {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.amount = 0;
+ form.taxAmount = 0;
+ form.totalAmount = 0;
+ }
+ return Promise.resolve();
+ }
+ inboundBatchLoading.value = true;
+ return getInboundBatchesBySupplier({ supplierId })
+ .then((res) => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ inboundBatchList.value = Array.isArray(list) ? list : [];
+ inboundBatchOptions.value = normalizeInboundBatchOptions(list);
+ } else {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ inboundBatchLoading.value = false;
+ if (keepSelected) {
+ ensureInboundOptionsForSelected();
+ restoreInboundTableSelection();
+ if (syncAmount && !isView.value) {
+ syncInvoiceAmount();
+ }
+ }
+ });
+};
+
+const handleSupplierChange = (supplierId) => {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.amount = 0;
+ form.taxAmount = 0;
+ form.totalAmount = 0;
+ loadInboundBatches(supplierId);
+};
+
+const handleInboundInputClick = () => {
+ if (isView.value) return;
+ openInboundSelectDialog();
+};
+
+const openInboundSelectDialog = () => {
+ if (!form.supplierId || isView.value) return;
+ inboundSelectVisible.value = true;
+ loadInboundBatches(form.supplierId, true).then(() => {
+ restoreInboundTableSelection();
+ });
+};
+
+const handleInboundDialogSelectionChange = (selection) => {
+ dialogInboundSelection.value = selection;
+};
+
+const confirmInboundSelection = () => {
+ if (dialogInboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叆搴撳崟");
+ return;
+ }
+ form.stockInRecordIds = dialogInboundSelection.value
+ .map((row) => getInboundRowId(row))
+ .filter((id) => id !== undefined && id !== null);
+ form.inboundBatches = dialogInboundSelection.value
+ .map((row) => row.inboundBatches ?? row.batchNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ dialogInboundSelection.value.forEach((row) => {
+ const [option] = normalizeInboundBatchOptions([row]);
+ if (option && !inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, option.value))) {
+ inboundBatchOptions.value.push(option);
+ }
+ });
+ inboundSelectVisible.value = false;
+ syncInvoiceAmount();
+ formRef.value?.validateField("stockInRecordIds");
+};
+
+const handleInboundDialogClosed = () => {
+ dialogInboundSelection.value = [];
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.inbound-batch-input :deep(.el-input__wrapper) {
+ cursor: pointer;
+}
+</style>
diff --git a/src/views/financialManagement/payable/payment.vue b/src/views/financialManagement/payable/payment.vue
new file mode 100644
index 0000000..18e7941
--- /dev/null
+++ b/src/views/financialManagement/payable/payment.vue
@@ -0,0 +1,299 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="浠樻鍗曞彿:">
+ <el-input v-model="filters.paymentNumber"
+ placeholder="璇疯緭鍏ヤ粯娆惧崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ clearable
+ filterable
+ style="width: 200px;">
+ <el-option v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠樻鏂瑰紡:">
+ <el-select v-model="filters.paymentMethod"
+ placeholder="璇烽�夋嫨浠樻鏂瑰紡"
+ clearable
+ style="width: 150px;">
+ <el-option v-for="item in checkout_payment"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠樻鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="鏈〉浠樻鍚堣"
+ :value="totalPaymentAmount"
+ :precision="2"
+ prefix="楼" />
+ </div>
+ <div>
+ <el-button @click="handleExport"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage">
+ <template #amount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #paymentMethod="{ row }">
+ <el-tag>{{ getPaymentMethodLabel(row.paymentMethod) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button :disabled="row.accountStatemen"
+ type="danger"
+ link
+ @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, onMounted, getCurrentInstance } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+ import {
+ listPageAccountPurchasePayment,
+ deleteAccountPurchasePayment,
+ } from "@/api/financialManagement/accountPurchasePayment.js";
+
+ defineOptions({
+ name: "浠樻鍗�",
+ });
+
+ const { proxy } = getCurrentInstance();
+ const { checkout_payment } = proxy.useDict("checkout_payment");
+
+ const filters = reactive({
+ paymentNumber: "",
+ supplierId: "",
+ paymentMethod: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "浠樻鍗曞彿", prop: "paymentNumber", width: "150" },
+ { label: "鍏宠仈鐢宠鍗�", prop: "invoiceApplicationNo", width: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { label: "浠樻鏃ユ湡", prop: "paymentDate", width: "120" },
+ { label: "浠樻閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ {
+ label: "浠樻鏂瑰紡",
+ prop: "paymentMethod",
+ dataType: "slot",
+ slot: "paymentMethod",
+ width: "120",
+ },
+ { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "slot",
+ slot: "operation",
+ width: "80",
+ fixed: "right",
+ },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+ const supplierList = ref([]);
+
+ const totalPaymentAmount = computed(() =>
+ dataList.value.reduce((sum, item) => sum + Number(item.amount ?? 0), 0)
+ );
+
+ const formatMoney = value => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value)
+ .toFixed(2)
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ };
+
+ const getPaymentMethodLabel = value => {
+ if (value === undefined || value === null || value === "") return "-";
+ const item = checkout_payment.value?.find(
+ m => String(m.value) === String(value)
+ );
+ return item?.label ?? value;
+ };
+
+ const normalizeTableRow = row => ({
+ ...row,
+ paymentNumber: row.paymentNumber ?? row.paymentCode,
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ amount: row.paymentAmount ?? row.amount,
+ bankAccountNum: row.bankAccountNum ?? row.bankAccount ?? "",
+ bankAccountName: row.bankAccountName ?? row.bankName ?? "",
+ });
+
+ const getSupplierList = () => {
+ getOptions().then(res => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+ };
+
+ const appendFilterParams = params => {
+ if (filters.paymentNumber) {
+ params.paymentNumber = filters.paymentNumber;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.paymentMethod) {
+ params.paymentMethod = filters.paymentMethod;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ };
+
+ const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+ const buildExportParams = () => appendFilterParams({});
+
+ const handleExport = () => {
+ proxy.download(
+ "/accountPurchasePayment/exportAccountPurchasePayment",
+ buildExportParams(),
+ `浠樻鍗昣${Date.now()}.xlsx`
+ );
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchasePayment(buildListParams())
+ .then(res => {
+ if (res.code === 200) {
+ dataList.value = (res.data?.records ?? []).map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const resetFilters = () => {
+ filters.paymentNumber = "";
+ filters.supplierId = "";
+ filters.paymentMethod = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+ };
+
+ const handleDelete = row => {
+ ElMessageBox.confirm(`纭鍒犻櫎浠樻鍗曘��${row.paymentNumber}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountPurchasePayment([row.id])
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+ };
+
+ onMounted(() => {
+ getSupplierList();
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+
+ .text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+</style>
diff --git a/src/views/financialManagement/payable/paymentApply.vue b/src/views/financialManagement/payable/paymentApply.vue
new file mode 100644
index 0000000..e34793f
--- /dev/null
+++ b/src/views/financialManagement/payable/paymentApply.vue
@@ -0,0 +1,1060 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鐢宠鍗曞彿:">
+ <el-input v-model="filters.invoiceApplicationNo" placeholder="璇疯緭鍏ョ敵璇峰崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="寰呭鏍�" :value="0" />
+ <el-option label="瀹℃牳閫氳繃" :value="1" />
+ <el-option label="瀹℃牳涓嶉�氳繃" :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鏃ユ湡:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板鐢宠</el-button>
+ <el-button @click="handleExport" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #paymentMethod="{ row }">
+ <el-tag>{{ getPaymentMethodLabel(row.paymentMethod) }}</el-tag>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)" v-if="isPendingStatus(row.status)">缂栬緫</el-button>
+ <el-button type="success" link @click="handleAudit(row)" v-if="isPendingStatus(row.status)">瀹℃牳</el-button>
+ <el-button type="warning" link @click="openPaymentDialog(row)" v-if="isApprovedStatus(row.status)">浠樻</el-button>
+ <el-button type="danger" link @click="handleDelete(row)" v-if="isPendingStatus(row.status)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹℃牳鐘舵��">
+ <el-tag :type="getStatusType(form.status)">{{ getStatusLabel(form.status) }}</el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐢宠鍗曞彿" prop="invoiceApplicationNo">
+ <el-input v-model="form.invoiceApplicationNo" placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="form.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 100%;"
+ filterable
+ :disabled="isEdit || isView"
+ @change="handleSupplierChange"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鍏ュ簱鍗�" prop="stockInRecordIds">
+ <el-input
+ :model-value="inboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨渚涘簲鍟�"
+ readonly
+ :disabled="!form.supplierId || isEdit || isView"
+ class="inbound-batch-input"
+ @click="handleInboundInputClick"
+ >
+ <template v-if="!isEdit && !isView" #append>
+ <el-button
+ :disabled="!form.supplierId"
+ :loading="inboundBatchLoading"
+ @click.stop="openInboundSelectDialog"
+ >
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢宠鏃ユ湡" prop="applyDate">
+ <el-date-picker
+ v-model="form.applyDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="formCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻閲戦" prop="paymentAmount">
+ <el-input-number
+ v-model="form.paymentAmount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ placeholder="鏍规嵁鍏ュ簱鍗曡嚜鍔ㄦ眹鎬伙紝鍙慨鏀�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-select
+ v-model="form.paymentMethod"
+ placeholder="璇烽�夋嫨浠樻鏂瑰紡"
+ style="width: 100%;"
+ :disabled="isView"
+ >
+ <el-option
+ v-for="item in checkout_payment"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="浠樻浜嬬敱" prop="paymentContent">
+ <el-input
+ v-model="form.paymentContent"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ粯娆句簨鐢�"
+ :disabled="isView"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <FormDialog
+ title="浠樻"
+ v-model="paymentDialogVisible"
+ width="800px"
+ @confirm="submitPayment"
+ @cancel="paymentDialogVisible = false"
+ >
+ <el-form :model="paymentForm" :rules="paymentRules" ref="paymentFormRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浠樻鍗曞彿" prop="paymentNumber">
+ <el-input v-model="paymentForm.paymentNumber" placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鐢宠鍗�" prop="invoiceApplicationNo">
+ <el-input v-model="paymentForm.invoiceApplicationNo" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�">
+ <el-input v-model="paymentForm.supplierName" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏃ユ湡" prop="paymentDate">
+ <el-date-picker
+ v-model="paymentForm.paymentDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="paymentFormCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻閲戦" prop="paymentAmount">
+ <el-input-number
+ v-model="paymentForm.paymentAmount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-select v-model="paymentForm.paymentMethod" placeholder="璇烽�夋嫨浠樻鏂瑰紡" style="width: 100%;">
+ <el-option
+ v-for="item in checkout_payment"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="isBankTransferPayment(paymentForm.paymentMethod)" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閾惰璐﹀彿" prop="bankAccount">
+ <el-input v-model="paymentForm.bankAccount" placeholder="閾惰璐﹀彿" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寮�鎴疯" prop="bankName">
+ <el-input v-model="paymentForm.bankName" placeholder="寮�鎴疯" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="paymentForm.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" :loading="paymentSubmitLoading" @click="submitPayment">纭畾</el-button>
+ <el-button @click="paymentDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <el-dialog
+ v-model="inboundSelectVisible"
+ title="閫夋嫨鍏ュ簱鍗曞彿"
+ width="1100px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleInboundDialogClosed"
+ >
+ <el-table
+ ref="inboundTableRef"
+ v-loading="inboundBatchLoading"
+ :data="inboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleInboundDialogSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="inboundBatches" label="鍏ュ簱鍗曞彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="supplierName" label="渚涘簲鍟�" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="purchaseContractNumber" label="閲囪喘璁㈠崟鍙�" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="inboundDate" label="鍏ュ簱鏃ユ湡" width="110" align="center" />
+ <el-table-column prop="inboundAmount" label="鍏ュ簱閲戦(鍚◣)" width="120" align="right">
+ <template #default="{ row }">楼{{ formatMoney(getInboundRowTaxInclusiveAmount(row)) }}</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="confirmInboundSelection">纭畾</el-button>
+ <el-button @click="inboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance } from "vue";
+import dayjs from "dayjs";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+ getInboundBatchesBySupplier,
+ addAccountPaymentApplication,
+ listPageAccountPaymentApplication,
+ updateAccountPaymentApplication,
+ auditAccountPaymentApplication,
+ deleteAccountPaymentApplication,
+} from "@/api/financialManagement/accountPaymentApplication.js";
+import { addAccountPurchasePayment } from "@/api/financialManagement/accountPurchasePayment.js";
+
+defineOptions({
+ name: "浠樻鐢宠",
+});
+
+const { proxy } = getCurrentInstance();
+const { checkout_payment } = proxy.useDict("checkout_payment");
+
+const filters = reactive({
+ invoiceApplicationNo: "",
+ supplierId: "",
+ status: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鐢宠鍗曞彿", prop: "applyCode", width: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { label: "浠樻閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "浠樻鏂瑰紡", prop: "paymentMethod", dataType: "slot", slot: "paymentMethod", width: "120" },
+ { label: "鐢宠鏃ユ湡", prop: "applyDate", width: "120" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "100" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const submitLoading = ref(false);
+const currentId = ref(null);
+const supplierList = ref([]);
+
+const inboundBatchList = ref([]);
+const inboundBatchOptions = ref([]);
+const inboundBatchLoading = ref(false);
+const inboundSelectVisible = ref(false);
+const inboundTableRef = ref(null);
+const dialogInboundSelection = ref([]);
+
+const paymentDialogVisible = ref(false);
+const paymentFormRef = ref(null);
+const paymentSubmitLoading = ref(false);
+
+const paymentForm = reactive({
+ paymentNumber: "",
+ invoiceApplicationNo: "",
+ supplierName: "",
+ supplierId: "",
+ accountPaymentApplicationId: null,
+ paymentDate: "",
+ paymentAmount: 0,
+ paymentMethod: "",
+ bankAccount: "",
+ bankName: "",
+ remark: "",
+ createTime: "",
+});
+
+const paymentRules = {
+ paymentDate: [{ required: true, message: "璇烽�夋嫨浠樻鏃ユ湡", trigger: "change" }],
+ paymentAmount: [{ required: true, message: "璇疯緭鍏ヤ粯娆鹃噾棰�", trigger: "blur" }],
+ paymentMethod: [{ required: true, message: "璇烽�夋嫨浠樻鏂瑰紡", trigger: "change" }],
+};
+
+const STATUS_LABEL_MAP = { 0: "寰呭鏍�", 1: "瀹℃牳閫氳繃", 2: "瀹℃牳涓嶉�氳繃" };
+const STATUS_TYPE_MAP = { 0: "warning", 1: "success", 2: "danger" };
+
+const form = reactive({
+ invoiceApplicationNo: "",
+ supplierId: "",
+ paymentAmount: 0,
+ paymentMethod: "",
+ applyDate: "",
+ paymentContent: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ status: 0,
+ createTime: "",
+});
+const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
+const paymentFormCreateTimeDate = computed({
+ get: () => (paymentForm.createTime ? String(paymentForm.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ paymentForm.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
+
+const rules = {
+ supplierId: [{ required: true, message: "璇烽�夋嫨渚涘簲鍟�", trigger: "change" }],
+ stockInRecordIds: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍏宠仈鍏ュ簱鍗�", trigger: "change" }],
+ paymentAmount: [{ required: true, message: "璇疯緭鍏ヤ粯娆鹃噾棰�", trigger: "blur" }],
+ paymentMethod: [{ required: true, message: "璇烽�夋嫨浠樻鏂瑰紡", trigger: "change" }],
+ applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return 0;
+ const num = Number(status);
+ return Number.isNaN(num) ? 0 : num;
+};
+
+const isPendingStatus = (status) => normalizeStatus(status) === 0;
+
+const isApprovedStatus = (status) => normalizeStatus(status) === 1;
+
+const isBankTransferPayment = (method) => {
+ if (method === undefined || method === null || method === "") return false;
+ const item = checkout_payment.value?.find((m) => String(m.value) === String(method));
+ if (item?.label?.includes("閾惰")) return true;
+ return String(method) === "bank_transfer" || String(method).toLowerCase().includes("bank");
+};
+
+const getStatusLabel = (status) => STATUS_LABEL_MAP[normalizeStatus(status)] ?? "寰呭鏍�";
+
+const getStatusType = (status) => STATUS_TYPE_MAP[normalizeStatus(status)] ?? "warning";
+
+const getPaymentMethodLabel = (value) => {
+ if (value === undefined || value === null || value === "") return "-";
+ const item = checkout_payment.value?.find((m) => String(m.value) === String(value));
+ return item?.label ?? value;
+};
+
+const getDefaultPaymentMethod = () => checkout_payment.value?.[0]?.value ?? "";
+
+const parseStockInRecordIds = (value) => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const formatInboundBatches = (value) => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+};
+
+const isSameInboundId = (a, b) => String(a) === String(b);
+
+const getInboundRowId = (row) => row?.id ?? row?.stockInRecordId;
+
+const getInboundRowTaxInclusiveAmount = (row) =>
+ Number(row?.inboundAmount ?? row?.taxInclusivePrice ?? row?.totalAmount ?? row?.amount ?? 0);
+
+const normalizeInboundBatchOptions = (data) => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ const label =
+ item.inboundBatches ?? item.batchNo ?? item.inboundNo ?? `鍏ュ簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockInRecordId ?? label;
+ return {
+ label: String(label),
+ value,
+ inboundAmount: getInboundRowTaxInclusiveAmount(item),
+ };
+ });
+};
+
+const syncPaymentAmount = () => {
+ const selected = form.stockInRecordIds || [];
+ let sum = inboundBatchOptions.value
+ .filter((opt) => selected.some((id) => isSameInboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.inboundAmount) || 0), 0);
+
+ if (sum <= 0 && selected.length) {
+ sum = inboundBatchList.value
+ .filter((row) => selected.some((id) => isSameInboundId(id, getInboundRowId(row))))
+ .reduce((acc, row) => acc + getInboundRowTaxInclusiveAmount(row), 0);
+ }
+
+ form.paymentAmount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+};
+
+const inboundBatchDisplayText = computed(() => {
+ if (form.inboundBatches) return form.inboundBatches;
+ const ids = form.stockInRecordIds || [];
+ if (!ids.length) return "";
+ const labels = inboundBatchOptions.value
+ .filter((opt) => ids.some((id) => isSameInboundId(id, opt.value)))
+ .map((opt) => opt.label);
+ if (labels.length) return labels.join("銆�");
+ return ids.join("銆�");
+});
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ applyCode: row.invoiceApplicationNo ?? row.applyCode,
+ amount: row.paymentAmount ?? row.amount,
+ reason: row.paymentContent ?? row.reason,
+ status: normalizeStatus(row.status),
+ stockInRecordIds: row.stockInRecordIds ?? "",
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+});
+
+const fillFormFromRow = (row) => {
+ const stockInRecordIds = parseStockInRecordIds(row.stockInRecordIds);
+ Object.assign(form, {
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ supplierId: row.supplierId,
+ paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+ paymentMethod: row.paymentMethod ?? getDefaultPaymentMethod(),
+ applyDate: row.applyDate ?? "",
+ paymentContent: row.paymentContent ?? row.reason ?? "",
+ remark: row.remark ?? "",
+ stockInRecordIds,
+ inboundBatches: formatInboundBatches(row.inboundBatches),
+ status: normalizeStatus(row.status),
+ createTime: row.createTime ?? "",
+ });
+};
+
+const buildPayloadFromRow = (row, statusOverride) => ({
+ id: row.id,
+ supplierId: row.supplierId,
+ stockInRecordIds:
+ typeof row.stockInRecordIds === "string"
+ ? row.stockInRecordIds
+ : (row.stockInRecordIds || []).join(","),
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ paymentMethod: row.paymentMethod,
+ paymentContent: row.paymentContent ?? row.reason ?? "",
+ applyDate: row.applyDate,
+ remark: row.remark ?? "",
+ status: statusOverride !== undefined ? statusOverride : normalizeStatus(row.status),
+ paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+ createTime: row.createTime,
+});
+
+const buildSubmitPayload = (forUpdate = false) => {
+ const payload = {
+ supplierId: form.supplierId,
+ stockInRecordIds: (form.stockInRecordIds || []).join(","),
+ invoiceApplicationNo: form.invoiceApplicationNo || "",
+ paymentMethod: form.paymentMethod,
+ paymentContent: form.paymentContent || "",
+ applyDate: form.applyDate,
+ remark: form.remark || "",
+ status: 0,
+ paymentAmount: form.paymentAmount,
+ createTime: form.createTime,
+ };
+ if (forUpdate) {
+ payload.id = currentId.value;
+ }
+ return payload;
+};
+
+const getSupplierList = () => {
+ getOptions().then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+};
+
+const appendFilterParams = (params) => {
+ if (filters.invoiceApplicationNo) {
+ params.invoiceApplicationNo = filters.invoiceApplicationNo;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+};
+
+const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+ proxy.download(
+ "/accountPaymentApplication/exportAccountPaymentApplication",
+ buildExportParams(),
+ `浠樻鐢宠_${Date.now()}.xlsx`
+ );
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPaymentApplication(buildListParams())
+ .then((res) => {
+ if (res.code === 200) {
+ dataList.value = (res.data?.records ?? []).map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.invoiceApplicationNo = "";
+ filters.supplierId = "";
+ filters.status = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ isView.value = false;
+ isEdit.value = false;
+ inboundSelectVisible.value = false;
+};
+
+const resetForm = () => {
+ Object.assign(form, {
+ invoiceApplicationNo: "",
+ supplierId: "",
+ paymentAmount: 0,
+ paymentMethod: getDefaultPaymentMethod(),
+ applyDate: new Date().toISOString().split("T")[0],
+ paymentContent: "",
+ remark: "",
+ stockInRecordIds: [],
+ inboundBatches: "",
+ status: 0,
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ });
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+};
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ dialogTitle.value = "鏂板浠樻鐢宠";
+ resetForm();
+ dialogVisible.value = true;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫浠樻鐢宠";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ isView.value = true;
+ isEdit.value = false;
+ dialogTitle.value = "鏌ョ湅浠樻鐢宠";
+ fillFormFromRow(row);
+ if (row.supplierId) {
+ loadInboundBatches(row.supplierId, true, false);
+ }
+ dialogVisible.value = true;
+};
+
+const submitAudit = (row, status) => {
+ auditAccountPaymentApplication(buildPayloadFromRow(row, status))
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(status === 1 ? "瀹℃牳閫氳繃" : "瀹℃牳涓嶉�氳繃");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "瀹℃牳澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("瀹℃牳澶辫触");
+ });
+};
+
+const handleAudit = (row) => {
+ ElMessageBox.confirm("璇烽�夋嫨瀹℃牳缁撴灉", "浠樻鐢宠瀹℃牳", {
+ confirmButtonText: "瀹℃牳閫氳繃",
+ cancelButtonText: "瀹℃牳涓嶉�氳繃",
+ distinguishCancelAndClose: true,
+ type: "warning",
+ })
+ .then(() => {
+ submitAudit(row, 1);
+ })
+ .catch((action) => {
+ if (action === "cancel") {
+ submitAudit(row, 2);
+ }
+ });
+};
+
+const openPaymentDialog = (row) => {
+ Object.assign(paymentForm, {
+ paymentNumber: "",
+ invoiceApplicationNo: row.invoiceApplicationNo ?? row.applyCode ?? "",
+ supplierName: row.supplierName ?? "",
+ supplierId: row.supplierId,
+ accountPaymentApplicationId: row.id,
+ paymentDate: new Date().toISOString().split("T")[0],
+ paymentAmount: Number(row.paymentAmount ?? row.amount ?? 0),
+ paymentMethod: row.paymentMethod ?? getDefaultPaymentMethod(),
+ bankAccount: row.bankAccountNum ?? row.bankAccount ?? "",
+ bankName: row.bankAccountName ?? row.bankName ?? "",
+ remark: "",
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ });
+ paymentDialogVisible.value = true;
+ nextTick(() => {
+ paymentFormRef.value?.clearValidate();
+ });
+};
+
+const submitPayment = () => {
+ paymentFormRef.value?.validate((valid) => {
+ if (!valid) return;
+ paymentSubmitLoading.value = true;
+ addAccountPurchasePayment({
+ accountPaymentApplicationId: paymentForm.accountPaymentApplicationId,
+ supplierId: paymentForm.supplierId,
+ paymentDate: paymentForm.paymentDate,
+ paymentMethod: paymentForm.paymentMethod,
+ paymentAmount: paymentForm.paymentAmount,
+ paymentNumber: paymentForm.paymentNumber || "",
+ remark: paymentForm.remark || "",
+ createTime: paymentForm.createTime,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("浠樻鎴愬姛");
+ paymentDialogVisible.value = false;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "浠樻澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("浠樻澶辫触");
+ })
+ .finally(() => {
+ paymentSubmitLoading.value = false;
+ });
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鐢宠鍗曘��${row.applyCode ?? row.invoiceApplicationNo}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountPaymentApplication([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const submitForm = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ submitLoading.value = true;
+ const request = isEdit.value
+ ? updateAccountPaymentApplication(buildSubmitPayload(true))
+ : addAccountPaymentApplication(buildSubmitPayload(false));
+
+ request
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(isEdit.value ? "缂栬緫鎴愬姛" : "鏂板鎴愬姛");
+ closeDialog();
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "淇濆瓨澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("淇濆瓨澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+const ensureInboundOptionsForSelected = () => {
+ const ids = form.stockInRecordIds || [];
+ ids.forEach((id) => {
+ const exists = inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, id));
+ if (exists) return;
+ const fromList = inboundBatchList.value.find((row) => isSameInboundId(getInboundRowId(row), id));
+ if (fromList) {
+ const [option] = normalizeInboundBatchOptions([fromList]);
+ if (option) inboundBatchOptions.value.push(option);
+ return;
+ }
+ inboundBatchOptions.value.push({
+ label: String(id),
+ value: id,
+ inboundAmount: 0,
+ });
+ });
+};
+
+const restoreInboundTableSelection = () => {
+ nextTick(() => {
+ const table = inboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set((form.stockInRecordIds || []).map((id) => String(id)));
+ inboundBatchList.value.forEach((row) => {
+ const rowId = getInboundRowId(row);
+ if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+};
+
+const loadInboundBatches = (supplierId, keepSelected = false, syncAmount = true) => {
+ if (!supplierId) {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.paymentAmount = 0;
+ }
+ return Promise.resolve();
+ }
+ inboundBatchLoading.value = true;
+ return getInboundBatchesBySupplier({ supplierId })
+ .then((res) => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ inboundBatchList.value = Array.isArray(list) ? list : [];
+ inboundBatchOptions.value = normalizeInboundBatchOptions(list);
+ } else {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ inboundBatchList.value = [];
+ inboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ inboundBatchLoading.value = false;
+ if (keepSelected) {
+ ensureInboundOptionsForSelected();
+ restoreInboundTableSelection();
+ if (syncAmount && !isView.value) {
+ syncPaymentAmount();
+ }
+ }
+ });
+};
+
+const handleSupplierChange = (supplierId) => {
+ form.stockInRecordIds = [];
+ form.inboundBatches = "";
+ form.paymentAmount = 0;
+ loadInboundBatches(supplierId);
+};
+
+const handleInboundInputClick = () => {
+ if (isEdit.value || isView.value) return;
+ openInboundSelectDialog();
+};
+
+const openInboundSelectDialog = () => {
+ if (!form.supplierId || isEdit.value || isView.value) return;
+ inboundSelectVisible.value = true;
+ loadInboundBatches(form.supplierId, true, false).then(() => {
+ restoreInboundTableSelection();
+ });
+};
+
+const handleInboundDialogSelectionChange = (selection) => {
+ dialogInboundSelection.value = selection;
+};
+
+const confirmInboundSelection = () => {
+ if (dialogInboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叆搴撳崟");
+ return;
+ }
+ form.stockInRecordIds = dialogInboundSelection.value
+ .map((row) => getInboundRowId(row))
+ .filter((id) => id !== undefined && id !== null);
+ form.inboundBatches = dialogInboundSelection.value
+ .map((row) => row.inboundBatches ?? row.batchNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ dialogInboundSelection.value.forEach((row) => {
+ const [option] = normalizeInboundBatchOptions([row]);
+ if (option && !inboundBatchOptions.value.some((opt) => isSameInboundId(opt.value, option.value))) {
+ inboundBatchOptions.value.push(option);
+ }
+ });
+ inboundSelectVisible.value = false;
+ syncPaymentAmount();
+ formRef.value?.validateField("stockInRecordIds");
+};
+
+const handleInboundDialogClosed = () => {
+ dialogInboundSelection.value = [];
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.inbound-batch-input :deep(.el-input__wrapper) {
+ cursor: pointer;
+}
+</style>
diff --git a/src/views/financialManagement/payable/purchaseIn.vue b/src/views/financialManagement/payable/purchaseIn.vue
new file mode 100644
index 0000000..532bcb4
--- /dev/null
+++ b/src/views/financialManagement/payable/purchaseIn.vue
@@ -0,0 +1,212 @@
+<template>
+ <!-- 閲囪喘鍏ュ簱 -->
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="鍏ュ簱鍗曞彿:">
+ <el-input v-model="filters.inboundBatches"
+ placeholder="璇疯緭鍏ュ叆搴撳崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ clearable
+ filterable
+ style="width: 200px;">
+ <el-option v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏ュ簱鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage">
+ <template #inboundDate="{ row }">
+ {{ row.inboundDate ?? row.InboundDate ?? "" }}
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { ElMessage } from "element-plus";
+ import { listPageAccountPurchase } from "@/api/financialManagement/accountPurchase";
+ import { listSupplier } from "@/api/basicData/supplierManageFile.js";
+
+ defineOptions({
+ name: "閲囪喘鍏ュ簱",
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ const filters = reactive({
+ inboundBatches: "",
+ supplierId: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "鍏ュ簱鍗曞彿", prop: "inboundBatches", minWidth: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", minWidth: "180" },
+ {
+ label: "鍏ュ簱鏃ユ湡",
+ prop: "inboundDate",
+ minWidth: "170",
+ dataType: "slot",
+ slot: "inboundDate",
+ },
+ { label: "浜у搧鍚嶇О", prop: "productName", minWidth: "140" },
+ { label: "瑙勬牸鍨嬪彿", prop: "specificationModel", minWidth: "140" },
+ {
+ label: "閲戦",
+ prop: "inboundAmount",
+ minWidth: "120",
+ align: "right",
+ formatData: val =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+ },
+ { label: "閲囪喘璁㈠崟鍙�", prop: "purchaseContractNumber", minWidth: "150" },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+ const supplierList = ref([]);
+
+ const buildFilterParams = () => {
+ const params = {};
+ if (filters.inboundBatches) {
+ params.inboundBatches = filters.inboundBatches;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ };
+
+ const getSupplierList = () => {
+ listSupplier({ current: -1, size: -1, isWhite: 0 }).then(res => {
+ if (res.code === 200) {
+ supplierList.value = res.data?.records ?? [];
+ }
+ });
+ };
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchase({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then(res => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const resetFilters = () => {
+ filters.inboundBatches = "";
+ filters.supplierId = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+ };
+
+ const handleOut = () => {
+ proxy.download(
+ "/accountPurchase/exportAccountPurchaseInbound",
+ buildFilterParams(),
+ `閲囪喘鍏ュ簱_${Date.now()}.xlsx`
+ );
+ };
+
+ onMounted(() => {
+ getSupplierList();
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ }
+</style>
diff --git a/src/views/financialManagement/payable/purchaseReturn.vue b/src/views/financialManagement/payable/purchaseReturn.vue
new file mode 100644
index 0000000..eeec383
--- /dev/null
+++ b/src/views/financialManagement/payable/purchaseReturn.vue
@@ -0,0 +1,198 @@
+<template>
+ <!-- 閲囪喘閫�璐� -->
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="閫�璐у崟鍙�:">
+ <el-input v-model="filters.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="閫�璐ф棩鏈�:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessage } from "element-plus";
+import { listPageAccountPurchaseReturn } from "@/api/financialManagement/accountPurchase";
+import { listSupplier } from "@/api/basicData/supplierManageFile.js";
+
+defineOptions({
+ name: "閲囪喘閫�璐�",
+});
+
+const { proxy } = getCurrentInstance();
+
+const filters = reactive({
+ returnNo: "",
+ supplierId: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", minWidth: "180" },
+ { label: "鍏宠仈鍏ュ簱鍗曞彿", prop: "inboundBatches", minWidth: "150" },
+ { label: "閫�璐ф棩鏈�", prop: "preparedAt", minWidth: "170" },
+ {
+ label: "閫�娆炬�婚",
+ prop: "totalAmount",
+ minWidth: "150",
+ align: "right",
+ formatData: (val) =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+ },
+ { label: "閫�璐ф柟寮�", prop: "returnType", minWidth: "150" },
+ { label: "閲囪喘璁㈠崟鍙�", prop: "purchaseContractNumber", minWidth: "150" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const supplierList = ref([]);
+
+const buildFilterParams = () => {
+ const params = {};
+ if (filters.returnNo) {
+ params.returnNo = filters.returnNo;
+ }
+ if (filters.supplierId) {
+ params.supplierId = filters.supplierId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+};
+
+const getSupplierList = () => {
+ listSupplier({ current: -1, size: -1, isWhite: 0 }).then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data?.records ?? [];
+ }
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountPurchaseReturn({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.returnNo = "";
+ filters.supplierId = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const handleOut = () => {
+ proxy.download(
+ "/accountPurchase/exportAccountPurchaseReturn",
+ buildFilterParams(),
+ `閲囪喘閫�璐${Date.now()}.xlsx`
+ );
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/financialManagement/payable/reconciliation.vue b/src/views/financialManagement/payable/reconciliation.vue
new file mode 100644
index 0000000..e749e56
--- /dev/null
+++ b/src/views/financialManagement/payable/reconciliation.vue
@@ -0,0 +1,766 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="渚涘簲鍟�:">
+ <el-select v-model="filters.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable filterable style="width: 200px;">
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :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>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-button type="primary" @click="generateStatement" icon="Document">鐢熸垚瀵硅处鍗�</el-button>
+ </div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭瀵硅处鍗�</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #openingBalance="{ row }">
+ <span :class="row.openingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.openingBalance) }}</span>
+ </template>
+ <template #currentPlan="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.currentPlan) }}</span>
+ </template>
+ <template #currentActually="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.currentActually) }}</span>
+ </template>
+ <template #closingBalance="{ row }">
+ <span :class="row.closingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.closingBalance) }}</span>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="viewDetail(row)">鏌ョ湅鏄庣粏</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog title="瀵硅处鏄庣粏" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
+ <div class="statement-header">
+ <h3>{{ currentSupplier }} 搴斾粯瀵硅处鍗�</h3>
+ <p>瀵硅处鏈熼棿: {{ currentPeriod }}</p>
+ </div>
+ <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
+ <el-table-column prop="date" label="鏃ユ湡" width="120" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="row.type === '鍏ュ簱' ? 'success' : row.type === '閫�璐�' ? 'danger' : 'primary'">{{ row.type }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="code" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="debit" label="鍊熸柟(浠樻)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-success">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟(搴斾粯)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-danger">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else-if="row.credit < 0" class="text-success">楼{{ formatMoney(Math.abs(row.credit)) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="balance" label="浣欓" width="120">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.balance) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" show-overflow-tooltip />
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="printDetail">鎵撳嵃</el-button>
+ <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+ </template>
+ </FormDialog>
+
+ <FormDialog title="鐢熸垚瀵硅处鍗�" v-model="generateDialogVisible" width="1000px" @confirm="confirmGenerate" @cancel="generateDialogVisible = false">
+ <el-form :model="generateForm" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閫夋嫨渚涘簲鍟�" prop="supplierId">
+ <el-select
+ v-model="generateForm.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 100%;"
+ filterable
+ @change="onSupplierChange"
+ >
+ <el-option
+ v-for="item in supplierList"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </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>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <div v-if="statementDetailLoaded" class="purchase-section">
+ <div v-if="purchaseData.length > 0" class="section-title">鏈湀閲囪喘鏁版嵁</div>
+ <el-table
+ v-if="purchaseData.length > 0"
+ ref="purchaseTableRef"
+ :data="purchaseData"
+ border
+ row-key="id"
+ style="width: 100%; margin-bottom: 15px;"
+ v-loading="purchaseLoading"
+ @selection-change="handlePurchaseSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="occurrenceDate" label="鏃ユ湡" width="120" />
+ <el-table-column prop="receiptNumber" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getDetailTypeTagType(row.type)">{{ row.typeLabel }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="amount" label="閲戦" width="120">
+ <template #default="{ row }">
+ <span :class="getDetailAmountClass(row.type)">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" />
+ </el-table>
+ <el-empty v-else description="璇ヤ緵搴斿晢鏈湀鏆傛棤鏄庣粏鏁版嵁" :image-size="80" />
+
+ <div class="summary-row">
+ <span>鏈熷垵浣欓: <strong class="text-primary">楼{{ formatMoney(generateForm.openingBalance) }}</strong></span>
+ <span>鏈湡搴斾粯: <strong class="text-danger">楼{{ formatMoney(generateForm.currentPlan) }}</strong></span>
+ <span>鏈湡浠樻: <strong class="text-success">楼{{ formatMoney(generateForm.currentActually) }}</strong></span>
+ <span>鏈熸湯浣欓: <strong :class="displayClosingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(displayClosingBalance) }}</strong></span>
+ </div>
+ </div>
+
+ <div v-else-if="generateForm.supplierId && generateForm.statementMonth && !purchaseLoading" class="empty-tip">
+ <el-empty description="璇ヤ緵搴斿晢鏈湀鏆傛棤閲囪喘鏁版嵁" />
+ </div>
+
+ <template #footer>
+ <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate" :loading="submitLoading">纭鐢熸垚</el-button>
+ <el-button @click="generateDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
+import {
+ getAccountStatementDetailsByMonth,
+ addAccountStatement,
+ listPageAccountStatement,
+ deleteAccountStatement,
+} from "@/api/financialManagement/accountStatement.js";
+
+const ACCOUNT_TYPE_PAYABLE = 2;
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+ name: "搴斾粯瀵硅处",
+});
+
+const filters = reactive({
+ supplierId: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "瀵硅处鍗曞彿", prop: "statementNumber", width: "150" },
+ { label: "渚涘簲鍟�", prop: "supplierName", width: "180" },
+ { 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" },
+ { label: "鏈熸湯浣欓", prop: "closingBalance", dataType: "slot", slot: "closingBalance" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const submitLoading = ref(false);
+const detailDialogVisible = ref(false);
+const currentSupplier = ref("");
+const currentPeriod = ref("");
+const detailData = ref([]);
+const detailLoading = ref(false);
+
+const generateDialogVisible = ref(false);
+const purchaseLoading = ref(false);
+const statementDetailLoaded = ref(false);
+const purchaseData = ref([]);
+const selectedPurchases = ref([]);
+const purchaseTableRef = ref(null);
+const supplierList = ref([]);
+
+/** 鏄庣粏 type锛�1鍑哄簱 2鍏ュ簱 3鏀舵 4浠樻 5閫�璐� */
+const STATEMENT_DETAIL_TYPE_MAP = {
+ 1: "鍑哄簱",
+ 2: "鍏ュ簱",
+ 3: "鏀舵",
+ 4: "浠樻",
+ 5: "閫�璐�",
+};
+
+const calculateEndBalance = (openingBalance, currentPlan, currentActually) => {
+ return openingBalance + currentPlan - currentActually;
+};
+
+const getDetailTypeLabel = (type) => STATEMENT_DETAIL_TYPE_MAP[Number(type)] ?? "";
+
+const getDetailTypeTagType = (type) => {
+ const t = Number(type);
+ if (t === 2) return "success";
+ if (t === 4) return "primary";
+ if (t === 5) return "danger";
+ return "info";
+};
+
+const getDetailAmountClass = (type) => {
+ const t = Number(type);
+ if (t === 2) return "text-danger";
+ if (t === 4) return "text-success";
+ return "text-danger";
+};
+
+const generateForm = reactive({
+ supplierId: "",
+ supplierName: "",
+ statementMonth: "",
+ openingBalance: 0,
+ currentPlan: 0,
+ currentActually: 0,
+ closingBalance: 0,
+});
+
+const displayClosingBalance = computed(() =>
+ calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ )
+);
+
+const canGenerate = computed(
+ () => generateForm.supplierId && generateForm.statementMonth && selectedPurchases.value.length > 0
+);
+
+const applyStatementSummary = (data) => {
+ generateForm.openingBalance = Number(data.openingBalance ?? 0);
+ generateForm.currentPlan = Number(data.currentPlan ?? 0);
+ generateForm.currentActually = Number(data.currentActually ?? 0);
+ generateForm.closingBalance = Number(
+ data.closingBalance ??
+ calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ )
+ );
+};
+
+const getSupplierList = () => {
+ getOptions().then((res) => {
+ if (res.code === 200) {
+ supplierList.value = res.data ?? [];
+ }
+ });
+};
+
+const normalizePurchaseRows = (list) => {
+ const rows = Array.isArray(list) ? list : [];
+ return rows.map((item, index) => {
+ const type = Number(item.type);
+ return {
+ id: item.id ?? `detail-${index}`,
+ accountStatementId: item.accountStatementId,
+ occurrenceDate: item.occurrenceDate ?? "",
+ receiptNumber: item.receiptNumber ?? "",
+ type,
+ typeLabel: getDetailTypeLabel(type),
+ amount: Math.abs(Number(item.amount ?? 0)),
+ remark: item.remark ?? "",
+ };
+ });
+};
+
+const selectAllPurchaseRows = (keepApiSummary = false) => {
+ nextTick(() => {
+ const table = purchaseTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ purchaseData.value.forEach((row) => table.toggleRowSelection(row, true));
+ selectedPurchases.value = [...purchaseData.value];
+ if (!keepApiSummary) {
+ calculateSummary();
+ }
+ });
+};
+
+const isNumericId = (id) => id !== undefined && id !== null && id !== "" && /^\d+$/.test(String(id));
+
+const buildFilterParams = (params = {}) => {
+ const result = { ...params, accountType: ACCOUNT_TYPE_PAYABLE };
+ 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;
+ }
+ return result;
+};
+
+const buildListParams = () =>
+ buildFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => buildFilterParams({});
+
+const buildDetailSubmitItem = (row) => {
+ const item = {
+ occurrenceDate: row.occurrenceDate,
+ receiptNumber: row.receiptNumber,
+ type: row.type,
+ amount: row.amount,
+ remark: row.remark ?? "",
+ };
+ if (isNumericId(row.id)) {
+ item.id = Number(row.id);
+ }
+ if (row.accountStatementId) {
+ item.accountStatementId = row.accountStatementId;
+ }
+ return item;
+};
+
+const buildAddPayload = () => ({
+ customerId: generateForm.supplierId,
+ customerName: generateForm.supplierName,
+ statementMonth: generateForm.statementMonth,
+ accountType: ACCOUNT_TYPE_PAYABLE,
+ statementNumber: "",
+ openingBalance: generateForm.openingBalance,
+ currentPlan: generateForm.currentPlan,
+ currentActually: generateForm.currentActually,
+ closingBalance: generateForm.closingBalance,
+ accountStatementDetails: selectedPurchases.value.map(buildDetailSubmitItem),
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountStatement(buildListParams())
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = (res.data.records ?? []).map((row) => ({
+ ...row,
+ supplierName: row.supplierName ?? row.customerName,
+ }));
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.supplierId = "";
+ filters.startMonth = "";
+ filters.endMonth = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const generateStatement = () => {
+ generateForm.supplierId = "";
+ generateForm.supplierName = "";
+ generateForm.statementMonth = "";
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ statementDetailLoaded.value = false;
+ purchaseData.value = [];
+ selectedPurchases.value = [];
+ generateDialogVisible.value = true;
+};
+
+const onSupplierChange = (supplierId) => {
+ const supplier = supplierList.value.find((item) => item.id === supplierId);
+ generateForm.supplierName = supplier?.supplierName ?? "";
+ loadPurchaseData();
+};
+
+const onStatementMonthChange = () => {
+ loadPurchaseData();
+};
+
+const loadPurchaseData = () => {
+ if (!generateForm.supplierId || !generateForm.statementMonth) {
+ purchaseData.value = [];
+ selectedPurchases.value = [];
+ statementDetailLoaded.value = false;
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ return;
+ }
+
+ purchaseLoading.value = true;
+ selectedPurchases.value = [];
+ statementDetailLoaded.value = false;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_PAYABLE,
+ customerId: generateForm.supplierId,
+ statementMonth: generateForm.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ const data = res.data ?? {};
+ const details = data.accountStatementDetails;
+ const list = Array.isArray(details) ? details : [];
+ purchaseData.value = normalizePurchaseRows(list);
+ applyStatementSummary(data);
+ statementDetailLoaded.value = true;
+
+ if (purchaseData.value.length > 0) {
+ selectAllPurchaseRows(true);
+ }
+ } else {
+ purchaseData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error(res.msg || "鏌ヨ瀵硅处鏄庣粏澶辫触");
+ }
+ })
+ .catch(() => {
+ purchaseData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error("鏌ヨ瀵硅处鏄庣粏澶辫触");
+ })
+ .finally(() => {
+ purchaseLoading.value = false;
+ });
+};
+
+const calculateSummary = () => {
+ let payable = 0;
+ let payment = 0;
+
+ selectedPurchases.value.forEach((item) => {
+ if (item.type === 2) {
+ payable += item.amount;
+ } else if (item.type === 5) {
+ payable -= item.amount;
+ } else if (item.type === 4) {
+ payment += item.amount;
+ }
+ });
+
+ generateForm.currentPlan = payable;
+ generateForm.currentActually = payment;
+ generateForm.closingBalance = calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ );
+};
+
+const handlePurchaseSelectionChange = (selection) => {
+ selectedPurchases.value = selection;
+ calculateSummary();
+};
+
+const confirmGenerate = () => {
+ if (!canGenerate.value) return;
+ submitLoading.value = true;
+ addAccountStatement(buildAddPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ generateDialogVisible.value = false;
+ ElMessage.success("瀵硅处鍗曠敓鎴愭垚鍔�");
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鐢熸垚澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鐢熸垚澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎瀵硅处鍗曘��${row.statementNumber || row.id}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountStatement([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const buildDetailTableFromApi = (data, statementMonth) => {
+ const details = Array.isArray(data.accountStatementDetails) ? data.accountStatementDetails : [];
+ let runningBalance = Number(data.openingBalance ?? 0);
+ const rows = [
+ {
+ date: statementMonth ?? "",
+ type: "鏈熷垵",
+ code: "-",
+ debit: 0,
+ credit: 0,
+ balance: runningBalance,
+ remark: "鏈熷垵浣欓",
+ },
+ ];
+
+ details.forEach((item) => {
+ const amount = Math.abs(Number(item.amount ?? 0));
+ const type = Number(item.type);
+ let debit = 0;
+ let credit = 0;
+
+ if (type === 2) {
+ credit = amount;
+ runningBalance += amount;
+ } else if (type === 4) {
+ debit = amount;
+ runningBalance -= amount;
+ } else if (type === 5) {
+ credit = -amount;
+ runningBalance -= amount;
+ }
+
+ rows.push({
+ date: item.occurrenceDate ?? "",
+ type: getDetailTypeLabel(type),
+ code: item.receiptNumber ?? "",
+ debit,
+ credit,
+ balance: runningBalance,
+ remark: item.remark ?? "",
+ });
+ });
+
+ return rows;
+};
+
+const viewDetail = (row) => {
+ const partnerId = row.customerId ?? row.supplierId;
+ if (!partnerId || !row.statementMonth) {
+ ElMessage.warning("缂哄皯渚涘簲鍟嗘垨瀵硅处鏈堜唤锛屾棤娉曟煡璇㈡槑缁�");
+ return;
+ }
+
+ currentSupplier.value = row.supplierName ?? row.customerName ?? "";
+ currentPeriod.value = row.statementMonth ?? "";
+ detailData.value = [];
+ detailDialogVisible.value = true;
+ detailLoading.value = true;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_PAYABLE,
+ customerId: partnerId,
+ statementMonth: row.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ detailData.value = buildDetailTableFromApi(res.data ?? {}, row.statementMonth);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ })
+ .finally(() => {
+ detailLoading.value = false;
+ });
+};
+
+const printDetail = () => {
+ ElMessage.info("鎵撳嵃鏄庣粏");
+};
+
+const handleOut = () => {
+ proxy.download(
+ "/accountStatement/exportAccountStatement",
+ buildExportParams(),
+ `搴斾粯瀵硅处鍗昣${Date.now()}.xlsx`
+ );
+};
+
+onMounted(() => {
+ getSupplierList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-success {
+ color: #67c23a;
+}
+
+.text-danger {
+ color: #f56c6c;
+}
+
+.text-primary {
+ color: #409eff;
+}
+
+.statement-header {
+ text-align: center;
+ margin-bottom: 20px;
+ h3 {
+ margin: 0 0 10px 0;
+ }
+ p {
+ color: #909399;
+ margin: 0;
+ }
+}
+
+.purchase-section {
+ margin-top: 20px;
+
+ .section-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ padding-left: 10px;
+ border-left: 4px solid #409eff;
+ }
+}
+
+.summary-row {
+ display: flex;
+ justify-content: space-around;
+ padding: 15px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ margin-top: 15px;
+
+ span {
+ font-size: 14px;
+
+ strong {
+ font-size: 16px;
+ margin-left: 5px;
+ }
+ }
+}
+
+.empty-tip {
+ margin-top: 30px;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/invoiceApply.vue b/src/views/financialManagement/receivable/invoiceApply.vue
new file mode 100644
index 0000000..85f30b2
--- /dev/null
+++ b/src/views/financialManagement/receivable/invoiceApply.vue
@@ -0,0 +1,927 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鐢宠鍗曞彿:">
+ <el-input v-model="filters.applyCode" placeholder="璇疯緭鍏ョ敵璇峰崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable style="width: 200px;">
+ <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-select v-model="filters.status" placeholder="璇烽�夋嫨瀹℃牳鐘舵��" clearable style="width: 150px;">
+ <el-option label="寰呭鏍�" :value="0" />
+ <el-option label="瀹℃牳閫氳繃" :value="1" />
+ <el-option label="瀹℃牳涓嶉�氳繃" :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐢宠鏃ユ湡:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板鐢宠</el-button>
+ <el-button type="success" @click="handleExport" icon="Download">瀵煎嚭寮�绁ㄧ敵璇�</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ v-loading="tableLoading"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #taxRate="{ row }">
+ <span>{{ row.taxRate }}%</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)" effect="light" round>
+ {{ getStatusLabel(row.status) }}
+ </el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)" v-if="isPendingStatus(row.status)">缂栬緫</el-button>
+ <el-button type="danger" link @click="handleDelete(row)" v-if="isPendingStatus(row.status)">鍒犻櫎</el-button>
+ <el-button type="success" link @click="handleAudit(row)" v-if="isPendingStatus(row.status)">瀹℃牳</el-button>
+ <el-button type="primary" link @click="openFileDialog(row)" v-if="isApprovedStatus(row.status)">闄勪欢</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹℃牳鐘舵��">
+ <el-tag :type="getStatusType(form.status)" effect="light" round>
+ {{ getStatusLabel(form.status) }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鐢宠鍗曞彿" prop="applyCode">
+ <el-input v-model="form.applyCode" placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="form.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ style="width: 100%;"
+ :disabled="isEdit || isView"
+ filterable
+ @change="handleCustomerChange"
+ >
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍑哄簱鍗曞彿" prop="outboundBatchNos">
+ <el-input
+ :model-value="outboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨瀹㈡埛"
+ readonly
+ :disabled="!form.customerId || isEdit || isView"
+ class="outbound-batch-input"
+ @click="handleOutboundInputClick"
+ >
+ <template v-if="!isEdit && !isView" #append>
+ <el-button
+ :disabled="!form.customerId"
+ :loading="outboundBatchLoading"
+ @click.stop="openOutboundSelectDialog"
+ >
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�绁ㄩ噾棰�" prop="amount">
+ <el-input-number
+ v-model="form.amount"
+ :min="0"
+ :precision="2"
+ :disabled="isView"
+ style="width: 100%;"
+ placeholder="鏍规嵁鎵�閫夊嚭搴撳崟鑷姩姹囨�伙紝鍙慨鏀�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼" prop="taxRate">
+ <el-select v-model="form.taxRate" placeholder="璇烽�夋嫨绋庣巼" style="width: 100%;" :disabled="isView">
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="Number(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+ <el-select v-model="form.invoiceType" placeholder="璇烽�夋嫨鍙戠エ绫诲瀷" style="width: 100%;" :disabled="isView">
+ <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+ <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+ <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢宠鏃ユ湡" prop="applyDate">
+ <el-date-picker
+ v-model="form.applyDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker
+ v-model="formCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍙戠エ鍐呭" prop="content">
+ <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <el-dialog
+ v-model="outboundSelectVisible"
+ title="閫夋嫨鍑哄簱鍗�"
+ width="1200px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleOutboundDialogClosed"
+ >
+ <el-table
+ ref="outboundTableRef"
+ v-loading="outboundBatchLoading"
+ :data="outboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleOutboundDialogSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="outboundBatches" label="鍑哄簱鍗曞彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="120" show-overflow-tooltip />
+ <el-table-column prop="specificationModel" label="瑙勬牸鍨嬪彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="salesContractNo" label="閿�鍞悎鍚屽彿" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="shippingNo" label="鍙戣揣鍗曞彿" min-width="130" show-overflow-tooltip />
+ <el-table-column prop="shippingDate" label="鍙戣揣鏃ユ湡" width="110" align="center" />
+ <el-table-column prop="outboundAmount" label="鍑哄簱閲戦" width="110" align="right">
+ <template #default="{ row }">楼{{ formatMoney(row.outboundAmount) }}</template>
+ </el-table-column>
+ <el-table-column prop="taxRate" label="绋庣巼" width="80" align="center">
+ <template #default="{ row }">{{ row.taxRate }}%</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="confirmOutboundSelection">纭畾</el-button>
+ <el-button @click="outboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+
+ <FileList
+ v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ record-type="account_invoice_application"
+ :record-id="currentRecordId"
+ />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, nextTick, getCurrentInstance, defineAsyncComponent } from "vue";
+import dayjs from "dayjs";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+ getOutboundBatchesByCustomer,
+ addAccountInvoiceApplication,
+ listPageAccountInvoiceApplication,
+ auditAccountInvoiceApplication,
+ updateAccountInvoiceApplication,
+ deleteAccountInvoiceApplication,
+} from "@/api/financialManagement/invoiceApply.js";
+
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+defineOptions({
+ name: "寮�绁ㄧ敵璇�",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+ applyCode: "",
+ customerId: "",
+ status: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鐢宠鍗曞彿", prop: "applyCode", width: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { label: "寮�绁ㄩ噾棰�", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "绋庣巼", prop: "taxRate", dataType: "slot", slot: "taxRate" },
+ { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+ { label: "鐢宠鏃ユ湡", prop: "applyDate", width: "120" },
+ { label: "瀹℃牳鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "110", align: "center" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "300", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const selectedRows = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isEdit = ref(false);
+const isView = ref(false);
+const currentId = ref(null);
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ outboundSelectVisible.value = false;
+ isView.value = false;
+ isEdit.value = false;
+};
+
+const customerList = ref([]);
+const outboundBatchList = ref([]);
+const outboundBatchOptions = ref([]);
+const outboundBatchLoading = ref(false);
+const outboundSelectVisible = ref(false);
+const outboundTableRef = ref(null);
+const dialogOutboundSelection = ref([]);
+
+const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+};
+
+const normalizeOutboundBatchOptions = (data) => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ if (typeof item === "string" || typeof item === "number") {
+ const text = String(item);
+ return { label: text, value: text, outboundAmount: 0 };
+ }
+ const label =
+ item.outboundBatches ??
+ item.batchNo ??
+ item.shippingNo ??
+ item.outboundNo ??
+ item.label ??
+ `鍑哄簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockOutRecordId ?? item.stockOutRecordIds ?? label;
+ const outboundAmount = Number(item.outboundAmount) || 0;
+ const taxRate =
+ item.taxRate !== undefined && item.taxRate !== null && item.taxRate !== ""
+ ? Number(item.taxRate)
+ : undefined;
+ return { label: String(label), value, outboundAmount, taxRate };
+ });
+};
+
+const isSameOutboundId = (a, b) => String(a) === String(b);
+
+const getSelectedOutboundOptions = () => {
+ const selected = form.outboundBatchNos || [];
+ return outboundBatchOptions.value.filter((opt) =>
+ selected.some((id) => isSameOutboundId(id, opt.value))
+ );
+};
+
+/** 鏍¢獙鎵�閫夊嚭搴撳崟绋庣巼鏄惁涓�鑷达紝涓�鑷村垯鍥炲~ form.taxRate */
+const checkTaxRateConsistency = (showMessage = true) => {
+ const selected = getSelectedOutboundOptions();
+ if (selected.length === 0) return true;
+
+ const withTaxRate = selected.filter(
+ (opt) => opt.taxRate !== undefined && opt.taxRate !== null && !Number.isNaN(opt.taxRate)
+ );
+ if (withTaxRate.length === 0) return true;
+
+ const uniqueRates = [...new Set(withTaxRate.map((opt) => Number(opt.taxRate)))];
+ if (uniqueRates.length > 1) {
+ if (showMessage) {
+ const detail = withTaxRate.map((opt) => `${opt.label}(${opt.taxRate}%)`).join("銆�");
+ ElMessage.error(`鎵�閫夊嚭搴撳崟绋庣巼涓嶄竴鑷达紝鏃犳硶寮�绁細${detail}`);
+ }
+ return false;
+ }
+
+ form.taxRate = uniqueRates[0];
+ return true;
+};
+
+/** 鏍规嵁鎵�閫夊嚭搴撳崟姹囨�� outboundAmount 浣滀负寮�绁ㄩ噾棰� */
+const syncInvoiceAmount = () => {
+ const selected = form.outboundBatchNos || [];
+ const sum = outboundBatchOptions.value
+ .filter((opt) => selected.some((id) => isSameOutboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.outboundAmount) || 0), 0);
+ form.amount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+};
+
+const getOutboundRowId = (row) => row?.id ?? row?.stockOutRecordId;
+
+const outboundBatchDisplayText = computed(() => {
+ if (isEdit.value || isView.value) {
+ return form.outboundBatches || "";
+ }
+ if (form.outboundBatches) return form.outboundBatches;
+ const ids = form.outboundBatchNos || [];
+ if (!ids.length) return "";
+ return outboundBatchOptions.value
+ .filter((opt) => ids.some((id) => isSameOutboundId(id, opt.value)))
+ .map((opt) => opt.label)
+ .join("銆�");
+});
+
+const handleOutboundInputClick = () => {
+ if (isEdit.value || isView.value) return;
+ openOutboundSelectDialog();
+};
+
+const restoreOutboundTableSelection = () => {
+ nextTick(() => {
+ const table = outboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set((form.outboundBatchNos || []).map((id) => String(id)));
+ outboundBatchList.value.forEach((row) => {
+ const rowId = getOutboundRowId(row);
+ if (rowId !== undefined && rowId !== null && selectedIds.has(String(rowId))) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+};
+
+const openOutboundSelectDialog = () => {
+ if (!form.customerId || isEdit.value || isView.value) return;
+ outboundSelectVisible.value = true;
+ loadOutboundBatches(form.customerId, true).then(() => {
+ restoreOutboundTableSelection();
+ });
+};
+
+const handleOutboundDialogSelectionChange = (selection) => {
+ dialogOutboundSelection.value = selection;
+};
+
+const confirmOutboundSelection = () => {
+ if (dialogOutboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″嚭搴撳崟");
+ return;
+ }
+ const prevIds = [...(form.outboundBatchNos || [])];
+ const prevBatches = form.outboundBatches;
+ form.outboundBatchNos = dialogOutboundSelection.value
+ .map((row) => getOutboundRowId(row))
+ .filter((id) => id !== undefined && id !== null);
+ form.outboundBatches = dialogOutboundSelection.value
+ .map((row) => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ if (!checkTaxRateConsistency()) {
+ form.outboundBatchNos = prevIds;
+ form.outboundBatches = prevBatches;
+ return;
+ }
+ outboundSelectVisible.value = false;
+ syncInvoiceAmount();
+ formRef.value?.validateField("outboundBatchNos");
+};
+
+const handleOutboundDialogClosed = () => {
+ dialogOutboundSelection.value = [];
+};
+
+const loadOutboundBatches = (customerId, keepSelected = false) => {
+ if (!customerId) {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.outboundBatchNos = [];
+ form.amount = 0;
+ }
+ return Promise.resolve();
+ }
+ outboundBatchLoading.value = true;
+ return getOutboundBatchesByCustomer({ customerId })
+ .then((res) => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ outboundBatchList.value = Array.isArray(list) ? list : [];
+ outboundBatchOptions.value = normalizeOutboundBatchOptions(list);
+ } else {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ outboundBatchLoading.value = false;
+ if (keepSelected) {
+ syncInvoiceAmount();
+ checkTaxRateConsistency(false);
+ }
+ });
+};
+
+const handleCustomerChange = (customerId) => {
+ form.outboundBatchNos = [];
+ form.outboundBatches = "";
+ form.amount = 0;
+ loadOutboundBatches(customerId);
+};
+
+const form = reactive({
+ applyCode: "",
+ customerId: "",
+ outboundBatchNos: [],
+ outboundBatches: "",
+ amount: 0,
+ taxRate: 13,
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ applyDate: "",
+ content: "",
+ remark: "",
+ createTime: "",
+});
+const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+});
+
+const rules = {
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ outboundBatchNos: [{ required: true, type: "array", min: 1, message: "璇烽�夋嫨鍑哄簱鍗曞彿", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ュ紑绁ㄩ噾棰�", trigger: "blur" }],
+ taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+ applyDate: [{ required: true, message: "璇烽�夋嫨鐢宠鏃ユ湡", trigger: "change" }],
+};
+
+/** 瀹℃牳鐘舵�侊細0寰呭鏍� 1瀹℃牳閫氳繃 2瀹℃牳涓嶉�氳繃 */
+const STATUS_LABEL_MAP = {
+ 0: "寰呭鏍�",
+ 1: "瀹℃牳閫氳繃",
+ 2: "瀹℃牳涓嶉�氳繃",
+};
+
+const STATUS_TYPE_MAP = {
+ 0: "warning",
+ 1: "success",
+ 2: "danger",
+};
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return status;
+ const num = Number(status);
+ return Number.isNaN(num) ? status : num;
+};
+
+const isPendingStatus = (status) => normalizeStatus(status) === 0;
+const isApprovedStatus = (status) => normalizeStatus(status) === 1;
+
+const fileDialogVisible = ref(false);
+const currentRecordId = ref(0);
+
+const openFileDialog = (row) => {
+ currentRecordId.value = row.id;
+ fileDialogVisible.value = true;
+};
+
+const formatOutboundBatches = (value) => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+};
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ applyCode: row.invoiceApplicationNo ?? row.applyCode,
+ amount: row.invoiceAmount ?? row.amount,
+ content: row.invoiceContent ?? row.content,
+ status: normalizeStatus(row.status ?? row.auditStatus),
+ stockOutRecordIds: row.stockOutRecordIds ?? row.stockOutRecordId ?? "",
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+});
+
+const appendFilterParams = (params) => {
+ if (filters.applyCode) {
+ params.invoiceApplicationNo = filters.applyCode;
+ }
+ if (filters.customerId) {
+ params.customerId = filters.customerId;
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+};
+
+const buildListParams = () => {
+ return appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+};
+
+const buildExportParams = () => {
+ const params = appendFilterParams({});
+ if (selectedRows.value.length > 0) {
+ params.ids = selectedRows.value.map((row) => row.id).join(",");
+ }
+ return params;
+};
+
+const handleExport = () => {
+ const params = buildExportParams();
+ const filename =
+ selectedRows.value.length > 0
+ ? `寮�绁ㄧ敵璇穇宸查��${selectedRows.value.length}鏉${Date.now()}.xlsx`
+ : `寮�绁ㄧ敵璇穇${Date.now()}.xlsx`;
+ proxy.download("/accountInvoiceApplication/exportAccountInvoiceApplication", params, filename);
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getStatusLabel = (status) => {
+ const num = normalizeStatus(status);
+ if (num === 0 || num === 1 || num === 2) {
+ return STATUS_LABEL_MAP[num];
+ }
+ return "-";
+};
+
+const getStatusType = (status) => {
+ const num = normalizeStatus(status);
+ if (num === 0 || num === 1 || num === 2) {
+ return STATUS_TYPE_MAP[num];
+ }
+ return "info";
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountInvoiceApplication(buildListParams())
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = (res.data.records ?? []).map(normalizeTableRow);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.applyCode = "";
+ filters.customerId = "";
+ filters.status = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+const fillFormFromRow = (row) => {
+ const outboundBatchNos = Array.isArray(row.outboundBatchNos)
+ ? row.outboundBatchNos
+ : parseStockOutRecordIds(row.stockOutRecordIds ?? row.stockOutRecordId);
+ Object.assign(form, {
+ ...row,
+ applyCode: row.applyCode ?? row.invoiceApplicationNo ?? "",
+ amount: Number(row.amount ?? row.invoiceAmount ?? 0),
+ content: row.content ?? row.invoiceContent,
+ status: normalizeStatus(row.status ?? row.auditStatus),
+ outboundBatchNos,
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+ createTime: row.createTime ?? "",
+ });
+};
+
+const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ dialogTitle.value = "鏂板寮�绁ㄧ敵璇�";
+ Object.assign(form, {
+ applyCode: "",
+ customerId: "",
+ outboundBatchNos: [],
+ outboundBatches: "",
+ amount: 0,
+ taxRate: 13,
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ applyDate: new Date().toISOString().split("T")[0],
+ content: "",
+ remark: "",
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ });
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ dialogVisible.value = true;
+};
+
+const parseStockOutRecordIds = (value) => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map((s) => s.trim())
+ .filter(Boolean)
+ .map((s) => (/^\d+$/.test(s) ? Number(s) : s));
+};
+
+const buildSubmitPayload = (forUpdate = false) => {
+ const payload = {
+ customerId: form.customerId,
+ stockOutRecordIds: (form.outboundBatchNos || []).join(","),
+ invoiceApplicationNo: form.applyCode || "",
+ invoiceType: form.invoiceType,
+ applyDate: form.applyDate,
+ invoiceContent: form.content,
+ remark: form.remark || "",
+ invoiceAmount: form.amount,
+ taxRate: form.taxRate,
+ status: 0,
+ createTime: form.createTime,
+ };
+ if (forUpdate) {
+ payload.id = currentId.value;
+ }
+ return payload;
+};
+
+const edit = (row) => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫寮�绁ㄧ敵璇�";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+ loadOutboundBatches(form.customerId, true);
+};
+
+const view = (row) => {
+ isView.value = true;
+ isEdit.value = false;
+ dialogTitle.value = "鏌ョ湅寮�绁ㄧ敵璇�";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+};
+
+const submitAudit = (row, status) => {
+ auditAccountInvoiceApplication({ id: row.id, status })
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(status === 1 ? "瀹℃牳閫氳繃" : "瀹℃牳涓嶉�氳繃");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "瀹℃壒澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("瀹℃壒澶辫触");
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鐢宠鍗曘��${row.applyCode ?? row.invoiceApplicationNo}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountInvoiceApplication([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const handleAudit = (row) => {
+ ElMessageBox.confirm("璇烽�夋嫨瀹℃壒缁撴灉", "寮�绁ㄧ敵璇峰鏍�", {
+ confirmButtonText: "瀹℃牳閫氳繃",
+ cancelButtonText: "瀹℃牳涓嶉�氳繃",
+ distinguishCancelAndClose: true,
+ type: "warning",
+ })
+ .then(() => {
+ submitAudit(row, 1);
+ })
+ .catch((action) => {
+ if (action === "cancel") {
+ submitAudit(row, 2);
+ }
+ });
+};
+
+const handleInvoice = (row) => {
+ ElMessageBox.confirm("纭宸插紑鍏峰彂绁紵", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(() => {
+ ElMessage.success("寮�绁ㄥ畬鎴�");
+ getTableData();
+ });
+};
+
+const handleBatchApply = () => {
+ ElMessage.success(`鎵归噺鐢宠 ${selectedRows.value.length} 鏉¤褰昤);
+};
+
+const submitLoading = ref(false);
+
+const submitForm = () => {
+ formRef.value.validate((valid) => {
+ if (!valid) return;
+ if (!checkTaxRateConsistency()) return;
+
+ submitLoading.value = true;
+ const request = isEdit.value
+ ? updateAccountInvoiceApplication(buildSubmitPayload(true))
+ : addAccountInvoiceApplication(buildSubmitPayload());
+
+ request
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success(isEdit.value ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+ closeDialog();
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || (isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触"));
+ }
+ })
+ .catch(() => {
+ ElMessage.error(isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+onMounted(() => {
+ getCustomerList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.outbound-batch-input:not(.is-disabled) {
+ :deep(.el-input__wrapper) {
+ cursor: pointer;
+ }
+}
+</style>
diff --git a/src/views/financialManagement/receivable/outputInvoice.vue b/src/views/financialManagement/receivable/outputInvoice.vue
new file mode 100644
index 0000000..d746aea
--- /dev/null
+++ b/src/views/financialManagement/receivable/outputInvoice.vue
@@ -0,0 +1,608 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍙戠エ鍙风爜:">
+ <el-input v-model="filters.invoiceNumber" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable style="width: 200px;">
+ <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.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="姝e父" :value="0" />
+ <el-option label="浣滃簾" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <!-- <el-button type="primary" @click="add" icon="Plus">褰曞叆鍙戠エ</el-button> -->
+ <el-button type="success" @click="handleExport" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ v-loading="tableLoading"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #amount="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #taxAmount="{ row }">
+ <span class="text-danger">楼{{ formatMoney(row.taxAmount) }}</span>
+ </template>
+ <template #totalAmount="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.totalAmount) }}</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)" effect="light" round>
+ {{ getStatusLabel(row.status) }}
+ </el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button
+ type="primary"
+ link
+ @click="openFileDialog(row)"
+ v-if="row.accountInvoiceApplicationId"
+ >
+ 闄勪欢
+ </el-button>
+ <el-button type="warning" link @click="handleCancel(row)" v-if="isNormalStatus(row.status)">浣滃簾</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog
+ :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
+ <el-row v-if="isView" :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐘舵��">
+ <el-tag :type="getStatusType(form.status)" effect="light" round>
+ {{ getStatusLabel(form.status) }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ鍙风爜" prop="invoiceNo">
+ <el-input v-model="form.invoiceNo" placeholder="璇疯緭鍏ュ彂绁ㄥ彿鐮�" :disabled="isView" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛" prop="customerId">
+ <el-select v-model="form.customerId" placeholder="璇烽�夋嫨瀹㈡埛" style="width: 100%;" :disabled="isView">
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�绁ㄦ棩鏈�" prop="invoiceDate">
+ <el-date-picker
+ v-model="form.invoiceDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷" prop="invoiceType">
+ <el-select
+ v-model="form.invoiceType"
+ placeholder="璇烽�夋嫨鍙戠エ绫诲瀷"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="handleInvoiceTypeChange"
+ >
+ <el-option label="澧炲�肩◣涓撶敤鍙戠エ" value="澧炲�肩◣涓撶敤鍙戠エ" />
+ <el-option label="澧炲�肩◣鏅�氬彂绁�" value="澧炲�肩◣鏅�氬彂绁�" />
+ <el-option label="鐢靛瓙鍙戠エ" value="鐢靛瓙鍙戠エ" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绋庣巼" prop="taxRate">
+ <el-select
+ v-model="form.taxRate"
+ placeholder="璇烽�夋嫨绋庣巼"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="calculateTax"
+ >
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="Number(dict.value)"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閲戦(涓嶅惈绋�)" prop="amount">
+ <el-input-number
+ v-model="form.amount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ @change="calculateTax"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="绋庨">
+ <el-input v-model="form.taxAmount" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浠风◣鍚堣">
+ <el-input v-model="form.totalAmount" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍙戠エ鍐呭" prop="content">
+ <el-input v-model="form.content" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ彂绁ㄥ唴瀹�" :disabled="isView" />
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏ュ娉�" :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView" #footer>
+ <el-button type="primary" :loading="submitLoading" @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+
+ <FileList
+ v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ record-type="account_invoice_application"
+ :record-id="currentRecordId"
+ :editable="false"
+ />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance, defineAsyncComponent } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+ addAccountSalesInvoice,
+ listPageAccountSalesInvoice,
+ cancelAccountSalesInvoice,
+ deleteAccountSalesInvoice,
+} from "@/api/financialManagement/accountSalesInvoice.js";
+
+const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+defineOptions({
+ name: "閿�椤瑰彂绁�",
+});
+
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+
+const filters = reactive({
+ invoiceNumber: "",
+ customerId: "",
+ dateRange: [],
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鍙戠エ鍙风爜", prop: "invoiceNo", width: "140" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { label: "寮�绁ㄦ棩鏈�", prop: "invoiceDate", width: "120" },
+ { label: "閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ { label: "绋庨", prop: "taxAmount", dataType: "slot", slot: "taxAmount" },
+ { label: "浠风◣鍚堣", prop: "totalAmount", dataType: "slot", slot: "totalAmount" },
+ { label: "鍙戠エ绫诲瀷", prop: "invoiceType", width: "130" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status", width: "90", align: "center" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "260", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const isView = ref(false);
+const submitLoading = ref(false);
+
+const customerList = ref([]);
+const fileDialogVisible = ref(false);
+const currentRecordId = ref(0);
+
+const openFileDialog = (row) => {
+ if (!row.accountInvoiceApplicationId) {
+ ElMessage.warning("鏈叧鑱斿紑绁ㄧ敵璇凤紝鏃犳硶鏌ョ湅闄勪欢");
+ return;
+ }
+ currentRecordId.value = row.accountInvoiceApplicationId;
+ fileDialogVisible.value = true;
+};
+
+/** 鐘舵�侊細0姝e父 1浣滃簾 */
+const STATUS_LABEL_MAP = { 0: "姝e父", 1: "浣滃簾" };
+const STATUS_TYPE_MAP = { 0: "success", 1: "info" };
+
+const normalizeStatus = (status) => {
+ if (status === undefined || status === null || status === "") return 0;
+ const num = Number(status);
+ return Number.isNaN(num) ? 0 : num;
+};
+
+const isNormalStatus = (status) => normalizeStatus(status) === 0;
+
+const getStatusLabel = (status) => {
+ const num = normalizeStatus(status);
+ return STATUS_LABEL_MAP[num] ?? "姝e父";
+};
+
+const getStatusType = (status) => {
+ const num = normalizeStatus(status);
+ return STATUS_TYPE_MAP[num] ?? "success";
+};
+
+const form = reactive({
+ invoiceNo: "",
+ customerId: "",
+ invoiceDate: "",
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ accountInvoiceApplicationId: undefined,
+ storageAttachmentId: undefined,
+});
+
+const rules = {
+ invoiceNo: [{ required: true, message: "璇疯緭鍏ュ彂绁ㄥ彿鐮�", trigger: "blur" }],
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ invoiceDate: [{ required: true, message: "璇烽�夋嫨寮�绁ㄦ棩鏈�", trigger: "change" }],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨鍙戠エ绫诲瀷", trigger: "change" }],
+ taxRate: [{ required: true, message: "璇烽�夋嫨绋庣巼", trigger: "change" }],
+ amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const calculateTax = () => {
+ form.taxAmount = Number((form.amount * form.taxRate / 100).toFixed(2));
+ form.totalAmount = Number((form.amount + form.taxAmount).toFixed(2));
+};
+
+const handleInvoiceTypeChange = () => {
+ calculateTax();
+};
+
+const normalizeTableRow = (row) => ({
+ ...row,
+ invoiceNo: row.invoiceNumber ?? row.invoiceNo,
+ invoiceDate: row.issueDate ?? row.invoiceDate,
+ amount: row.taxExclusivelPrice ?? row.amount,
+ taxAmount: row.taxPrice ?? row.taxAmount,
+ totalAmount: row.taxInclusivePrice ?? row.totalAmount,
+ content: row.invoiceContent ?? row.content,
+ status: normalizeStatus(row.status),
+});
+
+const fillFormFromRow = (row) => {
+ Object.assign(form, {
+ invoiceNo: row.invoiceNo ?? row.invoiceNumber ?? "",
+ customerId: row.customerId,
+ invoiceDate: row.invoiceDate ?? row.issueDate ?? "",
+ invoiceType: row.invoiceType ?? "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: row.taxRate ?? 13,
+ amount: row.amount ?? row.taxExclusivelPrice ?? 0,
+ taxAmount: row.taxAmount ?? row.taxPrice ?? 0,
+ totalAmount: row.totalAmount ?? row.taxInclusivePrice ?? 0,
+ content: row.content ?? row.invoiceContent ?? "",
+ remark: row.remark ?? "",
+ accountInvoiceApplicationId: row.accountInvoiceApplicationId,
+ storageAttachmentId: row.storageAttachmentId,
+ status: normalizeStatus(row.status),
+ });
+};
+
+const buildCancelPayload = (row) => ({
+ id: row.id,
+ accountInvoiceApplicationId: row.accountInvoiceApplicationId,
+ invoiceNumber: row.invoiceNumber ?? row.invoiceNo,
+ taxRate: row.taxRate,
+ invoiceType: row.invoiceType,
+ issueDate: row.issueDate ?? row.invoiceDate,
+ taxExclusivelPrice: row.taxExclusivelPrice ?? row.amount,
+ taxPrice: row.taxPrice ?? row.taxAmount,
+ taxInclusivePrice: row.taxInclusivePrice ?? row.totalAmount,
+ remark: row.remark ?? "",
+ invoiceContent: row.invoiceContent ?? row.content,
+ customerId: row.customerId,
+ storageAttachmentId: row.storageAttachmentId,
+ status: 1,
+});
+
+const buildSubmitPayload = () => ({
+ invoiceNumber: form.invoiceNo,
+ customerId: form.customerId,
+ issueDate: form.invoiceDate,
+ invoiceType: form.invoiceType,
+ taxRate: form.taxRate,
+ taxExclusivelPrice: form.amount,
+ taxPrice: form.taxAmount,
+ taxInclusivePrice: form.totalAmount,
+ invoiceContent: form.content,
+ remark: form.remark || "",
+ accountInvoiceApplicationId: form.accountInvoiceApplicationId,
+ storageAttachmentId: form.storageAttachmentId,
+});
+
+const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+};
+
+const appendFilterParams = (params) => {
+ if (filters.invoiceNumber) {
+ params.invoiceNumber = filters.invoiceNumber;
+ }
+ if (filters.customerId) {
+ params.customerId = filters.customerId;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ if (filters.status !== "" && filters.status != null) {
+ params.status = filters.status;
+ }
+ return params;
+};
+
+const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => appendFilterParams({});
+
+const handleExport = () => {
+ const params = buildExportParams();
+ proxy.download("/accountSalesInvoice/exportAccountSalesInvoice", params, `閿�椤瑰彂绁╛${Date.now()}.xlsx`);
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSalesInvoice(buildListParams())
+ .then((res) => {
+ if (res.code === 200) {
+ const records = res.data?.records ?? [];
+ dataList.value = records.map(normalizeTableRow);
+ pagination.total = res.data?.total ?? 0;
+ } else {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const resetFilters = () => {
+ filters.invoiceNumber = "";
+ filters.customerId = "";
+ filters.dateRange = [];
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const closeDialog = () => {
+ dialogVisible.value = false;
+ isView.value = false;
+};
+
+const add = () => {
+ isView.value = false;
+ dialogTitle.value = "褰曞叆鍙戠エ";
+ Object.assign(form, {
+ invoiceNo: "",
+ customerId: "",
+ invoiceDate: new Date().toISOString().split("T")[0],
+ invoiceType: "澧炲�肩◣涓撶敤鍙戠エ",
+ taxRate: 13,
+ amount: 0,
+ taxAmount: 0,
+ totalAmount: 0,
+ content: "",
+ remark: "",
+ accountInvoiceApplicationId: undefined,
+ storageAttachmentId: undefined,
+ });
+ dialogVisible.value = true;
+};
+
+const view = (row) => {
+ isView.value = true;
+ dialogTitle.value = "鏌ョ湅鍙戠エ";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+};
+
+const handleCancel = (row) => {
+ ElMessageBox.confirm(`纭浣滃簾鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛焋, "浣滃簾纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ cancelAccountSalesInvoice(buildCancelPayload(row))
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("浣滃簾鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "浣滃簾澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("浣滃簾澶辫触");
+ });
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎鍙戠エ銆�${row.invoiceNo ?? row.invoiceNumber}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙, "鍒犻櫎纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountSalesInvoice([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const submitForm = () => {
+ formRef.value.validate((valid) => {
+ if (!valid) return;
+ submitLoading.value = true;
+ addAccountSalesInvoice(buildSubmitPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("褰曞叆鎴愬姛");
+ closeDialog();
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "褰曞叆澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("褰曞叆澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+};
+
+onMounted(() => {
+ getCustomerList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/receipt.vue b/src/views/financialManagement/receivable/receipt.vue
new file mode 100644
index 0000000..ae7a763
--- /dev/null
+++ b/src/views/financialManagement/receivable/receipt.vue
@@ -0,0 +1,877 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="鏀舵鍗曞彿:">
+ <el-input v-model="filters.collectionNumber"
+ placeholder="璇疯緭鍏ユ敹娆惧崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ clearable
+ filterable
+ style="width: 200px;">
+ <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-select v-model="filters.collectionMethod"
+ placeholder="璇烽�夋嫨鏀舵鏂瑰紡"
+ clearable
+ style="width: 150px;">
+ <el-option v-for="item in payment_methods"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀舵鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ type="daterange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ style="width: 240px;" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="鏈〉鏀舵鍚堣"
+ :value="totalReceiptAmount"
+ :precision="2"
+ prefix="楼" />
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="add"
+ icon="Plus">鏂板鏀舵</el-button>
+ <el-button type="success"
+ @click="handleExport"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ v-loading="tableLoading"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage">
+ <template #amount="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ <template #receiptMethod="{ row }">
+ <span>{{ getReceiptMethodLabel(row.receiptMethod) }}</span>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary"
+ link
+ @click="view(row)">鏌ョ湅</el-button>
+ <el-button :disabled="row.accountStatemen"
+ type="primary"
+ link
+ @click="edit(row)">缂栬緫</el-button>
+ <el-button :disabled="row.accountStatemen"
+ type="danger"
+ link
+ @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+ <FormDialog :title="dialogTitle"
+ v-model="dialogVisible"
+ width="800px"
+ :operation-type="isView ? 'detail' : ''"
+ @confirm="submitForm"
+ @cancel="closeDialog">
+ <el-form :model="form"
+ :rules="rules"
+ ref="formRef"
+ label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鏀舵鍗曞彿"
+ prop="receiptCode">
+ <el-input v-model="form.receiptCode"
+ placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛"
+ prop="customerId">
+ <el-select v-model="form.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ style="width: 100%;"
+ :disabled="isEdit || isView"
+ filterable
+ @change="handleCustomerChange">
+ <el-option v-for="item in customerList"
+ :key="item.id"
+ :label="item.customerName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏宠仈鍗曟嵁"
+ prop="stockOutRecordIds">
+ <el-input :model-value="outboundBatchDisplayText"
+ placeholder="璇峰厛閫夋嫨瀹㈡埛"
+ readonly
+ :disabled="!form.customerId || isEdit || isView"
+ class="outbound-batch-input"
+ @click="handleOutboundInputClick">
+ <template v-if="!isEdit && !isView"
+ #append>
+ <el-button :disabled="!form.customerId"
+ :loading="outboundBatchLoading"
+ @click.stop="openOutboundSelectDialog">
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏀舵鏃ユ湡"
+ prop="receiptDate">
+ <el-date-picker v-model="form.receiptDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏀舵閲戦"
+ prop="amount">
+ <el-input-number v-model="form.amount"
+ :min="0"
+ :precision="2"
+ style="width: 100%;"
+ :disabled="isView"
+ placeholder="鏍规嵁鍏宠仈鍗曟嵁鑷姩姹囨�伙紝鍙慨鏀�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏀舵鏂瑰紡"
+ prop="receiptMethod">
+ <el-select v-model="form.receiptMethod"
+ placeholder="璇烽�夋嫨鏀舵鏂瑰紡"
+ style="width: 100%;"
+ :disabled="isView">
+ <el-option v-for="item in payment_methods"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿"
+ prop="createTime">
+ <el-date-picker v-model="formCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%;"
+ :disabled="isView" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�"
+ :disabled="isView" />
+ </el-form-item>
+ </el-form>
+ <template v-if="!isView"
+ #footer>
+ <el-button type="primary"
+ :loading="submitLoading"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ <el-dialog v-model="outboundSelectVisible"
+ title="閫夋嫨鍏宠仈鍗曟嵁"
+ width="1200px"
+ append-to-body
+ destroy-on-close
+ :close-on-click-modal="false"
+ @closed="handleOutboundDialogClosed">
+ <el-table ref="outboundTableRef"
+ v-loading="outboundBatchLoading"
+ :data="outboundBatchList"
+ row-key="id"
+ border
+ stripe
+ max-height="480"
+ @selection-change="handleOutboundDialogSelectionChange">
+ <el-table-column type="selection"
+ width="55"
+ align="center" />
+ <el-table-column prop="outboundBatches"
+ label="鍑哄簱鍗曞彿"
+ min-width="140"
+ show-overflow-tooltip />
+ <el-table-column prop="customerName"
+ label="瀹㈡埛鍚嶇О"
+ min-width="120"
+ show-overflow-tooltip />
+ <el-table-column prop="productName"
+ label="浜у搧鍚嶇О"
+ min-width="120"
+ show-overflow-tooltip />
+ <el-table-column prop="specificationModel"
+ label="瑙勬牸鍨嬪彿"
+ min-width="140"
+ show-overflow-tooltip />
+ <el-table-column prop="salesContractNo"
+ label="閿�鍞悎鍚屽彿"
+ min-width="140"
+ show-overflow-tooltip />
+ <el-table-column prop="shippingNo"
+ label="鍙戣揣鍗曞彿"
+ min-width="130"
+ show-overflow-tooltip />
+ <el-table-column prop="shippingDate"
+ label="鍙戣揣鏃ユ湡"
+ width="110"
+ align="center" />
+ <el-table-column prop="outboundAmount"
+ label="鍑哄簱閲戦"
+ width="110"
+ align="right">
+ <template #default="{ row }">楼{{ formatMoney(row.outboundAmount) }}</template>
+ </el-table-column>
+ <el-table-column prop="taxRate"
+ label="绋庣巼"
+ width="80"
+ align="center">
+ <template #default="{ row }">{{ row.taxRate }}%</template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <el-button type="primary"
+ @click="confirmOutboundSelection">纭畾</el-button>
+ <el-button @click="outboundSelectVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import {
+ ref,
+ reactive,
+ computed,
+ onMounted,
+ nextTick,
+ getCurrentInstance,
+ } from "vue";
+ import dayjs from "dayjs";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import { listCustomer } from "@/api/basicData/customer.js";
+ import {
+ getOutboundBatchesByCustomer,
+ addAccountSalesCollection,
+ listPageAccountSalesCollection,
+ updateAccountSalesCollection,
+ deleteAccountSalesCollection,
+ } from "@/api/financialManagement/accountSalesCollection.js";
+
+ defineOptions({
+ name: "鏀舵鍗�",
+ });
+
+ const { proxy } = getCurrentInstance();
+ const { payment_methods } = proxy.useDict("payment_methods");
+
+ const filters = reactive({
+ collectionNumber: "",
+ customerId: "",
+ collectionMethod: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "鏀舵鍗曞彿", prop: "receiptCode", width: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { label: "鏀舵鏃ユ湡", prop: "receiptDate", width: "120" },
+ { label: "鏀舵閲戦", prop: "amount", dataType: "slot", slot: "amount" },
+ {
+ label: "鏀舵鏂瑰紡",
+ prop: "receiptMethod",
+ dataType: "slot",
+ slot: "receiptMethod",
+ width: "120",
+ },
+ { label: "澶囨敞", prop: "remark", showOverflowTooltip: true },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "slot",
+ slot: "operation",
+ width: "200",
+ fixed: "right",
+ },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+ const dialogVisible = ref(false);
+ const dialogTitle = ref("");
+ const formRef = ref(null);
+ const isEdit = ref(false);
+ const isView = ref(false);
+ const currentId = ref(null);
+ const submitLoading = ref(false);
+
+ const customerList = ref([]);
+ const outboundBatchList = ref([]);
+ const outboundBatchOptions = ref([]);
+ const outboundBatchLoading = ref(false);
+ const outboundSelectVisible = ref(false);
+ const outboundTableRef = ref(null);
+ const dialogOutboundSelection = ref([]);
+
+ const getReceiptMethodLabel = value => {
+ if (value === undefined || value === null || value === "") return "-";
+ const item = payment_methods.value?.find(
+ m => String(m.value) === String(value)
+ );
+ return item?.label ?? value;
+ };
+
+ const getDefaultReceiptMethod = () => payment_methods.value?.[0]?.value ?? "";
+
+ const form = reactive({
+ receiptCode: "",
+ customerId: "",
+ receiptDate: "",
+ amount: 0,
+ receiptMethod: "",
+ stockOutRecordIds: [],
+ outboundBatches: "",
+ remark: "",
+ createTime: "",
+ });
+ const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
+
+ const rules = {
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ stockOutRecordIds: [
+ {
+ required: true,
+ type: "array",
+ min: 1,
+ message: "璇烽�夋嫨鍏宠仈鍗曟嵁",
+ trigger: "change",
+ },
+ ],
+ receiptDate: [
+ { required: true, message: "璇烽�夋嫨鏀舵鏃ユ湡", trigger: "change" },
+ ],
+ amount: [{ required: true, message: "璇疯緭鍏ユ敹娆鹃噾棰�", trigger: "blur" }],
+ receiptMethod: [
+ { required: true, message: "璇烽�夋嫨鏀舵鏂瑰紡", trigger: "change" },
+ ],
+ };
+
+ const totalReceiptAmount = computed(() =>
+ dataList.value.reduce((sum, item) => sum + Number(item.amount || 0), 0)
+ );
+
+ const formatMoney = value => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value)
+ .toFixed(2)
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ };
+
+ const parseStockOutRecordIds = value => {
+ if (!value) return [];
+ if (Array.isArray(value)) return value;
+ return String(value)
+ .split(/[,锛宂/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ .map(s => (/^\d+$/.test(s) ? Number(s) : s));
+ };
+
+ const formatOutboundBatches = value => {
+ if (value === undefined || value === null || value === "") return "";
+ if (Array.isArray(value)) return value.filter(Boolean).join("銆�");
+ return String(value)
+ .split(/[,锛宂/)
+ .map(s => s.trim())
+ .filter(Boolean)
+ .join("銆�");
+ };
+
+ const normalizeTableRow = row => ({
+ ...row,
+ receiptCode: row.collectionNumber ?? row.receiptCode,
+ receiptDate: row.collectionDate ?? row.receiptDate,
+ amount: row.collectionAmount ?? row.amount,
+ receiptMethod: row.collectionMethod ?? row.receiptMethod ?? "",
+ stockOutRecordIds: row.stockOutRecordIds ?? row.stockOutRecordId ?? "",
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+ });
+
+ const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then(res => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+ };
+
+ const normalizeOutboundBatchOptions = data => {
+ const list = Array.isArray(data) ? data : [];
+ return list.map((item, index) => {
+ if (typeof item === "string" || typeof item === "number") {
+ const text = String(item);
+ return { label: text, value: text, outboundAmount: 0 };
+ }
+ const label =
+ item.outboundBatches ??
+ item.batchNo ??
+ item.shippingNo ??
+ item.outboundNo ??
+ item.label ??
+ `鍑哄簱鍗�${index + 1}`;
+ const value = item.id ?? item.stockOutRecordId ?? label;
+ return {
+ label: String(label),
+ value,
+ outboundAmount: Number(item.outboundAmount) || 0,
+ };
+ });
+ };
+
+ const isSameOutboundId = (a, b) => String(a) === String(b);
+
+ const getOutboundRowId = row => row?.id ?? row?.stockOutRecordId;
+
+ const outboundBatchDisplayText = computed(() => {
+ if (isEdit.value || isView.value) {
+ return form.outboundBatches || "";
+ }
+ if (form.outboundBatches) return form.outboundBatches;
+ const ids = form.stockOutRecordIds || [];
+ if (!ids.length) return "";
+ const labels = outboundBatchOptions.value
+ .filter(opt => ids.some(id => isSameOutboundId(id, opt.value)))
+ .map(opt => opt.label);
+ if (labels.length) return labels.join("銆�");
+ return ids.join("銆�");
+ });
+
+ const handleOutboundInputClick = () => {
+ if (isEdit.value || isView.value) return;
+ openOutboundSelectDialog();
+ };
+
+ /** 涓哄凡閫� ID 琛ュ叏閫夐」锛堢紪杈�/鏌ョ湅鍥炴樉锛� */
+ const ensureOutboundOptionsForSelected = () => {
+ const ids = form.stockOutRecordIds || [];
+ ids.forEach(id => {
+ const exists = outboundBatchOptions.value.some(opt =>
+ isSameOutboundId(opt.value, id)
+ );
+ if (exists) return;
+ const fromList = outboundBatchList.value.find(row =>
+ isSameOutboundId(getOutboundRowId(row), id)
+ );
+ if (fromList) {
+ const [option] = normalizeOutboundBatchOptions([fromList]);
+ if (option) outboundBatchOptions.value.push(option);
+ return;
+ }
+ outboundBatchOptions.value.push({
+ label: String(id),
+ value: id,
+ outboundAmount: 0,
+ });
+ });
+ };
+
+ const syncCollectionAmount = () => {
+ const selected = form.stockOutRecordIds || [];
+ const sum = outboundBatchOptions.value
+ .filter(opt => selected.some(id => isSameOutboundId(id, opt.value)))
+ .reduce((acc, opt) => acc + (Number(opt.outboundAmount) || 0), 0);
+ form.amount = sum > 0 ? Number(sum.toFixed(2)) : 0;
+ };
+
+ const restoreOutboundTableSelection = () => {
+ nextTick(() => {
+ const table = outboundTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ const selectedIds = new Set(
+ (form.stockOutRecordIds || []).map(id => String(id))
+ );
+ outboundBatchList.value.forEach(row => {
+ const rowId = getOutboundRowId(row);
+ if (
+ rowId !== undefined &&
+ rowId !== null &&
+ selectedIds.has(String(rowId))
+ ) {
+ table.toggleRowSelection(row, true);
+ }
+ });
+ });
+ };
+
+ const loadOutboundBatches = (customerId, keepSelected = false) => {
+ if (!customerId) {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ if (!keepSelected) {
+ form.stockOutRecordIds = [];
+ form.amount = 0;
+ }
+ return Promise.resolve();
+ }
+ outboundBatchLoading.value = true;
+ return getOutboundBatchesByCustomer({ customerId })
+ .then(res => {
+ if (res.code === 200) {
+ const list = res.data?.records ?? res.data ?? [];
+ outboundBatchList.value = Array.isArray(list) ? list : [];
+ outboundBatchOptions.value = normalizeOutboundBatchOptions(list);
+ } else {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ }
+ })
+ .catch(() => {
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ })
+ .finally(() => {
+ outboundBatchLoading.value = false;
+ if (keepSelected) {
+ ensureOutboundOptionsForSelected();
+ restoreOutboundTableSelection();
+ }
+ });
+ };
+
+ const handleCustomerChange = customerId => {
+ form.stockOutRecordIds = [];
+ form.outboundBatches = "";
+ form.amount = 0;
+ loadOutboundBatches(customerId);
+ };
+
+ const openOutboundSelectDialog = () => {
+ if (!form.customerId || isEdit.value || isView.value) return;
+ outboundSelectVisible.value = true;
+ loadOutboundBatches(form.customerId, true).then(() => {
+ restoreOutboundTableSelection();
+ });
+ };
+
+ const handleOutboundDialogSelectionChange = selection => {
+ dialogOutboundSelection.value = selection;
+ };
+
+ const confirmOutboundSelection = () => {
+ if (dialogOutboundSelection.value.length === 0) {
+ ElMessage.warning("璇疯嚦灏戦�夋嫨涓�鏉″叧鑱斿崟鎹�");
+ return;
+ }
+ form.stockOutRecordIds = dialogOutboundSelection.value
+ .map(row => getOutboundRowId(row))
+ .filter(id => id !== undefined && id !== null);
+ form.outboundBatches = dialogOutboundSelection.value
+ .map(row => row.outboundBatches ?? row.batchNo ?? row.shippingNo ?? "")
+ .filter(Boolean)
+ .join("銆�");
+ outboundSelectVisible.value = false;
+ syncCollectionAmount();
+ formRef.value?.validateField("stockOutRecordIds");
+ };
+
+ const handleOutboundDialogClosed = () => {
+ dialogOutboundSelection.value = [];
+ };
+
+ const appendFilterParams = params => {
+ if (filters.collectionNumber) {
+ params.collectionNumber = filters.collectionNumber;
+ }
+ if (filters.customerId) {
+ params.customerId = filters.customerId;
+ }
+ if (filters.collectionMethod) {
+ params.collectionMethod = filters.collectionMethod;
+ }
+ if (filters.dateRange?.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ };
+
+ const buildListParams = () =>
+ appendFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+ const buildExportParams = () => appendFilterParams({});
+
+ const buildSubmitPayload = (forUpdate = false) => {
+ const payload = {
+ customerId: form.customerId,
+ collectionDate: form.receiptDate,
+ collectionAmount: form.amount,
+ collectionMethod: form.receiptMethod,
+ collectionNumber: form.receiptCode || "",
+ remark: form.remark || "",
+ stockOutRecordIds: (form.stockOutRecordIds || []).join(","),
+ createTime: form.createTime,
+ };
+ if (forUpdate) {
+ payload.id = currentId.value;
+ }
+ return payload;
+ };
+
+ const fillFormFromRow = row => {
+ const stockOutRecordIds = parseStockOutRecordIds(
+ row.stockOutRecordIds ?? row.stockOutRecordId
+ );
+ Object.assign(form, {
+ receiptCode: row.receiptCode ?? row.collectionNumber ?? "",
+ customerId: row.customerId,
+ receiptDate: row.receiptDate ?? row.collectionDate ?? "",
+ amount: Number(row.amount ?? row.collectionAmount ?? 0),
+ receiptMethod: row.receiptMethod ?? row.collectionMethod ?? "",
+ stockOutRecordIds,
+ outboundBatches: formatOutboundBatches(row.outboundBatches),
+ remark: row.remark ?? "",
+ createTime: row.createTime ?? "",
+ });
+ };
+
+ const closeDialog = () => {
+ dialogVisible.value = false;
+ outboundSelectVisible.value = false;
+ isView.value = false;
+ isEdit.value = false;
+ };
+
+ const handleExport = () => {
+ const params = buildExportParams();
+ proxy.download(
+ "/accountSalesCollection/exportAccountSalesCollection",
+ params,
+ `鏀舵鍗昣${Date.now()}.xlsx`
+ );
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSalesCollection(buildListParams())
+ .then(res => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = (res.data.records ?? []).map(normalizeTableRow);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const resetFilters = () => {
+ filters.collectionNumber = "";
+ filters.customerId = "";
+ filters.collectionMethod = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+ };
+
+ const add = () => {
+ isEdit.value = false;
+ isView.value = false;
+ dialogTitle.value = "鏂板鏀舵";
+ Object.assign(form, {
+ receiptCode: "",
+ customerId: "",
+ receiptDate: new Date().toISOString().split("T")[0],
+ amount: 0,
+ receiptMethod: getDefaultReceiptMethod(),
+ stockOutRecordIds: [],
+ outboundBatches: "",
+ remark: "",
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ });
+ outboundBatchList.value = [];
+ outboundBatchOptions.value = [];
+ dialogVisible.value = true;
+ };
+
+ const edit = row => {
+ isEdit.value = true;
+ isView.value = false;
+ currentId.value = row.id;
+ dialogTitle.value = "缂栬緫鏀舵";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+ };
+
+ const view = row => {
+ isView.value = true;
+ isEdit.value = false;
+ dialogTitle.value = "鏌ョ湅鏀舵";
+ fillFormFromRow(row);
+ dialogVisible.value = true;
+ };
+
+ const handleDelete = row => {
+ ElMessageBox.confirm(
+ `纭鍒犻櫎鏀舵鍗曘��${row.receiptCode ?? row.collectionNumber}銆嶅悧锛焋,
+ "鎻愮ず",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ ).then(() => {
+ deleteAccountSalesCollection([row.id])
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+ };
+
+ const submitForm = () => {
+ formRef.value.validate(valid => {
+ if (!valid) return;
+ submitLoading.value = true;
+ const request = isEdit.value
+ ? updateAccountSalesCollection(buildSubmitPayload(true))
+ : addAccountSalesCollection(buildSubmitPayload());
+ request
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success(isEdit.value ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+ closeDialog();
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || (isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触"));
+ }
+ })
+ .catch(() => {
+ ElMessage.error(isEdit.value ? "淇敼澶辫触" : "鏂板澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ });
+ };
+
+ onMounted(() => {
+ getCustomerList();
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+
+ .text-success {
+ color: #67c23a;
+ font-weight: bold;
+ }
+
+ .outbound-batch-input:not(.is-disabled) {
+ :deep(.el-input__wrapper) {
+ cursor: pointer;
+ }
+ }
+</style>
diff --git a/src/views/financialManagement/receivable/reconciliation.vue b/src/views/financialManagement/receivable/reconciliation.vue
new file mode 100644
index 0000000..b1bff0e
--- /dev/null
+++ b/src/views/financialManagement/receivable/reconciliation.vue
@@ -0,0 +1,738 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="瀹㈡埛:">
+ <el-select v-model="filters.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable filterable style="width: 200px;">
+ <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>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-button type="primary" @click="generateStatement" icon="Document">鐢熸垚瀵硅处鍗�</el-button>
+ </div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭瀵硅处鍗�</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #openingBalance="{ row }">
+ <span :class="row.openingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.openingBalance) }}</span>
+ </template>
+ <template #currentPlan="{ row }">
+ <span class="text-primary">楼{{ formatMoney(row.currentPlan) }}</span>
+ </template>
+ <template #currentActually="{ row }">
+ <span class="text-success">楼{{ formatMoney(row.currentActually) }}</span>
+ </template>
+ <template #closingBalance="{ row }">
+ <span :class="row.closingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.closingBalance) }}</span>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="viewDetail(row)">鏌ョ湅鏄庣粏</el-button>
+ <!-- <el-button type="primary" link @click="printStatement(row)">鎵撳嵃</el-button> -->
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog title="瀵硅处鏄庣粏" v-model="detailDialogVisible" width="900px" @confirm="printDetail" @cancel="detailDialogVisible = false" operationType="detail">
+ <div class="statement-header">
+ <h3>{{ currentCustomer }} 搴旀敹瀵硅处鍗�</h3>
+ <p>瀵硅处鏈熼棿: {{ currentPeriod }}</p>
+ </div>
+ <el-table :data="detailData" border style="width: 100%" v-loading="detailLoading">
+ <el-table-column prop="date" label="鏃ユ湡" width="120" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="row.type === '鍑哄簱' ? 'success' : row.type === '閫�璐�' ? 'danger' : 'primary'">{{ row.type }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="code" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="debit" label="鍊熸柟(搴旀敹)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟(鏀舵)" width="120">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="balance" label="浣欓" width="120">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(row.balance) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" show-overflow-tooltip />
+ </el-table>
+ <template #footer>
+ <el-button type="primary" @click="printDetail">鎵撳嵃</el-button>
+ <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
+ </template>
+ </FormDialog>
+
+ <FormDialog title="鐢熸垚瀵硅处鍗�" v-model="generateDialogVisible" width="1000px" @confirm="confirmGenerate" @cancel="generateDialogVisible = false">
+ <el-form :model="generateForm" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閫夋嫨瀹㈡埛" prop="customerId">
+ <el-select
+ v-model="generateForm.customerId"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ style="width: 100%;"
+ filterable
+ @change="onCustomerChange"
+ >
+ <el-option v-for="item in customerList" :key="item.id" :label="item.customerName" :value="item.id" />
+ </el-select>
+ </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>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <div v-if="statementDetailLoaded" class="sales-section">
+ <div v-if="salesData.length > 0" class="section-title">鏈湀閿�鍞暟鎹�</div>
+ <el-table
+ v-if="salesData.length > 0"
+ ref="salesTableRef"
+ :data="salesData"
+ border
+ row-key="id"
+ style="width: 100%; margin-bottom: 15px;"
+ v-loading="salesLoading"
+ @selection-change="handleSalesSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column prop="occurrenceDate" label="鏃ユ湡" width="120" />
+ <el-table-column prop="receiptNumber" label="鍗曟嵁缂栧彿" width="150" />
+ <el-table-column prop="type" label="绫诲瀷" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getDetailTypeTagType(row.type)">{{ row.typeLabel }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="amount" label="閲戦" width="120">
+ <template #default="{ row }">
+ <span :class="getDetailAmountClass(row.type)">楼{{ formatMoney(row.amount) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" />
+ </el-table>
+ <el-empty v-else description="璇ュ鎴锋湰鏈堟殏鏃犳槑缁嗘暟鎹�" :image-size="80" />
+
+ <div class="summary-row">
+ <span>鏈熷垵浣欓: <strong class="text-primary">楼{{ formatMoney(generateForm.openingBalance) }}</strong></span>
+ <span>鏈湡搴旀敹: <strong class="text-primary">楼{{ formatMoney(generateForm.currentPlan) }}</strong></span>
+ <span>鏈湡鏀舵: <strong class="text-success">楼{{ formatMoney(generateForm.currentActually) }}</strong></span>
+ <span>鏈熸湯浣欓: <strong :class="displayClosingBalance >= 0 ? 'text-success' : 'text-danger'">楼{{ formatMoney(displayClosingBalance) }}</strong></span>
+ </div>
+ </div>
+
+ <div v-else-if="generateForm.customerId && generateForm.statementMonth && !salesLoading" class="empty-tip">
+ <el-empty description="璇ュ鎴锋湰鏈堟殏鏃犻攢鍞暟鎹�" />
+ </div>
+
+ <template #footer>
+ <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate" :loading="submitLoading">纭鐢熸垚</el-button>
+ <el-button @click="generateDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick, getCurrentInstance } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listCustomer } from "@/api/basicData/customer.js";
+import {
+ getAccountStatementDetailsByMonth,
+ addAccountStatement,
+ listPageAccountStatement,
+ deleteAccountStatement,
+} from "@/api/financialManagement/accountStatement.js";
+
+const ACCOUNT_TYPE_RECEIVABLE = 1;
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+ name: "搴旀敹瀵硅处",
+});
+
+const filters = reactive({
+ customerId: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "瀵硅处鍗曞彿", prop: "statementNumber", width: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", width: "180" },
+ { 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" },
+ { label: "鏈熸湯浣欓", prop: "closingBalance", dataType: "slot", slot: "closingBalance" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "200", fixed: "right" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+const submitLoading = ref(false);
+const detailDialogVisible = ref(false);
+const currentCustomer = ref("");
+const currentPeriod = ref("");
+const detailData = ref([]);
+const detailLoading = ref(false);
+
+const generateDialogVisible = ref(false);
+const salesLoading = ref(false);
+const statementDetailLoaded = ref(false);
+const salesData = ref([]);
+const selectedSales = ref([]);
+const salesTableRef = ref(null);
+const customerList = ref([]);
+
+/** 鏄庣粏 type锛�1鍑哄簱 2鍏ュ簱 3鏀舵 4浠樻 5閫�璐� */
+const STATEMENT_DETAIL_TYPE_MAP = {
+ 1: "鍑哄簱",
+ 2: "鍏ュ簱",
+ 3: "鏀舵",
+ 4: "浠樻",
+ 5: "閫�璐�",
+};
+
+const calculateEndBalance = (openingBalance, currentPlan, currentActually) => {
+ return openingBalance + currentPlan - currentActually;
+};
+
+const getDetailTypeLabel = (type) => STATEMENT_DETAIL_TYPE_MAP[Number(type)] ?? "";
+
+const getDetailTypeTagType = (type) => {
+ const t = Number(type);
+ if (t === 1) return "success";
+ if (t === 3) return "primary";
+ if (t === 5) return "danger";
+ return "info";
+};
+
+const getDetailAmountClass = (type) => {
+ const t = Number(type);
+ if (t === 1) return "text-primary";
+ if (t === 3) return "text-success";
+ return "text-danger";
+};
+
+const generateForm = reactive({
+ customerId: "",
+ customerName: "",
+ statementMonth: "",
+ openingBalance: 0,
+ currentPlan: 0,
+ currentActually: 0,
+ closingBalance: 0,
+});
+
+const displayClosingBalance = computed(() => {
+ return calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ );
+});
+
+const canGenerate = computed(() => {
+ return generateForm.customerId && generateForm.statementMonth && selectedSales.value.length > 0;
+});
+
+const applyStatementSummary = (data) => {
+ generateForm.openingBalance = Number(data.openingBalance ?? 0);
+ generateForm.currentPlan = Number(data.currentPlan ?? 0);
+ generateForm.currentActually = Number(data.currentActually ?? 0);
+ generateForm.closingBalance = Number(
+ data.closingBalance ??
+ calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ )
+ );
+};
+
+const getCustomerList = () => {
+ listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+ if (res.code === 200) {
+ customerList.value = res.data?.records || [];
+ }
+ });
+};
+
+const normalizeSalesRows = (list) => {
+ const rows = Array.isArray(list) ? list : [];
+ return rows.map((item, index) => {
+ const type = Number(item.type);
+ return {
+ id: item.id ?? `detail-${index}`,
+ accountStatementId: item.accountStatementId,
+ occurrenceDate: item.occurrenceDate ?? "",
+ receiptNumber: item.receiptNumber ?? "",
+ type,
+ typeLabel: getDetailTypeLabel(type),
+ amount: Math.abs(Number(item.amount ?? 0)),
+ remark: item.remark ?? "",
+ };
+ });
+};
+
+const selectAllSalesRows = (keepApiSummary = false) => {
+ nextTick(() => {
+ const table = salesTableRef.value;
+ if (!table) return;
+ table.clearSelection();
+ salesData.value.forEach((row) => table.toggleRowSelection(row, true));
+ selectedSales.value = [...salesData.value];
+ if (!keepApiSummary) {
+ calculateSummary();
+ }
+ });
+};
+
+const isNumericId = (id) => id !== undefined && id !== null && id !== "" && /^\d+$/.test(String(id));
+
+const buildFilterParams = (params = {}) => {
+ const result = { ...params, accountType: ACCOUNT_TYPE_RECEIVABLE };
+ 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;
+ }
+ return result;
+};
+
+const buildListParams = () =>
+ buildFilterParams({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ });
+
+const buildExportParams = () => buildFilterParams({});
+
+const buildDetailSubmitItem = (row) => {
+ const item = {
+ occurrenceDate: row.occurrenceDate,
+ receiptNumber: row.receiptNumber,
+ type: row.type,
+ amount: row.amount,
+ remark: row.remark ?? "",
+ };
+ if (isNumericId(row.id)) {
+ item.id = Number(row.id);
+ }
+ if (row.accountStatementId) {
+ item.accountStatementId = row.accountStatementId;
+ }
+ return item;
+};
+
+const buildAddPayload = () => ({
+ customerId: generateForm.customerId,
+ customerName: generateForm.customerName,
+ statementMonth: generateForm.statementMonth,
+ accountType: ACCOUNT_TYPE_RECEIVABLE,
+ statementNumber: "",
+ openingBalance: generateForm.openingBalance,
+ currentPlan: generateForm.currentPlan,
+ currentActually: generateForm.currentActually,
+ closingBalance: generateForm.closingBalance,
+ accountStatementDetails: selectedSales.value.map(buildDetailSubmitItem),
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountStatement(buildListParams())
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ pagination.total = 0;
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ pagination.total = 0;
+ ElMessage.error("鏌ヨ澶辫触");
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.customerId = "";
+ filters.startMonth = "";
+ filters.endMonth = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const generateStatement = () => {
+ generateForm.customerId = "";
+ generateForm.customerName = "";
+ generateForm.statementMonth = "";
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ statementDetailLoaded.value = false;
+ salesData.value = [];
+ selectedSales.value = [];
+ generateDialogVisible.value = true;
+};
+
+const onCustomerChange = (customerId) => {
+ const customer = customerList.value.find((item) => item.id === customerId);
+ generateForm.customerName = customer?.customerName ?? "";
+ loadSalesData();
+};
+
+const onStatementMonthChange = () => {
+ loadSalesData();
+};
+
+const loadSalesData = () => {
+ if (!generateForm.customerId || !generateForm.statementMonth) {
+ salesData.value = [];
+ selectedSales.value = [];
+ statementDetailLoaded.value = false;
+ generateForm.openingBalance = 0;
+ generateForm.currentPlan = 0;
+ generateForm.currentActually = 0;
+ generateForm.closingBalance = 0;
+ return;
+ }
+
+ salesLoading.value = true;
+ selectedSales.value = [];
+ statementDetailLoaded.value = false;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_RECEIVABLE,
+ customerId: generateForm.customerId,
+ statementMonth: generateForm.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ const data = res.data ?? {};
+ const details = data.accountStatementDetails;
+ const list = Array.isArray(details) ? details : [];
+ salesData.value = normalizeSalesRows(list);
+ applyStatementSummary(data);
+ statementDetailLoaded.value = true;
+
+ if (salesData.value.length > 0) {
+ selectAllSalesRows(true);
+ }
+ } else {
+ salesData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error(res.msg || "鏌ヨ瀵硅处鏄庣粏澶辫触");
+ }
+ })
+ .catch(() => {
+ salesData.value = [];
+ statementDetailLoaded.value = false;
+ ElMessage.error("鏌ヨ瀵硅处鏄庣粏澶辫触");
+ })
+ .finally(() => {
+ salesLoading.value = false;
+ });
+};
+
+const calculateSummary = () => {
+ let receivable = 0;
+ let receipt = 0;
+
+ selectedSales.value.forEach((item) => {
+ if (item.type === 1) {
+ receivable += item.amount;
+ } else if (item.type === 5) {
+ receivable -= item.amount;
+ } else if (item.type === 3) {
+ receipt += item.amount;
+ }
+ });
+
+ generateForm.currentPlan = receivable;
+ generateForm.currentActually = receipt;
+ generateForm.closingBalance = calculateEndBalance(
+ generateForm.openingBalance,
+ generateForm.currentPlan,
+ generateForm.currentActually
+ );
+};
+
+const handleSalesSelectionChange = (selection) => {
+ selectedSales.value = selection;
+ calculateSummary();
+};
+
+const confirmGenerate = () => {
+ if (!canGenerate.value) return;
+ submitLoading.value = true;
+ addAccountStatement(buildAddPayload())
+ .then((res) => {
+ if (res.code === 200) {
+ generateDialogVisible.value = false;
+ ElMessage.success("瀵硅处鍗曠敓鎴愭垚鍔�");
+ pagination.currentPage = 1;
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鐢熸垚澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鐢熸垚澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+};
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎瀵硅处鍗曘��${row.statementNumber || row.id}銆嶅悧锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteAccountStatement([row.id])
+ .then((res) => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getTableData();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+};
+
+const buildDetailTableFromApi = (data, statementMonth) => {
+ const details = Array.isArray(data.accountStatementDetails) ? data.accountStatementDetails : [];
+ let runningBalance = Number(data.openingBalance ?? 0);
+ const rows = [
+ {
+ date: statementMonth ?? "",
+ type: "鏈熷垵",
+ code: "-",
+ debit: 0,
+ credit: 0,
+ balance: runningBalance,
+ remark: "鏈熷垵浣欓",
+ },
+ ];
+
+ details.forEach((item) => {
+ const amount = Math.abs(Number(item.amount ?? 0));
+ const type = Number(item.type);
+ let debit = 0;
+ let credit = 0;
+
+ if (type === 1) {
+ debit = amount;
+ runningBalance += amount;
+ } else if (type === 3 || type === 5) {
+ credit = amount;
+ runningBalance -= amount;
+ }
+
+ rows.push({
+ date: item.occurrenceDate ?? "",
+ type: getDetailTypeLabel(type),
+ code: item.receiptNumber ?? "",
+ debit,
+ credit,
+ balance: runningBalance,
+ remark: item.remark ?? "",
+ });
+ });
+
+ return rows;
+};
+
+const viewDetail = (row) => {
+ if (!row.customerId || !row.statementMonth) {
+ ElMessage.warning("缂哄皯瀹㈡埛鎴栧璐︽湀浠斤紝鏃犳硶鏌ヨ鏄庣粏");
+ return;
+ }
+
+ currentCustomer.value = row.customerName ?? "";
+ currentPeriod.value = row.statementMonth ?? "";
+ detailData.value = [];
+ detailDialogVisible.value = true;
+ detailLoading.value = true;
+
+ getAccountStatementDetailsByMonth({
+ accountType: ACCOUNT_TYPE_RECEIVABLE,
+ customerId: row.customerId,
+ statementMonth: row.statementMonth,
+ })
+ .then((res) => {
+ if (res.code === 200) {
+ detailData.value = buildDetailTableFromApi(res.data ?? {}, row.statementMonth);
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鏌ヨ鏄庣粏澶辫触");
+ detailDialogVisible.value = false;
+ })
+ .finally(() => {
+ detailLoading.value = false;
+ });
+};
+
+const printStatement = (row) => {
+ ElMessage.info(`鎵撳嵃瀵硅处鍗�: ${row.statementNumber}`);
+};
+
+const printDetail = () => {
+ ElMessage.info("鎵撳嵃鏄庣粏");
+};
+
+const handleOut = () => {
+ const params = buildExportParams();
+ proxy.download("/accountStatement/exportAccountStatement", params, `搴旀敹瀵硅处鍗昣${Date.now()}.xlsx`);
+};
+
+onMounted(() => {
+ getCustomerList();
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+
+.text-success {
+ color: #67c23a;
+}
+
+.text-danger {
+ color: #f56c6c;
+}
+
+.text-primary {
+ color: #409eff;
+}
+
+.statement-header {
+ text-align: center;
+ margin-bottom: 20px;
+ h3 {
+ margin: 0 0 10px 0;
+ }
+ p {
+ color: #909399;
+ margin: 0;
+ }
+}
+
+.sales-section {
+ margin-top: 20px;
+
+ .section-title {
+ font-size: 16px;
+ font-weight: bold;
+ margin-bottom: 15px;
+ padding-left: 10px;
+ border-left: 4px solid #409eff;
+ }
+}
+
+.summary-row {
+ display: flex;
+ justify-content: space-around;
+ padding: 15px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ margin-top: 15px;
+
+ span {
+ font-size: 14px;
+
+ strong {
+ font-size: 16px;
+ margin-left: 5px;
+ }
+ }
+}
+
+.empty-tip {
+ margin-top: 30px;
+}
+</style>
diff --git a/src/views/financialManagement/receivable/salesOut.vue b/src/views/financialManagement/receivable/salesOut.vue
new file mode 100644
index 0000000..0e24b37
--- /dev/null
+++ b/src/views/financialManagement/receivable/salesOut.vue
@@ -0,0 +1,180 @@
+<template>
+ <!-- 閿�鍞嚭搴� -->
+ <div class="app-container">
+ <el-form :model="filters"
+ :inline="true">
+ <el-form-item label="鍑哄簱鍗曞彿:">
+ <el-input v-model="filters.outboundBatches"
+ placeholder="璇疯緭鍏ュ嚭搴撳崟鍙�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О:">
+ <el-input v-model="filters.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="鍑哄簱鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut"
+ icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { ElMessage } from "element-plus";
+ import { listPageAccountSales } from "@/api/financialManagement/accountSales";
+
+ defineOptions({
+ name: "閿�鍞嚭搴�",
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ const filters = reactive({
+ outboundBatches: "",
+ customerName: "",
+ dateRange: [],
+ });
+
+ const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+ });
+
+ const columns = [
+ { label: "鍑哄簱鍗曞彿", prop: "outboundBatches", minWidth: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: "180" },
+ { label: "鍑哄簱鏃ユ湡", prop: "shippingDate", width: "170" },
+ { label: "浜у搧鍚嶇О", prop: "productName", minWidth: "140" },
+ { label: "瑙勬牸鍨嬪彿", prop: "specificationModel", minWidth: "140" },
+ {
+ label: "閲戦",
+ prop: "outboundAmount",
+ minWidth: "120",
+ align: "right",
+ formatData: val =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+ },
+ { label: "鍙戣揣缂栧彿", prop: "shippingNo", minWidth: "140" },
+ { label: "閿�鍞鍗曞彿", prop: "salesContractNo", minWidth: "150" },
+ ];
+
+ const dataList = ref([]);
+ const tableLoading = ref(false);
+
+ function buildFilterParams() {
+ const params = {
+ outboundBatches: filters.outboundBatches || undefined,
+ customerName: filters.customerName || undefined,
+ };
+ if (filters.dateRange && filters.dateRange.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+ }
+
+ const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSales({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then(res => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const resetFilters = () => {
+ filters.outboundBatches = "";
+ filters.customerName = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+ };
+
+ const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+ };
+
+ const handleOut = () => {
+ proxy.download(
+ "/accountSales/exportAccountSalesOutbound",
+ buildFilterParams(),
+ `閿�鍞嚭搴揰${new Date().getTime()}.xlsx`
+ );
+ };
+
+ onMounted(() => {
+ getTableData();
+ });
+</script>
+
+<style lang="scss" scoped>
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+ }
+</style>
diff --git a/src/views/financialManagement/receivable/salesReturn.vue b/src/views/financialManagement/receivable/salesReturn.vue
new file mode 100644
index 0000000..c58d330
--- /dev/null
+++ b/src/views/financialManagement/receivable/salesReturn.vue
@@ -0,0 +1,171 @@
+<template>
+ <!-- 閿�鍞��璐� -->
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="閫�璐у崟鍙�:">
+ <el-input v-model="filters.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О:">
+ <el-input v-model="filters.customerName" placeholder="璇疯緭鍏ュ鎴峰悕绉�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="閫�璐ф棩鏈�:">
+ <el-date-picker
+ v-model="filters.dateRange"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :tableLoading="tableLoading"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+import { ElMessage } from "element-plus";
+import { listPageAccountSalesReturn } from "@/api/financialManagement/accountSales";
+
+defineOptions({
+ name: "閿�鍞��璐�",
+});
+
+const { proxy } = getCurrentInstance();
+
+const filters = reactive({
+ returnNo: "",
+ customerName: "",
+ dateRange: [],
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: "150" },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: "180" },
+ { label: "鍏宠仈鍙戣揣鍗曞彿", prop: "shippingNo", minWidth: "150" },
+ { label: "閫�璐ф棩鏈�", prop: "makeTime", minWidth: "170" },
+ {
+ label: "閫�娆炬�婚",
+ prop: "refundAmount",
+ minWidth: "120",
+ align: "right",
+ formatData: (val) =>
+ val === null || val === undefined || val === ""
+ ? ""
+ : Number(val).toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }),
+ },
+ { label: "閫�璐у師鍥�", prop: "returnReason", minWidth: "150", showOverflowTooltip: true },
+ { label: "閿�鍞鍗曞彿", prop: "salesContractNo", minWidth: "150" },
+];
+
+const dataList = ref([]);
+const tableLoading = ref(false);
+
+function buildFilterParams() {
+ const params = {
+ returnNo: filters.returnNo || undefined,
+ customerName: filters.customerName || undefined,
+ };
+ if (filters.dateRange && filters.dateRange.length === 2) {
+ params.startDate = filters.dateRange[0];
+ params.endDate = filters.dateRange[1];
+ }
+ return params;
+}
+
+const onSearch = () => {
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const getTableData = () => {
+ tableLoading.value = true;
+ listPageAccountSalesReturn({
+ ...buildFilterParams(),
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ })
+ .then((res) => {
+ const ok = res.code === 200 || res.code === 0;
+ if (ok && res.data) {
+ pagination.total = res.data.total ?? 0;
+ dataList.value = res.data.records ?? [];
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ dataList.value = [];
+ }
+ })
+ .catch(() => {
+ dataList.value = [];
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const resetFilters = () => {
+ filters.returnNo = "";
+ filters.customerName = "";
+ filters.dateRange = [];
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ getTableData();
+};
+
+const handleOut = () => {
+ proxy.download(
+ "/accountSales/exportAccountSalesReturn",
+ buildFilterParams(),
+ `閿�鍞��璐${new Date().getTime()}.xlsx`
+ );
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 15px;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/detailLedger.vue b/src/views/financialManagement/voucher/detailLedger.vue
new file mode 100644
index 0000000..c07574c
--- /dev/null
+++ b/src/views/financialManagement/voucher/detailLedger.vue
@@ -0,0 +1,309 @@
+<template>
+ <div class="app-container ledger-page">
+ <div class="ledger-layout">
+ <aside class="subject-panel">
+ <el-input v-model="subjectKeyword" placeholder="璇疯緭鍏ョ鐩悕绉�/缂栧彿" clearable prefix-icon="Search" />
+ <el-scrollbar class="subject-tree-scroll">
+ <el-tree
+ ref="subjectTreeRef"
+ :data="subjectOptions"
+ node-key="code"
+ :props="{ label: 'name', children: 'children' }"
+ highlight-current
+ default-expand-all
+ :expand-on-click-node="false"
+ :filter-node-method="filterSubjectNode"
+ @node-click="handleSubjectClick"
+ >
+ <template #default="{ data }">
+ <span class="subject-node">{{ data.code }} {{ data.name }}</span>
+ </template>
+ </el-tree>
+ </el-scrollbar>
+ </aside>
+
+ <section class="ledger-content">
+ <el-form :model="filters" :inline="true" class="filter-form">
+ <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>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鏌ヨ</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+<!-- <el-button @click="handlePrint" icon="Printer">鎵撳嵃</el-button>-->
+ <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button>
+ </el-form-item>
+ </el-form>
+
+ <div class="table_list">
+ <el-table :data="dataList" border style="width: 100%">
+ <el-table-column prop="date" label="鏃ユ湡" width="120" />
+ <el-table-column prop="voucherNo" label="鍑瘉瀛楀彿" width="120" />
+ <el-table-column prop="summary" label="鎽樿" min-width="200" show-overflow-tooltip />
+ <el-table-column prop="debit" label="鍊熸柟" width="150">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟" width="150">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏂瑰悜" width="80">
+ <template #default="{ row }">
+ <el-tag :type="row.direction === '鍊�' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浣欓" width="150">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">楼{{ formatMoney(Math.abs(row.balance)) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <el-empty v-if="!currentSubject" description="璇烽�夋嫨浼氳绉戠洰鏌ヨ" style="margin-top: 50px;" />
+ </section>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import { getDetailLedger } from "@/api/financialManagement/ledger";
+
+defineOptions({
+ name: "绉戠洰鏄庣粏璐�",
+});
+
+const filters = reactive({
+ subject: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const dataList = ref([]);
+const subjectOptions = ref([]);
+const subjectKeyword = ref("");
+const subjectTreeRef = ref();
+
+const getPreviousMonth = () => {
+ const date = new Date();
+ date.setDate(1);
+ date.setMonth(date.getMonth() - 1);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ return `${year}-${month}`;
+};
+
+const defaultMonth = getPreviousMonth();
+filters.startMonth = defaultMonth;
+filters.endMonth = defaultMonth;
+
+const fallbackSubjects = [
+ { code: "1122", name: "搴旀敹璐︽" },
+ { code: "2202", name: "搴斾粯璐︽" },
+ { code: "6602", name: "绠$悊璐圭敤" },
+];
+
+const toTree = (nodes = []) =>
+ nodes
+ .filter(item => item.subjectCode && item.subjectName)
+ .map(item => ({
+ code: item.subjectCode,
+ name: item.subjectName,
+ children: toTree(item.children || []),
+ }));
+
+const findSubject = (options, code) => {
+ for (const item of options) {
+ if (item.code === code) return item;
+ if (item.children && item.children.length > 0) {
+ const found = findSubject(item.children, code);
+ if (found) return found;
+ }
+ }
+ return null;
+};
+
+const currentSubject = computed(() => {
+ if (!filters.subject) return null;
+ return findSubject(subjectOptions.value, filters.subject);
+});
+
+const getFirstSubjectCode = (nodes = []) => {
+ for (const item of nodes) {
+ if (item.code) return item.code;
+ if (item.children && item.children.length > 0) {
+ const childCode = getFirstSubjectCode(item.children);
+ if (childCode) return childCode;
+ }
+ }
+ return "";
+};
+
+const setDefaultSubjectSelection = async () => {
+ const firstCode = getFirstSubjectCode(subjectOptions.value);
+ if (!firstCode) {
+ filters.subject = "";
+ subjectTreeRef.value?.setCurrentKey(null);
+ return;
+ }
+ filters.subject = firstCode;
+ await nextTick();
+ subjectTreeRef.value?.setCurrentKey(firstCode);
+};
+
+const filterSubjectNode = (value, data) => {
+ const keyword = value?.trim();
+ if (!keyword) return true;
+ return `${data.code}${data.name}`.includes(keyword);
+};
+
+watch(subjectKeyword, (value) => {
+ subjectTreeRef.value?.filter(value || "");
+});
+
+const handleSubjectClick = async (data) => {
+ filters.subject = data.code;
+ await getTableData();
+};
+
+const loadSubjectOptions = async () => {
+ let options = [];
+ try {
+ const { data } = await listAccountSubject({
+ current: 1,
+ size: 1000,
+ });
+ options = toTree(data?.records || []);
+ } catch (error) {
+ // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず锛屼笅闈㈣蛋鍏滃簳绉戠洰
+ }
+ if (options.length === 0) {
+ options = fallbackSubjects.map(item => ({ ...item, children: [] }));
+ }
+ subjectOptions.value = options;
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+// 鑱旇皟绾﹀畾锛氭槑缁嗚处鎸夌鐩笌鏈熼棿杩囨护
+const getTableData = async () => {
+ if (!currentSubject.value) {
+ dataList.value = [];
+ return;
+ }
+ try {
+ const { data } = await getDetailLedger({
+ subjectCode: currentSubject.value.code,
+ startMonth: filters.startMonth,
+ endMonth: filters.endMonth,
+ });
+ dataList.value = Array.isArray(data) ? data : data?.records || [];
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const resetFilters = async () => {
+ filters.startMonth = defaultMonth;
+ filters.endMonth = defaultMonth;
+ dataList.value = [];
+ subjectKeyword.value = "";
+ subjectTreeRef.value?.filter("");
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const handlePrint = () => {
+ ElMessage.info("鎵撳嵃鍔熻兘");
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+onMounted(async () => {
+ await loadSubjectOptions();
+});
+</script>
+
+<style lang="scss" scoped>
+.ledger-layout {
+ display: flex;
+ gap: 16px;
+}
+
+.subject-panel {
+ width: 260px;
+ flex-shrink: 0;
+ padding: 12px;
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ background-color: #fff;
+}
+
+.subject-tree-scroll {
+ height: 600px;
+ margin-top: 12px;
+}
+
+.subject-node {
+ display: inline-flex;
+ align-items: center;
+}
+
+.ledger-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.filter-form {
+ margin-bottom: 12px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.subject-panel :deep(.el-tree-node__content) {
+ height: 34px;
+}
+
+.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
+ background-color: #f0f7ff;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/generalLedger.vue b/src/views/financialManagement/voucher/generalLedger.vue
new file mode 100644
index 0000000..b362279
--- /dev/null
+++ b/src/views/financialManagement/voucher/generalLedger.vue
@@ -0,0 +1,312 @@
+<template>
+ <div class="app-container ledger-page">
+ <div class="ledger-layout">
+ <aside class="subject-panel">
+ <el-input v-model="subjectKeyword" placeholder="璇疯緭鍏ョ鐩悕绉�/缂栧彿" clearable prefix-icon="Search" />
+ <el-scrollbar class="subject-tree-scroll">
+ <el-tree
+ ref="subjectTreeRef"
+ :data="subjectOptions"
+ node-key="code"
+ :props="{ label: 'name', children: 'children' }"
+ highlight-current
+ default-expand-all
+ :expand-on-click-node="false"
+ :filter-node-method="filterSubjectNode"
+ @node-click="handleSubjectClick"
+ >
+ <template #default="{ data }">
+ <span class="subject-node">{{ data.code }} {{ data.name }}</span>
+ </template>
+ </el-tree>
+ </el-scrollbar>
+ </aside>
+
+ <section class="ledger-content">
+ <el-form :model="filters" :inline="true" class="filter-form">
+ <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>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鏌ヨ</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+<!-- <el-button @click="handlePrint" icon="Printer">鎵撳嵃</el-button>-->
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </el-form-item>
+ </el-form>
+
+ <div class="table_list">
+ <el-table :data="dataList" border style="width: 100%">
+ <el-table-column prop="date" label="鏃ユ湡"/>
+ <!-- <el-table-column prop="voucherNo" label="鍑瘉瀛楀彿" width="120" /> -->
+ <!-- <el-table-column prop="summary" label="鎽樿" min-width="200" show-overflow-tooltip /> -->
+ <el-table-column prop="debit" label="鍊熸柟">
+ <template #default="{ row }">
+ <span v-if="row.debit > 0" class="text-danger">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="credit" label="璐锋柟">
+ <template #default="{ row }">
+ <span v-if="row.credit > 0" class="text-success">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏂瑰悜">
+ <template #default="{ row }">
+ <el-tag :type="row.direction === '鍊�' ? 'success' : 'danger'" size="small">{{ row.direction }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浣欓">
+ <template #default="{ row }">
+ <span :class="row.balance >= 0 ? 'text-primary' : 'text-warning'">楼{{ formatMoney(Math.abs(row.balance)) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <el-empty v-if="!currentSubject" description="璇烽�夋嫨浼氳绉戠洰鏌ヨ" style="margin-top: 50px;" />
+ </section>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
+import { ElMessage } from "element-plus";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import { getGeneralLedger } from "@/api/financialManagement/ledger";
+
+defineOptions({
+ name: "绉戠洰鎬昏处",
+});
+
+const filters = reactive({
+ subject: "",
+ startMonth: "",
+ endMonth: "",
+});
+
+const dataList = ref([]);
+const subjectOptions = ref([]);
+const subjectKeyword = ref("");
+const subjectTreeRef = ref();
+
+const getPreviousMonth = () => {
+ const date = new Date();
+ date.setDate(1);
+ date.setMonth(date.getMonth() - 1);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ return `${year}-${month}`;
+};
+
+const defaultMonth = getPreviousMonth();
+filters.startMonth = defaultMonth;
+filters.endMonth = defaultMonth;
+
+const fallbackSubjects = [
+ { code: "1001", name: "搴撳瓨鐜伴噾" },
+ { code: "1002", name: "閾惰瀛樻" },
+ { code: "1122", name: "搴旀敹璐︽" },
+ { code: "2202", name: "搴斾粯璐︽" },
+ { code: "6001", name: "涓昏惀涓氬姟鏀跺叆" },
+];
+
+const toTree = (nodes = []) =>
+ nodes
+ .filter(item => item.subjectCode && item.subjectName)
+ .map(item => ({
+ code: item.subjectCode,
+ name: item.subjectName,
+ children: toTree(item.children || []),
+ }));
+
+const findSubject = (options, code) => {
+ for (const item of options) {
+ if (item.code === code) return item;
+ if (item.children && item.children.length > 0) {
+ const found = findSubject(item.children, code);
+ if (found) return found;
+ }
+ }
+ return null;
+};
+
+const currentSubject = computed(() => {
+ if (!filters.subject) return null;
+ return findSubject(subjectOptions.value, filters.subject);
+});
+
+const getFirstSubjectCode = (nodes = []) => {
+ for (const item of nodes) {
+ if (item.code) return item.code;
+ if (item.children && item.children.length > 0) {
+ const childCode = getFirstSubjectCode(item.children);
+ if (childCode) return childCode;
+ }
+ }
+ return "";
+};
+
+const setDefaultSubjectSelection = async () => {
+ const firstCode = getFirstSubjectCode(subjectOptions.value);
+ if (!firstCode) {
+ filters.subject = "";
+ subjectTreeRef.value?.setCurrentKey(null);
+ return;
+ }
+ filters.subject = firstCode;
+ await nextTick();
+ subjectTreeRef.value?.setCurrentKey(firstCode);
+};
+
+const filterSubjectNode = (value, data) => {
+ const keyword = value?.trim();
+ if (!keyword) return true;
+ return `${data.code}${data.name}`.includes(keyword);
+};
+
+watch(subjectKeyword, (value) => {
+ subjectTreeRef.value?.filter(value || "");
+});
+
+const handleSubjectClick = async (data) => {
+ filters.subject = data.code;
+ await getTableData();
+};
+
+const loadSubjectOptions = async () => {
+ let options = [];
+ try {
+ const { data } = await listAccountSubject({
+ current: 1,
+ size: 1000,
+ status: 0,
+ });
+ options = toTree(data?.records || []);
+ } catch (error) {
+ // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず锛屼笅闈㈣蛋鍏滃簳绉戠洰
+ }
+ if (options.length === 0) {
+ options = fallbackSubjects.map(item => ({ ...item, children: [] }));
+ }
+ subjectOptions.value = options;
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+// 鑱旇皟绾﹀畾锛氭�昏处鎺ュ彛杩斿洖琛屾暟缁勶紙rowType/date/voucherNo/summary/debit/credit/direction/balance锛�
+const getTableData = async () => {
+ if (!currentSubject.value) {
+ dataList.value = [];
+ return;
+ }
+ try {
+ const { data } = await getGeneralLedger({
+ subjectCode: currentSubject.value.code,
+ startMonth: filters.startMonth,
+ endMonth: filters.endMonth,
+ });
+ dataList.value = Array.isArray(data) ? data : data?.records || [];
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const resetFilters = async () => {
+ filters.startMonth = defaultMonth;
+ filters.endMonth = defaultMonth;
+ dataList.value = [];
+ subjectKeyword.value = "";
+ subjectTreeRef.value?.filter("");
+ await setDefaultSubjectSelection();
+ if (filters.subject) {
+ await getTableData();
+ }
+};
+
+const handlePrint = () => {
+ ElMessage.info("鎵撳嵃鍔熻兘");
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+onMounted(async () => {
+ await loadSubjectOptions();
+});
+</script>
+
+<style lang="scss" scoped>
+.ledger-layout {
+ display: flex;
+ gap: 16px;
+}
+
+.subject-panel {
+ width: 260px;
+ flex-shrink: 0;
+ padding: 12px;
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ background-color: #fff;
+}
+
+.subject-tree-scroll {
+ height: 600px;
+ margin-top: 12px;
+}
+
+.subject-node {
+ display: inline-flex;
+ align-items: center;
+}
+
+.ledger-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.filter-form {
+ margin-bottom: 12px;
+}
+
+.text-primary {
+ color: #409eff;
+ font-weight: bold;
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+}
+
+.subject-panel :deep(.el-tree-node__content) {
+ height: 34px;
+}
+
+.subject-panel :deep(.el-tree-node.is-current > .el-tree-node__content) {
+ background-color: #f0f7ff;
+}
+</style>
diff --git a/src/views/financialManagement/voucher/index.vue b/src/views/financialManagement/voucher/index.vue
new file mode 100644
index 0000000..1aa6f69
--- /dev/null
+++ b/src/views/financialManagement/voucher/index.vue
@@ -0,0 +1,1110 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍑瘉瀛楀彿:">
+ <el-input v-model="filters.voucherNo" placeholder="璇疯緭鍏ュ嚟璇佸瓧鍙�" clearable style="width: 200px;" />
+ </el-form-item>
+ <el-form-item label="鍑瘉鏃ユ湡:">
+ <el-date-picker v-model="filters.dateRange" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange" range-separator="鑷�" start-placeholder="寮�濮嬫棩鏈�" end-placeholder="缁撴潫鏃ユ湡" clearable />
+ </el-form-item>
+ <el-form-item label="鍒跺崟浜�:">
+ <el-select v-model="filters.creator" placeholder="璇烽�夋嫨鍒跺崟浜�" clearable style="width: 150px;">
+ <el-option
+ v-for="item in creatorOptions"
+ :key="item"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="filters.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 150px;">
+ <el-option label="鏈繃璐�" value="unposted" />
+ <el-option label="宸茶繃璐�" value="posted" />
+ <el-option label="宸蹭綔搴�" value="cancelled" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div>
+ <el-statistic title="鍊熸柟鍚堣" :value="totalDebit" :precision="2" prefix="楼" />
+ <el-statistic title="璐锋柟鍚堣" :value="totalCredit" :precision="2" prefix="楼" style="margin-left: 30px;" />
+ </div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus">鏂板鍑瘉</el-button>
+ <!-- <el-button @click="handleImport" icon="Upload">瀵煎叆</el-button> -->
+ <!-- <el-button @click="handleOut" icon="Download">瀵煎嚭</el-button> -->
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ >
+ <template #debit="{ row }">
+ <span class="text-danger" v-if="row.debit > 0">楼{{ formatMoney(row.debit) }}</span>
+ <span v-else>-</span>
+ </template>
+ <template #credit="{ row }">
+ <span class="text-success" v-if="row.credit > 0">楼{{ formatMoney(row.credit) }}</span>
+ <span v-else>-</span>
+ </template>
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusLabel(row.status) }}</el-tag>
+ </template>
+ <template #operation="{ row }">
+ <el-button type="primary" link @click="view(row)">鏌ョ湅</el-button>
+ <el-button type="primary" link @click="edit(row)" v-if="canEditVoucher(row.status)">缂栬緫</el-button>
+ <el-button type="success" link @click="handlePost(row)" v-if="canEditVoucher(row.status)">杩囪处</el-button>
+ <el-button type="danger" link @click="handleCancel(row)" v-if="canEditVoucher(row.status)">浣滃簾</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDialog :title="dialogTitle" v-model="dialogVisible" width="1200px" @confirm="submitForm" @cancel="dialogVisible = false">
+ <div class="voucher-container">
+ <div class="voucher-header">
+ <h2 class="voucher-title">璁拌处鍑瘉</h2>
+ <div class="voucher-period">{{ form.voucherDate ? form.voucherDate.substring(0, 7) + '鏈�' : '' }}</div>
+ </div>
+ <el-form :model="form" :rules="rules" :disabled="isViewMode" ref="formRef" label-width="0">
+ <div class="voucher-info">
+ <div class="voucher-no-section">
+ <span class="label">鍑瘉瀛楋細</span>
+ <el-select v-model="form.voucherPrefix" :disabled="isViewMode" style="width: 70px;">
+ <el-option label="璁�" value="璁�" />
+ <el-option label="鐜�" value="鐜�" />
+ <el-option label="閾�" value="閾�" />
+ <el-option label="杞�" value="杞�" />
+ <el-option label="鏀�" value="鏀�" />
+ <el-option label="浠�" value="浠�" />
+ </el-select>
+ <el-input v-model="form.voucherNum" :disabled="isViewMode" style="width: 60px;" />
+ <span class="label" style="margin-left: 5px;">鍙�</span>
+ </div>
+ <div class="voucher-date-section">
+ <span class="label">鏃ユ湡锛�</span>
+ <el-date-picker v-model="form.voucherDate" :disabled="isViewMode" type="date" placeholder="閫夋嫨鏃ユ湡" value-format="YYYY-MM-DD" style="width: 140px;" />
+ </div>
+ <div class="voucher-attachment-section">
+ <span class="label">闄勪欢锛�</span>
+ <el-input-number v-model="form.attachmentCount" :disabled="isViewMode" :min="0" :controls="false" style="width: 60px;" />
+ <span class="label" style="margin-left: 5px;">寮�</span>
+ </div>
+ </div>
+ <div class="voucher-table">
+ <table class="accounting-voucher">
+ <thead>
+ <tr>
+ <th class="col-summary" rowspan="2">鎽樿</th>
+ <th class="col-subject" rowspan="2">浼氳绉戠洰</th>
+ <th class="col-debit-header" colspan="11">鍊熸柟</th>
+ <th class="col-credit-header" colspan="11">璐锋柟</th>
+ <th class="col-action" rowspan="2">鎿嶄綔</th>
+ </tr>
+ <tr class="amount-header">
+ <th>浜�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>涓�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>鍏�</th>
+ <th>瑙�</th>
+ <th>鍒�</th>
+ <th>浜�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>涓�</th>
+ <th>鍗�</th>
+ <th>鐧�</th>
+ <th>鍗�</th>
+ <th>鍏�</th>
+ <th>瑙�</th>
+ <th>鍒�</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(entry, rowIndex) in form.entries" :key="rowIndex" @click="selectRow(rowIndex)" :class="{ 'selected-row': selectedRowIndex === rowIndex }">
+ <td class="col-summary">
+ <el-input v-model="entry.summary" :disabled="isViewMode" placeholder="璇疯緭鍏ユ憳瑕�" @focus="selectRow(rowIndex)" />
+ </td>
+ <td class="col-subject">
+ <el-tree-select
+ v-model="entry.subjectCode"
+ :data="subjectTreeOptions"
+ :props="subjectTreeSelectProps"
+ :disabled="isViewMode"
+ placeholder="閫夋嫨绉戠洰"
+ filterable
+ check-strictly
+ clearable
+ :render-after-expand="false"
+ @change="(val) => handleSubjectChange(val, rowIndex)"
+ @focus="selectRow(rowIndex)"
+ />
+ <!-- <div class="subject-name">{{ entry.subjectName }}</div> -->
+ </td>
+ <!-- 鍊熸柟11鍒� -->
+ <template v-if="editingCell.row === rowIndex && editingCell.type === 'debit'">
+ <td colspan="11" class="debit-input-cell">
+ <el-input-number ref="amountInputRef" v-model="entry.debit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
+ </td>
+ </template>
+ <template v-else>
+ <td v-for="(digit, dIndex) in getAmountDigits(entry.debit, 11)" :key="'debit-'+dIndex" class="amount-cell debit-cell" @click="openAmountInput(rowIndex, 'debit')">
+ <span :class="{ 'text-primary': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
+ </td>
+ </template>
+ <!-- 璐锋柟11鍒� -->
+ <template v-if="editingCell.row === rowIndex && editingCell.type === 'credit'">
+ <td colspan="11" class="credit-input-cell">
+ <el-input-number ref="amountInputRef" v-model="entry.credit" :disabled="isViewMode" :min="0" :precision="2" :controls="false" :value-on-clear="undefined" size="small" @blur="finishEdit" class="full-width-input" />
+ </td>
+ </template>
+ <template v-else>
+ <td v-for="(digit, dIndex) in getAmountDigits(entry.credit, 11)" :key="'credit-'+dIndex" class="amount-cell credit-cell" @click="openAmountInput(rowIndex, 'credit')">
+ <span :class="{ 'text-danger': digit !== '', 'zero': digit === '' }">{{ digit || '' }}</span>
+ </td>
+ </template>
+ <td class="col-action">
+ <el-button type="danger" link size="small" @click="removeEntry(rowIndex)" icon="Delete" :disabled="isViewMode || form.entries.length <= 2">鍒犻櫎</el-button>
+ </td>
+ </tr>
+ <tr class="total-row">
+ <td class="col-summary" colspan="2" style="text-align: center; font-weight: bold;">鍚堣锛�</td>
+ <td v-for="(digit, index) in getAmountDigits(totalDebitEntry, 11)" :key="'total-debit-'+index" class="amount-cell total-debit-cell">
+ <span :class="{ 'text-primary': digit !== '' }">{{ digit }}</span>
+ </td>
+ <td v-for="(digit, index) in getAmountDigits(totalCreditEntry, 11)" :key="'total-credit-'+index" class="amount-cell total-credit-cell">
+ <span :class="{ 'text-danger': digit !== '' }">{{ digit }}</span>
+ </td>
+ <td class="col-action"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="voucher-toolbar">
+ <el-button type="primary" link @click="addEntry" icon="Plus" :disabled="isViewMode">鏂板琛�</el-button>
+ </div>
+ <div class="voucher-footer">
+ <div class="creator-section">
+ <span class="label">鍒跺崟浜猴細</span>
+ <el-select
+ v-model="form.creator"
+ :disabled="isViewMode"
+ placeholder="璇烽�夋嫨鍒跺崟浜�"
+ filterable
+ clearable
+ style="width: 200px;"
+ >
+ <el-option
+ v-for="item in creatorOptions"
+ :key="item"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </div>
+ </div>
+ <!-- 缂栬緫妯″紡锛氫娇鐢� AttachmentUploadFile 涓婁紶缁勪欢 -->
+ <div class="voucher-attachment-upload" v-if="!isViewMode">
+ <div class="attachment-label">闄勪欢涓婁紶锛�</div>
+ <AttachmentUploadFile
+ v-model:fileList="form.attachments"
+ :disabled="isViewMode"
+ :limit="10"
+ :fileSize="50"
+ buttonText="鐐瑰嚮涓婁紶闄勪欢"
+ @change="handleAttachmentChange"
+ />
+ </div>
+ </el-form>
+ <!-- 鏌ョ湅妯″紡锛氬睍绀洪檮浠跺垪琛紙鏀惧湪 el-form 澶栭潰锛岄伩鍏嶈 disabled锛� -->
+ <div class="voucher-attachment-upload" v-if="isViewMode && form.attachments?.length">
+ <div class="attachment-label">闄勪欢鍒楄〃锛�</div>
+ <el-table :data="form.attachments" border class="attachment-table">
+ <el-table-column label="闄勪欢鍚嶇О" show-overflow-tooltip>
+ <template #default="scope">
+ {{ scope.row.originalFilename || scope.row.name || scope.row.fileName || '鏈懡鍚嶆枃浠�' }}
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="previewFile(scope.row)">棰勮</el-button>
+ <el-button link type="primary" size="small" @click="downloadFile(scope.row)">涓嬭浇</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+ <template #footer>
+ <div>
+ <el-button v-if="!isViewMode" type="primary" @click="submitForm" :disabled="!isBalanced">淇濆瓨</el-button>
+ <el-button @click="dialogVisible = false">{{ isViewMode ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ </div>
+ </template>
+ </FormDialog>
+ <!-- 鏂囦欢棰勮缁勪欢 -->
+ <FilePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, computed, nextTick } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import AttachmentUploadFile from "@/components/AttachmentUpload/file/index.vue";
+import FileList from "@/components/Dialog/FileList.vue";
+import FilePreview from "@/components/filePreview/index.vue";
+import download from "@/plugins/download.js";
+import useUserStore from "@/store/modules/user";
+import { userListNoPageByTenantId } from "@/api/system/user";
+import { listAccountSubject } from "@/api/financialManagement/accountSubject";
+import {
+ listVoucherPage,
+ addVoucher,
+ updateVoucher,
+ postVoucher,
+ cancelVoucher,
+ getVoucherDetail,
+} from "@/api/financialManagement/voucher";
+
+defineOptions({
+ name: "鍑瘉绠$悊",
+});
+
+const userStore = useUserStore();
+const getDefaultCreator = () => userStore.nickName || userStore.name || "寮犱笁";
+
+const filters = reactive({
+ voucherNo: "",
+ dateRange: [],
+ creator: "",
+ status: "",
+});
+
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 10,
+ total: 0,
+});
+
+const columns = [
+ { label: "鍑瘉瀛楀彿", prop: "voucherNo", width: "120" },
+ { label: "鍑瘉鏃ユ湡", prop: "voucherDate", width: "120" },
+ { label: "鎽樿", prop: "summary", showOverflowTooltip: true },
+ { label: "鍊熸柟閲戦", prop: "debit", dataType: "slot", slot: "debit" },
+ { label: "璐锋柟閲戦", prop: "credit", dataType: "slot", slot: "credit" },
+ { label: "鍒跺崟浜�", prop: "creator", width: "100" },
+ { label: "鐘舵��", prop: "status", dataType: "slot", slot: "status" },
+ { label: "鎿嶄綔", prop: "operation", dataType: "slot", slot: "operation", width: "220", fixed: "right" },
+];
+
+const dataList = ref([]);
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref(null);
+const dialogMode = ref("add");
+const isEdit = ref(false);
+const currentId = ref(null);
+const isViewMode = computed(() => dialogMode.value === "view");
+const filePreviewRef = ref(null);
+
+const fallbackSubjectTree = [
+ { subjectCode: "1001", subjectName: "搴撳瓨鐜伴噾", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "1002", subjectName: "閾惰瀛樻", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "1122", subjectName: "搴旀敹璐︽", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "2202", subjectName: "搴斾粯璐︽", balanceDirection: "璐锋柟", children: [] },
+ { subjectCode: "5001", subjectName: "鐢熶骇鎴愭湰", balanceDirection: "鍊熸柟", children: [] },
+ { subjectCode: "6001", subjectName: "涓昏惀涓氬姟鏀跺叆", balanceDirection: "璐锋柟", children: [] },
+ { subjectCode: "6401", subjectName: "涓昏惀涓氬姟鎴愭湰", balanceDirection: "鍊熸柟", children: [] },
+];
+
+const subjectTreeOptions = ref([]);
+const subjectList = ref([]);
+const subjectTreeSelectProps = {
+ children: "children",
+ label: "label",
+ value: "value",
+};
+
+const buildSubjectTreeOptions = (nodes = [], flatList = []) =>
+ (nodes || [])
+ .filter(item => item.subjectCode && item.subjectName)
+ .map(item => {
+ const balanceDirection = item.balanceDirection || "";
+ const flatItem = {
+ code: item.subjectCode,
+ name: item.subjectName,
+ balanceDirection,
+ };
+ flatList.push(flatItem);
+ return {
+ value: flatItem.code,
+ label: `${flatItem.code} ${flatItem.name}${balanceDirection ? ` [${balanceDirection}]` : ""}`,
+ children: buildSubjectTreeOptions(item.children || [], flatList),
+ };
+ });
+
+const createEmptyEntry = () => ({
+ subjectCode: "",
+ subjectName: "",
+ balanceDirection: "",
+ summary: "",
+ debit: undefined,
+ credit: undefined,
+});
+
+const createDefaultForm = () => ({
+ voucherNo: "",
+ voucherPrefix: "璁�",
+ voucherNum: "",
+ voucherDate: "",
+ attachmentCount: 0,
+ attachments: [],
+ entries: [createEmptyEntry(), createEmptyEntry()],
+ creator: getDefaultCreator(),
+ remark: "",
+});
+
+const form = reactive({
+ ...createDefaultForm(),
+});
+
+const userOptions = ref([]);
+
+const creatorOptions = computed(() => {
+ const source = [
+ ...userOptions.value.map(item => item.nickName || item.userName || item.name),
+ getDefaultCreator(),
+ form.creator,
+ filters.creator,
+ ];
+ return [...new Set(source.filter(Boolean))];
+});
+
+const selectedRowIndex = ref(-1);
+const editingCell = reactive({
+ row: -1,
+ type: "",
+});
+const amountInputRef = ref(null);
+
+const isBalanced = computed(() => {
+ return totalDebitEntry.value === totalCreditEntry.value && totalDebitEntry.value > 0;
+});
+
+const rules = {
+ voucherDate: [{ required: true, message: "璇烽�夋嫨鍑瘉鏃ユ湡", trigger: "change" }],
+};
+
+const totalDebit = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.debit), 0);
+});
+
+const totalCredit = computed(() => {
+ return dataList.value.reduce((sum, item) => sum + Number(item.credit), 0);
+});
+
+const totalDebitEntry = computed(() => {
+ return form.entries.reduce((sum, item) => sum + Number(item.debit || 0), 0);
+});
+
+const totalCreditEntry = computed(() => {
+ return form.entries.reduce((sum, item) => sum + Number(item.credit || 0), 0);
+});
+
+const formatMoney = (value) => {
+ if (value === undefined || value === null) return "0.00";
+ return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+};
+
+const normalizeVoucherStatus = status => String(status || "").toLowerCase();
+
+const canEditVoucher = status => {
+ const key = normalizeVoucherStatus(status);
+ return key === "unposted" || status === "鏈繃璐�";
+};
+
+const getStatusLabel = (status) => {
+ const key = normalizeVoucherStatus(status);
+ const map = { unposted: "鏈繃璐�", posted: "宸茶繃璐�", cancelled: "宸蹭綔搴�" };
+ return map[key] || status;
+};
+
+const getStatusType = (status) => {
+ const key = normalizeVoucherStatus(status);
+ const map = { unposted: "warning", posted: "success", cancelled: "info" };
+ return map[key] || "";
+};
+
+// 鑱旇皟绾﹀畾锛氬垎椤靛弬鏁颁娇鐢� current/size锛屾棩鏈熻寖鍥存媶鍒嗕负 startDate/endDate
+const getTableData = async () => {
+ try {
+ const [startDate, endDate] =
+ filters.dateRange && filters.dateRange.length === 2 ? filters.dateRange : ["", ""];
+ const { data } = await listVoucherPage({
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ voucherNo: filters.voucherNo,
+ creator: filters.creator,
+ status: filters.status,
+ startDate,
+ endDate,
+ });
+ dataList.value = data?.records || [];
+ pagination.total = Number(data?.total || 0);
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+// 鍑瘉鍒嗗綍閲岀殑绉戠洰涓嬫媺涓庢�昏处绉戠洰淇濇寔涓�鑷达紝閬垮厤鎻愪氦涓嶅瓨鍦ㄧ鐩�
+const loadSubjectList = async () => {
+ try {
+ const { data } = await listAccountSubject({
+ current: 1,
+ size: 1000,
+ status: 0
+ });
+ const flatList = [];
+ const treeOptions = buildSubjectTreeOptions(data?.records || [], flatList);
+ if (treeOptions.length > 0) {
+ subjectTreeOptions.value = treeOptions;
+ subjectList.value = flatList;
+ return;
+ }
+ const fallbackFlatList = [];
+ subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
+ subjectList.value = fallbackFlatList;
+ } catch (error) {
+ // 鍏ㄥ眬鎷︽埅鍣ㄥ凡鎻愮ず閿欒锛岃繖閲屼繚鐣欓粯璁ょ鐩綔涓哄厹搴�
+ const fallbackFlatList = [];
+ subjectTreeOptions.value = buildSubjectTreeOptions(fallbackSubjectTree, fallbackFlatList);
+ subjectList.value = fallbackFlatList;
+ }
+};
+
+const loadUserOptions = async () => {
+ try {
+ const { data } = await userListNoPageByTenantId();
+ userOptions.value = Array.isArray(data) ? data : [];
+ } catch (error) {
+ userOptions.value = [];
+ }
+};
+
+const resetFilters = () => {
+ filters.voucherNo = "";
+ filters.dateRange = [];
+ filters.creator = "";
+ filters.status = "";
+ pagination.currentPage = 1;
+ getTableData();
+};
+
+const changePage = ({ current, size }) => {
+ pagination.currentPage = current;
+ pagination.pageSize = size;
+ getTableData();
+};
+
+const addEntry = () => {
+ if (isViewMode.value) {
+ return;
+ }
+ form.entries.push(createEmptyEntry());
+};
+
+const handleAttachmentChange = (fileList) => {
+ form.attachmentCount = fileList?.length || 0;
+};
+
+// 浣跨敤椤圭洰灏佽鐨� filePreview 缁勪欢棰勮鏂囦欢
+const previewFile = (row) => {
+ const url = row.previewURL || row.previewUrl || row.url;
+ if (url && filePreviewRef.value) {
+ filePreviewRef.value.open(url);
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曢瑙�');
+ }
+};
+
+// 浣跨敤椤圭洰灏佽鐨� download 鎻掍欢涓嬭浇鏂囦欢
+const downloadFile = (row) => {
+ const url = row.downloadURL || row.downloadUrl || row.url;
+ if (url) {
+ const filename = row.originalFilename || row.name || row.fileName || 'download';
+ download.byUrl(url, filename);
+ } else {
+ ElMessage.warning('鏂囦欢鍦板潃鏃犳晥锛屾棤娉曚笅杞�');
+ }
+};
+
+const selectRow = (index) => {
+ selectedRowIndex.value = index;
+};
+
+const openAmountInput = (index, type) => {
+ if (isViewMode.value) {
+ return;
+ }
+ editingCell.row = index;
+ editingCell.type = type;
+ nextTick(() => {
+ if (amountInputRef.value) {
+ amountInputRef.value.focus();
+ }
+ });
+};
+
+const finishEdit = () => {
+ editingCell.row = -1;
+ editingCell.type = "";
+};
+
+const getAmountDigits = (amount, length) => {
+ if (!amount || amount === 0) {
+ return new Array(length).fill('');
+ }
+
+ const amountStr = Number(amount).toFixed(2);
+ const [intPart, decPart] = amountStr.split('.');
+ const fullAmount = intPart + decPart;
+
+ // 宸﹀~鍏�0鍒版寚瀹氶暱搴�
+ const paddedAmount = fullAmount.padStart(length, '0');
+ const digits = paddedAmount.split('');
+
+ // 鎵惧埌绗竴涓潪闆舵暟瀛楃殑浣嶇疆
+ let firstNonZeroIndex = 0;
+ for (let i = 0; i < digits.length; i++) {
+ if (digits[i] !== '0') {
+ firstNonZeroIndex = i;
+ break;
+ }
+ }
+
+ // 鍙殣钘忓墠瀵奸浂锛堢涓�涓潪闆舵暟瀛椾箣鍓嶇殑闆讹級
+ return digits.map((d, index) => {
+ if (index < firstNonZeroIndex) {
+ return ''; // 鍓嶅闆舵樉绀轰负绌�
+ }
+ return d; // 淇濈暀閲戦涓殑闆�
+ });
+};
+
+const removeEntry = (index) => {
+ if (isViewMode.value) {
+ return;
+ }
+ if (form.entries.length <= 2) {
+ return;
+ }
+ form.entries.splice(index, 1);
+};
+
+const handleSubjectChange = (val, index) => {
+ const subject = subjectList.value.find(item => item.code === val);
+ if (subject) {
+ form.entries[index].subjectName = subject.name;
+ form.entries[index].balanceDirection = subject.balanceDirection || "";
+ } else {
+ form.entries[index].subjectName = "";
+ form.entries[index].balanceDirection = "";
+ }
+};
+
+const add = () => {
+ dialogMode.value = "add";
+ isEdit.value = false;
+ currentId.value = null;
+ dialogTitle.value = "鏂板鍑瘉";
+ const nextNum = String((pagination.total || 0) + 1).padStart(4, "0");
+ Object.assign(form, createDefaultForm(), {
+ voucherPrefix: "璁�",
+ voucherNum: nextNum,
+ voucherNo: `璁�-${nextNum}`,
+ voucherDate: new Date().toISOString().split('T')[0],
+ });
+ selectedRowIndex.value = 0;
+ dialogVisible.value = true;
+};
+
+const openVoucherDialog = async (row, mode = "edit") => {
+ try {
+ dialogMode.value = mode;
+ isEdit.value = mode === "edit";
+ currentId.value = row.id;
+ dialogTitle.value = mode === "view" ? "鏌ョ湅鍑瘉" : "缂栬緫鍑瘉";
+ const { data } = await getVoucherDetail(row.id);
+ const detail = data || row;
+ const parts = (detail.voucherNo || "").split("-");
+ const attachments = detail.storageBlobVOList || detail.storageBlobDTOs || detail.attachments || [];
+ Object.assign(form, createDefaultForm(), {
+ ...detail,
+ voucherPrefix: parts[0] || "璁�",
+ voucherNum: parts[1] || "",
+ creator: detail.creator || getDefaultCreator(),
+ attachments,
+ entries:
+ detail.entries?.map(item => ({
+ subjectCode: item.subjectCode || "",
+ subjectName: item.subjectName || "",
+ balanceDirection: item.balanceDirection || "",
+ summary: item.summary || "",
+ debit: Number(item.debit || 0),
+ credit: Number(item.credit || 0),
+ })) || [],
+ });
+ if (form.entries.length < 2) {
+ while (form.entries.length < 2) {
+ form.entries.push(createEmptyEntry());
+ }
+ }
+ selectedRowIndex.value = 0;
+ dialogVisible.value = true;
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+};
+
+const edit = async row => {
+ await openVoucherDialog(row, "edit");
+};
+
+const view = async row => {
+ await openVoucherDialog(row, "view");
+};
+
+const handlePost = (row) => {
+ ElMessageBox.confirm("纭杩囪处璇ュ嚟璇佸悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "info",
+ }).then(async () => {
+ await postVoucher({ id: row.id });
+ ElMessage.success("杩囪处鎴愬姛");
+ await getTableData();
+ });
+};
+
+const handleCancel = (row) => {
+ ElMessageBox.confirm("纭浣滃簾璇ュ嚟璇佸悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ await cancelVoucher({ id: row.id });
+ ElMessage.success("浣滃簾鎴愬姛");
+ await getTableData();
+ });
+};
+
+const handleImport = () => {
+ ElMessage.info("瀵煎叆鍔熻兘");
+};
+
+const handleOut = () => {
+ ElMessage.success("瀵煎嚭鎴愬姛");
+};
+
+const submitForm = () => {
+ if (isViewMode.value) {
+ dialogVisible.value = false;
+ return;
+ }
+ formRef.value.validate(async valid => {
+ if (valid) {
+ // 鍓嶇疆鏍¢獙锛氫笌鍚庣瑙勫垯瀵归綈锛屽噺灏戞棤鏁堣姹�
+ if (!isBalanced.value) {
+ ElMessage.error("鍊熻捶涓嶅钩琛★紝璇锋鏌ュ垎褰�");
+ return;
+ }
+
+ const validEntries = form.entries.filter(
+ entry => entry.subjectCode && (Number(entry.debit) > 0 || Number(entry.credit) > 0)
+ );
+ if (validEntries.length === 0) {
+ ElMessage.error("璇疯嚦灏戝~鍐欎竴鏉℃湁鏁堝垎褰�");
+ return;
+ }
+
+ const invalidEntry = validEntries.find(
+ entry => Number(entry.debit) > 0 && Number(entry.credit) > 0
+ );
+ if (invalidEntry) {
+ ElMessage.error("鍚屼竴鍒嗗綍涓嶈兘鍚屾椂濉啓鍊熸柟鍜岃捶鏂�");
+ return;
+ }
+
+ const summary = validEntries.find(e => e.debit > 0)?.summary || "";
+
+ const voucherNo = `${form.voucherPrefix}-${form.voucherNum}`;
+ const dataToSave = {
+ voucherNo,
+ voucherDate: form.voucherDate,
+ summary,
+ creator: form.creator,
+ attachmentCount: Number(form.attachmentCount || 0),
+ remark: form.remark,
+ debit: totalDebitEntry.value,
+ credit: totalCreditEntry.value,
+ storageBlobDTOs: form.attachments || [],
+ entries: validEntries.map(entry => ({
+ subjectCode: entry.subjectCode,
+ subjectName: entry.subjectName,
+ summary: entry.summary,
+ debit: Number(entry.debit || 0),
+ credit: Number(entry.credit || 0),
+ })),
+ };
+
+ try {
+ if (isEdit.value) {
+ await updateVoucher({
+ id: currentId.value,
+ ...dataToSave,
+ });
+ ElMessage.success("缂栬緫鎴愬姛");
+ } else {
+ await addVoucher(dataToSave);
+ ElMessage.success("鏂板鎴愬姛");
+ }
+ dialogVisible.value = false;
+ await getTableData();
+ } catch (error) {
+ // 鎻愮ず鐢卞叏灞�璇锋眰鎷︽埅鍣ㄥ鐞嗭紝杩欓噷浠呴槻姝㈡湭鎹曡幏寮傚父
+ }
+ }
+ });
+};
+
+onMounted(async () => {
+ await loadUserOptions();
+ await loadSubjectList();
+ await getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+
+ > div:first-child {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.text-success {
+ color: #67c23a;
+ font-weight: bold;
+}
+
+.text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+}
+
+.text-primary {
+ color: #409eff;
+}
+
+.voucher-container {
+ background: #fff;
+ padding: 20px;
+}
+
+.voucher-header {
+ text-align: center;
+ margin-bottom: 15px;
+
+ .voucher-title {
+ font-size: 22px;
+ font-weight: bold;
+ margin: 0 0 5px 0;
+ color: #303133;
+ }
+
+ .voucher-period {
+ font-size: 14px;
+ color: #909399;
+ }
+}
+
+.voucher-info {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ padding: 0 10px;
+
+ .label {
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .voucher-no-section,
+ .voucher-date-section,
+ .voucher-attachment-section {
+ display: flex;
+ align-items: center;
+ }
+}
+
+.voucher-attachment-upload {
+ margin-top: 15px;
+ padding: 0 10px;
+
+ .attachment-label {
+ font-size: 14px;
+ color: #606266;
+ margin-bottom: 10px;
+ }
+
+ .attachment-table {
+ border-radius: 4px;
+ }
+}
+
+.voucher-table {
+ border: 1px solid #dcdfe6;
+ border-right: none;
+ margin-bottom: 15px;
+}
+
+.accounting-voucher {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 13px;
+
+ th,
+ td {
+ border: 1px solid #dcdfe6;
+ text-align: center;
+ padding: 0;
+ height: 36px;
+ }
+
+ & th:last-child,
+ & td:last-child {
+ border-right: none !important;
+ }
+
+ thead {
+ background-color: #f5f7fa;
+
+ th {
+ font-weight: normal;
+ color: #606266;
+ font-size: 12px;
+ }
+
+ .col-summary,
+ .col-subject {
+ font-weight: bold;
+ font-size: 13px;
+ }
+
+ .col-debit-header,
+ .col-credit-header {
+ background-color: #ecf5ff;
+ color: #409eff;
+ font-weight: bold;
+ }
+ }
+
+ .amount-header {
+ th {
+ font-size: 11px;
+ padding: 2px 0;
+ background-color: #f5f7fa;
+ }
+ }
+
+ .col-summary {
+ width: 160px;
+ min-width: 160px;
+ }
+
+ .col-subject {
+ width: 180px;
+ min-width: 180px;
+ }
+
+ .col-action {
+ width: 60px;
+ min-width: 60px;
+ text-align: center;
+ }
+
+ .amount-cell {
+ width: 24px;
+ min-width: 24px;
+ max-width: 24px;
+ padding: 0;
+ font-size: 13px;
+ font-family: 'Courier New', monospace;
+ cursor: pointer;
+ text-align: center;
+
+ &:hover {
+ background-color: #f5f7fa;
+ }
+
+ span {
+ display: block;
+ width: 100%;
+ height: 100%;
+ line-height: 36px;
+
+ &.zero {
+ color: #c0c4cc;
+ }
+ }
+ }
+
+ .debit-input-cell,
+ .credit-input-cell {
+ padding: 0;
+ background-color: #ecf5ff;
+
+ .full-width-input {
+ width: 100%;
+
+ :deep(.el-input__wrapper) {
+ padding: 0 10px;
+ box-shadow: none;
+ background-color: transparent;
+ }
+
+ input {
+ text-align: right;
+ font-size: 14px;
+ height: 34px;
+ }
+ }
+ }
+
+ tbody {
+ tr {
+ &:hover {
+ background-color: #f5f7fa;
+ }
+
+ &.selected-row {
+ background-color: #ecf5ff;
+ }
+ }
+
+ td {
+ .el-input {
+ .el-input__wrapper {
+ box-shadow: none;
+ padding: 0 5px;
+ }
+
+ input {
+ text-align: center;
+ height: 34px;
+ }
+ }
+
+ .el-select {
+ width: 100%;
+
+ .el-input__wrapper {
+ box-shadow: none;
+ }
+
+ input {
+ text-align: center;
+ height: 34px;
+ }
+ }
+ }
+
+ .col-summary {
+ .el-input input {
+ text-align: left;
+ padding-left: 10px;
+ }
+ }
+
+ .col-subject {
+ position: relative;
+
+ .el-select,
+ .el-tree-select {
+ .el-input input {
+ font-size: 12px;
+ }
+ }
+
+ .subject-name {
+ font-size: 11px;
+ color: #909399;
+ margin-top: 2px;
+ line-height: 1.2;
+ }
+ }
+ }
+
+ .total-row {
+ background-color: #fdf6ec;
+
+ td {
+ font-weight: bold;
+ }
+
+ .total-cell {
+ background-color: #fdf6ec;
+ font-weight: bold;
+ }
+ }
+}
+
+.voucher-toolbar {
+ display: flex;
+ justify-content: flex-start;
+ padding: 10px 0;
+ margin-top: 5px;
+}
+
+.voucher-footer {
+ display: flex;
+ justify-content: flex-end;
+ padding: 0 10px;
+ margin-top: 10px;
+
+ .creator-section {
+ .label {
+ font-size: 14px;
+ color: #606266;
+ }
+ }
+}
+
+:deep(.el-dialog__body) {
+ padding: 10px 20px;
+}
+</style>
diff --git a/src/views/inventoryManagement/dispatchLog/Record.vue b/src/views/inventoryManagement/dispatchLog/Record.vue
new file mode 100644
index 0000000..2d7411d
--- /dev/null
+++ b/src/views/inventoryManagement/dispatchLog/Record.vue
@@ -0,0 +1,869 @@
+<template>
+ <div>
+ <div class="search_form"
+ style="margin-bottom: 10px">
+ <el-form ref="searchFormRef"
+ :model="searchForm"
+ class="demo-form-inline">
+ <el-row :gutter="20">
+ <el-col :span="4">
+ <el-form-item label="鍑哄簱鏃ユ湡"
+ prop="timeStr">
+ <el-date-picker v-model="searchForm.timeStr"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="浜у搧澶х被"
+ prop="productName">
+ <el-input v-model="searchForm.productName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="瑙勬牸鍨嬪彿"
+ prop="model">
+ <el-input v-model="searchForm.model"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鎵瑰彿"
+ prop="batchNo">
+ <el-input v-model="searchForm.batchNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鏉ユ簮"
+ prop="recordType">
+ <el-select v-model="searchForm.recordType"
+ style="width: 240px"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in stockRecordTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <!-- 鎸夐挳 -->
+ <el-col :span="4">
+ <el-form-item>
+ <el-button type="primary"
+ @click="getList">
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+ <div class="actions">
+ <el-button type="primary"
+ @click="handleBatchApprove">瀹℃壒</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ <el-button type="primary"
+ plain
+ @click="handlePrint">鎵撳嵃</el-button>
+ </div>
+ <div class="table_list">
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys"
+ :row-key="(row) => row.id"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)">
+ <el-table-column align="center"
+ type="selection"
+ width="55" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鍑哄簱鎵规"
+ prop="outboundBatches"
+ min-width="100"
+ show-overflow-tooltip />
+ <el-table-column label="鍑哄簱鏃ユ湡"
+ prop="createTime"
+ show-overflow-tooltip />
+ <el-table-column label="浜у搧澶х被"
+ prop="productName"
+ show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model"
+ show-overflow-tooltip />
+ <el-table-column label="鎵瑰彿"
+ prop="batchNo"
+ show-overflow-tooltip />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ show-overflow-tooltip />
+ <el-table-column label="鍑哄簱鏁伴噺"
+ prop="stockOutNum"
+ show-overflow-tooltip />
+ <el-table-column label="鍑哄簱浜�"
+ prop="createBy"
+ show-overflow-tooltip />
+ <el-table-column label="鏉ユ簮"
+ prop="recordType"
+ show-overflow-tooltip>
+ <template #default="scope">
+ {{ getRecordType(scope.row.recordType) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹℃壒鐘舵��"
+ prop="approvalStatus"
+ show-overflow-tooltip>
+ <template #default="scope">
+ <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)"
+ size="small">
+ {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import pagination from "@/components/PIMTable/Pagination.vue";
+ import { ref } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import useUserStore from "@/store/modules/user";
+ import { getCurrentDate } from "@/utils/index.js";
+ import {
+ getStockOutPage,
+ delPendingStockOut,
+ batchApproveStockOutRecords,
+ } from "@/api/inventoryManagement/stockOut.js";
+ import {
+ findAllQualifiedStockOutRecordTypeOptions,
+ findAllUnQualifiedStockOutRecordTypeOptions,
+ } from "@/api/basicData/enum.js";
+
+ const userStore = useUserStore();
+ const { proxy } = getCurrentInstance();
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ // 鏉ユ簮绫诲瀷閫夐」
+ const stockRecordTypeOptions = ref([]);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ });
+ const total = ref(0);
+
+ const props = defineProps({
+ type: {
+ type: String,
+ required: true,
+ default: "0",
+ },
+ topParentProductId: {
+ type: [String, Number],
+ default: undefined,
+ },
+ });
+
+ // 鎵撳嵃鐩稿叧
+ const printPreviewVisible = ref(false);
+ const printData = ref([]);
+
+ // 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+ const data = reactive({
+ searchForm: {
+ supplierName: "",
+ timeStr: "",
+ recordType: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ const searchFormRef = ref(null);
+
+ const resetSearch = () => {
+ searchFormRef.value?.resetFields();
+ page.current = 1;
+ getList();
+ };
+
+ const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ getStockOutPage({
+ ...searchForm.value,
+ ...page,
+ topParentProductId: props.topParentProductId,
+ })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ tableData.value.map(item => {
+ item.children = [];
+ });
+ total.value = res.data.total;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const getRecordType = recordType => {
+ return (
+ stockRecordTypeOptions.value.find(item => item.value === recordType)
+ ?.label || ""
+ );
+ };
+
+ const approvalStatusLabelMap = {
+ 0: "寰呭鎵�",
+ 1: "閫氳繃",
+ 2: "椹冲洖",
+ 3: "寰呯‘璁�",
+ pending: "寰呭鎵�",
+ approved: "閫氳繃",
+ rejected: "椹冲洖",
+ PENDING: "寰呭鎵�",
+ APPROVED: "閫氳繃",
+ REJECTED: "椹冲洖",
+ };
+
+ const getApprovalStatusLabel = status => {
+ if (status === null || status === undefined || status === "") {
+ return "寰呭鎵�";
+ }
+ return approvalStatusLabelMap[status] || "寰呭鎵�";
+ };
+
+ // 閫氳繃/椹冲洖鍥哄畾鑹诧紱鍏朵綑锛堝惈寰呭鎵广�佺┖鍊笺�佹湭鏄犲皠浣嗘枃妗堜负寰呭鎵癸級缁熶竴鐢� warning 棰勮鑹�
+ const getApprovalStatusTagType = status => {
+ if (
+ status === 1 ||
+ status === "1" ||
+ status === "approved" ||
+ status === "APPROVED"
+ )
+ return "success";
+ if (
+ status === 2 ||
+ status === "2" ||
+ status === "rejected" ||
+ status === "REJECTED"
+ )
+ return "danger";
+ return "warning";
+ };
+
+ // 鑾峰彇鏉ユ簮绫诲瀷閫夐」
+ const fetchStockRecordTypeOptions = () => {
+ if (props.type === "0") {
+ findAllQualifiedStockOutRecordTypeOptions().then(res => {
+ stockRecordTypeOptions.value = res.data;
+ });
+ return;
+ }
+ findAllUnQualifiedStockOutRecordTypeOptions().then(res => {
+ stockRecordTypeOptions.value = res.data;
+ });
+ };
+
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter(item => item.id);
+ console.log("selection", selectedRows.value);
+ };
+ const expandedRowKeys = ref([]);
+
+ const handleBatchApprove = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+ ElMessageBox.confirm("璇烽�夋嫨瀹℃壒缁撴灉", "瀹℃壒", {
+ confirmButtonText: "閫氳繃",
+ cancelButtonText: "椹冲洖",
+ type: "warning",
+ distinguishCancelAndClose: true,
+ })
+ .then(() => {
+ batchApproveStockOutRecords({ ids, approvalStatus: 1 })
+ .then(() => {
+ proxy.$modal.msgSuccess("瀹℃壒閫氳繃鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("瀹℃壒閫氳繃澶辫触");
+ });
+ })
+ .catch(action => {
+ if (action === "cancel") {
+ batchApproveStockOutRecords({ ids, approvalStatus: 2 })
+ .then(() => {
+ proxy.$modal.msgSuccess("瀹℃壒椹冲洖鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("瀹℃壒椹冲洖澶辫触");
+ });
+ return;
+ }
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(
+ "/stockOutRecord/exportStockOutRecord",
+ { type: props.type },
+ props.type === "0" ? "鍚堟牸鍑哄簱鍙拌处.xlsx" : "涓嶅悎鏍煎嚭搴撳彴璐�.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delPendingStockOut(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鎵撳嵃鍔熻兘
+ const handlePrint = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨瑕佹墦鍗扮殑鏁版嵁");
+ return;
+ }
+ printData.value = [...selectedRows.value];
+ console.log("鎵撳嵃鏁版嵁:", printData.value);
+ printPreviewVisible.value = true;
+ };
+
+ // 鎵ц鎵撳嵃
+ const executePrint = () => {
+ console.log("寮�濮嬫墽琛屾墦鍗帮紝鏁版嵁鏉℃暟:", printData.value.length);
+ console.log("鎵撳嵃鏁版嵁:", printData.value);
+
+ // 鍒涘缓涓�涓柊鐨勬墦鍗扮獥鍙�
+ const printWindow = window.open("", "_blank", "width=800,height=600");
+
+ // 鏋勫缓鎵撳嵃鍐呭
+ let printContent = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="UTF-8">
+ <title>鎵撳嵃棰勮</title>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ font-family: "SimSun", serif;
+ background: white;
+ }
+ .print-page {
+ width: 200mm;
+ height: 75mm;
+ padding: 10mm;
+ padding-left: 20mm;
+ background: white;
+ box-sizing: border-box;
+ page-break-after: always;
+ page-break-inside: avoid;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+ .delivery-note {
+ width: 100%;
+ height: 100%;
+ font-size: 12px;
+ line-height: 1.2;
+ display: flex;
+ flex-direction: column;
+ color: #000;
+ }
+ .header {
+ text-align: center;
+ margin-bottom: 8px;
+ }
+ .company-name {
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+ .document-title {
+ font-size: 16px;
+ font-weight: bold;
+ }
+ .info-section {
+ margin-bottom: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .info-row {
+ line-height: 20px;
+ }
+ .label {
+ font-weight: bold;
+ width: 60px;
+ font-size: 12px;
+ }
+ .value {
+ margin-right: 20px;
+ min-width: 80px;
+ font-size: 12px;
+ }
+ .table-section {
+ margin-bottom: 40px;
+ // flex: 0.6;
+ }
+ .product-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid #000;
+ }
+ .product-table th, .product-table td {
+ border: 1px solid #000;
+ padding: 6px;
+ text-align: center;
+ font-size: 12px;
+ line-height: 1.4;
+ }
+ .product-table th {
+ font-weight: bold;
+ }
+ .total-value {
+ font-weight: bold;
+ }
+ .footer-section {
+ margin-top: auto;
+ }
+ .footer-row {
+ display: flex;
+ margin-bottom: 3px;
+ line-height: 22px;
+ justify-content: space-between;
+ }
+ .footer-item {
+ display: flex;
+ margin-right: 20px;
+ }
+ .footer-item .label {
+ font-weight: bold;
+ width: 80px;
+ font-size: 12px;
+ }
+ .footer-item .value {
+ min-width: 80px;
+ font-size: 12px;
+ }
+ .address-item .address-value {
+ min-width: 200px;
+ }
+ @media print {
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ .print-page {
+ margin: 0;
+ padding: 10mm;
+ /* padding-left: 20mm; */
+ page-break-inside: avoid;
+ page-break-after: always;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ `;
+
+ // 涓烘瘡鏉℃暟鎹敓鎴愭墦鍗伴〉闈�
+ printData.value.forEach((item, index) => {
+ printContent += `
+ <div class="print-page">
+ <div class="delivery-note">
+ <div class="header">
+ <div class="document-title">闆跺敭鍙戣揣鍗�</div>
+ </div>
+
+ <div class="info-section">
+ <div class="info-row">
+ <div>
+ <span class="label">鍙戣揣鏃ユ湡锛�</span>
+ <span class="value">${formatDate(item.createTime)}</span>
+ </div>
+ <div>
+ <span class="label">瀹㈡埛鍚嶇О锛�</span>
+ <span class="value">${item.supplierName}</span>
+ </div>
+ </div>
+ <div class="info-row">
+ <span class="label">鍗曞彿锛�</span>
+ <span class="value">${item.code || ""}</span>
+ </div>
+ </div>
+
+ <div class="table-section">
+ <table class="product-table">
+ <thead>
+ <tr>
+ <th>浜у搧鍚嶇О</th>
+ <th>瑙勬牸鍨嬪彿</th>
+ <th>鍗曚綅</th>
+ <th>鍗曚环</th>
+ <th>闆跺敭鏁伴噺</th>
+ <th>闆跺敭閲戦</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>${item.productName || "鐮傜伆鐮�"}</td>
+ <td>${item.model || "鏍囧噯"}</td>
+ <td>${item.unit || "鍧�"}</td>
+ <td>${item.taxInclusiveUnitPrice || "0"}</td>
+ <td>${item.inboundNum || "2000"}</td>
+ <td>${item.taxInclusiveTotalPrice || "0"}</td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td class="label">鍚堣</td>
+ <td class="total-value"></td>
+ <td class="total-value"></td>
+ <td class="total-value"></td>
+ <td class="total-value">${item.inboundNum || "2000"}</td>
+ <td class="total-value">${
+ item.taxInclusiveTotalPrice || "0"
+ }</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+
+ <div class="footer-section">
+ <div class="footer-row">
+ <div class="footer-item">
+ <span class="label">鏀惰揣鐢佃瘽锛�</span>
+ <span class="value"></span>
+ </div>
+ <div class="footer-item">
+ <span class="label">鏀惰揣浜猴細</span>
+ <span class="value"></span>
+ </div>
+ <div class="footer-item address-item">
+ <span class="label">鏀惰揣鍦板潃锛�</span>
+ <span class="value address-value"></span>
+ </div>
+ </div>
+ <div class="footer-row">
+ <div class="footer-item">
+ <span class="label">鎿嶄綔鍛橈細</span>
+ <span class="value">${userStore.nickName || "鎾曞紑鍓�"}</span>
+ </div>
+ <div class="footer-item">
+ <span class="label">鎵撳嵃鏃ユ湡锛�</span>
+ <span class="value">${formatDateTime(new Date())}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ `;
+ });
+
+ printContent += `
+ </body>
+ </html>
+ `;
+
+ // 鍐欏叆鍐呭鍒版柊绐楀彛
+ printWindow.document.write(printContent);
+ printWindow.document.close();
+
+ // 绛夊緟鍐呭鍔犺浇瀹屾垚鍚庢墦鍗�
+ printWindow.onload = () => {
+ setTimeout(() => {
+ printWindow.print();
+ printWindow.close();
+ printPreviewVisible.value = false;
+ }, 500);
+ };
+ };
+
+ // 鏍煎紡鍖栨棩鏈�
+ const formatDate = dateString => {
+ if (!dateString) return getCurrentDate();
+ const date = new Date(dateString);
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}/${month}/${day}`;
+ };
+
+ // 鏍煎紡鍖栨棩鏈熸椂闂�
+ const formatDateTime = date => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ const hours = String(date.getHours()).padStart(2, "0");
+ const minutes = String(date.getMinutes()).padStart(2, "0");
+ const seconds = String(date.getSeconds()).padStart(2, "0");
+ return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
+ };
+ onMounted(() => {
+ getList();
+ fetchStockRecordTypeOptions();
+ });
+
+ watch(
+ () => props.topParentProductId,
+ () => {
+ page.current = 1;
+ getList();
+ }
+ );
+</script>
+
+<style scoped lang="scss">
+ .print-preview-dialog {
+ .el-dialog__body {
+ padding: 0;
+ max-height: 80vh;
+ overflow-y: auto;
+ }
+ }
+
+ .print-preview-container {
+ .print-preview-header {
+ padding: 15px;
+ border-bottom: 1px solid #e4e7ed;
+ text-align: center;
+
+ .el-button {
+ margin: 0 10px;
+ }
+ }
+
+ .print-preview-content {
+ padding: 20px;
+ background-color: #f5f5f5;
+ min-height: 400px;
+ }
+ }
+
+ .print-page {
+ width: 220mm;
+ height: 90mm;
+ padding: 10mm;
+ margin: 0 auto;
+ background: white;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ margin-bottom: 10px;
+ box-sizing: border-box;
+ }
+
+ .delivery-note {
+ width: 100%;
+ height: 100%;
+ font-family: "SimSun", serif;
+ font-size: 10px;
+ line-height: 1.2;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .header {
+ text-align: center;
+ margin-bottom: 8px;
+
+ .company-name {
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+
+ .document-title {
+ font-size: 16px;
+ font-weight: bold;
+ }
+ }
+
+ .info-section {
+ margin-bottom: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .info-row {
+ line-height: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 60px;
+ font-size: 14px;
+ }
+
+ .value {
+ margin-right: 20px;
+ min-width: 80px;
+ font-size: 14px;
+ }
+ }
+ }
+
+ .table-section {
+ margin-bottom: 4px;
+ flex: 1;
+
+ .product-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid #000;
+
+ th,
+ td {
+ border: 1px solid #000;
+ padding: 6px;
+ text-align: center;
+ font-size: 14px;
+ line-height: 1.4;
+ }
+
+ th {
+ font-weight: bold;
+ }
+
+ .total-label {
+ text-align: right;
+ font-weight: bold;
+ }
+
+ .total-value {
+ font-weight: bold;
+ }
+ }
+ }
+
+ .footer-section {
+ .footer-row {
+ display: flex;
+ margin-bottom: 3px;
+ line-height: 20px;
+ justify-content: space-between;
+
+ .footer-item {
+ display: flex;
+ margin-right: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 80px;
+ font-size: 14px;
+ }
+
+ .value {
+ min-width: 80px;
+ font-size: 14px;
+ }
+
+ &.address-item {
+ .address-value {
+ min-width: 200px;
+ }
+ }
+ }
+ }
+ }
+
+ @media print {
+ .app-container {
+ display: none;
+ }
+
+ .print-page {
+ box-shadow: none;
+ margin: 0;
+ padding: 10mm;
+ padding-left: 20mm;
+ page-break-inside: avoid;
+ page-break-after: always;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+ }
+
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+</style>
diff --git a/src/views/inventoryManagement/dispatchLog/index.vue b/src/views/inventoryManagement/dispatchLog/index.vue
new file mode 100644
index 0000000..458e9ec
--- /dev/null
+++ b/src/views/inventoryManagement/dispatchLog/index.vue
@@ -0,0 +1,55 @@
+<!-- 鍦ㄤ綘鐨勪富椤甸潰涓� -->
+<template>
+ <div class="app-container">
+ <div v-loading="loading" element-loading-text="鍔犺浇涓�..." style="min-height: 80vh;">
+ <el-tabs v-model="activeTab" @tab-change="handleTabChange" v-if="!loading">
+ <el-tab-pane v-for="tab in tabs"
+ :label="tab.productName"
+ :name="tab.id"
+ :key="tab.id">
+ <Record v-bind="{ type: tab.type, topParentProductId: activeTab }" v-if="tab.id === activeTab" />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { productTreeList } from "@/api/basicData/product.js";
+import Record from "@/views/inventoryManagement/dispatchLog/Record.vue";
+const activeTab = ref(null)
+const tabs = ref([])
+const loading = ref(false)
+
+const resolveTypeByName = (name) => {
+ return String(name || "").includes("涓嶅悎鏍�") ? "1" : "0";
+};
+
+const handleTabChange = (tabName) => {
+ activeTab.value = tabName;
+}
+
+const fetchProducts = async () => {
+ loading.value = true;
+ try {
+ const res = await productTreeList();
+ tabs.value = res
+ .filter((item) => item.parentId === null)
+ .map(({ id, productName }) => ({
+ id,
+ productName,
+ type: resolveTypeByName(productName),
+ }));
+ if (tabs.value.length > 0) {
+ activeTab.value = tabs.value[0].id;
+ }
+ } finally {
+ loading.value = false;
+ }
+}
+
+onMounted(() => {
+ fetchProducts();
+})
+</script>
diff --git a/src/views/inventoryManagement/index.vue b/src/views/inventoryManagement/index.vue
new file mode 100644
index 0000000..f371e26
--- /dev/null
+++ b/src/views/inventoryManagement/index.vue
@@ -0,0 +1,309 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">鍙戞斁瀛e害锛�</span>
+ <el-select
+ style="width: 200px;"
+ @change="handleQuery"
+ v-model="searchForm.season"
+ placeholder="璇烽�夋嫨"
+ :clearable="false"
+ >
+ <el-option :label="item.label" :value="item.value" v-for="(item,index) in jidu" :key="item.value" />
+ </el-select>
+ <span class="search_title ml10">鍛樺伐鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.staffName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus"> 鏂板 </el-button>
+ <el-button @click="handleOut" icon="download">瀵煎嚭</el-button>
+ <el-button
+ type="danger"
+ icon="Delete"
+ :disabled="multipleList.length <= 0"
+ @click="deleteRow(multipleList.map((item) => item.id))"
+ >
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <el-table
+ ref="tableRef"
+ v-loading="tableLoading"
+ :data="tableData"
+ border
+ height="calc(100vh - 21em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ style="width: 100%"
+ @selection-change="handleSelectionChange"
+ >
+ <!-- 閫夋嫨鍒� -->
+ <el-table-column
+ align="center"
+ type="selection"
+ width="55"
+ fixed="left"
+ />
+
+ <!-- 搴忓彿鍒� -->
+ <el-table-column
+ align="center"
+ label="搴忓彿"
+ type="index"
+ width="60"
+ fixed="left"
+ />
+
+ <!-- 鍥哄畾鍒楋細濮撳悕 -->
+ <el-table-column
+ label="濮撳悕"
+ prop="staffName"
+ width="100"
+ show-overflow-tooltip
+ align="center"
+ fixed="left"
+ />
+
+ <!-- 鍥哄畾鍒楋細宸ュ彿 -->
+ <el-table-column
+ label="宸ュ彿"
+ prop="staffNo"
+ width="100"
+ show-overflow-tooltip
+ align="center"
+ fixed="left"
+ />
+
+ <!-- 鍔ㄦ�佸垪锛氭牴鎹瓧鍏告覆鏌� -->
+ <el-table-column
+ v-for="(dictItem, index) in sys_lavor_issue"
+ :key="dictItem.value"
+ :label="dictItem.label"
+ :prop="dictItem.value"
+ show-overflow-tooltip
+ >
+ </el-table-column>
+
+ <!-- 鎿嶄綔鍒� -->
+ <el-table-column
+ label="鎿嶄綔"
+ width="150"
+ align="center"
+ fixed="right"
+ >
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ size="small"
+ @click="edit(scope.row)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="danger"
+ link
+ size="small"
+ :disabled="!!scope.row.adoptedDate"
+ @click="adopted(scope.row)"
+ >
+ 棰嗙敤
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination :total="total" layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current" :limit="page.size" @pagination="paginationChange" />
+ </div>
+ <!-- <Modal ref="modalRef" @success="handleQuery"></Modal> -->
+ <!-- <files-dia ref="filesDia"></files-dia> -->
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, reactive, toRefs, nextTick, getCurrentInstance } from 'vue'
+import dayjs from "dayjs";
+// import Modal from "./Modal.vue";
+// import FilesDia from "./filesDia.vue";
+import Pagination from "@/components/Pagination/index.vue";
+import {listPage, deleteLedger, update} from "@/api/lavorissce/ledger.js";
+import {ElMessageBox, ElMessage} from "element-plus";
+const { proxy } = getCurrentInstance();
+import { getCurrentMonth } from "@/utils/util"
+
+const page = ref({
+ current: 1,
+ size: 100,
+})
+const total = ref(0)
+// 鍝嶅簲寮忔暟鎹�
+const tableRef = ref(null)
+const tableData = ref([])
+const tableLoading = ref(false)
+const { sys_lavor_issue } = proxy.useDict("sys_lavor_issue")
+const data = reactive({
+ searchForm: {
+ season: "",
+ staffName: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const modalRef = ref();
+// const filesDia = ref();
+const multipleList = ref([]);
+const jidu = ref([
+ {
+ value: '1',
+ label: '绗竴瀛e害'
+ },
+ {
+ value: '2',
+ label: '绗簩瀛e害'
+ },
+ {
+ value: '3',
+ label: '绗笁瀛e害'
+ },
+ {
+ value: '4',
+ label: '绗洓瀛e害'
+ }
+])
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+};
+// 鑾峰彇瀛楀吀鏁版嵁
+const getList = async () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page.value };
+ listPage(params).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ total.value = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+}
+const add = () => {
+ modalRef.value.openModal();
+};
+const edit = (row) => {
+ modalRef.value.loadForm(row);
+};
+const deleteRow = (id) => {
+ ElMessageBox.confirm("姝ゆ搷浣滃皢姘镐箙鍒犻櫎璇ユ暟鎹�, 鏄惁缁х画?", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ const { code } = await deleteLedger(id);
+ if (code == 200) {
+ ElMessage({
+ type: "success",
+ message: "鍒犻櫎鎴愬姛",
+ });
+ await getList();
+ }
+ });
+};
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(`/lavorIssue/exportCopy`, {season: searchForm.value.season}, "鍔充繚鍙拌处.xlsx");
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑�");
+ });
+};
+const adopted = (row) => {
+ ElMessageBox.confirm("鏄惁纭棰嗙敤?", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ const params = {
+ id: row.id,
+ adoptedDate: dayjs().format("YYYY-MM-DD")
+ }
+ const { code } = await update(params);
+ if (code == 200) {
+ ElMessage({
+ type: "success",
+ message: "棰嗙敤鎴愬姛",
+ });
+ await getList();
+ }
+ })
+}
+// 鎵撳紑闄勪欢寮规
+// const openFilesFormDia = (row) => {
+// nextTick(() => {
+// filesDia.value?.openDialog( row,'鏀跺叆')
+// })
+// };
+// 浜嬩欢澶勭悊鍑芥暟
+const handleSelectionChange = (selection) => {
+ multipleList.value = selection;
+}
+
+const paginationChange = (pagination) => {
+ page.value.current = pagination.page;
+ page.value.size = pagination.limit;
+ getList();
+}
+
+// 缁勪欢鎸傝浇鏃跺姞杞藉瓧鍏告暟鎹�
+onMounted(() => {
+ handleQuery()
+})
+</script>
+
+<style scoped>
+.dynamic-table-container {
+ width: 100%;
+}
+
+.pagination-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+:deep(.el-table .el-table__header-wrapper th) {
+ background-color: #F0F1F5 !important;
+ color: #333333;
+ font-weight: 600;
+}
+
+:deep(.el-table .el-table__body-wrapper td) {
+ padding: 8px 0;
+}
+
+:deep(.el-select) {
+ width: 100%;
+}
+
+:deep(.el-input) {
+ width: 100%;
+}
+</style>
diff --git a/src/views/inventoryManagement/issueManagement/index.vue b/src/views/inventoryManagement/issueManagement/index.vue
new file mode 100644
index 0000000..2bba7d0
--- /dev/null
+++ b/src/views/inventoryManagement/issueManagement/index.vue
@@ -0,0 +1,285 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">渚涘簲鍟嗗悕绉帮細</span>
+ <el-input v-model="searchForm.supplierName" style="width: 240px" placeholder="璇疯緭鍏�" @change="handleQuery"
+ clearable prefix-icon="Search" />
+ <span class="search_title ml10">鍏ュ簱鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.timeStr"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ @change="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <!-- <el-button type="primary" @click="openForm('add')">鏂板鍑哄簱</el-button> -->
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <!-- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button> -->
+ </div>
+ </div>
+ <div class="table_list">
+ <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys" :row-key="row => row.id" show-summary style="width: 100%"
+ :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)">
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="鍏ュ簱鏃堕棿" prop="createTime" width="100" show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱鎵规" prop="inboundBatches" width="160" show-overflow-tooltip />
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" width="240" show-overflow-tooltip />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" width="100" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" width="200" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="70" show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱鏁伴噺" prop="inboundNum" width="90" show-overflow-tooltip />
+ <el-table-column label="搴撳瓨鏁伴噺" prop="inboundNum0" width="90" show-overflow-tooltip />
+ <el-table-column label="鍚◣鍗曚环" prop="taxInclusiveUnitPrice" width="100" show-overflow-tooltip />
+ <el-table-column label="鍚◣鎬讳环" prop="taxInclusiveTotalPrice" width="100" show-overflow-tooltip />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" width="80" show-overflow-tooltip />
+ <el-table-column label="涓嶅惈绋庢�讳环" prop="taxExclusiveTotalPrice" width="100" show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱浜�" prop="createBy" width="80" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" min-width="60" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="openForm(scope.row);">棰嗙敤</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current" :limit="page.size" @pagination="paginationChange" />
+ </div>
+ <el-dialog v-model="dialogFormVisible" :title="'鏂板鍑哄簱'" width="40%" @close="closeDia">
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-form-item label="鍑哄簱鏁伴噺锛�" prop="salesContractNo">
+ <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.inboundQuantity" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ <el-form-item label="鍑哄簱鏃ユ湡锛�" prop="projectName">
+ <el-date-picker style="width: 100%" v-model="form.inboundTime" value-format="YYYY-MM-DD" format="YYYY-MM-DD"
+ type="date" placeholder="璇烽�夋嫨" clearable />
+ </el-form-item>
+ <el-form-item label="鍑哄簱浜猴細" prop="entryPerson">
+ <el-select v-model="form.nickName" placeholder="璇烽�夋嫨" clearable>
+ <el-option v-for="item in userList" :key="item.userId" :label="item.nickName" :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import pagination from '@/components/PIMTable/Pagination.vue'
+import { ref } from 'vue'
+import { ElMessageBox } from "element-plus";
+import useUserStore from '@/store/modules/user'
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import {
+ getStockInPage
+} from "@/api/inventoryManagement/stockIn.js";
+import {
+ getStockManagePage,
+ delStockManage,
+ stockOut,
+} from "@/api/inventoryManagement/stockManage.js";
+
+const userStore = useUserStore()
+const { proxy } = getCurrentInstance()
+const tableData = ref([])
+const selectedRows = ref([])
+const userList = ref([])
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 100,
+})
+const total = ref(0)
+const fileList = ref([])
+
+// 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+const dialogFormVisible = ref(false)
+const data = reactive({
+ searchForm: {
+ supplierName: '',
+ inboundQuantity:'',
+ inboundTime:'',
+ nickName: '',
+ userId: '',
+ timeStr: '',
+ },
+ form: {
+ productrecordId: '',
+ },
+ rules: {
+ inboundTime: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ inboundQuantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ nickname: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }]
+ }
+})
+const { searchForm, form, rules } = toRefs(data)
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1
+ getList()
+}
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList()
+}
+const getList = () => {
+ tableLoading.value = true
+ getStockInPage({ ...searchForm.value, ...page }).then(res => {
+ tableLoading.value = false
+ tableData.value = res.data.records
+ total.value = res.data.total
+ console.log('res', res.data.records)
+ }).catch(() => {
+ tableLoading.value = false
+ })
+}
+
+const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥炶鑺傜偣
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId);
+ if (foundNode) {
+ return foundNode.label; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝杩斿洖璇ヨ妭鐐�
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter(item => item.id);
+ console.log('selection', selectedRows.value)
+}
+const expandedRowKeys = ref([])
+
+// 涓昏〃鍚堣鏂规硶
+const summarizeMainTable = (param) => {
+ return proxy.summarizeTable(param, ['contractAmount', 'taxInclusiveTotalPrice', 'taxExclusiveTotalPrice']);
+};
+const currentRowId = ref(null) // 鏂板锛氬瓨鍌ㄥ綋鍓嶆搷浣滅殑琛孖D
+
+const currentRowNum = ref(0)
+const salesLedgerProductId = ref(null);
+
+// 鎵撳紑寮规
+const openForm = async (row) => {
+ dialogFormVisible.value = true
+ currentRowId.value = row.id
+ currentRowNum.value = row.inboundNum0
+ salesLedgerProductId.value = row.salesLedgerProductId
+ form.value = {}
+ // 鍒濆鍖栬〃鍗曟暟鎹�
+ form.value = {
+ productrecordId: '',
+ inboundQuantity: '', // 鍑哄簱鏁伴噺娓呯┖
+ inboundTime: getCurrentDate(), // 榛樿褰撳墠鏃ユ湡
+ nickName: '', // 榛樿褰撳墠鐢ㄦ埛
+ }
+ console.log('form',form.value)
+ // 鍔犺浇鐢ㄦ埛鍒楄〃
+ try {
+ const userLists = await userListNoPageByTenantId()
+ userList.value = userLists.data
+ } catch (error) {
+ console.error('鍔犺浇鐢ㄦ埛鍒楄〃澶辫触:', error)
+ }
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ let num = Number(form.value.inboundQuantity)
+ if(num <= 0 || num > currentRowNum.value){
+ return proxy.$modal.msgWarning("璇峰~鍏ユ湁鏁堟暟瀛�")
+ }
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid && currentRowId.value) {
+ const outData = {
+ id: currentRowId.value, // 鍘熷璁板綍ID
+ salesLedgerProductId: salesLedgerProductId.value,
+ quantity: form.value.inboundQuantity, // 鍑哄簱鏁伴噺
+ time: form.value.inboundTime, // 鍑哄簱鏃堕棿
+ userId: form.value.nickName // 鎿嶄綔浜�
+ }
+ console.log(outData)
+
+ stockOut(outData).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛")
+ closeDia()
+ getList()
+ }).catch(err => {
+ proxy.$modal.msgError("鍑哄簱澶辫触")
+ })
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef")
+ dialogFormVisible.value = false
+}
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm(
+ '鏄惁纭瀵煎嚭锛�',
+ '瀵煎嚭', {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }
+ ).then(() => {
+ proxy.download("/stockin/export", {}, '鍏ュ簱鍙拌处.xlsx')
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�")
+ })
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = []
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ ElMessageBox.confirm(
+ '閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�',
+ '瀵煎嚭', {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }
+ ).then(() => {
+ delStockManage(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ getList()
+ })
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�")
+ })
+}
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/inventoryManagement/receiptManagement/Record.vue b/src/views/inventoryManagement/receiptManagement/Record.vue
new file mode 100644
index 0000000..0a01def
--- /dev/null
+++ b/src/views/inventoryManagement/receiptManagement/Record.vue
@@ -0,0 +1,514 @@
+<template>
+ <div>
+ <div class="search_form"
+ style="margin-bottom: 10px;">
+ <el-form ref="searchFormRef"
+ :model="searchForm"
+ class="demo-form-inline">
+ <el-row :gutter="20">
+ <el-col :span="4">
+ <el-form-item label="鍏ュ簱鏃ユ湡"
+ prop="timeStr">
+ <el-date-picker v-model="searchForm.timeStr"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="浜у搧澶х被"
+ prop="productName">
+ <el-input v-model="searchForm.productName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="瑙勬牸鍨嬪彿"
+ prop="model">
+ <el-input v-model="searchForm.model"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鎵瑰彿"
+ prop="batchNo">
+ <el-input v-model="searchForm.batchNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鏉ユ簮"
+ prop="recordType">
+ <el-select v-model="searchForm.recordType"
+ style="width: 240px"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in stockRecordTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <!-- 鎸夐挳 -->
+ <el-col :span="4">
+ <el-form-item>
+ <el-button type="primary"
+ @click="getList">
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+ <div class="actions">
+ <el-button type="primary"
+ :disabled="!canBatchApprove"
+ @click="handleBatchApprove">瀹℃壒</el-button>
+ <el-button :disabled="!canReverseApprove"
+ @click="handleReverseApprove">鍙嶅</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ :disabled="!canDelete"
+ @click="handleDelete">鍒犻櫎
+ </el-button>
+ </div>
+ <div class="table_list">
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys"
+ :row-key="row => row.id"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)">
+ <el-table-column align="center"
+ type="selection"
+ :selectable="isRowSelectable"
+ width="55" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鍏ュ簱鎵规"
+ prop="inboundBatches"
+ width="200"
+ show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱鏃堕棿"
+ prop="createTime"
+ show-overflow-tooltip />
+ <el-table-column label="浜у搧澶х被"
+ prop="productName"
+ show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model"
+ show-overflow-tooltip />
+ <el-table-column label="鎵瑰彿"
+ prop="batchNo"
+ show-overflow-tooltip />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱鏁伴噺"
+ prop="stockInNum"
+ show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱浜�"
+ prop="createBy"
+ show-overflow-tooltip />
+ <el-table-column label="鏉ユ簮"
+ prop="recordType"
+ show-overflow-tooltip>
+ <template #default="scope">
+ {{ getRecordType(scope.row.recordType) }}
+ </template>
+ </el-table-column>
+ <el-table-column v-if="showSourceOrderNoColumn"
+ label="婧愬崟鍙�"
+ width="150"
+ prop="sourceOrderNo"
+ show-overflow-tooltip>
+ <template #default="scope">
+ {{ formatSourceOrderNo(scope.row?.sourceOrderNo) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹℃壒鐘舵��"
+ prop="approvalStatus"
+ show-overflow-tooltip>
+ <template #default="scope">
+ <el-tag :type="getApprovalStatusTagType(scope.row.approvalStatus)"
+ size="small">
+ {{ getApprovalStatusLabel(scope.row.approvalStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="pageProductChange" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import pagination from "@/components/PIMTable/Pagination.vue";
+ import {
+ ref,
+ reactive,
+ toRefs,
+ computed,
+ onMounted,
+ getCurrentInstance,
+ } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ getStockInRecordListPage,
+ batchDeletePendingStockInRecords,
+ batchApproveStockInRecords,
+ batchUnapproveStockInRecords,
+ } from "@/api/inventoryManagement/stockInRecord.js";
+ import {
+ findAllQualifiedStockInRecordTypeOptions,
+ // findAllUnQualifiedStockInRecordTypeOptions,
+ } from "@/api/basicData/enum.js";
+
+ const { proxy } = getCurrentInstance();
+
+ const props = defineProps({
+ type: {
+ type: String,
+ required: true,
+ default: "0",
+ },
+ topParentProductId: {
+ type: [String, Number],
+ default: undefined,
+ },
+ });
+
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ // 鏉ユ簮绫诲瀷閫夐」
+ const stockRecordTypeOptions = ref([]);
+ const page = reactive({
+ current: 1,
+ size: 10,
+ });
+ const total = ref(0);
+
+ const data = reactive({
+ searchForm: {
+ productName: "",
+ batchNo: "",
+ model: "",
+ timeStr: "",
+ recordType: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+ const searchFormRef = ref(null);
+
+ const resetSearch = () => {
+ searchFormRef.value?.resetFields();
+ page.current = 1;
+ getList();
+ };
+
+ const getRecordType = recordType => {
+ return (
+ stockRecordTypeOptions.value.find(item => item.value === recordType)
+ ?.label || ""
+ );
+ };
+
+ const approvalStatusLabelMap = {
+ 0: "寰呭鎵�",
+ 1: "閫氳繃",
+ 2: "椹冲洖",
+ pending: "寰呭鎵�",
+ approved: "閫氳繃",
+ rejected: "椹冲洖",
+ PENDING: "寰呭鎵�",
+ APPROVED: "閫氳繃",
+ REJECTED: "椹冲洖",
+ };
+ approvalStatusLabelMap[3] = "寰呯‘璁�";
+
+ const getApprovalStatusLabel = status => {
+ if (status === null || status === undefined || status === "") {
+ return "寰呭鎵�";
+ }
+ return approvalStatusLabelMap[status] || "寰呭鎵�";
+ };
+
+ // 閫氳繃/椹冲洖鍥哄畾鑹诧紱鍏朵綑锛堝惈寰呭鎵广�佺┖鍊笺�佹湭鏄犲皠浣嗘枃妗堜负寰呭鎵癸級缁熶竴鐢� warning 棰勮鑹�
+ const getApprovalStatusTagType = status => {
+ if (
+ status === 1 ||
+ status === "1" ||
+ status === "approved" ||
+ status === "APPROVED"
+ )
+ return "success";
+ if (
+ status === 2 ||
+ status === "2" ||
+ status === "rejected" ||
+ status === "REJECTED"
+ )
+ return "danger";
+ return "warning";
+ };
+
+ const isPendingApproval = status => {
+ return (
+ status === 0 ||
+ status === "0" ||
+ status === "pending" ||
+ status === "PENDING" ||
+ status === null ||
+ status === undefined ||
+ status === ""
+ );
+ };
+
+ const isRejectedApproval = status => {
+ return (
+ status === 2 ||
+ status === "2" ||
+ status === "rejected" ||
+ status === "REJECTED"
+ );
+ };
+
+ const isRowSelectable = row => {
+ return (
+ isPendingApproval(row?.approvalStatus) ||
+ isRejectedApproval(row?.approvalStatus)
+ );
+ };
+
+ const canBatchApprove = computed(() => {
+ return (
+ selectedRows.value.length > 0 &&
+ selectedRows.value.every(row => isPendingApproval(row.approvalStatus))
+ );
+ });
+
+ const canReverseApprove = computed(() => {
+ return (
+ selectedRows.value.length > 0 &&
+ selectedRows.value.every(row => isRejectedApproval(row.approvalStatus))
+ );
+ });
+
+ const canDelete = computed(() => canBatchApprove.value);
+ const showSourceOrderNoColumn = computed(() => {
+ const topParentProductId = Number(props.topParentProductId);
+ return topParentProductId === 276 || topParentProductId === 278;
+ });
+
+ const formatSourceOrderNo = value => {
+ const text = String(value ?? "").trim();
+ return text || "--";
+ };
+
+ const pageProductChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ getStockInRecordListPage(
+ Object.assign(
+ {},
+ {
+ ...searchForm.value,
+ ...page,
+ topParentProductId: props.topParentProductId,
+ }
+ )
+ )
+ .then(res => {
+ tableData.value = res.data.records;
+ total.value = res.data.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鑾峰彇鏉ユ簮绫诲瀷閫夐」
+ const fetchStockRecordTypeOptions = () => {
+ if (props.type === "0") {
+ findAllQualifiedStockInRecordTypeOptions().then(res => {
+ stockRecordTypeOptions.value = res.data;
+ });
+ return;
+ }
+ // findAllUnQualifiedStockInRecordTypeOptions()
+ // .then(res => {
+ // stockRecordTypeOptions.value = res.data;
+ // })
+ };
+
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection.filter(
+ item => item.id && isRowSelectable(item)
+ );
+ };
+
+ const expandedRowKeys = ref([]);
+
+ const handleReverseApprove = () => {
+ if (!canReverseApprove.value) {
+ proxy.$modal.msgWarning("璇烽�夋嫨宸查┏鍥炵殑鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+ ElMessageBox.confirm("鍙嶅鍚庤褰曞皢鎭㈠涓哄緟瀹℃壒鐘舵�侊紝鏄惁纭鍙嶅锛�", "鍙嶅", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchUnapproveStockInRecords({ ids })
+ .then(() => {
+ proxy.$modal.msgSuccess("鍙嶅鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鍙嶅澶辫触");
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ const handleBatchApprove = () => {
+ if (!canBatchApprove.value) {
+ proxy.$modal.msgWarning("璇烽�夋嫨寰呭鎵圭殑鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+ ElMessageBox.confirm("璇烽�夋嫨瀹℃壒缁撴灉", "瀹℃壒", {
+ confirmButtonText: "閫氳繃",
+ cancelButtonText: "椹冲洖",
+ type: "warning",
+ distinguishCancelAndClose: true,
+ })
+ .then(() => {
+ batchApproveStockInRecords({ ids, approvalStatus: 1 })
+ .then(() => {
+ proxy.$modal.msgSuccess("瀹℃壒閫氳繃鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("瀹℃壒閫氳繃澶辫触");
+ });
+ })
+ .catch(action => {
+ if (action === "cancel") {
+ batchApproveStockInRecords({ ids, approvalStatus: 2 })
+ .then(() => {
+ proxy.$modal.msgSuccess("瀹℃壒椹冲洖鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("瀹℃壒椹冲洖澶辫触");
+ });
+ return;
+ }
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 鏍规嵁涓嶅悓鐨� tab 绫诲瀷璋冪敤涓嶅悓鐨勫鍑烘帴鍙�
+ proxy.download(
+ "/stockInRecord/exportStockInRecord",
+ { type: props.type },
+ props.type === "0" ? "鍚堟牸鍏ュ簱.xlsx" : "涓嶅悎鏍煎叆搴�.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鍒犻櫎
+ const handleDelete = () => {
+ if (!canDelete.value) {
+ proxy.$modal.msgWarning("璇烽�夋嫨寰呭鎵圭殑鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchDeletePendingStockInRecords(ids)
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ fetchStockRecordTypeOptions();
+ });
+
+ watch(
+ () => props.topParentProductId,
+ () => {
+ page.current = 1;
+ getList();
+ }
+ );
+</script>
+
+<style scoped lang="scss">
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+</style>
diff --git a/src/views/inventoryManagement/receiptManagement/index.vue b/src/views/inventoryManagement/receiptManagement/index.vue
new file mode 100644
index 0000000..17a0823
--- /dev/null
+++ b/src/views/inventoryManagement/receiptManagement/index.vue
@@ -0,0 +1,55 @@
+<template>
+ <div class="app-container">
+ <div v-loading="loading" element-loading-text="鍔犺浇涓�..." style="min-height: 80vh;">
+ <el-tabs v-model="activeTab" @tab-change="handleTabChange" v-if="!loading">
+ <el-tab-pane v-for="tab in tabs"
+ :label="tab.productName"
+ :name="tab.id"
+ :key="tab.id">
+ <Record v-bind="{ type: tab.type, topParentProductId: activeTab }" v-if="tab.id === activeTab" />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { productTreeList } from "@/api/basicData/product.js";
+import Record from "@/views/inventoryManagement/receiptManagement/Record.vue";
+
+const activeTab = ref(null)
+const tabs = ref([])
+const loading = ref(false)
+
+const resolveTypeByName = (name) => {
+ return String(name || "").includes("涓嶅悎鏍�") ? "1" : "0";
+};
+
+const handleTabChange = (tabName) => {
+ activeTab.value = tabName;
+}
+
+const fetchProducts = async () => {
+ loading.value = true;
+ try {
+ const res = await productTreeList();
+ tabs.value = res
+ .filter((item) => item.parentId === null)
+ .map(({ id, productName }) => ({
+ id,
+ productName,
+ type: resolveTypeByName(productName),
+ }));
+ if (tabs.value.length > 0) {
+ activeTab.value = tabs.value[0].id;
+ }
+ } finally {
+ loading.value = false;
+ }
+}
+
+onMounted(() => {
+ fetchProducts();
+})
+</script>
diff --git a/src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue b/src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue
new file mode 100644
index 0000000..a835ef4
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/BatchNoQtyDetail.vue
@@ -0,0 +1,227 @@
+<template>
+ <el-dialog
+ v-model="isShow"
+ title="搴撳瓨璇︽儏"
+ width="90%"
+ top="3vh"
+ class="batch-no-qty-detail-dialog"
+ @close="closeModal"
+ >
+ <div class="detail-content">
+ <div class="detail-table-wrapper">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ style="width: 100%"
+ height="100%"
+ >
+ <el-table-column
+ label="浜у搧鍚嶇О"
+ prop="productName"
+ show-overflow-tooltip
+ />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" show-overflow-tooltip />
+ <el-table-column label="鎵瑰彿" prop="batchNo" show-overflow-tooltip />
+ <el-table-column
+ label="鍚堟牸搴撳瓨鏁伴噺"
+ prop="qualifiedQuantity"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="涓嶅悎鏍煎簱瀛樻暟閲�"
+ prop="unQualifiedQuantity"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="鍚堟牸鍐荤粨鏁伴噺"
+ prop="qualifiedLockedQuantity"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="涓嶅悎鏍煎喕缁撴暟閲�"
+ prop="unQualifiedLockedQuantity"
+ 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 fixed="right" label="鎿嶄綔" min-width="180" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ @click="handleSubtract(scope.row)"
+ :disabled="
+ (scope.row.qualifiedUnLockedQuantity || 0) +
+ (scope.row.qualifiedPendingOutQuantity || 0) <=
+ 0 &&
+ (scope.row.unQualifiedUnLockedQuantity || 0) +
+ (scope.row.unQualifiedPendingOutQuantity || 0) <=
+ 0
+ "
+ >棰嗙敤</el-button
+ >
+ <el-button
+ link
+ type="primary"
+ v-if="
+ scope.row.unQualifiedUnLockedQuantity > 0 ||
+ scope.row.qualifiedUnLockedQuantity > 0
+ "
+ @click="handleFrozen(scope.row)"
+ >鍐荤粨</el-button
+ >
+ <el-button
+ link
+ type="primary"
+ v-if="
+ scope.row.qualifiedLockedQuantity > 0 ||
+ scope.row.unQualifiedLockedQuantity > 0
+ "
+ @click="handleThaw(scope.row)"
+ >瑙e喕</el-button
+ >
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+ </el-dialog>
+</template>
+
+<script setup>
+import pagination from "@/components/PIMTable/Pagination.vue";
+import { computed, reactive, ref, watch } from "vue";
+import { getStockInventoryBatchNoQty } from "@/api/inventoryManagement/stockInventory.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ record: {
+ type: Object,
+ default: () => ({}),
+ },
+});
+
+const emit = defineEmits(["update:visible", "subtract", "frozen", "thaw"]);
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+});
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+const total = ref(0);
+const page = reactive({
+ current: 1,
+ size: 20,
+});
+
+const getList = () => {
+ if (!props.record?.productId || !props.record?.productModelId) {
+ tableData.value = [];
+ total.value = 0;
+ return;
+ }
+
+ tableLoading.value = true;
+ getStockInventoryBatchNoQty({
+ current: page.current,
+ size: page.size,
+ productId: props.record.productId,
+ productModelId: props.record.productModelId,
+ })
+ .then((res) => {
+ tableData.value = res.data?.records || [];
+ total.value = res.data?.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const handleSubtract = (row) => {
+ emit("subtract", row);
+};
+
+const handleFrozen = (row) => {
+ emit("frozen", row);
+};
+
+const handleThaw = (row) => {
+ emit("thaw", row);
+};
+
+const closeModal = () => {
+ isShow.value = false;
+ page.current = 1;
+ page.size = 20;
+ tableData.value = [];
+ total.value = 0;
+};
+
+watch(
+ () => props.visible,
+ (visible) => {
+ if (!visible) {
+ return;
+ }
+ page.current = 1;
+ getList();
+ },
+ { immediate: true }
+);
+</script>
+
+<style scoped lang="scss">
+.detail-content {
+ display: flex;
+ flex-direction: column;
+ height: calc(100vh - 170px);
+ min-height: 520px;
+}
+
+.detail-table-wrapper {
+ flex: 1;
+ min-height: 0;
+}
+
+:deep(.batch-no-qty-detail-dialog .el-dialog) {
+ max-width: calc(100vw - 48px);
+}
+
+:deep(.batch-no-qty-detail-dialog .el-dialog__body) {
+ padding-top: 12px;
+}
+</style>
diff --git a/src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue b/src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue
new file mode 100644
index 0000000..a7a9400
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue
@@ -0,0 +1,192 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ :title="operationType === 'frozen' ? '鍐荤粨搴撳瓨' : '瑙e喕搴撳瓨'"
+ width="800"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" ref="formRef">
+ <el-form-item
+ label="搴撳瓨绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨搴撳瓨绫诲瀷',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨搴撳瓨绫诲瀷" @change="handleChangeType">
+ <el-option label="鍚堟牸搴撳瓨" value="qualified" :disabled="(operationType === 'frozen' && props.record.qualifiedUnLockedQuantity <= 0) || (operationType === 'thaw' && props.record.qualifiedLockedQuantity <= 0)" />
+ <el-option label="涓嶅悎鏍煎簱瀛�" value="unqualified" :disabled="(operationType === 'frozen' && props.record.unQualifiedUnLockedQuantity <= 0) || (operationType === 'thaw' && props.record.unQualifiedLockedQuantity <= 0)" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ :label="operationType === 'frozen' ? '鍐荤粨鏁伴噺锛�' : '瑙e喕鏁伴噺锛�'"
+ prop="lockedQuantity"
+ >
+ <el-input-number v-model="formState.lockedQuantity" :step="1" :min="maxCount > 0 ? 1 : 0" precision="0" style="width: 100%" :max="maxCount" :disabled="maxCount < 1" />
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, computed, getCurrentInstance, onMounted} from "vue";
+import {frozenStockInventory, thawStockInventory} from "@/api/inventoryManagement/stockInventory.js";
+import {frozenStockUninventory, thawStockUninventory} from "@/api/inventoryManagement/stockUninventory.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ operationType: {
+ type: String,
+ required: true,
+ default: 'frozen',
+ },
+
+ record: {
+ type: Object,
+ default: () => {},
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ type: undefined,
+ lockedQuantity: undefined,
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ formState.value = {
+ lockedQuantity: undefined,
+ type: undefined,
+ };
+ isShow.value = false;
+};
+
+const maxCount = computed(() => {
+ // 鍐荤粨搴撳瓨鏈�澶ф暟閲忎负鏈В鍐绘暟閲�
+ if (props.operationType === 'frozen') {
+ // 鍐荤粨鍚堟牸搴撳瓨鏈�澶ф暟閲忎负鏈В鍐诲悎鏍兼暟閲�
+ if (formState.value.type === 'qualified') {
+ return Math.max(0, props.record.qualifiedUnLockedQuantity || 0)
+ }
+ // 鍐荤粨涓嶅悎鏍煎簱瀛樻渶澶ф暟閲忎负鏈В鍐讳笉鍚堟牸鏁伴噺
+ return Math.max(0, props.record.unQualifiedUnLockedQuantity || 0)
+ }
+ // 瑙e喕搴撳瓨鏈�澶ф暟閲忎负宸插喕缁撴暟閲�
+ if (formState.value.type === 'qualified') {
+ // 瑙e喕鍚堟牸搴撳瓨鏈�澶ф暟閲忎负宸插喕缁撳悎鏍兼暟閲�
+ return Math.max(0, props.record.qualifiedLockedQuantity || 0)
+ }
+ // 瑙e喕涓嶅悎鏍煎簱瀛樻渶澶ф暟閲忎负宸插喕缁撲笉鍚堟牸鏁伴噺
+ return Math.max(0, props.record.unQualifiedLockedQuantity || 0)
+})
+
+const handleChangeType = (type) => {
+ formState.value.lockedQuantity = maxCount.value;
+}
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ const data = Object.assign({}, formState.value);
+ if (formState.value.type === 'qualified') {
+ data.id = props.record.qualifiedId;
+ // 鍐荤粨
+ if (props.operationType === 'frozen') {
+ frozenStockInventory(data).then(res => {
+ if (res.code === 200) {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ }
+ })
+ } else {
+ thawStockInventory(data).then(res => {
+ if (res.code === 200) {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ }
+ })
+ }
+ } else {
+ data.id = props.record.unQualifiedId;
+ if (props.operationType === 'frozen') {
+ frozenStockUninventory(data).then(res => {
+ if (res.code === 200) {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ }
+ })
+ } else {
+ thawStockUninventory(data).then(res => {
+ if (res.code === 200) {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ }
+ })
+ }
+ }
+ }
+ })
+};
+
+onMounted(() => {
+})
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
\ No newline at end of file
diff --git a/src/views/inventoryManagement/stockManagement/Import.vue b/src/views/inventoryManagement/stockManagement/Import.vue
new file mode 100644
index 0000000..2f3ff00
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/Import.vue
@@ -0,0 +1,93 @@
+<template>
+ <el-dialog v-model="isShow" title="瀵煎叆搴撳瓨" @close="closeModal">
+ <FileUpload
+ ref="fileUploadRef"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url"
+ :disabled="upload.isUploading"
+ :showTip="true"
+ @success="handleFileSuccess"
+ :downloadTemplate="downloadTemplate"
+ />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="closeModal">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import {computed, getCurrentInstance, reactive} from "vue";
+import { getToken } from "@/utils/auth.js";
+import { FileUpload } from "@/components/Upload";
+import { ElMessage } from "element-plus";
+
+defineOptions({
+ name: "瀵煎叆搴撳瓨",
+});
+
+const { proxy } = getCurrentInstance()
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ type: {
+ type: String,
+ required: true,
+ default: 'qualified',
+ },
+});
+
+const emit = defineEmits(['update:visible', 'uploadSuccess']);
+
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const fileUploadRef = ref();
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙搴撳瓨瀵煎叆锛�
+ open: false,
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/stockInventory/importStockInventory",
+});
+
+const submitFileForm = () => {
+ fileUploadRef.value.uploadApi();
+};
+
+const handleFileSuccess = (response) => {
+ const { code, msg } = response;
+ if (code == 200) {
+ ElMessage({ message: "瀵煎叆鎴愬姛", type: "success" });
+ emit('uploadSuccess');
+ closeModal();
+ } else {
+ ElMessage({ message: msg, type: "error" });
+ }
+};
+
+const downloadTemplate = () => {
+ proxy.download("/stockInventory/downloadStockInventory", {}, "搴撳瓨瀵煎叆妯℃澘.xlsx");
+}
+
+const closeModal = () => {
+ isShow.value = false;
+};
+</script>
diff --git a/src/views/inventoryManagement/stockManagement/New.vue b/src/views/inventoryManagement/stockManagement/New.vue
new file mode 100644
index 0000000..2addb95
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/New.vue
@@ -0,0 +1,223 @@
+<template>
+ <div>
+ <el-dialog v-model="isShow"
+ title="鏂板搴撳瓨"
+ width="800"
+ @close="closeModal">
+ <el-form label-width="140px"
+ :model="formState"
+ label-position="top"
+ ref="formRef">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productModelId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨浜у搧',
+ trigger: 'change',
+ }
+ ]">
+ <el-button type="primary"
+ @click="showProductSelectDialog = true">
+ {{ formState.productName ? formState.productName : '閫夋嫨浜у搧' }}
+ </el-button>
+ </el-form-item>
+ <el-form-item label="瑙勬牸"
+ prop="productModelName">
+ <el-input v-model="formState.productModelName"
+ disabled />
+ </el-form-item>
+ <el-form-item label="鍗曚綅"
+ prop="unit">
+ <el-input v-model="formState.unit"
+ disabled />
+ </el-form-item>
+ <el-form-item label="搴撳瓨绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨搴撳瓨绫诲瀷',
+ trigger: 'change',
+ }
+ ]">
+ <el-select v-model="formState.type"
+ placeholder="璇烽�夋嫨搴撳瓨绫诲瀷">
+ <el-option label="鍚堟牸搴撳瓨"
+ value="qualified" />
+ <el-option label="涓嶅悎鏍煎簱瀛�"
+ value="unqualified" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="搴撳瓨鏁伴噺"
+ prop="qualitity">
+ <el-input-number v-model="formState.qualitity"
+ :step="1"
+ :min="1"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="鎵瑰彿"
+ prop="batchNo">
+ <el-input v-model="formState.batchNo"
+ placeholder="璇疯緭鍏ユ壒鍙�" />
+ </el-form-item>
+ <el-form-item v-if="formState.type === 'qualified'"
+ label="搴撳瓨棰勮鏁伴噺"
+ prop="warnNum">
+ <el-input-number v-model="formState.warnNum"
+ :step="1"
+ :min="0"
+ :max="formState.qualitity"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="formState.remark"
+ type="textarea" />
+ </el-form-item>
+ </el-form>
+ <!-- 浜у搧閫夋嫨寮圭獥 -->
+ <ProductSelectDialog v-model="showProductSelectDialog"
+ @confirm="handleProductSelect"
+ :top-product-parent-id="props.topProductParentId"
+ single />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, computed, watch, getCurrentInstance } from "vue";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+ import { addStockInRecordOnly } from "@/api/inventoryManagement/stockInventory.js";
+ import { createStockUnInventory } from "@/api/inventoryManagement/stockUninventory.js";
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ topProductParentId: {
+ type: Number,
+ default: undefined,
+ required: false,
+ },
+ });
+
+ const emit = defineEmits(["update:visible", "completed"]);
+
+ // 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+ const formState = ref({
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ productModelName: "",
+ unit: "",
+ type: undefined,
+ qualitity: 0,
+ batchNo: null,
+ warnNum: 0,
+ remark: "",
+ });
+
+ const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+ });
+
+ const showProductSelectDialog = ref(false);
+
+ // 鎵瑰彿涓虹┖鏃惰浆涓� null
+ watch(
+ () => formState.value.batchNo,
+ val => {
+ if (val === "") {
+ formState.value.batchNo = null;
+ }
+ }
+ );
+
+ let { proxy } = getCurrentInstance();
+
+ const closeModal = () => {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ formState.value = {
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ productModelName: "",
+ unit: "",
+ type: undefined,
+ qualitity: 0,
+ batchNo: null,
+ warnNum: 0,
+ remark: "",
+ };
+ isShow.value = false;
+ };
+
+ // 浜у搧閫夋嫨澶勭悊
+ const handleProductSelect = async products => {
+ if (products && products.length > 0) {
+ const product = products[0];
+ formState.value.productId = product.productId;
+ formState.value.productName = product.productName;
+ formState.value.productModelName = product.model;
+ formState.value.productModelId = product.id;
+ formState.value.unit = product.unit;
+ showProductSelectDialog.value = false;
+ // 瑙﹀彂琛ㄥ崟楠岃瘉鏇存柊
+ proxy.$refs["formRef"]?.validateField("productModelId");
+ }
+ };
+
+ const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ // 楠岃瘉鏄惁閫夋嫨浜嗕骇鍝佸拰瑙勬牸
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨浜у搧");
+ return;
+ }
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨瑙勬牸");
+ return;
+ }
+ if (formState.value.type === "qualified") {
+ addStockInRecordOnly(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit("completed");
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ });
+ } else {
+ formState.value.warnNum = 0;
+ createStockUnInventory(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit("completed");
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ });
+ }
+ }
+ });
+ };
+
+ defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+ });
+</script>
diff --git a/src/views/inventoryManagement/stockManagement/Qualified.vue b/src/views/inventoryManagement/stockManagement/Qualified.vue
new file mode 100644
index 0000000..4286772
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/Qualified.vue
@@ -0,0 +1,210 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title ml10">浜у搧澶х被锛�</span>
+ <el-input v-model="searchForm.productName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable/>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="primary" @click="isShowNewModal = true">鏂板搴撳瓨</el-button>
+ <el-button type="info" plain icon="Upload" @click="isShowImportModal = true">
+ 瀵煎叆搴撳瓨
+ </el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys" :row-key="row => row.id" style="width: 100%"
+ :row-class-name="tableRowClassName" height="calc(100vh - 18.5em)">
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productName" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" show-overflow-tooltip />
+ <el-table-column label="鎵瑰彿" prop="batchNo" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" show-overflow-tooltip />
+ <el-table-column label="搴撳瓨鏁伴噺" prop="qualitity" show-overflow-tooltip />
+ <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 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>
+ <el-button link type="primary" v-if="scope.row.unLockedQuantity > 0" @click="showFrozenModal(scope.row)">鍐荤粨</el-button>
+ <el-button link type="primary" v-if="scope.row.lockedQuantity > 0" @click="showThawModal(scope.row)">瑙e喕</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current" :limit="page.size" @pagination="paginationChange" />
+ </div>
+ <new-stock-inventory v-if="isShowNewModal"
+ v-model:visible="isShowNewModal"
+ type="qualified"
+ @completed="handleQuery" />
+
+ <subtract-stock-inventory v-if="isShowSubtractModal"
+ v-model:visible="isShowSubtractModal"
+ :record="record"
+ type="qualified"
+ @completed="handleQuery" />
+ <!-- 瀵煎叆搴撳瓨-->
+ <import-stock-inventory v-if="isShowImportModal"
+ v-model:visible="isShowImportModal"
+ type="qualified"
+ @uploadSuccess="handleQuery" />
+ <!-- 鍐荤粨/瑙e喕搴撳瓨-->
+ <frozen-and-thaw-stock-inventory v-if="isShowFrozenAndThawModal"
+ v-model:visible="isShowFrozenAndThawModal"
+ :record="record"
+ :operation-type="operationType"
+ type="qualified"
+ @completed="handleQuery" />
+ </div>
+</template>
+
+<script setup>
+import pagination from '@/components/PIMTable/Pagination.vue'
+import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
+import {ElMessage, ElMessageBox} from "element-plus";
+import { getStockInventoryListPage } from "@/api/inventoryManagement/stockInventory.js";
+const NewStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/New.vue"));
+const SubtractStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Subtract.vue"));
+const ImportStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Import.vue"));
+const FrozenAndThawStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue"));
+const { proxy } = getCurrentInstance()
+const tableData = ref([])
+const selectedRows = ref([])
+const record = ref({})
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 100,
+})
+const total = ref(0)
+// 鏄惁鏄剧ず鏂板寮规
+const isShowNewModal = ref(false)
+// 鏄惁鏄剧ず棰嗙敤寮规
+const isShowSubtractModal = ref(false)
+// 鏄惁鏄剧ず鍐荤粨/瑙e喕寮规
+const isShowFrozenAndThawModal = ref(false)
+// 鎿嶄綔绫诲瀷
+const operationType = ref('frozen')
+// 鏄惁鏄剧ず瀵煎叆寮规
+const isShowImportModal = ref(false)
+const data = reactive({
+ searchForm: {
+ productName: '',
+ }
+})
+const { searchForm } = toRefs(data)
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1
+ getList()
+}
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList()
+}
+const getList = () => {
+ tableLoading.value = true
+ getStockInventoryListPage({ ...searchForm.value, ...page }).then(res => {
+ tableLoading.value = false
+ tableData.value = res.data.records
+ total.value = res.data.total
+ // 鏁版嵁鍔犺浇瀹屾垚鍚庢鏌ュ簱瀛�
+ // checkStockAndCreatePurchase();
+ }).catch(() => {
+ tableLoading.value = false
+ })
+}
+
+const handleFileSuccess = (response) => {
+ const { code, msg } = response;
+ if (code == 200) {
+ ElMessage({ message: "瀵煎叆鎴愬姛", type: "success" });
+ upload.open = false;
+ emits("uploadSuccess");
+ } else {
+ ElMessage({ message: msg, type: "error" });
+ }
+};
+
+// 鐐瑰嚮棰嗙敤
+const showSubtractModal = (row) => {
+ record.value = row
+ isShowSubtractModal.value = true
+}
+
+// 鐐瑰嚮鍐荤粨
+const showFrozenModal = (row) => {
+ record.value = row
+ isShowFrozenAndThawModal.value = true
+ operationType.value = 'frozen'
+}
+
+// 鐐瑰嚮瑙e喕
+const showThawModal = (row) => {
+ record.value = row
+ isShowFrozenAndThawModal.value = true
+ operationType.value = 'thaw'
+}
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter(item => item.id);
+ console.log('selection', selectedRows.value)
+}
+const expandedRowKeys = ref([])
+
+// 琛ㄦ牸琛岀被鍚�
+const tableRowClassName = ({ row }) => {
+ const stock = Number(row?.unLockedQuantity ?? 0);
+ const warn = Number(row?.warnNum ?? 0);
+ if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
+ return '';
+ }
+ return stock < warn ? 'row-low-stock' : '';
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm(
+ '鏄惁纭瀵煎嚭锛�',
+ '瀵煎嚭', {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }
+ ).then(() => {
+ proxy.download("/stockInventory/exportStockInventory", {}, '鍚堟牸搴撳瓨淇℃伅.xlsx')
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�")
+ })
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+:deep(.row-low-stock td) {
+ background-color: #fde2e2;
+ color: #c45656;
+}
+
+:deep(.row-low-stock:hover > td) {
+ background-color: #fcd4d4;
+}
+</style>
diff --git a/src/views/inventoryManagement/stockManagement/Subtract.vue b/src/views/inventoryManagement/stockManagement/Subtract.vue
new file mode 100644
index 0000000..2cdfcb6
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/Subtract.vue
@@ -0,0 +1,219 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="棰嗙敤"
+ width="800"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="浜у搧鍚嶇О"
+ prop="productModelId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨浜у搧',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-button type="primary" @click="showProductSelectDialog = true" disabled>
+ {{ formState.productName ? formState.productName : '閫夋嫨浜у搧' }}
+ </el-button>
+ </el-form-item>
+
+ <el-form-item
+ label="瑙勬牸"
+ prop="productModelName"
+ >
+ <el-input v-model="formState.model" disabled />
+ </el-form-item>
+
+ <el-form-item
+ label="鍗曚綅"
+ prop="unit"
+ >
+ <el-input v-model="formState.unit" disabled />
+ </el-form-item>
+
+ <el-form-item
+ label="搴撳瓨绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨搴撳瓨绫诲瀷',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨搴撳瓨绫诲瀷" @change="handleTypeChange">
+ <el-option label="鍚堟牸搴撳瓨" value="qualified" :disabled="(props.record.qualifiedUnLockedQuantity || 0) + (props.record.qualifiedPendingOutQuantity || 0) <= 0" />
+ <el-option label="涓嶅悎鏍煎簱瀛�" value="unqualified" :disabled="(props.record.unQualifiedUnLockedQuantity || 0) + (props.record.unQualifiedPendingOutQuantity || 0) <= 0" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="鏁伴噺"
+ prop="qualitity"
+ >
+ <el-input-number v-model="formState.qualitity" :step="1" :min="1" :max="maxQuality" style="width: 100%" />
+ </el-form-item>
+
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formState.remark" type="textarea" />
+ </el-form-item>
+ </el-form>
+
+ <!-- 浜у搧閫夋嫨寮圭獥 -->
+ <ProductSelectDialog
+ v-model="showProductSelectDialog"
+ @confirm="handleProductSelect"
+ single
+ />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, computed, getCurrentInstance} from "vue";
+import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+import {addStockOutRecordOnly} from "@/api/inventoryManagement/stockInventory.js";
+import {addUnqualifiedStockOutRecordOnly} from "@/api/inventoryManagement/stockUninventory.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ record: {
+ type: Object,
+ default: () => {},
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+onMounted(() => {
+ initFormData()
+})
+
+const maxQuality = computed(() => {
+ if (formState.value.type === 'qualified') {
+ // 鍚堟牸鍙嚭 = 鏈喕缁撻噺 + 寰呭鏍稿嚭搴撻噺锛堝嵆宸茬敵璇蜂絾灏氭湭瀹℃壒鐨勬暟閲忥級
+ return Math.max(1, Number(props.record.qualifiedUnLockedQuantity || 0) + Number(props.record.qualifiedPendingOutQuantity || 0));
+ } else {
+ return Math.max(1, Number(props.record.unQualifiedUnLockedQuantity || 0) + Number(props.record.unQualifiedPendingOutQuantity || 0));
+ }
+})
+
+const handleTypeChange = () => {
+ formState.value.qualitity = undefined;
+}
+
+const initFormData = () => {
+ if (props.record) {
+ formState.value = {
+ ...props.record,
+ }
+ }
+}
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ model: "",
+ unit: "",
+ qualitity: 0,
+ remark: '',
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const showProductSelectDialog = ref(false);
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ formState.value = {
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ productModelName: "",
+ description: '',
+ };
+ isShow.value = false;
+};
+
+// 浜у搧閫夋嫨澶勭悊
+const handleProductSelect = async (products) => {
+ if (products && products.length > 0) {
+ const product = products[0];
+ formState.value.productId = product.productId;
+ formState.value.productName = product.productName;
+ formState.value.productModelName = product.model;
+ formState.value.productModelId = product.id;
+ formState.value.unit = product.unit;
+ showProductSelectDialog.value = false;
+ // 瑙﹀彂琛ㄥ崟楠岃瘉鏇存柊
+ proxy.$refs["formRef"]?.validateField('productModelId');
+ }
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ // 楠岃瘉鏄惁閫夋嫨浜嗕骇鍝佸拰瑙勬牸
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨浜у搧");
+ return;
+ }
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨瑙勬牸");
+ return;
+ }
+ if (formState.value.type === 'qualified') {
+ addStockOutRecordOnly(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ } else {
+ addUnqualifiedStockOutRecordOnly(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ }
+ })
+};
+
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
\ No newline at end of file
diff --git a/src/views/inventoryManagement/stockManagement/Unqualified.vue b/src/views/inventoryManagement/stockManagement/Unqualified.vue
new file mode 100644
index 0000000..55662be
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/Unqualified.vue
@@ -0,0 +1,187 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title ml10">浜у搧澶х被锛�</span>
+ <el-input v-model="searchForm.productName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ clearable/>
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="primary" @click="isShowNewModal = true">鏂板搴撳瓨</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys" :row-key="row => row.id" style="width: 100%"
+ :row-class-name="tableRowClassName" height="calc(100vh - 18.5em)">
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productName" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" show-overflow-tooltip />
+ <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 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>
+ <el-button link type="primary" v-if="scope.row.unLockedQuantity > 0" @click="showFrozenModal(scope.row)">鍐荤粨</el-button>
+ <el-button link type="primary" v-if="scope.row.lockedQuantity > 0" @click="showThawModal(scope.row)">瑙e喕</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0" :total="total" layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current" :limit="page.size" @pagination="paginationChange" />
+ </div>
+ <new-stock-inventory v-if="isShowNewModal"
+ v-model:visible="isShowNewModal"
+ type="unqualified"
+ @completed="handleQuery" />
+
+ <subtract-stock-inventory v-if="isShowSubtractModal"
+ v-model:visible="isShowSubtractModal"
+ :record="record"
+ type="unqualified"
+ @completed="handleQuery" />
+ <!-- 鍐荤粨/瑙e喕搴撳瓨-->
+ <frozen-and-thaw-stock-inventory v-if="isShowFrozenAndThawModal"
+ v-model:visible="isShowFrozenAndThawModal"
+ :record="record"
+ :operation-type="operationType"
+ type="unqualified"
+ @completed="handleQuery" />
+ </div>
+</template>
+
+<script setup>
+import pagination from '@/components/PIMTable/Pagination.vue'
+import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
+import { ElMessageBox } from "element-plus";
+import { getStockUninventoryListPage } from "@/api/inventoryManagement/stockUninventory.js";
+const NewStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/New.vue"));
+const SubtractStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/Subtract.vue"));
+const FrozenAndThawStockInventory = defineAsyncComponent(() => import("@/views/inventoryManagement/stockManagement/FrozenAndThaw.vue"));
+
+const { proxy } = getCurrentInstance()
+const tableData = ref([])
+const selectedRows = ref([])
+const record = ref({})
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 100,
+})
+const total = ref(0)
+// 鏄惁鏄剧ず鏂板寮规
+const isShowNewModal = ref(false)
+// 鏄惁鏄剧ず棰嗙敤寮规
+const isShowSubtractModal = ref(false)
+// 鏄惁鏄剧ず鍐荤粨/瑙e喕寮规
+const isShowFrozenAndThawModal = ref(false)
+// 鎿嶄綔绫诲瀷
+const operationType = ref('frozen')
+const data = reactive({
+ searchForm: {
+ productName: '',
+ }
+})
+const { searchForm } = toRefs(data)
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1
+ getList()
+}
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList()
+}
+const getList = () => {
+ tableLoading.value = true
+ getStockUninventoryListPage({ ...searchForm.value, ...page }).then(res => {
+ tableLoading.value = false
+ tableData.value = res.data.records
+ total.value = res.data.total
+ // 鏁版嵁鍔犺浇瀹屾垚鍚庢鏌ュ簱瀛�
+ // checkStockAndCreatePurchase();
+ }).catch(() => {
+ tableLoading.value = false
+ })
+}
+
+// 鐐瑰嚮棰嗙敤
+const showSubtractModal = (row) => {
+ record.value = row
+ isShowSubtractModal.value = true
+}
+
+// 鐐瑰嚮鍐荤粨
+const showFrozenModal = (row) => {
+ record.value = row
+ isShowFrozenAndThawModal.value = true
+ operationType.value = 'frozen'
+}
+
+// 鐐瑰嚮瑙e喕
+const showThawModal = (row) => {
+ record.value = row
+ isShowFrozenAndThawModal.value = true
+ operationType.value = 'thaw'
+}
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter(item => item.id);
+ console.log('selection', selectedRows.value)
+}
+const expandedRowKeys = ref([])
+
+// 琛ㄦ牸琛岀被鍚�
+const tableRowClassName = ({ row }) => {
+ // const stock = Number(row?.unLockedQuantity ?? 0);
+ // const warn = Number(row?.warnNum ?? 0);
+ // if (!Number.isFinite(stock) || !Number.isFinite(warn)) {
+ // return '';
+ // }
+ // return stock < warn ? 'row-low-stock' : '';
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm(
+ '鏄惁纭瀵煎嚭锛�',
+ '瀵煎嚭', {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }
+ ).then(() => {
+ proxy.download("/stockUninventory/exportStockUninventory", {}, '涓嶅悎鏍煎簱瀛樹俊鎭�.xlsx')
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�")
+ })
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+:deep(.row-low-stock td) {
+ background-color: #fde2e2;
+ color: #c45656;
+}
+
+:deep(.row-low-stock:hover > td) {
+ background-color: #fcd4d4;
+}
+</style>
diff --git a/src/views/inventoryManagement/stockManagement/index.vue b/src/views/inventoryManagement/stockManagement/index.vue
new file mode 100644
index 0000000..b3aa7ee
--- /dev/null
+++ b/src/views/inventoryManagement/stockManagement/index.vue
@@ -0,0 +1,44 @@
+<template>
+ <div class="app-container">
+ <div v-loading="loading" element-loading-text="鍔犺浇涓�..." style="min-height: 80vh;">
+ <el-tabs v-model="activeTab" @tab-change="handleTabChange" v-if="!loading">
+ <el-tab-pane v-for="tab in products"
+ :label="tab.productName"
+ :name="tab.id"
+ :key="tab.id">
+ <Record :product-id="tab.id" v-if="tab.id === activeTab" />
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+import { productTreeList } from "@/api/basicData/product.js";
+import Record from "@/views/inventoryManagement/stockManagement/Record.vue";
+const products = ref([])
+const activeTab = ref(null)
+const loading = ref(false)
+
+const handleTabChange = (tabName) => {
+ activeTab.value = tabName;
+}
+
+const fetchProducts = async () => {
+ loading.value = true;
+ try {
+ const res = await productTreeList();
+ products.value = res.filter((item) => item.parentId === null).map(({ id, productName }) => ({ id, productName }));
+ if (products.value.length > 0) {
+ activeTab.value = products.value[0].id;
+ }
+ } finally {
+ loading.value = false;
+ }
+}
+
+onMounted(() => {
+ fetchProducts();
+})
+</script>
\ No newline at end of file
diff --git a/src/views/inventoryManagement/stockReport/index.vue b/src/views/inventoryManagement/stockReport/index.vue
new file mode 100644
index 0000000..26bc8a0
--- /dev/null
+++ b/src/views/inventoryManagement/stockReport/index.vue
@@ -0,0 +1,686 @@
+<template>
+ <div class="app-container">
+ <!-- 鎼滅储琛ㄥ崟 -->
+ <div class="search_form">
+ <div class="search_left">
+ <span class="search_title">鎶ヨ〃绫诲瀷锛�</span>
+ <el-select v-model="searchForm.reportType"
+ style="width: 150px;"
+ placeholder="璇烽�夋嫨"
+ @change="handleReportTypeChange">
+ <el-option label="鏃ユ姤"
+ value="daily" />
+ <el-option label="鏈堟姤"
+ value="monthly" />
+ <el-option label="杩涘嚭瀛樻姤琛�"
+ value="inout" />
+ </el-select>
+ <span class="search_title ml10">鏃堕棿鑼冨洿锛�</span>
+ <el-date-picker v-if="searchForm.reportType === 'daily'"
+ v-model="searchForm.singleDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 200px;" />
+ <el-date-picker v-else-if="searchForm.reportType === 'monthly'"
+ v-model="searchForm.monthRange"
+ type="monthrange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫湀浠�"
+ end-placeholder="缁撴潫鏈堜唤"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 240px;" />
+ <el-date-picker v-else
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 240px;" />
+ <el-button type="primary"
+ @click="onSearch"
+ style="margin-left: 10px">
+ 鏌ヨ
+ </el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </div>
+ <div class="search_right">
+ <!-- <el-button type="success" @click="handleExport" icon="Download">-->
+ <!-- 瀵煎嚭鎶ヨ〃-->
+ <!-- </el-button>-->
+ </div>
+ </div>
+ <!-- <!– 缁熻鍗$墖 –>-->
+ <!-- <div class="stats_cards" v-if="reportData.summary">-->
+ <!-- <el-row :gutter="20">-->
+ <!-- <el-col :span="6">-->
+ <!-- <el-card class="stats_card">-->
+ <!-- <div class="stats_content">-->
+ <!-- <div class="stats_icon in">-->
+ <!-- <el-icon><TrendCharts /></el-icon>-->
+ <!-- </div>-->
+ <!-- <div class="stats_info">-->
+ <!-- <div class="stats_value">{{ reportData.summary.totalIn || 0 }}</div>-->
+ <!-- <div class="stats_label">鎬诲叆搴撻噺</div>-->
+ <!-- </div>-->
+ <!-- </div>-->
+ <!-- </el-card>-->
+ <!-- </el-col>-->
+ <!-- <el-col :span="6">-->
+ <!-- <el-card class="stats_card">-->
+ <!-- <div class="stats_content">-->
+ <!-- <div class="stats_icon out">-->
+ <!-- <el-icon><TrendCharts /></el-icon>-->
+ <!-- </div>-->
+ <!-- <div class="stats_info">-->
+ <!-- <div class="stats_value">{{ reportData.summary.totalOut || 0 }}</div>-->
+ <!-- <div class="stats_label">鎬诲嚭搴撻噺</div>-->
+ <!-- </div>-->
+ <!-- </div>-->
+ <!-- </el-card>-->
+ <!-- </el-col>-->
+ <!-- <el-col :span="6">-->
+ <!-- <el-card class="stats_card">-->
+ <!-- <div class="stats_content">-->
+ <!-- <div class="stats_icon stock">-->
+ <!-- <el-icon><Box /></el-icon>-->
+ <!-- </div>-->
+ <!-- <div class="stats_info">-->
+ <!-- <div class="stats_value">{{ reportData.summary.currentStock || 0 }}</div>-->
+ <!-- <div class="stats_label">褰撳墠搴撳瓨</div>-->
+ <!-- </div>-->
+ <!-- </div>-->
+ <!-- </el-card>-->
+ <!-- </el-col>-->
+ <!-- <el-col :span="6">-->
+ <!-- <el-card class="stats_card">-->
+ <!-- <div class="stats_content">-->
+ <!-- <div class="stats_icon turnover">-->
+ <!-- <el-icon><Refresh /></el-icon>-->
+ <!-- </div>-->
+ <!-- <div class="stats_info">-->
+ <!-- <div class="stats_value">{{ reportData.summary.turnoverRate || 0 }}%</div>-->
+ <!-- <div class="stats_label">鍛ㄨ浆鐜�</div>-->
+ <!-- </div>-->
+ <!-- </div>-->
+ <!-- </el-card>-->
+ <!-- </el-col>-->
+ <!-- </el-row>-->
+ <!-- </div>-->
+ <!-- <!– 鍥捐〃鍖哄煙 –>-->
+ <!-- <div class="chart_section" v-if="reportData.chartData">-->
+ <!-- <el-row :gutter="20">-->
+ <!-- <el-col :span="12">-->
+ <!-- <el-card>-->
+ <!-- <template #header>-->
+ <!-- <span>搴撳瓨瓒嬪娍鍥�</span>-->
+ <!-- </template>-->
+ <!-- <div ref="trendChart" style="height: 300px;"></div>-->
+ <!-- </el-card>-->
+ <!-- </el-col>-->
+ <!-- <el-col :span="12">-->
+ <!-- <el-card>-->
+ <!-- <template #header>-->
+ <!-- <span>杩涘嚭搴撳姣�</span>-->
+ <!-- </template>-->
+ <!-- <div ref="comparisonChart" style="height: 300px;"></div>-->
+ <!-- </el-card>-->
+ <!-- </el-col>-->
+ <!-- </el-row>-->
+ <!-- </div>-->
+ <!-- 璇︾粏鏁版嵁琛ㄦ牸 -->
+ <div class="table_section">
+ <el-card>
+ <template #header>
+ <span>{{ getTableTitle() }}</span>
+ </template>
+ <el-table v-loading="tableLoading"
+ :data="reportData.tableData"
+ border
+ height="400"
+ style="width: 100%"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }">
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鍏ュ簱鏃堕棿"
+ prop="createTime"
+ width="200"
+ show-overflow-tooltip
+ v-if="searchForm.reportType !== 'inout'" />
+ <el-table-column label="鍏ュ簱鎵规"
+ prop="inboundBatches"
+ width="180"
+ show-overflow-tooltip
+ v-if="searchForm.reportType !== 'inout'" />
+ <el-table-column label="鎵瑰彿"
+ prop="batchNo"
+ width="180"
+ show-overflow-tooltip
+ v-if="searchForm.reportType !== 'inout'" />
+ <el-table-column label="浜у搧澶х被"
+ prop="productName"
+ show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model"
+ show-overflow-tooltip />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ show-overflow-tooltip />
+ <el-table-column label="鍏ュ簱鏁伴噺"
+ prop="totalStockIn"
+ align="center"
+ v-if="searchForm.reportType === 'inout'" />
+ <el-table-column label="鍏ュ簱鏁伴噺"
+ prop="stockInNum"
+ align="center"
+ v-else />
+ <el-table-column label="鍑哄簱鏁伴噺"
+ prop="totalStockOut"
+ width="100"
+ align="center"
+ v-if="searchForm.reportType === 'inout'" />
+ <el-table-column label="鐜板湪搴撳瓨"
+ prop="currentStock"
+ align="center" />
+ <el-table-column label="鏉ユ簮"
+ prop="recordType"
+ v-if="searchForm.reportType !== 'inout'"
+ show-overflow-tooltip>
+ <template #default="scope">
+ {{ getRecordType(scope.row.recordType) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏ュ簱浜�"
+ prop="createBy"
+ width="80"
+ v-if="searchForm.reportType !== 'inout'"
+ show-overflow-tooltip />
+ </el-table>
+ <pagination :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
+ </el-card>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, nextTick, getCurrentInstance } from "vue";
+ import { ElMessage } from "element-plus";
+ import * as echarts from "echarts";
+ import pagination from "@/components/PIMTable/Pagination.vue";
+ import {
+ getStockInventoryInAndOutReportList,
+ getStockInventoryReportList,
+ } from "@/api/inventoryManagement/stockInventory.js";
+ import {
+ findAllQualifiedStockInRecordTypeOptions,
+ // findAllUnQualifiedStockInRecordTypeOptions,
+ } from "@/api/basicData/enum.js";
+
+ const { proxy } = getCurrentInstance();
+ // 鍝嶅簲寮忔暟鎹�
+ const tableLoading = ref(false);
+ const trendChart = ref(null);
+ const comparisonChart = ref(null);
+
+ const searchForm = reactive({
+ reportType: "daily",
+ singleDate: "",
+ dateRange: [],
+ monthRange: [],
+ });
+
+ const reportData = ref({
+ summary: null,
+ chartData: null,
+ tableData: [],
+ });
+
+ const page = reactive({
+ current: 1,
+ size: 10,
+ });
+
+ const total = ref(0);
+
+ const stockRecordTypeOptions = ref([]);
+
+ const getRecordType = recordType => {
+ return (
+ stockRecordTypeOptions.value.find(item => item.value === recordType)
+ ?.label || ""
+ );
+ };
+
+ // 鑾峰彇鏉ユ簮绫诲瀷閫夐」
+ const fetchStockRecordTypeOptions = () => {
+ findAllQualifiedStockInRecordTypeOptions().then(res => {
+ stockRecordTypeOptions.value = res.data;
+ // findAllUnQualifiedStockInRecordTypeOptions().then(res => {
+ // stockRecordTypeOptions.value = [
+ // ...stockRecordTypeOptions.value,
+ // ...res.data,
+ // ];
+ // });
+ });
+ };
+
+ // 鑾峰彇琛ㄦ牸鏍囬
+ const getTableTitle = () => {
+ const typeMap = {
+ daily: "鏃ユ姤璇︾粏鏁版嵁",
+ monthly: "鏈堟姤璇︾粏鏁版嵁",
+ inout: "杩涘嚭瀛樻姤琛ㄨ缁嗘暟鎹�",
+ };
+ return typeMap[searchForm.reportType] || "鎶ヨ〃璇︾粏鏁版嵁";
+ };
+
+ // 鎶ヨ〃绫诲瀷鏀瑰彉
+ const handleReportTypeChange = () => {
+ page.current = 1;
+ reportData.value = {
+ summary: null,
+ chartData: null,
+ tableData: [],
+ };
+ };
+
+ // 鏌ヨ鏁版嵁
+ const handleQuery = async () => {
+ if (!validateSearchForm()) {
+ return;
+ }
+
+ tableLoading.value = true;
+ try {
+ const baseParams = getQueryParams();
+ const params = {
+ ...baseParams,
+ current: page.current,
+ size: page.size,
+ };
+ let response;
+
+ if (searchForm.reportType === "inout") {
+ response = await getStockInventoryInAndOutReportList(params);
+ } else {
+ response = await getStockInventoryReportList(params);
+ }
+ if (response.code === 200) {
+ reportData.value.tableData = response.data.records || [];
+ total.value = response.data.total || 0;
+ // reportData.value.summary = response.data.summary
+ // reportData.value.chartData = response.data.chartData
+ // nextTick(() => {
+ // initCharts()
+ // })
+ }
+ } catch (error) {
+ ElMessage.error("鏌ヨ澶辫触锛�" + error.message);
+ } finally {
+ tableLoading.value = false;
+ }
+ };
+
+ // 鏌ヨ鎸夐挳锛氶噸缃埌绗竴椤靛苟鏌ヨ
+ const onSearch = () => {
+ page.current = 1;
+ handleQuery();
+ };
+
+ // 鍒嗛〉鍙樺寲
+ const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ handleQuery();
+ };
+ // // 鐢熸垚鍋囨暟鎹�
+ // const generateMockData = () => {
+ // // 鐢熸垚缁熻鍗$墖鍋囨暟鎹�
+ // const summary = {
+ // totalIn: 1000,
+ // totalOut: 600,
+ // currentStock: 400,
+ // turnoverRate: 30
+ // }
+
+ // // 鐢熸垚鍥捐〃鍋囨暟鎹�
+ // const trendDates = ['2025-09-15', '2025-09-16', '2025-09-17', '2025-09-18', '2025-09-19']
+ // const trendValues = [300, 350, 400, 380, 420]
+ // const comparisonDates = ['2025-09-15', '2025-09-16', '2025-09-17']
+ // const inValues = [100, 150, 200]
+ // const outValues = [80, 120, 100]
+
+ // const chartData = {
+ // trendDates,
+ // trendValues,
+ // comparisonDates,
+ // inValues,
+ // outValues
+ // }
+
+ // reportData.value = {
+ // summary,
+ // chartData,
+ // tableData: []
+ // }
+ // }
+ // 楠岃瘉鎼滅储琛ㄥ崟
+ const validateSearchForm = () => {
+ if (searchForm.reportType === "daily") {
+ if (!searchForm.singleDate) {
+ ElMessage.warning("璇烽�夋嫨鏃ユ湡");
+ return false;
+ }
+ } else if (searchForm.reportType === "inout") {
+ if (!searchForm.dateRange || searchForm.dateRange.length !== 2) {
+ ElMessage.warning("璇烽�夋嫨鏃ユ湡鑼冨洿");
+ return false;
+ }
+ } else if (searchForm.reportType === "monthly") {
+ if (!searchForm.monthRange || searchForm.monthRange.length !== 2) {
+ ElMessage.warning("璇烽�夋嫨鏈堜唤鑼冨洿");
+ return false;
+ }
+ }
+ return true;
+ };
+
+ // 鑾峰彇鏌ヨ鍙傛暟
+ const getQueryParams = () => {
+ const params = {
+ reportType: searchForm.reportType,
+ reportDate: "",
+ startMonth: "",
+ endMonth: "",
+ startDate: "",
+ endDate: "",
+ };
+
+ if (searchForm.reportType === "daily") {
+ params.reportDate = searchForm.singleDate;
+ } else if (searchForm.reportType === "monthly") {
+ params.startMonth = searchForm.monthRange[0];
+ params.endMonth = searchForm.monthRange[1];
+ } else {
+ params.startDate = searchForm.dateRange[0];
+ params.endDate = searchForm.dateRange[1];
+ }
+
+ return params;
+ };
+
+ // 閲嶇疆鎼滅储
+ const handleReset = () => {
+ searchForm.reportType = "daily";
+ searchForm.singleDate = "";
+ searchForm.dateRange = [];
+ searchForm.monthRange = [];
+ reportData.value = {
+ summary: null,
+ chartData: null,
+ tableData: [],
+ };
+ };
+
+ // 瀵煎嚭鎶ヨ〃
+ const handleExport = async () => {
+ if (!validateSearchForm()) {
+ return;
+ }
+
+ try {
+ const params = getQueryParams();
+ // const response = await exportStockReport(params)
+ proxy.download("/stockin/exportCopy", params, "搴撳瓨鎶ヨ〃.xlsx");
+ // 鍒涘缓涓嬭浇閾炬帴
+ // const blob = new Blob([response], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
+ // const url = window.URL.createObjectURL(blob)
+ // const link = document.createElement('a')
+ // link.href = url
+ // link.download = `${getTableTitle()}_${new Date().getTime()}.xlsx`
+ // document.body.appendChild(link)
+ // link.click()
+ // document.body.removeChild(link)
+ // window.URL.revokeObjectURL(url)
+
+ // ElMessage.success('瀵煎嚭鎴愬姛')
+ } catch (error) {
+ ElMessage.error("瀵煎嚭澶辫触锛�" + error.message);
+ }
+ };
+
+ // 鍒濆鍖栧浘琛�
+ const initCharts = () => {
+ if (!reportData.value.chartData) return;
+
+ initTrendChart();
+ initComparisonChart();
+ };
+
+ // 鍒濆鍖栬秼鍔垮浘
+ const initTrendChart = () => {
+ if (!trendChart.value) return;
+
+ const chart = echarts.init(trendChart.value);
+ const option = {
+ title: {
+ text: "搴撳瓨鍙樺寲瓒嬪娍",
+ left: "center",
+ },
+ tooltip: {
+ trigger: "axis",
+ },
+ legend: {
+ data: ["搴撳瓨閲�"],
+ top: 30,
+ },
+ xAxis: {
+ type: "category",
+ data: reportData.value.chartData.trendDates || [],
+ },
+ yAxis: {
+ type: "value",
+ },
+ series: [
+ {
+ name: "搴撳瓨閲�",
+ type: "line",
+ data: reportData.value.chartData.trendValues || [],
+ smooth: true,
+ itemStyle: {
+ color: "#409EFF",
+ },
+ },
+ ],
+ };
+ chart.setOption(option);
+ };
+
+ // 鍒濆鍖栧姣斿浘
+ const initComparisonChart = () => {
+ if (!comparisonChart.value) return;
+
+ const chart = echarts.init(comparisonChart.value);
+ const option = {
+ title: {
+ text: "杩涘嚭搴撳姣�",
+ left: "center",
+ },
+ tooltip: {
+ trigger: "axis",
+ },
+ legend: {
+ data: ["鍏ュ簱", "鍑哄簱"],
+ top: 30,
+ },
+ xAxis: {
+ type: "category",
+ data: reportData.value.chartData.comparisonDates || [],
+ },
+ yAxis: {
+ type: "value",
+ },
+ series: [
+ {
+ name: "鍏ュ簱",
+ type: "bar",
+ data: reportData.value.chartData.inValues || [],
+ itemStyle: {
+ color: "#67C23A",
+ },
+ },
+ {
+ name: "鍑哄簱",
+ type: "bar",
+ data: reportData.value.chartData.outValues || [],
+ itemStyle: {
+ color: "#F56C6C",
+ },
+ },
+ ],
+ };
+ chart.setOption(option);
+ };
+
+ // 缁勪欢鎸傝浇鏃惰缃粯璁ゆ椂闂�
+ onMounted(() => {
+ const today = new Date();
+ searchForm.singleDate = today.toISOString().split("T")[0];
+
+ const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
+ searchForm.dateRange = [
+ yesterday.toISOString().split("T")[0],
+ today.toISOString().split("T")[0],
+ ];
+
+ fetchStockRecordTypeOptions();
+ // 鍒濆鍖栧姞杞戒竴娆℃暟鎹�
+ handleQuery();
+ });
+</script>
+
+<style scoped>
+ .app-container {
+ padding: 20px;
+ }
+
+ .search_form {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding: 20px;
+ background: #fff;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .search_left {
+ display: flex;
+ align-items: center;
+ }
+
+ .search_title {
+ font-weight: 500;
+ color: #333;
+ margin-right: 8px;
+ }
+
+ .ml10 {
+ margin-left: 10px;
+ }
+
+ .stats_cards {
+ margin-bottom: 20px;
+ }
+
+ .stats_card {
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ }
+
+ .stats_content {
+ display: flex;
+ align-items: center;
+ padding: 10px 0;
+ }
+
+ .stats_icon {
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 15px;
+ font-size: 24px;
+ color: #fff;
+ }
+
+ .stats_icon.in {
+ background: linear-gradient(135deg, #67c23a, #85ce61);
+ }
+
+ .stats_icon.out {
+ background: linear-gradient(135deg, #f56c6c, #f78989);
+ }
+
+ .stats_icon.stock {
+ background: linear-gradient(135deg, #409eff, #66b1ff);
+ }
+
+ .stats_icon.turnover {
+ background: linear-gradient(135deg, #e6a23c, #eebe77);
+ }
+
+ .stats_info {
+ flex: 1;
+ }
+
+ .stats_value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #333;
+ line-height: 1;
+ margin-bottom: 5px;
+ }
+
+ .stats_label {
+ font-size: 14px;
+ color: #666;
+ }
+
+ .chart_section {
+ margin-bottom: 20px;
+ }
+
+ .table_section {
+ margin-bottom: 20px;
+ }
+
+ :deep(.el-card__header) {
+ background: #f8f9fa;
+ border-bottom: 1px solid #e9ecef;
+ font-weight: 500;
+ }
+
+ :deep(.el-table .el-table__header-wrapper th) {
+ background-color: #f0f1f5 !important;
+ color: #333333;
+ font-weight: 600;
+ }
+
+ :deep(.el-table .el-table__body-wrapper td) {
+ padding: 8px 0;
+ }
+</style>
diff --git a/src/views/inventoryManagement/stockWarning/index.vue b/src/views/inventoryManagement/stockWarning/index.vue
new file mode 100644
index 0000000..b5216cb
--- /dev/null
+++ b/src/views/inventoryManagement/stockWarning/index.vue
@@ -0,0 +1,943 @@
+<template>
+ <div class="app-container">
+ <!-- 鎼滅储琛ㄥ崟 -->
+ <div class="search_form">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="鍌ㄦ皵缃愬悕绉帮細">
+ <el-input v-model="searchForm.tankName" placeholder="璇疯緭鍏ュ偍姘旂綈鍚嶇О" clearable style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="鍌ㄦ皵缃愮被鍨嬶細">
+ <el-select v-model="searchForm.tankType" placeholder="璇烽�夋嫨鍌ㄦ皵缃愮被鍨�" clearable style="width: 200px">
+ <el-option label="娑插寲姘斿偍缃�" value="娑插寲姘斿偍缃�" />
+ <el-option label="鍘嬬缉姘斿偍缃�" value="鍘嬬缉姘斿偍缃�" />
+ <el-option label="澶╃劧姘斿偍缃�" value="澶╃劧姘斿偍缃�" />
+ <el-option label="姘ф皵鍌ㄧ綈" value="姘ф皵鍌ㄧ綈" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="棰勮绫诲瀷锛�">
+ <el-select v-model="searchForm.warningType" placeholder="璇烽�夋嫨棰勮绫诲瀷" clearable style="width: 200px">
+ <el-option label="姘斾綋涓嶈冻" value="姘斾綋涓嶈冻" />
+ <el-option label="鍘嬪姏寮傚父" value="鍘嬪姏寮傚父" />
+ <el-option label="娓╁害寮傚父" value="娓╁害寮傚父" />
+ <el-option label="娉勬紡棰勮" value="娉勬紡棰勮" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="棰勮绾у埆锛�">
+ <el-select v-model="searchForm.warningLevel" placeholder="璇烽�夋嫨棰勮绾у埆" clearable style="width: 200px">
+ <el-option label="绱ф��" value="绱ф��" />
+ <el-option label="閲嶈" value="閲嶈" />
+ <el-option label="涓�鑸�" value="涓�鑸�" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ <el-button @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <div class="table_list">
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="table-operations">
+ <el-button type="primary" @click="handleAdd">鏂板棰勮瑙勫垯</el-button>
+ <!-- <el-button type="success" @click="handleBatchProcess">鎵归噺澶勭悊@</el-button> -->
+ <el-button @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ style="width: 100%"
+ height="calc(100vh - 280px)"
+ >
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+
+ <!-- 鍩虹淇℃伅瀛楁 -->
+ <el-table-column label="鍌ㄦ皵缃愮紪鐮�" prop="tankCode" width="120" show-overflow-tooltip />
+ <el-table-column label="鍌ㄦ皵缃愬悕绉�" prop="tankName" width="200" show-overflow-tooltip />
+ <el-table-column label="鍌ㄦ皵缃愮被鍨�" prop="tankType" width="120" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" width="150" show-overflow-tooltip />
+ <el-table-column label="瀹圭Н(m鲁)" prop="volume" width="100" show-overflow-tooltip />
+
+ <!-- 搴撳瓨鐩稿叧瀛楁 -->
+ <el-table-column label="褰撳墠姘斾綋閲�" prop="currentGasLevel" width="120" show-overflow-tooltip>
+ <template #default="scope">
+ <span :class="getGasLevelClass(scope.row)">{{ scope.row.currentGasLevel }}%</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹夊叏姘斾綋閲�" prop="safetyGasLevel" width="120" show-overflow-tooltip />
+ <el-table-column label="鏈�浣庢皵浣撻噺" prop="minGasLevel" width="120" show-overflow-tooltip />
+ <el-table-column label="鏈�楂樻皵浣撻噺" prop="maxGasLevel" width="120" show-overflow-tooltip />
+ <el-table-column label="褰撳墠鍘嬪姏(MPa)" prop="currentPressure" width="140" show-overflow-tooltip />
+
+ <!-- 棰勮瑙勫垯瀛楁 -->
+ <el-table-column label="棰勮绫诲瀷" prop="warningType" width="100" show-overflow-tooltip>
+ <template #default="scope">
+ <el-tag :type="getWarningTypeTag(scope.row.warningType)">
+ {{ scope.row.warningType }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="棰勮绾у埆" prop="warningLevel" width="100" show-overflow-tooltip>
+ <template #default="scope">
+ <el-tag :type="getWarningLevelTag(scope.row.warningLevel)">
+ {{ scope.row.warningLevel }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="棰勮闃堝��" prop="warningThreshold" width="100" show-overflow-tooltip />
+ <el-table-column label="鏄惁鍚敤" prop="isEnabled" width="100" show-overflow-tooltip>
+ <template #default="scope">
+ <el-switch v-model="scope.row.isEnabled" @change="handleEnableChange(scope.row)" />
+ </template>
+ </el-table-column>
+
+ <!-- 鏃堕棿鐩稿叧瀛楁 -->
+ <el-table-column label="棰勮鏃堕棿" prop="warningTime" width="150" show-overflow-tooltip />
+ <el-table-column label="棰勮鎸佺画澶╂暟" prop="warningDuration" width="120" show-overflow-tooltip />
+ <el-table-column label="鏈�鍚庢洿鏂版椂闂�" prop="lastUpdateTime" width="150" show-overflow-tooltip />
+ <el-table-column label="棰勮鍏呰鏃堕棿" prop="expectedRefillTime" width="150" show-overflow-tooltip />
+ <el-table-column label="棰勮缂烘皵鏃堕棿" prop="expectedShortageTime" width="150" show-overflow-tooltip>
+ <template #default="scope">
+ <div v-if="scope.row.expectedShortageTime">
+ <div v-if="getCountdown(scope.row.expectedShortageTime).isExpired" class="countdown-expired">
+ <el-tag type="danger">宸茬己姘�</el-tag>
+ </div>
+ <div v-else class="countdown-timer">
+ <span :class="getCountdownClass(scope.row.expectedShortageTime)">
+ {{ getCountdown(scope.row.expectedShortageTime).text }}
+ </span>
+ </div>
+ </div>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+
+ <!-- 鎿嶄綔鍒� -->
+ <el-table-column fixed="right" label="鎿嶄綔" width="200" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleEdit(scope.row)">缂栬緫</el-button>
+<!-- <el-button link type="success" size="small" @click="handleProcess(scope.row)">澶勭悊@</el-button>-->
+ <el-button link type="danger" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+
+ <!-- 鏂板/缂栬緫棰勮瑙勫垯寮圭獥 -->
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板棰勮瑙勫垯' : '缂栬緫棰勮瑙勫垯'"
+ width="50%"
+ @close="closeDialog"
+ >
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="140px">
+ <el-row :gutter="20">
+ <!-- 鍩虹淇℃伅 -->
+ <el-col :span="12">
+ <el-form-item label="鍌ㄦ皵缃愮紪鐮侊細" prop="tankCode">
+ <el-input v-model="form.tankCode" placeholder="璇疯緭鍏ュ偍姘旂綈缂栫爜" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍌ㄦ皵缃愬悕绉帮細" prop="tankName">
+ <el-input v-model="form.tankName" placeholder="璇疯緭鍏ュ偍姘旂綈鍚嶇О" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍌ㄦ皵缃愮被鍨嬶細" prop="tankType">
+ <el-select v-model="form.tankType" placeholder="璇烽�夋嫨鍌ㄦ皵缃愮被鍨�" style="width: 100%">
+ <el-option label="娑插寲姘斿偍缃�" value="娑插寲姘斿偍缃�" />
+ <el-option label="鍘嬬缉姘斿偍缃�" value="鍘嬬缉姘斿偍缃�" />
+ <el-option label="澶╃劧姘斿偍缃�" value="澶╃劧姘斿偍缃�" />
+ <el-option label="姘ф皵鍌ㄧ綈" value="姘ф皵鍌ㄧ綈" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="specificationModel">
+ <el-input v-model="form.specificationModel" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹圭Н(m鲁)锛�" prop="volume">
+ <el-input-number v-model="form.volume" :min="0" :precision="2" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰撳墠姘斾綋閲�(%)锛�" prop="currentGasLevel">
+ <el-input-number v-model="form.currentGasLevel" :min="0" :max="100" :precision="1" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 搴撳瓨鐩稿叧 -->
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹夊叏姘斾綋閲�(%)锛�" prop="safetyGasLevel">
+ <el-input-number v-model="form.safetyGasLevel" :min="0" :max="100" :precision="1" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏈�浣庢皵浣撻噺(%)锛�" prop="minGasLevel">
+ <el-input-number v-model="form.minGasLevel" :min="0" :max="100" :precision="1" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏈�楂樻皵浣撻噺(%)锛�" prop="maxGasLevel">
+ <el-input-number v-model="form.maxGasLevel" :min="0" :max="100" :precision="1" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰撳墠鍘嬪姏(MPa)锛�" prop="currentPressure">
+ <el-input-number v-model="form.currentPressure" :min="0" :precision="2" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 棰勮瑙勫垯 -->
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="棰勮绫诲瀷锛�" prop="warningType">
+ <el-select v-model="form.warningType" placeholder="璇烽�夋嫨棰勮绫诲瀷" style="width: 100%">
+ <el-option label="姘斾綋涓嶈冻" value="姘斾綋涓嶈冻" />
+ <el-option label="鍘嬪姏寮傚父" value="鍘嬪姏寮傚父" />
+ <el-option label="娓╁害寮傚父" value="娓╁害寮傚父" />
+ <el-option label="娉勬紡棰勮" value="娉勬紡棰勮" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="棰勮绾у埆锛�" prop="warningLevel">
+ <el-select v-model="form.warningLevel" placeholder="璇烽�夋嫨棰勮绾у埆" style="width: 100%">
+ <el-option label="绱ф��" value="绱ф��" />
+ <el-option label="閲嶈" value="閲嶈" />
+ <el-option label="涓�鑸�" value="涓�鑸�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="棰勮闃堝�硷細" prop="warningThreshold">
+ <el-input-number v-model="form.warningThreshold" :min="0" :precision="2" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄惁鍚敤锛�" prop="isEnabled">
+ <el-switch v-model="form.isEnabled" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 鏃堕棿鐩稿叧 -->
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="棰勮鏃堕棿锛�" prop="warningTime">
+ <el-date-picker
+ v-model="form.warningTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨棰勮鏃堕棿"
+ style="width: 100%"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="棰勮鍏呰鏃堕棿锛�" prop="expectedRefillTime">
+ <el-date-picker
+ v-model="form.expectedRefillTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨棰勮鍏呰鏃堕棿"
+ style="width: 100%"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="棰勮缂烘皵鏃堕棿锛�" prop="expectedShortageTime">
+ <el-date-picker
+ v-model="form.expectedShortageTime"
+ type="datetime"
+ placeholder="璇烽�夋嫨棰勮缂烘皵鏃堕棿"
+ style="width: 100%"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="棰勮瑙勫垯鎻忚堪锛�" prop="warningRule">
+ <el-input
+ v-model="form.warningRule"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ラ璀﹁鍒欐弿杩�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 缂烘皵棰勮寮规 -->
+ <el-dialog
+ v-model="shortageWarningVisible"
+ title="鈿狅笍 缂烘皵棰勮"
+ width="400px"
+ :close-on-click-modal="false"
+ :close-on-press-escape="false"
+ :show-close="false"
+ >
+ <div class="shortage-warning-content">
+ <div class="warning-icon">
+ <el-icon size="48" color="#f56c6c"><WarningFilled /></el-icon>
+ </div>
+ <div class="warning-message">
+ <h3>{{ currentWarningTank.tankName }}</h3>
+ <p>鍌ㄦ皵缃愬凡缂烘皵锛岃鍙婃椂澶勭悊锛�</p>
+ <p class="warning-details">
+ 鍌ㄦ皵缃愮紪鐮侊細{{ currentWarningTank.tankCode }}<br>
+ 鍌ㄦ皵缃愮被鍨嬶細{{ currentWarningTank.tankType }}<br>
+ 褰撳墠姘斾綋閲忥細{{ currentWarningTank.currentGasLevel }}%
+ </p>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleShortageWarning">绔嬪嵆澶勭悊</el-button>
+ <el-button @click="closeShortageWarning">绋嶅悗澶勭悊</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 缂烘皵棰勮寮规 -->
+ <el-dialog
+ v-model="shortageWarningVisible"
+ title="鈿狅笍 缂烘皵棰勮"
+ width="400px"
+ :close-on-click-modal="false"
+ :close-on-press-escape="false"
+ :show-close="false"
+ >
+ <div class="shortage-warning-content">
+ <div class="warning-icon">
+ <el-icon size="48" color="#f56c6c"><WarningFilled /></el-icon>
+ </div>
+ <div class="warning-message">
+ <h3>{{ currentWarningTank.tankName }}</h3>
+ <p>鍌ㄦ皵缃愬凡缂烘皵锛岃鍙婃椂澶勭悊锛�</p>
+ <p class="warning-details">
+ 鍌ㄦ皵缃愮紪鐮侊細{{ currentWarningTank.tankCode }}<br>
+ 鍌ㄦ皵缃愮被鍨嬶細{{ currentWarningTank.tankType }}<br>
+ 褰撳墠姘斾綋閲忥細{{ currentWarningTank.currentGasLevel }}%
+ </p>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleShortageWarning">绔嬪嵆澶勭悊</el-button>
+ <el-button @click="closeShortageWarning">绋嶅悗澶勭悊</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { WarningFilled } from '@element-plus/icons-vue'
+import pagination from '@/components/PIMTable/Pagination.vue'
+// 娉ㄩ噴鎺堿PI瀵煎叆锛屼娇鐢ㄥ亣鏁版嵁
+import {
+ getStockWarningPage,
+ addStockWarning,
+ updateStockWarning,
+ deleteStockWarning,
+ batchProcessStockWarning,
+ exportStockWarning,
+ toggleStockWarningStatus
+} from '@/api/inventoryManagement/stockWarning.js'
+
+const { proxy } = getCurrentInstance()
+
+// 鍝嶅簲寮忔暟鎹�
+const tableData = ref([])
+const tableLoading = ref(false)
+const selectedRows = ref([])
+const dialogFormVisible = ref(false)
+const operationType = ref('add')
+const total = ref(0)
+
+// 缂烘皵棰勮鐩稿叧
+const shortageWarningVisible = ref(false)
+const currentWarningTank = ref({})
+const countdownTimer = ref(null)
+
+// 鍒嗛〉鍙傛暟
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0
+})
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ tankName: '',
+ tankType: '',
+ warningType: '',
+ warningLevel: ''
+})
+
+// 琛ㄥ崟鏁版嵁
+const form = reactive({
+ id: null,
+ tankCode: '',
+ tankName: '',
+ tankType: '',
+ specificationModel: '',
+ volume: 0,
+ currentGasLevel: 0,
+ safetyGasLevel: 0,
+ minGasLevel: 0,
+ maxGasLevel: 0,
+ currentPressure: 0,
+ warningType: '',
+ warningLevel: '',
+ warningThreshold: 0,
+ isEnabled: true,
+ warningTime: '',
+ warningDuration: 0,
+ lastUpdateTime: '',
+ expectedRefillTime: '',
+ expectedShortageTime: '',
+ warningRule: ''
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+ tankCode: [{ required: true, message: '璇疯緭鍏ュ偍姘旂綈缂栫爜', trigger: 'blur' }],
+ tankName: [{ required: true, message: '璇疯緭鍏ュ偍姘旂綈鍚嶇О', trigger: 'blur' }],
+ tankType: [{ required: true, message: '璇烽�夋嫨鍌ㄦ皵缃愮被鍨�', trigger: 'change' }],
+ warningType: [{ required: true, message: '璇烽�夋嫨棰勮绫诲瀷', trigger: 'change' }],
+ warningLevel: [{ required: true, message: '璇烽�夋嫨棰勮绾у埆', trigger: 'change' }],
+ warningThreshold: [{ required: true, message: '璇疯緭鍏ラ璀﹂槇鍊�', trigger: 'blur' }]
+}
+
+// 鑾峰彇鍊掕鏃朵俊鎭�
+const getCountdown = (expectedTime) => {
+ if (!expectedTime) return { text: '-', isExpired: false }
+
+ const now = new Date().getTime()
+ const expected = new Date(expectedTime).getTime()
+ const diff = expected - now
+
+ if (diff <= 0) {
+ return { text: '宸茬己姘�', isExpired: true }
+ }
+
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24))
+ const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
+ const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
+
+ if (days > 0) {
+ return { text: `${days}澶�${hours}灏忔椂`, isExpired: false }
+ } else if (hours > 0) {
+ return { text: `${hours}灏忔椂${minutes}鍒嗛挓`, isExpired: false }
+ } else {
+ return { text: `${minutes}鍒嗛挓`, isExpired: false }
+ }
+}
+
+// 鑾峰彇鍊掕鏃舵牱寮忕被
+const getCountdownClass = (expectedTime) => {
+ if (!expectedTime) return ''
+
+ const now = new Date().getTime()
+ const expected = new Date(expectedTime).getTime()
+ const diff = expected - now
+
+ if (diff <= 0) {
+ return 'countdown-expired'
+ } else if (diff <= 24 * 60 * 60 * 1000) { // 24灏忔椂鍐�
+ return 'countdown-urgent'
+ } else if (diff <= 7 * 24 * 60 * 60 * 1000) { // 7澶╁唴
+ return 'countdown-warning'
+ } else {
+ return 'countdown-normal'
+ }
+}
+
+// 妫�鏌ョ己姘旈璀�
+const checkShortageWarnings = () => {
+ tableData.value.forEach(tank => {
+ if (tank.expectedShortageTime) {
+ const countdown = getCountdown(tank.expectedShortageTime)
+ if (countdown.isExpired && !tank.warningShown) {
+ // 鏍囪宸叉樉绀洪璀︼紝閬垮厤閲嶅寮规
+ tank.warningShown = true
+ showShortageWarning(tank)
+ }
+ }
+ })
+}
+
+// 鏄剧ず缂烘皵棰勮寮规
+const showShortageWarning = (tank) => {
+ currentWarningTank.value = tank
+ shortageWarningVisible.value = true
+
+ // 鎾斁鎻愮ず闊筹紙鍙�夛級
+ // const audio = new Audio('/path/to/warning-sound.mp3')
+ // audio.play()
+}
+
+// 澶勭悊缂烘皵棰勮
+const handleShortageWarning = () => {
+ ElMessage.success(`姝e湪澶勭悊鍌ㄦ皵缃� ${currentWarningTank.value.tankName} 鐨勭己姘旈棶棰榒)
+ shortageWarningVisible.value = false
+ // 杩欓噷鍙互璋冪敤澶勭悊API
+}
+// 澶勭悊缂烘皵棰勮
+const closeShortageWarning = () => {
+ // ElMessage.success(`姝e湪澶勭悊鍌ㄦ皵缃� ${currentWarningTank.value.tankName} 鐨勭己姘旈棶棰榒)
+ shortageWarningVisible.value = false
+ // 杩欓噷鍙互璋冪敤澶勭悊API
+}
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = async () => {
+ tableLoading.value = true
+ getStockWarningPage(page, searchForm)
+ .then(res => {
+
+ tableData.value = res.data.records
+ page.value.total = res.data.total;
+ tableLoading.value = false;
+ // 妫�鏌ョ己姘旈璀�
+ checkShortageWarnings()
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+}
+
+// 鎼滅储
+const handleQuery = () => {
+ page.current = 1
+ getList()
+}
+
+// 閲嶇疆鎼滅储
+const resetQuery = () => {
+ Object.keys(searchForm).forEach(key => {
+ searchForm[key] = ''
+ })
+ handleQuery()
+}
+
+// 鍒嗛〉鍙樺寲
+const paginationChange = (obj) => {
+ page.current = obj.page
+ page.size = obj.limit
+ getList()
+}
+
+// 琛ㄦ牸閫夋嫨鍙樺寲
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection
+}
+
+// 鏂板
+const handleAdd = () => {
+ operationType.value = 'add'
+ // resetForm()
+ dialogFormVisible.value = true
+}
+
+// 缂栬緫
+const handleEdit = (row) => {
+ operationType.value = 'edit'
+ Object.assign(form, row)
+ dialogFormVisible.value = true
+}
+
+// 澶勭悊棰勮
+const handleProcess = async (row) => {
+ try {
+ // 妯℃嫙API璋冪敤寤惰繜
+ await new Promise(resolve => setTimeout(resolve, 300))
+ ElMessage.success(`姝e湪澶勭悊棰勮锛�${row.tankName}`)
+ // getList()
+ } catch (error) {
+ ElMessage.error('澶勭悊棰勮澶辫触')
+ }
+}
+
+// 鍒犻櫎
+const handleDelete = async (row) => {
+ try {
+ await ElMessageBox.confirm(`纭畾瑕佸垹闄ら璀﹁鍒欙細${row.tankName}鍚楋紵`, '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ let ids = [];
+ ids.push(row.id);
+ deleteStockWarning(ids).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ ids.value = [];
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ // // 妯℃嫙API璋冪敤寤惰繜
+ // await new Promise(resolve => setTimeout(resolve, 300))
+ // ElMessage.success('鍒犻櫎鎴愬姛')
+ // getList()
+ } catch (error) {
+ if (error !== 'cancel') {
+ ElMessage.error('鍒犻櫎澶辫触')
+ }
+ }
+}
+
+// 鎵归噺澶勭悊
+const handleBatchProcess = async () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸鐞嗙殑棰勮')
+ return
+ }
+
+ try {
+ // 妯℃嫙API璋冪敤寤惰繜
+ await new Promise(resolve => setTimeout(resolve, 500))
+ ElMessage.success(`鎵归噺澶勭悊浜� ${selectedRows.value.length} 鏉¢璀)
+ getList()
+ } catch (error) {
+ ElMessage.error('鎵归噺澶勭悊澶辫触')
+ }
+}
+
+// 瀵煎嚭
+const handleExport = async () => {
+ // if (selectedRows.value.length === 0) {
+ // exportStockWarning().then(res => {
+ // // // 鍒涘缓涓嬭浇閾炬帴
+ // // const blob = new Blob([res.data], { type: 'text/csv;charset=utf-8;' })
+ // // const url = window.URL.createObjectURL(blob)
+ // // const link = document.createElement('a')
+ // // link.href = url
+ // // link.download = `鍌ㄦ皵缃愰璀︽暟鎹甠${new Date().getTime()}.csv`
+ // // link.click()
+ // // window.URL.revokeObjectURL(url)
+ // }).catch(err => {
+ // ElMessage.error(err.msg);
+ // })
+ // }else{
+ // let ids = [];
+ // selectedRows.value.forEach(item => {
+ // ids.push(item.id);
+ // })
+ // exportStockWarning(ids).then(res => {
+ // // // 鍒涘缓涓嬭浇閾炬帴
+ // // const blob = new Blob([res.data], { type: 'text/csv;charset=utf-8;' })
+ // // const url = window.URL.createObjectURL(blob)
+ // // const link = document.createElement('a')
+ // // link.href = url
+ // // link.download = `鍌ㄦ皵缃愰璀︽暟鎹甠${new Date().getTime()}.csv`
+ // // link.click()
+ // // window.URL.revokeObjectURL(url)
+ // }).catch(err => {
+ // ElMessage.error(err.msg);
+ // })
+ // }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/gasTankWarning/export", {ids: selectedRows.value.map(item => item.id)}, "鍌ㄦ皵缃愰璀�.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+}
+
+
+
+// // 鍚敤鐘舵�佸彉鍖�
+const handleEnableChange = async (row) => {
+
+ try {
+ updateStockWarning(row).then(res => {
+ if(res.code == 200){
+ ElMessage.success(`${row.tankName} 鐨勫惎鐢ㄧ姸鎬佸凡鏇存柊`);
+ getList();
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ } catch (error) {
+ ElMessage.error('鐘舵�佹洿鏂板け璐�')
+ // 鎭㈠鍘熺姸鎬�
+ row.isEnabled = !row.isEnabled
+ }
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = async () => {
+ try {
+ await proxy.$refs.formRef.validate()
+
+ // 妯℃嫙API璋冪敤寤惰繜
+ // await new Promise(resolve => setTimeout(resolve, 500))
+
+ if (operationType.value === 'add') {
+ addStockWarning(form).then(res => {
+ if(res.code == 200){
+ ElMessage.success("娣诲姞鎴愬姛");
+ dialogFormVisible.value = false
+ getList()
+ resetForm()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+
+ // ElMessage.success('鏂板鎴愬姛')
+ } else {
+ updateStockWarning(form).then(res => {
+ if(res.code == 200){
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogFormVisible.value = false
+ getList()
+ resetForm()
+ }
+ }).catch(err => {
+ ElMessage.error(err.msg);
+ })
+ // ElMessage.success('缂栬緫鎴愬姛')
+ }
+
+ // closeDialog()
+ // getList()
+ } catch (error) {
+ if (!error.errors) {
+ ElMessage.error(operationType.value === 'add' ? '鏂板澶辫触' : '缂栬緫澶辫触')
+ }
+ }
+}
+
+// 鍏抽棴寮圭獥
+const closeDialog = () => {
+ dialogFormVisible.value = false
+ // resetForm()
+}
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+ Object.keys(form).forEach(key => {
+ if (key === 'isEnabled') {
+ form[key] = true
+ } else if (typeof form[key] === 'number') {
+ form[key] = 0
+ } else {
+ form[key] = ''
+ }
+ })
+ proxy.$refs.formRef?.resetFields()
+}
+
+// 鑾峰彇姘斾綋閲忔牱寮忕被
+const getGasLevelClass = (row) => {
+ if (row.currentGasLevel < row.minGasLevel) {
+ return 'text-danger'
+ } else if (row.currentGasLevel > row.maxGasLevel) {
+ return 'text-warning'
+ }
+ return 'text-success'
+}
+
+// 鑾峰彇棰勮绫诲瀷鏍囩鏍峰紡
+const getWarningTypeTag = (type) => {
+ const typeMap = {
+ '姘斾綋涓嶈冻': 'danger',
+ '鍘嬪姏寮傚父': 'warning',
+ '娓╁害寮傚父': 'info',
+ '娉勬紡棰勮': 'danger'
+ }
+ return typeMap[type] || 'info'
+}
+
+// 鑾峰彇棰勮绾у埆鏍囩鏍峰紡
+const getWarningLevelTag = (level) => {
+ const levelMap = {
+ '绱ф��': 'danger',
+ '閲嶈': 'warning',
+ '涓�鑸�': 'info'
+ }
+ return levelMap[level] || 'info'
+}
+
+// 鍚姩鍊掕鏃跺畾鏃跺櫒
+const startCountdownTimer = () => {
+ countdownTimer.value = setInterval(() => {
+ checkShortageWarnings()
+ }, 60000) // 姣忓垎閽熸鏌ヤ竴娆�
+}
+
+// 鍋滄鍊掕鏃跺畾鏃跺櫒
+const stopCountdownTimer = () => {
+ if (countdownTimer.value) {
+ clearInterval(countdownTimer.value)
+ countdownTimer.value = null
+ }
+}
+
+// 椤甸潰鍔犺浇
+onMounted(() => {
+ getList()
+ startCountdownTimer()
+})
+
+// 椤甸潰鍗歌浇
+onUnmounted(() => {
+ stopCountdownTimer()
+})
+</script>
+
+<style scoped lang="scss">
+.app-container {
+ padding: 20px;
+
+ .table-operations {
+ text-align: right;
+ margin-bottom: 20px;
+
+ .el-button {
+ margin-right: 10px;
+ }
+ }
+
+ .table_list {
+ background: #fff;
+ border-radius: 4px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .text-danger {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+
+ .text-warning {
+ color: #e6a23c;
+ font-weight: bold;
+ }
+
+ .text-success {
+ color: #67c23a;
+ font-weight: bold;
+ }
+
+ .dialog-footer {
+ text-align: right;
+ }
+
+ // 鍊掕鏃舵牱寮�
+ .countdown-timer {
+ font-weight: bold;
+ }
+
+ .countdown-normal {
+ color: #67c23a;
+ }
+
+ .countdown-warning {
+ color: #e6a23c;
+ }
+
+ .countdown-urgent {
+ color: #f56c6c;
+ animation: blink 1s infinite;
+ }
+
+ .countdown-expired {
+ color: #f56c6c;
+ font-weight: bold;
+ }
+
+ @keyframes blink {
+ 0%, 50% { opacity: 1; }
+ 51%, 100% { opacity: 0.5; }
+ }
+
+ // 缂烘皵棰勮寮规鏍峰紡
+ .shortage-warning-content {
+ text-align: center;
+ padding: 20px 0;
+
+ .warning-icon {
+ margin-bottom: 20px;
+ }
+
+ .warning-message {
+ h3 {
+ color: #f56c6c;
+ margin-bottom: 10px;
+ }
+
+ p {
+ margin-bottom: 10px;
+ color: #606266;
+ }
+
+ .warning-details {
+ background: #f5f7fa;
+ padding: 15px;
+ border-radius: 4px;
+ text-align: left;
+ font-size: 14px;
+ line-height: 1.6;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/inventoryManagement/transportTaskManagement/index.vue b/src/views/inventoryManagement/transportTaskManagement/index.vue
new file mode 100644
index 0000000..8e73004
--- /dev/null
+++ b/src/views/inventoryManagement/transportTaskManagement/index.vue
@@ -0,0 +1,692 @@
+<template>
+ <div class="app-container">
+ <!-- 缁熻姒傝 -->
+ <el-row :gutter="16" style="margin-bottom: 16px">
+ <el-col :span="6">
+ <el-card shadow="never">
+ <div>鎬讳换鍔℃暟</div>
+ <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
+ {{ totalTasks }}
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card shadow="never">
+ <div>杩涜涓换鍔�</div>
+ <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
+ {{ runningTasks }}
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card shadow="never">
+ <div>宸插畬鎴愪换鍔�</div>
+ <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
+ {{ finishedTasks }}
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card shadow="never">
+ <div>瀹屾垚鐜�</div>
+ <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
+ {{ completionRate }}%
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 鏌ヨ鏉′欢 -->
+ <div class="search_form">
+ <div>
+ <span class="search_title">浠诲姟缂栧彿锛�</span>
+ <el-input
+ v-model="searchForm.taskNo"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ヤ换鍔$紪鍙�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+
+ <span class="search_title ml10">杞﹁締缂栧彿锛�</span>
+ <el-input
+ v-model="searchForm.vehicleCode"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ヨ溅杈嗙紪鍙�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+
+ <span class="search_title ml10">浠诲姟鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ @change="handleQuery"
+ />
+
+ <span class="search_title ml10">鐘舵�侊細</span>
+ <el-select
+ v-model="searchForm.status"
+ style="width: 140px"
+ placeholder="璇烽�夋嫨浠诲姟鐘舵��"
+ clearable
+ >
+ <el-option
+ v-for="item in statusOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div>
+ <el-button type="primary" icon="Plus" @click="openAdd">
+ 鏂板缓杩愯緭浠诲姟
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 琛ㄦ牸 -->
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ style="width: 100%"
+ height="calc(100vh - 22em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ :row-class-name="tableRowClassName"
+ >
+ <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+ <el-table-column
+ prop="taskNo"
+ label="浠诲姟缂栧彿"
+ width="150"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="outboundOrderNo"
+ label="鍑哄簱璁㈠崟鍙�"
+ width="180"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="vehicleCode"
+ label="杞﹁締缂栧彿"
+ width="130"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="plateNumber"
+ label="杞︾墝鍙风爜"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="driverName"
+ label="鍙告満"
+ width="100"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="loadAddress"
+ label="瑁呰揣鍦扮偣"
+ width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="deliveryAddress"
+ label="閫佽揣鍦扮偣"
+ width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="loadTime"
+ label="瑁呰揣鏃堕棿"
+ width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="deliveryTime"
+ label="閫佽揣鏃堕棿"
+ width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="signTime"
+ label="绛炬敹鏃堕棿"
+ width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column label="鐘舵��" width="110" align="center">
+ <template #default="scope">
+ <el-tag :type="statusTagType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="杩涘害" width="150" align="center">
+ <template #default="scope">
+ <el-progress
+ :percentage="scope.row.progress"
+ :status="scope.row.status === '宸插畬鎴�' ? 'success' : undefined"
+ :stroke-width="12"
+ :show-text="false"
+ />
+ <div style="font-size: 12px; margin-top: 4px">
+ {{ scope.row.progress }}%
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" fixed="right" width="160" align="center">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ size="small"
+ @click="openEdit(scope.row)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="danger"
+ link
+ size="small"
+ @click="removeRow(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 鏂板/缂栬緫寮圭獥 -->
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="780px"
+ destroy-on-close
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="110px"
+ label-position="right"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浠诲姟缂栧彿锛�" prop="taskNo">
+ <el-input
+ v-model="form.taskNo"
+ placeholder="璇疯緭鍏ヤ换鍔$紪鍙�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍑哄簱璁㈠崟鍙凤細" prop="outboundOrderNo">
+ <el-input
+ v-model="form.outboundOrderNo"
+ placeholder="璇疯緭鍏ュ叧鑱斿嚭搴撹鍗曞彿"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="杞﹁締缂栧彿锛�" prop="vehicleCode">
+ <el-input
+ v-model="form.vehicleCode"
+ placeholder="璇疯緭鍏ヨ溅杈嗙紪鍙�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杞︾墝鍙风爜锛�" prop="plateNumber">
+ <el-input
+ v-model="form.plateNumber"
+ placeholder="璇疯緭鍏ヨ溅鐗屽彿鐮�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙告満锛�" prop="driverName">
+ <el-input
+ v-model="form.driverName"
+ placeholder="璇疯緭鍏ュ徃鏈哄鍚�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍙告満鐢佃瘽锛�" prop="driverPhone">
+ <el-input
+ v-model="form.driverPhone"
+ placeholder="璇疯緭鍏ュ徃鏈鸿仈绯荤數璇�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瑁呰揣鍦扮偣锛�" prop="loadAddress">
+ <el-input
+ v-model="form.loadAddress"
+ placeholder="璇疯緭鍏ヨ璐у湴鐐�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閫佽揣鍦扮偣锛�" prop="deliveryAddress">
+ <el-input
+ v-model="form.deliveryAddress"
+ placeholder="璇疯緭鍏ラ�佽揣鍦扮偣"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="瑁呰揣鏃堕棿锛�" prop="loadTime">
+ <el-date-picker
+ v-model="form.loadTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm"
+ placeholder="璇烽�夋嫨瑁呰揣鏃堕棿"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="閫佽揣鏃堕棿锛�" prop="deliveryTime">
+ <el-date-picker
+ v-model="form.deliveryTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm"
+ placeholder="璇烽�夋嫨閫佽揣鏃堕棿"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="绛炬敹鏃堕棿锛�" prop="signTime">
+ <el-date-picker
+ v-model="form.signTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm"
+ placeholder="璇烽�夋嫨绛炬敹鏃堕棿"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐘舵�侊細" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨浠诲姟鐘舵��">
+ <el-option
+ v-for="item in statusOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁″垝鏃ユ湡锛�" prop="planDate">
+ <el-date-picker
+ v-model="form.planDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨璁″垝鏃ユ湡"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="handleCancel">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleSubmit">淇� 瀛�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+// 妯℃嫙杩愯緭浠诲姟鏁版嵁
+const rawTasks = ref([
+ {
+ id: 1,
+ taskNo: "T2024-1201-001",
+ outboundOrderNo: "OUT-2024-1201-1001",
+ vehicleCode: "CL-202401",
+ plateNumber: "绮12345",
+ driverName: "寮犲笀鍌�",
+ driverPhone: "13800000001",
+ loadAddress: "娣卞湷浠撳簱A鍖�",
+ deliveryAddress: "骞垮窞瀹㈡埛涓�閮�",
+ planDate: "2024-12-01",
+ loadTime: "2024-12-01 09:00:00",
+ deliveryTime: "2024-12-01 14:30:00",
+ signTime: "2024-12-01 15:00:00",
+ status: "宸插畬鎴�",
+ },
+ {
+ id: 2,
+ taskNo: "T2024-1201-002",
+ outboundOrderNo: "OUT-2024-1201-1002",
+ vehicleCode: "CL-202402",
+ plateNumber: "绮67890",
+ driverName: "鏉庡笀鍌�",
+ driverPhone: "13800000002",
+ loadAddress: "娣卞湷浠撳簱B鍖�",
+ deliveryAddress: "涓滆帪瀹㈡埛浜岄儴",
+ planDate: "2024-12-01",
+ loadTime: "2024-12-01 10:00:00",
+ deliveryTime: "2024-12-01 13:00:00",
+ signTime: "",
+ status: "杩愯緭涓�",
+ },
+ {
+ id: 3,
+ taskNo: "T2024-1202-001",
+ outboundOrderNo: "OUT-2024-1202-1003",
+ vehicleCode: "CL-202401",
+ plateNumber: "绮12345",
+ driverName: "寮犲笀鍌�",
+ driverPhone: "13800000001",
+ loadAddress: "娣卞湷浠撳簱A鍖�",
+ deliveryAddress: "浣涘北瀹㈡埛涓夐儴",
+ planDate: "2024-12-02",
+ loadTime: "2024-12-02 08:30:00",
+ deliveryTime: "",
+ signTime: "",
+ status: "寰呭彂杞�",
+ },
+ {
+ id: 4,
+ taskNo: "T2024-1203-001",
+ outboundOrderNo: "OUT-2024-1203-1004",
+ vehicleCode: "CL-202403",
+ plateNumber: "绮11223",
+ driverName: "鐜嬪笀鍌�",
+ driverPhone: "13800000003",
+ loadAddress: "娣卞湷浠撳簱C鍖�",
+ deliveryAddress: "鎯犲窞瀹㈡埛鍥涢儴",
+ planDate: "2024-12-03",
+ loadTime: "",
+ deliveryTime: "",
+ signTime: "",
+ status: "鏈紑濮�",
+ },
+]);
+
+// 鐘舵�佹灇涓�
+const statusOptions = [
+ { label: "鏈紑濮�", value: "鏈紑濮�" },
+ { label: "寰呭彂杞�", value: "寰呭彂杞�" },
+ { label: "杩愯緭涓�", value: "杩愯緭涓�" },
+ { label: "寰呯鏀�", value: "寰呯鏀�" },
+ { label: "宸插畬鎴�", value: "宸插畬鎴�" },
+];
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ taskNo: "",
+ vehicleCode: "",
+ dateRange: [],
+ status: "",
+});
+
+// 琛ㄦ牸鏁版嵁锛堝甫杩涘害绛夎绠楀瓧娈碉級
+const tableData = ref([]);
+
+// 缁熻
+const totalTasks = computed(() => rawTasks.value.length);
+const finishedTasks = computed(
+ () => rawTasks.value.filter((t) => t.status === "宸插畬鎴�").length
+);
+const runningTasks = computed(
+ () =>
+ rawTasks.value.filter((t) =>
+ ["寰呭彂杞�", "杩愯緭涓�", "寰呯鏀�"].includes(t.status)
+ ).length
+);
+const completionRate = computed(() => {
+ if (!totalTasks.value) return 0;
+ return Math.round((finishedTasks.value / totalTasks.value) * 100);
+});
+
+// 璁$畻鍗曟潯浠诲姟杩涘害
+const computeProgress = (task) => {
+ if (task.status === "宸插畬鎴�" || task.signTime) return 100;
+ if (task.status === "寰呯鏀�" || task.deliveryTime) return 80;
+ if (task.status === "杩愯緭涓�") return 60;
+ if (task.status === "寰呭彂杞�" || task.loadTime) return 30;
+ if (task.status === "鏈紑濮�") return 0;
+ return 0;
+};
+
+// 鐘舵�� tag 鏍峰紡
+const statusTagType = (status) => {
+ if (status === "宸插畬鎴�") return "success";
+ if (status === "杩愯緭涓�") return "warning";
+ if (status === "寰呯鏀�" || status === "寰呭彂杞�") return "info";
+ return "default";
+};
+
+// 閲嶇畻琛ㄦ牸鏁版嵁
+const recomputeTable = () => {
+ const filtered = rawTasks.value
+ .filter((t) => {
+ if (searchForm.taskNo && !t.taskNo.includes(searchForm.taskNo.trim())) {
+ return false;
+ }
+ if (
+ searchForm.vehicleCode &&
+ !t.vehicleCode.includes(searchForm.vehicleCode.trim())
+ ) {
+ return false;
+ }
+ if (searchForm.status && t.status !== searchForm.status) {
+ return false;
+ }
+ if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
+ const [start, end] = searchForm.dateRange;
+ if (!t.planDate || t.planDate < start || t.planDate > end) {
+ return false;
+ }
+ }
+ return true;
+ })
+ .map((t) => ({
+ ...t,
+ progress: computeProgress(t),
+ }));
+
+ tableData.value = filtered;
+};
+
+// 鏌ヨ
+const handleQuery = () => {
+ recomputeTable();
+};
+
+const resetSearch = () => {
+ searchForm.taskNo = "";
+ searchForm.vehicleCode = "";
+ searchForm.dateRange = [];
+ searchForm.status = "";
+ recomputeTable();
+};
+
+// 琛屾牱寮�
+const tableRowClassName = ({ row }) => {
+ if (row.status === "宸插畬鎴�") {
+ return "row-finished";
+ }
+ if (row.status === "杩愯緭涓�") {
+ return "row-running";
+ }
+ return "";
+};
+
+// 寮圭獥 & 琛ㄥ崟
+const dialogVisible = ref(false);
+const dialogTitle = ref("鏂板缓杩愯緭浠诲姟");
+const isEdit = ref(false);
+const formRef = ref(null);
+const form = reactive({
+ id: null,
+ taskNo: "",
+ outboundOrderNo: "",
+ vehicleCode: "",
+ plateNumber: "",
+ driverName: "",
+ driverPhone: "",
+ loadAddress: "",
+ deliveryAddress: "",
+ planDate: "",
+ loadTime: "",
+ deliveryTime: "",
+ signTime: "",
+ status: "鏈紑濮�",
+});
+
+const rules = {
+ taskNo: [{ required: true, message: "璇疯緭鍏ヤ换鍔$紪鍙�", trigger: "blur" }],
+ outboundOrderNo: [
+ { required: true, message: "璇疯緭鍏ュ嚭搴撹鍗曞彿", trigger: "blur" },
+ ],
+ vehicleCode: [{ required: true, message: "璇疯緭鍏ヨ溅杈嗙紪鍙�", trigger: "blur" }],
+ plateNumber: [{ required: true, message: "璇疯緭鍏ヨ溅鐗屽彿鐮�", trigger: "blur" }],
+ driverName: [{ required: true, message: "璇疯緭鍏ュ徃鏈哄鍚�", trigger: "blur" }],
+ loadAddress: [{ required: true, message: "璇疯緭鍏ヨ璐у湴鐐�", trigger: "blur" }],
+ deliveryAddress: [
+ { required: true, message: "璇疯緭鍏ラ�佽揣鍦扮偣", trigger: "blur" },
+ ],
+ planDate: [{ required: true, message: "璇烽�夋嫨璁″垝鏃ユ湡", trigger: "change" }],
+};
+
+// 鏂板缓
+const openAdd = () => {
+ dialogTitle.value = "鏂板缓杩愯緭浠诲姟";
+ isEdit.value = false;
+ Object.assign(form, {
+ id: null,
+ taskNo: "",
+ outboundOrderNo: "",
+ vehicleCode: "",
+ plateNumber: "",
+ driverName: "",
+ driverPhone: "",
+ loadAddress: "",
+ deliveryAddress: "",
+ planDate: "",
+ loadTime: "",
+ deliveryTime: "",
+ signTime: "",
+ status: "鏈紑濮�",
+ });
+ dialogVisible.value = true;
+};
+
+// 缂栬緫
+const openEdit = (row) => {
+ dialogTitle.value = "缂栬緫杩愯緭浠诲姟";
+ isEdit.value = true;
+ Object.assign(form, row);
+ dialogVisible.value = true;
+};
+
+// 淇濆瓨
+const handleSubmit = () => {
+ if (!formRef.value) return;
+ formRef.value.validate((valid) => {
+ if (!valid) return;
+
+ if (isEdit.value) {
+ const index = rawTasks.value.findIndex((t) => t.id === form.id);
+ if (index !== -1) {
+ rawTasks.value[index] = { ...form };
+ }
+ ElMessage.success("杩愯緭浠诲姟宸叉洿鏂�");
+ } else {
+ const newId = rawTasks.value.length
+ ? Math.max(...rawTasks.value.map((t) => t.id)) + 1
+ : 1;
+ rawTasks.value.push({ ...form, id: newId });
+ ElMessage.success("杩愯緭浠诲姟宸叉柊澧�");
+ }
+ dialogVisible.value = false;
+ recomputeTable();
+ });
+};
+
+const handleCancel = () => {
+ dialogVisible.value = false;
+};
+
+// 鍒犻櫎
+const removeRow = (row) => {
+ ElMessageBox.confirm("鏄惁纭鍒犻櫎璇ヨ繍杈撲换鍔★紵", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ rawTasks.value = rawTasks.value.filter((t) => t.id !== row.id);
+ recomputeTable();
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ })
+ .catch(() => {});
+};
+
+onMounted(() => {
+ recomputeTable();
+});
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+ text-align: right;
+}
+
+:deep(.row-finished) {
+ background-color: #f6ffed;
+}
+
+:deep(.row-running) {
+ background-color: #fffbe6;
+}
+</style>
+
diff --git a/src/views/inventoryManagement/vehicleFuelManagement/index.vue b/src/views/inventoryManagement/vehicleFuelManagement/index.vue
new file mode 100644
index 0000000..eaf543c
--- /dev/null
+++ b/src/views/inventoryManagement/vehicleFuelManagement/index.vue
@@ -0,0 +1,556 @@
+<template>
+ <div class="app-container">
+ <!-- 鏌ヨ鏉′欢 -->
+ <div class="search_form">
+ <div>
+ <span class="search_title">杞﹁締缂栧彿锛�</span>
+ <el-input
+ v-model="searchForm.vehicleCode"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ヨ溅杈嗙紪鍙�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+
+ <span class="search_title ml10">鍔犳补鏃堕棿锛�</span>
+ <el-date-picker
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ @change="handleQuery"
+ />
+
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div>
+ <el-button type="primary" icon="Plus" @click="openAdd">
+ 鏂板鍔犳补璁板綍
+ </el-button>
+ </div>
+ </div>
+
+ <!-- 琛ㄦ牸 -->
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ style="width: 100%"
+ height="calc(100vh - 18.5em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ :row-class-name="tableRowClassName"
+ >
+ <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+ <el-table-column
+ prop="vehicleCode"
+ label="杞﹁締缂栧彿"
+ width="130"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="plateNumber"
+ label="杞︾墝鍙风爜"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="fuelDate"
+ label="鍔犳补鏃ユ湡"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="gunNo"
+ label="娌规灙鍙�"
+ width="90"
+ align="center"
+ />
+ <el-table-column
+ prop="amount"
+ label="閲戦(鍏�)"
+ width="100"
+ align="right"
+ >
+ <template #default="scope">
+ <span>{{ scope.row.amount?.toFixed(2) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ prop="liters"
+ label="鍗囨暟(L)"
+ width="90"
+ align="right"
+ >
+ <template #default="scope">
+ <span>{{ scope.row.liters?.toFixed(2) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ prop="startMileage"
+ label="璧峰閲岀▼(km)"
+ width="120"
+ align="right"
+ />
+ <el-table-column
+ prop="endMileage"
+ label="缁撴潫閲岀▼(km)"
+ width="120"
+ align="right"
+ />
+ <el-table-column
+ prop="distance"
+ label="琛岄┒閲岀▼(km)"
+ width="120"
+ align="right"
+ />
+ <el-table-column
+ prop="fuelConsumption"
+ label="娌硅��(L/100km)"
+ width="130"
+ align="center"
+ >
+ <template #default="scope">
+ <span
+ :style="scope.row.isAbnormal ? 'color:#F56C6C;font-weight:600;' : ''"
+ >
+ {{ scope.row.fuelConsumption != null ? scope.row.fuelConsumption.toFixed(2) : '-' }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ prop="avgConsumption"
+ label="杞﹁締骞冲潎娌硅��"
+ width="130"
+ align="center"
+ >
+ <template #default="scope">
+ <span>
+ {{ scope.row.avgConsumption != null ? scope.row.avgConsumption.toFixed(2) : '-' }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="寮傚父棰勮" width="100" align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.isAbnormal" type="danger" size="small">
+ 寮傚父
+ </el-tag>
+ <span v-else>-</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" fixed="right" width="140" align="center">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ size="small"
+ @click="openEdit(scope.row)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="danger"
+ link
+ size="small"
+ @click="removeRow(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 鏂板/缂栬緫寮圭獥 -->
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="640px"
+ destroy-on-close
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="110px"
+ label-position="right"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="杞﹁締缂栧彿锛�" prop="vehicleCode">
+ <el-input
+ v-model="form.vehicleCode"
+ placeholder="璇疯緭鍏ヨ溅杈嗙紪鍙�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杞︾墝鍙风爜锛�" prop="plateNumber">
+ <el-input
+ v-model="form.plateNumber"
+ placeholder="璇疯緭鍏ヨ溅鐗屽彿鐮�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍔犳补鏃ユ湡锛�" prop="fuelDate">
+ <el-date-picker
+ v-model="form.fuelDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨鍔犳补鏃ユ湡"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="娌规灙鍙凤細" prop="gunNo">
+ <el-input
+ v-model="form.gunNo"
+ placeholder="璇疯緭鍏ユ补鏋彿"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閲戦(鍏�)锛�" prop="amount">
+ <el-input-number
+ v-model="form.amount"
+ :min="0"
+ :step="0.01"
+ :precision="2"
+ placeholder="璇疯緭鍏ラ噾棰�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍗囨暟(L)锛�" prop="liters">
+ <el-input-number
+ v-model="form.liters"
+ :min="0"
+ :step="0.01"
+ :precision="2"
+ placeholder="璇疯緭鍏ュ崌鏁�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璧峰閲岀▼(km)锛�" prop="startMileage">
+ <el-input-number
+ v-model="form.startMileage"
+ :min="0"
+ :step="1"
+ placeholder="璇疯緭鍏ヨ捣濮嬮噷绋�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁撴潫閲岀▼(km)锛�" prop="endMileage">
+ <el-input-number
+ v-model="form.endMileage"
+ :min="0"
+ :step="1"
+ placeholder="璇疯緭鍏ョ粨鏉熼噷绋�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="handleCancel">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleSubmit">淇� 瀛�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+// 妯℃嫙鍔犳补璁板綍鏁版嵁
+const rawRecords = ref([
+ {
+ id: 1,
+ vehicleCode: "CL-202401",
+ plateNumber: "绮12345",
+ fuelDate: "2024-12-01",
+ gunNo: "01",
+ amount: 500,
+ liters: 70,
+ startMileage: 12000,
+ endMileage: 12600,
+ },
+ {
+ id: 2,
+ vehicleCode: "CL-202401",
+ plateNumber: "绮12345",
+ fuelDate: "2024-12-15",
+ gunNo: "02",
+ amount: 520,
+ liters: 72,
+ startMileage: 12600,
+ endMileage: 13250,
+ },
+ {
+ id: 3,
+ vehicleCode: "CL-202402",
+ plateNumber: "绮67890",
+ fuelDate: "2024-12-05",
+ gunNo: "03",
+ amount: 430,
+ liters: 60,
+ startMileage: 8000,
+ endMileage: 8520,
+ },
+ {
+ id: 4,
+ vehicleCode: "CL-202402",
+ plateNumber: "绮67890",
+ fuelDate: "2024-12-20",
+ gunNo: "01",
+ amount: 450,
+ liters: 63,
+ startMileage: 8520,
+ endMileage: 9000,
+ },
+ {
+ id: 5,
+ vehicleCode: "CL-202401",
+ plateNumber: "绮12345",
+ fuelDate: "2025-01-05",
+ gunNo: "01",
+ amount: 700,
+ liters: 90,
+ startMileage: 13250,
+ endMileage: 13600, // 鏄庢樉寮傚父娌硅��
+ },
+]);
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ vehicleCode: "",
+ dateRange: [],
+});
+
+// 琛ㄦ牸鏁版嵁锛堝寘鍚绠楀瓧娈碉級
+const tableData = ref([]);
+
+// 寮圭獥 & 琛ㄥ崟
+const dialogVisible = ref(false);
+const dialogTitle = ref("鏂板鍔犳补璁板綍");
+const isEdit = ref(false);
+const formRef = ref(null);
+const form = reactive({
+ id: null,
+ vehicleCode: "",
+ plateNumber: "",
+ fuelDate: "",
+ gunNo: "",
+ amount: null,
+ liters: null,
+ startMileage: null,
+ endMileage: null,
+});
+
+const rules = {
+ vehicleCode: [{ required: true, message: "璇疯緭鍏ヨ溅杈嗙紪鍙�", trigger: "blur" }],
+ plateNumber: [{ required: true, message: "璇疯緭鍏ヨ溅鐗屽彿鐮�", trigger: "blur" }],
+ fuelDate: [{ required: true, message: "璇烽�夋嫨鍔犳补鏃ユ湡", trigger: "change" }],
+ gunNo: [{ required: true, message: "璇疯緭鍏ユ补鏋彿", trigger: "blur" }],
+ amount: [{ required: true, message: "璇疯緭鍏ラ噾棰�", trigger: "blur" }],
+ liters: [{ required: true, message: "璇疯緭鍏ュ崌鏁�", trigger: "blur" }],
+ startMileage: [{ required: true, message: "璇疯緭鍏ヨ捣濮嬮噷绋�", trigger: "blur" }],
+ endMileage: [{ required: true, message: "璇疯緭鍏ョ粨鏉熼噷绋�", trigger: "blur" }],
+};
+
+// 閲嶆柊璁$畻娌硅�椼�佸钩鍧囨补鑰楀拰寮傚父棰勮
+const recomputeTable = () => {
+ const records = rawRecords.value;
+
+ // 1. 鍏堟寜杞﹁締缁熻骞冲潎娌硅��
+ const stats = {};
+ records.forEach((r) => {
+ const distance = r.endMileage - r.startMileage;
+ if (distance <= 0 || !r.liters) return;
+ const cons = (r.liters / distance) * 100; // L/100km
+ if (!stats[r.vehicleCode]) {
+ stats[r.vehicleCode] = { totalCons: 0, count: 0 };
+ }
+ stats[r.vehicleCode].totalCons += cons;
+ stats[r.vehicleCode].count += 1;
+ });
+
+ const avgMap = {};
+ Object.keys(stats).forEach((key) => {
+ avgMap[key] = stats[key].totalCons / stats[key].count;
+ });
+
+ // 2. 鎸夌瓫閫夋潯浠惰繃婊ゅ苟琛ュ厖璁$畻瀛楁
+ const filtered = records
+ .filter((r) => {
+ if (
+ searchForm.vehicleCode &&
+ !r.vehicleCode.includes(searchForm.vehicleCode.trim())
+ ) {
+ return false;
+ }
+ if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
+ const [start, end] = searchForm.dateRange;
+ if (r.fuelDate < start || r.fuelDate > end) {
+ return false;
+ }
+ }
+ return true;
+ })
+ .map((r) => {
+ const distance = r.endMileage - r.startMileage;
+ const fuelConsumption =
+ distance > 0 && r.liters
+ ? (r.liters / distance) * 100
+ : null;
+ const avgConsumption =
+ avgMap[r.vehicleCode] != null ? avgMap[r.vehicleCode] : null;
+ const isAbnormal =
+ avgConsumption != null &&
+ fuelConsumption != null &&
+ fuelConsumption > avgConsumption * 1.2;
+
+ return {
+ ...r,
+ distance,
+ fuelConsumption,
+ avgConsumption,
+ isAbnormal,
+ };
+ });
+
+ tableData.value = filtered;
+};
+
+// 鏌ヨ
+const handleQuery = () => {
+ recomputeTable();
+};
+
+const resetSearch = () => {
+ searchForm.vehicleCode = "";
+ searchForm.dateRange = [];
+ recomputeTable();
+};
+
+// 琛屾牱寮忥紙寮傚父楂樹寒锛�
+const tableRowClassName = ({ row }) => {
+ if (row.isAbnormal) {
+ return "row-abnormal";
+ }
+ return "";
+};
+
+// 鏂板
+const openAdd = () => {
+ dialogTitle.value = "鏂板鍔犳补璁板綍";
+ isEdit.value = false;
+ Object.assign(form, {
+ id: null,
+ vehicleCode: "",
+ plateNumber: "",
+ fuelDate: "",
+ gunNo: "",
+ amount: null,
+ liters: null,
+ startMileage: null,
+ endMileage: null,
+ });
+ dialogVisible.value = true;
+};
+
+// 缂栬緫
+const openEdit = (row) => {
+ dialogTitle.value = "缂栬緫鍔犳补璁板綍";
+ isEdit.value = true;
+ Object.assign(form, row);
+ dialogVisible.value = true;
+};
+
+// 淇濆瓨
+const handleSubmit = () => {
+ if (!formRef.value) return;
+ formRef.value.validate((valid) => {
+ if (!valid) return;
+
+ if (form.endMileage <= form.startMileage) {
+ ElMessage.warning("缁撴潫閲岀▼蹇呴』澶т簬璧峰閲岀▼");
+ return;
+ }
+
+ if (isEdit.value) {
+ const index = rawRecords.value.findIndex((r) => r.id === form.id);
+ if (index !== -1) {
+ rawRecords.value[index] = { ...form };
+ }
+ ElMessage.success("鍔犳补璁板綍宸叉洿鏂�");
+ } else {
+ const newId = rawRecords.value.length
+ ? Math.max(...rawRecords.value.map((r) => r.id)) + 1
+ : 1;
+ rawRecords.value.push({ ...form, id: newId });
+ ElMessage.success("鍔犳补璁板綍宸叉柊澧�");
+ }
+ dialogVisible.value = false;
+ recomputeTable();
+ });
+};
+
+const handleCancel = () => {
+ dialogVisible.value = false;
+};
+
+// 鍒犻櫎
+const removeRow = (row) => {
+ ElMessageBox.confirm("鏄惁纭鍒犻櫎璇ュ姞娌硅褰曪紵", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ rawRecords.value = rawRecords.value.filter((r) => r.id !== row.id);
+ recomputeTable();
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ })
+ .catch(() => {});
+};
+
+onMounted(() => {
+ recomputeTable();
+});
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+ text-align: right;
+}
+
+:deep(.row-abnormal) {
+ background-color: #fff5f5;
+}
+</style>
+
diff --git a/src/views/inventoryManagement/vehicleManagement/index.vue b/src/views/inventoryManagement/vehicleManagement/index.vue
new file mode 100644
index 0000000..1e383c6
--- /dev/null
+++ b/src/views/inventoryManagement/vehicleManagement/index.vue
@@ -0,0 +1,581 @@
+<template>
+ <div class="app-container">
+ <!-- 鏌ヨ鏉′欢 -->
+ <div class="search_form">
+ <div>
+ <span class="search_title">杞︾墝鍙风爜锛�</span>
+ <el-input
+ v-model="searchForm.plateNumber"
+ style="width: 180px"
+ placeholder="璇疯緭鍏ヨ溅鐗屽彿鐮�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+
+ <span class="search_title ml10">杞﹁締绫诲瀷锛�</span>
+ <el-select
+ v-model="searchForm.vehicleType"
+ style="width: 160px"
+ placeholder="璇烽�夋嫨杞﹁締绫诲瀷"
+ clearable
+ >
+ <el-option
+ v-for="item in vehicleTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+
+ <span class="search_title ml10">鎵�灞為儴闂細</span>
+ <el-select
+ v-model="searchForm.department"
+ style="width: 160px"
+ placeholder="璇烽�夋嫨鎵�灞為儴闂�"
+ clearable
+ >
+ <el-option
+ v-for="item in departmentOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+
+ <span class="search_title ml10">鐘舵�侊細</span>
+ <el-select
+ v-model="searchForm.status"
+ style="width: 140px"
+ placeholder="璇烽�夋嫨鐘舵��"
+ clearable
+ >
+ <el-option
+ v-for="item in statusOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+
+ <span class="search_title ml10">褰掓。鐘舵�侊細</span>
+ <el-select
+ v-model="searchForm.archived"
+ style="width: 140px"
+ placeholder="璇烽�夋嫨褰掓。鐘舵��"
+ clearable
+ >
+ <el-option label="鏈綊妗�" value="false" />
+ <el-option label="宸插綊妗�" value="true" />
+ </el-select>
+
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div>
+ <el-button type="primary" icon="Plus" @click="openAdd">鏂板杞﹁締</el-button>
+ </div>
+ </div>
+
+ <!-- 琛ㄦ牸 -->
+ <div class="table_list">
+ <el-table
+ :data="tableData"
+ border
+ style="width: 100%"
+ height="calc(100vh - 18.5em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ >
+ <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+ <el-table-column
+ prop="vehicleCode"
+ label="杞﹁締缂栧彿"
+ width="140"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="plateNumber"
+ label="杞︾墝鍙风爜"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="vehicleType"
+ label="杞﹁締绫诲瀷"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="department"
+ label="鎵�灞為儴闂�"
+ width="140"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="purchaseDate"
+ label="璐疆鏃ユ湡"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="licenseNumber"
+ label="琛岄┒璇佺紪鍙�"
+ width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="licenseIssueDate"
+ label="鍙戣瘉鏃ユ湡"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="licenseExpireDate"
+ label="鍒版湡鏃ユ湡"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column label="鐘舵��" width="100" align="center">
+ <template #default="scope">
+ <el-tag :type="statusTagType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="褰掓。鐘舵��" width="100" align="center">
+ <template #default="scope">
+ <el-tag :type="scope.row.archived ? 'info' : 'success'">
+ {{ scope.row.archived ? '宸插綊妗�' : '鏈綊妗�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" fixed="right" width="220" align="center">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ link
+ size="small"
+ @click="openEdit(scope.row)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="warning"
+ link
+ size="small"
+ :disabled="scope.row.archived"
+ @click="archiveRow(scope.row)"
+ >
+ 褰掓。
+ </el-button>
+ <el-button
+ type="danger"
+ link
+ size="small"
+ @click="removeRow(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <!-- 鏂板/缂栬緫寮圭獥 -->
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="600px"
+ destroy-on-close
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px"
+ label-position="right"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="杞﹁締缂栧彿锛�" prop="vehicleCode">
+ <el-input v-model="form.vehicleCode" placeholder="璇疯緭鍏ヨ溅杈嗙紪鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="杞︾墝鍙风爜锛�" prop="plateNumber">
+ <el-input v-model="form.plateNumber" placeholder="璇疯緭鍏ヨ溅鐗屽彿鐮�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="杞﹁締绫诲瀷锛�" prop="vehicleType">
+ <el-select
+ v-model="form.vehicleType"
+ placeholder="璇烽�夋嫨杞﹁締绫诲瀷"
+ clearable
+ >
+ <el-option
+ v-for="item in vehicleTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎵�灞為儴闂細" prop="department">
+ <el-select
+ v-model="form.department"
+ placeholder="璇烽�夋嫨鎵�灞為儴闂�"
+ clearable
+ >
+ <el-option
+ v-for="item in departmentOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璐疆鏃ユ湡锛�" prop="purchaseDate">
+ <el-date-picker
+ v-model="form.purchaseDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨璐疆鏃ユ湡"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵�侊細" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option
+ v-for="item in statusOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="琛岄┒璇佺紪鍙凤細" prop="licenseNumber">
+ <el-input
+ v-model="form.licenseNumber"
+ placeholder="璇疯緭鍏ヨ椹惰瘉缂栧彿"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍙戣瘉鏃ユ湡锛�" prop="licenseIssueDate">
+ <el-date-picker
+ v-model="form.licenseIssueDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨鍙戣瘉鏃ユ湡"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒版湡鏃ユ湡锛�" prop="licenseExpireDate">
+ <el-date-picker
+ v-model="form.licenseExpireDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨鍒版湡鏃ユ湡"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="handleCancel">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleSubmit">淇� 瀛�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+// 妯℃嫙杞﹁締鍩虹鏁版嵁
+const allVehicles = ref([
+ {
+ id: 1,
+ vehicleCode: "CL-202401",
+ plateNumber: "绮12345",
+ vehicleType: "鍘㈠紡璐ц溅",
+ department: "鐗╂祦涓�閮�",
+ purchaseDate: "2022-03-15",
+ licenseNumber: "4401-2022-0001",
+ licenseIssueDate: "2022-03-10",
+ licenseExpireDate: "2026-03-10",
+ status: "鍦ㄧ敤",
+ archived: false,
+ },
+ {
+ id: 2,
+ vehicleCode: "CL-202402",
+ plateNumber: "绮67890",
+ vehicleType: "鍐疯棌杞�",
+ department: "鐗╂祦浜岄儴",
+ purchaseDate: "2021-08-01",
+ licenseNumber: "4401-2021-0123",
+ licenseIssueDate: "2021-07-28",
+ licenseExpireDate: "2025-07-28",
+ status: "缁翠慨",
+ archived: false,
+ },
+ {
+ id: 3,
+ vehicleCode: "CL-202403",
+ plateNumber: "绮11223",
+ vehicleType: "鐗靛紩杞�",
+ department: "椤圭洰杩愯緭閮�",
+ purchaseDate: "2020-05-20",
+ licenseNumber: "4401-2020-0456",
+ licenseIssueDate: "2020-05-18",
+ licenseExpireDate: "2024-05-18",
+ status: "闂茬疆",
+ archived: false,
+ },
+ {
+ id: 4,
+ vehicleCode: "CL-202404",
+ plateNumber: "绮33445",
+ vehicleType: "鍘㈠紡璐ц溅",
+ department: "璧勪骇绠$悊閮�",
+ purchaseDate: "2019-11-11",
+ licenseNumber: "4401-2019-0789",
+ licenseIssueDate: "2019-11-08",
+ licenseExpireDate: "2023-11-08",
+ status: "鍦ㄧ敤",
+ archived: true,
+ },
+]);
+
+// 涓嬫媺鏋氫妇
+const vehicleTypeOptions = [
+ { label: "鍘㈠紡璐ц溅", value: "鍘㈠紡璐ц溅" },
+ { label: "鍐疯棌杞�", value: "鍐疯棌杞�" },
+ { label: "鐗靛紩杞�", value: "鐗靛紩杞�" },
+ { label: "鍏朵粬", value: "鍏朵粬" },
+];
+
+const departmentOptions = [
+ { label: "鐗╂祦涓�閮�", value: "鐗╂祦涓�閮�" },
+ { label: "鐗╂祦浜岄儴", value: "鐗╂祦浜岄儴" },
+ { label: "椤圭洰杩愯緭閮�", value: "椤圭洰杩愯緭閮�" },
+ { label: "璧勪骇绠$悊閮�", value: "璧勪骇绠$悊閮�" },
+];
+
+const statusOptions = [
+ { label: "鍦ㄧ敤", value: "鍦ㄧ敤" },
+ { label: "闂茬疆", value: "闂茬疆" },
+ { label: "缁翠慨", value: "缁翠慨" },
+];
+
+// 鏌ヨ琛ㄥ崟
+const searchForm = reactive({
+ plateNumber: "",
+ vehicleType: "",
+ department: "",
+ status: "",
+ archived: "",
+});
+
+// 琛ㄦ牸鏁版嵁
+const tableData = ref([...allVehicles.value]);
+
+// 寮圭獥 & 琛ㄥ崟
+const dialogVisible = ref(false);
+const dialogTitle = ref("鏂板杞﹁締");
+const isEdit = ref(false);
+const formRef = ref(null);
+const form = reactive({
+ id: null,
+ vehicleCode: "",
+ plateNumber: "",
+ vehicleType: "",
+ department: "",
+ purchaseDate: "",
+ licenseNumber: "",
+ licenseIssueDate: "",
+ licenseExpireDate: "",
+ status: "鍦ㄧ敤",
+ archived: false,
+});
+
+const rules = {
+ vehicleCode: [{ required: true, message: "璇疯緭鍏ヨ溅杈嗙紪鍙�", trigger: "blur" }],
+ plateNumber: [{ required: true, message: "璇疯緭鍏ヨ溅鐗屽彿鐮�", trigger: "blur" }],
+ vehicleType: [{ required: true, message: "璇烽�夋嫨杞﹁締绫诲瀷", trigger: "change" }],
+ department: [{ required: true, message: "璇烽�夋嫨鎵�灞為儴闂�", trigger: "change" }],
+ purchaseDate: [{ required: true, message: "璇烽�夋嫨璐疆鏃ユ湡", trigger: "change" }],
+ status: [{ required: true, message: "璇烽�夋嫨鐘舵��", trigger: "change" }],
+ licenseNumber: [{ required: true, message: "璇疯緭鍏ヨ椹惰瘉缂栧彿", trigger: "blur" }],
+ licenseIssueDate: [{ required: true, message: "璇烽�夋嫨鍙戣瘉鏃ユ湡", trigger: "change" }],
+};
+
+// 鏌ヨ
+const handleQuery = () => {
+ tableData.value = allVehicles.value.filter((item) => {
+ if (
+ searchForm.plateNumber &&
+ !item.plateNumber.includes(searchForm.plateNumber.trim())
+ ) {
+ return false;
+ }
+ if (searchForm.vehicleType && item.vehicleType !== searchForm.vehicleType) {
+ return false;
+ }
+ if (searchForm.department && item.department !== searchForm.department) {
+ return false;
+ }
+ if (searchForm.status && item.status !== searchForm.status) {
+ return false;
+ }
+ if (searchForm.archived !== "") {
+ const targetArchived = searchForm.archived === "true";
+ if (item.archived !== targetArchived) return false;
+ }
+ return true;
+ });
+};
+
+const resetSearch = () => {
+ searchForm.plateNumber = "";
+ searchForm.vehicleType = "";
+ searchForm.department = "";
+ searchForm.status = "";
+ searchForm.archived = "";
+ handleQuery();
+};
+
+// 鏂板
+const openAdd = () => {
+ dialogTitle.value = "鏂板杞﹁締";
+ isEdit.value = false;
+ Object.assign(form, {
+ id: null,
+ vehicleCode: "",
+ plateNumber: "",
+ vehicleType: "",
+ department: "",
+ purchaseDate: "",
+ licenseInfo: "",
+ status: "鍦ㄧ敤",
+ archived: false,
+ });
+ dialogVisible.value = true;
+};
+
+// 缂栬緫
+const openEdit = (row) => {
+ dialogTitle.value = "缂栬緫杞﹁締";
+ isEdit.value = true;
+ Object.assign(form, row);
+ dialogVisible.value = true;
+};
+
+// 淇濆瓨
+const handleSubmit = () => {
+ if (!formRef.value) return;
+ formRef.value.validate((valid) => {
+ if (!valid) return;
+ if (isEdit.value) {
+ const index = allVehicles.value.findIndex((v) => v.id === form.id);
+ if (index !== -1) {
+ allVehicles.value[index] = { ...form };
+ }
+ ElMessage.success("杞﹁締淇℃伅宸叉洿鏂�");
+ } else {
+ const newId = allVehicles.value.length
+ ? Math.max(...allVehicles.value.map((v) => v.id)) + 1
+ : 1;
+ allVehicles.value.push({ ...form, id: newId });
+ ElMessage.success("杞﹁締淇℃伅宸叉柊澧�");
+ }
+ dialogVisible.value = false;
+ handleQuery();
+ });
+};
+
+const handleCancel = () => {
+ dialogVisible.value = false;
+};
+
+// 褰掓。
+const archiveRow = (row) => {
+ if (row.archived) return;
+ ElMessageBox.confirm(
+ "鏄惁纭灏嗚杞﹁締褰掓。锛熷綊妗e悗浠呬繚鐣欐煡璇紝涓嶅啀鍙備笌杩愯緭浠诲姟鍒嗛厤銆�",
+ "褰掓。鎻愮ず",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ row.archived = true;
+ if (row.status === "鍦ㄧ敤") {
+ row.status = "闂茬疆";
+ }
+ ElMessage.success("杞﹁締宸插綊妗�");
+ handleQuery();
+ })
+ .catch(() => {});
+};
+
+// 鍒犻櫎
+const removeRow = (row) => {
+ ElMessageBox.confirm("鏄惁纭鍒犻櫎璇ヨ溅杈嗗熀纭�淇℃伅锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ allVehicles.value = allVehicles.value.filter((v) => v.id !== row.id);
+ handleQuery();
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ })
+ .catch(() => {});
+};
+
+// 鐘舵�佹牱寮�
+const statusTagType = (status) => {
+ if (status === "鍦ㄧ敤") return "success";
+ if (status === "闂茬疆") return "info";
+ if (status === "缁翠慨") return "warning";
+ return "default";
+};
+</script>
+
+<style scoped lang="scss">
+.dialog-footer {
+ text-align: right;
+}
+</style>
+
diff --git a/src/views/lavorissue/ledger/Form.vue b/src/views/lavorissue/ledger/Form.vue
new file mode 100644
index 0000000..785ef7a
--- /dev/null
+++ b/src/views/lavorissue/ledger/Form.vue
@@ -0,0 +1,158 @@
+<template>
+ <el-form :model="form" label-width="100px" :rules="formRules" ref="formRef">
+ <el-form-item label="閮ㄩ棬鍚嶇О" prop="deptId">
+ <el-select
+ v-model="form.deptId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ disabled
+ >
+ <el-option :label="item.deptName" :value="item.deptId" v-for="(item,index) in productOptions" :key="deptId" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍛樺伐鍚嶇О" prop="staffId">
+ <el-select
+ v-model="form.staffId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option :label="item.staffName" :value="item.id" v-for="(item,index) in personList" :key="id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍔充繚绫诲瀷" prop="dictType">
+ <el-select
+ v-model="form.dictType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option :label="item.label" :value="item.value" v-for="(item,index) in sys_lavor_issue_type" :key="value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍔充繚闃插叿" prop="dictId">
+ <el-select
+ v-model="form.dictId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option :label="item.label" :value="item.value" v-for="(item,index) in sys_lavor_issue" :key="value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍙戞斁鏁伴噺" prop="num">
+ <el-input-number :step="1" :min="0" style="width: 100%"
+ v-model="form.num"
+ placeholder="璇疯緭鍏�"
+ />
+ </el-form-item>
+ <el-form-item label="杩涘巶鏃ユ湡" prop="factoryDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.factoryDate"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="鍙戞斁鏃ユ湡" prop="issueDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.issueDate"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+
+ </el-form>
+</template>
+
+<script setup>
+import useFormData from "@/hooks/useFormData";
+import {ref,onMounted} from "vue";
+import useUserStore from "@/store/modules/user";
+import {deepCopySameProperties} from '@/utils/util'
+const userStore = useUserStore();
+import {
+ getDept
+} from "@/api/collaborativeApproval/approvalProcess.js";
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+const { proxy } = getCurrentInstance();
+
+
+defineOptions({
+ name: "鏂板鏀跺叆",
+});
+const { sys_lavor_issue } = proxy.useDict("sys_lavor_issue")
+const { sys_lavor_issue_type } = proxy.useDict("sys_lavor_issue_type")
+const formRef = ref(null);
+const productOptions = ref([]);
+const personList = ref([]);
+const formRules = {
+ deptId: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ dictType: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+ staffId: [{ required: true, trigger: "blur", message: "璇疯緭鍏�" }],
+ dictId: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+ num: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+ adoptedDate: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+ factoryDate: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+ issueDate: [{ required: true, trigger: "change", message: "璇烽�夋嫨" }],
+}
+
+const { form, resetForm } = useFormData({
+ deptId: undefined, //
+ dictType: undefined,
+ staffId: undefined, //
+ dictId: undefined, //
+ num: undefined, //
+ adoptedDate: undefined,
+ factoryDate: undefined,
+ issueDate: undefined,
+});
+const getPersonList = () => {
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1
+ }).then(res => {
+ personList.value = res.data.records
+ })
+};
+const loadForm = (data) => {
+ deepCopySameProperties(data, form)
+};
+
+const getProductOptions = () => {
+ getDept().then((res) => {
+ productOptions.value = res.data;
+ });
+}
+// 娓呴櫎琛ㄥ崟鏍¢獙鐘舵��
+const clearValidate = () => {
+ formRef.value?.clearValidate();
+};
+
+// 閲嶇疆琛ㄥ崟鏁版嵁鍜屾牎楠岀姸鎬�
+const resetFormAndValidate = () => {
+ resetForm();
+ clearValidate();
+ form.deptId = userStore.currentDeptId
+ getProductOptions();
+ getPersonList();
+};
+onMounted(() => {
+ form.deptId = userStore.currentDeptId
+ getProductOptions();
+ getPersonList();
+})
+defineExpose({
+ form,
+ resetForm,
+ clearValidate,
+ loadForm,
+ resetFormAndValidate,
+ formRef,
+});
+</script>
diff --git a/src/views/lavorissue/ledger/Modal.vue b/src/views/lavorissue/ledger/Modal.vue
new file mode 100644
index 0000000..5d63236
--- /dev/null
+++ b/src/views/lavorissue/ledger/Modal.vue
@@ -0,0 +1,70 @@
+<template>
+ <el-dialog :title="modalOptions.title" v-model="visible" @close="close" width="30%">
+ <Form ref="formRef"></Form>
+ <template #footer>
+ <el-button type="primary" @click="sendForm" :loading="loading">
+ {{ modalOptions.confirmText }}
+ </el-button>
+ <el-button @click="closeModal">{{ modalOptions.cancelText }}</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { useModal } from "@/hooks/useModal";
+import { add, update } from "@/api/lavorissce/ledger";
+import Form from "./Form.vue";
+import { ElMessage } from "element-plus";
+const { proxy } = getCurrentInstance()
+
+defineOptions({
+ name: "鏀跺叆鏂板缂栬緫",
+});
+
+const emits = defineEmits(["success"]);
+
+const formRef = ref();
+const {
+ id,
+ visible,
+ loading,
+ openModal,
+ modalOptions,
+ handleConfirm,
+ closeModal,
+} = useModal({ title: "鍔充繚鍙拌处" });
+
+const sendForm = () => {
+ proxy.$refs.formRef.$refs.formRef.validate(async valid => {
+ if (valid) {
+ const {code} = id.value
+ ? await update({id: id.value, ...formRef.value.form})
+ : await add(formRef.value.form);
+ if (code == 200) {
+ emits("success");
+ ElMessage({message: "鎿嶄綔鎴愬姛", type: "success"});
+ close();
+ } else {
+ loading.value = false;
+ }
+ }
+ })
+};
+
+const close = () => {
+ formRef.value.resetFormAndValidate();
+ closeModal();
+};
+
+const loadForm = async (row) => {
+ openModal(row.id);
+ await nextTick();
+ formRef.value.loadForm(row);
+
+};
+
+defineExpose({
+ openModal,
+ loadForm,
+});
+</script>
diff --git a/src/views/lavorissue/ledger/filesDia.vue b/src/views/lavorissue/ledger/filesDia.vue
new file mode 100644
index 0000000..46da350
--- /dev/null
+++ b/src/views/lavorissue/ledger/filesDia.vue
@@ -0,0 +1,202 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ style="display: inline;margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ height="500"
+ >
+ </PIMTable>
+ <pagination
+ style="margin: 10px 0"
+ v-show="total > 0"
+ @pagination="paginationSearch"
+ :total="total"
+ :page="page.current"
+ :limit="page.size"
+ />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import filePreview from '@/components/filePreview/index.vue'
+import {
+ fileAdd,
+ fileDel,
+ fileListPage
+} from "@/api/financialManagement/revenueManagement.js";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const filePreviewRef = ref()
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: (row) => {
+ lookFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const accountType = ref('')
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row,type) => {
+ accountType.value = type;
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
+ tableData.value = res.data.records;
+ total.value = res.data.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const fileRow = {}
+ fileRow.name = res.data.originalName
+ fileRow.url = res.data.tempPath
+ uploadFile(fileRow)
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+function uploadFile(file) {
+ file.accountId = currentId.value;
+ file.accountType = accountType.value;
+ fileAdd(file).then(res => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ fileDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 棰勮闄勪欢
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/lavorissue/ledger/index.vue b/src/views/lavorissue/ledger/index.vue
new file mode 100644
index 0000000..19d0e59
--- /dev/null
+++ b/src/views/lavorissue/ledger/index.vue
@@ -0,0 +1,300 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鍙戞斁瀛e害:" prop="season">
+ <el-select
+ style="width: 200px;"
+ @change="handleQuery"
+ v-model="filters.season"
+ placeholder="璇烽�夋嫨"
+ :clearable="false"
+ >
+ <el-option :label="item.label" :value="item.value" v-for="(item,index) in jidu" :key="value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍛樺伐鍚嶇О:">
+ <el-input
+ v-model="filters.staffName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+ <el-button @click="resetFilters">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="add" icon="Plus"> 鏂板 </el-button>
+ <el-button @click="handleOut" icon="download">瀵煎嚭</el-button>
+ <el-button
+ type="danger"
+ icon="Delete"
+ :disabled="multipleList.length <= 0"
+ @click="deleteRow(multipleList.map((item) => item.id))"
+ >
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ isSelection
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @selection-change="handleSelectionChange"
+ @pagination="changePage"
+ >
+ <template #operation="{ row }">
+ <el-button type="primary" text @click="edit(row)" icon="editPen">
+ 缂栬緫
+ </el-button>
+ <el-button type="primary" :disabled="row.adoptedDate ? true : false" text @click="adopted(row)">
+ 棰嗙敤
+ </el-button>
+ </template>
+ </PIMTable>
+ </div>
+ <Modal ref="modalRef" @success="getTableData"></Modal>
+ <files-dia ref="filesDia"></files-dia>
+ </div>
+</template>
+
+<script setup>
+import { usePaginationApi } from "@/hooks/usePaginationApi";
+import { listPage,deleteLedger,update } from "@/api/lavorissce/ledger";
+import { onMounted, getCurrentInstance } from "vue";
+import Modal from "./Modal.vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import dayjs from "dayjs";
+import FilesDia from "./filesDia.vue";
+import { getCurrentMonth } from "@/utils/util"
+
+// 琛ㄦ牸澶氶�夋閫変腑椤�
+const multipleList = ref([]);
+const { proxy } = getCurrentInstance();
+const modalRef = ref();
+const { payment_methods } = proxy.useDict("payment_methods");
+const { income_types } = proxy.useDict("income_types");
+const filesDia = ref()
+
+const {
+ filters,
+ columns,
+ dataList,
+ pagination,
+ getTableData,
+ resetFilters,
+ onCurrentChange,
+} = usePaginationApi(
+ listPage,
+ {
+ staffName: '',
+ season: getCurrentMonth(),
+ },
+ [
+ {
+ label: "鍔充繚鍗曞彿",
+ align: "center",
+ prop: "orderNo",
+ },
+ {
+ label: "鍛樺伐鍚嶇О",
+ align: "center",
+ prop: "staffName",
+ },
+ {
+ label: "鍛樺伐缂栧彿",
+ align: "center",
+ prop: "staffNo"
+ },
+
+ {
+ label: "鍔充繚绫诲瀷",
+ align: "center",
+ prop: "dictTypeName",
+
+ },
+ {
+ label: "鍔充繚闃插叿",
+ align: "center",
+ prop: "dictName",
+
+ },
+ {
+ label: "鍙戞斁鏁伴噺",
+ align: "center",
+ prop: "num",
+
+ },
+ {
+ label: "杩涘巶鏃ユ湡",
+ align: "center",
+ prop: "factoryDate",
+
+ },
+ {
+ label: "鍙戞斁鏃ユ湡",
+ align: "center",
+ prop: "issueDate",
+
+ },
+ {
+ label: "棰嗙敤鏃ユ湡",
+ align: "center",
+ prop: "adoptedDate",
+
+ },
+ {
+ fixed: "right",
+ label: "鎿嶄綔",
+ dataType: "slot",
+ slot: "operation",
+ align: "center",
+ width: "200px",
+ },
+ ]
+);
+
+const jidu = ref([
+ {
+ value: '1',
+ label: '绗竴瀛e害'
+ },
+ {
+ value: '2',
+ label: '绗簩瀛e害'
+ },
+ {
+ value: '3',
+ label: '绗笁瀛e害'
+ },
+ {
+ value: '4',
+ label: '绗洓瀛e害'
+ }
+])
+
+// 澶氶�夊悗鍋氫粈涔�
+const handleSelectionChange = (selectionList) => {
+ multipleList.value = selectionList;
+};
+
+const adopted = (row) => {
+ ElMessageBox.confirm("鏄惁纭棰嗙敤?", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ const params = {
+ id: row.id,
+ adoptedDate: dayjs().format("YYYY-MM-DD")
+ }
+ const { code } = await update(params);
+ if (code == 200) {
+ ElMessage({
+ type: "success",
+ message: "棰嗙敤鎴愬姛",
+ });
+ getTableData();
+ }
+ })
+}
+
+const add = () => {
+ modalRef.value.openModal();
+};
+const edit = (row) => {
+ modalRef.value.loadForm(row);
+};
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ getTableData();
+};
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ onCurrentChange(page);
+};
+const deleteRow = (id) => {
+ ElMessageBox.confirm("姝ゆ搷浣滃皢姘镐箙鍒犻櫎璇ユ暟鎹�, 鏄惁缁х画?", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(async () => {
+ const { code } = await deleteLedger(id);
+ if (code == 200) {
+ ElMessage({
+ type: "success",
+ message: "鍒犻櫎鎴愬姛",
+ });
+ getTableData();
+ }
+ });
+};
+
+const changeDaterange = (value) => {
+ if (value) {
+ filters.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ filters.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ } else {
+ filters.entryDateStart = undefined;
+ filters.entryDateEnd = undefined;
+ }
+ getTableData();
+};
+
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(`/lavorIssue/export`, {}, "鍔充繚鍙拌处.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 鎵撳紑闄勪欢寮规
+const openFilesFormDia = (row) => {
+ nextTick(() => {
+ filesDia.value?.openDialog( row,'鏀跺叆')
+ })
+};
+
+onMounted(() => {
+ filters.entryDate = [
+ dayjs().format("YYYY-MM-DD"),
+ dayjs().add(1, "day").format("YYYY-MM-DD"),
+ ]
+ filters.entryDateStart = dayjs().format("YYYY-MM-DD")
+ filters.entryDateEnd = dayjs().add(1, "day").format("YYYY-MM-DD")
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.table_list {
+ margin-top: unset;
+}
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+</style>
+
diff --git a/src/views/lavorissue/statistics/index.vue b/src/views/lavorissue/statistics/index.vue
new file mode 100644
index 0000000..54e1f38
--- /dev/null
+++ b/src/views/lavorissue/statistics/index.vue
@@ -0,0 +1,285 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">鍙戞斁瀛e害锛�</span>
+ <el-select
+ style="width: 200px;"
+ @change="handleQuery"
+ v-model="searchForm.season"
+ placeholder="璇烽�夋嫨"
+ @clear="clearSeason"
+ clearable
+ :disabled="searchForm.issueDate ? true : false"
+
+ >
+ <el-option :label="item.label" :value="item.value" v-for="(item,index) in jidu" :key="item.value" />
+ </el-select>
+ <span class="search_title ml10">鍙戞斁鏈堜唤锛�</span>
+ <el-date-picker
+ style="width: 200px;"
+ :disabled="searchForm.season ? true : false"
+ v-model="searchForm.issueDate"
+ @change="handleQuery"
+ @clear="clearIssueDaten"
+ type="month"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨鏈堜唤"
+ clearable
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ <el-button type="primary" @click="resetHandleQuery" style="margin-left: 10px"
+ >閲嶇疆</el-button
+ >
+ </div>
+ <div>
+ <el-button @click="handleOut" icon="download">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <div class="actions">
+ <div class="head" @click="handleQuery(1)">宸查鍙栧姵淇濇暟閲�:{{statisticsObj.ylqNum}}</div>
+ <div class="head" @click="handleQuery(2)">鏈鍙栧姵淇濇暟閲�: {{ statisticsObj.wlqNum }}</div>
+ <div class="head" @click="handleQuery(3)">瓒呮椂宸查鍙栧姵淇濇暟閲�: {{statisticsObj.csylqNum}}</div>
+ <div class="head" @click="handleQuery(4)">瓒呮椂鏈鍙栧姵淇濇暟閲�: {{statisticsObj.cswlqNum}}</div>
+ </div>
+ <el-table
+ ref="tableRef"
+ v-loading="tableLoading"
+ :data="tableData"
+ border
+ height="calc(100vh - 21em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ style="width: 100%"
+ @selection-change="handleSelectionChange"
+ >
+ <!-- 閫夋嫨鍒� -->
+ <el-table-column
+ align="center"
+ type="selection"
+ width="55"
+ fixed="left"
+ />
+
+ <!-- 搴忓彿鍒� -->
+ <el-table-column
+ align="center"
+ label="搴忓彿"
+ type="index"
+ width="60"
+ fixed="left"
+ />
+
+ <!-- 鍥哄畾鍒楋細濮撳悕 -->
+ <el-table-column
+ label="濮撳悕"
+ prop="staffName"
+ width="100"
+ show-overflow-tooltip
+ align="center"
+ fixed="left"
+ />
+
+ <!-- 鍥哄畾鍒楋細宸ュ彿 -->
+ <el-table-column
+ label="宸ュ彿"
+ prop="staffNo"
+ width="100"
+ show-overflow-tooltip
+ align="center"
+ fixed="left"
+ />
+
+ <!-- 鍔ㄦ�佸垪锛氭牴鎹瓧鍏告覆鏌� -->
+ <el-table-column
+ v-for="(dictItem, index) in sys_lavor_issue"
+ :key="dictItem.value"
+ :label="dictItem.label"
+ :prop="dictItem.value"
+ show-overflow-tooltip
+ >
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, reactive, toRefs, nextTick, getCurrentInstance } from 'vue'
+import dayjs from "dayjs";
+import {statisticsList, statistics} from "@/api/lavorissce/ledger.js";
+import {ElMessageBox, ElMessage} from "element-plus";
+const { proxy } = getCurrentInstance();
+import { getCurrentMonth } from "@/utils/util"
+
+const page = ref({
+ current: 1,
+ size: 100,
+})
+const total = ref(0)
+// 鍝嶅簲寮忔暟鎹�
+const tableRef = ref(null)
+const tableData = ref([])
+const tableLoading = ref(false)
+const { sys_lavor_issue } = proxy.useDict("sys_lavor_issue")
+const data = reactive({
+ searchForm: {
+ season: getCurrentMonth(),
+ issueDate: "",
+ status: 0
+ },
+});
+const { searchForm } = toRefs(data);
+
+const modalRef = ref();
+const filesDia = ref();
+const multipleList = ref([]);
+const jidu = ref([
+ {
+ value: '1',
+ label: '绗竴瀛e害'
+ },
+ {
+ value: '2',
+ label: '绗簩瀛e害'
+ },
+ {
+ value: '3',
+ label: '绗笁瀛e害'
+ },
+ {
+ value: '4',
+ label: '绗洓瀛e害'
+ }
+])
+const clearSeason = () => {
+ console.log("req")
+ searchForm.value.season = ""
+ searchForm.value.issueDate = dayjs().format("YYYY-MM-DD");
+}
+
+const clearIssueDaten = () => {
+ searchForm.value.issueDate = ""
+ searchForm.value.season = getCurrentMonth()
+}
+const statisticsObj = ref({
+ ylqNum: 0, // 宸查鍙栨暟閲�
+ wlqNum: 0, // 鏈鍙栨暟閲�
+ csylqNum: 0, // 瓒呮椂宸查鍙栨暟閲�
+ cswlqNum: 0 // 瓒呮椂鏈鍙栨暟閲�
+})
+const resetHandleQuery = () => {
+ searchForm.value.issueDate = "";
+ searchForm.value.season = getCurrentMonth();
+ handleQuery(0)
+};
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = (status) => {
+ switch (status){
+ case 1:
+ searchForm.value.status = 1
+ break;
+ case 2:
+ searchForm.value.status = 2
+ break;
+ case 3:
+ searchForm.value.status = 3
+ break;
+ case 4:
+ searchForm.value.status = 4
+ break;
+ default:
+ searchForm.value.status = 0
+ }
+ getList();
+ getStatistics();
+};
+
+const getStatistics = () => {
+ statistics(searchForm.value).then(res => {
+ statisticsObj.value.cswlqNum = res.data.cswlqNum
+ statisticsObj.value.csylqNum = res.data.csylqNum
+ statisticsObj.value.ylqNum = res.data.ylqNum
+ statisticsObj.value.wlqNum = res.data.wlqNum
+ })
+}
+// 鑾峰彇瀛楀吀鏁版嵁
+const getList = async () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value};
+ statisticsList(params).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+}
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(`/lavorIssue/exportCopy`, {season: searchForm.value.season,issueDate: searchForm.value.issueDate}, "鍔充繚鍙拌处.xlsx");
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑�");
+ });
+};
+
+// 浜嬩欢澶勭悊鍑芥暟
+const handleSelectionChange = (selection) => {
+ multipleList.value = selection;
+}
+
+// 缁勪欢鎸傝浇鏃跺姞杞藉瓧鍏告暟鎹�
+onMounted(() => {
+ handleQuery()
+})
+</script>
+
+<style scoped>
+.dynamic-table-container {
+ width: 100%;
+}
+
+.pagination-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+:deep(.el-table .el-table__header-wrapper th) {
+ background-color: #F0F1F5 !important;
+ color: #333333;
+ font-weight: 600;
+}
+
+:deep(.el-table .el-table__body-wrapper td) {
+ padding: 8px 0;
+}
+
+:deep(.el-select) {
+ width: 100%;
+}
+
+:deep(.el-input) {
+ width: 100%;
+}
+.actions {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ margin-bottom: 30px;
+}
+.head{
+ cursor: pointer;
+ font-size: 18px;
+ font-weight: 600;
+}
+</style>
diff --git a/src/views/login.vue b/src/views/login.vue
new file mode 100644
index 0000000..c7db15a
--- /dev/null
+++ b/src/views/login.vue
@@ -0,0 +1,893 @@
+<template>
+ <div class="login-page">
+ <main class="page">
+ <section class="factory">
+ <div class="brand hero-brand">
+ <div class="logo hero-logo">
+ <img
+ :src="brandLogoUrl"
+ :alt="`${companyName} logo`"
+ class="logo-image hero-logo-image"
+ @error="handleLogoError"
+ />
+ </div>
+ </div>
+
+ <div class="hero">
+ <div class="chip">鏁板瓧宸ュ巶 路 鏅鸿兘鎺掍骇 路 璁惧浜掕仈 路 璐ㄩ噺杩芥函</div>
+ <h1>鏁板瓧宸ュ巶<br />MOM 鏅洪�犲钩鍙�</h1>
+ <p>
+ 浠ュ疄鏃舵暟鎹┍鍔ㄧ敓浜х幇鍦猴紝鎶婂伐鍗曘�佽澶囥�佺墿鏂欍�佽川閲忋�佽兘鑰椾笌浠撳偍杩炴帴鎴愪竴寮犻�忔槑鐨勫埗閫犺繍钀ョ綉缁溿��
+ </p>
+ </div>
+
+ <div class="scene" aria-hidden="true">
+ <div class="floor"></div>
+ <svg class="factory-svg" viewBox="0 0 920 360" preserveAspectRatio="xMidYMid meet">
+ <defs>
+ <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0" stop-color="#40e4ff" stop-opacity=".9" />
+ <stop offset="1" stop-color="#1f78ff" stop-opacity=".68" />
+ </linearGradient>
+ <linearGradient id="g2" x1="0" y1="0" x2="1" y2="1">
+ <stop offset="0" stop-color="#ffffff" stop-opacity=".28" />
+ <stop offset="1" stop-color="#ffffff" stop-opacity=".06" />
+ </linearGradient>
+ </defs>
+
+ <path d="M45 255H830" stroke="url(#g1)" stroke-width="16" stroke-linecap="round" opacity=".55" />
+ <path class="belt" d="M45 255H830" stroke="#fff" stroke-width="3" stroke-linecap="round" opacity=".75" />
+
+ <g class="box">
+ <rect x="60" y="212" width="54" height="42" rx="8" fill="#ffb15f" />
+ <path d="M60 225h54" stroke="#fff" opacity=".45" />
+ </g>
+ <g class="box two">
+ <rect x="60" y="212" width="54" height="42" rx="8" fill="#28d9cd" />
+ <path d="M60 225h54" stroke="#fff" opacity=".45" />
+ </g>
+ <g class="box three">
+ <rect x="60" y="212" width="54" height="42" rx="8" fill="#8b5cf6" />
+ <path d="M60 225h54" stroke="#fff" opacity=".45" />
+ </g>
+
+ <g>
+ <rect x="120" y="112" width="138" height="128" rx="18" fill="url(#g2)" stroke="rgba(255,255,255,.42)" />
+ <path d="M145 185h88M145 210h58" stroke="#40e4ff" stroke-width="6" stroke-linecap="round" />
+ <path d="M145 140h88" stroke="#fff" stroke-opacity=".5" stroke-width="4" stroke-linecap="round" />
+ </g>
+
+ <g>
+ <rect x="315" y="76" width="190" height="164" rx="22" fill="url(#g2)" stroke="rgba(255,255,255,.42)" />
+ <path d="M350 126h120M350 158h90M350 190h112" stroke="#fff" stroke-opacity=".5" stroke-width="6" stroke-linecap="round" />
+ <circle class="signal" cx="472" cy="104" r="10" fill="#20e0d2" />
+ <circle class="signal two" cx="448" cy="104" r="10" fill="#1f78ff" />
+ <circle class="signal three" cx="424" cy="104" r="10" fill="#ff8a3d" />
+ </g>
+
+ <g class="arm">
+ <path d="M612 124h92" stroke="#40e4ff" stroke-width="14" stroke-linecap="round" />
+ <path d="M704 124l42 56" stroke="#40e4ff" stroke-width="14" stroke-linecap="round" />
+ <circle cx="612" cy="124" r="25" fill="#1f78ff" stroke="#fff" stroke-opacity=".45" />
+ <circle cx="704" cy="124" r="18" fill="#20e0d2" />
+ <path d="M744 180v34M727 214h34" stroke="#fff" stroke-width="7" stroke-linecap="round" />
+ </g>
+
+ <g>
+ <rect x="690" y="82" width="148" height="158" rx="20" fill="url(#g2)" stroke="rgba(255,255,255,.42)" />
+ <path d="M724 206V134M764 206V112M804 206V154" stroke="#20e0d2" stroke-width="12" stroke-linecap="round" />
+ <path d="M720 206h92" stroke="#fff" stroke-opacity=".44" stroke-width="5" stroke-linecap="round" />
+ </g>
+
+ <path
+ d="M190 112C265 42 348 48 410 76C502 118 568 76 612 124C654 170 700 74 764 82"
+ fill="none"
+ stroke="#20e0d2"
+ stroke-width="2"
+ stroke-dasharray="8 10"
+ opacity=".58"
+ >
+ <animate attributeName="stroke-dashoffset" from="80" to="0" dur="2s" repeatCount="indefinite" />
+ </path>
+ </svg>
+ </div>
+ </section>
+
+ <section class="login-wrap">
+ <div class="time">
+ <span>{{ todayLabel }}</span>
+ {{ clockLabel }}
+ </div>
+
+ <form class="login-card" @submit.prevent="handleLogin">
+ <div class="brand card-brand">
+ <div class="logo">
+ <img :src="brandIconUrl" :alt="`${companyName} icon`" class="logo-image card-logo-image" />
+ </div>
+ <div class="brand-copy card-brand-copy">
+ <div class="brand-title">{{ companyName }}</div>
+ <small>鏁板瓧宸ュ巶缁熶竴鍏ュ彛</small>
+ </div>
+ </div>
+
+ <h2>娆㈣繋鐧诲綍</h2>
+ <p class="sub">杩涘叆 MOM 鏁板瓧宸ュ巶杩愯惀椹鹃┒鑸�</p>
+
+ <div class="form-row">
+ <label>璐﹀彿</label>
+ <div class="input">
+ <svg viewBox="0 0 24 24" fill="none">
+ <path d="M20 21a8 8 0 0 0-16 0" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
+ <path d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="currentColor" stroke-width="1.8" />
+ </svg>
+ <input v-model.trim="loginForm.username" type="text" placeholder="璇疯緭鍏ョ鐞嗗憳璐﹀彿" />
+ </div>
+ </div>
+
+ <div class="form-row">
+ <label>瀵嗙爜</label>
+ <div class="input">
+ <svg viewBox="0 0 24 24" fill="none">
+ <path d="M7 10V8a5 5 0 0 1 10 0v2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
+ <path d="M6.8 10h10.4A1.8 1.8 0 0 1 19 11.8v6.4A1.8 1.8 0 0 1 17.2 20H6.8A1.8 1.8 0 0 1 5 18.2v-6.4A1.8 1.8 0 0 1 6.8 10Z" stroke="currentColor" stroke-width="1.8" />
+ </svg>
+ <input
+ v-model="loginForm.password"
+ type="password"
+ placeholder="璇疯緭鍏ョ櫥褰曞瘑鐮�"
+ autocomplete="current-password"
+ @keyup.enter="handleLogin"
+ />
+ </div>
+ </div>
+
+ <div class="options">
+ <label class="check"><input v-model="loginForm.rememberMe" type="checkbox" />璁颁綇璐﹀彿</label>
+ </div>
+
+ <button class="login-btn" type="submit" :disabled="loading">
+ {{ loading ? "鐧诲綍涓�..." : "鐧诲綍鏁板瓧宸ュ巶" }}
+ </button>
+ </form>
+ </section>
+ </main>
+ </div>
+</template>
+
+<script setup>
+import { ElMessage } from "element-plus"
+import Cookies from "js-cookie"
+import { encrypt, decrypt } from "@/utils/jsencrypt"
+import useUserStore from "@/store/modules/user"
+import defaultBrandLogo from "@/assets/logo/logo.png"
+
+const userStore = useUserStore()
+const route = useRoute()
+const router = useRouter()
+
+const appTitle = String(import.meta.env.VITE_APP_TITLE || "鏁板瓧宸ュ巶 MOM 绯荤粺").trim()
+const companySubtitle = String(import.meta.env.VITE_LOGIN_SUBTITLE || "Digital Factory Operation Center").trim()
+const configuredLogo = String(import.meta.env.VITE_APP_LOGO || "").trim()
+const logoModules = import.meta.glob("/src/assets/logo/*.png", { eager: true })
+const brandIconUrl = `${import.meta.env.BASE_URL}favicon.ico`
+
+const redirect = ref("")
+const loading = ref(false)
+const now = ref(new Date())
+const brandLogoUrl = ref(defaultBrandLogo)
+
+const loginForm = ref({
+ username: "",
+ password: "",
+ rememberMe: false,
+})
+
+const companyName = computed(() => {
+ const currentFactoryName = String(userStore.currentFactoryName || "").trim()
+ return currentFactoryName || appTitle
+})
+
+const todayLabel = computed(() => {
+ const date = now.value
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
+})
+
+const clockLabel = computed(() => {
+ const date = now.value
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
+})
+
+watch(
+ route,
+ (newRoute) => {
+ redirect.value = String(newRoute.query?.redirect || "")
+ },
+ { immediate: true }
+)
+
+watch(
+ () => userStore.currentFactoryName,
+ () => updateBrandLogo(),
+ { immediate: true }
+)
+
+let timer = 0
+onMounted(() => {
+ timer = window.setInterval(() => {
+ now.value = new Date()
+ }, 1000)
+})
+
+onBeforeUnmount(() => {
+ if (timer) {
+ window.clearInterval(timer)
+ timer = 0
+ }
+})
+
+function pad(value) {
+ return String(value).padStart(2, "0")
+}
+
+function resolveConfiguredLogo() {
+ if (!configuredLogo) {
+ return ""
+ }
+
+ if (/^(https?:)?\/\//.test(configuredLogo) || configuredLogo.startsWith("data:")) {
+ return configuredLogo
+ }
+
+ const cleanPath = configuredLogo.replace(/^\/+/, "")
+ const fullPath = cleanPath.startsWith("src/") ? `/${cleanPath}` : `/src/${cleanPath}`
+ const localLogo = logoModules[fullPath]
+
+ if (localLogo && localLogo.default) {
+ return localLogo.default
+ }
+
+ if (configuredLogo.startsWith("/")) {
+ return configuredLogo
+ }
+
+ return `${import.meta.env.BASE_URL}${cleanPath}`
+}
+
+function updateBrandLogo() {
+ const logoFromConfig = resolveConfiguredLogo()
+ if (logoFromConfig) {
+ brandLogoUrl.value = logoFromConfig
+ return
+ }
+
+ const currentFactoryName = String(userStore.currentFactoryName || "").trim()
+ if (!currentFactoryName) {
+ brandLogoUrl.value = defaultBrandLogo
+ return
+ }
+
+ const factoryLogoPath = `/src/assets/logo/${currentFactoryName}.png`
+ const matched = logoModules[factoryLogoPath]
+ brandLogoUrl.value = matched && matched.default ? matched.default : defaultBrandLogo
+}
+
+function handleLogoError() {
+ brandLogoUrl.value = defaultBrandLogo
+}
+
+function handleRememberCookie() {
+ if (!loginForm.value.rememberMe) {
+ Cookies.remove("username")
+ Cookies.remove("password")
+ Cookies.remove("rememberMe")
+ return
+ }
+
+ Cookies.set("username", loginForm.value.username, { expires: 30 })
+ Cookies.set("password", encrypt(loginForm.value.password), { expires: 30 })
+ Cookies.set("rememberMe", "true", { expires: 30 })
+}
+
+function getCookie() {
+ const username = Cookies.get("username")
+ const password = Cookies.get("password")
+ const rememberMe = Cookies.get("rememberMe")
+
+ loginForm.value.username = username || ""
+ loginForm.value.password = password ? decrypt(password) : ""
+ loginForm.value.rememberMe = rememberMe === "true"
+}
+
+function handleLogin() {
+ if (!loginForm.value.username) {
+ ElMessage.error("璇疯緭鍏ヨ处鍙�")
+ return
+ }
+ if (!loginForm.value.password) {
+ ElMessage.error("璇疯緭鍏ュ瘑鐮�")
+ return
+ }
+
+ loading.value = true
+ handleRememberCookie()
+ userStore
+ .loginCheckFactory(loginForm.value)
+ .then(() => {
+ const query = route.query
+ const otherQueryParams = Object.keys(query).reduce((acc, cur) => {
+ if (cur !== "redirect") {
+ acc[cur] = query[cur]
+ }
+ return acc
+ }, {})
+ router.push({ path: redirect.value || "/", query: otherQueryParams })
+ })
+ .catch(() => {})
+ .finally(() => {
+ loading.value = false
+ })
+}
+
+getCookie()
+</script>
+
+<style scoped lang="scss">
+* {
+ box-sizing: border-box;
+}
+
+.login-page {
+ --blue: #1f78ff;
+ --cyan: #20e0d2;
+ --violet: #8b5cf6;
+ --orange: #ff8a3d;
+ --text: #09203f;
+ --muted: #7a8da8;
+ --line: rgba(113, 154, 214, 0.24);
+ --shadow: 0 28px 80px rgba(14, 57, 120, 0.18);
+ min-height: 100vh;
+ overflow: hidden;
+ background:
+ radial-gradient(circle at 12% 16%, rgba(31, 120, 255, 0.22), transparent 32%),
+ radial-gradient(circle at 84% 14%, rgba(32, 224, 210, 0.24), transparent 28%),
+ radial-gradient(circle at 80% 84%, rgba(139, 92, 246, 0.14), transparent 28%),
+ linear-gradient(135deg, #eef6ff 0%, #f7fbff 52%, #edf8ff 100%);
+}
+
+.login-page::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ background-image:
+ linear-gradient(rgba(22, 78, 160, 0.055) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(22, 78, 160, 0.055) 1px, transparent 1px);
+ background-size: 36px 36px;
+ mask-image: radial-gradient(circle at center, #000 0%, transparent 78%);
+ pointer-events: none;
+}
+
+.page {
+ position: relative;
+ display: grid;
+ grid-template-columns: 1.15fr 0.85fr;
+ gap: 28px;
+ min-height: 100vh;
+ padding: 36px;
+}
+
+.factory {
+ position: relative;
+ overflow: hidden;
+ min-height: calc(100vh - 72px);
+ border-radius: 30px;
+ padding: 32px;
+ color: #fff;
+ background:
+ linear-gradient(135deg, rgba(5, 27, 67, 0.98), rgba(9, 71, 143, 0.92)),
+ radial-gradient(circle at 76% 18%, rgba(32, 224, 210, 0.38), transparent 30%);
+ box-shadow: var(--shadow);
+ border: 1px solid rgba(255, 255, 255, 0.16);
+ animation: enter 0.8s ease both;
+}
+
+.factory::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background:
+ linear-gradient(120deg, transparent 0%, rgba(255, 255, 255, 0.11) 46%, transparent 72%),
+ linear-gradient(rgba(255, 255, 255, 0.055) 1px, transparent 1px),
+ linear-gradient(90deg, rgba(255, 255, 255, 0.055) 1px, transparent 1px);
+ background-size: 100% 100%, 44px 44px, 44px 44px;
+ opacity: 0.86;
+}
+
+.brand {
+ position: relative;
+ z-index: 2;
+ display: inline-flex;
+ align-items: center;
+}
+
+.logo {
+ width: 76px;
+ height: 76px;
+ border-radius: 14px;
+ display: grid;
+ place-items: center;
+ background: linear-gradient(135deg, var(--blue), var(--cyan));
+ box-shadow: 0 16px 40px rgba(32, 224, 210, 0.26);
+ overflow: hidden;
+ position: relative;
+}
+
+.logo-image {
+ width: 82%;
+ height: 82%;
+ object-fit: contain;
+}
+
+.hero-brand {
+ display: block;
+}
+
+.hero-logo {
+ width: 240px;
+ height: 84px;
+ border-radius: 0;
+ background: transparent;
+ box-shadow: none;
+ overflow: visible;
+ place-items: center start;
+}
+
+.hero-logo-image {
+ width: 100%;
+ height: auto;
+ max-height: 100%;
+ object-fit: contain;
+}
+
+.hero {
+ position: relative;
+ z-index: 2;
+ margin-top: 64px;
+ max-width: 680px;
+}
+
+.chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.18);
+ background: rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.78);
+ font-size: 13px;
+ animation: enter 0.75s ease 0.15s both;
+}
+
+.hero h1 {
+ margin: 24px 0 14px;
+ font-size: clamp(42px, 5.6vw, 78px);
+ line-height: 1.02;
+ font-weight: 900;
+ letter-spacing: 0;
+ animation: enter 0.8s ease 0.25s both;
+}
+
+.hero p {
+ margin: 0;
+ max-width: 630px;
+ font-size: 17px;
+ line-height: 1.8;
+ color: rgba(255, 255, 255, 0.72);
+ animation: enter 0.8s ease 0.35s both;
+}
+
+.scene {
+ position: absolute;
+ left: 42px;
+ right: 42px;
+ bottom: 24px;
+ height: 44%;
+ z-index: 1;
+}
+
+.floor {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 48%;
+ border-radius: 28px;
+ background:
+ linear-gradient(90deg, rgba(32, 224, 210, 0), rgba(32, 224, 210, 0.22), rgba(31, 120, 255, 0)),
+ repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.1) 0 1px, transparent 1px 64px),
+ repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.1) 0 1px, transparent 1px 34px);
+ transform: perspective(620px) rotateX(58deg);
+ transform-origin: bottom;
+ opacity: 0.82;
+}
+
+.factory-svg {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 18%;
+ width: 100%;
+ height: 78%;
+ filter: drop-shadow(0 28px 30px rgba(0, 0, 0, 0.18));
+}
+
+.belt {
+ stroke-dasharray: 10 12;
+ animation: flow 1.2s linear infinite;
+}
+
+.arm {
+ transform-origin: 612px 124px;
+ animation: armMove 3.6s ease-in-out infinite;
+}
+
+.box {
+ animation: boxMove 5.4s linear infinite;
+}
+
+.box.two {
+ animation-delay: -1.8s;
+}
+
+.box.three {
+ animation-delay: -3.6s;
+}
+
+.signal {
+ opacity: 0.8;
+ animation: pulse 2.2s ease-in-out infinite;
+}
+
+.signal.two {
+ animation-delay: -0.7s;
+}
+
+.signal.three {
+ animation-delay: -1.4s;
+}
+
+.login-wrap {
+ position: relative;
+ z-index: 2;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: calc(100vh - 72px);
+ border-radius: 30px;
+ border: 1px solid rgba(255, 255, 255, 0.8);
+ background: rgba(255, 255, 255, 0.62);
+ backdrop-filter: blur(20px);
+ box-shadow: var(--shadow);
+ animation: enter 0.8s ease 0.12s both;
+}
+
+.time {
+ position: absolute;
+ top: 26px;
+ right: 28px;
+ display: flex;
+ gap: 9px;
+ align-items: center;
+ font-weight: 900;
+ color: #12325e;
+}
+
+.time span {
+ padding: 8px 12px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.66);
+ border: 1px solid var(--line);
+ color: var(--muted);
+ font-size: 13px;
+ font-weight: 700;
+}
+
+.login-card {
+ width: min(440px, 100%);
+ padding: 34px;
+ border-radius: 28px;
+ border: 1px solid rgba(255, 255, 255, 0.95);
+ background: rgba(255, 255, 255, 0.88);
+ box-shadow: 0 22px 60px rgba(21, 73, 143, 0.15);
+ position: relative;
+ overflow: hidden;
+}
+
+.login-card::before {
+ content: "";
+ position: absolute;
+ right: -92px;
+ top: -92px;
+ width: 190px;
+ height: 190px;
+ border-radius: 999px;
+ background: conic-gradient(from 0deg, rgba(31, 120, 255, 0.2), rgba(32, 224, 210, 0.32), rgba(139, 92, 246, 0.18), rgba(31, 120, 255, 0.2));
+ animation: rotate 9s linear infinite;
+}
+
+.login-card > * {
+ position: relative;
+ z-index: 1;
+}
+
+.card-brand {
+ margin-bottom: 18px;
+}
+
+.card-brand .logo {
+ width: 52px;
+ height: 52px;
+}
+
+.card-brand-copy {
+ margin-left: 12px;
+}
+
+.card-logo-image {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.card-brand .logo {
+ width: 68px;
+ height: 68px;
+}
+
+.login-card h2 {
+ margin: 8px 0;
+ font-size: 31px;
+ line-height: 1.15;
+ color: #0d2c5e;
+ font-weight: 900;
+ letter-spacing: 0;
+}
+
+.sub {
+ margin: 0 0 24px;
+ color: var(--muted);
+ font-size: 14px;
+}
+
+.form-row {
+ margin-bottom: 14px;
+ animation: enter 0.6s ease both;
+}
+
+.form-row:nth-of-type(1) {
+ animation-delay: 0.32s;
+}
+
+.form-row:nth-of-type(2) {
+ animation-delay: 0.4s;
+}
+
+.form-row:nth-of-type(3) {
+ animation-delay: 0.48s;
+}
+
+label {
+ display: block;
+ margin-bottom: 8px;
+ color: #24436b;
+ font-size: 13px;
+ font-weight: 800;
+}
+
+.input {
+ position: relative;
+}
+
+.input svg {
+ position: absolute;
+ left: 15px;
+ top: 50%;
+ width: 19px;
+ height: 19px;
+ transform: translateY(-50%);
+ color: #8197b6;
+ pointer-events: none;
+}
+
+input[type="text"],
+input[type="password"] {
+ width: 100%;
+ height: 52px;
+ padding: 0 15px 0 46px;
+ border: 1px solid rgba(108, 143, 190, 0.34);
+ border-radius: 16px;
+ outline: none;
+ background: linear-gradient(180deg, #fff, #f8fbff);
+ color: var(--text);
+ font-size: 15px;
+ transition: 0.2s ease;
+}
+
+input[type="text"]:hover,
+input[type="password"]:hover {
+ border-color: rgba(31, 120, 255, 0.48);
+ box-shadow: 0 10px 24px rgba(31, 120, 255, 0.08);
+}
+
+input[type="text"]:focus,
+input[type="password"]:focus {
+ border-color: rgba(31, 120, 255, 0.72);
+ box-shadow: 0 0 0 4px rgba(31, 120, 255, 0.1);
+}
+
+.options {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin: 12px 0 22px;
+ font-size: 13px;
+ color: var(--muted);
+}
+
+.check {
+ display: inline-flex;
+ gap: 8px;
+ align-items: center;
+ cursor: pointer;
+}
+
+.check input {
+ width: 16px;
+ height: 16px;
+ padding: 0;
+ accent-color: var(--blue);
+}
+
+.login-btn {
+ width: 100%;
+ height: 54px;
+ border: none;
+ border-radius: 17px;
+ cursor: pointer;
+ color: #fff;
+ font-size: 16px;
+ font-weight: 900;
+ background: linear-gradient(135deg, var(--blue), var(--cyan));
+ box-shadow: 0 18px 38px rgba(31, 120, 255, 0.25);
+ position: relative;
+ overflow: hidden;
+ transition: 0.18s ease;
+}
+
+.login-btn:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 22px 48px rgba(31, 120, 255, 0.32);
+}
+
+.login-btn:disabled {
+ opacity: 0.8;
+ cursor: not-allowed;
+}
+
+@keyframes enter {
+ from {
+ opacity: 0;
+ transform: translateY(26px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@keyframes rotate {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes flow {
+ to {
+ stroke-dashoffset: -44;
+ }
+}
+
+@keyframes boxMove {
+ 0% {
+ transform: translateX(-150px);
+ opacity: 0;
+ }
+ 12% {
+ opacity: 1;
+ }
+ 78% {
+ opacity: 1;
+ }
+ 100% {
+ transform: translateX(650px);
+ opacity: 0;
+ }
+}
+
+@keyframes armMove {
+ 0%,
+ 100% {
+ transform: rotate(-4deg);
+ }
+ 50% {
+ transform: rotate(9deg);
+ }
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 0.45;
+ transform: scale(1);
+ }
+ 50% {
+ opacity: 1;
+ transform: scale(1.18);
+ }
+}
+
+@keyframes glow {
+ 0%,
+ 100% {
+ box-shadow: 0 0 18px rgba(32, 224, 210, 0.3);
+ }
+ 50% {
+ box-shadow: 0 0 34px rgba(32, 224, 210, 0.68);
+ }
+}
+
+@media (max-width: 1320px) {
+ .hero h1 {
+ font-size: clamp(52px, 5.2vw, 78px);
+ }
+}
+
+@media (max-width: 980px) {
+ .login-page {
+ overflow: auto;
+ }
+
+ .page {
+ grid-template-columns: 1fr;
+ padding: 18px;
+ min-height: auto;
+ }
+
+ .factory,
+ .login-wrap {
+ min-height: auto;
+ }
+
+ .factory {
+ min-height: 760px;
+ }
+
+ .time {
+ display: none;
+ }
+
+ .login-wrap {
+ padding: 22px;
+ }
+
+ .login-card {
+ width: 100%;
+ padding: 26px;
+ }
+
+ .login-btn {
+ font-size: 16px;
+ }
+
+}
+</style>
diff --git a/src/views/monitor/cache/index.vue b/src/views/monitor/cache/index.vue
new file mode 100644
index 0000000..b351fa9
--- /dev/null
+++ b/src/views/monitor/cache/index.vue
@@ -0,0 +1,132 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="10">
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍩烘湰淇℃伅</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%">
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">Redis鐗堟湰</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">杩愯妯″紡</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "鍗曟満" : "闆嗙兢" }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">绔彛</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">瀹㈡埛绔暟</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">杩愯鏃堕棿(澶�)</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">浣跨敤鍐呭瓨</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">浣跨敤CPU</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">鍐呭瓨閰嶇疆</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">AOF鏄惁寮�鍚�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "鍚�" : "鏄�" }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">RDB鏄惁鎴愬姛</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">Key鏁伴噺</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">缃戠粶鍏ュ彛/鍑哄彛</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><PieChart style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍛戒护缁熻</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <div ref="commandstats" style="height: 420px" />
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍐呭瓨淇℃伅</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <div ref="usedmemory" style="height: 420px" />
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup name="Cache">
+import { getCache } from '@/api/monitor/cache'
+import * as echarts from 'echarts'
+
+const cache = ref([])
+const commandstats = ref(null)
+const usedmemory = ref(null)
+const { proxy } = getCurrentInstance()
+
+function getList() {
+ proxy.$modal.loading("姝e湪鍔犺浇缂撳瓨鐩戞帶鏁版嵁锛岃绋嶅�欙紒")
+ getCache().then(response => {
+ proxy.$modal.closeLoading()
+ cache.value = response.data
+
+ const commandstatsIntance = echarts.init(commandstats.value, "macarons")
+ commandstatsIntance.setOption({
+ tooltip: {
+ trigger: "item",
+ formatter: "{a} <br/>{b} : {c} ({d}%)"
+ },
+ series: [
+ {
+ name: "鍛戒护",
+ type: "pie",
+ roseType: "radius",
+ radius: [15, 95],
+ center: ["50%", "38%"],
+ data: response.data.commandStats,
+ animationEasing: "cubicInOut",
+ animationDuration: 1000
+ }
+ ]
+ })
+ const usedmemoryInstance = echarts.init(usedmemory.value, "macarons")
+ usedmemoryInstance.setOption({
+ tooltip: {
+ formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
+ },
+ series: [
+ {
+ name: "宄板��",
+ type: "gauge",
+ min: 0,
+ max: 1000,
+ detail: {
+ formatter: cache.value.info.used_memory_human
+ },
+ data: [
+ {
+ value: parseFloat(cache.value.info.used_memory_human),
+ name: "鍐呭瓨娑堣��"
+ }
+ ]
+ }
+ ]
+ })
+ window.addEventListener("resize", () => {
+ commandstatsIntance.resize()
+ usedmemoryInstance.resize()
+ })
+ })
+}
+
+getList()
+</script>
diff --git a/src/views/monitor/cache/list.vue b/src/views/monitor/cache/list.vue
new file mode 100644
index 0000000..77baec4
--- /dev/null
+++ b/src/views/monitor/cache/list.vue
@@ -0,0 +1,246 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="10">
+ <el-col :span="8">
+ <el-card style="height: calc(100vh - 125px)">
+ <template #header>
+ <Collection style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">缂撳瓨鍒楄〃</span>
+ <el-button
+ style="float: right; padding: 3px 0"
+ link
+ type="primary"
+ icon="Refresh"
+ @click="refreshCacheNames()"
+ ></el-button>
+ </template>
+ <el-table
+ v-loading="loading"
+ :data="cacheNames"
+ :height="tableHeight"
+ highlight-current-row
+ @row-click="getCacheKeys"
+ style="width: 100%"
+ >
+ <el-table-column
+ label="搴忓彿"
+ width="60"
+ type="index"
+ ></el-table-column>
+
+ <el-table-column
+ label="缂撳瓨鍚嶇О"
+ align="center"
+ prop="cacheName"
+ :show-overflow-tooltip="true"
+ :formatter="nameFormatter"
+ ></el-table-column>
+
+ <el-table-column
+ label="澶囨敞"
+ align="center"
+ prop="remark"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="鎿嶄綔"
+ width="60"
+ align="center"
+ class-name="small-padding fixed-width"
+ >
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ icon="Delete"
+ @click="handleClearCacheName(scope.row)"
+ ></el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card style="height: calc(100vh - 125px)">
+ <template #header>
+ <Key style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">閿悕鍒楄〃</span>
+ <el-button
+ style="float: right; padding: 3px 0"
+ link
+ type="primary"
+ icon="Refresh"
+ @click="refreshCacheKeys()"
+ ></el-button>
+ </template>
+ <el-table
+ v-loading="subLoading"
+ :data="cacheKeys"
+ :height="tableHeight"
+ highlight-current-row
+ @row-click="handleCacheValue"
+ style="width: 100%"
+ >
+ <el-table-column
+ label="搴忓彿"
+ width="60"
+ type="index"
+ ></el-table-column>
+ <el-table-column
+ label="缂撳瓨閿悕"
+ align="center"
+ :show-overflow-tooltip="true"
+ :formatter="keyFormatter"
+ >
+ </el-table-column>
+ <el-table-column
+ label="鎿嶄綔"
+ width="60"
+ align="center"
+ class-name="small-padding fixed-width"
+ >
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ icon="Delete"
+ @click="handleClearCacheKey(scope.row)"
+ ></el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card :bordered="false" style="height: calc(100vh - 125px)">
+ <template #header>
+ <Document style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">缂撳瓨鍐呭</span>
+ <el-button
+ style="float: right; padding: 3px 0"
+ link
+ type="primary"
+ icon="Refresh"
+ @click="handleClearCacheAll()"
+ >娓呯悊鍏ㄩ儴</el-button
+ >
+ </template>
+ <el-form :model="cacheForm">
+ <el-row :gutter="32">
+ <el-col :offset="1" :span="22">
+ <el-form-item label="缂撳瓨鍚嶇О:" prop="cacheName">
+ <el-input v-model="cacheForm.cacheName" :readOnly="true" />
+ </el-form-item>
+ </el-col>
+ <el-col :offset="1" :span="22">
+ <el-form-item label="缂撳瓨閿悕:" prop="cacheKey">
+ <el-input v-model="cacheForm.cacheKey" :readOnly="true" />
+ </el-form-item>
+ </el-col>
+ <el-col :offset="1" :span="22">
+ <el-form-item label="缂撳瓨鍐呭:" prop="cacheValue">
+ <el-input
+ v-model="cacheForm.cacheValue"
+ type="textarea"
+ :rows="8"
+ :readOnly="true"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup name="CacheList">
+import { listCacheName, listCacheKey, getCacheValue, clearCacheName, clearCacheKey, clearCacheAll } from "@/api/monitor/cache"
+
+const { proxy } = getCurrentInstance()
+
+const cacheNames = ref([])
+const cacheKeys = ref([])
+const cacheForm = ref({})
+const loading = ref(true)
+const subLoading = ref(false)
+const nowCacheName = ref("")
+const tableHeight = ref(window.innerHeight - 200)
+
+/** 鏌ヨ缂撳瓨鍚嶇О鍒楄〃 */
+function getCacheNames() {
+ loading.value = true
+ listCacheName().then(response => {
+ cacheNames.value = response.data
+ loading.value = false
+ })
+}
+
+/** 鍒锋柊缂撳瓨鍚嶇О鍒楄〃 */
+function refreshCacheNames() {
+ getCacheNames()
+ proxy.$modal.msgSuccess("鍒锋柊缂撳瓨鍒楄〃鎴愬姛")
+}
+
+/** 娓呯悊鎸囧畾鍚嶇О缂撳瓨 */
+function handleClearCacheName(row) {
+ clearCacheName(row.cacheName).then(response => {
+ proxy.$modal.msgSuccess("娓呯悊缂撳瓨鍚嶇О[" + row.cacheName + "]鎴愬姛")
+ getCacheKeys()
+ })
+}
+
+/** 鏌ヨ缂撳瓨閿悕鍒楄〃 */
+function getCacheKeys(row) {
+ const cacheName = row !== undefined ? row.cacheName : nowCacheName.value
+ if (cacheName === "") {
+ return
+ }
+ subLoading.value = true
+ listCacheKey(cacheName).then(response => {
+ cacheKeys.value = response.data
+ subLoading.value = false
+ nowCacheName.value = cacheName
+ })
+}
+
+/** 鍒锋柊缂撳瓨閿悕鍒楄〃 */
+function refreshCacheKeys() {
+ getCacheKeys()
+ proxy.$modal.msgSuccess("鍒锋柊閿悕鍒楄〃鎴愬姛")
+}
+
+/** 娓呯悊鎸囧畾閿悕缂撳瓨 */
+function handleClearCacheKey(cacheKey) {
+ clearCacheKey(cacheKey).then(response => {
+ proxy.$modal.msgSuccess("娓呯悊缂撳瓨閿悕[" + cacheKey + "]鎴愬姛")
+ getCacheKeys()
+ })
+}
+
+/** 鍒楄〃鍓嶇紑鍘婚櫎 */
+function nameFormatter(row) {
+ return row.cacheName.replace(":", "")
+}
+
+/** 閿悕鍓嶇紑鍘婚櫎 */
+function keyFormatter(cacheKey) {
+ return cacheKey.replace(nowCacheName.value, "")
+}
+
+/** 鏌ヨ缂撳瓨鍐呭璇︾粏 */
+function handleCacheValue(cacheKey) {
+ getCacheValue(nowCacheName.value, cacheKey).then(response => {
+ cacheForm.value = response.data
+ })
+}
+
+/** 娓呯悊鍏ㄩ儴缂撳瓨 */
+function handleClearCacheAll() {
+ clearCacheAll().then(response => {
+ proxy.$modal.msgSuccess("娓呯悊鍏ㄩ儴缂撳瓨鎴愬姛")
+ })
+}
+
+getCacheNames()
+</script>
diff --git a/src/views/monitor/druid/index.vue b/src/views/monitor/druid/index.vue
new file mode 100644
index 0000000..1faaead
--- /dev/null
+++ b/src/views/monitor/druid/index.vue
@@ -0,0 +1,13 @@
+<template>
+ <div>
+ <i-frame v-model:src="url"></i-frame>
+ </div>
+</template>
+
+<script setup>
+import iFrame from '@/components/iFrame'
+
+import { ref } from 'vue'
+
+const url = ref(import.meta.env.VITE_APP_BASE_API + '/druid/login.html')
+</script>
diff --git a/src/views/monitor/job/index.vue b/src/views/monitor/job/index.vue
new file mode 100644
index 0000000..ee291a4
--- /dev/null
+++ b/src/views/monitor/job/index.vue
@@ -0,0 +1,501 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="浠诲姟鍚嶇О" prop="jobName">
+ <el-input
+ v-model="queryParams.jobName"
+ placeholder="璇疯緭鍏ヤ换鍔″悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="浠诲姟缁勫悕" prop="jobGroup">
+ <el-select v-model="queryParams.jobGroup" placeholder="璇烽�夋嫨浠诲姟缁勫悕" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_job_group"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠诲姟鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="璇烽�夋嫨浠诲姟鐘舵��" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_job_status"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['monitor:job:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['monitor:job:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['monitor:job:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['monitor:job:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="Operation"
+ @click="handleJobLog"
+ v-hasPermi="['monitor:job:query']"
+ >鏃ュ織</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="浠诲姟缂栧彿" width="100" align="center" prop="jobId" />
+ <el-table-column label="浠诲姟鍚嶇О" align="center" prop="jobName" :show-overflow-tooltip="true" />
+ <el-table-column label="浠诲姟缁勫悕" align="center" prop="jobGroup">
+ <template #default="scope">
+ <dict-tag :options="sys_job_group" :value="scope.row.jobGroup" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璋冪敤鐩爣瀛楃涓�" align="center" prop="invokeTarget" :show-overflow-tooltip="true" />
+ <el-table-column label="cron鎵ц琛ㄨ揪寮�" align="center" prop="cronExpression" :show-overflow-tooltip="true" />
+ <el-table-column label="鐘舵��" align="center">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ active-value="0"
+ inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="200" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="淇敼" placement="top">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['monitor:job:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['monitor:job:remove']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鎵ц涓�娆�" placement="top">
+ <el-button link type="primary" icon="CaretRight" @click="handleRun(scope.row)" v-hasPermi="['monitor:job:changeStatus']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="浠诲姟璇︾粏" placement="top">
+ <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['monitor:job:query']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="璋冨害鏃ュ織" placement="top">
+ <el-button link type="primary" icon="Operation" @click="handleJobLog(scope.row)" v-hasPermi="['monitor:job:query']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀瑰畾鏃朵换鍔″璇濇 -->
+ <el-dialog :title="title" v-model="open" width="820px" append-to-body>
+ <el-form ref="jobRef" :model="form" :rules="rules" label-width="120px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="浠诲姟鍚嶇О" prop="jobName">
+ <el-input v-model="form.jobName" placeholder="璇疯緭鍏ヤ换鍔″悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠诲姟鍒嗙粍" prop="jobGroup">
+ <el-select v-model="form.jobGroup" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="dict in sys_job_group"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item prop="invokeTarget">
+ <template #label>
+ <span>
+ 璋冪敤鏂规硶
+ <el-tooltip placement="top">
+ <template #content>
+ <div>
+ Bean璋冪敤绀轰緥锛歳yTask.ryParams('ry')
+ <br />Class绫昏皟鐢ㄧず渚嬶細com.ruoyi.quartz.task.RyTask.ryParams('ry')
+ <br />鍙傛暟璇存槑锛氭敮鎸佸瓧绗︿覆锛屽竷灏旂被鍨嬶紝闀挎暣鍨嬶紝娴偣鍨嬶紝鏁村瀷
+ </div>
+ </template>
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </span>
+ </template>
+ <el-input v-model="form.invokeTarget" placeholder="璇疯緭鍏ヨ皟鐢ㄧ洰鏍囧瓧绗︿覆" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="cron琛ㄨ揪寮�" prop="cronExpression">
+ <el-input v-model="form.cronExpression" placeholder="璇疯緭鍏ron鎵ц琛ㄨ揪寮�">
+ <template #append>
+ <el-button type="primary" @click="handleShowCron">
+ 鐢熸垚琛ㄨ揪寮�
+ <i class="el-icon-time el-icon--right"></i>
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" v-if="form.jobId !== undefined">
+ <el-form-item label="鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_job_status"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎵ц绛栫暐" prop="misfirePolicy">
+ <el-radio-group v-model="form.misfirePolicy">
+ <el-radio-button value="1">绔嬪嵆鎵ц</el-radio-button>
+ <el-radio-button value="2">鎵ц涓�娆�</el-radio-button>
+ <el-radio-button value="3">鏀惧純鎵ц</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄惁骞跺彂" prop="concurrent">
+ <el-radio-group v-model="form.concurrent">
+ <el-radio-button value="0">鍏佽</el-radio-button>
+ <el-radio-button value="1">绂佹</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <el-dialog title="Cron琛ㄨ揪寮忕敓鎴愬櫒" v-model="openCron" append-to-body destroy-on-close>
+ <crontab ref="crontabRef" @hide="openCron=false" @fill="crontabFill" :expression="expression"></crontab>
+ </el-dialog>
+
+ <!-- 浠诲姟鏃ュ織璇︾粏 -->
+ <el-dialog title="浠诲姟璇︾粏" v-model="openView" width="700px" append-to-body>
+ <el-form :model="form" label-width="120px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="浠诲姟缂栧彿锛�">{{ form.jobId }}</el-form-item>
+ <el-form-item label="浠诲姟鍚嶇О锛�">{{ form.jobName }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠诲姟鍒嗙粍锛�">{{ jobGroupFormat(form) }}</el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿锛�">{{ form.createTime }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="cron琛ㄨ揪寮忥細">{{ form.cronExpression }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓嬫鎵ц鏃堕棿锛�">{{ parseTime(form.nextValidTime) }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="璋冪敤鐩爣鏂规硶锛�">{{ form.invokeTarget }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠诲姟鐘舵�侊細">
+ <div v-if="form.status == 0">姝e父</div>
+ <div v-else-if="form.status == 1">鏆傚仠</div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄惁骞跺彂锛�">
+ <div v-if="form.concurrent == 0">鍏佽</div>
+ <div v-else-if="form.concurrent == 1">绂佹</div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎵ц绛栫暐锛�">
+ <div v-if="form.misfirePolicy == 0">榛樿绛栫暐</div>
+ <div v-else-if="form.misfirePolicy == 1">绔嬪嵆鎵ц</div>
+ <div v-else-if="form.misfirePolicy == 2">鎵ц涓�娆�</div>
+ <div v-else-if="form.misfirePolicy == 3">鏀惧純鎵ц</div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="openView = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Job">
+import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job"
+import Crontab from '@/components/Crontab'
+const router = useRouter()
+const { proxy } = getCurrentInstance()
+const { sys_job_group, sys_job_status } = proxy.useDict("sys_job_group", "sys_job_status")
+
+const jobList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const openView = ref(false)
+const openCron = ref(false)
+const expression = ref("")
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ jobName: undefined,
+ jobGroup: undefined,
+ status: undefined
+ },
+ rules: {
+ jobName: [{ required: true, message: "浠诲姟鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ invokeTarget: [{ required: true, message: "璋冪敤鐩爣瀛楃涓蹭笉鑳戒负绌�", trigger: "blur" }],
+ cronExpression: [{ required: true, message: "cron鎵ц琛ㄨ揪寮忎笉鑳戒负绌�", trigger: "change" }]
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ瀹氭椂浠诲姟鍒楄〃 */
+function getList() {
+ loading.value = true
+ listJob(queryParams.value).then(response => {
+ jobList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 浠诲姟缁勫悕瀛楀吀缈昏瘧 */
+function jobGroupFormat(row, column) {
+ return proxy.selectDictLabel(sys_job_group.value, row.jobGroup)
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ jobId: undefined,
+ jobName: undefined,
+ jobGroup: undefined,
+ invokeTarget: undefined,
+ cronExpression: undefined,
+ misfirePolicy: 1,
+ concurrent: 1,
+ status: "0"
+ }
+ proxy.resetForm("jobRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+// 澶氶�夋閫変腑鏁版嵁
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.jobId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+// 鏇村鎿嶄綔瑙﹀彂
+function handleCommand(command, row) {
+ switch (command) {
+ case "handleRun":
+ handleRun(row)
+ break
+ case "handleView":
+ handleView(row)
+ break
+ case "handleJobLog":
+ handleJobLog(row)
+ break
+ default:
+ break
+ }
+}
+
+// 浠诲姟鐘舵�佷慨鏀�
+function handleStatusChange(row) {
+ let text = row.status === "0" ? "鍚敤" : "鍋滅敤"
+ proxy.$modal.confirm('纭瑕�"' + text + '""' + row.jobName + '"浠诲姟鍚�?').then(function () {
+ return changeJobStatus(row.jobId, row.status)
+ }).then(() => {
+ proxy.$modal.msgSuccess(text + "鎴愬姛")
+ }).catch(function () {
+ row.status = row.status === "0" ? "1" : "0"
+ })
+}
+
+/* 绔嬪嵆鎵ц涓�娆� */
+function handleRun(row) {
+ proxy.$modal.confirm('纭瑕佺珛鍗虫墽琛屼竴娆�"' + row.jobName + '"浠诲姟鍚�?').then(function () {
+ return runJob(row.jobId, row.jobGroup)
+ }).then(() => {
+ proxy.$modal.msgSuccess("鎵ц鎴愬姛")})
+ .catch(() => {})
+}
+
+/** 浠诲姟璇︾粏淇℃伅 */
+function handleView(row) {
+ getJob(row.jobId).then(response => {
+ form.value = response.data
+ openView.value = true
+ })
+}
+
+/** cron琛ㄨ揪寮忔寜閽搷浣� */
+function handleShowCron() {
+ expression.value = form.value.cronExpression
+ openCron.value = true
+}
+
+/** 纭畾鍚庡洖浼犲�� */
+function crontabFill(value) {
+ form.value.cronExpression = value
+}
+
+/** 浠诲姟鏃ュ織鍒楄〃鏌ヨ */
+function handleJobLog(row) {
+ const jobId = row.jobId || 0
+ router.push('/monitor/job-log/index/' + jobId)
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞浠诲姟"
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const jobId = row.jobId || ids.value
+ getJob(jobId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼浠诲姟"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["jobRef"].validate(valid => {
+ if (valid) {
+ if (form.value.jobId != undefined) {
+ updateJob(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addJob(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const jobIds = row.jobId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎瀹氭椂浠诲姟缂栧彿涓�"' + jobIds + '"鐨勬暟鎹」?').then(function () {
+ return delJob(jobIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("monitor/job/export", {
+ ...queryParams.value,
+ }, `job_${new Date().getTime()}.xlsx`)
+}
+
+getList()
+</script>
diff --git a/src/views/monitor/job/log.vue b/src/views/monitor/job/log.vue
new file mode 100644
index 0000000..b425e45
--- /dev/null
+++ b/src/views/monitor/job/log.vue
@@ -0,0 +1,283 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="浠诲姟鍚嶇О" prop="jobName">
+ <el-input
+ v-model="queryParams.jobName"
+ placeholder="璇疯緭鍏ヤ换鍔″悕绉�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="浠诲姟缁勫悕" prop="jobGroup">
+ <el-select
+ v-model="queryParams.jobGroup"
+ placeholder="璇烽�夋嫨浠诲姟缁勫悕"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_job_group"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎵ц鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨鎵ц鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_common_status"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎵ц鏃堕棿" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['monitor:job:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ @click="handleClean"
+ v-hasPermi="['monitor:job:remove']"
+ >娓呯┖</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['monitor:job:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Close"
+ @click="handleClose"
+ >鍏抽棴</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="jobLogList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鏃ュ織缂栧彿" width="80" align="center" prop="jobLogId" />
+ <el-table-column label="浠诲姟鍚嶇О" align="center" prop="jobName" :show-overflow-tooltip="true" />
+ <el-table-column label="浠诲姟缁勫悕" align="center" prop="jobGroup" :show-overflow-tooltip="true">
+ <template #default="scope">
+ <dict-tag :options="sys_job_group" :value="scope.row.jobGroup" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璋冪敤鐩爣瀛楃涓�" align="center" prop="invokeTarget" :show-overflow-tooltip="true" />
+ <el-table-column label="鏃ュ織淇℃伅" align="center" prop="jobMessage" :show-overflow-tooltip="true" />
+ <el-table-column label="鎵ц鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_common_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎵ц鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['monitor:job:query']">璇︾粏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 璋冨害鏃ュ織璇︾粏 -->
+ <el-dialog title="璋冨害鏃ュ織璇︾粏" v-model="open" width="700px" append-to-body>
+ <el-form :model="form" label-width="100px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鏃ュ織搴忓彿锛�">{{ form.jobLogId }}</el-form-item>
+ <el-form-item label="浠诲姟鍚嶇О锛�">{{ form.jobName }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠诲姟鍒嗙粍锛�">{{ form.jobGroup }}</el-form-item>
+ <el-form-item label="鎵ц鏃堕棿锛�">{{ form.createTime }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="璋冪敤鏂规硶锛�">{{ form.invokeTarget }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鏃ュ織淇℃伅锛�">{{ form.jobMessage }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鎵ц鐘舵�侊細">
+ <div v-if="form.status == 0">姝e父</div>
+ <div v-else-if="form.status == 1">澶辫触</div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="寮傚父淇℃伅锛�" v-if="form.status == 1">{{ form.exceptionInfo }}</el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="open = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="JobLog">
+import { getJob } from "@/api/monitor/job"
+import { listJobLog, delJobLog, cleanJobLog } from "@/api/monitor/jobLog"
+
+const { proxy } = getCurrentInstance()
+const { sys_common_status, sys_job_group } = proxy.useDict("sys_common_status", "sys_job_group")
+
+const jobLogList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const multiple = ref(true)
+const total = ref(0)
+const dateRange = ref([])
+const route = useRoute()
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ dictName: undefined,
+ dictType: undefined,
+ status: undefined
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ璋冨害鏃ュ織鍒楄〃 */
+function getList() {
+ loading.value = true
+ listJobLog(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
+ jobLogList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+// 杩斿洖鎸夐挳
+function handleClose() {
+ const obj = { path: "/monitor/job" }
+ proxy.$tab.closeOpenPage(obj)
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+// 澶氶�夋閫変腑鏁版嵁
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.jobLogId)
+ multiple.value = !selection.length
+}
+
+/** 璇︾粏鎸夐挳鎿嶄綔 */
+function handleView(row) {
+ open.value = true
+ form.value = row
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎璋冨害鏃ュ織缂栧彿涓�"' + ids.value + '"鐨勬暟鎹」?').then(function () {
+ return delJobLog(ids.value)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 娓呯┖鎸夐挳鎿嶄綔 */
+function handleClean() {
+ proxy.$modal.confirm("鏄惁纭娓呯┖鎵�鏈夎皟搴︽棩蹇楁暟鎹」?").then(function () {
+ return cleanJobLog()
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("娓呯┖鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("monitor/jobLog/export", {
+ ...queryParams.value,
+ }, `job_log_${new Date().getTime()}.xlsx`)
+}
+
+(() => {
+ const jobId = route.params && route.params.jobId
+ if (jobId !== undefined && jobId != 0) {
+ getJob(jobId).then(response => {
+ queryParams.value.jobName = response.data.jobName
+ queryParams.value.jobGroup = response.data.jobGroup
+ getList()
+ })
+ } else {
+ getList()
+ }
+})()
+</script>
diff --git a/src/views/monitor/logininfor/index.vue b/src/views/monitor/logininfor/index.vue
new file mode 100644
index 0000000..aa42b52
--- /dev/null
+++ b/src/views/monitor/logininfor/index.vue
@@ -0,0 +1,233 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="鐧诲綍鍦板潃" prop="ipaddr">
+ <el-input
+ v-model="queryParams.ipaddr"
+ placeholder="璇疯緭鍏ョ櫥褰曞湴鍧�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="userName">
+ <el-input
+ v-model="queryParams.userName"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="鐧诲綍鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_common_status"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐧诲綍鏃堕棿" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['monitor:logininfor:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ @click="handleClean"
+ v-hasPermi="['monitor:logininfor:remove']"
+ >娓呯┖</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Unlock"
+ :disabled="single"
+ @click="handleUnlock"
+ v-hasPermi="['monitor:logininfor:unlock']"
+ >瑙i攣</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['monitor:logininfor:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table ref="logininforRef" v-loading="loading" :data="logininforList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="璁块棶缂栧彿" align="center" prop="infoId" />
+ <el-table-column label="鐢ㄦ埛鍚嶇О" align="center" prop="userName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
+ <el-table-column label="鍦板潃" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
+ <el-table-column label="鐧诲綍鍦扮偣" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
+ <el-table-column label="鎿嶄綔绯荤粺" align="center" prop="os" :show-overflow-tooltip="true" />
+ <el-table-column label="娴忚鍣�" align="center" prop="browser" :show-overflow-tooltip="true" />
+ <el-table-column label="鐧诲綍鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_common_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻忚堪" align="center" prop="msg" :show-overflow-tooltip="true" />
+ <el-table-column label="璁块棶鏃堕棿" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.loginTime) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </div>
+</template>
+
+<script setup name="Logininfor">
+import { list, delLogininfor, cleanLogininfor, unlockLogininfor } from "@/api/monitor/logininfor"
+
+const { proxy } = getCurrentInstance()
+const { sys_common_status } = proxy.useDict("sys_common_status")
+
+const logininforList = ref([])
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const selectName = ref("")
+const total = ref(0)
+const dateRange = ref([])
+const defaultSort = ref({ prop: "loginTime", order: "descending" })
+
+// 鏌ヨ鍙傛暟
+const queryParams = ref({
+ pageNum: 1,
+ pageSize: 10,
+ ipaddr: undefined,
+ userName: undefined,
+ status: undefined,
+ orderByColumn: undefined,
+ isAsc: undefined
+})
+
+/** 鏌ヨ鐧诲綍鏃ュ織鍒楄〃 */
+function getList() {
+ loading.value = true
+ list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
+ logininforList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ queryParams.value.pageNum = 1
+ proxy.$refs["logininforRef"].sort(defaultSort.value.prop, defaultSort.value.order)
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.infoId)
+ multiple.value = !selection.length
+ single.value = selection.length != 1
+ selectName.value = selection.map(item => item.userName)
+}
+
+/** 鎺掑簭瑙﹀彂浜嬩欢 */
+function handleSortChange(column, prop, order) {
+ queryParams.value.orderByColumn = column.prop
+ queryParams.value.isAsc = column.order
+ getList()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const infoIds = row.infoId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎璁块棶缂栧彿涓�"' + infoIds + '"鐨勬暟鎹」?').then(function () {
+ return delLogininfor(infoIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 娓呯┖鎸夐挳鎿嶄綔 */
+function handleClean() {
+ proxy.$modal.confirm("鏄惁纭娓呯┖鎵�鏈夌櫥褰曟棩蹇楁暟鎹」?").then(function () {
+ return cleanLogininfor()
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("娓呯┖鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瑙i攣鎸夐挳鎿嶄綔 */
+function handleUnlock() {
+ const username = selectName.value
+ proxy.$modal.confirm('鏄惁纭瑙i攣鐢ㄦ埛"' + username + '"鏁版嵁椤�?').then(function () {
+ return unlockLogininfor(username)
+ }).then(() => {
+ proxy.$modal.msgSuccess("鐢ㄦ埛" + username + "瑙i攣鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("monitor/logininfor/export", {
+ ...queryParams.value,
+ }, `logininfor_${new Date().getTime()}.xlsx`)
+}
+
+getList()
+</script>
diff --git a/src/views/monitor/online/index.vue b/src/views/monitor/online/index.vue
new file mode 100644
index 0000000..21f6463
--- /dev/null
+++ b/src/views/monitor/online/index.vue
@@ -0,0 +1,109 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true">
+ <el-form-item label="鐧诲綍鍦板潃" prop="ipaddr">
+ <el-input
+ v-model="queryParams.ipaddr"
+ placeholder="璇疯緭鍏ョ櫥褰曞湴鍧�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="userName">
+ <el-input
+ v-model="queryParams.userName"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <el-table
+ v-loading="loading"
+ :data="onlineList.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
+ style="width: 100%;"
+ >
+ <el-table-column label="搴忓彿" width="50" type="index" align="center">
+ <template #default="scope">
+ <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="浼氳瘽缂栧彿" align="center" prop="tokenId" :show-overflow-tooltip="true" />
+ <el-table-column label="鐧诲綍鍚嶇О" align="center" prop="userName" :show-overflow-tooltip="true" />
+ <el-table-column label="鎵�灞為儴闂�" align="center" prop="deptName" :show-overflow-tooltip="true" />
+ <el-table-column label="涓绘満" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
+ <el-table-column label="鐧诲綍鍦扮偣" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
+ <el-table-column label="鎿嶄綔绯荤粺" align="center" prop="os" :show-overflow-tooltip="true" />
+ <el-table-column label="娴忚鍣�" align="center" prop="browser" :show-overflow-tooltip="true" />
+ <el-table-column label="鐧诲綍鏃堕棿" align="center" prop="loginTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.loginTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Delete" @click="handleForceLogout(scope.row)" v-hasPermi="['monitor:online:forceLogout']">寮洪��</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
+ </div>
+</template>
+
+<script setup name="Online">
+import { forceLogout, list as initData } from "@/api/monitor/online"
+
+const { proxy } = getCurrentInstance()
+
+const onlineList = ref([])
+const loading = ref(true)
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = ref(10)
+
+const queryParams = ref({
+ ipaddr: undefined,
+ userName: undefined
+})
+
+/** 鏌ヨ鐧诲綍鏃ュ織鍒楄〃 */
+function getList() {
+ loading.value = true
+ initData(queryParams.value).then(response => {
+ onlineList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ pageNum.value = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 寮洪��鎸夐挳鎿嶄綔 */
+function handleForceLogout(row) {
+ proxy.$modal.confirm('鏄惁纭寮洪��鍚嶇О涓�"' + row.userName + '"鐨勭敤鎴�?').then(function () {
+ return forceLogout(row.tokenId)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+getList()
+</script>
diff --git a/src/views/monitor/operlog/index.vue b/src/views/monitor/operlog/index.vue
new file mode 100644
index 0000000..92f9c35
--- /dev/null
+++ b/src/views/monitor/operlog/index.vue
@@ -0,0 +1,313 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="鎿嶄綔鍦板潃" prop="operIp">
+ <el-input
+ v-model="queryParams.operIp"
+ placeholder="璇疯緭鍏ユ搷浣滃湴鍧�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绯荤粺妯″潡" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ョ郴缁熸ā鍧�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎿嶄綔浜哄憳" prop="operName">
+ <el-input
+ v-model="queryParams.operName"
+ placeholder="璇疯緭鍏ユ搷浣滀汉鍛�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="businessType">
+ <el-select
+ v-model="queryParams.businessType"
+ placeholder="鎿嶄綔绫诲瀷"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_oper_type"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="鎿嶄綔鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_common_status"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎿嶄綔鏃堕棿" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['monitor:operlog:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ @click="handleClean"
+ v-hasPermi="['monitor:operlog:remove']"
+ >娓呯┖</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['monitor:operlog:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="operId" />
+ <el-table-column label="绯荤粺妯″潡" align="center" prop="title" :show-overflow-tooltip="true" />
+ <el-table-column label="鎿嶄綔绫诲瀷" align="center" prop="businessType">
+ <template #default="scope">
+ <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔浜哄憳" align="center" width="110" prop="operName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
+ <el-table-column label="鎿嶄綔鍦板潃" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
+ <el-table-column label="鎿嶄綔鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_common_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔鏃ユ湡" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.operTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="娑堣�楁椂闂�" align="center" prop="costTime" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']">
+ <template #default="scope">
+ <span>{{ scope.row.costTime }}姣</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="View" @click="handleView(scope.row, scope.index)" v-hasPermi="['monitor:operlog:query']">璇︾粏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 鎿嶄綔鏃ュ織璇︾粏 -->
+ <el-dialog title="鎿嶄綔鏃ュ織璇︾粏" v-model="open" width="800px" append-to-body>
+ <el-form :model="form" label-width="100px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎿嶄綔妯″潡锛�">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
+ <el-form-item
+ label="鐧诲綍淇℃伅锛�"
+ >{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇锋眰鍦板潃锛�">{{ form.operUrl }}</el-form-item>
+ <el-form-item label="璇锋眰鏂瑰紡锛�">{{ form.requestMethod }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鎿嶄綔鏂规硶锛�">{{ form.method }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="璇锋眰鍙傛暟锛�">{{ form.operParam }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="杩斿洖鍙傛暟锛�">{{ form.jsonResult }}</el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎿嶄綔鐘舵�侊細">
+ <div v-if="form.status === 0">姝e父</div>
+ <div v-else-if="form.status === 1">澶辫触</div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="娑堣�楁椂闂达細">{{ form.costTime }}姣</el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎿嶄綔鏃堕棿锛�">{{ parseTime(form.operTime) }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="寮傚父淇℃伅锛�" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="open = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Operlog">
+import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog"
+import {onMounted} from "vue";
+
+const { proxy } = getCurrentInstance()
+const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type","sys_common_status")
+
+const operlogList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const dateRange = ref([])
+const defaultSort = ref({ prop: "operTime", order: "descending" })
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ operIp: undefined,
+ title: undefined,
+ operName: undefined,
+ businessType: undefined,
+ status: undefined
+ }
+})
+
+const { queryParams, form } = toRefs(data)
+
+/** 鏌ヨ鐧诲綍鏃ュ織 */
+function getList() {
+ loading.value = true
+ list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
+ operlogList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鎿嶄綔鏃ュ織绫诲瀷瀛楀吀缈昏瘧 */
+function typeFormat(row, column) {
+ return proxy.selectDictLabel(sys_oper_type.value, row.businessType)
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ queryParams.value.pageNum = 1
+ proxy.$refs["operlogRef"].sort(defaultSort.value.prop, defaultSort.value.order)
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.operId)
+ multiple.value = !selection.length
+}
+
+/** 鎺掑簭瑙﹀彂浜嬩欢 */
+function handleSortChange(column, prop, order) {
+ queryParams.value.orderByColumn = column.prop
+ queryParams.value.isAsc = column.order
+ getList()
+}
+
+/** 璇︾粏鎸夐挳鎿嶄綔 */
+function handleView(row) {
+ open.value = true
+ form.value = row
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const operIds = row.operId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鏃ュ織缂栧彿涓�"' + operIds + '"鐨勬暟鎹」?').then(function () {
+ return delOperlog(operIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 娓呯┖鎸夐挳鎿嶄綔 */
+function handleClean() {
+ proxy.$modal.confirm("鏄惁纭娓呯┖鎵�鏈夋搷浣滄棩蹇楁暟鎹」?").then(function () {
+ return cleanOperlog()
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("娓呯┖鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("monitor/operlog/export",{
+ ...queryParams.value,
+ }, `config_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/monitor/server/index.vue b/src/views/monitor/server/index.vue
new file mode 100644
index 0000000..d9840ac
--- /dev/null
+++ b/src/views/monitor/server/index.vue
@@ -0,0 +1,190 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="10">
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><Cpu style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">CPU</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <thead>
+ <tr>
+ <th class="el-table__cell is-leaf"><div class="cell">灞炴��</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鍊�</div></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鏍稿績鏁�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.cpuNum }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鐢ㄦ埛浣跨敤鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.used }}%</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">绯荤粺浣跨敤鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.sys }}%</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">褰撳墠绌洪棽鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.free }}%</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><Tickets style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍐呭瓨</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <thead>
+ <tr>
+ <th class="el-table__cell is-leaf"><div class="cell">灞炴��</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鍐呭瓨</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">JVM</div></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鎬诲唴瀛�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.total }}G</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.total }}M</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">宸茬敤鍐呭瓨</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.used}}G</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.used}}M</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鍓╀綑鍐呭瓨</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.free }}G</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.free }}M</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">浣跨敤鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem" :class="{'text-danger': server.mem.usage > 80}">{{ server.mem.usage }}%</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm" :class="{'text-danger': server.jvm.usage > 80}">{{ server.jvm.usage }}%</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鏈嶅姟鍣ㄤ俊鎭�</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鏈嶅姟鍣ㄥ悕绉�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">鎿嶄綔绯荤粺</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osName }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鏈嶅姟鍣↖P</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerIp }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">绯荤粺鏋舵瀯</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osArch }}</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><CoffeeCup style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">Java铏氭嫙鏈轰俊鎭�</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;table-layout:fixed;">
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">Java鍚嶇О</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.name }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">Java鐗堟湰</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.version }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鍚姩鏃堕棿</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.startTime }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">杩愯鏃堕暱</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.runTime }}</div></td>
+ </tr>
+ <tr>
+ <td colspan="1" class="el-table__cell is-leaf"><div class="cell">瀹夎璺緞</div></td>
+ <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.home }}</div></td>
+ </tr>
+ <tr>
+ <td colspan="1" class="el-table__cell is-leaf"><div class="cell">椤圭洰璺緞</div></td>
+ <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.userDir }}</div></td>
+ </tr>
+ <tr>
+ <td colspan="1" class="el-table__cell is-leaf"><div class="cell">杩愯鍙傛暟</div></td>
+ <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.inputArgs }}</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><MessageBox style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">纾佺洏鐘舵��</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <thead>
+ <tr>
+ <th class="el-table__cell el-table__cell is-leaf"><div class="cell">鐩樼璺緞</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鏂囦欢绯荤粺</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鐩樼绫诲瀷</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鎬诲ぇ灏�</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鍙敤澶у皬</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">宸茬敤澶у皬</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">宸茬敤鐧惧垎姣�</div></th>
+ </tr>
+ </thead>
+ <tbody v-if="server.sysFiles">
+ <tr v-for="(sysFile, index) in server.sysFiles" :key="index">
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.dirName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.sysTypeName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.typeName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.total }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.free }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.used }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" :class="{'text-danger': sysFile.usage > 80}">{{ sysFile.usage }}%</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import { getServer } from '@/api/monitor/server'
+import {onMounted} from "vue";
+
+const server = ref([])
+const { proxy } = getCurrentInstance()
+
+function getList() {
+ proxy.$modal.loading("姝e湪鍔犺浇鏈嶅姟鐩戞帶鏁版嵁锛岃绋嶅�欙紒")
+ getServer().then(response => {
+ server.value = response.data
+ proxy.$modal.closeLoading()
+ })
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/monitorManagement/areaControl/index.vue b/src/views/monitorManagement/areaControl/index.vue
new file mode 100644
index 0000000..1dd5a36
--- /dev/null
+++ b/src/views/monitorManagement/areaControl/index.vue
@@ -0,0 +1,264 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="16">
+ <el-col :span="16">
+ <el-card shadow="never" class="section-card">
+ <template #header>
+ <div class="card-header">
+ <span>鍖哄煙绠$悊锛堝弻閲嶉棬绂侊級</span>
+ <div class="header-actions">
+ <el-select v-model="selectedPlant" placeholder="閫夋嫨鍘傚尯" size="small" style="width: 160px" @change="filterZones">
+ <el-option v-for="plant in plants" :key="plant.id" :label="plant.name" :value="plant.id" />
+ </el-select>
+ <el-switch v-model="onlyCritical" inline-prompt :active-text="'浠呭叧閿尯'" :inactive-text="'鍏ㄩ儴'" @change="filterZones" />
+ </div>
+ </div>
+ </template>
+ <el-table :data="filteredZones" border style="width: 100%" height="320">
+ <el-table-column type="index" width="60" label="搴忓彿" align="center" />
+ <el-table-column prop="name" label="鍖哄煙鍚嶇О" min-width="160" show-overflow-tooltip />
+ <el-table-column prop="zoneType" label="绫诲瀷" width="120" />
+ <el-table-column label="鍙岄棬鑱斿姩" width="120" align="center">
+ <template #default="{ row }">
+ <el-tag v-if="row.dualAccess" type="success">宸插惎鐢�</el-tag>
+ <el-tag v-else type="info">鏈惎鐢�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍦ㄧ嚎浜烘暟" width="100" align="center">
+ <template #default="{ row }">{{ row.currentPersons }}</template>
+ </el-table-column>
+ <el-table-column label="瀹夊叏鐘舵��" width="140" align="center">
+ <template #default="{ row }">
+ <el-tag :type="row.status === '姝e父' ? 'success' : row.status === '棰勮' ? 'warning' : 'danger'">
+ {{ row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" align="center" fixed="right">
+ <template #default="{ row }">
+ <el-button link type="primary" size="small" @click="toggleDual(row)">
+ {{ row.dualAccess ? '鍋滅敤鍙岄棬' : '鍚敤鍙岄棬' }}
+ </el-button>
+ <el-button link type="success" size="small" @click="openAccessSim(row)">妯℃嫙寮�闂�</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <el-card shadow="never" class="section-card">
+ <template #header>
+ <div class="card-header">
+ <span>鍩硅鑱斿姩锛堟湭瀹屾垚/杩囨湡绂佹杩涘叆锛�</span>
+ <div class="header-actions">
+ <el-input v-model="accessSim.personId" placeholder="浜哄憳宸ュ彿" size="small" style="width: 140px" />
+ <el-select v-model="accessSim.targetZoneId" placeholder="閫夋嫨鐩爣鍖哄煙" size="small" style="width: 180px">
+ <el-option v-for="z in zones" :key="z.id" :label="z.name" :value="z.id" />
+ </el-select>
+ <el-button type="primary" size="small" @click="simulateAccess">妫�楠屽噯鍏�</el-button>
+ </div>
+ </div>
+ </template>
+ <el-descriptions :column="3" border size="small" v-if="accessResult">
+ <el-descriptions-item label="宸ュ彿">{{ accessResult.person.id }}锛坽{ accessResult.person.dept }}锛�</el-descriptions-item>
+ <el-descriptions-item label="鍩硅鐘舵��">
+ <el-tag :type="accessResult.person.training.valid ? 'success' : 'danger'">
+ {{ accessResult.person.training.valid ? '鏈夋晥' : '澶辨晥/鏈畬鎴�' }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐩爣鍖哄煙">{{ accessResult.zone.name }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�杩戝煿璁�">{{ accessResult.person.training.lastDate }}</el-descriptions-item>
+ <el-descriptions-item label="閫傚矖璇佹湁鏁堟湡">{{ accessResult.person.training.expireDate }}</el-descriptions-item>
+ <el-descriptions-item label="鍑嗗叆缁撴灉">
+ <el-tag :type="accessResult.allowed ? 'success' : 'danger'">{{ accessResult.allowed ? '鍏佽杩涘叆' : '绂佹杩涘叆' }}</el-tag>
+ </el-descriptions-item>
+ </el-descriptions>
+ <el-empty v-else description="璇疯緭鍏ヤ汉鍛樹笌鍖哄煙杩涜妫�楠�" />
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card shadow="never" class="section-card">
+ <template #header>
+ <div class="card-header">
+ <span>浣╂埓璁惧婊炵暀鍛婅锛堝嵄闄╁尯瓒呮椂锛�</span>
+ <div class="header-actions">
+ <el-select v-model="stayThreshold" size="small" style="width: 140px">
+ <el-option :value="10" label="闃堝�� 10 鍒嗛挓" />
+ <el-option :value="20" label="闃堝�� 20 鍒嗛挓" />
+ <el-option :value="30" label="闃堝�� 30 鍒嗛挓" />
+ </el-select>
+ <el-switch v-model="alarmOn" inline-prompt :active-text="'鍛婅寮�'" :inactive-text="'鍛婅鍏�'" />
+ </div>
+ </div>
+ </template>
+ <el-timeline style="max-height: 520px; overflow: auto">
+ <el-timeline-item v-for="(item, idx) in alarms" :key="idx" :type="item.level" :timestamp="item.time">
+ <div class="alarm-item">
+ <div class="title">
+ {{ item.personId }} 路 {{ item.zoneName }} 路 婊炵暀 {{ item.stayMins }} 鍒嗛挓
+ </div>
+ <div class="desc">璁惧锛歿{ item.deviceId }}锛堜俊鍙峰己搴� {{ item.rssi }} dBm锛�</div>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+ <el-dialog v-model="doorSimVisible" title="闂ㄧ寮�闂ㄦā鎷�" width="420px">
+ <el-form :model="doorSim" label-width="90px">
+ <el-form-item label="鍖哄煙">
+ <el-input v-model="doorSim.zoneName" disabled />
+ </el-form-item>
+ <el-form-item label="闂ㄧ1">
+ <el-switch v-model="doorSim.door1" />
+ </el-form-item>
+ <el-form-item label="闂ㄧ2">
+ <el-switch v-model="doorSim.door2" />
+ </el-form-item>
+ <el-alert type="info" show-icon :closable="false" title="鍙岄棬鍧囦负寮�鍚柟鍙�氳" />
+ </el-form>
+ <template #footer>
+ <el-button @click="doorSimVisible = false">鍏抽棴</el-button>
+ <el-button type="primary" :disabled="!(doorSim.door1 && doorSim.door2)" @click="confirmPass">閫氳</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from "vue";
+
+// 鍘傚尯涓庡尯鍩燂紙鐓ょ偔琛屼笟璇箟銆佸敖閲忚创杩戠湡瀹烇級
+const plants = ref([
+ { id: "P01", name: "涓�鍙烽�夌叅鍘�" },
+ { id: "P02", name: "浜屽彿娲楃叅鍒嗗巶" },
+]);
+const zones = ref([
+ { id: "Z01", plantId: "P01", name: "涓帶瀹�", zoneType: "鎺у埗瀹�", dualAccess: true, currentPersons: 4, status: "姝e父" },
+ { id: "Z02", plantId: "P01", name: "鐓ゅ満A鍖�", zoneType: "鍫嗗瓨鍖�", dualAccess: true, currentPersons: 12, status: "棰勮" },
+ { id: "Z03", plantId: "P01", name: "鍗遍櫓鍝佸簱", zoneType: "鍗卞寲鍝�", dualAccess: true, currentPersons: 1, status: "姝e父" },
+ { id: "Z04", plantId: "P01", name: "楂樺帇閰嶇數瀹�", zoneType: "鐢垫皵闂�", dualAccess: true, currentPersons: 2, status: "姝e父" },
+ { id: "Z05", plantId: "P02", name: "鐨甫寤婂寳娈�", zoneType: "杈撻�佸粖閬�", dualAccess: false, currentPersons: 5, status: "姝e父" },
+ { id: "Z06", plantId: "P02", name: "绛涘垎杞﹂棿", zoneType: "浣滀笟鍖�", dualAccess: false, currentPersons: 9, status: "棰勮" },
+]);
+
+const selectedPlant = ref(plants.value[0].id);
+const onlyCritical = ref(true);
+const filteredZones = ref([]);
+
+function filterZones() {
+ const data = zones.value.filter((z) => z.plantId === selectedPlant.value);
+ filteredZones.value = onlyCritical.value ? data.filter((z) => z.dualAccess) : data;
+}
+
+function toggleDual(row) {
+ row.dualAccess = !row.dualAccess;
+ filterZones();
+}
+
+// 闂ㄧ寮�闂ㄦā鎷�
+const doorSimVisible = ref(false);
+const doorSim = reactive({ zoneId: "", zoneName: "", door1: false, door2: false });
+function openAccessSim(row) {
+ doorSim.zoneId = row.id;
+ doorSim.zoneName = row.name;
+ doorSim.door1 = false;
+ doorSim.door2 = false;
+ doorSimVisible.value = true;
+}
+function confirmPass() {
+ doorSimVisible.value = false;
+}
+
+// 鍩硅鑱斿姩妯℃嫙
+const persons = ref([
+ { id: "EMP1001", dept: "鐢熶骇涓�闃�", training: { valid: true, lastDate: "2025-09-12", expireDate: "2026-09-12" } },
+ { id: "EMP1018", dept: "鏈虹數鐝�", training: { valid: false, lastDate: "2024-07-03", expireDate: "2025-07-03" } },
+ { id: "EMP1022", dept: "瀹夌洃绉�", training: { valid: true, lastDate: "2025-08-01", expireDate: "2026-08-01" } },
+]);
+const accessSim = reactive({ personId: "", targetZoneId: "" });
+const accessResult = ref(null);
+
+function simulateAccess() {
+ const person = persons.value.find((p) => p.id === accessSim.personId);
+ const zone = zones.value.find((z) => z.id === accessSim.targetZoneId);
+ if (!person || !zone) {
+ accessResult.value = null;
+ return;
+ }
+ const allowed = person.training.valid && (zone.zoneType !== "鍗卞寲鍝�" || person.dept === "瀹夌洃绉�");
+ accessResult.value = { allowed, person, zone };
+}
+
+// 浣╂埓璁惧婊炵暀鍛婅锛堝亣鏁版嵁瀹氭椂鎺ㄩ�侊級
+const stayThreshold = ref(20);
+const alarmOn = ref(true);
+const alarms = ref([
+ { time: "09:35", level: "warning", personId: "EMP1001", zoneName: "鐓ゅ満A鍖�", stayMins: 18, deviceId: "TAG-7A12", rssi: -67 },
+]);
+
+let timer = null;
+function pushMockAlarm() {
+ if (!alarmOn.value) return;
+ const candidates = [
+ { personId: "EMP1018", zoneName: "绛涘垎杞﹂棿", base: 12 },
+ { personId: "EMP1022", zoneName: "楂樺帇閰嶇數瀹�", base: 9 },
+ { personId: "EMP1001", zoneName: "鐓ゅ満A鍖�", base: 16 },
+ ];
+ const pick = candidates[Math.floor(Math.random() * candidates.length)];
+ const stay = pick.base + Math.floor(Math.random() * 10);
+ if (stay >= stayThreshold.value) {
+ const now = new Date();
+ const hh = String(now.getHours()).padStart(2, "0");
+ const mm = String(now.getMinutes()).padStart(2, "0");
+ alarms.value.unshift({
+ time: `${hh}:${mm}`,
+ level: stay >= stayThreshold.value + 10 ? "danger" : "warning",
+ personId: pick.personId,
+ zoneName: pick.zoneName,
+ stayMins: stay,
+ deviceId: `TAG-${Math.random().toString(16).slice(2, 6).toUpperCase()}`,
+ rssi: -60 - Math.floor(Math.random() * 15),
+ });
+ if (alarms.value.length > 30) alarms.value.pop();
+ }
+}
+
+onMounted(() => {
+ filterZones();
+ timer = setInterval(pushMockAlarm, 4500);
+});
+
+// 绂诲紑鏃舵竻鐞�
+if (import.meta.hot) {
+ import.meta.hot.dispose(() => {
+ if (timer) clearInterval(timer);
+ });
+}
+</script>
+
+<style scoped lang="scss">
+.section-card {
+ margin-bottom: 16px;
+}
+.card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.header-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+.alarm-item .title {
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+.alarm-item .desc {
+ color: #666;
+ font-size: 12px;
+}
+</style>
+
+
diff --git a/src/views/monitorManagement/videoMonitor/index.vue b/src/views/monitorManagement/videoMonitor/index.vue
new file mode 100644
index 0000000..b78c7f5
--- /dev/null
+++ b/src/views/monitorManagement/videoMonitor/index.vue
@@ -0,0 +1,990 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="16">
+ <!-- 宸︿晶锛氳棰戠洃鎺у垪琛ㄤ笌鎶撴媿璁板綍 -->
+ <el-col :span="16">
+ <!-- 瑙嗛鐩戞帶鍒楄〃 -->
+ <el-card shadow="never" class="section-card">
+ <template #header>
+ <div class="card-header">
+ <span>瑙嗛鐩戞帶鐐逛綅绠$悊</span>
+ <div class="header-actions">
+ <el-select v-model="selectedArea" placeholder="閫夋嫨鍖哄煙" size="small" style="width: 160px" @change="filterCameras">
+ <el-option label="鍏ㄩ儴鍖哄煙" value="all" />
+ <el-option v-for="area in areas" :key="area.id" :label="area.name" :value="area.id" />
+ </el-select>
+ <el-select v-model="cameraStatus" placeholder="璁惧鐘舵��" size="small" style="width: 120px" @change="filterCameras">
+ <el-option label="鍏ㄩ儴鐘舵��" value="all" />
+ <el-option label="鍦ㄧ嚎" value="online" />
+ <el-option label="绂荤嚎" value="offline" />
+ </el-select>
+ </div>
+ </div>
+ </template>
+ <el-table :data="filteredCameras" border style="width: 100%" max-height="320">
+ <el-table-column type="index" width="50" label="搴忓彿" align="center" />
+ <el-table-column prop="name" label="鐩戞帶鐐逛綅" min-width="140" show-overflow-tooltip />
+ <el-table-column prop="areaName" label="鎵�灞炲尯鍩�" width="120" />
+ <el-table-column label="璁惧鐘舵��" width="100" align="center">
+ <template #default="{ row }">
+ <el-tag :type="row.status === 'online' ? 'success' : 'danger'" size="small">
+ {{ row.status === 'online' ? '鍦ㄧ嚎' : '绂荤嚎' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="AI璇嗗埆" width="100" align="center">
+ <template #default="{ row }">
+ <el-tag v-if="row.aiEnabled" type="success" size="small">宸插惎鐢�</el-tag>
+ <el-tag v-else type="info" size="small">鏈惎鐢�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="闂ㄧ鑱斿姩" width="100" align="center">
+ <template #default="{ row }">
+ <el-tag v-if="row.doorLinked" type="primary" size="small">宸茬粦瀹�</el-tag>
+ <el-tag v-else type="info" size="small">鏈粦瀹�</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" align="center" fixed="right">
+ <template #default="{ row }">
+ <el-button link type="primary" size="small" @click="viewRealtime(row)">瀹炴椂鐢婚潰</el-button>
+ <el-button link type="success" size="small" @click="viewCaptures(row)">鎶撴媿璁板綍</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 闂ㄧ鎶撴媿璁板綍 -->
+ <el-card shadow="never" class="section-card">
+ <template #header>
+ <div class="card-header">
+ <span>闂ㄧ鎶撴媿璁板綍</span>
+ <div class="header-actions">
+ <el-date-picker
+ v-model="captureDate"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ size="small"
+ style="width: 260px"
+ @change="loadCaptures"
+ />
+ <el-select v-model="captureEventType" placeholder="浜嬩欢绫诲瀷" size="small" style="width: 100px" clearable>
+ <el-option label="鍏ㄩ儴" value="" />
+ <el-option label="杩涘叆" value="entry" />
+ <el-option label="绂诲紑" value="exit" />
+ </el-select>
+ <el-input v-model="captureSearch" placeholder="鎼滅储宸ュ彿/鍖哄煙" size="small" style="width: 140px" clearable>
+ <template #prefix>
+ <el-icon><Search /></el-icon>
+ </template>
+ </el-input>
+ <el-button type="primary" size="small" @click="exportCaptures">
+ <el-icon><Download /></el-icon>
+ 瀵煎嚭
+ </el-button>
+ </div>
+ </div>
+ </template>
+
+ <!-- 缁熻淇℃伅 -->
+ <div class="capture-stats">
+ <el-row :gutter="16">
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-label">浠婃棩鎶撴媿</div>
+ <div class="stat-value">{{ captureStats.today }}</div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-label">杩涘叆璁板綍</div>
+ <div class="stat-value" style="color: #67c23a">{{ captureStats.entry }}</div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-label">绂诲紑璁板綍</div>
+ <div class="stat-value" style="color: #e6a23c">{{ captureStats.exit }}</div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-label">浣庡尮閰嶅害</div>
+ <div class="stat-value" style="color: #f56c6c">{{ captureStats.lowMatch }}</div>
+ </div>
+ </el-col>
+ </el-row>
+ </div>
+
+ <el-table
+ :data="paginatedCaptures"
+ border
+ style="width: 100%"
+ max-height="350"
+ @selection-change="handleCaptureSelection"
+ >
+ <el-table-column type="selection" width="45" align="center" />
+ <el-table-column type="index" width="50" label="搴忓彿" align="center" :index="indexMethod" />
+ <el-table-column prop="time" label="鎶撴媿鏃堕棿" width="155" sortable />
+ <el-table-column prop="personId" label="宸ュ彿" width="110" show-overflow-tooltip />
+ <el-table-column prop="department" label="閮ㄩ棬" width="100" show-overflow-tooltip />
+ <el-table-column prop="areaName" label="鍖哄煙" width="110" show-overflow-tooltip />
+ <el-table-column label="闂ㄧ浜嬩欢" width="90" align="center">
+ <template #default="{ row }">
+ <el-tag :type="row.eventType === 'entry' ? 'success' : 'warning'" size="small">
+ {{ row.eventType === 'entry' ? '杩涘叆' : '绂诲紑' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜鸿劯鍖归厤" width="95" align="center" sortable :sort-method="(a, b) => a.faceMatch - b.faceMatch">
+ <template #default="{ row }">
+ <el-tag :type="getFaceMatchType(row.faceMatch)" size="small">
+ {{ row.faceMatch }}%
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="cameraName" label="鎽勫儚澶�" width="120" show-overflow-tooltip />
+ <el-table-column label="鎿嶄綔" width="150" align="center" fixed="right">
+ <template #default="{ row }">
+ <el-button link type="primary" size="small" @click="viewSnapshot(row)">
+ <el-icon><View /></el-icon>
+ 鏌ョ湅
+ </el-button>
+ <el-button link type="success" size="small" @click="downloadSnapshot(row)">
+ <el-icon><Download /></el-icon>
+ 涓嬭浇
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <div class="pagination-container">
+ <el-pagination
+ v-model:current-page="capturePage"
+ v-model:page-size="capturePageSize"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="filteredCaptures.length"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </el-card>
+ </el-col>
+
+ <!-- 鍙充晶锛氳繚瑙勫憡璀� -->
+ <el-col :span="8">
+ <el-card shadow="never" class="section-card">
+ <template #header>
+ <div class="card-header">
+ <span>杩濊琛屼负鍛婅</span>
+ <div class="header-actions">
+ <el-badge :value="unreadAlarmCount" :max="99" type="danger">
+ <el-switch v-model="alarmEnabled" inline-prompt active-text="鍛婅寮�" inactive-text="鍛婅鍏�" />
+ </el-badge>
+ </div>
+ </div>
+ </template>
+ <el-tabs v-model="alarmTab" type="border-card" style="height: 650px">
+ <el-tab-pane label="鍏ㄩ儴鍛婅" name="all">
+ <div>
+ <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
+ <el-timeline-item
+ v-for="(alarm, idx) in displayAlarms"
+ :key="idx"
+ :type="getAlarmType(alarm.type)"
+ :timestamp="alarm.time"
+ :hollow="alarm.handled"
+ >
+ <div class="alarm-item" :class="{ handled: alarm.handled }">
+ <div class="alarm-header">
+ <el-tag :type="getAlarmTagType(alarm.type)" size="small">{{ alarm.typeText }}</el-tag>
+ <span class="alarm-area">{{ alarm.areaName }}</span>
+ </div>
+ <div class="alarm-content">
+ {{ alarm.description }}
+ </div>
+ <div class="alarm-footer">
+ <span class="alarm-camera">鎽勫儚澶�: {{ alarm.cameraName }}</span>
+ <el-button
+ v-if="!alarm.handled"
+ link
+ type="primary"
+ size="small"
+ @click="handleAlarm(alarm)"
+ >
+ 澶勭悊
+ </el-button>
+ <span v-else class="handled-text">宸插鐞�</span>
+ </div>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="寮洪棷鍛婅" name="intrusion">
+ <div>
+ <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
+ <el-timeline-item
+ v-for="(alarm, idx) in intrusionAlarms"
+ :key="idx"
+ type="danger"
+ :timestamp="alarm.time"
+ >
+ <div class="alarm-item">
+ <div class="alarm-header">
+ <el-tag type="danger" size="small">寮洪棷鍛婅</el-tag>
+ <span class="alarm-area">{{ alarm.areaName }}</span>
+ </div>
+ <div class="alarm-content">{{ alarm.description }}</div>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="灏鹃殢鍛婅" name="tailgating">
+ <div>
+ <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
+ <el-timeline-item
+ v-for="(alarm, idx) in tailgatingAlarms"
+ :key="idx"
+ type="warning"
+ :timestamp="alarm.time"
+ >
+ <div class="alarm-item">
+ <div class="alarm-header">
+ <el-tag type="warning" size="small">灏鹃殢鍛婅</el-tag>
+ <span class="alarm-area">{{ alarm.areaName }}</span>
+ </div>
+ <div class="alarm-content">{{ alarm.description }}</div>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="澶氫汉閫氳" name="multiple">
+ <div>
+ <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px">
+ <el-timeline-item
+ v-for="(alarm, idx) in multipleAlarms"
+ :key="idx"
+ type="warning"
+ :timestamp="alarm.time"
+ >
+ <div class="alarm-item">
+ <div class="alarm-header">
+ <el-tag type="warning" size="small">澶氫汉閫氳</el-tag>
+ <span class="alarm-area">{{ alarm.areaName }}</span>
+ </div>
+ <div class="alarm-content">{{ alarm.description }}</div>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 瀹炴椂鐢婚潰瀵硅瘽妗� -->
+ <el-dialog v-model="realtimeVisible" :title="`瀹炴椂鐩戞帶 - ${currentCamera.name}`" width="800px">
+ <div class="video-container">
+ <div class="video-placeholder">
+ <el-icon :size="80" color="#909399"><VideoCameraFilled /></el-icon>
+ <div class="video-info">
+ <p>鎽勫儚澶�: {{ currentCamera.name }}</p>
+ <p>浣嶇疆: {{ currentCamera.areaName }}</p>
+ <p>鐘舵��: <el-tag :type="currentCamera.status === 'online' ? 'success' : 'danger'" size="small">{{ currentCamera.status === 'online' ? '鍦ㄧ嚎' : '绂荤嚎' }}</el-tag></p>
+ <p class="tip">锛堝疄闄呯幆澧冨皢鏄剧ず瀹炴椂瑙嗛娴侊級</p>
+ </div>
+ </div>
+ </div>
+ <template #footer>
+ <el-button @click="realtimeVisible = false">鍏抽棴</el-button>
+ <el-button type="primary" @click="captureSnapshot">鎵嬪姩鎶撴媿</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 蹇収鏌ョ湅瀵硅瘽妗� -->
+ <el-dialog v-model="snapshotVisible" title="鎶撴媿蹇収璇︽儏" width="800px" :close-on-click-modal="false">
+ <div class="snapshot-dialog-body">
+ <el-row :gutter="20">
+ <el-col :span="14">
+ <div class="snapshot-preview">
+ <div class="snapshot-placeholder">
+ <el-icon :size="80" color="#909399"><Picture /></el-icon>
+ <p>鎶撴媿鍥剧墖棰勮</p>
+ <p class="tip">锛堝疄闄呯幆澧冨皢鏄剧ず楂樻竻鎶撴媿鍥剧墖锛�</p>
+ <div class="snapshot-tools">
+ <el-button-group>
+ <el-button size="small">
+ <el-icon><ZoomIn /></el-icon>
+ 鏀惧ぇ
+ </el-button>
+ <el-button size="small">
+ <el-icon><ZoomOut /></el-icon>
+ 缂╁皬
+ </el-button>
+ <el-button size="small">
+ <el-icon><RefreshRight /></el-icon>
+ 鏃嬭浆
+ </el-button>
+ </el-button-group>
+ </div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="10">
+ <el-descriptions :column="1" border size="small">
+ <el-descriptions-item label="鎶撴媿鏃堕棿">
+ <el-icon><Clock /></el-icon>
+ {{ currentSnapshot.time }}
+ </el-descriptions-item>
+ <el-descriptions-item label="宸ュ彿">
+ <el-icon><User /></el-icon>
+ {{ currentSnapshot.personId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="閮ㄩ棬">
+ {{ currentSnapshot.department }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵�灞炲尯鍩�">
+ <el-icon><Location /></el-icon>
+ {{ currentSnapshot.areaName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎽勫儚澶�">
+ <el-icon><VideoCameraFilled /></el-icon>
+ {{ currentSnapshot.cameraName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浜嬩欢绫诲瀷">
+ <el-tag :type="currentSnapshot.eventType === 'entry' ? 'success' : 'warning'" size="small">
+ {{ currentSnapshot.eventType === 'entry' ? '杩涘叆' : '绂诲紑' }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="浜鸿劯鍖归厤搴�">
+ <el-progress
+ :percentage="currentSnapshot.faceMatch"
+ :color="currentSnapshot.faceMatch >= 90 ? '#67c23a' : '#e6a23c'"
+ :stroke-width="12"
+ >
+ <span style="font-size: 12px">{{ currentSnapshot.faceMatch }}%</span>
+ </el-progress>
+ </el-descriptions-item>
+ <el-descriptions-item label="浣撴俯妫�娴�">
+ <span :style="{ color: currentSnapshot.temperature > 37.3 ? '#f56c6c' : '#67c23a' }">
+ {{ currentSnapshot.temperature }}掳C
+ </span>
+ <el-tag v-if="currentSnapshot.temperature > 37.3" type="danger" size="small" style="margin-left: 8px">
+ 寮傚父
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙g僵浣╂埓">
+ <el-tag :type="currentSnapshot.maskWearing ? 'success' : 'warning'" size="small">
+ {{ currentSnapshot.maskWearing ? '宸蹭僵鎴�' : '鏈僵鎴�' }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹夊叏甯�">
+ <el-tag :type="currentSnapshot.helmetWearing ? 'success' : 'danger'" size="small">
+ {{ currentSnapshot.helmetWearing ? '宸蹭僵鎴�' : '鏈僵鎴�' }}
+ </el-tag>
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-col>
+ </el-row>
+
+ <div class="snapshot-notes">
+ <el-divider content-position="left">澶囨敞淇℃伅</el-divider>
+ <el-input
+ v-model="currentSnapshot.notes"
+ type="textarea"
+ :rows="3"
+ placeholder="娣诲姞澶囨敞淇℃伅..."
+ />
+ </div>
+
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer-custom">
+ <div>
+ <el-button size="small" @click="printSnapshot">
+ <el-icon><Printer /></el-icon>
+ 鎵撳嵃
+ </el-button>
+ </div>
+ <div>
+ <el-button @click="snapshotVisible = false">鍏抽棴</el-button>
+ <el-button type="success" @click="downloadSnapshot(currentSnapshot)">
+ <el-icon><Download /></el-icon>
+ 涓嬭浇鍥剧墖
+ </el-button>
+ <el-button type="primary" @click="saveNotes">淇濆瓨澶囨敞</el-button>
+ </div>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
+import {
+ VideoCameraFilled, Picture, Search, Download, View,
+ Clock, User, Location, ZoomIn, ZoomOut, RefreshRight, Printer
+} from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+
+// 鍖哄煙鏁版嵁
+const areas = ref([
+ { id: "A01", name: "鐓ゅ満鍏ュ彛" },
+ { id: "A02", name: "娲楃叅杞﹂棿" },
+ { id: "A03", name: "鍗遍櫓鍝佸簱" },
+ { id: "A04", name: "涓帶瀹�" },
+ { id: "A05", name: "閰嶇數瀹�" },
+]);
+
+// 鎽勫儚澶存暟鎹�
+const cameras = ref([
+ { id: "C001", name: "鐓ゅ満鍏ュ彛涓滈棬", areaId: "A01", areaName: "鐓ゅ満鍏ュ彛", status: "online", aiEnabled: true, doorLinked: true },
+ { id: "C002", name: "鐓ゅ満鍏ュ彛瑗块棬", areaId: "A01", areaName: "鐓ゅ満鍏ュ彛", status: "online", aiEnabled: true, doorLinked: true },
+ { id: "C003", name: "娲楃叅杞﹂棿涓婚�氶亾", areaId: "A02", areaName: "娲楃叅杞﹂棿", status: "online", aiEnabled: true, doorLinked: true },
+ { id: "C004", name: "娲楃叅杞﹂棿涓滀晶", areaId: "A02", areaName: "娲楃叅杞﹂棿", status: "online", aiEnabled: false, doorLinked: false },
+ { id: "C005", name: "鍗遍櫓鍝佸簱澶ч棬", areaId: "A03", areaName: "鍗遍櫓鍝佸簱", status: "online", aiEnabled: true, doorLinked: true },
+ { id: "C006", name: "鍗遍櫓鍝佸簱鍐呴儴", areaId: "A03", areaName: "鍗遍櫓鍝佸簱", status: "offline", aiEnabled: true, doorLinked: false },
+ { id: "C007", name: "涓帶瀹ゅ叆鍙�", areaId: "A04", areaName: "涓帶瀹�", status: "online", aiEnabled: true, doorLinked: true },
+ { id: "C008", name: "閰嶇數瀹ら棬绂�", areaId: "A05", areaName: "閰嶇數瀹�", status: "online", aiEnabled: true, doorLinked: true },
+]);
+
+const selectedArea = ref("all");
+const cameraStatus = ref("all");
+const filteredCameras = ref([]);
+
+function filterCameras() {
+ let result = cameras.value;
+ if (selectedArea.value !== "all") {
+ result = result.filter(c => c.areaId === selectedArea.value);
+ }
+ if (cameraStatus.value !== "all") {
+ result = result.filter(c => c.status === cameraStatus.value);
+ }
+ filteredCameras.value = result;
+}
+
+// 闂ㄧ鎶撴媿璁板綍
+const captureDate = ref([new Date(), new Date()]);
+const captureSearch = ref("");
+const captureEventType = ref("");
+const capturePage = ref(1);
+const capturePageSize = ref(10);
+const selectedCaptures = ref([]);
+
+// 鐢熸垚鏇村妯℃嫙鏁版嵁
+const generateCaptureData = () => {
+ const departments = ["鐢熶骇涓�闃�", "鏈虹數鐝�", "瀹夌洃绉�", "璋冨害瀹�", "鍖栭獙瀹�", "杩愯緭闃�", "缁翠慨鐝�", "浠撳偍绉�"];
+ const areaList = ["鐓ゅ満鍏ュ彛", "娲楃叅杞﹂棿", "鍗遍櫓鍝佸簱", "涓帶瀹�", "閰嶇數瀹�"];
+ const cameraList = [
+ "鐓ゅ満鍏ュ彛涓滈棬", "鐓ゅ満鍏ュ彛瑗块棬", "娲楃叅杞﹂棿涓婚�氶亾", "娲楃叅杞﹂棿涓滀晶",
+ "鍗遍櫓鍝佸簱澶ч棬", "鍗遍櫓鍝佸簱鍐呴儴", "涓帶瀹ゅ叆鍙�", "閰嶇數瀹ら棬绂�"
+ ];
+
+ const data = [];
+ const now = new Date();
+
+ for (let i = 0; i < 86; i++) {
+ const randomMinutes = Math.floor(Math.random() * 1440); // 24灏忔椂鍐�
+ const captureTime = new Date(now.getTime() - randomMinutes * 60 * 1000);
+ const hh = String(captureTime.getHours()).padStart(2, "0");
+ const mm = String(captureTime.getMinutes()).padStart(2, "0");
+ const ss = String(captureTime.getSeconds()).padStart(2, "0");
+
+ const area = areaList[Math.floor(Math.random() * areaList.length)];
+ const eventType = Math.random() > 0.5 ? "entry" : "exit";
+ const faceMatch = Math.floor(Math.random() * 15) + 85; // 85-100
+ const temperature = (36.0 + Math.random() * 1.5).toFixed(1);
+
+ data.push({
+ id: i + 1,
+ time: `2025-10-28 ${hh}:${mm}:${ss}`,
+ personId: `EMP${String(1001 + i).padStart(4, '0')}`,
+ department: departments[Math.floor(Math.random() * departments.length)],
+ areaName: area,
+ cameraName: cameraList[Math.floor(Math.random() * cameraList.length)],
+ eventType: eventType,
+ faceMatch: faceMatch,
+ temperature: parseFloat(temperature),
+ maskWearing: Math.random() > 0.1,
+ helmetWearing: Math.random() > 0.15,
+ notes: ""
+ });
+ }
+
+ return data.sort((a, b) => b.time.localeCompare(a.time));
+};
+
+const captures = ref(generateCaptureData());
+
+// 缁熻淇℃伅
+const captureStats = computed(() => {
+ const today = captures.value.length;
+ const entry = captures.value.filter(c => c.eventType === 'entry').length;
+ const exit = captures.value.filter(c => c.eventType === 'exit').length;
+ const lowMatch = captures.value.filter(c => c.faceMatch < 90).length;
+
+ return { today, entry, exit, lowMatch };
+});
+
+// 杩囨护鎶撴媿璁板綍
+const filteredCaptures = computed(() => {
+ let result = captures.value;
+
+ // 鎸夊叧閿瘝鎼滅储
+ if (captureSearch.value) {
+ const keyword = captureSearch.value.toLowerCase();
+ result = result.filter(c =>
+ c.personId.toLowerCase().includes(keyword) ||
+ c.areaName.toLowerCase().includes(keyword)
+ );
+ }
+
+ // 鎸変簨浠剁被鍨嬭繃婊�
+ if (captureEventType.value) {
+ result = result.filter(c => c.eventType === captureEventType.value);
+ }
+
+ return result;
+});
+
+// 鍒嗛〉鏁版嵁
+const paginatedCaptures = computed(() => {
+ const start = (capturePage.value - 1) * capturePageSize.value;
+ const end = start + capturePageSize.value;
+ return filteredCaptures.value.slice(start, end);
+});
+
+// 鍒嗛〉绱㈠紩鏂规硶
+const indexMethod = (index) => {
+ return (capturePage.value - 1) * capturePageSize.value + index + 1;
+};
+
+function loadCaptures() {
+ ElMessage.success("宸插姞杞介�夊畾鏃ユ湡鑼冨洿鐨勬姄鎷嶈褰�");
+}
+
+function handleSizeChange(val) {
+ capturePageSize.value = val;
+ capturePage.value = 1;
+}
+
+function handleCurrentChange(val) {
+ capturePage.value = val;
+}
+
+function handleCaptureSelection(selection) {
+ selectedCaptures.value = selection;
+}
+
+function getFaceMatchType(faceMatch) {
+ if (faceMatch >= 95) return 'success';
+ if (faceMatch >= 90) return 'info';
+ if (faceMatch >= 85) return 'warning';
+ return 'danger';
+}
+
+// 瀵煎嚭鎶撴媿璁板綍
+function exportCaptures() {
+ if (filteredCaptures.value.length === 0) {
+ ElMessage.warning("娌℃湁鍙鍑虹殑璁板綍");
+ return;
+ }
+ ElMessage.success(`姝e湪瀵煎嚭 ${filteredCaptures.value.length} 鏉℃姄鎷嶈褰�...`);
+ // 瀹為檯鐜涓繖閲屼細璋冪敤瀵煎嚭鎺ュ彛
+}
+
+// 涓嬭浇蹇収
+function downloadSnapshot(capture) {
+ ElMessage.success(`姝e湪涓嬭浇 ${capture.personId} 鐨勬姄鎷嶅浘鐗�...`);
+ // 瀹為檯鐜涓繖閲屼細涓嬭浇鍥剧墖鏂囦欢
+}
+
+// 鎵撳嵃蹇収
+function printSnapshot() {
+ ElMessage.info("姝e湪鍑嗗鎵撳嵃...");
+ // 瀹為檯鐜涓繖閲屼細璋冪敤鎵撳嵃鍔熻兘
+}
+
+// 淇濆瓨澶囨敞
+function saveNotes() {
+ ElMessage.success("澶囨敞淇℃伅宸蹭繚瀛�");
+ snapshotVisible.value = false;
+}
+
+// 鍛婅鏁版嵁
+const alarmEnabled = ref(true);
+const alarmTab = ref("all");
+const alarms = ref([
+ {
+ id: 1,
+ time: "09:25:15",
+ type: "intrusion",
+ typeText: "寮洪棷鍛婅",
+ areaName: "鍗遍櫓鍝佸簱",
+ cameraName: "鍗遍櫓鍝佸簱澶ч棬",
+ description: "妫�娴嬪埌鏈巿鏉冧汉鍛樺己琛岄棷鍏ワ紝鏈埛鍗$洿鎺ュ紑闂�",
+ handled: false
+ },
+ {
+ id: 2,
+ time: "09:18:42",
+ type: "tailgating",
+ typeText: "灏鹃殢鍛婅",
+ areaName: "涓帶瀹�",
+ cameraName: "涓帶瀹ゅ叆鍙�",
+ description: "妫�娴嬪埌灏鹃殢琛屼负锛屼竴浜哄埛鍗″悗涓や汉杩涘叆",
+ handled: false
+ },
+ {
+ id: 3,
+ time: "09:10:28",
+ type: "multiple",
+ typeText: "澶氫汉閫氳",
+ areaName: "鐓ゅ満鍏ュ彛",
+ cameraName: "鐓ゅ満鍏ュ彛涓滈棬",
+ description: "妫�娴嬪埌3浜哄悓鏃堕�氳繃鍗曚汉闂ㄧ閫氶亾",
+ handled: true
+ },
+]);
+
+const unreadAlarmCount = computed(() => alarms.value.filter(a => !a.handled).length);
+
+const displayAlarms = computed(() => {
+ if (alarmTab.value === "all") return alarms.value;
+ return alarms.value.filter(a => a.type === alarmTab.value);
+});
+
+const intrusionAlarms = computed(() => alarms.value.filter(a => a.type === "intrusion"));
+const tailgatingAlarms = computed(() => alarms.value.filter(a => a.type === "tailgating"));
+const multipleAlarms = computed(() => alarms.value.filter(a => a.type === "multiple"));
+
+function getAlarmType(type) {
+ const typeMap = { intrusion: "danger", tailgating: "warning", multiple: "warning" };
+ return typeMap[type] || "info";
+}
+
+function getAlarmTagType(type) {
+ const typeMap = { intrusion: "danger", tailgating: "warning", multiple: "warning" };
+ return typeMap[type] || "info";
+}
+
+function handleAlarm(alarm) {
+ alarm.handled = true;
+ ElMessage.success("鍛婅宸叉爣璁颁负宸插鐞�");
+}
+
+// 瀹炴椂鐢婚潰
+const realtimeVisible = ref(false);
+const currentCamera = ref({});
+
+function viewRealtime(camera) {
+ currentCamera.value = camera;
+ realtimeVisible.value = true;
+}
+
+function captureSnapshot() {
+ ElMessage.success("鎶撴媿鎴愬姛锛屽凡淇濆瓨鑷虫姄鎷嶈褰�");
+}
+
+// 鏌ョ湅鎶撴媿璁板綍璇︽儏
+const snapshotVisible = ref(false);
+const currentSnapshot = ref({});
+
+function viewSnapshot(capture) {
+ currentSnapshot.value = capture;
+ snapshotVisible.value = true;
+}
+
+function viewCaptures(camera) {
+ ElMessage.info(`鏌ョ湅 ${camera.name} 鐨勫巻鍙叉姄鎷嶈褰昤);
+}
+
+// 妯℃嫙鍛婅鎺ㄩ��
+let alarmTimer = null;
+const alarmTemplates = [
+ { type: "intrusion", typeText: "寮洪棷鍛婅", areas: ["鍗遍櫓鍝佸簱", "閰嶇數瀹�", "涓帶瀹�"] },
+ { type: "tailgating", typeText: "灏鹃殢鍛婅", areas: ["涓帶瀹�", "閰嶇數瀹�", "鍗遍櫓鍝佸簱"] },
+ { type: "multiple", typeText: "澶氫汉閫氳", areas: ["鐓ゅ満鍏ュ彛", "娲楃叅杞﹂棿"] },
+];
+
+function pushMockAlarm() {
+ if (!alarmEnabled.value) return;
+
+ const template = alarmTemplates[Math.floor(Math.random() * alarmTemplates.length)];
+ const area = template.areas[Math.floor(Math.random() * template.areas.length)];
+ const camera = cameras.value.find(c => c.areaName === area);
+
+ const now = new Date();
+ const hh = String(now.getHours()).padStart(2, "0");
+ const mm = String(now.getMinutes()).padStart(2, "0");
+ const ss = String(now.getSeconds()).padStart(2, "0");
+
+ let description = "";
+ if (template.type === "intrusion") {
+ description = "妫�娴嬪埌鏈巿鏉冧汉鍛樺己琛岄棷鍏ワ紝鏈埛鍗$洿鎺ュ紑闂�";
+ } else if (template.type === "tailgating") {
+ description = "妫�娴嬪埌灏鹃殢琛屼负锛屼竴浜哄埛鍗″悗涓や汉杩涘叆";
+ } else if (template.type === "multiple") {
+ const count = Math.floor(Math.random() * 3) + 2;
+ description = `妫�娴嬪埌${count}浜哄悓鏃堕�氳繃鍗曚汉闂ㄧ閫氶亾`;
+ }
+
+ alarms.value.unshift({
+ id: Date.now(),
+ time: `${hh}:${mm}:${ss}`,
+ type: template.type,
+ typeText: template.typeText,
+ areaName: area,
+ cameraName: camera ? camera.name : area + "鐩戞帶",
+ description: description,
+ handled: false
+ });
+
+ // 闄愬埗鍛婅鍒楄〃闀垮害
+ if (alarms.value.length > 50) {
+ alarms.value = alarms.value.slice(0, 50);
+ }
+}
+
+onMounted(() => {
+ filterCameras();
+ // 姣忛殧8绉掓ā鎷熶竴娆″憡璀�
+ alarmTimer = setInterval(pushMockAlarm, 8000);
+});
+
+onBeforeUnmount(() => {
+ if (alarmTimer) {
+ clearInterval(alarmTimer);
+ }
+});
+</script>
+
+<style scoped lang="scss">
+.section-card {
+ margin-bottom: 16px;
+}
+
+.card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.header-actions {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.alarm-item {
+ padding: 8px;
+ background: #f5f7fa;
+ border-radius: 4px;
+
+ &.handled {
+ opacity: 0.6;
+ }
+}
+
+.alarm-header {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 6px;
+}
+
+.alarm-area {
+ font-weight: 600;
+ color: #303133;
+}
+
+.alarm-content {
+ color: #606266;
+ font-size: 14px;
+ margin-bottom: 6px;
+ line-height: 1.5;
+}
+
+.alarm-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 12px;
+ color: #909399;
+}
+
+.alarm-camera {
+ flex: 1;
+}
+
+.handled-text {
+ color: #67c23a;
+ font-size: 12px;
+}
+
+.video-container {
+ width: 100%;
+ height: 450px;
+ background: #000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.video-placeholder {
+ text-align: center;
+ color: #909399;
+}
+
+.video-info {
+ margin-top: 20px;
+
+ p {
+ margin: 8px 0;
+ font-size: 14px;
+ }
+
+ .tip {
+ color: #c0c4cc;
+ font-size: 12px;
+ margin-top: 16px;
+ }
+}
+
+.snapshot-placeholder {
+ margin-top: 20px;
+ height: 300px;
+ background: #f5f7fa;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ color: #909399;
+
+ p {
+ margin: 8px 0;
+ }
+
+ .tip {
+ color: #c0c4cc;
+ font-size: 12px;
+ }
+}
+
+:deep(.el-tabs--border-card) {
+ border: none;
+ box-shadow: none;
+}
+
+:deep(.el-tabs__content) {
+ padding: 10px;
+}
+
+// 鎶撴媿璁板綍缁熻
+.capture-stats {
+ margin-bottom: 16px;
+ padding: 16px;
+ background: #f5f7fa;
+ border-radius: 4px;
+}
+
+.stat-item {
+ text-align: center;
+ padding: 8px;
+ background: #fff;
+ border-radius: 4px;
+}
+
+.stat-label {
+ font-size: 13px;
+ color: #909399;
+ margin-bottom: 8px;
+}
+
+.stat-value {
+ font-size: 24px;
+ font-weight: bold;
+ color: #303133;
+}
+
+// 鍒嗛〉瀹瑰櫒
+.pagination-container {
+ margin-top: 16px;
+ display: flex;
+ justify-content: flex-end;
+}
+
+// 蹇収棰勮
+.snapshot-preview {
+ width: 100%;
+}
+
+.snapshot-placeholder {
+ width: 100%;
+ height: 420px;
+ background: #f5f7fa;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ border: 1px dashed #dcdfe6;
+ color: #909399;
+
+ p {
+ margin: 8px 0;
+ font-size: 14px;
+ }
+
+ .tip {
+ color: #c0c4cc;
+ font-size: 12px;
+ }
+}
+
+.snapshot-tools {
+ margin-top: 20px;
+}
+
+.snapshot-notes {
+ margin-top: 20px;
+
+ :deep(.el-divider__text) {
+ font-weight: 600;
+ color: #606266;
+ }
+}
+
+.dialog-footer-custom {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ > div {
+ display: flex;
+ gap: 8px;
+ }
+}
+
+// 鎻忚堪鍒楄〃鏍峰紡浼樺寲
+:deep(.el-descriptions__label) {
+ width: 100px;
+}
+
+:deep(.el-descriptions__content) {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+</style>
+
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
new file mode 100644
index 0000000..3251f0c
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/approveListConstants.js
@@ -0,0 +1,609 @@
+import { createEmptyNode, formatDisplayTime, mapNodesFromApi, mapSignModeFromApi, mapSignModeToApi, normalizeFlowNodes, nodeSignModeLabel } from "../approve-template/approveTemplateConstants.js";
+import { buildFormPayloadFromFields, parseFormConfigToData } from "../approve-template/formConfigUtils.js";
+import { isDynamicOptionSource, resolveSelectDisplayLabel } from "../approve-template/selectOptionSource.js";
+import { appendDotNotationQuery, buildApprovalInstanceSearchDto } from "../approve-shared/approvalInstanceListSearch.js";
+
+/** 瀹℃壒绫诲瀷锛堜笌鍚庣瀛楁 approvalType 瀵归綈锛屽悗鏈熷彲鍚屾锛� */
+export const APPROVAL_TYPE_OPTIONS = [
+ { value: "cost_reimburse", label: "璐圭敤鎶ラ攢鐢宠", cellBg: "#e8f8ef", cellColor: "#1a7f4b" },
+ { value: "travel_reimburse", label: "宸梾鎶ラ攢鐢宠", cellBg: "#f0f2f5", cellColor: "#606266" },
+ { value: "overtime", label: "鍔犵彮鐢宠", cellBg: "#fdf3e8", cellColor: "#c45c26" },
+ { value: "leave", label: "璇峰亣鐢宠", cellBg: "#fce8f0", cellColor: "#b84d7a" },
+ { value: "work_handover", label: "宸ヤ綔浜ゆ帴鐢宠", cellBg: "#f0e8fc", cellColor: "#6b4d9e" },
+ { value: "regular", label: "杞鐢宠", cellBg: "#e8f4fc", cellColor: "#2b6cb0" },
+ { value: "resign", label: "绂昏亴鐢宠", cellBg: "#ffffff", cellColor: "#303133", border: "1px solid #e4e7ed" },
+ { value: "transfer", label: "璋冨矖鐢宠", cellBg: "#ffffff", cellColor: "#303133", border: "1px solid #e4e7ed" },
+ { value: "out_office", label: "鍏嚭鐢宠", cellBg: "#e8f4ff", cellColor: "#409eff" },
+ { value: "business_trip", label: "鍑哄樊鐢宠", cellBg: "#fdf6ec", cellColor: "#e6a23c" },
+ { value: "procurement", label: "閲囪喘瀹℃壒", cellBg: "#f4f4f5", cellColor: "#909399" },
+ { value: "quotation", label: "鎶ヤ环瀹℃壒", cellBg: "#f4ecfc", cellColor: "#9b59b6" },
+ { value: "shipment", label: "鍙戣揣瀹℃壒", cellBg: "#e8faf6", cellColor: "#1abc9c" },
+ { value: "enterprise_news", label: "浼佷笟鏂伴椈", cellBg: "#ecf5ff", cellColor: "#409eff" },
+];
+
+/** 鍒楄〃鏌ヨ锛氬鎵圭姸鎬侊紙涓庡悗绔� status 鏋氫妇涓�鑷达級 */
+export const APPROVAL_STATUS_SEARCH_OPTIONS = [
+ { value: "DRAFT", label: "鑽夌" },
+ { value: "PENDING", label: "寰呭鎵�" },
+ { value: "APPROVED", label: "宸查�氳繃" },
+ { value: "REJECTED", label: "宸查┏鍥�" },
+];
+
+/**
+ * 瀹℃壒鐘舵�佸睍绀猴紙涓庡悗绔� status 鏋氫妇涓�鑷达級
+ * DRAFT鈫掕崏绋� PENDING鈫掑緟瀹℃壒/杩涜涓� APPROVED鈫掑凡閫氳繃/宸插畬鎴� REJECTED鈫掑凡椹冲洖
+ */
+export const APPROVAL_STATUS_OPTIONS = [
+ { value: "draft", api: "DRAFT", label: "鑽夌" },
+ { value: "pending", api: "PENDING", label: "寰呭鎵�" },
+ { value: "approved", api: "APPROVED", label: "宸查�氳繃" },
+ { value: "rejected", api: "REJECTED", label: "宸查┏鍥�" },
+ { value: "cancelled", api: "CANCELLED", label: "宸叉挙閿�" },
+];
+
+/** 鏁板瓧鐘舵�佺爜锛堥儴鍒嗗悗绔敤 0/1/2锛� */
+const STATUS_NUMERIC_MAP = {
+ 0: "pending",
+ 1: "approved",
+ 2: "rejected",
+ 3: "cancelled",
+ 4: "cancelled",
+};
+
+/** 鍚庣 status / 椤甸潰 approvalStatus 鈫� 缁熶竴椤甸潰 key锛坧ending | approved | rejected | cancelled锛� */
+export function normalizeApprovalStatusKey(v) {
+ if (v == null || v === "") return "pending";
+ if (typeof v === "number" || (typeof v === "string" && /^\d+$/.test(v.trim()))) {
+ const numKey = STATUS_NUMERIC_MAP[Number(v)];
+ if (numKey) return numKey;
+ }
+ const s = String(v).trim();
+ if (!s) return "pending";
+ const upper = s.toUpperCase();
+ if (upper === "DRAFT") return "draft";
+ if (upper === "PUBLISHED") return "approved";
+ if (upper === "OFFLINE") return "cancelled";
+ if (upper === "APPROVED" || upper === "APPROVE" || upper === "PASS" || upper === "AGREE") {
+ return "approved";
+ }
+ if (upper === "REJECTED" || upper === "REJECT" || upper === "REFUSE" || upper === "REFUSED" || upper === "DENIED") {
+ return "rejected";
+ }
+ if (upper === "CANCELLED" || upper === "CANCEL" || upper === "REVOKED") return "cancelled";
+ if (upper === "PENDING" || upper === "IN_PROGRESS" || upper === "PROCESSING" || upper === "RUNNING" || upper === "WAIT" || upper === "WAITING") {
+ return "pending";
+ }
+ if (s.includes("鑽夌")) return "draft";
+ if (s.includes("椹冲洖") || s.includes("鎷掔粷")) return "rejected";
+ if (s.includes("涓嬬嚎")) return "cancelled";
+ if (s.includes("鎾ら攢")) return "cancelled";
+ if (s.includes("鍙戝竷") || s.includes("閫氳繃") || s.includes("瀹屾垚")) return "approved";
+ if (s.includes("寰呭") || s.includes("杩涜涓�") || s.includes("瀹℃壒涓�")) return "pending";
+ const lower = s.toLowerCase();
+ if (["draft", "pending", "approved", "rejected", "cancelled"].includes(lower)) return lower;
+ return "pending";
+}
+
+/** 浠庡垪琛�/璇︽儏琛岃В鏋愬悗绔師濮嬬姸鎬侊紙鍏煎澶氬瓧娈靛懡鍚嶏級 */
+export function resolveInstanceStatusRaw(row) {
+ if (!row || typeof row !== "object") return "";
+ const candidates = [
+ row.status,
+ row.statusRaw,
+ row.approvalStatus,
+ row.statusName,
+ row.statusLabel,
+ row.approvalStatusName,
+ row.statusDesc,
+ row.instanceStatus,
+ row.approvalInstanceStatus,
+ row.approveStatus,
+ row.auditStatus,
+ row.approvalInstance?.status,
+ row.approvalInstanceVo?.status,
+ ];
+ for (const c of candidates) {
+ if (c != null && c !== "") return c;
+ }
+ const tasks = row.tasks;
+ if (Array.isArray(tasks) && tasks.length) {
+ const rejected = tasks.some(t => normalizeApprovalStatusKey(t?.status ?? t?.taskStatus) === "rejected");
+ if (rejected) return "REJECTED";
+ const allApproved = tasks.every(t => normalizeApprovalStatusKey(t?.status ?? t?.taskStatus) === "approved");
+ if (allApproved) return "APPROVED";
+ }
+ return "";
+}
+
+/** 鎻愪氦寮圭獥锛氭ā鏉垮崱鐗囷紙鏉ヨ嚜鍚庣鍒楄〃锛� */
+export function mapSubmitTemplateCard(row) {
+ const cfg = parseFormConfigToData(row?.formConfig);
+ return {
+ id: row?.id,
+ key: String(row?.id ?? ""),
+ businessType: row?.businessType ?? cfg.approvalType ?? row?.approvalType ?? "",
+ approvalType: cfg.approvalType || row?.approvalType || "",
+ label: row?.templateName || "鈥�",
+ summaryPlaceholder: (row?.description || "").trim() || cfg.summaryPlaceholder || "鐐瑰嚮濉啓骞舵彁浜�",
+ };
+}
+
+export function matchBusinessTypeValue(a, b) {
+ if (a == null || a === "" || b == null || b === "") return false;
+ return a === b || a === Number(b) || Number(a) === b || String(a) === String(b);
+}
+
+/** 瀹℃壒璁板綍 approveAction 鈫� 椤甸潰 result */
+export function mapRecordResultFromApi(action) {
+ const s = String(action || "").toUpperCase();
+ if (s === "APPROVED" || s === "APPROVE" || s === "PASS") return "approved";
+ if (s === "REJECTED" || s === "REJECT" || s === "REFUSE") return "rejected";
+ return "pending";
+}
+
+/** 鍚庣 records 鈫� 鏃堕棿绾垮睍绀虹粨鏋� */
+export function mapRecordsFromApi(records) {
+ const list = Array.isArray(records) ? records : [];
+ return list.map(r => ({
+ id: r.id,
+ operatorName: r.approverName || r.operatorName || r.createUserName || "",
+ result: mapRecordResultFromApi(r.approveAction ?? r.action ?? r.status),
+ opinion: r.approveComment || r.comment || r.opinion || "",
+ time: formatDisplayTime(r.approveTime || r.createTime || r.time || ""),
+ raw: r,
+ }));
+}
+
+export function mapTaskStatusLabel(status) {
+ return approvalStatusLabel(status);
+}
+
+export function mapTaskStatusTagType(status) {
+ return approvalStatusTagType(status);
+}
+
+/** 鍚庣 tasks 鈫� 椤甸潰 flowNodes锛堟寜 levelNo 鍒嗙粍锛屼緵娴佺▼缂栬緫/灞曠ず锛� */
+export function mapTasksToFlowNodes(tasks) {
+ const list = Array.isArray(tasks) ? tasks : [];
+ if (!list.length) return [];
+ const byLevel = new Map();
+ list.forEach(t => {
+ const level = Number(t.levelNo ?? t.taskLevel ?? t.nodeOrder ?? 1);
+ if (!byLevel.has(level)) {
+ byLevel.set(level, {
+ id: t.nodeId,
+ templateId: t.templateId,
+ nodeOrder: level,
+ signMode: mapSignModeFromApi(t.approveType),
+ approvers: [],
+ tasks: [],
+ });
+ }
+ const node = byLevel.get(level);
+ node.approvers.push({
+ id: t.id,
+ nodeId: t.nodeId,
+ templateId: t.templateId,
+ approverId: t.approverId,
+ approverName: t.approverName || "",
+ status: t.status,
+ approveComment: t.approveComment,
+ approveTime: t.approveTime,
+ });
+ node.tasks.push(t);
+ if (t.approveType != null) {
+ node.signMode = mapSignModeFromApi(t.approveType);
+ }
+ });
+ return [...byLevel.entries()].sort(([a], [b]) => a - b).map(([, node]) => node);
+}
+
+/** 椤甸潰 flowNodes 鈫� 鍚庣 tasks */
+export function mapFlowNodesToTasks(flowNodes, { instanceId, templateId } = {}) {
+ const nodes = normalizeFlowNodes(flowNodes);
+ const tasks = [];
+ nodes.forEach(n => {
+ const levelNo = n.nodeOrder ?? 1;
+ const approveType = mapSignModeToApi(n.signMode);
+ n.approvers.forEach((a, idx) => {
+ const task = {
+ levelNo,
+ approveType,
+ approverId: a.approverId,
+ approverName: a.approverName || "",
+ sortNo: a.sortNo ?? idx + 1,
+ };
+ if (a.id != null) task.id = a.id;
+ if (a.nodeId != null) task.nodeId = a.nodeId;
+ if (a.templateId != null) task.templateId = a.templateId;
+ else if (templateId) task.templateId = templateId;
+ if (instanceId) task.instanceId = instanceId;
+ if (a.status != null) task.status = a.status;
+ tasks.push(task);
+ });
+ });
+ return tasks;
+}
+
+function guessFieldTypeFromValue(val) {
+ if (Array.isArray(val) && val.length === 2) return "datetimerange";
+ if (typeof val === "number") return "number";
+ if (typeof val === "string" && /^\d{4}-\d{2}-\d{2}$/.test(val)) return "date";
+ if (typeof val === "string" && val.length > 100) return "textarea";
+ return "text";
+}
+
+/**
+ * 鍗曞瓧娈靛睍绀哄�硷紙璇︽儏鍙銆佸垪琛ㄤ富琛級
+ * @param {object} [caches] 浜哄憳/閮ㄩ棬涓嬫媺缂撳瓨锛岀敤浜庤В鏋愩�屼汉鍛樺垪琛ㄣ�嶇被瀛楁涓哄鍚�
+ */
+export function formatFieldDisplayValue(field, val, caches) {
+ if (val == null || val === "" || (Array.isArray(val) && !val.length)) return "鈥�";
+ if (field?.type === "select" && isDynamicOptionSource(field.optionSource)) {
+ const label = resolveSelectDisplayLabel(field, val, caches || {});
+ if (label && label !== "鈥�") return label;
+ return String(val);
+ }
+ if (field?.type === "select" && field.options?.length) {
+ const hit = field.options.find(o => String(o.value) === String(val));
+ return hit?.label || String(val);
+ }
+ if (Array.isArray(val)) return val.join(" 鑷� ");
+ return String(val);
+}
+
+/**
+ * 浠庤鏁版嵁 / formConfig 瑙f瀽濉姤瀛楁瀹氫箟涓� formPayload锛堜笌鏂板鎻愪氦缁撴瀯涓�鑷达級
+ */
+export function resolveInstanceFormFields(row) {
+ const cfg = parseInstanceFormConfig(row?.formConfig);
+ let fields = (row?.formFieldDefs?.length ? row.formFieldDefs : cfg.fields) || [];
+ const formPayload = {
+ ...(fields.length ? buildFormPayloadFromFields(fields) : {}),
+ ...cfg.formPayload,
+ ...(row?.formPayload || {}),
+ };
+ if (!fields.length && Object.keys(formPayload).length) {
+ fields = Object.keys(formPayload)
+ .filter(k => k && k !== "summary")
+ .map(k => ({
+ key: k,
+ label: k,
+ type: guessFieldTypeFromValue(formPayload[k]),
+ required: false,
+ rows: 3,
+ min: 0,
+ precision: 0,
+ options: [],
+ }));
+ }
+ const templateSnapshot = {
+ label: row?.templateName || row?.title || "瀹℃壒",
+ approvalType: cfg.approvalType || row?.approvalType || "",
+ summaryPlaceholder: cfg.summaryPlaceholder || "",
+ templateId: row?.templateId,
+ fields,
+ };
+ return { fields, formPayload, templateSnapshot, formConfigData: cfg };
+}
+
+/** 瑙f瀽瀹炰緥 formConfig */
+export function parseInstanceFormConfig(formConfig) {
+ let raw = {};
+ if (formConfig) {
+ if (typeof formConfig === "object") raw = formConfig;
+ else {
+ try {
+ raw = JSON.parse(formConfig);
+ } catch {
+ raw = {};
+ }
+ }
+ }
+ const data = parseFormConfigToData(formConfig);
+ const payload = raw.formPayload;
+ return {
+ summaryPlaceholder: raw.summaryPlaceholder || data.summaryPlaceholder || "",
+ approvalType: raw.approvalType || "",
+ fields: data.fields || [],
+ formPayload: payload && typeof payload === "object" ? payload : {},
+ };
+}
+
+export function unwrapInstanceDetail(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") return {};
+ if (data.id != null || data.instanceNo) return data;
+ if (data.approvalInstanceVo) return data.approvalInstanceVo;
+ return data;
+}
+
+/** 濉姤鍐呭 + 妯℃澘瀛楁瀹氫箟 鈫� formConfig JSON */
+export function buildInstanceFormConfigJson(templateSnapshot, formPayload) {
+ const payload = formPayload || {};
+ return JSON.stringify({
+ summaryPlaceholder: templateSnapshot?.summaryPlaceholder || "",
+ approvalType: templateSnapshot?.approvalType || "",
+ fields: templateSnapshot?.fields || [],
+ formPayload: payload,
+ });
+}
+
+/** 缁勮淇濆瓨/鏇存柊瀹℃壒 DTO */
+export function buildInstanceDto({ submitForm, activeTemplate, userStore, flowNodes, existingRow }) {
+ const payload = submitForm?.formPayload || {};
+ const tpl = activeTemplate || {};
+ const title = String(payload.summary || payload.title || "").trim() || tpl.label || submitForm?.templateName || "瀹℃壒鐢宠";
+ const templateId = submitForm?.templateId || tpl.templateId;
+ const instanceId = existingRow?.id ?? submitForm?.instanceId;
+ const taskList = mapFlowNodesToTasks(flowNodes || submitForm?.flowNodes, {
+ instanceId,
+ templateId,
+ });
+ const isUpdate = Boolean(instanceId);
+
+ const dto = {
+ templateId,
+ templateName: submitForm?.templateName || tpl.label || "",
+ businessType: tpl.businessType ?? submitForm?.businessType ?? "",
+ title,
+ formConfig: buildInstanceFormConfigJson({ ...tpl, fields: tpl.fields || submitForm?.formFieldDefs }, payload),
+ tasks: taskList,
+ };
+
+ const attachments = (Array.isArray(submitForm?.storageBlobDTOs) && submitForm.storageBlobDTOs.length ? submitForm.storageBlobDTOs : null) || tpl.storageBlobDTOs;
+ if (attachments?.length) dto.storageBlobDTOs = attachments;
+
+ if (isUpdate) {
+ dto.id = existingRow?.id ?? submitForm?.instanceId;
+ dto.instanceNo = existingRow?.instanceNo ?? submitForm?.instanceNo ?? "";
+ dto.status = submitForm?.saveStatusApi || existingRow?.statusRaw || mapInstanceStatusToApi(existingRow?.approvalStatus) || "PENDING";
+ dto.currentLevel = existingRow?.currentLevel ?? submitForm?.currentLevel ?? 1;
+ dto.applicantId = existingRow?.applicantId ?? existingRow?.applicantNo;
+ dto.applicantName = existingRow?.applicantName || "";
+ } else {
+ dto.status = submitForm?.saveStatusApi || "PENDING";
+ dto.currentLevel = 1;
+ dto.applicantId = userStore?.id;
+ dto.applicantName = userStore?.nickName || userStore?.name || "";
+ }
+ return dto;
+}
+
+/** 鏍¢獙鎻愪氦瀹℃壒娴佺▼锛堜笌妯℃澘椤佃鍒欎竴鑷达級 */
+export function validateSubmitFlowNodes(flowNodes) {
+ const nodes = normalizeFlowNodes(flowNodes);
+ if (!nodes.length) return { ok: false, message: "璇疯嚦灏戦厤缃竴涓鎵硅妭鐐�" };
+ for (let i = 0; i < nodes.length; i++) {
+ if (!nodes[i].approvers.length) {
+ return { ok: false, message: `璇蜂负绗� ${i + 1} 涓妭鐐归�夋嫨鑷冲皯涓�鍚嶅鎵逛汉` };
+ }
+ }
+ return { ok: true, nodes };
+}
+
+/** 鍚庣 status 鈫� 椤甸潰 approvalStatus */
+export function mapInstanceStatusFromApi(status) {
+ return normalizeApprovalStatusKey(status);
+}
+
+/** 鍒楄〃/璇︽儏琛� 鈫� 椤甸潰 approvalStatus key */
+export function mapInstanceApprovalStatusFromRow(row) {
+ const raw = resolveInstanceStatusRaw(row);
+ return normalizeApprovalStatusKey(raw);
+}
+
+/** 椤甸潰 approvalStatus 鈫� 鍚庣 status */
+export function mapInstanceStatusToApi(approvalStatus) {
+ const key = normalizeApprovalStatusKey(approvalStatus);
+ const hit = APPROVAL_STATUS_OPTIONS.find(x => x.value === key);
+ return hit?.api || "PENDING";
+}
+
+export function unwrapInstancePage(res) {
+ const data = res?.data ?? res;
+ return {
+ records: Array.isArray(data?.records) ? data.records : [],
+ total: Number(data?.total ?? 0),
+ };
+}
+
+/** 鍒嗛〉鍒楄〃椤� 鈫� 琛ㄦ牸琛� */
+export function mapInstanceFromApi(row) {
+ if (!row) return {};
+ const statusRaw = resolveInstanceStatusRaw(row);
+ const approvalStatus = normalizeApprovalStatusKey(statusRaw);
+ const createTime = formatDisplayTime(row.createTime ?? row.applyTime ?? "");
+ const applyTime = formatDisplayTime(row.applyTime ?? "");
+ const finishTime = formatDisplayTime(row.finishTime ?? "");
+ const resolved = resolveInstanceFormFields(row);
+ const { fields, formPayload, templateSnapshot } = resolved;
+ const tasks = Array.isArray(row.tasks) ? row.tasks : [];
+ const flowNodes = tasks.length ? mapTasksToFlowNodes(tasks) : mapNodesFromApi(row.nodes || row.flowNodes);
+ const approvalRecords = mapRecordsFromApi(row.records);
+ return {
+ id: row.id,
+ bizId: row.instanceNo || String(row.id ?? ""),
+ instanceNo: row.instanceNo || "",
+ templateId: row.templateId,
+ templateName: row.templateName || "",
+ businessId: row.businessId,
+ businessType: row.businessType,
+ businessName: row.businessName || "",
+ applicantId: row.applicantId,
+ applicantNo: row.applicantId != null ? String(row.applicantId) : "",
+ applicantName: row.applicantName || "",
+ approvalType: row.approvalType || row.templateName || "",
+ unread: Boolean(row.isApprove) && approvalStatus === "pending",
+ isApprove: Boolean(row.isApprove),
+ approvalStatus,
+ statusRaw: statusRaw || row.status,
+ createTime,
+ applyTime: applyTime === "鈥�" ? "" : applyTime,
+ finishTime: finishTime === "鈥�" ? "" : finishTime,
+ title: row.title || "",
+ summary: row.title || row.templateName || "",
+ currentLevel: row.currentLevel,
+ formConfig: row.formConfig,
+ formPayload,
+ formFieldDefs: fields,
+ templateSnapshot,
+ tasks,
+ records: Array.isArray(row.records) ? row.records : [],
+ storageBlobVOList: row.storageBlobVOList || [],
+ storageBlobDTOs: row.storageBlobVOList || row.storageBlobDTOs || [],
+ flowNodes,
+ approvalFlowNodes: [],
+ currentNodeIndex: 0,
+ approvalRecords,
+ rejectReason: approvalRecords.find(r => r.result === "rejected")?.opinion || "",
+ };
+}
+
+/** 瀹℃壒鎿嶄綔锛氫笌鍚庣 status 鏋氫妇涓�鑷� */
+export const APPROVE_ACTION_APPROVED = "APPROVED";
+export const APPROVE_ACTION_REJECTED = "REJECTED";
+
+/** 椤甸潰鎿嶄綔 鈫� approveAction */
+export function mapApproveActionToApi(uiResult) {
+ return uiResult === "rejected" ? APPROVE_ACTION_REJECTED : APPROVE_ACTION_APPROVED;
+}
+
+/** 缁勮瀹℃壒鎻愪氦 DTO */
+export function buildApproveInstanceDto(row, uiResult, comment) {
+ const opinion = (comment || "").trim();
+ return {
+ id: row?.id,
+ approveAction: mapApproveActionToApi(uiResult),
+ approveComment: opinion || (uiResult === "approved" ? "鍚屾剰" : ""),
+ };
+}
+
+export function buildApprovalInstanceListParams({ page, searchForm, businessType, extraParams }) {
+ const dto = buildApprovalInstanceSearchDto(searchForm, extraParams);
+ const bizType = businessType ?? searchForm?.businessType;
+ if (bizType != null && bizType !== "") {
+ dto.businessType = bizType;
+ }
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ "page.current": page.current,
+ "page.size": page.size,
+ ...dto,
+ };
+ appendDotNotationQuery(params, "approvalInstanceDto", dto);
+ return params;
+}
+
+export function approvalTypeLabel(v) {
+ return APPROVAL_TYPE_OPTIONS.find(x => x.value === v)?.label || v || "鈥�";
+}
+
+export function approvalTypeStyle(v) {
+ const hit = APPROVAL_TYPE_OPTIONS.find(x => x.value === v);
+ if (!hit) return {};
+ return {
+ backgroundColor: hit.cellBg,
+ color: hit.cellColor,
+ border: hit.border || "none",
+ };
+}
+
+export function approvalStatusLabel(v) {
+ const key = normalizeApprovalStatusKey(v);
+ return APPROVAL_STATUS_OPTIONS.find(x => x.value === key)?.label || "鈥�";
+}
+
+/** 涓氬姟鐢宠椤电姸鎬佹枃妗堬細PENDING鈫掕繘琛屼腑 APPROVED鈫掑凡瀹屾垚 REJECTED鈫掑凡椹冲洖 */
+export function businessApprovalStatusLabel(v) {
+ const key = normalizeApprovalStatusKey(v);
+ if (key === "draft") return "鑽夌";
+ if (key === "pending") return "杩涜涓�";
+ if (key === "approved") return "宸插畬鎴�";
+ if (key === "rejected") return "宸查┏鍥�";
+ if (key === "cancelled") return "宸叉挙閿�";
+ return "鈥�";
+}
+
+/**
+ * 涓氬姟鐢宠椤垫槸鍚﹀厑璁镐慨鏀癸紙浜斾釜鐢宠椤碉級
+ * 杩涜涓�(PENDING)銆佸凡瀹屾垚(APPROVED) 涓嶅彲淇敼锛涘凡椹冲洖銆佸凡鎾ら攢绛夊彲淇敼
+ */
+export function canEditBusinessInstanceRow(row) {
+ const key = normalizeApprovalStatusKey(row?.approvalStatus ?? row?.statusRaw ?? row?.status);
+ return key !== "pending" && key !== "approved";
+}
+
+export function businessApprovalStatusTagType(v) {
+ const key = normalizeApprovalStatusKey(v);
+ if (key === "draft") return "info";
+ if (key === "approved") return "success";
+ if (key === "rejected") return "danger";
+ if (key === "cancelled") return "info";
+ return "warning";
+}
+
+export function approvalStatusTagType(v) {
+ const key = normalizeApprovalStatusKey(v);
+ if (key === "draft") return "info";
+ if (key === "approved") return "success";
+ if (key === "rejected") return "danger";
+ if (key === "cancelled") return "info";
+ return "warning";
+}
+
+/** 鍒楄〃琛� 鈫� 缂栬緫琛ㄥ崟锛堜粎鐢ㄨ鏁版嵁鍥炴樉锛� */
+export function buildEditFormFromInstanceRow(row) {
+ const { fields, formPayload, templateSnapshot } = resolveInstanceFormFields(row);
+ const normalized = normalizeFlowNodes(row?.flowNodes?.length ? row.flowNodes : mapTasksToFlowNodes(row?.tasks));
+ const flowNodes = normalized.length ? JSON.parse(JSON.stringify(normalized)) : [createEmptyNode(1)];
+
+ return {
+ templateKey: String(row?.templateId || ""),
+ templateId: row?.templateId,
+ templateName: row?.templateName || templateSnapshot.label,
+ instanceId: row?.id,
+ instanceNo: row?.instanceNo || "",
+ statusRaw: row?.statusRaw || row?.status || "PENDING",
+ currentLevel: row?.currentLevel ?? 1,
+ applicantId: row?.applicantId,
+ applicantName: row?.applicantName || "",
+ templateSnapshot,
+ formFieldDefs: fields,
+ formPayload,
+ flowNodes,
+ templateAttachments: initTemplateAttachmentsFromSnapshot(templateSnapshot),
+ storageBlobDTOs: (row?.storageBlobDTOs?.length ? row.storageBlobDTOs : row?.storageBlobVOList || []).map(f => JSON.parse(JSON.stringify(f))),
+ };
+}
+
+export function createEmptySubmitForm(templateKey, templateOverride, flowNodesOverride) {
+ const tpl = templateOverride || null;
+ const payload = tpl?.fields?.length ? buildFormPayloadFromFields(tpl.fields) : { summary: "" };
+ const normalized = normalizeFlowNodes(flowNodesOverride);
+ const flowNodes = normalized.length ? JSON.parse(JSON.stringify(normalized)) : [createEmptyNode(1)];
+ return {
+ templateKey: templateKey || "",
+ templateId: tpl?.templateId || "",
+ templateName: tpl?.label || "",
+ instanceId: "",
+ instanceNo: "",
+ statusRaw: "",
+ currentLevel: 1,
+ applicantId: null,
+ applicantName: "",
+ templateSnapshot: templateOverride || null,
+ formFieldDefs: tpl?.fields || [],
+ formPayload: payload,
+ flowNodes,
+ templateAttachments: tpl?.storageBlobDTOs ? JSON.parse(JSON.stringify(tpl.storageBlobDTOs)) : [],
+ storageBlobDTOs: [],
+ };
+}
+
+export function initTemplateAttachmentsFromSnapshot(templateSnapshot) {
+ const list = templateSnapshot?.storageBlobDTOs;
+ return list?.length ? JSON.parse(JSON.stringify(list)) : [];
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
new file mode 100644
index 0000000..721a5c2
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -0,0 +1,167 @@
+<!-- 瀹℃壒璇︽儏锛氬熀纭�淇℃伅 + 濉姤鍐呭 -->
+<template>
+ <div class="approve-detail-panel">
+ <div class="detail-block">
+ <div class="detail-block-title">鍩烘湰淇℃伅</div>
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="涓氬姟鍗曞彿">{{ row.bizId || row.id || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒鐘舵��">
+ <el-tag :type="approvalStatusTagType(row.approvalStatus)"
+ size="small"
+ effect="plain">
+ {{ approvalStatusLabel(row.approvalStatus) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹℃壒绫诲瀷">
+ <span class="approve-type-cell"
+ :style="approvalTypeStyle(row.approvalType)">
+ {{ approvalTypeLabel(row.approvalType) }}
+ </span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜虹紪鍙�">{{ row.applicantNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠浜哄悕绉�">{{ row.applicantName || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鎽樿">{{ row.summary || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item v-if="row.rejectReason"
+ label="椹冲洖鍘熷洜"
+ :span="2">
+ <span class="reject-text">{{ row.rejectReason }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿"
+ :span="2">
+ {{ formatDisplayTime(row.createTime) }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </div>
+ <div class="detail-block">
+ <div class="detail-block-title">濉姤鍐呭</div>
+ <FormPayloadFields :fields="formResolved.fields"
+ :form-payload="formResolved.formPayload"
+ readonly />
+ </div>
+ <div v-if="attachmentList.length"
+ class="detail-block">
+ <div class="detail-block-title">闄勪欢鍒楄〃</div>
+ <div class="attachment-list">
+ <div v-for="file in attachmentList"
+ :key="file.id"
+ class="attachment-item">
+ <el-icon class="file-icon">
+ <Paperclip />
+ </el-icon>
+ <span class="file-name"
+ :title="file.name || file.originalFilename">
+ {{ file.name || file.originalFilename }}
+ </span>
+ <div class="file-actions">
+ <el-link v-if="file.previewURL || file.url"
+ type="primary"
+ :underline="false"
+ @click="openFile(file.previewURL || file.url)">棰勮</el-link>
+ <el-divider v-if="(file.previewURL || file.url) && file.downloadURL"
+ direction="vertical" />
+ <el-link v-if="file.downloadURL"
+ type="primary"
+ :underline="false"
+ @click="openFile(file.downloadURL)">涓嬭浇</el-link>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { computed } from "vue";
+ import { Paperclip } from "@element-plus/icons-vue";
+ import { formatDisplayTime } from "../../approve-template/approveTemplateConstants.js";
+ import {
+ approvalTypeLabel,
+ approvalTypeStyle,
+ approvalStatusLabel,
+ approvalStatusTagType,
+ resolveInstanceFormFields,
+ } from "../approveListConstants.js";
+ import FormPayloadFields from "./FormPayloadFields.vue";
+
+ const props = defineProps({
+ row: { type: Object, default: () => ({}) },
+ });
+
+ const formResolved = computed(() => resolveInstanceFormFields(props.row));
+
+ const attachmentList = computed(() => {
+ const list = props.row.storageBlobVOList || props.row.storageBlobDTOs || [];
+ return Array.isArray(list) ? list : [];
+ });
+
+ function openFile(url) {
+ if (!url) return;
+ window.open(url, "_blank");
+ }
+</script>
+
+<style scoped>
+ .approve-detail-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+ .detail-block-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin: 0 0 12px;
+ padding-left: 10px;
+ border-left: 3px solid var(--el-color-primary);
+ line-height: 1.4;
+ }
+ .approve-type-cell {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 4px;
+ font-size: 13px;
+ line-height: 1.5;
+ }
+ .reject-text {
+ color: var(--el-color-danger);
+ }
+
+ .attachment-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 12px;
+ }
+ .attachment-item {
+ display: flex;
+ align-items: center;
+ padding: 10px 12px;
+ background-color: var(--el-fill-color-light);
+ border-radius: 6px;
+ border: 1px solid var(--el-border-color-lighter);
+ transition: all 0.3s;
+ }
+ .attachment-item:hover {
+ border-color: var(--el-color-primary-light-5);
+ background-color: var(--el-color-primary-light-9);
+ }
+ .file-icon {
+ font-size: 18px;
+ color: var(--el-text-color-secondary);
+ margin-right: 10px;
+ }
+ .file-name {
+ flex: 1;
+ font-size: 13px;
+ color: var(--el-text-color-primary);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-right: 12px;
+ }
+ .file-actions {
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ }
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
new file mode 100644
index 0000000..7933db5
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/FormPayloadFields.vue
@@ -0,0 +1,152 @@
+<!-- 濉姤椤癸細缂栬緫涓鸿〃鍗曟帶浠讹紝璇︽儏涓� descriptions 琛ㄦ牸锛堜笌涓婃柟鍩虹淇℃伅涓�鑷达級 -->
+<template>
+ <template v-if="fields?.length">
+ <el-descriptions
+ v-if="readonly"
+ :column="2"
+ border
+ class="form-payload-desc"
+ >
+ <el-descriptions-item
+ v-for="field in fields"
+ :key="field.key"
+ :label="field.label"
+ :span="field.type === 'textarea' || field.type === 'datetimerange' ? 2 : 1"
+ >
+ <span class="field-value">{{ displayValue(field) }}</span>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <div
+ v-else
+ class="form-payload-edit"
+ v-loading="optionSourceLoading"
+ >
+ <el-form-item
+ v-for="field in fields"
+ :key="field.key"
+ :label="field.label"
+ :prop="`formPayload.${field.key}`"
+ :required="Boolean(field.required)"
+ >
+ <el-input
+ v-if="field.type === 'text'"
+ v-model="formPayload[field.key]"
+ :placeholder="`璇疯緭鍏�${field.label}`"
+ maxlength="200"
+ />
+ <el-input
+ v-else-if="field.type === 'textarea'"
+ v-model="formPayload[field.key]"
+ type="textarea"
+ :rows="field.rows || 3"
+ :placeholder="`璇峰~鍐�${field.label}`"
+ maxlength="2000"
+ show-word-limit
+ />
+ <el-input-number
+ v-else-if="field.type === 'number'"
+ v-model="formPayload[field.key]"
+ :min="field.min ?? 0"
+ :precision="field.precision ?? 0"
+ controls-position="right"
+ style="width: 100%"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'date'"
+ v-model="formPayload[field.key]"
+ type="date"
+ :placeholder="`璇烽�夋嫨${field.label}`"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'datetimerange'"
+ v-model="formPayload[field.key]"
+ type="datetimerange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫椂闂�"
+ end-placeholder="缁撴潫鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ />
+ <el-select
+ v-else-if="field.type === 'select'"
+ v-model="formPayload[field.key]"
+ :placeholder="`璇烽�夋嫨${field.label}`"
+ style="width: 100%"
+ clearable
+ filterable
+ >
+ <el-option
+ v-for="o in getOptions(field)"
+ :key="String(o.value)"
+ :label="o.label"
+ :value="o.value"
+ />
+ </el-select>
+ <span v-else class="field-value">{{ displayValue(field) }}</span>
+ </el-form-item>
+ </div>
+ </template>
+ <el-empty v-else description="鏆傛棤濉姤椤�" :image-size="48" />
+</template>
+
+<script setup>
+import { onMounted, watch } from "vue";
+import { useSelectOptionSources } from "../../approve-template/useSelectOptionSources.js";
+import { formatFieldDisplayValue } from "../approveListConstants.js";
+
+const props = defineProps({
+ fields: { type: Array, default: () => [] },
+ formPayload: { type: Object, default: () => ({}) },
+ readonly: { type: Boolean, default: false },
+});
+
+const { loading: optionSourceLoading, ensureForFields, getOptions, getDisplayLabel } =
+ useSelectOptionSources();
+
+async function loadOptionCaches() {
+ await ensureForFields(props.fields);
+}
+
+onMounted(() => {
+ loadOptionCaches();
+});
+
+watch(
+ () => props.fields,
+ () => {
+ loadOptionCaches();
+ },
+ { deep: true }
+);
+
+function displayValue(field) {
+ const val = props.formPayload?.[field.key];
+ if (field.type === "select" && field.optionSource && field.optionSource !== "static") {
+ return getDisplayLabel(field, val);
+ }
+ return formatFieldDisplayValue(field, val);
+}
+</script>
+
+<style scoped>
+.form-payload-desc {
+ width: 100%;
+}
+.form-payload-desc :deep(.el-descriptions__label) {
+ width: 120px;
+ font-weight: 500;
+}
+.field-value {
+ color: var(--el-text-color-primary);
+ line-height: 1.6;
+ word-break: break-word;
+}
+.form-payload-edit {
+ width: 100%;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue
new file mode 100644
index 0000000..e5f2eef
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/components/InstanceFlowDisplay.vue
@@ -0,0 +1,147 @@
+<!-- 瀹℃壒瀹炰緥锛歵asks 瀹℃壒娴佺▼灞曠ず锛堟í鍚戞楠ゆ潯锛� -->
+<template>
+ <div v-if="displayNodes.length" class="flow-track">
+ <div
+ v-for="(node, index) in displayNodes"
+ :key="index"
+ class="flow-step"
+ :class="{ 'is-last': index === displayNodes.length - 1 }"
+ >
+ <div class="flow-step-card">
+ <div class="flow-step-badge">{{ index + 1 }}</div>
+ <div class="flow-step-main">
+ <div class="flow-step-head">
+ <span class="flow-step-name">鑺傜偣 {{ index + 1 }}</span>
+ <el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'" effect="plain">
+ {{ nodeSignModeLabel(node.signMode) }}
+ </el-tag>
+ </div>
+ <div class="flow-approvers">
+ <div
+ v-for="a in node.approvers"
+ :key="String(a.approverId ?? a.id)"
+ class="flow-approver"
+ >
+ <span class="flow-approver-name">{{ a.approverName || "鈥�" }}</span>
+ <el-tag
+ v-if="a.status"
+ size="small"
+ :type="mapTaskStatusTagType(a.status)"
+ effect="plain"
+ >
+ {{ mapTaskStatusLabel(a.status) }}
+ </el-tag>
+ </div>
+ <span v-if="!node.approvers?.length" class="flow-empty">鏈厤缃鎵逛汉</span>
+ </div>
+ </div>
+ </div>
+ <div v-if="index < displayNodes.length - 1" class="flow-connector" aria-hidden="true">
+ <el-icon><ArrowRight /></el-icon>
+ </div>
+ </div>
+ </div>
+ <el-empty v-else description="鏆傛棤娴佺▼鑺傜偣" :image-size="48" />
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { ArrowRight } from "@element-plus/icons-vue";
+import { nodeSignModeLabel } from "../../approve-template/approveTemplateConstants.js";
+import {
+ mapTaskStatusLabel,
+ mapTaskStatusTagType,
+ mapTasksToFlowNodes,
+} from "../approveListConstants.js";
+
+const props = defineProps({
+ tasks: { type: Array, default: () => [] },
+ nodes: { type: Array, default: () => [] },
+});
+
+const displayNodes = computed(() => {
+ if (props.tasks?.length) return mapTasksToFlowNodes(props.tasks);
+ return props.nodes || [];
+});
+</script>
+
+<style scoped>
+.flow-track {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ overflow-x: auto;
+ padding: 4px 2px 8px;
+}
+.flow-step {
+ display: flex;
+ align-items: center;
+ flex: 0 0 auto;
+}
+.flow-step-card {
+ display: flex;
+ gap: 12px;
+ min-width: 200px;
+ max-width: 260px;
+ padding: 14px;
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: 8px;
+ background: var(--el-bg-color);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
+}
+.flow-step-badge {
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ border-radius: 50%;
+ background: var(--el-color-primary-light-9);
+ color: var(--el-color-primary);
+ font-size: 13px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.flow-step-main {
+ flex: 1;
+ min-width: 0;
+}
+.flow-step-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin-bottom: 10px;
+}
+.flow-step-name {
+ font-weight: 600;
+ font-size: 13px;
+ color: var(--el-text-color-primary);
+}
+.flow-approvers {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+.flow-approver {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+}
+.flow-approver-name {
+ font-size: 13px;
+ color: var(--el-text-color-regular);
+}
+.flow-empty {
+ font-size: 12px;
+ color: var(--el-text-color-placeholder);
+}
+.flow-connector {
+ display: flex;
+ align-items: center;
+ padding: 0 6px;
+ color: var(--el-text-color-placeholder);
+ font-size: 16px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
new file mode 100644
index 0000000..bbfa56a
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/index.vue
@@ -0,0 +1,613 @@
+<!--OA妯″潡锛氬鎵瑰垪琛�-->
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div class="search_fields">
+ <span class="search_title">妯℃澘绫诲瀷锛�</span>
+ <el-select
+ v-model="searchForm.businessType"
+ placeholder="璇烽�夋嫨妯℃澘绫诲瀷"
+ clearable
+ filterable
+ style="width: 200px"
+ >
+ <el-option
+ v-for="opt in searchBusinessTypeOptions"
+ :key="`search-biz-type-${opt.value}`"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ <span class="search_title" style="margin-left: 12px">瀹℃壒鐘舵�侊細</span>
+ <el-select
+ v-model="searchForm.status"
+ placeholder="璇烽�夋嫨瀹℃壒鐘舵��"
+ clearable
+ style="width: 140px"
+ >
+ <el-option
+ v-for="opt in APPROVAL_STATUS_SEARCH_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ <span class="search_title" style="margin-left: 12px">鍒涘缓鏃堕棿锛�</span>
+ <el-date-picker
+ v-model="searchForm.createTimeRange"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 260px"
+ clearable
+ />
+ <el-button type="primary" :icon="Search" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+ <el-button :icon="RefreshRight" @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div class="search_actions">
+ <el-button type="primary" :icon="Plus" @click="openSubmitDialog">鎻愪氦瀹℃壒</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="false"
+ :tableLoading="tableLoading"
+ :total="page.total"
+ @pagination="pagination"
+ >
+ <template #approveType="{ row }">
+ <span class="approve-type-cell" :style="approvalTypeStyle(row.approvalType)">
+ {{ approvalTypeLabel(row.approvalType) }}
+ </span>
+ </template>
+ </PIMTable>
+ </div>
+
+ <!-- 鎻愪氦瀹℃壒锛堟寜妯℃澘锛� -->
+ <el-dialog
+ v-model="submitDialog.visible"
+ :title="submitDialogTitle"
+ width="720px"
+ append-to-body
+ destroy-on-close
+ class="approve-submit-dialog"
+ @closed="resetSubmitDialogState"
+ >
+ <template v-if="submitDialog.step === 1 && !isSubmitEdit">
+ <p class="template-hint">璇峰厛閫夋嫨妯℃澘绫诲瀷锛屽啀閫夋嫨璇ョ被鍨嬩笅宸插惎鐢ㄧ殑瀹℃壒妯℃澘銆�</p>
+ <div v-loading="submitTemplatesLoading" class="template-grid">
+ <div
+ v-for="opt in submitBusinessTypeOptions"
+ :key="`biz-type-${opt.value}`"
+ class="template-card"
+ :class="{ 'is-disabled': !countTemplatesByBusinessType(opt.value) }"
+ @click="onBusinessTypePick(opt.value)"
+ >
+ <span class="template-card-type">{{ opt.label }}</span>
+ <span class="template-card-desc">
+ {{ countTemplatesByBusinessType(opt.value) }} 涓彲鐢ㄦā鏉�
+ </span>
+ </div>
+ <el-empty
+ v-if="!submitTemplatesLoading && !submitBusinessTypeOptions.length"
+ description="鏆傛棤妯℃澘绫诲瀷"
+ :image-size="80"
+ class="template-empty"
+ />
+ </div>
+ </template>
+
+ <template v-else-if="submitDialog.step === 2 && !isSubmitEdit">
+ <p class="template-hint">
+ 褰撳墠绫诲瀷锛歿{ selectedBusinessTypeLabel || "鈥�" }}锛岃閫夋嫨鍏蜂綋瀹℃壒妯℃澘銆�
+ <el-button type="primary" link class="ml8" @click="backToBusinessTypePick">鏇存崲绫诲瀷</el-button>
+ </p>
+ <ApprovalTemplatePicker
+ :cards="submitTemplateCards"
+ :loading="submitTemplatesLoading"
+ @pick="onTemplatePick"
+ />
+ </template>
+
+ <template v-else>
+ <div v-loading="submitTemplatesLoading && !isSubmitEdit">
+ <el-form ref="submitFormRef" :model="submitForm" :rules="submitFormRules" label-width="120px">
+ <el-form-item v-if="isSubmitEdit" label="瀹℃壒绫诲瀷">
+ <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate.approvalType)">
+ {{ activeTemplate.label }}
+ </span>
+ </el-form-item>
+ <ApprovalTemplateFormSection
+ :active-template="activeTemplate"
+ :fields="submitFormFields"
+ :form-payload="submitForm.formPayload"
+ v-model:flow-nodes="submitForm.flowNodes"
+ v-model:attachments="submitForm.storageBlobDTOs"
+ :template-attachments="submitForm.templateAttachments"
+ :user-options="flowUserOptions"
+ :show-template-name="!isSubmitEdit"
+ :allow-change-template="!isSubmitEdit"
+ @change-template="backToTemplatePick"
+ />
+ </el-form>
+ </div>
+ </template>
+
+ <template #footer>
+ <el-button
+ v-if="submitDialog.step === 3 || isSubmitEdit"
+ type="primary"
+ :loading="submitSaving"
+ @click="onSubmitInstance"
+ >
+ {{ isSubmitEdit ? "淇� 瀛�" : "鎻� 浜�" }}
+ </el-button>
+ <el-button
+ v-if="submitDialog.step === 2 && !isSubmitEdit"
+ @click="backToBusinessTypePick"
+ >
+ 涓婁竴姝�
+ </el-button>
+ <el-button @click="submitDialog.visible = false">
+ {{ submitDialog.step === 1 && !isSubmitEdit ? "鍙� 娑�" : "鍏� 闂�" }}
+ </el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 璇︽儏 -->
+ <el-dialog
+ v-model="detailDialog.visible"
+ title="瀹℃壒璇︽儏"
+ width="920px"
+ append-to-body
+ destroy-on-close
+ class="approve-detail-dialog"
+ >
+ <div class="approve-detail-body">
+ <ApproveDetailPanel :row="detailRow" />
+ <div class="detail-block">
+ <div class="detail-block-title">
+ 瀹℃壒娴佺▼锛坽{ detailRow.tasks?.length || detailRow.flowNodes?.length || 0 }} 椤癸級
+ </div>
+ <InstanceFlowDisplay :tasks="detailRow.tasks" :nodes="detailRow.flowNodes" />
+ </div>
+ <div class="detail-block">
+ <div class="detail-block-title">瀹℃壒璁板綍</div>
+ <el-timeline v-if="detailRow.approvalRecords?.length" class="approve-record-timeline">
+ <el-timeline-item
+ v-for="(rec, i) in detailRow.approvalRecords"
+ :key="rec.id ?? i"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+ :timestamp="formatRecordTime(rec.time)"
+ placement="top"
+ >
+ <div class="record-item">
+ <span class="record-operator">{{ rec.operatorName || "鈥�" }}</span>
+ <el-tag
+ size="small"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'info'"
+ effect="plain"
+ >
+ {{ approvalActionLabel(rec.result) }}
+ </el-tag>
+ <p class="record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</p>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="48" />
+ </div>
+ </div>
+ <template #footer>
+ <el-button
+ v-if="detailRow.approvalStatus === 'pending'"
+ @click="openEditFromDetail"
+ >
+ 淇� 鏀�
+ </el-button>
+ <el-button
+ v-if="detailRow.approvalStatus === 'pending' && detailRow.isApprove"
+ type="primary"
+ @click="openApproveFromDetail"
+ >
+ 鍘诲鎵�
+ </el-button>
+ <el-button @click="detailDialog.visible = false">鍏� 闂�</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 宸梾/璐圭敤鎶ラ攢璇︽儏锛堝鎵瑰垪琛級 -->
+ <el-dialog
+ v-model="reimburseDialog.visible"
+ :title="reimburseDialog.mode === 'approve' ? reimburseApproveTitle : reimburseDetailTitle"
+ width="1000px"
+ append-to-body
+ destroy-on-close
+ @closed="approveOpinion = ''"
+ >
+ <FinReimburseApprovePanel
+ :mode="reimburseDialog.mode"
+ :module-key="reimburseDialog.moduleKey"
+ :reimburse-row="reimburseDialog.reimburseRow"
+ :loading="reimburseDialog.loading"
+ v-model:approve-opinion="approveOpinion"
+ />
+ <template #footer>
+ <template v-if="reimburseDialog.mode === 'approve'">
+ <el-button
+ type="success"
+ :loading="approveSubmitting"
+ @click="onReimburseApprove('approved')"
+ >
+ 閫� 杩�
+ </el-button>
+ <el-button
+ type="danger"
+ :loading="approveSubmitting"
+ @click="onReimburseApprove('rejected')"
+ >
+ 椹� 鍥�
+ </el-button>
+ <el-button :disabled="approveSubmitting" @click="reimburseDialog.visible = false">
+ 鍙� 娑�
+ </el-button>
+ </template>
+ <template v-else>
+ <el-button
+ v-if="reimburseDialog.instanceRow?.approvalStatus === 'pending'"
+ @click="openEditFromReimburseDetail"
+ >
+ 淇� 鏀�
+ </el-button>
+ <el-button
+ v-if="
+ reimburseDialog.instanceRow?.approvalStatus === 'pending' &&
+ reimburseDialog.instanceRow?.isApprove
+ "
+ type="primary"
+ @click="openReimburseApproveFromDetail"
+ >
+ 鍘诲鎵�
+ </el-button>
+ <el-button type="primary" @click="reimburseDialog.visible = false">鍏� 闂�</el-button>
+ </template>
+ </template>
+ </el-dialog>
+
+ <!-- 瀹℃壒鎿嶄綔 -->
+ <el-dialog
+ v-model="approveDialog.visible"
+ title="瀹℃壒澶勭悊"
+ width="960px"
+ append-to-body
+ destroy-on-close
+ @closed="approveOpinion = ''"
+ >
+ <ApproveDetailPanel :row="approveDialog.row" />
+ <div class="detail-block mt16">
+ <div class="detail-block-title">
+ 瀹℃壒娴佺▼锛坽{ approveDialog.row?.tasks?.length || approveDialog.row?.flowNodes?.length || 0 }} 椤癸級
+ </div>
+ <InstanceFlowDisplay :tasks="approveDialog.row?.tasks" :nodes="approveDialog.row?.flowNodes" />
+ </div>
+ <el-form label-width="100px" class="mt16">
+ <el-form-item label="瀹℃壒鎰忚" required>
+ <el-input
+ v-model="approveOpinion"
+ type="textarea"
+ :rows="3"
+ maxlength="500"
+ show-word-limit
+ placeholder="閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏叿浣撳師鍥�"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button
+ type="success"
+ :loading="approveSubmitting"
+ @click="onApprove('approved')"
+ >
+ 閫� 杩�
+ </el-button>
+ <el-button
+ type="danger"
+ :loading="approveSubmitting"
+ @click="onApprove('rejected')"
+ >
+ 椹� 鍥�
+ </el-button>
+ <el-button :disabled="approveSubmitting" @click="approveDialog.visible = false">
+ 鍙� 娑�
+ </el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { Plus, RefreshRight } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import { computed, onMounted, ref } from "vue";
+import { APPROVAL_MODULE_KEYS } from "../approve-shared/approvalModuleRegistry.js";
+import FinReimburseApprovePanel from "../../ReimburseManage/shared/components/FinReimburseApprovePanel.vue";
+import ApprovalTemplateFormSection from "../approve-shared/components/ApprovalTemplateFormSection.vue";
+import ApprovalTemplatePicker from "../approve-shared/components/ApprovalTemplatePicker.vue";
+import { useFlowUserOptions } from "../approve-shared/useFlowUserOptions.js";
+import { formatDisplayTime } from "../approve-template/approveTemplateConstants.js";
+import { approvalTypeStyle } from "./approveListConstants.js";
+import ApproveDetailPanel from "./components/ApproveDetailPanel.vue";
+import InstanceFlowDisplay from "./components/InstanceFlowDisplay.vue";
+import { useApproveList } from "./useApproveList.js";
+
+const al = useApproveList();
+const {
+ Search,
+ APPROVAL_STATUS_SEARCH_OPTIONS,
+ searchBusinessTypeOptions,
+ loadSearchBusinessTypeOptions,
+ submitBusinessTypeOptions,
+ submitTemplateCards,
+ selectedBusinessTypeLabel,
+ countTemplatesByBusinessType,
+ submitTemplatesLoading,
+ onBusinessTypePick,
+ backToBusinessTypePick,
+ approvalTypeLabel,
+ approvalActionLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ detailDialog,
+ detailRow,
+ reimburseDialog,
+ approveDialog,
+ approveOpinion,
+ approveSubmitting,
+ submitReimburseApprove,
+ submitDialog,
+ isSubmitEdit,
+ submitDialogTitle,
+ submitForm,
+ submitFormRef,
+ submitSaving,
+ activeTemplate,
+ submitFormFields,
+ submitFormRules,
+ handleQuery,
+ resetSearch,
+ pagination,
+ resetSubmitDialogState,
+ openSubmitDialog,
+ openEditDialog,
+ onTemplatePick,
+ backToTemplatePick,
+ submitInstanceForm,
+ submitApprove,
+ openDetail,
+ openApprove,
+} = al;
+
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
+
+async function onSubmitInstance() {
+ const ok = await submitInstanceForm();
+ if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "瀹℃壒宸叉彁浜�");
+}
+
+const reimburseDetailTitle = computed(() =>
+ reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+ ? "璐圭敤鎶ラ攢璇︽儏"
+ : "宸梾鎶ラ攢璇︽儏"
+);
+const reimburseApproveTitle = computed(() =>
+ reimburseDialog.moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+ ? "璐圭敤鎶ラ攢瀹℃壒"
+ : "宸梾鎶ラ攢瀹℃壒"
+);
+
+async function onApprove(result) {
+ const ret = await submitApprove(result);
+ if (ret?.needOpinion) {
+ ElMessage.warning("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
+ return;
+ }
+ if (ret?.ok) {
+ ElMessage.success(result === "approved" ? "宸查�氳繃" : "宸查┏鍥�");
+ }
+}
+
+async function onReimburseApprove(result) {
+ const ret = await submitReimburseApprove(result);
+ if (ret?.needOpinion) {
+ ElMessage.warning("椹冲洖鏃惰濉啓瀹℃壒鎰忚");
+ return;
+ }
+ if (ret?.ok) {
+ ElMessage.success(result === "approved" ? "宸查�氳繃" : "宸查┏鍥�");
+ }
+}
+
+function formatRecordTime(time) {
+ return formatDisplayTime(time) || "鈥�";
+}
+
+async function openApproveFromDetail() {
+ const row = detailRow.value;
+ detailDialog.visible = false;
+ await openApprove(row);
+}
+
+function openEditFromDetail() {
+ const row = detailRow.value;
+ detailDialog.visible = false;
+ openEditDialog(row);
+}
+
+function openEditFromReimburseDetail() {
+ const row = reimburseDialog.instanceRow;
+ reimburseDialog.visible = false;
+ if (row) openEditDialog(row);
+}
+
+async function openReimburseApproveFromDetail() {
+ const row = reimburseDialog.instanceRow;
+ if (!row) return;
+ reimburseDialog.mode = "approve";
+ approveOpinion.value = "";
+}
+
+onMounted(() => {
+ loadFlowUsers();
+ loadSearchBusinessTypeOptions();
+ handleQuery();
+});
+</script>
+
+<style scoped>
+.mb20 {
+ margin-bottom: 20px;
+}
+.ml12 {
+ margin-left: 12px;
+}
+.mt16 {
+ margin-top: 16px;
+}
+.search_form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.search_fields {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px;
+}
+.search_actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.approve-type-cell {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 4px;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.template-hint {
+ font-size: 13px;
+ color: var(--el-text-color-secondary);
+ margin: 0 0 16px;
+}
+.template-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 12px;
+ min-height: 120px;
+}
+.template-empty {
+ grid-column: 1 / -1;
+}
+.template-card {
+ padding: 14px 16px;
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: var(--radius-md, 8px);
+ cursor: pointer;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ background: var(--el-fill-color-blank);
+}
+.template-card:hover {
+ border-color: var(--el-color-primary);
+ box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06));
+}
+.template-card.is-disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+.template-card.is-disabled:hover {
+ border-color: var(--el-border-color-lighter);
+ box-shadow: none;
+}
+.ml8 {
+ margin-left: 8px;
+}
+.template-card-type {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+.template-card-desc {
+ display: block;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ line-height: 1.5;
+}
+.flow-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin-top: 8px;
+}
+.approve-submit-dialog :deep(.el-dialog__body) {
+ padding-top: 12px;
+}
+.approve-detail-dialog :deep(.el-dialog__body) {
+ padding-top: 16px;
+ max-height: 70vh;
+ overflow-y: auto;
+}
+.approve-detail-body .detail-block {
+ margin-top: 20px;
+}
+.approve-detail-body .detail-block-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin: 0 0 12px;
+ padding-left: 10px;
+ border-left: 3px solid var(--el-color-primary);
+ line-height: 1.4;
+}
+.approve-record-timeline {
+ padding-left: 4px;
+}
+.record-item {
+ padding: 4px 0 2px;
+}
+.record-operator {
+ font-weight: 600;
+ margin-right: 8px;
+ color: var(--el-text-color-primary);
+}
+.record-opinion {
+ margin: 8px 0 0;
+ font-size: 13px;
+ color: var(--el-text-color-regular);
+ line-height: 1.5;
+}
+.detail-block-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin: 0 0 12px;
+ padding-left: 10px;
+ border-left: 3px solid var(--el-color-primary);
+ line-height: 1.4;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
new file mode 100644
index 0000000..a4aa917
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -0,0 +1,628 @@
+import {
+ getApprovalTemplateDetail,
+ listApprovalTemplate,
+ TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import {
+ approveApprovalInstance,
+ deleteApprovalInstance,
+ listApprovalInstancePage,
+ saveApprovalInstance,
+ updateApprovalInstance,
+} from "@/api/officeProcessAutomation/approvalInstance.js";
+import useUserStore from "@/store/modules/user";
+import { Search } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, getCurrentInstance, reactive, ref } from "vue";
+import {
+ inferReimburseModuleKeyFromInstance,
+ loadReimburseDetailForInstance,
+ navigateToReimburseManageForEdit,
+ resolveFinReimbursementIdFromInstance,
+} from "../../ReimburseManage/shared/reimburseApproveBridge.js";
+import {
+ fetchBusinessTypeOptions,
+ formatDisplayTime,
+ mapEnabledFromApi,
+ unwrapTemplateList,
+} from "../approve-template/approveTemplateConstants.js";
+import {
+ buildFormPayloadRules,
+ buildTemplateBindingFromDetail,
+ validateTemplateBinding,
+} from "../approve-shared/approvalTemplateBindingUtils.js";
+import {
+ APPROVAL_STATUS_SEARCH_OPTIONS,
+ APPROVAL_TYPE_OPTIONS,
+ approvalStatusLabel,
+ approvalStatusTagType,
+ approvalTypeLabel,
+ buildApprovalInstanceListParams,
+ buildApproveInstanceDto,
+ buildEditFormFromInstanceRow,
+ buildInstanceDto,
+ createEmptySubmitForm,
+ mapInstanceFromApi,
+ mapSubmitTemplateCard,
+ matchBusinessTypeValue,
+ unwrapInstancePage,
+} from "./approveListConstants.js";
+
+export function useApproveList() {
+ const { proxy } = getCurrentInstance() || {};
+ const userStore = useUserStore();
+
+ const tableData = ref([]);
+ const searchBusinessTypeOptions = ref([]);
+ const submitBusinessTypeOptions = ref([]);
+ const allSubmitTemplates = ref([]);
+ const selectedBusinessType = ref("");
+ const submitTemplatesLoading = ref(false);
+
+ const submitTemplateCards = computed(() => {
+ if (selectedBusinessType.value == null || selectedBusinessType.value === "") return [];
+ return allSubmitTemplates.value.filter((card) =>
+ matchBusinessTypeValue(card.businessType, selectedBusinessType.value)
+ );
+ });
+
+ const searchForm = reactive({
+ businessType: "",
+ status: "",
+ createTimeRange: [],
+ });
+
+ const tableLoading = ref(false);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+
+ const detailDialog = reactive({ visible: false });
+ const detailRow = ref({});
+
+ const approveDialog = reactive({ visible: false, row: null });
+ const approveOpinion = ref("");
+ const approveSubmitting = ref(false);
+
+ /** 宸梾/璐圭敤鎶ラ攢涓撶敤璇︽儏銆佸鎵瑰脊绐� */
+ const reimburseDialog = reactive({
+ visible: false,
+ mode: "detail",
+ moduleKey: "",
+ loading: false,
+ reimburseRow: {},
+ instanceRow: null,
+ });
+
+ const submitDialog = reactive({ visible: false, step: 1, mode: "add" });
+ const submitEditRow = ref(null);
+ const submitForm = reactive(createEmptySubmitForm(""));
+ const submitFormRef = ref();
+ const submitSaving = ref(false);
+
+ const isSubmitEdit = computed(() => submitDialog.mode === "edit");
+ const submitDialogTitle = computed(() => {
+ if (submitDialog.mode === "edit") {
+ return `淇敼${activeTemplate.value?.label || submitForm.templateName || "瀹℃壒"}`;
+ }
+ if (submitDialog.step === 1) return "閫夋嫨妯℃澘绫诲瀷";
+ if (submitDialog.step === 2) return `閫夋嫨瀹℃壒妯℃澘${businessTypeLabel(selectedBusinessType.value) ? `锛�${businessTypeLabel(selectedBusinessType.value)}锛塦 : ""}`;
+ return `鎻愪氦${activeTemplate.value?.label || "瀹℃壒"}`;
+ });
+
+ const selectedBusinessTypeLabel = computed(() => businessTypeLabel(selectedBusinessType.value));
+
+ function businessTypeLabel(type) {
+ if (type == null || type === "") return "";
+ const hit = submitBusinessTypeOptions.value.find((x) => matchBusinessTypeValue(x.value, type));
+ return hit?.label || "";
+ }
+
+ function countTemplatesByBusinessType(type) {
+ return allSubmitTemplates.value.filter((card) => matchBusinessTypeValue(card.businessType, type)).length;
+ }
+
+ const activeTemplate = computed(() => submitForm.templateSnapshot || null);
+
+ /** 濉姤椤瑰畾涔夛紙鏂板/淇敼涓� formConfig 涓�鑷达級 */
+ const submitFormFields = computed(() => {
+ const tplFields = activeTemplate.value?.fields;
+ if (tplFields?.length) return tplFields;
+ return submitForm.formFieldDefs || [];
+ });
+
+ const submitFormRules = computed(() => ({
+ templateKey: [{ required: true, message: "璇烽�夋嫨瀹℃壒绫诲瀷", trigger: "change" }],
+ ...buildFormPayloadRules(submitFormFields.value),
+ }));
+
+ const tableColumn = ref([
+ // { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
+ { label: "鐢宠浜哄悕绉�", prop: "applicantName", minWidth: 100 },
+ { label: "妯℃澘绫诲瀷", prop: "businessName", minWidth: 120 },
+ {
+ label: "瀹℃壒绫诲瀷",
+ prop: "approvalType",
+ minWidth: 140,
+ dataType: "slot",
+ slot: "approveType",
+ },
+ {
+ label: "寰呮垜瀹℃壒",
+ prop: "unread",
+ width: 90,
+ align: "center",
+ formatData: (v) => (v ? "鏄�" : "鍚�"),
+ },
+ {
+ label: "瀹℃壒鐘舵��",
+ prop: "approvalStatus",
+ width: 100,
+ dataType: "tag",
+ formatData: (v) => approvalStatusLabel(v),
+ formatType: (v) => approvalStatusTagType(v),
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 170,
+ formatData: (v) => formatDisplayTime(v),
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 240,
+ operation: [
+ { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+ {
+ name: "淇敼",
+ type: "text",
+ disabled: (row) => row.approvalStatus !== "pending",
+ clickFun: (row) => openEditDialog(row),
+ },
+ {
+ name: "瀹℃壒",
+ type: "text",
+ disabled: (row) => row.approvalStatus !== "pending" || !row.isApprove,
+ clickFun: (row) => openApprove(row),
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ clickFun: (row) => removeInstance(row),
+ },
+ ],
+ },
+ ]);
+
+ async function fetchApprovalList() {
+ tableLoading.value = true;
+ try {
+ const res = await listApprovalInstancePage(
+ buildApprovalInstanceListParams({ page, searchForm })
+ );
+ const { records, total } = unwrapInstancePage(res);
+ tableData.value = records.map(mapInstanceFromApi);
+ page.total = total;
+ } catch {
+ tableData.value = [];
+ page.total = 0;
+ ElMessage.error("瀹℃壒鍒楄〃鍔犺浇澶辫触");
+ } finally {
+ tableLoading.value = false;
+ }
+ }
+
+ async function loadSubmitTemplates() {
+ submitTemplatesLoading.value = true;
+ try {
+ const [typeOptions, customRes] = await Promise.all([
+ fetchBusinessTypeOptions(),
+ listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
+ ]);
+ submitBusinessTypeOptions.value = typeOptions;
+ allSubmitTemplates.value = unwrapTemplateList(customRes)
+ .filter((row) => mapEnabledFromApi(row.enabled))
+ .map(mapSubmitTemplateCard);
+ } catch {
+ submitBusinessTypeOptions.value = [];
+ allSubmitTemplates.value = [];
+ ElMessage.error("鍔犺浇瀹℃壒妯℃澘澶辫触");
+ } finally {
+ submitTemplatesLoading.value = false;
+ }
+ }
+
+ function handleQuery() {
+ page.current = 1;
+ fetchApprovalList();
+ }
+
+ function resetSearch() {
+ searchForm.businessType = "";
+ searchForm.status = "";
+ searchForm.createTimeRange = [];
+ handleQuery();
+ }
+
+ async function loadSearchBusinessTypeOptions() {
+ try {
+ searchBusinessTypeOptions.value = await fetchBusinessTypeOptions();
+ } catch {
+ searchBusinessTypeOptions.value = [];
+ }
+ }
+
+ function pagination({ page: p, limit }) {
+ page.current = p;
+ page.size = limit;
+ fetchApprovalList();
+ }
+
+ async function openReimburseDetail(row, mode) {
+ const moduleKey = inferReimburseModuleKeyFromInstance(row);
+ if (!moduleKey) return false;
+ reimburseDialog.mode = mode;
+ reimburseDialog.moduleKey = moduleKey;
+ reimburseDialog.instanceRow = row;
+ reimburseDialog.visible = true;
+ reimburseDialog.loading = true;
+ reimburseDialog.reimburseRow = {};
+ try {
+ const { reimburseRow, moduleKey: resolvedMk } =
+ await loadReimburseDetailForInstance(row, moduleKey);
+ reimburseDialog.moduleKey = resolvedMk || moduleKey;
+ reimburseDialog.reimburseRow = reimburseRow;
+ return true;
+ } catch {
+ ElMessage.error("鍔犺浇鎶ラ攢璇︽儏澶辫触");
+ reimburseDialog.visible = false;
+ return false;
+ } finally {
+ reimburseDialog.loading = false;
+ }
+ }
+
+ async function openDetail(row) {
+ if (isReimburseApprovalInstance(row)) {
+ await openReimburseDetail(row, "detail");
+ return;
+ }
+ detailRow.value = { ...row };
+ detailDialog.visible = true;
+ }
+
+ async function openApprove(row) {
+ if (inferReimburseModuleKeyFromInstance(row)) {
+ approveOpinion.value = "";
+ await openReimburseDetail(row, "approve");
+ return;
+ }
+ approveDialog.row = { ...row };
+ approveOpinion.value = "";
+ approveDialog.visible = true;
+ }
+
+ function isReimburseApprovalInstance(row) {
+ return Boolean(inferReimburseModuleKeyFromInstance(row));
+ }
+
+ function resetSubmitDialogState() {
+ submitDialog.mode = "add";
+ submitDialog.step = 1;
+ selectedBusinessType.value = "";
+ submitEditRow.value = null;
+ Object.assign(submitForm, createEmptySubmitForm(""));
+ }
+
+ function openSubmitDialog() {
+ resetSubmitDialogState();
+ submitDialog.visible = true;
+ loadSubmitTemplates();
+ }
+
+ async function openEditDialog(row) {
+ if (row?.approvalStatus !== "pending") {
+ ElMessage.warning("浠呭鏍镐腑鐨勫鎵瑰彲淇敼");
+ return;
+ }
+ const moduleKey = inferReimburseModuleKeyFromInstance(row);
+ if (moduleKey) {
+ const rid = resolveFinReimbursementIdFromInstance(row);
+ if (rid == null) {
+ ElMessage.warning("鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID");
+ return;
+ }
+ try {
+ await navigateToReimburseManageForEdit(proxy?.$router, moduleKey, rid);
+ } catch {
+ ElMessage.warning("鏈壘鍒板樊鏃�/璐圭敤鎶ラ攢鑿滃崟璺敱锛岃浠庡乏渚ц彍鍗曡繘鍏ュ悗鍐嶇紪杈�");
+ }
+ return;
+ }
+ if (!row?.id) {
+ ElMessage.warning("鏃犳硶淇敼锛氱己灏戝鎵瑰疄渚� ID");
+ return;
+ }
+ submitDialog.mode = "edit";
+ submitDialog.step = 3;
+ submitEditRow.value = { ...row };
+ Object.assign(submitForm, buildEditFormFromInstanceRow(row));
+ submitDialog.visible = true;
+ }
+
+ async function onTemplatePick(card) {
+ if (!card?.id) return;
+ submitTemplatesLoading.value = true;
+ try {
+ const res = await getApprovalTemplateDetail(card.id);
+ const applied = buildTemplateBindingFromDetail(res);
+ Object.assign(submitForm, {
+ templateKey: String(card.id),
+ ...applied,
+ businessType:
+ applied.businessType ?? card.businessType ?? selectedBusinessType.value,
+ });
+ submitDialog.step = 3;
+ } catch {
+ ElMessage.error("鍔犺浇妯℃澘璇︽儏澶辫触");
+ } finally {
+ submitTemplatesLoading.value = false;
+ }
+ }
+
+ function onBusinessTypePick(type) {
+ if (!countTemplatesByBusinessType(type)) {
+ ElMessage.warning("璇ョ被鍨嬩笅鏆傛棤鍙敤瀹℃壒妯℃澘");
+ return;
+ }
+ selectedBusinessType.value = type;
+ submitDialog.step = 2;
+ }
+
+ function backToBusinessTypePick() {
+ selectedBusinessType.value = "";
+ submitDialog.step = 1;
+ }
+
+ function backToTemplatePick() {
+ submitDialog.step = 2;
+ }
+
+ async function submitInstanceForm() {
+ if (submitDialog.mode === "edit") return submitEditApproval();
+ return submitNewApproval();
+ }
+
+ async function submitNewApproval() {
+ if (!submitFormRef.value) return false;
+ try {
+ await submitFormRef.value.validate();
+ } catch {
+ return false;
+ }
+ if (!activeTemplate.value) return false;
+ const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+ if (!bindingCheck.ok) {
+ ElMessage.warning(bindingCheck.message);
+ return false;
+ }
+ if (!submitForm.templateId) {
+ ElMessage.warning("缂哄皯妯℃澘 ID锛屾棤娉曟彁浜�");
+ return false;
+ }
+ if (submitSaving.value) return false;
+ submitSaving.value = true;
+ try {
+ await saveApprovalInstance(
+ buildInstanceDto({
+ submitForm,
+ activeTemplate: activeTemplate.value,
+ userStore,
+ flowNodes: bindingCheck.nodes,
+ })
+ );
+ submitDialog.visible = false;
+ page.current = 1;
+ await fetchApprovalList();
+ return true;
+ } catch {
+ return false;
+ } finally {
+ submitSaving.value = false;
+ }
+ }
+
+ async function submitEditApproval() {
+ if (!submitFormRef.value) return false;
+ try {
+ await submitFormRef.value.validate();
+ } catch {
+ return false;
+ }
+ if (!activeTemplate.value) return false;
+ const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+ if (!bindingCheck.ok) {
+ ElMessage.warning(bindingCheck.message);
+ return false;
+ }
+ if (!submitForm.instanceId) {
+ ElMessage.warning("缂哄皯瀹℃壒瀹炰緥 ID锛屾棤娉曚繚瀛�");
+ return false;
+ }
+ if (submitSaving.value) return false;
+ submitSaving.value = true;
+ try {
+ await updateApprovalInstance(
+ buildInstanceDto({
+ submitForm,
+ activeTemplate: activeTemplate.value,
+ flowNodes: bindingCheck.nodes,
+ existingRow: submitEditRow.value,
+ })
+ );
+ submitDialog.visible = false;
+ await fetchApprovalList();
+ if (detailDialog.visible && detailRow.value?.id === submitForm.instanceId) {
+ const hit = tableData.value.find((r) => r.id === submitForm.instanceId);
+ if (hit) detailRow.value = { ...hit };
+ else detailDialog.visible = false;
+ }
+ return true;
+ } catch {
+ return false;
+ } finally {
+ submitSaving.value = false;
+ }
+ }
+
+ async function removeInstance(row) {
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戝鎵瑰疄渚� ID");
+ return;
+ }
+ const title = row.title || row.templateName || row.instanceNo || "璇ュ鎵�";
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゅ鎵广��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
+ } catch {
+ return;
+ }
+ try {
+ await deleteApprovalInstance([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ if (detailDialog.visible && detailRow.value?.id === row.id) {
+ detailDialog.visible = false;
+ }
+ if (approveDialog.visible && approveDialog.row?.id === row.id) {
+ approveDialog.visible = false;
+ }
+ await fetchApprovalList();
+ } catch {
+ /* 閿欒鐢辨嫤鎴櫒鎻愮ず */
+ }
+ }
+
+ async function submitReimburseApprove(result) {
+ const row = reimburseDialog.instanceRow;
+ if (!row?.id) return { ok: false };
+ if (result === "rejected" && !(approveOpinion.value || "").trim()) {
+ return { needOpinion: true };
+ }
+ if (approveSubmitting.value) return { ok: false };
+ approveSubmitting.value = true;
+ try {
+ await approveApprovalInstance(
+ buildApproveInstanceDto(row, result, approveOpinion.value)
+ );
+ reimburseDialog.visible = false;
+ await fetchApprovalList();
+ return { ok: true, result };
+ } catch {
+ ElMessage.error("瀹℃壒鎿嶄綔澶辫触");
+ return { ok: false };
+ } finally {
+ approveSubmitting.value = false;
+ }
+ }
+
+ async function submitApprove(result) {
+ const row = approveDialog.row;
+ if (!row?.id) return { ok: false };
+ if (result === "rejected" && !(approveOpinion.value || "").trim()) {
+ return { needOpinion: true };
+ }
+ if (approveSubmitting.value) return { ok: false };
+ approveSubmitting.value = true;
+ try {
+ await approveApprovalInstance(
+ buildApproveInstanceDto(row, result, approveOpinion.value)
+ );
+ approveDialog.visible = false;
+ await fetchApprovalList();
+ if (detailDialog.visible && detailRow.value?.id === row.id) {
+ const hit = tableData.value.find((r) => r.id === row.id);
+ if (hit) detailRow.value = { ...hit };
+ else detailDialog.visible = false;
+ }
+ return { ok: true, result };
+ } catch {
+ ElMessage.error("瀹℃壒鎿嶄綔澶辫触");
+ return { ok: false };
+ } finally {
+ approveSubmitting.value = false;
+ }
+ }
+
+ function approvalActionLabel(result) {
+ if (result === "approved") return "閫氳繃";
+ if (result === "rejected") return "椹冲洖";
+ return "寰呭鐞�";
+ }
+
+ return {
+ Search,
+ APPROVAL_TYPE_OPTIONS,
+ APPROVAL_STATUS_SEARCH_OPTIONS,
+ searchBusinessTypeOptions,
+ loadSearchBusinessTypeOptions,
+ approvalTypeLabel,
+ approvalStatusLabel,
+ approvalStatusTagType,
+ approvalActionLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ detailDialog,
+ detailRow,
+ reimburseDialog,
+ approveDialog,
+ approveOpinion,
+ approveSubmitting,
+ submitReimburseApprove,
+ isReimburseApprovalInstance,
+ submitDialog,
+ isSubmitEdit,
+ submitDialogTitle,
+ submitForm,
+ submitFormRef,
+ submitSaving,
+ activeTemplate,
+ submitFormFields,
+ submitFormRules,
+ submitBusinessTypeOptions,
+ submitTemplateCards,
+ selectedBusinessType,
+ selectedBusinessTypeLabel,
+ businessTypeLabel,
+ countTemplatesByBusinessType,
+ submitTemplatesLoading,
+ handleQuery,
+ resetSearch,
+ pagination,
+ resetSubmitDialogState,
+ openSubmitDialog,
+ openEditDialog,
+ onBusinessTypePick,
+ onTemplatePick,
+ backToBusinessTypePick,
+ backToTemplatePick,
+ submitInstanceForm,
+ submitNewApproval,
+ submitApprove,
+ openDetail,
+ openApprove,
+ fetchApprovalList,
+ };
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js
new file mode 100644
index 0000000..d6bec18
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceFormConfigTable.js
@@ -0,0 +1,116 @@
+import { computed } from "vue";
+import { businessApprovalStatusLabel, businessApprovalStatusTagType, formatFieldDisplayValue, resolveInstanceFormFields } from "../approve-list/approveListConstants.js";
+import { INSTANCE_NO_SEARCH_MODULE_KEYS, INSTANCE_NO_TABLE_COLUMN } from "./approvalInstanceListSearch.js";
+
+/** 鍒楄〃/璇︽儏涓嶅洖鏄句负鐙珛鍒楃殑濉姤椤� key锛堥伩鍏嶈鐩栧疄渚嬬郴缁熷瓧娈碉級 */
+const DEFAULT_EXCLUDE_KEYS = new Set(["summary", "status", "approvalStatus", "approvalstatus", "instanceStatus", "publishStatus", "newsStatus"]);
+
+/** enrich 鍚庡繀椤讳繚鐣欑殑瀹炰緥瀛楁锛堜笉琚� formConfig 閾哄钩瑕嗙洊锛� */
+const PRESERVE_INSTANCE_FIELDS = [
+ "id",
+ "approvalStatus",
+ "statusRaw",
+ "status",
+ "instanceNo",
+ "templateId",
+ "templateName",
+ "businessType",
+ "businessId",
+ "businessName",
+ "applicantId",
+ "applicantNo",
+ "applicantName",
+ "createTime",
+ "applyTime",
+ "finishTime",
+ "title",
+ "isApprove",
+ "unread",
+ "currentLevel",
+ "newsStatus",
+ "storageBlobVOList",
+ "storageBlobDTOs",
+];
+
+/**
+ * 浠庤鏁版嵁 formConfig 瑙f瀽瀛楁瀹氫箟涓庡~鎶ュ�硷紝骞堕摵骞冲埌琛屼笂渚涗富琛� prop 缁戝畾锛堝睍绀虹敤鏍煎紡鍖栧�硷級
+ */
+export function enrichInstanceRowFromFormConfig(row, caches) {
+ const { fields, formPayload, templateSnapshot } = resolveInstanceFormFields(row);
+ const formDisplay = {};
+ const displayRow = {
+ ...row,
+ formFieldDefs: fields,
+ formPayload,
+ templateSnapshot: row.templateSnapshot || templateSnapshot,
+ formDisplay,
+ };
+
+ for (const f of fields) {
+ if (!f?.key || DEFAULT_EXCLUDE_KEYS.has(f.key)) continue;
+ const val = formPayload[f.key];
+ let text = formatFieldDisplayValue(f, val, caches);
+ if (text === String(val) && row?.applicantName && (f.label === "鐢宠浜�" || f.key === "applicant" || f.key === "applicantName")) {
+ const idMatch = String(val) === String(row.applicantId) || String(val) === String(row.applicantNo);
+ if (idMatch) text = row.applicantName;
+ }
+ formDisplay[f.key] = text;
+ displayRow[f.key] = text;
+ }
+
+ for (const key of PRESERVE_INSTANCE_FIELDS) {
+ if (row[key] !== undefined) displayRow[key] = row[key];
+ }
+
+ return displayRow;
+}
+
+/**
+ * 浠庡垪琛ㄩ琛� formConfig 鐢熸垚涓昏〃鍔ㄦ�佸垪锛坙abel 鍙栬嚜妯℃澘瀛楁 label锛�
+ */
+export function getFormConfigFieldColumns(firstRow, { excludeKeys = DEFAULT_EXCLUDE_KEYS } = {}) {
+ const fields = (firstRow?.formFieldDefs || []).filter(f => f?.key && !excludeKeys.has(f.key));
+ return fields.map(f => ({
+ label: f.label || f.key,
+ prop: f.key,
+ minWidth: f.type === "textarea" ? 200 : f.type === "datetimerange" ? 160 : 120,
+ showOverflowTooltip: true,
+ }));
+}
+
+/**
+ * 涓氬姟鐢宠涓昏〃鍒楋細鍥哄畾鍒� + formConfig 鍔ㄦ�佸垪 + 瀹℃壒鐘舵�� + 鎿嶄綔
+ */
+export function buildInstanceTableColumns(tableDataRef, buildTableActions, options = {}) {
+ const { moduleKey, excludeKeys = DEFAULT_EXCLUDE_KEYS, beforeFormColumns = [], extraColumns = [], afterFormColumns = [], actionWidth = 260 } = options;
+
+ const leadingCols = moduleKey && INSTANCE_NO_SEARCH_MODULE_KEYS.has(moduleKey) ? [INSTANCE_NO_TABLE_COLUMN] : [];
+
+ return computed(() => {
+ const formCols = getFormConfigFieldColumns(tableDataRef.value?.[0], { excludeKeys });
+ return [
+ ...leadingCols,
+ ...beforeFormColumns,
+ ...formCols,
+ ...extraColumns,
+ ...afterFormColumns,
+ { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
+ {
+ label: "瀹℃壒鐘舵��",
+ prop: "approvalStatus",
+ width: 110,
+ dataType: "tag",
+ formatData: v => businessApprovalStatusLabel(v),
+ formatType: v => businessApprovalStatusTagType(v),
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: actionWidth,
+ operation: buildTableActions(),
+ },
+ ];
+ });
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceListSearch.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceListSearch.js
new file mode 100644
index 0000000..80dda5c
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalInstanceListSearch.js
@@ -0,0 +1,158 @@
+import dayjs from "dayjs";
+import { APPROVAL_MODULE_KEYS } from "./approvalModuleRegistry.js";
+
+/** 鏀寔瀹℃壒鍗曞彿鏌ヨ/涓昏〃灞曠ず鐨勫鎵圭敵璇锋ā鍧� */
+export const INSTANCE_NO_SEARCH_MODULE_KEYS = new Set([
+ APPROVAL_MODULE_KEYS.REGULAR,
+ APPROVAL_MODULE_KEYS.TRANSFER,
+ APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+ APPROVAL_MODULE_KEYS.LEAVE,
+ APPROVAL_MODULE_KEYS.OVERTIME,
+]);
+
+export const INSTANCE_NO_TABLE_COLUMN = {
+ label: "瀹℃壒鍗曞彿",
+ prop: "instanceNo",
+ width: 170,
+ showOverflowTooltip: true,
+};
+
+/** 鎵佸钩鍖栦负 Spring GET 鍙粦瀹氱殑 query锛坅pprovalInstanceDto.xxx锛屽嬁鐢ㄦ柟鎷彿锛� */
+export function appendDotNotationQuery(target, prefix, fields) {
+ if (!fields || typeof fields !== "object") return;
+ for (const [key, value] of Object.entries(fields)) {
+ if (value == null || value === "") continue;
+ target[`${prefix}.${key}`] = value;
+ }
+}
+
+function pickApplicantFromSearchForm(searchForm = {}) {
+ const out = {};
+ const sf = searchForm || {};
+ const name = (sf.applicantName || "").trim();
+ const kw = (sf.applicantKeyword || "").trim();
+ const id = sf.applicantId;
+
+ if (name) out.applicantName = name;
+ if (kw) {
+ out.applicantName = kw;
+ if (/^\d+$/.test(kw)) out.applicantId = Number(kw);
+ }
+ if (id != null && id !== "") {
+ out.applicantId = typeof id === "number" ? id : Number(id) || id;
+ }
+ return out;
+}
+
+function pickInstanceNoFromSearchForm(searchForm = {}) {
+ const no = (searchForm?.instanceNo || "").trim();
+ return no ? { instanceNo: no } : {};
+}
+
+/** 缁勮 approvalInstanceDto 鏌ヨ鐗囨锛堢敵璇蜂汉 + 瀹℃壒鍗曞彿 + 鐘舵�� + 鏃堕棿鑼冨洿锛� */
+export function buildApprovalInstanceSearchDto(searchForm = {}, extraParams = {}) {
+ const dto = {
+ ...(extraParams && typeof extraParams === "object" ? extraParams : {}),
+ };
+ Object.assign(dto, pickApplicantFromSearchForm(searchForm));
+ Object.assign(dto, pickInstanceNoFromSearchForm(searchForm));
+
+ // 瀹℃壒鐘舵��
+ if (searchForm?.status) {
+ dto.status = searchForm.status;
+ }
+
+ // 鍒涘缓鏃堕棿鑼冨洿
+ const range = searchForm?.createTimeRange;
+ if (Array.isArray(range) && range[0]) {
+ dto.createTimeStart = range[0] + (range[0].includes(":") ? "" : " 00:00:00");
+ }
+ if (Array.isArray(range) && range[1]) {
+ dto.createTimeEnd = range[1] + (range[1].includes(":") ? "" : " 23:59:59");
+ }
+
+ delete dto.createTime;
+ return dto;
+}
+
+function getRowPayloadValue(row, keys) {
+ const keyList = Array.isArray(keys) ? keys : [keys];
+ const payload = row?.formPayload || {};
+ for (const k of keyList) {
+ if (row?.[k] != null && row[k] !== "") return row[k];
+ if (payload[k] != null && payload[k] !== "") return payload[k];
+ }
+ return "";
+}
+
+function matchApplicantKeyword(row, keyword) {
+ const kw = (keyword || "").trim().toLowerCase();
+ if (!kw) return true;
+ const parts = [row?.applicantName, row?.applicantNo, row?.applicantId, getRowPayloadValue(row, ["applicant", "applicantName", "applicantId"])]
+ .filter(v => v != null && v !== "")
+ .map(v => String(v).toLowerCase());
+ return parts.some(p => p.includes(kw));
+}
+
+function matchApplicantId(row, applicantId) {
+ if (applicantId == null || applicantId === "") return true;
+ const id = String(applicantId);
+ if (row?.applicantId != null && String(row.applicantId) === id) return true;
+ const payloadApplicant = getRowPayloadValue(row, ["applicant", "applicantId", "applicantUserId"]);
+ return String(payloadApplicant) === id;
+}
+
+function matchSelectValue(row, keys, expected) {
+ if (!expected) return true;
+ const raw = getRowPayloadValue(row, keys);
+ return String(raw) === String(expected);
+}
+
+function matchInstanceNo(row, instanceNo) {
+ const kw = (instanceNo || "").trim().toLowerCase();
+ if (!kw) return true;
+ const parts = [row?.instanceNo, row?.bizId].filter(v => v != null && v !== "").map(v => String(v).toLowerCase());
+ return parts.some(p => p.includes(kw));
+}
+
+/** 鏄惁瀛樺湪鍒楄〃绛涢�夋潯浠讹紙鐢宠浜� / 瀹℃壒鍗曞彿 / 鐘舵�� / 鏃堕棿鑼冨洿锛� */
+export function hasActiveModuleSearch(moduleKey, searchForm = {}) {
+ const sf = searchForm || {};
+ if ((sf.instanceNo || "").trim()) return true;
+ if ((sf.applicantKeyword || "").trim()) return true;
+ if ((sf.applicantName || "").trim()) return true;
+ if (sf.applicantId != null && sf.applicantId !== "") return true;
+ if (sf.status) return true;
+ if (Array.isArray(sf.createTimeRange) && sf.createTimeRange.length === 2) return true;
+ return false;
+}
+
+/** 鎸夌敵璇蜂汉銆佸鎵瑰崟鍙枫�佺姸鎬併�佹椂闂磋寖鍥村仛鍓嶇鍏滃簳绛涢�� */
+export function filterInstanceRowsByModuleSearch(moduleKey, rows, searchForm = {}) {
+ const sf = searchForm || {};
+ const list = Array.isArray(rows) ? rows : [];
+ if (!hasActiveModuleSearch(moduleKey, sf)) return list;
+
+ return list.filter(row => {
+ // 瀹℃壒鍗曞彿
+ if (!matchInstanceNo(row, sf.instanceNo)) return false;
+ // 鐢宠浜�
+ if (!matchApplicantId(row, sf.applicantId)) return false;
+ if (!matchApplicantKeyword(row, sf.applicantKeyword || sf.applicantName)) return false;
+ // 鐘舵��
+ if (sf.status && String(row.statusRaw || row.status).toUpperCase() !== String(sf.status).toUpperCase()) {
+ return false;
+ }
+ // 鏃堕棿鑼冨洿
+ if (Array.isArray(sf.createTimeRange) && sf.createTimeRange.length === 2) {
+ const rowTime = row.createTime || row.applyTime;
+ if (rowTime) {
+ const t = dayjs(rowTime);
+ const start = dayjs(sf.createTimeRange[0] + " 00:00:00");
+ const end = dayjs(sf.createTimeRange[1] + " 23:59:59");
+ if (t.isBefore(start) || t.isAfter(end)) return false;
+ }
+ }
+ return true;
+ });
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
new file mode 100644
index 0000000..fd56eca
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalModuleRegistry.js
@@ -0,0 +1,152 @@
+/**
+ * 鍚勪笟鍔℃ā鍧椾笌瀹℃壒妯℃澘绫诲瀷鐨勬槧灏勶紙閰嶇疆鍖栧叆鍙o級
+ * businessType 涓庡悗绔� TypeEnums / listPage 绾﹀畾涓�鑷达紙鍐欐鏋氫妇鍊硷級
+ */
+export const APPROVAL_MODULE_KEYS = {
+ REGULAR: "regular",
+ TRANSFER: "transfer",
+ RESIGN: "resign",
+ WORK_HANDOVER: "work_handover",
+ LEAVE: "leave",
+ OVERTIME: "overtime",
+ TRAVEL_REIMBURSE: "travel_reimburse",
+ COST_REIMBURSE: "cost_reimburse",
+ ENTERPRISE_NEWS: "enterprise_news",
+};
+
+/** 瀹℃壒瀹炰緥 listPage / 淇濆瓨 浣跨敤鐨� businessType 鏋氫妇 */
+export const APPROVAL_BUSINESS_TYPE = {
+ [APPROVAL_MODULE_KEYS.REGULAR]: 10,
+ [APPROVAL_MODULE_KEYS.TRANSFER]: 11,
+ [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: 13,
+ [APPROVAL_MODULE_KEYS.LEAVE]: 14,
+ [APPROVAL_MODULE_KEYS.OVERTIME]: 15,
+ [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: 16,
+ [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: 17,
+ [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: 18,
+};
+
+/** @type {Record<string, import('./approvalModuleRegistry.js').ApprovalModuleConfig>} */
+export const APPROVAL_MODULE_REGISTRY = {
+ [APPROVAL_MODULE_KEYS.REGULAR]: {
+ label: "杞鐢宠",
+ approvalType: "regular",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.REGULAR],
+ typeLabels: ["杞", "杞鐢宠"],
+ },
+ [APPROVAL_MODULE_KEYS.TRANSFER]: {
+ label: "璋冨矖鐢宠",
+ approvalType: "transfer",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.TRANSFER],
+ typeLabels: ["璋冨矖", "璋冨姩", "璋冨矖鐢宠", "璋冨姩鐢宠"],
+ },
+ [APPROVAL_MODULE_KEYS.RESIGN]: {
+ label: "绂昏亴鐢宠",
+ approvalType: "resign",
+ typeLabels: ["绂昏亴", "绂昏亴鐢宠", "绂昏亴瀹℃壒"],
+ },
+ [APPROVAL_MODULE_KEYS.WORK_HANDOVER]: {
+ label: "宸ヤ綔浜ゆ帴",
+ approvalType: "work_handover",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.WORK_HANDOVER],
+ typeLabels: ["宸ヤ綔浜ゆ帴", "浜ゆ帴", "宸ヤ綔浜ゆ帴瀹℃壒"],
+ },
+ [APPROVAL_MODULE_KEYS.LEAVE]: {
+ label: "璇峰亣鐢宠",
+ approvalType: "leave",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.LEAVE],
+ typeLabels: ["璇峰亣", "璇峰亣鐢宠", "璇峰亣瀹℃壒"],
+ },
+ [APPROVAL_MODULE_KEYS.OVERTIME]: {
+ label: "鍔犵彮鐢宠",
+ approvalType: "overtime",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.OVERTIME],
+ typeLabels: ["鍔犵彮", "鍔犵彮鐢宠", "鍔犵彮瀹℃壒"],
+ },
+ [APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE]: {
+ label: "宸梾鎶ラ攢",
+ approvalType: "travel_reimburse",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE],
+ typeLabels: ["宸梾", "宸梾鎶ラ攢", "鍑哄樊鎶ラ攢"],
+ },
+ [APPROVAL_MODULE_KEYS.COST_REIMBURSE]: {
+ label: "璐圭敤鎶ラ攢",
+ approvalType: "cost_reimburse",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.COST_REIMBURSE],
+ typeLabels: ["璐圭敤", "璐圭敤鎶ラ攢"],
+ },
+ [APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS]: {
+ label: "浼佷笟鏂伴椈",
+ approvalType: "enterprise_news",
+ businessType: APPROVAL_BUSINESS_TYPE[APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS],
+ typeLabels: ["浼佷笟鏂伴椈", "鏂伴椈", "鏂伴椈鍙戝竷"],
+ },
+};
+
+/**
+ * @typedef {object} ApprovalModuleConfig
+ * @property {string} label
+ * @property {string} [approvalType]
+ * @property {string|number} [businessType]
+ * @property {string[]} [typeLabels]
+ */
+
+export function getApprovalModuleConfig(moduleKey) {
+ if (!moduleKey) return null;
+ return APPROVAL_MODULE_REGISTRY[moduleKey] || null;
+}
+
+/** 鍒楄〃鏌ヨ businessType锛堜紭鍏堥厤缃灇涓撅紝涓嶅啀鍥為�� approvalType 瀛楃涓诧級 */
+export function getModuleListBusinessType(moduleKey) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return "";
+ if (cfg.businessType != null && cfg.businessType !== "") return cfg.businessType;
+ return APPROVAL_BUSINESS_TYPE[moduleKey] ?? "";
+}
+
+/** 浠� TypeEnums 瑙f瀽鏈ā鍧� businessType锛涘凡閰嶇疆鏋氫妇鏃剁洿鎺ヨ繑鍥� */
+export function resolveModuleBusinessType(moduleKey, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return null;
+
+ const fixed = getModuleListBusinessType(moduleKey);
+ if (fixed != null && fixed !== "") return fixed;
+
+ const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+ const hitByLabel = (typeOptions || []).find((opt) => {
+ const optLabel = String(opt?.label || opt?.name || "").trim();
+ if (!optLabel) return false;
+ return labels.some(
+ (l) => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
+ );
+ });
+ if (hitByLabel?.value != null && hitByLabel.value !== "") return hitByLabel.value;
+
+ return cfg.approvalType || null;
+}
+
+/** 鍒楄〃/妯℃澘杩囨护鐢ㄧ殑 businessType 闆嗗悎 */
+export function getModuleMatchingBusinessTypes(moduleKey, typeOptions = []) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) return [];
+
+ const fixed = getModuleListBusinessType(moduleKey);
+ if (fixed != null && fixed !== "") return [fixed];
+
+ const values = new Set();
+ const primary = resolveModuleBusinessType(moduleKey, typeOptions);
+ if (primary != null && primary !== "") values.add(primary);
+
+ const labels = [cfg.label, ...(cfg.typeLabels || [])].filter(Boolean);
+ for (const opt of typeOptions || []) {
+ const optLabel = String(opt?.label || opt?.name || "").trim();
+ if (!optLabel) continue;
+ const matched = labels.some(
+ (l) => optLabel === l || optLabel.includes(l) || l.includes(optLabel)
+ );
+ if (matched && opt.value != null && opt.value !== "") {
+ values.add(opt.value);
+ }
+ }
+ return [...values];
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js
new file mode 100644
index 0000000..d68016f
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/approvalTemplateBindingUtils.js
@@ -0,0 +1,91 @@
+import {
+ mapAttachmentsFromApi,
+ mapTemplateFromApi,
+ unwrapTemplateDetail,
+} from "../approve-template/approveTemplateConstants.js";
+import { buildSubmitTemplateFromRow } from "../approve-template/formConfigUtils.js";
+import {
+ createEmptySubmitForm,
+ validateSubmitFlowNodes,
+} from "../approve-list/approveListConstants.js";
+
+export function attachmentDisplayName(file) {
+ return (
+ file?.fileName ||
+ file?.originalFilename ||
+ file?.name ||
+ file?.blobName ||
+ "闄勪欢"
+ );
+}
+
+/** 鎺ュ彛璇︽儏 鈫� 鎻愪氦缁戝畾蹇収锛堝惈娴佺▼銆侀檮浠躲�佸~鎶ラ」锛� */
+export function buildTemplateBindingFromDetail(detailRow) {
+ const mapped = mapTemplateFromApi(unwrapTemplateDetail(detailRow));
+ const templateAttachments = mapAttachmentsFromApi(mapped);
+ const tpl = {
+ ...buildSubmitTemplateFromRow(mapped),
+ templateId: mapped.id,
+ businessType: mapped.businessType,
+ storageBlobDTOs: templateAttachments,
+ };
+ const base = createEmptySubmitForm(String(mapped.id ?? ""), tpl, mapped.flowNodes);
+ return {
+ templateId: mapped.id,
+ templateName: mapped.templateName || tpl.label || "",
+ businessType: mapped.businessType ?? "",
+ templateSnapshot: tpl,
+ formFieldDefs: tpl.fields || [],
+ formPayload: base.formPayload,
+ flowNodes: base.flowNodes,
+ templateAttachments: JSON.parse(JSON.stringify(templateAttachments)),
+ storageBlobDTOs: [],
+ };
+}
+
+/** 鏍规嵁妯℃澘 fields 鐢熸垚 el-form rules锛坧rop 涓� formPayload.xxx锛� */
+export function buildFormPayloadRules(fields = []) {
+ const rules = {};
+ (fields || []).forEach((f) => {
+ if (!f.required || !f.key) return;
+ const prop = `formPayload.${f.key}`;
+ if (f.type === "number") {
+ rules[prop] = [{ required: true, message: `璇峰~鍐�${f.label}`, trigger: "blur" }];
+ } else if (f.type === "datetimerange" || f.type === "date" || f.type === "select") {
+ rules[prop] = [{ required: true, message: `璇烽�夋嫨${f.label}`, trigger: "change" }];
+ } else {
+ rules[prop] = [{ required: true, message: `璇峰~鍐�${f.label}`, trigger: "blur" }];
+ }
+ });
+ return rules;
+}
+
+/** 鏍¢獙妯℃澘缁戝畾锛氬鎵规祦绋嬶紙闄勪欢閫夊~锛岀敱鐢ㄦ埛鑷涓婁紶锛� */
+export function validateTemplateBinding({ flowNodes }) {
+ const flowCheck = validateSubmitFlowNodes(flowNodes);
+ if (!flowCheck.ok) return flowCheck;
+ return { ok: true, nodes: flowCheck.nodes };
+}
+
+/** 鍚堝苟缁戝畾缁撴灉鍒颁笟鍔¤〃鍗曞璞★紙瀛楁鍚嶅彲鎸変笟鍔¤鐩栵級 */
+export function applyBindingToForm(target, binding, fieldMap = {}) {
+ if (!target || !binding) return target;
+ const map = {
+ templateId: "templateId",
+ templateName: "templateName",
+ businessType: "businessType",
+ templateSnapshot: "templateSnapshot",
+ formFieldDefs: "formFieldDefs",
+ formPayload: "formPayload",
+ flowNodes: "flowNodes",
+ templateAttachments: "templateAttachments",
+ storageBlobDTOs: "storageBlobDTOs",
+ ...fieldMap,
+ };
+ Object.entries(map).forEach(([srcKey, destKey]) => {
+ if (binding[srcKey] !== undefined) {
+ target[destKey] = binding[srcKey];
+ }
+ });
+ return target;
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue
new file mode 100644
index 0000000..2488216
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceDetailDialog.vue
@@ -0,0 +1,123 @@
+<!-- 涓庡鎵瑰垪琛ㄨ鎯呭脊绐椾竴鑷� -->
+<template>
+ <el-dialog
+ v-model="visible"
+ :title="title"
+ width="920px"
+ append-to-body
+ destroy-on-close
+ class="approve-detail-dialog"
+ @closed="emit('closed')"
+ >
+ <div class="approve-detail-body">
+ <ApproveDetailPanel :row="row" />
+ <div class="detail-block">
+ <div class="detail-block-title">
+ 瀹℃壒娴佺▼锛坽{ row?.tasks?.length || row?.flowNodes?.length || 0 }} 椤癸級
+ </div>
+ <InstanceFlowDisplay :tasks="row?.tasks" :nodes="row?.flowNodes" />
+ </div>
+ <div class="detail-block">
+ <div class="detail-block-title">瀹℃壒璁板綍</div>
+ <el-timeline v-if="row?.approvalRecords?.length" class="approve-record-timeline">
+ <el-timeline-item
+ v-for="(rec, i) in row.approvalRecords"
+ :key="rec.id ?? i"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+ :timestamp="formatRecordTime(rec.time)"
+ placement="top"
+ >
+ <div class="record-item">
+ <span class="record-operator">{{ rec.operatorName || "鈥�" }}</span>
+ <el-tag
+ size="small"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'info'"
+ effect="plain"
+ >
+ {{ approvalActionLabel(rec.result) }}
+ </el-tag>
+ <p class="record-opinion">{{ rec.opinion || "鏃犳剰瑙�" }}</p>
+ </div>
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="48" />
+ </div>
+ </div>
+ <template #footer>
+ <el-button v-if="canEditRow(row)" @click="emit('edit', row)">淇� 鏀�</el-button>
+ <el-button @click="visible = false">鍏� 闂�</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { canEditBusinessInstanceRow } from "../../approve-list/approveListConstants.js";
+import { formatDisplayTime } from "../../approve-template/approveTemplateConstants.js";
+import ApproveDetailPanel from "../../approve-list/components/ApproveDetailPanel.vue";
+import InstanceFlowDisplay from "../../approve-list/components/InstanceFlowDisplay.vue";
+
+function canEditRow(row) {
+ return canEditBusinessInstanceRow(row);
+}
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ row: { type: Object, default: () => ({}) },
+ title: { type: String, default: "瀹℃壒璇︽儏" },
+});
+
+const emit = defineEmits(["update:modelValue", "edit", "closed"]);
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: (v) => emit("update:modelValue", v),
+});
+
+function approvalActionLabel(result) {
+ if (result === "approved") return "閫氳繃";
+ if (result === "rejected") return "椹冲洖";
+ return "寰呭鐞�";
+}
+
+function formatRecordTime(time) {
+ return formatDisplayTime(time) || "鈥�";
+}
+</script>
+
+<style scoped>
+.approve-detail-dialog :deep(.el-dialog__body) {
+ padding-top: 16px;
+ max-height: 70vh;
+ overflow-y: auto;
+}
+.approve-detail-body .detail-block {
+ margin-top: 20px;
+}
+.detail-block-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin: 0 0 12px;
+ padding-left: 10px;
+ border-left: 3px solid var(--el-color-primary);
+ line-height: 1.4;
+}
+.approve-record-timeline {
+ padding-left: 4px;
+}
+.record-item {
+ padding: 4px 0 2px;
+}
+.record-operator {
+ font-weight: 600;
+ margin-right: 8px;
+ color: var(--el-text-color-primary);
+}
+.record-opinion {
+ margin: 8px 0 0;
+ font-size: 13px;
+ color: var(--el-text-color-regular);
+ line-height: 1.5;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue
new file mode 100644
index 0000000..53de9e1
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalInstanceSubmitDialog.vue
@@ -0,0 +1,112 @@
+<!-- 涓庡鎵瑰垪琛ㄦ彁浜�/淇敼寮圭獥锛堢涓夋锛変竴鑷� -->
+<template>
+ <el-dialog
+ v-model="visible"
+ :title="title"
+ width="720px"
+ append-to-body
+ destroy-on-close
+ class="approve-submit-dialog"
+ @closed="emit('closed')"
+ >
+ <el-form ref="innerFormRef" :model="form" :rules="rules" label-width="120px">
+ <el-form-item v-if="isEdit" label="瀹℃壒绫诲瀷">
+ <span class="approve-type-cell" :style="approvalTypeStyle(activeTemplate?.approvalType)">
+ {{ activeTemplate?.label || form.templateName || "鈥�" }}
+ </span>
+ </el-form-item>
+ <slot name="before" :form="form" :fields="fields" />
+ <ApprovalTemplateFormSection
+ :active-template="activeTemplate"
+ :fields="fields"
+ :form-payload="form.formPayload"
+ v-model:flow-nodes="form.flowNodes"
+ v-model:attachments="form.storageBlobDTOs"
+ :template-attachments="form.templateAttachments"
+ :user-options="userOptions"
+ :show-template-name="!isEdit"
+ :allow-change-template="false"
+ :flow-attachments-only="flowAttachmentsOnly"
+ :flow-only="flowOnly"
+ />
+ <slot name="after" :form="form" :fields="fields" />
+ </el-form>
+ <template #footer>
+ <el-button type="primary" :loading="saving" @click="handleSubmitClick">
+ {{ isEdit ? "淇� 瀛�" : "鎻� 浜�" }}
+ </el-button>
+ <el-button @click="visible = false">鍙� 娑�</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+import { approvalTypeStyle } from "../../approve-list/approveListConstants.js";
+import ApprovalTemplateFormSection from "./ApprovalTemplateFormSection.vue";
+
+const innerFormRef = ref(null);
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ title: { type: String, default: "" },
+ form: { type: Object, required: true },
+ rules: { type: Object, default: () => ({}) },
+ fields: { type: Array, default: () => [] },
+ activeTemplate: { type: Object, default: null },
+ userOptions: { type: Array, default: () => [] },
+ isEdit: { type: Boolean, default: false },
+ saving: { type: Boolean, default: false },
+ formRef: { type: Object, default: null },
+ /** 濉姤椤圭敱 before 鎻掓Ы鍗曠嫭娓叉煋鏃惰涓� true */
+ flowAttachmentsOnly: { type: Boolean, default: false },
+ flowOnly: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["update:modelValue", "submit", "closed"]);
+
+const visible = computed({
+ get: () => props.modelValue,
+ set: (v) => emit("update:modelValue", v),
+});
+
+watch(
+ innerFormRef,
+ (el) => {
+ if (props.formRef) props.formRef.value = el;
+ },
+ { flush: "post" }
+);
+
+watch(visible, (v) => {
+ if (!v && props.formRef) props.formRef.value = null;
+});
+
+async function handleSubmitClick() {
+ if (!innerFormRef.value) {
+ ElMessage.warning("琛ㄥ崟鏈氨缁紝璇风◢鍚庡啀璇�");
+ return;
+ }
+ try {
+ await innerFormRef.value.validate();
+ } catch {
+ ElMessage.warning("璇峰畬鍠勮〃鍗曞繀濉」鍚庡啀淇濆瓨");
+ return;
+ }
+ emit("submit");
+}
+</script>
+
+<style scoped>
+.approve-type-cell {
+ display: inline-block;
+ padding: 2px 10px;
+ border-radius: 4px;
+ font-size: 13px;
+ line-height: 1.5;
+}
+.approve-submit-dialog :deep(.el-dialog__body) {
+ padding-top: 12px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue
new file mode 100644
index 0000000..409dd41
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue
@@ -0,0 +1,176 @@
+<!--
+ 涓氬姟妯″潡銆屾柊澧炪�嶆椂瀵煎叆瀹℃壒妯℃澘锛堝浐瀹� moduleKey锛屼粎灞曠ず璇ョ被鍨嬩笅妯℃澘锛�
+
+ 鐢ㄦ硶锛�
+ <ApprovalTemplateBindDialog
+ v-model:visible="visible"
+ module-key="regular"
+ @confirm="onTemplateBound"
+ />
+-->
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ :width="step === formStep ? 720 : 640"
+ append-to-body
+ class="approval-template-bind-dialog"
+ @closed="onClosed"
+ >
+ <template v-if="step === 1">
+ <div v-loading="templatesLoading || confirming">
+ <ApprovalTemplatePicker
+ :cards="templateCards"
+ :loading="false"
+ :hint="pickerHint"
+ @pick="onPickTemplate"
+ />
+ </div>
+ </template>
+
+ <template v-else>
+ <div v-loading="templatesLoading">
+ <el-form
+ ref="formRef"
+ :model="bindingForm"
+ :rules="mergedRules"
+ label-width="120px"
+ >
+ <ApprovalTemplateFormSection
+ :active-template="activeTemplate"
+ :fields="formFields"
+ :form-payload="bindingForm.formPayload"
+ v-model:flow-nodes="bindingForm.flowNodes"
+ v-model:attachments="bindingForm.storageBlobDTOs"
+ :template-attachments="bindingForm.templateAttachments"
+ :user-options="flowUserOptions"
+ allow-change-template
+ @change-template="step = 1"
+ />
+ </el-form>
+ </div>
+ </template>
+
+ <template #footer>
+ <el-button v-if="step === formStep" type="primary" :loading="confirming" @click="onConfirm">
+ 纭� 瀹�
+ </el-button>
+ <el-button v-if="step === formStep" @click="step = 1">閲嶉�夋ā鏉�</el-button>
+ <el-button @click="dialogVisible = false">{{ step === 1 ? "鍙� 娑�" : "鍏� 闂�" }}</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref, watch } from "vue";
+import { ElMessage } from "element-plus";
+import ApprovalTemplatePicker from "./ApprovalTemplatePicker.vue";
+import ApprovalTemplateFormSection from "./ApprovalTemplateFormSection.vue";
+import { useApprovalTemplateBinding } from "../useApprovalTemplateBinding.js";
+import { useFlowUserOptions } from "../useFlowUserOptions.js";
+import { getApprovalModuleConfig } from "../approvalModuleRegistry.js";
+
+const props = defineProps({
+ visible: { type: Boolean, default: false },
+ /** approvalModuleRegistry 涓殑 moduleKey */
+ moduleKey: { type: String, required: true },
+ /** 涓� true 鏃堕�夋ā鏉垮悗鐩存帴纭锛岃烦杩囥�岀‘璁ゅ鎵逛俊鎭�嶅~鎶ユ楠� */
+ skipFormConfirm: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["update:visible", "confirm", "closed"]);
+
+const dialogVisible = computed({
+ get: () => props.visible,
+ set: (v) => emit("update:visible", v),
+});
+
+const {
+ step,
+ bindingForm,
+ templateCards,
+ activeTemplate,
+ formFields,
+ formRules,
+ templatesLoading,
+ loadTemplates,
+ resetBinding,
+ pickTemplate,
+ validateBinding,
+ getBindingPayload,
+ moduleConfig,
+} = useApprovalTemplateBinding({ moduleKey: props.moduleKey, mode: "module" });
+
+const formStep = 2;
+const formRef = ref();
+const confirming = ref(false);
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
+
+const mergedRules = computed(() => ({ ...formRules.value }));
+
+const dialogTitle = computed(() => {
+ const label = moduleConfig.value?.label || "瀹℃壒";
+ return step.value === 1 ? `閫夋嫨${label}妯℃澘` : `纭${label}瀹℃壒淇℃伅`;
+});
+
+const pickerHint = computed(
+ () => `璇烽�夋嫨銆�${moduleConfig.value?.label || "鈥�"}銆嶇被鍨嬩笅宸插惎鐢ㄧ殑瀹℃壒妯℃澘锛屽鎵规祦绋嬪皢鑷姩甯﹀叆銆俙
+);
+
+watch(
+ () => props.visible,
+ async (v) => {
+ if (!v) return;
+ resetBinding();
+ step.value = 1;
+ await Promise.all([loadTemplates(), loadFlowUsers()]);
+ const cfg = getApprovalModuleConfig(props.moduleKey);
+ if (!cfg) {
+ ElMessage.warning(`鏈厤缃ā鍧椼��${props.moduleKey}銆嶏紝璇锋鏌� approvalModuleRegistry`);
+ return;
+ }
+ if (!templateCards.value.length) {
+ ElMessage.warning(
+ `銆�${cfg.label}銆嶄笅鏆傛棤宸插惎鐢ㄧ殑瀹℃壒妯℃澘锛岃鍏堝湪瀹℃壒妯℃澘绠$悊涓垱寤哄苟鍚敤瀵瑰簲绫诲瀷鐨勬ā鏉縛
+ );
+ }
+ }
+);
+
+async function onPickTemplate(card) {
+ const ok = await pickTemplate(card);
+ if (!ok) return;
+ if (props.skipFormConfirm) {
+ step.value = 1;
+ await onConfirm();
+ return;
+ }
+ step.value = formStep;
+}
+
+async function onConfirm() {
+ confirming.value = true;
+ try {
+ const check = await validateBinding(props.skipFormConfirm ? null : formRef.value);
+ if (!check.ok) {
+ if (check.message) ElMessage.warning(check.message);
+ return;
+ }
+ emit("confirm", { ...getBindingPayload(), flowNodes: check.nodes });
+ dialogVisible.value = false;
+ } finally {
+ confirming.value = false;
+ }
+}
+
+function onClosed() {
+ resetBinding();
+ emit("closed");
+}
+</script>
+
+<style scoped>
+.approval-template-bind-dialog :deep(.el-dialog__body) {
+ padding-top: 12px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
new file mode 100644
index 0000000..d6e7073
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplateFormSection.vue
@@ -0,0 +1,122 @@
+<!-- 妯℃澘缁戝畾琛ㄥ崟鍖猴細濉姤椤� + 瀹℃壒娴佺▼ + 闄勪欢锛堥』鎸傚湪澶栧眰 el-form 涓嬶級 -->
+<template>
+ <template v-if="activeTemplate">
+ <el-form-item
+ v-if="showTemplateName && !hideTemplateName && !flowAttachmentsOnly && !flowOnly"
+ label="瀹℃壒妯℃澘"
+ >
+ <span class="template-name">{{ activeTemplate.label }}</span>
+ <el-button v-if="allowChangeTemplate" type="primary" link class="ml12" @click="emit('change-template')">
+ 鏇存崲妯℃澘
+ </el-button>
+ </el-form-item>
+
+ <FormPayloadFields
+ v-if="!hideFormFields && !flowAttachmentsOnly && !flowOnly"
+ :fields="fields"
+ :form-payload="formPayload"
+ />
+
+ <el-form-item label="瀹℃壒娴佺▼" required>
+ <TemplateFlowEditor
+ v-model="flowNodesModel"
+ :user-options="userOptions"
+ :readonly="!flowEditable"
+ />
+ <p class="section-tip">
+ {{
+ flowEditable
+ ? "娴佺▼涓庡鎵逛汉鐢辨ā鏉块缃紝鍙寜闇�寰皟鑺傜偣瀹℃壒浜恒��"
+ : "娴佺▼涓庡鎵逛汉鐢辨墍閫夋ā鏉垮浐瀹氾紝涓嶅彲淇敼銆�"
+ }}
+ </p>
+ </el-form-item>
+
+ <el-form-item v-if="!flowOnly && templateAttachments.length" label="妯℃澘鍙傝��">
+ <el-tag
+ v-for="(f, i) in templateAttachments"
+ :key="`tpl-${i}`"
+ class="attachment-tag"
+ type="info"
+ effect="plain"
+ >
+ {{ attachmentDisplayName(f) }}
+ </el-tag>
+ <p class="section-tip">浠ヤ笂涓烘ā鏉块檮甯︽枃浠讹紝浠呬緵鍙傝�冿紱鎻愪氦闄勪欢璇峰湪涓嬫柟涓婁紶銆�</p>
+ </el-form-item>
+
+ <el-form-item v-if="!flowOnly" label="闄勪欢">
+ <FileUpload
+ v-model:file-list="attachmentsModel"
+ :limit="uploadLimit"
+ button-text="鐐瑰嚮閫夋嫨鏂囦欢"
+ />
+ <p class="section-tip">閫夊~锛屽彲涓婁紶涓庣敵璇风浉鍏崇殑璇存槑鏉愭枡銆�</p>
+ </el-form-item>
+ </template>
+ <el-empty v-else description="璇峰厛閫夋嫨瀹℃壒妯℃澘" :image-size="64" />
+</template>
+
+<script setup>
+import { computed } from "vue";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import TemplateFlowEditor from "../../approve-template/components/TemplateFlowEditor.vue";
+import FormPayloadFields from "../../approve-list/components/FormPayloadFields.vue";
+import { attachmentDisplayName } from "../approvalTemplateBindingUtils.js";
+
+const props = defineProps({
+ activeTemplate: { type: Object, default: null },
+ fields: { type: Array, default: () => [] },
+ formPayload: { type: Object, required: true },
+ flowNodes: { type: Array, default: () => [] },
+ /** 鐢ㄦ埛鑷涓婁紶鐨勯檮浠� */
+ attachments: { type: Array, default: () => [] },
+ /** 妯℃澘棰勭疆闄勪欢锛堝彧璇诲睍绀猴級 */
+ templateAttachments: { type: Array, default: () => [] },
+ userOptions: { type: Array, default: () => [] },
+ showTemplateName: { type: Boolean, default: true },
+ allowChangeTemplate: { type: Boolean, default: true },
+ /** 涓� true 鏃朵笉灞曠ず妯℃澘鑷畾涔夊~鎶ラ」锛堜粎淇濈暀瀹℃壒娴佺▼涓庨檮浠讹級 */
+ hideFormFields: { type: Boolean, default: false },
+ /** 涓� true 鏃朵笉灞曠ず瀹℃壒妯℃澘鍚嶇О琛岋紙鐢辩埗绾х疆椤跺睍绀猴級 */
+ hideTemplateName: { type: Boolean, default: false },
+ /** 涓� true 鏃朵粎灞曠ず瀹℃壒娴佺▼涓庨檮浠讹紙濉姤椤圭敱鐖剁骇鍗曠嫭娓叉煋锛� */
+ flowAttachmentsOnly: { type: Boolean, default: false },
+ /** 涓� true 鏃朵粎灞曠ず瀹℃壒娴佺▼锛堜笉灞曠ず妯℃澘濉姤椤广�侀檮浠剁瓑锛� */
+ flowOnly: { type: Boolean, default: false },
+ uploadLimit: { type: Number, default: 10 },
+ /** 涓� true 鏃跺彲缂栬緫妯℃澘棰勭疆鐨勫鎵逛汉锛堜粎瀹℃壒妯℃澘绠$悊椤典娇鐢級 */
+ flowEditable: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["update:flowNodes", "update:attachments", "change-template"]);
+
+const flowNodesModel = computed({
+ get: () => props.flowNodes,
+ set: (v) => emit("update:flowNodes", v),
+});
+
+const attachmentsModel = computed({
+ get: () => props.attachments,
+ set: (v) => emit("update:attachments", v),
+});
+</script>
+
+<style scoped>
+.template-name {
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+.ml12 {
+ margin-left: 12px;
+}
+.section-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin: 8px 0 0;
+ line-height: 1.5;
+}
+.attachment-tag {
+ margin: 0 8px 8px 0;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue
new file mode 100644
index 0000000..8adfebc
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/components/ApprovalTemplatePicker.vue
@@ -0,0 +1,85 @@
+<!-- 瀹℃壒妯℃澘鍗$墖閫夋嫨锛堟寜 businessType 杩囨护锛� -->
+<template>
+ <div class="approval-template-picker">
+ <p v-if="hint" class="picker-hint">{{ hint }}</p>
+ <div v-loading="loading" class="template-grid">
+ <div
+ v-for="card in cards"
+ :key="card.key || card.id"
+ class="template-card"
+ @click="emit('pick', card)"
+ >
+ <span class="template-card-type" :style="typeStyle(card.approvalType)">
+ {{ card.label }}
+ </span>
+ <span class="template-card-desc">{{ card.summaryPlaceholder }}</span>
+ </div>
+ <el-empty
+ v-if="!loading && !cards.length"
+ :description="emptyText"
+ :image-size="80"
+ class="template-empty"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { approvalTypeStyle } from "../../approve-list/approveListConstants.js";
+
+defineProps({
+ cards: { type: Array, default: () => [] },
+ loading: { type: Boolean, default: false },
+ hint: { type: String, default: "" },
+ emptyText: { type: String, default: "璇ョ被鍨嬩笅鏆傛棤鍙敤瀹℃壒妯℃澘" },
+});
+
+const emit = defineEmits(["pick"]);
+
+function typeStyle(approvalType) {
+ return approvalTypeStyle(approvalType);
+}
+</script>
+
+<style scoped>
+.picker-hint {
+ font-size: 13px;
+ color: var(--el-text-color-secondary);
+ margin: 0 0 16px;
+}
+.template-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 12px;
+ min-height: 120px;
+}
+.template-empty {
+ grid-column: 1 / -1;
+}
+.template-card {
+ padding: 14px 16px;
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: var(--radius-md, 8px);
+ cursor: pointer;
+ transition: border-color 0.2s, box-shadow 0.2s;
+ background: var(--el-fill-color-blank);
+}
+.template-card:hover {
+ border-color: var(--el-color-primary);
+ box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.06));
+}
+.template-card-type {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+.template-card-desc {
+ display: block;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ line-height: 1.5;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js
new file mode 100644
index 0000000..b474bb2
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalInstanceModule.js
@@ -0,0 +1,408 @@
+import {
+ deleteApprovalInstance,
+ listApprovalInstancePage,
+ saveApprovalInstance,
+ updateApprovalInstance,
+} from "@/api/officeProcessAutomation/approvalInstance.js";
+import useUserStore from "@/store/modules/user";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, reactive, ref } from "vue";
+import {
+ applyBindingToForm,
+ buildFormPayloadRules,
+ validateTemplateBinding,
+} from "./approvalTemplateBindingUtils.js";
+import {
+ buildApprovalInstanceListParams,
+ buildEditFormFromInstanceRow,
+ buildInstanceDto,
+ canEditBusinessInstanceRow,
+ createEmptySubmitForm,
+ mapInstanceFromApi,
+ resolveInstanceFormFields,
+ unwrapInstancePage,
+} from "../approve-list/approveListConstants.js";
+import { fetchBusinessTypeOptions } from "../approve-template/approveTemplateConstants.js";
+import {
+ collectOptionSourcesFromFields,
+ fetchSelectOptionCaches,
+} from "../approve-template/selectOptionSource.js";
+import { enrichInstanceRowFromFormConfig } from "./approvalInstanceFormConfigTable.js";
+import {
+ filterInstanceRowsByModuleSearch,
+ hasActiveModuleSearch,
+} from "./approvalInstanceListSearch.js";
+import {
+ getApprovalModuleConfig,
+ getModuleListBusinessType,
+ resolveModuleBusinessType,
+} from "./approvalModuleRegistry.js";
+
+/**
+ * 涓氬姟鐢宠椤靛叡鐢細瀹℃壒瀹炰緥鍒楄〃鏌ヨ銆佹柊澧�/淇敼淇濆瓨銆佽鎯�/缂栬緫寮圭獥锛堜笌瀹℃壒鍒楄〃涓�鑷达級
+ *
+ * @param {object} options
+ * @param {string} options.moduleKey approvalModuleRegistry 涓殑 key
+ * @param {(row: object) => object} [options.enrichListRow] 鍒楄〃琛屽寮猴紙浠� formPayload 瑙f瀽灞曠ず瀛楁锛�
+ * @param {(base: object) => object} [options.buildExtraListParams] 杩藉姞鏌ヨ鍙傛暟
+ * @param {() => void} [options.beforeSave] 淇濆瓨鍓嶉挬瀛愶紙濡傚悓姝ヤ笟鍔″瓧娈靛埌 formPayload锛�
+ * @param {import('vue').ComputedRef|object} [options.extraFormRules] 棰濆琛ㄥ崟鏍¢獙
+ */
+export function useApprovalInstanceModule(options = {}) {
+ const {
+ moduleKey,
+ enrichListRow,
+ buildExtraListParams,
+ beforeSave,
+ extraFormRules,
+ } = options;
+
+ const userStore = useUserStore();
+ const moduleConfig = computed(() => getApprovalModuleConfig(moduleKey));
+ const businessTypeOptions = ref([]);
+
+ /** 鍒楄〃鏌ヨ businessType锛氫紭鍏� registry 鍐欐鏋氫妇锛屽啀鍥為�� TypeEnums */
+ const defaultListBusinessType = computed(() => {
+ const fixed = getModuleListBusinessType(moduleKey);
+ if (fixed != null && fixed !== "") return fixed;
+ const resolved = resolveModuleBusinessType(moduleKey, businessTypeOptions.value);
+ if (resolved != null && resolved !== "") return resolved;
+ return "";
+ });
+
+ async function loadBusinessTypeOptions() {
+ if (businessTypeOptions.value.length) return;
+ try {
+ businessTypeOptions.value = await fetchBusinessTypeOptions();
+ } catch {
+ businessTypeOptions.value = [];
+ }
+ }
+
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+
+ const detailDialog = reactive({ visible: false });
+ const detailRow = ref({});
+
+ const submitDialog = reactive({ visible: false, mode: "add" });
+ const submitEditRow = ref(null);
+ const submitForm = reactive(createEmptySubmitForm(""));
+ const submitFormRef = ref();
+ const submitSaving = ref(false);
+
+ const templateBindVisible = ref(false);
+ const pendingTemplateBinding = ref(null);
+ /** 鏈�杩戜竴娆″垪琛ㄦ煡璇㈡潯浠讹紙淇濆瓨鍚庡埛鏂板垪琛ㄦ椂娌跨敤锛� */
+ let lastListSearchForm = null;
+
+ const isSubmitEdit = computed(() => submitDialog.mode === "edit");
+ const activeTemplate = computed(() => submitForm.templateSnapshot || null);
+ const submitFormFields = computed(() => {
+ const tplFields = activeTemplate.value?.fields;
+ if (tplFields?.length) return tplFields;
+ return submitForm.formFieldDefs || [];
+ });
+
+ const submitFormRules = computed(() => ({
+ ...buildFormPayloadRules(submitFormFields.value),
+ ...(extraFormRules?.value ?? extraFormRules ?? {}),
+ }));
+
+ const submitDialogTitle = computed(() => {
+ const label = moduleConfig.value?.label || "鐢宠";
+ if (submitDialog.mode === "edit") {
+ return `淇敼${activeTemplate.value?.label || submitForm.templateName || label}`;
+ }
+ return `鏂板${label}`;
+ });
+
+ function mapListRow(row, caches) {
+ const mapped = mapInstanceFromApi(row);
+ const fromFormConfig = enrichInstanceRowFromFormConfig(mapped, caches);
+ return enrichListRow ? enrichListRow(fromFormConfig) : fromFormConfig;
+ }
+
+ async function fetchList(searchForm = {}) {
+ await loadBusinessTypeOptions();
+ tableLoading.value = true;
+ try {
+ let extraParams = {};
+ if (buildExtraListParams) {
+ extraParams = buildExtraListParams(searchForm) || {};
+ }
+ const res = await listApprovalInstancePage(
+ buildApprovalInstanceListParams({
+ page,
+ searchForm,
+ businessType: defaultListBusinessType.value,
+ extraParams,
+ })
+ );
+ const { records, total } = unwrapInstancePage(res);
+ const mapped = records.map(mapInstanceFromApi);
+ const allFields = [];
+ for (const row of mapped) {
+ const { fields } = resolveInstanceFormFields(row);
+ allFields.push(...fields);
+ }
+ const caches = await fetchSelectOptionCaches(
+ collectOptionSourcesFromFields(allFields)
+ );
+ let rows = mapped.map((row) => mapListRow(row, caches));
+ if (hasActiveModuleSearch(moduleKey, searchForm)) {
+ rows = filterInstanceRowsByModuleSearch(moduleKey, rows, searchForm);
+ }
+ tableData.value = rows;
+ page.total = hasActiveModuleSearch(moduleKey, searchForm)
+ ? rows.length
+ : total;
+ } catch {
+ tableData.value = [];
+ page.total = 0;
+ ElMessage.error(`${moduleConfig.value?.label || "鐢宠"}鍒楄〃鍔犺浇澶辫触`);
+ } finally {
+ tableLoading.value = false;
+ }
+ }
+
+ function handleQuery(searchForm) {
+ lastListSearchForm = searchForm;
+ page.current = 1;
+ return fetchList(searchForm);
+ }
+
+ /** 杩涘叆椤甸潰锛氬厛鎷� TypeEnums 瑙f瀽 businessType锛屽啀鏌ュ垪琛� */
+ async function initModuleList(searchForm) {
+ await loadBusinessTypeOptions();
+ return handleQuery(searchForm);
+ }
+
+ function pagination({ page: p, limit }, searchForm) {
+ page.current = p;
+ page.size = limit;
+ return fetchList(searchForm);
+ }
+
+ function openDetail(row) {
+ detailRow.value = { ...row };
+ detailDialog.visible = true;
+ }
+
+ function openEdit(row) {
+ if (!canEditBusinessInstanceRow(row)) {
+ ElMessage.warning("杩涜涓垨宸插畬鎴愮殑瀹℃壒涓嶅彲淇敼");
+ return;
+ }
+ if (!row?.id) {
+ ElMessage.warning("鏃犳硶淇敼锛氱己灏戝鎵瑰疄渚� ID");
+ return;
+ }
+ submitDialog.mode = "edit";
+ submitEditRow.value = { ...row };
+ Object.assign(submitForm, buildEditFormFromInstanceRow(row));
+ submitDialog.visible = true;
+ }
+
+ function openEditFromDetail() {
+ const row = detailRow.value;
+ detailDialog.visible = false;
+ openEdit(row);
+ }
+
+ function resetSubmitForm() {
+ Object.assign(submitForm, createEmptySubmitForm(""));
+ submitEditRow.value = null;
+ }
+
+ function openAddWithTemplate() {
+ submitDialog.visible = false;
+ pendingTemplateBinding.value = null;
+ templateBindVisible.value = true;
+ }
+
+ function onTemplateBound(binding) {
+ pendingTemplateBinding.value = binding;
+ }
+
+ function onTemplateBindClosed() {
+ const binding = pendingTemplateBinding.value;
+ if (!binding) return;
+ pendingTemplateBinding.value = null;
+ openAddFromBinding(binding);
+ }
+
+ function openAddFromBinding(binding) {
+ resetSubmitForm();
+ applyBindingToForm(submitForm, binding);
+ submitDialog.mode = "add";
+ submitEditRow.value = null;
+ submitDialog.visible = true;
+ }
+
+ function closeSubmitDialog() {
+ submitDialog.visible = false;
+ }
+
+ async function submitInstanceForm(options = {}) {
+ const { skipValidate = false } = options;
+ if (!skipValidate) {
+ if (!submitFormRef.value?.validate) {
+ ElMessage.warning("琛ㄥ崟鏈氨缁紝璇峰叧闂脊绐楀悗閲嶈瘯");
+ return false;
+ }
+ try {
+ await submitFormRef.value.validate();
+ } catch {
+ ElMessage.warning("璇峰畬鍠勮〃鍗曞繀濉」鍚庡啀淇濆瓨");
+ return false;
+ }
+ }
+ if (!activeTemplate.value) {
+ ElMessage.warning("鏈姞杞藉鎵规ā鏉匡紝鏃犳硶淇濆瓨");
+ return false;
+ }
+ const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+ if (!bindingCheck.ok) {
+ ElMessage.warning(bindingCheck.message);
+ return false;
+ }
+ if (!submitForm.templateId) {
+ ElMessage.warning("缂哄皯妯℃澘 ID锛屾棤娉曟彁浜�");
+ return false;
+ }
+ if (beforeSave) {
+ try {
+ await beforeSave(submitForm, { isEdit: isSubmitEdit.value, editRow: submitEditRow.value });
+ } catch {
+ return false;
+ }
+ }
+ if (submitSaving.value) return false;
+ submitSaving.value = true;
+ try {
+ const dto = buildInstanceDto({
+ submitForm,
+ activeTemplate: activeTemplate.value,
+ userStore,
+ flowNodes: bindingCheck.nodes,
+ existingRow: isSubmitEdit.value ? submitEditRow.value : null,
+ });
+ if (isSubmitEdit.value) {
+ await updateApprovalInstance(dto);
+ } else {
+ await saveApprovalInstance(dto);
+ }
+ submitDialog.visible = false;
+ if (!isSubmitEdit.value) page.current = 1;
+ await fetchList(lastListSearchForm ?? {});
+ if (detailDialog.visible && detailRow.value?.id === submitForm.instanceId) {
+ const hit = tableData.value.find((r) => r.id === submitForm.instanceId);
+ if (hit) detailRow.value = { ...hit };
+ else detailDialog.visible = false;
+ }
+ return true;
+ } catch {
+ ElMessage.error(isSubmitEdit.value ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触");
+ return false;
+ } finally {
+ submitSaving.value = false;
+ }
+ }
+
+ async function removeInstance(row) {
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戝鎵瑰疄渚� ID");
+ return;
+ }
+ const title = row.title || row.templateName || row.instanceNo || "璇ュ鎵�";
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゅ鎵广��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
+ } catch {
+ return;
+ }
+ try {
+ await deleteApprovalInstance([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ if (detailDialog.visible && detailRow.value?.id === row.id) {
+ detailDialog.visible = false;
+ }
+ if (submitDialog.visible && submitEditRow.value?.id === row.id) {
+ submitDialog.visible = false;
+ }
+ await fetchList(lastListSearchForm ?? {});
+ } catch {
+ /* 閿欒鐢辨嫤鎴櫒鎻愮ず */
+ }
+ }
+
+ /** 鏋勫缓鏍囧噯鎿嶄綔鍒楋細璇︽儏銆佷慨鏀广�佸垹闄わ紙涓庡鎵瑰垪琛ㄤ竴鑷达級 */
+ function buildTableActions(extraOperations = []) {
+ return [
+ { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+ {
+ name: "淇敼",
+ type: "text",
+ disabled: (row) => !canEditBusinessInstanceRow(row),
+ clickFun: (row) => openEdit(row),
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ clickFun: (row) => removeInstance(row),
+ },
+ ...extraOperations,
+ ];
+ }
+
+ return {
+ moduleConfig,
+ defaultListBusinessType,
+ tableData,
+ tableLoading,
+ page,
+ detailDialog,
+ detailRow,
+ submitDialog,
+ submitEditRow,
+ submitForm,
+ submitFormRef,
+ submitSaving,
+ isSubmitEdit,
+ activeTemplate,
+ submitFormFields,
+ submitFormRules,
+ submitDialogTitle,
+ templateBindVisible,
+ pendingTemplateBinding,
+ fetchList,
+ handleQuery,
+ initModuleList,
+ pagination,
+ openDetail,
+ openEdit,
+ openEditFromDetail,
+ openAddWithTemplate,
+ onTemplateBound,
+ onTemplateBindClosed,
+ openAddFromBinding,
+ closeSubmitDialog,
+ resetSubmitForm,
+ submitInstanceForm,
+ removeInstance,
+ buildTableActions,
+ loadBusinessTypeOptions,
+ canEditBusinessInstanceRow,
+ };
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js
new file mode 100644
index 0000000..d49ec53
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useApprovalTemplateBinding.js
@@ -0,0 +1,259 @@
+import {
+ getApprovalTemplateDetail,
+ listApprovalTemplate,
+ TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { computed, reactive, ref } from "vue";
+import { ElMessage } from "element-plus";
+import {
+ fetchBusinessTypeOptions,
+ mapEnabledFromApi,
+ unwrapTemplateList,
+} from "../approve-template/approveTemplateConstants.js";
+import {
+ createEmptySubmitForm,
+ mapSubmitTemplateCard,
+ matchBusinessTypeValue,
+} from "../approve-list/approveListConstants.js";
+import {
+ getApprovalModuleConfig,
+ getModuleMatchingBusinessTypes,
+ resolveModuleBusinessType,
+} from "./approvalModuleRegistry.js";
+import {
+ buildFormPayloadRules,
+ buildTemplateBindingFromDetail,
+ validateTemplateBinding,
+} from "./approvalTemplateBindingUtils.js";
+
+/**
+ * 瀹℃壒妯℃澘缁戝畾锛堜笟鍔℃ā鍧楀浐瀹氱被鍨� / 瀹℃壒鍒楄〃閫氱敤锛�
+ *
+ * @param {object} options
+ * @param {string} [options.moduleKey] 涓氬姟妯″潡 key锛岃 approvalModuleRegistry
+ * @param {string|number} [options.businessType] 鐩存帴鎸囧畾绫诲瀷锛堜紭鍏堢骇楂樹簬 moduleKey锛�
+ * @param {'module'|'universal'} [options.mode] module=浠呮湰绫诲瀷妯℃澘锛泆niversal=闇�鍏堥�夌被鍨�
+ */
+export function useApprovalTemplateBinding(options = {}) {
+ const { moduleKey = null, businessType: fixedBusinessType = null, mode = moduleKey ? "module" : "universal" } =
+ options;
+
+ const isUniversal = mode === "universal" && !moduleKey && fixedBusinessType == null;
+
+ const allTemplates = ref([]);
+ const businessTypeOptions = ref([]);
+ const selectedBusinessType = ref(fixedBusinessType ?? "");
+ const templatesLoading = ref(false);
+ const step = ref(isUniversal ? 1 : 1);
+
+ const bindingForm = reactive(createEmptySubmitForm(""));
+
+ const moduleConfig = computed(() => getApprovalModuleConfig(moduleKey));
+
+ const resolvedBusinessType = computed(() => {
+ if (fixedBusinessType != null && fixedBusinessType !== "") return fixedBusinessType;
+ if (selectedBusinessType.value != null && selectedBusinessType.value !== "") {
+ return selectedBusinessType.value;
+ }
+ if (moduleKey) {
+ return resolveModuleBusinessType(moduleKey, businessTypeOptions.value);
+ }
+ return "";
+ });
+
+ const matchingBusinessTypes = computed(() => {
+ if (fixedBusinessType != null && fixedBusinessType !== "") return [fixedBusinessType];
+ if (isUniversal) {
+ const t = selectedBusinessType.value;
+ return t != null && t !== "" ? [t] : [];
+ }
+ if (moduleKey) {
+ return getModuleMatchingBusinessTypes(moduleKey, businessTypeOptions.value);
+ }
+ const t = resolvedBusinessType.value;
+ return t != null && t !== "" ? [t] : [];
+ });
+
+ const templateCards = computed(() => {
+ const types = matchingBusinessTypes.value;
+ if (!types.length) return [];
+ return allTemplates.value.filter((card) =>
+ types.some(
+ (t) =>
+ matchBusinessTypeValue(card.businessType, t) ||
+ matchBusinessTypeValue(card.approvalType, t)
+ )
+ );
+ });
+
+ const activeTemplate = computed(() => bindingForm.templateSnapshot || null);
+
+ const formFields = computed(() => {
+ const tplFields = activeTemplate.value?.fields;
+ if (tplFields?.length) return tplFields;
+ return bindingForm.formFieldDefs || [];
+ });
+
+ const formRules = computed(() => buildFormPayloadRules(formFields.value));
+
+ const hasTemplateBound = computed(() => Boolean(activeTemplate.value?.templateId || bindingForm.templateId));
+
+ function businessTypeLabel(type) {
+ if (type == null || type === "") return "";
+ const hit = businessTypeOptions.value.find((x) => matchBusinessTypeValue(x.value, type));
+ return hit?.label || moduleConfig.value?.label || "";
+ }
+
+ const selectedBusinessTypeLabel = computed(() => businessTypeLabel(resolvedBusinessType.value));
+
+ function countTemplatesByBusinessType(type) {
+ const types =
+ moduleKey && !fixedBusinessType
+ ? getModuleMatchingBusinessTypes(moduleKey, businessTypeOptions.value)
+ : [type];
+ return allTemplates.value.filter((card) =>
+ types.some(
+ (t) =>
+ matchBusinessTypeValue(card.businessType, t) ||
+ matchBusinessTypeValue(card.approvalType, t)
+ )
+ ).length;
+ }
+
+ async function loadTemplates() {
+ templatesLoading.value = true;
+ try {
+ const [typeOptions, customRes] = await Promise.all([
+ fetchBusinessTypeOptions(),
+ listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
+ ]);
+ businessTypeOptions.value = typeOptions;
+ allTemplates.value = unwrapTemplateList(customRes)
+ .filter((row) => mapEnabledFromApi(row.enabled))
+ .map(mapSubmitTemplateCard);
+
+ if (moduleKey && !fixedBusinessType) {
+ const resolved = resolveModuleBusinessType(moduleKey, typeOptions);
+ if (resolved != null && resolved !== "") selectedBusinessType.value = resolved;
+ }
+ } catch {
+ businessTypeOptions.value = [];
+ allTemplates.value = [];
+ ElMessage.error("鍔犺浇瀹℃壒妯℃澘澶辫触");
+ } finally {
+ templatesLoading.value = false;
+ }
+ }
+
+ function resetBinding() {
+ step.value = isUniversal ? 1 : 1;
+ if (!fixedBusinessType && !moduleKey) selectedBusinessType.value = "";
+ else if (moduleKey) {
+ selectedBusinessType.value =
+ fixedBusinessType ?? resolveModuleBusinessType(moduleKey, businessTypeOptions.value) ?? "";
+ }
+ Object.assign(bindingForm, createEmptySubmitForm(""));
+ }
+
+ function pickBusinessType(type) {
+ if (!countTemplatesByBusinessType(type)) {
+ ElMessage.warning("璇ョ被鍨嬩笅鏆傛棤鍙敤瀹℃壒妯℃澘");
+ return;
+ }
+ selectedBusinessType.value = type;
+ step.value = 2;
+ }
+
+ function backToBusinessTypePick() {
+ selectedBusinessType.value = "";
+ step.value = 1;
+ }
+
+ function backToTemplatePick() {
+ step.value = isUniversal ? 2 : 1;
+ }
+
+ async function pickTemplate(card) {
+ if (!card?.id) return false;
+ templatesLoading.value = true;
+ try {
+ const res = await getApprovalTemplateDetail(card.id);
+ const applied = buildTemplateBindingFromDetail(res);
+ Object.assign(bindingForm, {
+ templateKey: String(card.id),
+ ...applied,
+ });
+ step.value = isUniversal ? 3 : 2;
+ return true;
+ } catch {
+ ElMessage.error("鍔犺浇妯℃澘璇︽儏澶辫触");
+ return false;
+ } finally {
+ templatesLoading.value = false;
+ }
+ }
+
+ /** 鐩存帴浠ヨ鎯呰缁戝畾锛堢紪杈戝洖鏄撅級 */
+ function applyBindingState(state) {
+ if (!state) return;
+ Object.assign(bindingForm, createEmptySubmitForm(""), state);
+ step.value = isUniversal ? 3 : 2;
+ }
+
+ async function validateBinding(formRef) {
+ if (formRef?.validate) {
+ try {
+ await formRef.validate();
+ } catch {
+ return { ok: false };
+ }
+ }
+ if (!hasTemplateBound.value) {
+ return { ok: false, message: "璇烽�夋嫨瀹℃壒妯℃澘" };
+ }
+ return validateTemplateBinding({ flowNodes: bindingForm.flowNodes });
+ }
+
+ function getBindingPayload() {
+ return {
+ templateId: bindingForm.templateId,
+ templateName: bindingForm.templateName,
+ businessType: bindingForm.businessType ?? resolvedBusinessType.value,
+ templateSnapshot: bindingForm.templateSnapshot,
+ formFieldDefs: bindingForm.formFieldDefs,
+ formPayload: bindingForm.formPayload,
+ flowNodes: bindingForm.flowNodes,
+ templateAttachments: bindingForm.templateAttachments,
+ storageBlobDTOs: bindingForm.storageBlobDTOs,
+ };
+ }
+
+ return {
+ isUniversal,
+ moduleConfig,
+ step,
+ bindingForm,
+ allTemplates,
+ businessTypeOptions,
+ selectedBusinessType,
+ resolvedBusinessType,
+ selectedBusinessTypeLabel,
+ templateCards,
+ activeTemplate,
+ formFields,
+ formRules,
+ hasTemplateBound,
+ templatesLoading,
+ loadTemplates,
+ resetBinding,
+ pickBusinessType,
+ backToBusinessTypePick,
+ backToTemplatePick,
+ pickTemplate,
+ applyBindingState,
+ validateBinding,
+ getBindingPayload,
+ countTemplatesByBusinessType,
+ businessTypeLabel,
+ };
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js
new file mode 100644
index 0000000..2788ac7
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-shared/useFlowUserOptions.js
@@ -0,0 +1,35 @@
+import { ref } from "vue";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+/** 瀹℃壒娴佺▼閫変汉涓嬫媺锛堟ā鏉�/瀹炰緥鍏辩敤锛� */
+export function useFlowUserOptions() {
+ const flowUserOptions = ref([]);
+ const loading = ref(false);
+
+ async function loadFlowUsers() {
+ loading.value = true;
+ try {
+ const res = await userListNoPageByTenantId();
+ flowUserOptions.value = unwrapArray(res).filter(isActiveUser);
+ } catch {
+ flowUserOptions.value = [];
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ return { flowUserOptions, loading, loadFlowUsers };
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
new file mode 100644
index 0000000..727f896
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/approveTemplateConstants.js
@@ -0,0 +1,355 @@
+import dayjs from "dayjs";
+import { getTypeEnums } from "@/api/basicData/enum.js";
+import {
+ TEMPLATE_TYPE_BUILTIN,
+ TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { APPROVAL_TYPE_OPTIONS } from "../approve-list/approveListConstants.js";
+import {
+ buildFormConfigJson,
+ createEmptyFormConfigData,
+ parseFormConfigToData,
+ validateFormConfigData,
+} from "./formConfigUtils.js";
+
+export function unwrapEnumList(data) {
+ if (Array.isArray(data)) return data;
+ if (!data || typeof data !== "object") return [];
+ if (Array.isArray(data.TypeEnums)) return data.TypeEnums;
+ if (Array.isArray(data.typeEnums)) return data.typeEnums;
+ const nested = Object.values(data).find((v) => Array.isArray(v));
+ return nested || [];
+}
+
+export function normalizeBusinessTypeOptions(data) {
+ return unwrapEnumList(data)
+ .map((item) => {
+ const rawValue = item?.value ?? item?.code ?? item?.businessType ?? item?.dictValue ?? item?.key;
+ if (rawValue == null || rawValue === "") return null;
+ const num = Number(rawValue);
+ const value =
+ typeof rawValue === "number" || (Number.isFinite(num) && String(rawValue).trim() !== "")
+ ? num
+ : rawValue;
+ const label =
+ item?.label ?? item?.name ?? item?.desc ?? item?.dictLabel ?? item?.text ?? String(value);
+ return { label, value };
+ })
+ .filter(Boolean);
+}
+
+export async function fetchBusinessTypeOptions() {
+ try {
+ const res = await getTypeEnums();
+ return normalizeBusinessTypeOptions(res?.data);
+ } catch {
+ return [];
+ }
+}
+
+/** 鏄惁涓虹郴缁熷唴缃ā鏉匡紙templateType === 0锛� */
+export function isBuiltinTemplate(row) {
+ return Number(row?.templateType) === TEMPLATE_TYPE_BUILTIN;
+}
+
+/** 鑺傜偣鍐呭鎵规柟寮忥細浼氱 / 鎴栫 */
+export const NODE_SIGN_MODE_OPTIONS = [
+ { value: "countersign", label: "浼氱", desc: "鏈妭鐐规墍鏈夊鎵逛汉鍧囬渶閫氳繃" },
+ { value: "or_sign", label: "鎴栫", desc: "鏈妭鐐逛换涓�瀹℃壒浜洪�氳繃鍗冲彲" },
+];
+
+function parseFormConfig(formConfig) {
+ if (!formConfig) return {};
+ if (typeof formConfig === "object") return formConfig;
+ try {
+ return JSON.parse(formConfig);
+ } catch {
+ return {};
+ }
+}
+
+function resolveDefaultMode(row, cfg, nodes) {
+ let mode = cfg.approvalMode || cfg.defaultMode;
+ if (!mode && nodes.length) {
+ const t = String(nodes[0]?.approveType || "").toUpperCase();
+ mode = t === "OR" ? "or_sign" : "parallel";
+ }
+ const m = String(mode || "").toLowerCase();
+ if (m === "or" || m === "or_sign") return "or_sign";
+ return "parallel";
+}
+
+/** 灏嗘帴鍙h繑鍥炵殑妯℃澘杞负銆岀郴缁熷父鐢ㄥ鎵广�嶅崱鐗囨暟鎹� */
+export function mapBuiltinCardFromApi(row) {
+ const cfg = parseFormConfig(row?.formConfig);
+ const fields = cfg.fields || cfg.formFields || [];
+ const nodes = row?.nodes || row?.flowNodes || [];
+ return {
+ key: String(row?.id ?? row?.templateName ?? ""),
+ id: row?.id,
+ approvalType: cfg.approvalType || row?.approvalType || "",
+ label: row?.templateName || row?.name || "鈥�",
+ summary: (row?.description || "").trim() || cfg.summaryPlaceholder || "绯荤粺棰勭疆濉姤瀛楁",
+ fieldCount: fields.length,
+ defaultMode: resolveDefaultMode(row, cfg, nodes),
+ };
+}
+
+export function unwrapTemplateList(payload) {
+ const data = payload?.data ?? payload;
+ if (Array.isArray(data)) return data;
+ if (Array.isArray(data?.records)) return data.records;
+ if (Array.isArray(data?.list)) return data.list;
+ return [];
+}
+
+/** 鍚庣 approveType 鈫� 椤甸潰 signMode */
+export function mapSignModeFromApi(approveType) {
+ const t = String(approveType || "").toUpperCase();
+ return t === "OR" ? "or_sign" : "countersign";
+}
+
+/** 椤甸潰 signMode 鈫� 鍚庣 approveType */
+export function mapSignModeToApi(signMode) {
+ return signMode === "or_sign" ? "OR" : "AND";
+}
+
+/** 椤甸潰 enabled 鈫� 鍚庣 enabled锛�1 鍚敤锛�0 鍋滅敤锛� */
+export function mapEnabledToApi(enabled) {
+ return enabled !== false ? "1" : "0";
+}
+
+/** 鍚庣 nodes 鈫� 椤甸潰 flowNodes锛堜繚鐣� id 渚涗慨鏀规彁浜わ級 */
+export function mapNodesFromApi(nodes) {
+ const list = Array.isArray(nodes) ? nodes : [];
+ return list.map((n, i) => ({
+ id: n.id,
+ templateId: n.templateId,
+ nodeOrder: n.levelNo ?? i + 1,
+ signMode: mapSignModeFromApi(n.approveType ?? n.signMode),
+ approvers: (n.approvers || [])
+ .filter((a) => a?.approverId != null && a.approverId !== "")
+ .map((a) => ({
+ id: a.id,
+ nodeId: a.nodeId,
+ templateId: a.templateId,
+ approverId: a.approverId,
+ approverName: a.approverName || "",
+ })),
+ }));
+}
+
+/** enabled锛�1 鍚敤锛�0 鍋滅敤 */
+export function mapEnabledFromApi(enabled) {
+ return enabled === "1" || enabled === 1 || enabled === true;
+}
+
+/** 鍏煎澶氱鍚庣鏃堕棿瀛楁鍚嶅苟鏍煎紡鍖栧睍绀� */
+export function pickTemplateTimes(row) {
+ const rawCreated =
+ row?.createdTime ?? row?.createTime ?? row?.gmtCreate ?? row?.created_at ?? "";
+ const rawUpdated =
+ row?.updatedTime ?? row?.updateTime ?? row?.gmtModified ?? row?.modifyTime ?? row?.updated_at ?? "";
+ const createdTime = normalizeTimeValue(rawCreated);
+ const updatedTime = normalizeTimeValue(rawUpdated);
+ return { createdTime, updatedTime, createTime: createdTime, updateTime: updatedTime };
+}
+
+function normalizeTimeValue(val) {
+ 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");
+ }
+ if (typeof val === "number") {
+ const d = val > 1e12 ? dayjs(val) : dayjs.unix(val);
+ return d.isValid() ? d.format("YYYY-MM-DD HH:mm:ss") : "";
+ }
+ 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;
+}
+
+export function formatDisplayTime(val) {
+ const t = normalizeTimeValue(val);
+ return t || "鈥�";
+}
+
+/** 璇︽儏鎺ュ彛 data 瑙e寘 */
+export function unwrapTemplateDetail(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") return {};
+ if (data.templateName != null || data.id != null) return data;
+ if (data.approvalTemplateVo) return data.approvalTemplateVo;
+ if (data.records && data.records[0]) return data.records[0];
+ return data;
+}
+
+/** 鍚庣闄勪欢瀛楁 鈫� 椤甸潰 storageBlobDTOs */
+export function mapAttachmentsFromApi(row) {
+ const list =
+ row?.storageBlobDTOs ||
+ row?.storageBlobDTOS ||
+ row?.storageBlobVOS ||
+ row?.storageBlobVOList ||
+ row?.attachmentList ||
+ [];
+ return Array.isArray(list) ? list : [];
+}
+
+/** 鍒嗛〉鍒楄〃椤� 鈫� 椤甸潰琛屾暟鎹紙涓昏〃 + 鑺傜偣锛� */
+export function mapTemplateFromApi(row) {
+ if (!row) return {};
+ const flowNodes = mapNodesFromApi(row.nodes || row.flowNodes);
+ const times = pickTemplateTimes(row);
+ return {
+ id: row.id,
+ templateName: row.templateName || "",
+ description: row.description || "",
+ enabled: mapEnabledFromApi(row.enabled),
+ enabledRaw: row.enabled,
+ templateType: row.templateType != null ? Number(row.templateType) : undefined,
+ businessType: row.businessType ?? "",
+ formConfig: row.formConfig,
+ formConfigData: parseFormConfigToData(row.formConfig),
+ storageBlobDTOs: mapAttachmentsFromApi(row),
+ createdUser: row.createdUser,
+ createdUserName: row.createdUserName,
+ ...times,
+ flowNodes,
+ nodes: row.nodes || row.flowNodes,
+ };
+}
+
+/** 琛ㄥ崟鏁版嵁 鈫� 鎻愪氦 DTO锛圓pprovalTemplateDto锛� */
+export function mapTemplateToApi(form) {
+ const nodes = normalizeFlowNodes(form.flowNodes);
+ const templateId = form.id || null;
+ const dto = {
+ templateName: (form.templateName || "").trim(),
+ description: (form.description || "").trim(),
+ enabled: mapEnabledToApi(form.enabled),
+ templateType:
+ form.templateType != null ? Number(form.templateType) : TEMPLATE_TYPE_CUSTOM,
+ businessType: form.businessType ?? "",
+ formConfig: buildFormConfigJson(form.formConfigData),
+ nodes: nodes.map((n, i) => {
+ const node = {
+ levelNo: n.nodeOrder ?? i + 1,
+ approveType: mapSignModeToApi(n.signMode),
+ approvers: n.approvers.map((a, idx) => {
+ const approver = {
+ approverId: a.approverId,
+ approverName: a.approverName || "",
+ sortNo: idx + 1,
+ };
+ if (a.id != null) approver.id = a.id;
+ if (a.nodeId != null) approver.nodeId = a.nodeId;
+ if (a.templateId != null) approver.templateId = a.templateId;
+ else if (templateId) approver.templateId = templateId;
+ return approver;
+ }),
+ };
+ if (n.id != null) node.id = n.id;
+ if (n.templateId != null) node.templateId = n.templateId;
+ else if (templateId) node.templateId = templateId;
+ return node;
+ }),
+ };
+ if (templateId) dto.id = templateId;
+ const attachments = Array.isArray(form.storageBlobDTOs) ? form.storageBlobDTOs : [];
+ if (attachments.length) dto.storageBlobDTOs = attachments;
+ return dto;
+}
+
+export function buildApprovalTemplateListParams({ page, searchForm }) {
+ const params = {
+ current: page.current,
+ size: page.size,
+ };
+ const kw = (searchForm?.keyword || "").trim();
+ if (kw) params.templateName = kw;
+ if (searchForm?.enabledOnly) params.enabled = "1";
+ return params;
+}
+
+export function nodeSignModeLabel(mode) {
+ return NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.label || "鈥�";
+}
+
+export function approvalTypeLabel(type) {
+ return APPROVAL_TYPE_OPTIONS.find((x) => x.value === type)?.label || type || "鈥�";
+}
+
+export function createEmptyNode(order = 1) {
+ return {
+ nodeOrder: order,
+ signMode: "countersign",
+ approvers: [],
+ };
+}
+
+export function createEmptyTemplateForm() {
+ return {
+ id: "",
+ templateName: "",
+ description: "",
+ templateType: TEMPLATE_TYPE_CUSTOM,
+ lockedFormFieldUids: [],
+ businessType: "",
+ formConfig: "",
+ formConfigData: createEmptyFormConfigData(),
+ enabled: true,
+ flowNodes: [createEmptyNode(1)],
+ storageBlobDTOs: [],
+ };
+}
+
+export function normalizeFlowNodes(nodes) {
+ const list = Array.isArray(nodes) ? nodes : [];
+ return list.map((n, i) => ({
+ id: n.id,
+ templateId: n.templateId,
+ nodeOrder: i + 1,
+ signMode: n.signMode === "or_sign" ? "or_sign" : "countersign",
+ approvers: (n.approvers || [])
+ .filter((a) => a?.approverId != null && a.approverId !== "")
+ .map((a) => ({
+ id: a.id,
+ nodeId: a.nodeId,
+ templateId: a.templateId,
+ approverId: a.approverId,
+ approverName: a.approverName || "",
+ })),
+ }));
+}
+
+export function validateTemplateForm(form) {
+ const name = (form.templateName || "").trim();
+ if (!name) return { ok: false, message: "璇峰~鍐欐ā鏉垮悕绉�" };
+ if (form.businessType == null || form.businessType === "") {
+ return { ok: false, message: "璇烽�夋嫨妯℃澘绫诲瀷" };
+ }
+ const nodes = normalizeFlowNodes(form.flowNodes);
+ if (!nodes.length) return { ok: false, message: "璇疯嚦灏戦厤缃竴涓鎵硅妭鐐�" };
+ for (let i = 0; i < nodes.length; i++) {
+ if (!nodes[i].approvers.length) {
+ return { ok: false, message: `璇蜂负绗� ${i + 1} 涓妭鐐归�夋嫨鑷冲皯涓�鍚嶅鎵逛汉` };
+ }
+ }
+ const cfgCheck = validateFormConfigData(form.formConfigData);
+ if (!cfgCheck.ok) return cfgCheck;
+ return { ok: true, nodes, name };
+}
+
+export function flowNodesSummary(nodes) {
+ const list = normalizeFlowNodes(nodes);
+ if (!list.length) return "鈥�";
+ return list
+ .map((n, i) => {
+ const names = n.approvers.map((a) => a.approverName || "鏈懡鍚�").join("銆�") || "鏈厤缃�";
+ return `鑺傜偣${i + 1}(${nodeSignModeLabel(n.signMode)}:${names})`;
+ })
+ .join(" 鈫� ");
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
new file mode 100644
index 0000000..6880f3f
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/FormConfigEditor.vue
@@ -0,0 +1,857 @@
+<!-- 瀹℃壒妯℃澘锛氬彲閰嶇疆濉姤椤癸紝搴忓垪鍖栧埌 formConfig -->
+<template>
+ <div class="fce">
+ <div class="fce-hint">
+ <span class="fce-hint-label">濉姤鎻愮ず</span>
+ <el-input
+ v-model="inner.summaryPlaceholder"
+ placeholder="濡傦細璇峰~鍐欐姤閿�浜嬬敱銆侀噾棰濈瓑"
+ maxlength="200"
+ show-word-limit
+ @input="emitOut"
+ />
+ </div>
+
+ <div class="fce-panel">
+ <div class="fce-toolbar">
+ <div class="fce-toolbar-left">
+ <span class="fce-title">濉姤椤归厤缃�</span>
+ <el-tag v-if="inner.fields.length" size="small" type="info" effect="plain">
+ 鍏� {{ inner.fields.length }} 椤�
+ </el-tag>
+ </div>
+ <div class="fce-toolbar-actions">
+ <el-dropdown
+ trigger="click"
+ :disabled="disableImport"
+ @visible-change="onImportDropdownVisible"
+ @command="importFromTemplate"
+ >
+ <el-button size="small" :loading="templateImportLoading" :disabled="disableImport">
+ 浠庡凡鏈夋ā鏉垮鍏�
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item v-if="!templateImportOptions.length" disabled>
+ 鏆傛棤鍏朵粬瀹℃壒妯℃澘
+ </el-dropdown-item>
+ <el-dropdown-item
+ v-for="t in templateImportOptions"
+ :key="t.id"
+ :command="t.id"
+ >
+ <span>{{ t.label }}</span>
+ <el-tag v-if="!t.enabled" size="small" type="info" class="import-tag">宸插仠鐢�</el-tag>
+ </el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ <el-button type="primary" size="small" :icon="Plus" @click="addField">娣诲姞濉姤椤�</el-button>
+ </div>
+ </div>
+
+ <el-empty
+ v-if="!inner.fields.length"
+ class="fce-empty"
+ description="鏆傛棤濉姤椤癸紝鍙坊鍔犳垨浠庡凡鏈夊鎵规ā鏉垮鍏�"
+ :image-size="72"
+ />
+
+ <div v-else class="fce-list">
+ <div
+ v-for="(field, index) in inner.fields"
+ :key="field._uid"
+ class="fce-card"
+ :class="{
+ 'fce-card--required': field.required,
+ 'fce-card--locked': isFieldLocked(field),
+ }"
+ >
+ <div class="fce-card-badge">{{ index + 1 }}</div>
+
+ <div class="fce-card-head">
+ <div class="fce-card-title">
+ <span class="fce-card-name">{{ field.label || `濉姤椤� ${index + 1}` }}</span>
+ <el-tag size="small" effect="light" type="primary">{{ typeLabel(field.type) }}</el-tag>
+ <el-tag v-if="field.required" size="small" type="danger" effect="plain">蹇呭~</el-tag>
+ <el-tag v-if="isFieldLocked(field)" size="small" type="info" effect="plain">鍐呯疆椤�</el-tag>
+ </div>
+ <div v-if="!isFieldLocked(field)" class="fce-card-btns">
+ <el-tooltip content="涓婄Щ" placement="top">
+ <el-button circle size="small" :disabled="index === 0" @click="moveField(index, -1)">
+ <el-icon><Top /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="涓嬬Щ" placement="top">
+ <el-button
+ circle
+ size="small"
+ :disabled="index >= inner.fields.length - 1"
+ @click="moveField(index, 1)"
+ >
+ <el-icon><Bottom /></el-icon>
+ </el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top">
+ <el-button circle size="small" type="danger" plain @click="removeField(index)">
+ <el-icon><Delete /></el-icon>
+ </el-button>
+ </el-tooltip>
+ </div>
+ </div>
+
+ <div class="fce-section">
+ <span class="fce-section-title">鍩虹淇℃伅</span>
+ <el-row :gutter="16">
+ <el-col :span="8">
+ <el-form-item label="鏄剧ず鍚嶇О" required class="fce-field-item">
+ <el-input
+ v-model="field.label"
+ placeholder="濡傦細鎶ラ攢璇存槑"
+ maxlength="50"
+ :disabled="isFieldLocked(field)"
+ @input="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="瀛楁鏍囪瘑" required class="fce-field-item">
+ <el-input
+ v-model="field.key"
+ placeholder="濡傦細summary"
+ maxlength="50"
+ :disabled="isFieldLocked(field)"
+ @input="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎺т欢绫诲瀷" class="fce-field-item">
+ <el-select
+ v-model="field.type"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ @change="onTypeChange(field)"
+ >
+ <el-option
+ v-for="t in FORM_FIELD_TYPE_OPTIONS"
+ :key="t.value"
+ :label="t.label"
+ :value="t.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+
+ <div class="fce-section">
+ <span class="fce-section-title">鏍¢獙涓庢牸寮�</span>
+ <el-row :gutter="16" align="middle">
+ <el-col :span="8">
+ <el-form-item label="鏄惁蹇呭~" class="fce-field-item fce-field-item--switch">
+ <el-switch
+ v-model="field.required"
+ inline-prompt
+ active-text="蹇呭~"
+ inactive-text="閫夊~"
+ :disabled="isFieldLocked(field)"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col v-if="field.type === 'textarea'" :span="8">
+ <el-form-item label="琛屾暟" class="fce-field-item">
+ <el-input-number
+ v-model="field.rows"
+ :min="1"
+ :max="10"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <template v-if="field.type === 'number'">
+ <el-col :span="8">
+ <el-form-item label="鏈�灏忓��" class="fce-field-item">
+ <el-input-number
+ v-model="field.min"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="灏忔暟浣�" class="fce-field-item">
+ <el-input-number
+ v-model="field.precision"
+ :min="0"
+ :max="4"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ @change="emitOut"
+ />
+ </el-form-item>
+ </el-col>
+ </template>
+ </el-row>
+ </div>
+
+ <div class="fce-section fce-section--default">
+ <span class="fce-section-title">榛樿鍊�</span>
+ <p class="fce-section-desc">閫夋嫨璇ユā鏉挎彁浜ゅ鎵规椂锛屽皢鑷姩棰勫~浠ヤ笅鍐呭锛堢敤鎴蜂粛鍙慨鏀癸級</p>
+ <el-input
+ v-if="field.type === 'text' || field.type === 'textarea'"
+ v-model="field.defaultValue"
+ :type="field.type === 'textarea' ? 'textarea' : 'text'"
+ :rows="field.type === 'textarea' ? 2 : undefined"
+ :placeholder="defaultPlaceholder(field)"
+ :disabled="isFieldLocked(field)"
+ clearable
+ @input="emitOut"
+ />
+ <el-input-number
+ v-else-if="field.type === 'number'"
+ v-model="field.defaultValue"
+ :min="field.min"
+ :precision="field.precision ?? 0"
+ controls-position="right"
+ placeholder="閫夊~"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ @change="emitOut"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'date'"
+ v-model="field.defaultValue"
+ type="date"
+ placeholder="閫夊~"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ clearable
+ @change="emitOut"
+ />
+ <el-date-picker
+ v-else-if="field.type === 'datetimerange'"
+ v-model="field.defaultValue"
+ type="datetimerange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫椂闂�"
+ end-placeholder="缁撴潫鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ clearable
+ @change="emitOut"
+ />
+ <el-select
+ v-else-if="field.type === 'select'"
+ v-model="field.defaultValue"
+ placeholder="閫夊~"
+ style="width: 100%"
+ clearable
+ filterable
+ :loading="optionSourceLoading"
+ :disabled="isFieldLocked(field)"
+ @change="emitOut"
+ >
+ <el-option
+ v-for="o in resolvedSelectOptions(field)"
+ :key="String(o.value)"
+ :label="o.label || o.value"
+ :value="o.value"
+ />
+ </el-select>
+ </div>
+
+ <div v-if="field.type === 'select'" class="fce-section fce-section--options">
+ <span class="fce-section-title">涓嬫媺閫夐」</span>
+ <el-row :gutter="16" class="fce-source-row">
+ <el-col :span="12">
+ <el-form-item label="閫夐」鏉ユ簮" class="fce-field-item">
+ <el-select
+ v-model="field.optionSource"
+ style="width: 100%"
+ :disabled="isFieldLocked(field)"
+ @change="onOptionSourceChange(field)"
+ >
+ <el-option
+ v-for="s in SELECT_OPTION_SOURCE_OPTIONS"
+ :key="s.value"
+ :label="s.label"
+ :value="s.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <p v-if="isDynamicOptionSource(field.optionSource)" class="fce-source-tip">
+ {{ optionSourceDesc(field.optionSource) }}銆傛彁浜ゅ鎵规椂灏嗚嚜鍔ㄥ姞杞芥渶鏂版暟鎹紝鏃犻渶鎵嬪姩缁存姢閫夐」銆�
+ </p>
+ <template v-if="!isDynamicOptionSource(field.optionSource)">
+ <div class="fce-options-head">
+ <span class="fce-section-subtitle">鎵嬪姩閫夐」</span>
+ <el-button
+ type="primary"
+ link
+ size="small"
+ :icon="Plus"
+ :disabled="isFieldLocked(field)"
+ @click="addOption(field)"
+ >
+ 娣诲姞閫夐」
+ </el-button>
+ </div>
+ <div
+ v-for="(opt, oi) in field.options"
+ :key="oi"
+ class="fce-option-row"
+ >
+ <span class="fce-option-index">{{ oi + 1 }}</span>
+ <el-input
+ v-model="opt.label"
+ placeholder="鏄剧ず鏂囨湰"
+ :disabled="isFieldLocked(field)"
+ @input="emitOut"
+ />
+ <el-input
+ v-model="opt.value"
+ placeholder="閫夐」鍊�"
+ class="fce-option-value"
+ :disabled="isFieldLocked(field)"
+ @input="emitOut"
+ />
+ <el-button
+ type="danger"
+ link
+ :icon="Delete"
+ :disabled="isFieldLocked(field) || field.options.length <= 1"
+ @click="removeOption(field, oi)"
+ />
+ </div>
+ </template>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { Bottom, Delete, Plus, Top } from "@element-plus/icons-vue";
+import {
+ getApprovalTemplateDetail,
+ listApprovalTemplate,
+ TEMPLATE_TYPE_BUILTIN,
+ TEMPLATE_TYPE_CUSTOM,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, reactive, ref, watch } from "vue";
+import {
+ mapEnabledFromApi,
+ unwrapTemplateDetail,
+ unwrapTemplateList,
+} from "../approveTemplateConstants.js";
+import {
+ FORM_FIELD_TYPE_OPTIONS,
+ createEmptyFormConfigData,
+ createEmptyFormField,
+ formFieldTypeLabel,
+ parseFormConfigToData,
+} from "../formConfigUtils.js";
+import {
+ SELECT_OPTION_SOURCE,
+ SELECT_OPTION_SOURCE_OPTIONS,
+ isDynamicOptionSource,
+} from "../selectOptionSource.js";
+import { useSelectOptionSources } from "../useSelectOptionSources.js";
+
+const props = defineProps({
+ modelValue: { type: Object, default: () => createEmptyFormConfigData() },
+ /** 缂栬緫褰撳墠妯℃澘鏃舵帓闄よ嚜韬紝閬垮厤浠庤嚜宸卞鍏� */
+ excludeTemplateId: { type: [String, Number], default: null },
+ /** 绂佺敤銆屼粠宸叉湁妯℃澘瀵煎叆銆� */
+ disableImport: { type: Boolean, default: false },
+ /** 绯荤粺鍐呯疆妯℃澘缂栬緫鏃讹紝鎵撳紑寮圭獥鍗冲瓨鍦ㄧ殑濉姤椤� _uid锛屼笉鍙敼鍒� */
+ lockedFieldUids: { type: Array, default: () => [] },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const inner = reactive(createEmptyFormConfigData());
+
+const { loading: optionSourceLoading, ensureForFields, getOptions } = useSelectOptionSources();
+
+const templateImportOptions = ref([]);
+const templateImportLoading = ref(false);
+
+const lockedUidSet = computed(
+ () => new Set((props.lockedFieldUids || []).filter(Boolean))
+);
+
+function isFieldLocked(field) {
+ return field?._uid != null && lockedUidSet.value.has(field._uid);
+}
+
+function typeLabel(type) {
+ return formFieldTypeLabel(type);
+}
+
+function defaultPlaceholder(field) {
+ const name = field.label || "璇ュ瓧娈�";
+ return `閫夊~锛岄�夋嫨妯℃澘鏃跺皢棰勫~${name}`;
+}
+
+function optionSourceDesc(source) {
+ return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.desc || "";
+}
+
+function resolvedSelectOptions(field) {
+ if (field.type !== "select") return [];
+ return getOptions(field);
+}
+
+function syncFromProps(v) {
+ const src = v || createEmptyFormConfigData();
+ inner.summaryPlaceholder = src.summaryPlaceholder || "";
+ inner.fields = (src.fields || []).map((f) => ({
+ ...createEmptyFormField(),
+ ...f,
+ _uid: f._uid || createEmptyFormField()._uid,
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
+ options: (f.options || [{ label: "", value: "" }]).map((o) => ({ ...o })),
+ }));
+ ensureForFields(inner.fields);
+}
+
+function emitOut() {
+ emit("update:modelValue", {
+ summaryPlaceholder: inner.summaryPlaceholder,
+ fields: inner.fields.map((f) => ({
+ _uid: f._uid,
+ key: f.key,
+ label: f.label,
+ type: f.type,
+ required: f.required,
+ rows: f.rows,
+ min: f.min,
+ precision: f.precision,
+ defaultValue: cloneDefaultValue(f),
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
+ options: (f.options || []).map((o) => ({ label: o.label, value: o.value })),
+ })),
+ });
+}
+
+function cloneDefaultValue(f) {
+ if (f.type === "datetimerange" && Array.isArray(f.defaultValue)) {
+ return [...f.defaultValue];
+ }
+ return f.defaultValue;
+}
+
+watch(
+ () => props.modelValue,
+ (v) => syncFromProps(v),
+ { deep: true, immediate: true }
+);
+
+function addField() {
+ inner.fields.push(createEmptyFormField());
+ ensureForFields(inner.fields);
+ emitOut();
+}
+
+function removeField(index) {
+ if (isFieldLocked(inner.fields[index])) return;
+ inner.fields.splice(index, 1);
+ emitOut();
+}
+
+function moveField(index, delta) {
+ if (isFieldLocked(inner.fields[index])) return;
+ const next = index + delta;
+ if (next < 0 || next >= inner.fields.length) return;
+ if (isFieldLocked(inner.fields[next])) return;
+ const t = inner.fields[index];
+ inner.fields[index] = inner.fields[next];
+ inner.fields[next] = t;
+ emitOut();
+}
+
+function resetDefaultValueForType(field) {
+ if (field.type === "number") field.defaultValue = undefined;
+ else if (field.type === "datetimerange") field.defaultValue = [];
+ else field.defaultValue = "";
+}
+
+function onTypeChange(field) {
+ if (field.type === "select") {
+ if (!field.optionSource) field.optionSource = SELECT_OPTION_SOURCE.STATIC;
+ if (!field.options || !field.options.length) {
+ field.options = [{ label: "", value: "" }];
+ }
+ ensureForFields(inner.fields);
+ }
+ resetDefaultValueForType(field);
+ emitOut();
+}
+
+function onOptionSourceChange(field) {
+ field.defaultValue = "";
+ if (!isDynamicOptionSource(field.optionSource) && (!field.options || !field.options.length)) {
+ field.options = [{ label: "", value: "" }];
+ }
+ ensureForFields(inner.fields);
+ emitOut();
+}
+
+function addOption(field) {
+ field.options.push({ label: "", value: "" });
+ emitOut();
+}
+
+function removeOption(field, oi) {
+ if (field.options.length <= 1) return;
+ field.options.splice(oi, 1);
+ emitOut();
+}
+
+async function loadTemplateImportOptions() {
+ templateImportLoading.value = true;
+ try {
+ const [customRes, builtinRes] = await Promise.all([
+ listApprovalTemplate(TEMPLATE_TYPE_CUSTOM),
+ listApprovalTemplate(TEMPLATE_TYPE_BUILTIN),
+ ]);
+ const excludeId =
+ props.excludeTemplateId != null && props.excludeTemplateId !== ""
+ ? String(props.excludeTemplateId)
+ : "";
+ templateImportOptions.value = [...unwrapTemplateList(customRes), ...unwrapTemplateList(builtinRes)]
+ .filter((row) => row?.id != null && String(row.id) !== excludeId)
+ .map((row) => ({
+ id: row.id,
+ label: row.templateName || `妯℃澘 #${row.id}`,
+ enabled: mapEnabledFromApi(row.enabled),
+ }));
+ } catch {
+ templateImportOptions.value = [];
+ ElMessage.error("鍔犺浇瀹℃壒妯℃澘鍒楄〃澶辫触");
+ } finally {
+ templateImportLoading.value = false;
+ }
+}
+
+function onImportDropdownVisible(visible) {
+ if (props.disableImport) return;
+ if (visible) loadTemplateImportOptions();
+}
+
+async function importFromTemplate(templateId) {
+ if (!templateId) return;
+ if (inner.fields.length) {
+ try {
+ await ElMessageBox.confirm("灏嗚鐩栧綋鍓嶅~鎶ラ」閰嶇疆锛屾槸鍚︾户缁紵", "浠庢ā鏉垮鍏�", {
+ type: "warning",
+ confirmButtonText: "缁х画瀵煎叆",
+ cancelButtonText: "鍙栨秷",
+ });
+ } catch {
+ return;
+ }
+ }
+ templateImportLoading.value = true;
+ try {
+ const res = await getApprovalTemplateDetail(templateId);
+ const row = unwrapTemplateDetail(res);
+ const data = parseFormConfigToData(row?.formConfig);
+ if (!data.fields?.length) {
+ ElMessage.warning("璇ユā鏉挎湭閰嶇疆濉姤椤�");
+ return;
+ }
+ syncFromProps(data);
+ emitOut();
+ ElMessage.success(`宸插鍏ャ��${row.templateName || "妯℃澘"}銆嶇殑濉姤椤筦);
+ } catch {
+ ElMessage.error("鍔犺浇妯℃澘璇︽儏澶辫触");
+ } finally {
+ templateImportLoading.value = false;
+ }
+}
+</script>
+
+<style scoped>
+.fce {
+ width: 100%;
+}
+
+.fce-hint {
+ padding: 14px 16px;
+ margin-bottom: 14px;
+ border-radius: 10px;
+ background: linear-gradient(135deg, var(--el-color-primary-light-9) 0%, var(--el-fill-color-blank) 100%);
+ border: 1px solid var(--el-color-primary-light-7);
+}
+
+.fce-hint-label {
+ display: block;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+ margin-bottom: 8px;
+}
+
+.fce-panel {
+ padding: 16px;
+ border-radius: 12px;
+ background: var(--el-fill-color-lighter);
+ border: 1px solid var(--el-border-color-lighter);
+}
+
+.fce-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin-bottom: 16px;
+}
+
+.fce-toolbar-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.fce-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+
+.fce-toolbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.import-tag {
+ margin-left: 8px;
+ vertical-align: middle;
+}
+
+.fce-empty {
+ padding: 24px 0;
+}
+
+.fce-list {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.fce-card {
+ position: relative;
+ padding: 16px 16px 12px;
+ border-radius: 12px;
+ background: var(--el-bg-color);
+ border: 1px solid var(--el-border-color-lighter);
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
+ transition: border-color 0.2s, box-shadow 0.2s;
+}
+
+.fce-card:hover {
+ border-color: var(--el-color-primary-light-5);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
+}
+
+.fce-card--required {
+ border-left: 3px solid var(--el-color-danger-light-3);
+}
+
+.fce-card--locked {
+ background: var(--el-fill-color-light);
+}
+
+.fce-card-badge {
+ position: absolute;
+ top: -10px;
+ left: 16px;
+ min-width: 22px;
+ height: 22px;
+ padding: 0 6px;
+ border-radius: 11px;
+ background: var(--el-color-primary);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 6px rgba(var(--el-color-primary-rgb), 0.35);
+}
+
+.fce-card-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 14px;
+ padding-top: 4px;
+}
+
+.fce-card-title {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.fce-card-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+
+.fce-card-btns {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-shrink: 0;
+}
+
+.fce-section {
+ margin-bottom: 12px;
+ padding-bottom: 12px;
+ border-bottom: 1px dashed var(--el-border-color-extra-light);
+}
+
+.fce-section:last-child {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ border-bottom: none;
+}
+
+.fce-section-title {
+ display: block;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--el-text-color-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 10px;
+}
+
+.fce-section-desc {
+ margin: -6px 0 10px;
+ font-size: 12px;
+ color: var(--el-text-color-placeholder);
+ line-height: 1.5;
+}
+
+.fce-section--default {
+ padding: 12px 14px;
+ border-radius: 8px;
+ background: var(--el-fill-color-lighter);
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+.fce-section--default .fce-section-title {
+ margin-bottom: 4px;
+ color: var(--el-color-primary);
+ text-transform: none;
+ letter-spacing: 0;
+ font-size: 13px;
+}
+
+.fce-section--options {
+ padding-top: 4px;
+ border-bottom: none;
+ margin-bottom: 0;
+}
+
+.fce-field-item {
+ margin-bottom: 0;
+}
+
+.fce-field-item :deep(.el-form-item__label) {
+ font-size: 13px;
+ color: var(--el-text-color-regular);
+}
+
+.fce-field-item--switch :deep(.el-form-item__content) {
+ line-height: 32px;
+}
+
+.fce-options-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.fce-options-head .fce-section-title,
+.fce-options-head .fce-section-subtitle {
+ margin-bottom: 0;
+}
+
+.fce-section-subtitle {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--el-text-color-secondary);
+}
+
+.fce-source-row {
+ margin-bottom: 4px;
+}
+
+.fce-source-tip {
+ margin: 0 0 10px;
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ line-height: 1.5;
+}
+
+.fce-option-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 8px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ background: var(--el-fill-color-lighter);
+}
+
+.fce-option-row:last-child {
+ margin-bottom: 0;
+}
+
+.fce-option-index {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background: var(--el-color-info-light-8);
+ color: var(--el-text-color-secondary);
+ font-size: 11px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.fce-option-value {
+ width: 140px;
+ flex-shrink: 0;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
new file mode 100644
index 0000000..78304ea
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/components/TemplateFlowEditor.vue
@@ -0,0 +1,399 @@
+<!-- 瀹℃壒妯℃澘锛氬彲閰嶇疆鑺傜偣鏁帮紝姣忚妭鐐瑰浜� + 浼氱/鎴栫 -->
+<template>
+ <div class="tfe">
+ <div v-if="innerList.length" class="tfe-flow">
+ <div v-for="(item, index) in innerList" :key="item._uid" class="tfe-flow-item">
+ <div class="tfe-card" :class="{ 'tfe-card--empty': !item.approvers?.length }">
+ <div class="tfe-badge">{{ index + 1 }}</div>
+ <div class="tfe-head">
+ <span class="tfe-level">{{ levelText(index) }}</span>
+ <el-radio-group
+ v-if="!readonly"
+ v-model="item.signMode"
+ size="small"
+ @change="emitOut"
+ >
+ <el-radio-button value="countersign">浼氱</el-radio-button>
+ <el-radio-button value="or_sign">鎴栫</el-radio-button>
+ </el-radio-group>
+ <el-tag v-else size="small" type="info" effect="plain">
+ {{ signModeLabel(item.signMode) }}
+ </el-tag>
+ </div>
+ <p class="tfe-mode-tip">{{ signModeTip(item.signMode) }}</p>
+ <div v-if="!readonly" class="tfe-select">
+ <el-select
+ v-model="item.approverIds"
+ multiple
+ collapse-tags
+ collapse-tags-tooltip
+ :max-collapse-tags="2"
+ filterable
+ placeholder="璇烽�夋嫨瀹℃壒浜猴紙鍙閫夛級"
+ style="width: 100%"
+ @change="(ids) => onApproversChange(ids, item)"
+ >
+ <el-option
+ v-for="u in userOptions"
+ :key="String(u.userId ?? u.id)"
+ :label="optionLabel(u)"
+ :value="u.userId ?? u.id"
+ />
+ </el-select>
+ </div>
+ <div v-if="item.approvers?.length" class="tfe-chips" :class="{ 'tfe-chips--readonly': readonly }">
+ <el-tag
+ v-for="a in item.approvers"
+ :key="String(a.approverId)"
+ size="small"
+ type="info"
+ effect="plain"
+ >
+ {{ a.approverName || "鈥�" }}
+ </el-tag>
+ </div>
+ <div v-if="!readonly" class="tfe-actions">
+ <el-button type="primary" circle size="small" :disabled="index === 0" title="鍓嶇Щ" @click="moveLeft(index)">
+ <el-icon><ArrowLeft /></el-icon>
+ </el-button>
+ <el-button
+ type="primary"
+ circle
+ size="small"
+ :disabled="index === innerList.length - 1"
+ title="鍚庣Щ"
+ @click="moveRight(index)"
+ >
+ <el-icon><ArrowRight /></el-icon>
+ </el-button>
+ <el-button type="danger" circle size="small" title="鍒犻櫎鑺傜偣" @click="remove(index)">
+ <el-icon><Delete /></el-icon>
+ </el-button>
+ </div>
+ <p v-else-if="!item.approvers?.length" class="tfe-empty-approver">鏆傛棤瀹℃壒浜�</p>
+ </div>
+ <div v-if="index < innerList.length - 1" class="tfe-conn">
+ <div class="tfe-conn-line"></div>
+ <el-icon class="tfe-conn-icon"><ArrowRight /></el-icon>
+ </div>
+ </div>
+
+ <div v-if="!readonly" class="tfe-add-wrap">
+ <div v-if="innerList.length" class="tfe-conn">
+ <div class="tfe-conn-line"></div>
+ <el-icon class="tfe-conn-icon"><ArrowRight /></el-icon>
+ </div>
+ <div class="tfe-add-card" @click="addNode">
+ <div class="tfe-add-icon"><el-icon :size="26"><Plus /></el-icon></div>
+ <span>鏂板鑺傜偣</span>
+ </div>
+ </div>
+ </div>
+
+ <div v-else class="tfe-empty">
+ <el-icon :size="44" color="#c0c4cc"><User /></el-icon>
+ <p>鏆傛棤瀹℃壒鑺傜偣</p>
+ <el-button v-if="!readonly" type="primary" @click="addNode">娣诲姞绗竴涓妭鐐�</el-button>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ArrowLeft, ArrowRight, Delete, Plus, User } from "@element-plus/icons-vue";
+import { ref, watch } from "vue";
+import { NODE_SIGN_MODE_OPTIONS, normalizeFlowNodes } from "../approveTemplateConstants.js";
+
+const props = defineProps({
+ modelValue: { type: Array, default: () => [] },
+ userOptions: { type: Array, default: () => [] },
+ /** 閫夋嫨妯℃澘鍚庣敵璇峰満鏅細浠呭睍绀猴紝涓嶅彲鏀瑰鎵逛汉/鑺傜偣 */
+ readonly: { type: Boolean, default: false },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const innerList = ref([]);
+
+function signModeTip(mode) {
+ return NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.desc || "";
+}
+
+function signModeLabel(mode) {
+ return (
+ NODE_SIGN_MODE_OPTIONS.find((x) => x.value === mode)?.label ||
+ (mode === "or_sign" ? "鎴栫" : "浼氱")
+ );
+}
+
+function levelText(i) {
+ const t = ["绗竴绾�", "绗簩绾�", "绗笁绾�", "绗洓绾�", "绗簲绾�", "绗叚绾�", "绗竷绾�", "绗叓绾�"];
+ return t[i] || `绗�${i + 1}绾;
+}
+
+function optionLabel(u) {
+ const nick = u.nickName || "";
+ const un = u.userName || "";
+ if (nick && un && nick !== un) return `${nick}锛�${un}锛塦;
+ return nick || un || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
+}
+
+function newUid() {
+ return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+}
+
+function mapIn(rows) {
+ const normalized = normalizeFlowNodes(rows);
+ return normalized.map((n) => ({
+ _uid: newUid(),
+ id: n.id,
+ templateId: n.templateId,
+ nodeOrder: n.nodeOrder,
+ signMode: n.signMode,
+ approverIds: n.approvers.map((a) => a.approverId),
+ approvers: [...n.approvers],
+ }));
+}
+
+function publicShape(rows) {
+ return normalizeFlowNodes(
+ (rows || []).map((r) => ({
+ id: r.id,
+ templateId: r.templateId,
+ nodeOrder: r.nodeOrder,
+ signMode: r.signMode,
+ approvers: r.approvers || [],
+ }))
+ );
+}
+
+function emitOut() {
+ emit("update:modelValue", publicShape(innerList.value));
+}
+
+watch(
+ () => props.modelValue,
+ (v) => {
+ const next = publicShape(v || []);
+ if (JSON.stringify(next) === JSON.stringify(publicShape(innerList.value))) return;
+ innerList.value = mapIn(v || []);
+ },
+ { deep: true, immediate: true }
+);
+
+function findUser(id) {
+ if (id == null || id === "") return null;
+ return props.userOptions.find((u) => String(u.userId ?? u.id) === String(id)) ?? null;
+}
+
+function onApproversChange(ids, row) {
+ const idList = Array.isArray(ids) ? ids : [];
+ const prevById = new Map((row.approvers || []).map((a) => [String(a.approverId), a]));
+ row.approverIds = idList;
+ row.approvers = idList.map((id) => {
+ const prev = prevById.get(String(id));
+ const u = findUser(id);
+ const item = {
+ approverId: id,
+ approverName: u ? u.nickName || u.userName || "" : prev?.approverName || "",
+ };
+ if (prev?.id != null) item.id = prev.id;
+ if (prev?.nodeId != null) item.nodeId = prev.nodeId;
+ else if (row.id != null) item.nodeId = row.id;
+ if (prev?.templateId != null) item.templateId = prev.templateId;
+ else if (row.templateId != null) item.templateId = row.templateId;
+ return item;
+ });
+ emitOut();
+}
+
+function addNode() {
+ if (props.readonly) return;
+ innerList.value.push({
+ _uid: newUid(),
+ nodeOrder: innerList.value.length + 1,
+ signMode: "countersign",
+ approverIds: [],
+ approvers: [],
+ });
+ emitOut();
+}
+
+function remove(index) {
+ if (props.readonly) return;
+ innerList.value.splice(index, 1);
+ emitOut();
+}
+
+function moveLeft(index) {
+ if (props.readonly) return;
+ if (index < 1) return;
+ const t = innerList.value[index];
+ innerList.value[index] = innerList.value[index - 1];
+ innerList.value[index - 1] = t;
+ emitOut();
+}
+
+function moveRight(index) {
+ if (props.readonly) return;
+ if (index >= innerList.value.length - 1) return;
+ const t = innerList.value[index];
+ innerList.value[index] = innerList.value[index + 1];
+ innerList.value[index + 1] = t;
+ emitOut();
+}
+</script>
+
+<style scoped>
+.tfe {
+ width: 100%;
+}
+.tfe-flow {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ padding: 6px 0 10px;
+}
+.tfe-flow-item {
+ display: flex;
+ align-items: center;
+}
+.tfe-card {
+ width: 248px;
+ flex-shrink: 0;
+ border: 2px solid var(--el-border-color);
+ border-radius: 12px;
+ padding: 14px 12px 12px;
+ position: relative;
+ background: var(--el-bg-color);
+}
+.tfe-card--empty {
+ border-style: dashed;
+ background: var(--el-fill-color-lighter);
+}
+.tfe-badge {
+ position: absolute;
+ top: -8px;
+ left: 12px;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: var(--el-color-primary);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.tfe-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ margin: 8px 0 4px;
+}
+.tfe-level {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+.tfe-mode-tip {
+ font-size: 11px;
+ color: var(--el-text-color-secondary);
+ margin: 0 0 10px;
+ line-height: 1.4;
+ min-height: 30px;
+}
+.tfe-select {
+ margin-bottom: 8px;
+}
+.tfe-chips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-bottom: 8px;
+ min-height: 24px;
+}
+.tfe-chips--readonly {
+ margin-top: 4px;
+ margin-bottom: 0;
+}
+.tfe-empty-approver {
+ font-size: 12px;
+ color: var(--el-text-color-placeholder);
+ margin: 4px 0 0;
+}
+.tfe-actions {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding-top: 10px;
+ border-top: 1px solid var(--el-border-color-lighter);
+}
+.tfe-conn {
+ display: flex;
+ align-items: center;
+ width: 40px;
+ flex-shrink: 0;
+ align-self: center;
+}
+.tfe-conn-line {
+ flex: 1;
+ height: 2px;
+ background: var(--el-border-color);
+}
+.tfe-conn-icon {
+ font-size: 14px;
+ color: var(--el-text-color-placeholder);
+ margin-left: -2px;
+}
+.tfe-add-wrap {
+ display: flex;
+ align-items: center;
+}
+.tfe-add-card {
+ width: 120px;
+ min-height: 200px;
+ flex-shrink: 0;
+ border: 2px dashed var(--el-border-color);
+ border-radius: 12px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ cursor: pointer;
+ color: var(--el-text-color-regular);
+ font-size: 13px;
+ background: var(--el-fill-color-lighter);
+ transition: border-color 0.2s, background 0.2s;
+}
+.tfe-add-card:hover {
+ border-color: var(--el-color-primary);
+ background: var(--el-color-primary-light-9);
+ color: var(--el-color-primary);
+}
+.tfe-add-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: var(--el-color-primary);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.tfe-empty {
+ text-align: center;
+ padding: 28px 16px;
+ border: 1px dashed var(--el-border-color);
+ border-radius: 12px;
+ background: var(--el-fill-color-lighter);
+}
+.tfe-empty p {
+ margin: 10px 0 14px;
+ color: var(--el-text-color-secondary);
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
new file mode 100644
index 0000000..c1f66bd
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/formConfigUtils.js
@@ -0,0 +1,301 @@
+import { mapAttachmentsFromApi } from "./approveTemplateConstants.js";
+import {
+ isDynamicOptionSource,
+ SELECT_OPTION_SOURCE,
+ selectOptionSourceLabel,
+} from "./selectOptionSource.js";
+
+export { selectOptionSourceLabel };
+
+/** 濉姤椤圭被鍨嬶紙涓庡鎵规彁浜ら〉 field.type 涓�鑷达級 */
+export const FORM_FIELD_TYPE_OPTIONS = [
+ { value: "text", label: "鍗曡鏂囨湰" },
+ { value: "textarea", label: "澶氳鏂囨湰" },
+ { value: "number", label: "鏁板瓧" },
+ { value: "date", label: "鏃ユ湡" },
+ { value: "datetimerange", label: "鏃ユ湡鏃堕棿鑼冨洿" },
+ { value: "select", label: "涓嬫媺閫夋嫨" },
+];
+
+/** 甯哥敤棰勮锛堝璐圭敤鎶ラ攢锛� */
+export const FORM_CONFIG_PRESETS = [
+ {
+ key: "cost_reimburse",
+ label: "璐圭敤鎶ラ攢",
+ summaryPlaceholder: "璇峰~鍐欐姤閿�浜嬬敱銆侀噾棰濈瓑",
+ fields: [
+ { key: "summary", label: "鎶ラ攢璇存槑", type: "textarea", required: true, rows: 3 },
+ { key: "amount", label: "鎶ラ攢閲戦(鍏�)", type: "number", required: true, min: 0, precision: 2 },
+ ],
+ },
+ {
+ key: "travel_reimburse",
+ label: "宸梾鎶ラ攢",
+ summaryPlaceholder: "鍑哄樊琛岀▼涓庤垂鐢ㄨ鏄�",
+ fields: [
+ { key: "summary", label: "宸梾璇存槑", type: "textarea", required: true, rows: 3 },
+ { key: "amount", label: "鎶ラ攢閲戦(鍏�)", type: "number", required: true, min: 0, precision: 2 },
+ { key: "tripDays", label: "鍑哄樊澶╂暟", type: "number", required: false, min: 0, precision: 0 },
+ ],
+ },
+ {
+ key: "leave",
+ label: "璇峰亣鐢宠",
+ summaryPlaceholder: "璇峰~鍐欒鍋囩被鍨嬩笌鏃堕棿",
+ fields: [
+ {
+ key: "leaveType",
+ label: "璇峰亣绫诲瀷",
+ type: "select",
+ required: true,
+ options: [
+ { label: "骞村亣", value: "annual" },
+ { label: "鐥呭亣", value: "sick" },
+ { label: "浜嬪亣", value: "personal" },
+ { label: "璋冧紤", value: "compensatory" },
+ ],
+ },
+ { key: "summary", label: "璇峰亣浜嬬敱", type: "textarea", required: true, rows: 2 },
+ { key: "dateRange", label: "璇峰亣鏃堕棿", type: "datetimerange", required: true },
+ ],
+ },
+];
+
+function newFieldUid() {
+ return `f_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
+}
+
+export function createEmptyFormField() {
+ return {
+ _uid: newFieldUid(),
+ key: "",
+ label: "",
+ type: "text",
+ required: true,
+ rows: 3,
+ min: 0,
+ precision: 0,
+ defaultValue: "",
+ optionSource: SELECT_OPTION_SOURCE.STATIC,
+ options: [{ label: "", value: "" }],
+ };
+}
+
+/** 瑙f瀽鍗曢」榛樿鍊硷紙渚涙彁浜ら〉 formPayload 鍒濆鍖栵級 */
+export function resolveFieldDefaultValue(field) {
+ const type = field?.type || "text";
+ const dv = field?.defaultValue;
+ if (dv === undefined || dv === null || dv === "") {
+ if (type === "number") return undefined;
+ if (type === "datetimerange") return [];
+ return "";
+ }
+ if (type === "number") {
+ const n = Number(dv);
+ return Number.isNaN(n) ? undefined : n;
+ }
+ if (type === "datetimerange") {
+ return Array.isArray(dv) ? [...dv] : [];
+ }
+ return dv;
+}
+
+function hasDefaultValue(field) {
+ const type = field?.type || "text";
+ const dv = field?.defaultValue;
+ if (dv === undefined || dv === null) return false;
+ if (type === "number") return dv !== "" && !Number.isNaN(Number(dv));
+ if (type === "datetimerange") return Array.isArray(dv) && dv.length === 2;
+ if (type === "select") return dv !== "";
+ return String(dv).trim() !== "";
+}
+
+/** 鏍规嵁瀛楁瀹氫箟鐢熸垚 formPayload 鍒濆鍊硷紙鍚粯璁ゅ�硷級 */
+export function buildFormPayloadFromFields(fields) {
+ const payload = {};
+ (fields || []).forEach((f) => {
+ const key = (f.key || "").trim();
+ if (!key) return;
+ payload[key] = resolveFieldDefaultValue(f);
+ });
+ return payload;
+}
+
+export function createEmptyFormConfigData() {
+ return {
+ summaryPlaceholder: "",
+ fields: [],
+ };
+}
+
+function parseFormConfigRaw(formConfig) {
+ if (!formConfig) return {};
+ if (typeof formConfig === "object") return formConfig;
+ try {
+ return JSON.parse(formConfig);
+ } catch {
+ return {};
+ }
+}
+
+function normalizeDefaultValueFromApi(f) {
+ const type = f.type || "text";
+ if (f.defaultValue === undefined || f.defaultValue === null) {
+ if (type === "number") return undefined;
+ if (type === "datetimerange") return [];
+ return "";
+ }
+ if (type === "datetimerange" && Array.isArray(f.defaultValue)) {
+ return [...f.defaultValue];
+ }
+ return f.defaultValue;
+}
+
+/** 鎺ュ彛 formConfig 鈫� 缂栬緫鍣ㄦ暟鎹� */
+export function parseFormConfigToData(formConfig) {
+ const raw = parseFormConfigRaw(formConfig);
+ const fields = (raw.fields || raw.formFields || []).map((f) => ({
+ _uid: newFieldUid(),
+ key: f.key || "",
+ label: f.label || "",
+ type: f.type || "text",
+ required: f.required !== false,
+ rows: f.rows ?? 3,
+ min: f.min ?? 0,
+ precision: f.precision ?? 0,
+ defaultValue: normalizeDefaultValueFromApi(f),
+ optionSource: f.optionSource || SELECT_OPTION_SOURCE.STATIC,
+ options: (f.options || []).length
+ ? f.options.map((o) => ({ label: o.label || "", value: o.value ?? "" }))
+ : [{ label: "", value: "" }],
+ }));
+ return {
+ summaryPlaceholder: raw.summaryPlaceholder || "",
+ fields,
+ };
+}
+
+/** 缂栬緫鍣ㄦ暟鎹� 鈫� 鎻愪氦鐢� JSON 瀛楃涓� */
+export function buildFormConfigJson(formConfigData) {
+ const data = formConfigData || createEmptyFormConfigData();
+ const fields = (data.fields || []).map((f) => {
+ const item = {
+ key: (f.key || "").trim(),
+ label: (f.label || "").trim(),
+ type: f.type || "text",
+ required: f.required !== false,
+ };
+ if (item.type === "textarea") item.rows = Number(f.rows) || 3;
+ if (item.type === "number") {
+ item.min = f.min ?? 0;
+ item.precision = f.precision ?? 0;
+ }
+ if (item.type === "select") {
+ const source = f.optionSource || SELECT_OPTION_SOURCE.STATIC;
+ item.optionSource = source;
+ if (!isDynamicOptionSource(source)) {
+ item.options = (f.options || [])
+ .filter((o) => (o.label || "").trim() || (o.value !== "" && o.value != null))
+ .map((o) => ({ label: (o.label || "").trim(), value: o.value }));
+ }
+ }
+ if (hasDefaultValue(f)) {
+ item.defaultValue =
+ f.type === "datetimerange" && Array.isArray(f.defaultValue)
+ ? f.defaultValue
+ : f.defaultValue;
+ }
+ return item;
+ });
+ const payload = {
+ summaryPlaceholder: (data.summaryPlaceholder || "").trim(),
+ fields,
+ };
+ return JSON.stringify(payload);
+}
+
+export function applyFormConfigPreset(presetKey) {
+ const preset = FORM_CONFIG_PRESETS.find((p) => p.key === presetKey);
+ if (!preset) return createEmptyFormConfigData();
+ return parseFormConfigToData({
+ summaryPlaceholder: preset.summaryPlaceholder,
+ fields: preset.fields,
+ });
+}
+
+export function validateFormConfigData(formConfigData) {
+ const fields = formConfigData?.fields || [];
+ if (!fields.length) {
+ return { ok: true };
+ }
+ const keys = new Set();
+ for (let i = 0; i < fields.length; i++) {
+ const f = fields[i];
+ const key = (f.key || "").trim();
+ const label = (f.label || "").trim();
+ if (!key) return { ok: false, message: `璇峰~鍐欑 ${i + 1} 涓~鎶ラ」鐨勫瓧娈垫爣璇哷 };
+ if (!label) return { ok: false, message: `璇峰~鍐欑 ${i + 1} 涓~鎶ラ」鐨勬樉绀哄悕绉癭 };
+ if (keys.has(key)) return { ok: false, message: `瀛楁鏍囪瘑銆�${key}銆嶉噸澶嶏紝璇蜂慨鏀筦 };
+ keys.add(key);
+ if (f.type === "select") {
+ const source = f.optionSource || SELECT_OPTION_SOURCE.STATIC;
+ if (isDynamicOptionSource(source)) continue;
+ const opts = (f.options || []).filter((o) => (o.label || "").trim() && o.value !== "" && o.value != null);
+ if (!opts.length) return { ok: false, message: `璇蜂负銆�${label}銆嶉厤缃嚦灏戜竴涓笅鎷夐�夐」` };
+ }
+ }
+ return { ok: true };
+}
+
+export function formFieldTypeLabel(type) {
+ return FORM_FIELD_TYPE_OPTIONS.find((x) => x.value === type)?.label || type || "鈥�";
+}
+
+export function formatDefaultValueDisplay(field) {
+ const dv = field?.defaultValue;
+ if (dv === undefined || dv === null || dv === "") return "鈥�";
+ if (field?.type === "datetimerange" && Array.isArray(dv)) {
+ return dv.length === 2 ? `${dv[0]} ~ ${dv[1]}` : "鈥�";
+ }
+ if (field?.type === "select") {
+ if (isDynamicOptionSource(field.optionSource)) {
+ return `${selectOptionSourceLabel(field.optionSource)}锛�${String(dv)}`;
+ }
+ const opt = (field.options || []).find((o) => String(o.value) === String(dv));
+ return opt?.label || String(dv);
+ }
+ return String(dv);
+}
+
+/** 灏嗗悗绔ā鏉胯杞负鎻愪氦椤垫ā鏉跨粨鏋勶紙鍚� fields 榛樿鍊笺�侀檮浠讹級 */
+export function buildSubmitTemplateFromRow(row) {
+ const cfg = parseFormConfigToData(row?.formConfig);
+ const fields = (cfg.fields || []).map(({ _uid, ...rest }) => ({
+ ...rest,
+ key: rest.key,
+ label: rest.label,
+ type: rest.type,
+ required: rest.required,
+ rows: rest.rows,
+ min: rest.min,
+ precision: rest.precision,
+ defaultValue: rest.defaultValue,
+ optionSource: rest.optionSource,
+ options: rest.options,
+ }));
+ return {
+ label: row?.templateName || "瀹℃壒",
+ businessType: row?.businessType ?? cfg.approvalType ?? "",
+ approvalType: cfg.approvalType || "",
+ summaryPlaceholder: cfg.summaryPlaceholder || "",
+ approvalMode: cfg.approvalMode || "parallel",
+ fields,
+ storageBlobDTOs: mapAttachmentsFromApi(row),
+ };
+}
+
+export function formConfigFieldsSummary(formConfigData) {
+ const fields = formConfigData?.fields || [];
+ if (!fields.length) return "鈥�";
+ return fields.map((f) => f.label || f.key || "鏈懡鍚�").join("銆�");
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
new file mode 100644
index 0000000..d094c13
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/index.vue
@@ -0,0 +1,819 @@
+<!--OA妯″潡锛氬鎵规ā鏉�-->
+
+<template>
+
+ <div class="app-container approve-template-page">
+
+ <div class="search_form mb20">
+
+ <div class="search_fields">
+
+ <span class="search_title">妯℃澘鍚嶇О锛�</span>
+
+ <el-input
+
+ v-model="searchForm.keyword"
+
+ style="width: 220px"
+
+ placeholder="鎼滅储鍚嶇О鎴栬鏄�"
+
+ clearable
+
+ :prefix-icon="Search"
+
+ @keyup.enter="handleQuery"
+
+ />
+
+ <el-checkbox v-model="searchForm.enabledOnly" class="ml12" @change="handleQuery">
+
+ 浠呮樉绀哄惎鐢�
+
+ </el-checkbox>
+
+ <el-button type="primary" :icon="Search" class="ml10" @click="handleQuery">鎼滅储</el-button>
+
+ <el-button :icon="RefreshRight" @click="resetSearch">閲嶇疆</el-button>
+
+ </div>
+
+ <div class="search_actions">
+
+ <el-button type="primary" :icon="Plus" @click="openFormDialog('add')">鏂板缓妯℃澘</el-button>
+
+ </div>
+
+ </div>
+
+
+
+ <div class="table_list">
+
+ <PIMTable
+
+ rowKey="id"
+
+ :column="tableColumn"
+
+ :tableData="tableData"
+
+ :page="page"
+
+ :isSelection="false"
+
+ :tableLoading="tableLoading"
+
+ :total="page.total"
+
+ @pagination="pagination"
+
+ />
+
+ </div>
+
+
+
+ <!-- 鏂板缓 / 缂栬緫 -->
+
+ <el-dialog
+
+ v-model="formDialog.visible"
+
+ :title="formDialog.title"
+
+ width="1020px"
+
+ append-to-body
+
+ destroy-on-close
+
+ class="template-form-dialog"
+
+ @closed="onFormDialogClosed"
+
+ >
+
+ <el-form
+
+ v-if="formDialog.visible"
+
+ ref="formRef"
+
+ :model="form"
+
+ :rules="formRules"
+
+ label-width="100px"
+
+ >
+
+ <el-row :gutter="20">
+
+ <el-col :span="8">
+
+ <el-form-item label="妯℃澘鍚嶇О" prop="templateName">
+
+ <el-input
+ v-model="form.templateName"
+ placeholder="濡傦細椤圭洰绔嬮」瀹℃壒"
+ maxlength="50"
+ show-word-limit
+ :disabled="isEditingBuiltin"
+ />
+
+ </el-form-item>
+
+ </el-col>
+
+ <el-col :span="8">
+
+ <el-form-item label="妯℃澘绫诲瀷" prop="businessType">
+
+ <el-select
+ v-model="form.businessType"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ :disabled="isEditingBuiltin"
+ >
+
+ <el-option
+
+ v-for="opt in templateTypeOptions"
+
+ :key="`tpl-type-${opt.value}`"
+
+ :label="opt.label"
+
+ :value="opt.value"
+
+ />
+
+ </el-select>
+
+ </el-form-item>
+
+ </el-col>
+
+ <el-col :span="8">
+
+ <el-form-item label="鍚敤鐘舵��">
+
+ <el-switch v-model="form.enabled" active-text="鍚敤" inactive-text="鍋滅敤" />
+
+ </el-form-item>
+
+ </el-col>
+
+ </el-row>
+
+ <el-form-item label="妯℃澘璇存槑">
+
+ <el-input
+
+ v-model="form.description"
+
+ type="textarea"
+
+ :rows="2"
+
+ placeholder="绠�瑕佽鏄庤妯℃澘鐨勯�傜敤鍦烘櫙"
+
+ maxlength="200"
+
+ show-word-limit
+
+ />
+
+ </el-form-item>
+
+ <el-form-item label="濉姤閰嶇疆">
+
+ <FormConfigEditor
+ v-model="form.formConfigData"
+ :exclude-template-id="form.id"
+ :disable-import="isEditingBuiltin"
+ :locked-field-uids="isEditingBuiltin ? form.lockedFormFieldUids : []"
+ />
+
+ <p class="flow-tip">閰嶇疆鎻愪氦瀹℃壒鏃堕渶濉啓鐨勮〃鍗曢」锛屼繚瀛樺悗鍐欏叆 formConfig锛圝SON锛夈��</p>
+
+ </el-form-item>
+
+ <el-form-item label="瀹℃壒娴佺▼" required>
+
+ <TemplateFlowEditor v-model="form.flowNodes" :user-options="flowUserOptions" />
+
+ <p class="flow-tip">
+
+ 鎸夐『搴忔祦杞細鍙负姣忎釜鑺傜偣娣诲姞澶氬悕瀹℃壒浜猴紱浼氱闇�鍏ㄩ儴閫氳繃锛屾垨绛句换涓�浜洪�氳繃鍗冲彲杩涘叆涓嬩竴鑺傜偣銆�
+
+ </p>
+
+ </el-form-item>
+
+ <el-form-item label="闄勪欢">
+
+ <div class="upload-block">
+
+ <FileUpload v-model:file-list="form.storageBlobDTOs" :limit="10" button-text="鐐瑰嚮閫夋嫨鏂囦欢" />
+
+ </div>
+
+ <p class="flow-tip">鍙笂浼犳ā鏉胯鏄庢枃妗c�佸埗搴︽枃浠剁瓑锛堥�夊~锛夈��</p>
+
+ </el-form-item>
+
+ </el-form>
+
+ <template #footer>
+
+ <el-button type="primary" @click="onSubmitForm">淇� 瀛�</el-button>
+
+ <el-button @click="formDialog.visible = false">鍙� 娑�</el-button>
+
+ </template>
+
+ </el-dialog>
+
+
+
+ <!-- 璇︽儏 -->
+
+ <el-dialog v-model="detailDialog.visible" title="妯℃澘璇︽儏" width="880px" append-to-body destroy-on-close>
+
+ <div v-loading="detailLoading" class="detail-dialog-body">
+
+ <el-descriptions :column="2" border>
+
+ <el-descriptions-item label="妯℃澘鍚嶇О">{{ detailRow.templateName }}</el-descriptions-item>
+
+ <el-descriptions-item label="妯℃澘绫诲瀷">{{ templateTypeLabel(detailRow.businessType) }}</el-descriptions-item>
+
+ <el-descriptions-item label="鐘舵��">
+
+ <el-tag :type="detailRow.enabled !== false ? 'success' : 'info'" size="small">
+
+ {{ detailRow.enabled !== false ? "鍚敤" : "鍋滅敤" }}
+
+ </el-tag>
+
+ </el-descriptions-item>
+
+ <el-descriptions-item label="璇存槑" :span="2">{{ detailRow.description || "鈥�" }}</el-descriptions-item>
+
+ <el-descriptions-item label="濉姤鎻愮ず" :span="2">
+
+ {{ detailFormConfig.summaryPlaceholder || "鈥�" }}
+
+ </el-descriptions-item>
+
+ <el-descriptions-item label="鍒涘缓浜�">{{ detailRow.createdUserName || "鈥�" }}</el-descriptions-item>
+
+ <el-descriptions-item label="鍒涘缓鏃堕棿">{{ formatDisplayTime(detailRow.createdTime) }}</el-descriptions-item>
+
+ <el-descriptions-item label="鏇存柊鏃堕棿">{{ formatDisplayTime(detailRow.updatedTime) }}</el-descriptions-item>
+
+ </el-descriptions>
+
+ <el-divider content-position="left">濉姤椤癸紙{{ detailFormConfig.fields?.length || 0 }} 椤癸級</el-divider>
+
+ <el-table
+
+ v-if="detailFormConfig.fields?.length"
+
+ :data="detailFormConfig.fields"
+
+ border
+
+ size="small"
+
+ class="mb16"
+
+ >
+
+ <el-table-column prop="label" label="鏄剧ず鍚嶇О" min-width="120" />
+
+ <el-table-column prop="key" label="瀛楁鏍囪瘑" min-width="100" />
+
+ <el-table-column label="绫诲瀷" width="100">
+
+ <template #default="{ row }">{{ formFieldTypeLabel(row.type) }}</template>
+
+ </el-table-column>
+
+ <el-table-column label="閫夐」鏉ユ簮" width="100">
+
+ <template #default="{ row }">
+
+ {{ row.type === 'select' ? selectOptionSourceLabel(row.optionSource) : '鈥�' }}
+
+ </template>
+
+ </el-table-column>
+
+ <el-table-column label="蹇呭~" width="70" align="center">
+
+ <template #default="{ row }">{{ row.required !== false ? "鏄�" : "鍚�" }}</template>
+
+ </el-table-column>
+
+ <el-table-column label="榛樿鍊�" min-width="120" show-overflow-tooltip>
+
+ <template #default="{ row }">{{ formatDefaultValueDisplay(row) }}</template>
+
+ </el-table-column>
+
+ </el-table>
+
+ <el-empty v-else description="鏈厤缃~鎶ラ」" :image-size="48" class="mb16" />
+
+ <el-divider content-position="left">瀹℃壒娴佺▼锛坽{ detailRow.flowNodes?.length || 0 }} 涓妭鐐癸級</el-divider>
+
+ <div v-if="detailRow.flowNodes?.length" class="detail-flow">
+
+ <div v-for="(node, index) in detailRow.flowNodes" :key="index" class="detail-node">
+
+ <div class="detail-node-head">
+
+ <span class="detail-node-order">鑺傜偣 {{ index + 1 }}</span>
+
+ <el-tag size="small" :type="node.signMode === 'or_sign' ? 'warning' : 'primary'">
+
+ {{ nodeSignModeLabel(node.signMode) }}
+
+ </el-tag>
+
+ </div>
+
+ <div class="detail-approvers">
+
+ <el-tag
+
+ v-for="a in node.approvers"
+
+ :key="String(a.approverId)"
+
+ class="detail-approver-tag"
+
+ effect="plain"
+
+ >
+
+ {{ a.approverName || "鈥�" }}
+
+ </el-tag>
+
+ <span v-if="!node.approvers?.length" class="text-muted">鏈厤缃鎵逛汉</span>
+
+ </div>
+
+ <el-icon v-if="index < detailRow.flowNodes.length - 1" class="detail-arrow"><ArrowRight /></el-icon>
+
+ </div>
+
+ </div>
+
+ <el-empty v-else description="鏆傛棤娴佺▼鑺傜偣" :image-size="60" />
+
+ <el-divider content-position="left">闄勪欢锛坽{ detailAttachments.length }} 涓級</el-divider>
+
+ <template v-if="detailAttachments.length">
+
+ <el-tag
+
+ v-for="(f, i) in detailAttachments"
+
+ :key="i"
+
+ class="detail-attachment-tag"
+
+ type="info"
+
+ effect="plain"
+
+ >
+
+ {{ attachmentDisplayName(f) }}
+
+ </el-tag>
+
+ </template>
+
+ <el-empty v-else description="鏆傛棤闄勪欢" :image-size="48" />
+
+ </div>
+
+ <template #footer>
+
+ <el-button @click="detailDialog.visible = false">鍏� 闂�</el-button>
+
+ <el-button type="primary" @click="editFromDetail">缂� 杈�</el-button>
+
+ </template>
+
+ </el-dialog>
+
+ </div>
+
+</template>
+
+
+
+<script setup>
+
+import { ArrowRight, Plus, RefreshRight } from "@element-plus/icons-vue";
+
+import { ElMessage } from "element-plus";
+
+import { computed, nextTick, onMounted, ref } from "vue";
+
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+
+import FormConfigEditor from "./components/FormConfigEditor.vue";
+
+import TemplateFlowEditor from "./components/TemplateFlowEditor.vue";
+
+import { formatDisplayTime, mapAttachmentsFromApi } from "./approveTemplateConstants.js";
+
+import { formatDefaultValueDisplay, formFieldTypeLabel, parseFormConfigToData } from "./formConfigUtils.js";
+import { selectOptionSourceLabel } from "./selectOptionSource.js";
+
+import { useApproveTemplate } from "./useApproveTemplate.js";
+
+
+
+const {
+
+ Search,
+
+ templateTypeOptions,
+
+ loadTemplateTypeOptions,
+
+ templateTypeLabel,
+
+ nodeSignModeLabel,
+
+ searchForm,
+
+ tableLoading,
+
+ page,
+
+ tableData,
+
+ tableColumn,
+
+ formDialog,
+
+ form,
+
+ formRef,
+
+ formRules,
+
+ isEditingBuiltin,
+
+ detailDialog,
+
+ detailRow,
+
+ detailLoading,
+
+ fetchTemplateList,
+
+ handleQuery,
+
+ resetSearch,
+
+ pagination,
+
+ openFormDialog,
+
+ openDetail,
+
+ submitForm,
+
+} = useApproveTemplate();
+
+
+
+const flowUserOptions = ref([]);
+
+
+
+const detailFormConfig = computed(() =>
+
+ parseFormConfigToData(detailRow.value?.formConfigData ?? detailRow.value?.formConfig)
+
+);
+
+
+
+const detailAttachments = computed(() => mapAttachmentsFromApi(detailRow.value));
+
+
+
+function attachmentDisplayName(file) {
+
+ if (!file) return "鏈懡鍚�";
+
+ return file.name || file.originalFilename || file.fileName || "鏈懡鍚�";
+
+}
+
+
+
+function unwrapArray(payload) {
+
+ if (Array.isArray(payload)) return payload;
+
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+
+ return [];
+
+}
+
+
+
+function isActiveUser(u) {
+
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+
+ if (u.status == null) return true;
+
+ return String(u.status) === "0";
+
+}
+
+
+
+async function loadUsers() {
+
+ try {
+
+ const res = await userListNoPageByTenantId();
+
+ flowUserOptions.value = unwrapArray(res).filter(isActiveUser);
+
+ } catch {
+
+ flowUserOptions.value = [];
+
+ }
+
+}
+
+
+
+async function onSubmitForm() {
+
+ const ret = await submitForm();
+
+ if (ret?.message) {
+
+ ElMessage.warning(ret.message);
+
+ return;
+
+ }
+
+ if (ret?.ok) ElMessage.success("淇濆瓨鎴愬姛");
+
+}
+
+
+
+function onFormDialogClosed() {
+
+ formRef.value?.resetFields?.();
+
+}
+
+
+
+async function editFromDetail() {
+
+ const row = detailRow.value;
+
+ detailDialog.visible = false;
+
+ await nextTick();
+
+ openFormDialog("edit", row);
+
+}
+
+
+
+onMounted(() => {
+
+ loadUsers();
+
+ loadTemplateTypeOptions();
+
+ fetchTemplateList();
+
+});
+
+</script>
+
+
+
+<style scoped>
+
+.mb20 {
+
+ margin-bottom: 20px;
+
+}
+
+.mb16 {
+
+ margin-bottom: 16px;
+
+}
+
+.mb16.el-empty {
+
+ padding: 8px 0;
+
+}
+
+.ml10 {
+
+ margin-left: 10px;
+
+}
+
+.ml12 {
+
+ margin-left: 12px;
+
+}
+
+.search_form {
+
+ display: flex;
+
+ flex-wrap: wrap;
+
+ align-items: center;
+
+ justify-content: space-between;
+
+ gap: 12px;
+
+}
+
+.search_fields {
+
+ display: flex;
+
+ flex-wrap: wrap;
+
+ align-items: center;
+
+ gap: 4px;
+
+}
+
+.search_actions {
+
+ display: flex;
+
+ gap: 8px;
+
+}
+
+.flow-tip {
+
+ font-size: 12px;
+
+ color: var(--el-text-color-secondary);
+
+ margin: 8px 0 0;
+
+ line-height: 1.5;
+
+}
+
+.detail-flow {
+
+ display: flex;
+
+ flex-wrap: wrap;
+
+ align-items: flex-start;
+
+ gap: 8px;
+
+}
+
+.detail-node {
+
+ position: relative;
+
+ min-width: 180px;
+
+ max-width: 240px;
+
+ padding: 12px;
+
+ border: 1px solid var(--el-border-color-lighter);
+
+ border-radius: 8px;
+
+ background: var(--el-fill-color-lighter);
+
+}
+
+.detail-node-head {
+
+ display: flex;
+
+ align-items: center;
+
+ justify-content: space-between;
+
+ margin-bottom: 8px;
+
+}
+
+.detail-node-order {
+
+ font-weight: 600;
+
+ font-size: 13px;
+
+}
+
+.detail-approvers {
+
+ display: flex;
+
+ flex-wrap: wrap;
+
+ gap: 4px;
+
+}
+
+.detail-approver-tag {
+
+ margin: 0;
+
+}
+
+.detail-arrow {
+
+ position: absolute;
+
+ right: -20px;
+
+ top: 50%;
+
+ transform: translateY(-50%);
+
+ color: var(--el-text-color-placeholder);
+
+}
+
+.detail-dialog-body {
+
+ min-height: 120px;
+
+}
+
+.upload-block {
+
+ width: 100%;
+
+}
+
+.detail-attachment-tag {
+
+ margin: 0 8px 8px 0;
+
+}
+
+.text-muted {
+
+ font-size: 12px;
+
+ color: var(--el-text-color-placeholder);
+
+}
+
+.template-form-dialog :deep(.el-dialog__body) {
+
+ padding-top: 8px;
+
+}
+
+</style>
+
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js
new file mode 100644
index 0000000..99706b4
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/selectOptionSource.js
@@ -0,0 +1,140 @@
+import { deptTreeSelect, userListNoPageByTenantId } from "@/api/system/user.js";
+
+/** 涓嬫媺閫夐」鏉ユ簮锛堝啓鍏� formConfig锛屾彁浜ら〉鎸夋潵婧愭媺鍙栨暟鎹級 */
+export const SELECT_OPTION_SOURCE = {
+ STATIC: "static",
+ USER: "user",
+ DEPT: "dept",
+};
+
+export const SELECT_OPTION_SOURCE_OPTIONS = [
+ { value: SELECT_OPTION_SOURCE.STATIC, label: "鎵嬪姩閰嶇疆", desc: "鍦ㄦā鏉夸腑鑷畾涔夐�夐」鏂囨湰涓庡��" },
+ { value: SELECT_OPTION_SOURCE.USER, label: "浜哄憳鍒楄〃", desc: "浠庣郴缁熺敤鎴蜂腑閫夋嫨锛屽�间负鐢ㄦ埛 ID" },
+ { value: SELECT_OPTION_SOURCE.DEPT, label: "閮ㄩ棬鍒楄〃", desc: "浠庣粍缁囨灦鏋勪腑閫夋嫨锛屽�间负閮ㄩ棬 ID" },
+];
+
+export function selectOptionSourceLabel(source) {
+ return SELECT_OPTION_SOURCE_OPTIONS.find((x) => x.value === source)?.label || "鈥�";
+}
+
+export function isDynamicOptionSource(source) {
+ return source === SELECT_OPTION_SOURCE.USER || source === SELECT_OPTION_SOURCE.DEPT;
+}
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+/** 鐢ㄦ埛 鈫� 涓嬫媺 option */
+export function mapUserToSelectOption(u) {
+ const value = u.userId ?? u.id;
+ return {
+ label: u.nickName || u.userName || `鐢ㄦ埛${value}`,
+ value,
+ };
+}
+
+/** 閮ㄩ棬鏍戞媿骞充负涓嬫媺 option */
+export function flattenDeptToSelectOptions(nodes, result = []) {
+ (nodes || []).forEach((node) => {
+ const value = node.id ?? node.deptId ?? node.value;
+ if (value != null && value !== "") {
+ result.push({
+ label: node.label ?? node.deptName ?? node.name ?? String(value),
+ value,
+ });
+ }
+ if (node.children?.length) flattenDeptToSelectOptions(node.children, result);
+ });
+ return result;
+}
+
+function filterDisabledDept(deptList) {
+ if (!Array.isArray(deptList)) return [];
+ return deptList.filter((dept) => {
+ if (dept.disabled) return false;
+ if (dept.children?.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+}
+
+/** 鎸夊瓧娈甸厤缃В鏋愪笅鎷� options锛堥渶浼犲叆宸插姞杞界殑缂撳瓨锛� */
+export function resolveFieldSelectOptions(field, caches = {}) {
+ const source = field?.optionSource || SELECT_OPTION_SOURCE.STATIC;
+ if (source === SELECT_OPTION_SOURCE.USER) {
+ return (caches.users || []).map(mapUserToSelectOption);
+ }
+ if (source === SELECT_OPTION_SOURCE.DEPT) {
+ return caches.deptOptions || [];
+ }
+ return (field?.options || []).filter((o) => o.value !== "" && o.value != null);
+}
+
+/** 鏍规嵁宸茶В鏋愮殑 options 鍙嶆煡灞曠ず鏂囨湰 */
+export function resolveSelectDisplayLabel(field, val, caches = {}) {
+ if (val == null || val === "") return "鈥�";
+ const options = resolveFieldSelectOptions(field, caches);
+ const hit = options.find((o) => String(o.value) === String(val));
+ return hit?.label || String(val);
+}
+
+/** 鍔犺浇浜哄憳 / 閮ㄩ棬缂撳瓨锛堝澶勫鐢級 */
+export async function fetchSelectOptionCaches(sources = []) {
+ const needUser = sources.includes(SELECT_OPTION_SOURCE.USER);
+ const needDept = sources.includes(SELECT_OPTION_SOURCE.DEPT);
+ const caches = { users: [], deptOptions: [] };
+
+ if (!needUser && !needDept) return caches;
+
+ const tasks = [];
+ if (needUser) {
+ tasks.push(
+ userListNoPageByTenantId()
+ .then((res) => {
+ caches.users = unwrapArray(res).filter(isActiveUser);
+ })
+ .catch(() => {
+ caches.users = [];
+ })
+ );
+ }
+ if (needDept) {
+ tasks.push(
+ deptTreeSelect()
+ .then((res) => {
+ let tree = unwrapArray(res);
+ tree = tree.length ? filterDisabledDept(JSON.parse(JSON.stringify(tree))) : [];
+ if (!tree.length) tree = unwrapArray(res);
+ caches.deptOptions = flattenDeptToSelectOptions(tree);
+ })
+ .catch(() => {
+ caches.deptOptions = [];
+ })
+ );
+ }
+
+ await Promise.all(tasks);
+ return caches;
+}
+
+/** 浠庡瓧娈靛垪琛ㄦ敹闆嗛渶瑕侀鍔犺浇鐨勫姩鎬佹潵婧� */
+export function collectOptionSourcesFromFields(fields) {
+ const set = new Set();
+ (fields || []).forEach((f) => {
+ if (f?.type === "select" && isDynamicOptionSource(f.optionSource)) {
+ set.add(f.optionSource);
+ }
+ });
+ return [...set];
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
new file mode 100644
index 0000000..61aa6c0
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useApproveTemplate.js
@@ -0,0 +1,334 @@
+import {
+ addApprovalTemplate,
+ deleteApprovalTemplate,
+ getApprovalTemplateDetail,
+ listApprovalTemplatePage,
+ TEMPLATE_TYPE_BUILTIN,
+ updateApprovalTemplate,
+} from "@/api/officeProcessAutomation/approvalTemplate.js";
+import { Search } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { computed, reactive, ref } from "vue";
+import {
+ buildApprovalTemplateListParams,
+ createEmptyTemplateForm,
+ fetchBusinessTypeOptions,
+ flowNodesSummary,
+ isBuiltinTemplate,
+ mapTemplateFromApi,
+ mapTemplateToApi,
+ nodeSignModeLabel,
+ formatDisplayTime,
+ unwrapTemplateDetail,
+ validateTemplateForm,
+} from "./approveTemplateConstants.js";
+import { parseFormConfigToData } from "./formConfigUtils.js";
+
+const FALLBACK_TEMPLATE_TYPE_OPTIONS = [
+ { value: 0, label: "绯荤粺鍐呯疆" },
+ { value: 1, label: "鑷畾涔�" },
+];
+
+function matchTemplateTypeValue(options, type) {
+ if (type == null || type === "") return false;
+ return options.some(
+ (x) => x.value === type || x.value === Number(type) || String(x.value) === String(type)
+ );
+}
+
+export function useApproveTemplate() {
+ const templateTypeOptions = ref([...FALLBACK_TEMPLATE_TYPE_OPTIONS]);
+
+ function templateTypeLabel(type) {
+ if (type == null || type === "") return "鈥�";
+ const hit = templateTypeOptions.value.find(
+ (x) => x.value === type || x.value === Number(type) || String(x.value) === String(type)
+ );
+ return hit?.label || "鈥�";
+ }
+
+ const searchForm = reactive({
+ keyword: "",
+ enabledOnly: false,
+ });
+
+ const tableLoading = ref(false);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const tableData = ref([]);
+
+ const formDialog = reactive({ visible: false, title: "", mode: "add" });
+ const form = reactive(createEmptyTemplateForm());
+ const formRef = ref();
+
+ const isEditingBuiltin = computed(
+ () => formDialog.mode === "edit" && Number(form.templateType) === TEMPLATE_TYPE_BUILTIN
+ );
+
+ async function loadTemplateTypeOptions() {
+ try {
+ const list = await fetchBusinessTypeOptions();
+ templateTypeOptions.value = list.length ? list : [...FALLBACK_TEMPLATE_TYPE_OPTIONS];
+ } catch {
+ templateTypeOptions.value = [...FALLBACK_TEMPLATE_TYPE_OPTIONS];
+ }
+ if (!matchTemplateTypeValue(templateTypeOptions.value, form.businessType)) {
+ form.businessType = templateTypeOptions.value[0]?.value ?? "";
+ }
+ }
+
+ const detailDialog = reactive({ visible: false });
+ const detailRow = ref({});
+ const detailLoading = ref(false);
+
+ const formRules = {
+ templateName: [{ required: true, message: "璇疯緭鍏ユā鏉垮悕绉�", trigger: "blur" }],
+ businessType: [{ required: true, message: "璇烽�夋嫨妯℃澘绫诲瀷", trigger: "change" }],
+ };
+
+ const tableColumn = ref([
+ { label: "妯℃澘鍚嶇О", prop: "templateName", minWidth: 140 },
+ {
+ label: "妯℃澘绫诲瀷",
+ prop: "businessType",
+ width: 100,
+ align: "center",
+ formatData: (v) => templateTypeLabel(v),
+ },
+ { label: "璇存槑", prop: "description", minWidth: 160, showOverflowTooltip: true },
+ {
+ label: "鑺傜偣鏁�",
+ prop: "flowNodes",
+ width: 80,
+ align: "center",
+ formatData: (v) => (Array.isArray(v) ? v.length : 0),
+ },
+ {
+ label: "娴佺▼姒傝",
+ prop: "flowNodes",
+ minWidth: 220,
+ showOverflowTooltip: true,
+ formatData: (v) => flowNodesSummary(v),
+ },
+ {
+ label: "鐘舵��",
+ prop: "enabled",
+ width: 90,
+ align: "center",
+ dataType: "tag",
+ formatData: (v) => (v !== false ? "鍚敤" : "鍋滅敤"),
+ formatType: (v) => (v !== false ? "success" : "info"),
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createdTime",
+ width: 170,
+ showOverflowTooltip: true,
+ formatData: (v) => formatDisplayTime(v),
+ },
+ {
+ label: "鏇存柊鏃堕棿",
+ prop: "updatedTime",
+ width: 170,
+ showOverflowTooltip: true,
+ formatData: (v) => formatDisplayTime(v),
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 220,
+ operation: [
+ { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+ { name: "缂栬緫", type: "text", clickFun: (row) => openFormDialog("edit", row) },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ link: true,
+ disabled: (row) => isBuiltinTemplate(row),
+ clickFun: (row) => removeTemplate(row),
+ },
+ ],
+ },
+ ]);
+
+ async function fetchTemplateList() {
+ tableLoading.value = true;
+ try {
+ const res = await listApprovalTemplatePage(
+ buildApprovalTemplateListParams({ page, searchForm })
+ );
+ const data = res?.data || {};
+ tableData.value = (data.records || []).map(mapTemplateFromApi);
+ page.total = Number(data.total || 0);
+ } catch {
+ tableData.value = [];
+ page.total = 0;
+ } finally {
+ tableLoading.value = false;
+ }
+ }
+
+ function handleQuery() {
+ page.current = 1;
+ fetchTemplateList();
+ }
+
+ function resetSearch() {
+ searchForm.keyword = "";
+ searchForm.enabledOnly = false;
+ handleQuery();
+ }
+
+ function pagination({ page: p, limit }) {
+ page.current = p;
+ page.size = limit;
+ fetchTemplateList();
+ }
+
+ function resetForm(row) {
+ const base = createEmptyTemplateForm();
+ if (!row) {
+ Object.assign(form, base);
+ return;
+ }
+ const formConfigData = JSON.parse(
+ JSON.stringify(row.formConfigData || parseFormConfigToData(row.formConfig))
+ );
+ const builtin = isBuiltinTemplate(row);
+ Object.assign(form, {
+ ...base,
+ id: row.id,
+ templateName: row.templateName || "",
+ description: row.description || "",
+ templateType: row.templateType != null ? Number(row.templateType) : base.templateType,
+ businessType: row.businessType ?? "",
+ formConfig: row.formConfig || "",
+ formConfigData,
+ lockedFormFieldUids: builtin
+ ? (formConfigData.fields || []).map((f) => f._uid).filter(Boolean)
+ : [],
+ enabled: row.enabled !== false,
+ flowNodes: JSON.parse(JSON.stringify(row.flowNodes || [base.flowNodes[0]])),
+ storageBlobDTOs: JSON.parse(JSON.stringify(row.storageBlobDTOs || [])),
+ });
+ }
+
+ function openFormDialog(mode, row) {
+ formDialog.mode = mode;
+ formDialog.title = mode === "add" ? "鏂板缓瀹℃壒妯℃澘" : "缂栬緫瀹℃壒妯℃澘";
+ resetForm(mode === "edit" ? row : null);
+ formDialog.visible = true;
+ }
+
+ async function openDetail(row) {
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鏌ョ湅璇︽儏锛氱己灏戞ā鏉� ID");
+ return;
+ }
+ detailDialog.visible = true;
+ detailLoading.value = true;
+ detailRow.value = {};
+ try {
+ const res = await getApprovalTemplateDetail(row.id);
+ detailRow.value = mapTemplateFromApi(unwrapTemplateDetail(res));
+ } catch {
+ detailDialog.visible = false;
+ } finally {
+ detailLoading.value = false;
+ }
+ }
+
+ async function submitForm() {
+ if (!formRef.value) return false;
+ try {
+ await formRef.value.validate();
+ } catch {
+ return false;
+ }
+ const validated = validateTemplateForm(form);
+ if (!validated.ok) {
+ return { message: validated.message };
+ }
+ if (formDialog.mode === "edit" && !form.id) {
+ return { message: "缂哄皯妯℃澘 ID锛屾棤娉曚繚瀛樹慨鏀�" };
+ }
+ const dto = mapTemplateToApi(form);
+ try {
+ if (formDialog.mode === "add") {
+ await addApprovalTemplate(dto);
+ } else {
+ await updateApprovalTemplate(dto);
+ }
+ } catch {
+ return false;
+ }
+ formDialog.visible = false;
+ page.current = 1;
+ await fetchTemplateList();
+ return { ok: true };
+ }
+
+ async function removeTemplate(row) {
+ if (isBuiltinTemplate(row)) {
+ ElMessage.warning("绯荤粺鍐呯疆妯℃澘涓嶅厑璁稿垹闄�");
+ return;
+ }
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戞ā鏉� ID");
+ return;
+ }
+ const name = row.templateName || "鏈懡鍚嶆ā鏉�";
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゅ鎵规ā鏉裤��${name}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
+ } catch {
+ return;
+ }
+ try {
+ await deleteApprovalTemplate([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await fetchTemplateList();
+ } catch {
+ /* 閿欒鐢辨嫤鎴櫒鎻愮ず */
+ }
+ }
+
+ return {
+ Search,
+ templateTypeOptions,
+ loadTemplateTypeOptions,
+ templateTypeLabel,
+ fetchTemplateList,
+ nodeSignModeLabel,
+ flowNodesSummary,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ formDialog,
+ form,
+ formRef,
+ formRules,
+ isEditingBuiltin,
+ detailDialog,
+ detailRow,
+ detailLoading,
+ handleQuery,
+ resetSearch,
+ pagination,
+ openFormDialog,
+ openDetail,
+ submitForm,
+ };
+}
diff --git a/src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js b/src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js
new file mode 100644
index 0000000..8397288
--- /dev/null
+++ b/src/views/officeProcessAutomation/ApproveManage/approve-template/useSelectOptionSources.js
@@ -0,0 +1,45 @@
+import { reactive, ref } from "vue";
+import {
+ collectOptionSourcesFromFields,
+ fetchSelectOptionCaches,
+ resolveFieldSelectOptions,
+ resolveSelectDisplayLabel,
+} from "./selectOptionSource.js";
+
+/** 涓嬫媺鍔ㄦ�侀�夐」锛氫汉鍛� / 閮ㄩ棬缂撳瓨涓庤В鏋� */
+export function useSelectOptionSources() {
+ const loading = ref(false);
+ const caches = reactive({
+ users: [],
+ deptOptions: [],
+ });
+
+ async function ensureForFields(fields) {
+ const sources = collectOptionSourcesFromFields(fields);
+ if (!sources.length) return;
+ loading.value = true;
+ try {
+ const next = await fetchSelectOptionCaches(sources);
+ caches.users = next.users;
+ caches.deptOptions = next.deptOptions;
+ } finally {
+ loading.value = false;
+ }
+ }
+
+ function getOptions(field) {
+ return resolveFieldSelectOptions(field, caches);
+ }
+
+ function getDisplayLabel(field, val) {
+ return resolveSelectDisplayLabel(field, val, caches);
+ }
+
+ return {
+ loading,
+ caches,
+ ensureForFields,
+ getOptions,
+ getDisplayLabel,
+ };
+}
diff --git a/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
new file mode 100644
index 0000000..4d800df
--- /dev/null
+++ b/src/views/officeProcessAutomation/AttendManage/leave-apply/index.vue
@@ -0,0 +1,325 @@
+<!--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>
+ <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"
+ flow-attachments-only
+ @submit="onSubmit"
+ >
+ <template #before="{ form, fields }">
+ <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" />
+ <el-row :gutter="24">
+ <el-col :span="12">
+ <el-form-item label="鍋囨湡浣欓" prop="leaveBalanceDays">
+ <el-input-number
+ v-model="form.leaveBalanceDays"
+ :min="0"
+ :max="999"
+ :precision="2"
+ :step="0.5"
+ controls-position="right"
+ placeholder="澶�"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇峰亣鏃堕暱">
+ <el-input :model-value="leaveDurationDisplay(form)" readonly placeholder="鏍规嵁妯℃澘涓鍋囨椂闂磋嚜鍔ㄨ绠�">
+ <template #append>澶�</template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </template>
+ </ApprovalInstanceSubmitDialog>
+
+ <ApprovalTemplateBindDialog
+ v-model:visible="templateBindVisible"
+ :module-key="APPROVAL_MODULE_KEYS.LEAVE"
+ 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 dayjs from "dayjs";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { ElMessage } from "element-plus";
+import { computed, onMounted, reactive, ref, watch } from "vue";
+import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.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";
+import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js";
+
+const LEAVE_TYPE_OPTIONS = [
+ { label: "骞村亣", value: "annual" },
+ { label: "鐥呭亣", value: "sick" },
+ { label: "浜嬪亣", value: "personal" },
+ { label: "濠氬亣", value: "marriage" },
+ { label: "浜у亣", value: "maternity" },
+ { label: "鍝轰钩鍋�", value: "nursing" },
+ { label: "鎱板攣鍋�", value: "condolence" },
+ { label: "璋冧紤", value: "compensatory" },
+];
+
+function isLeaveBalanceField(field) {
+ const label = String(field?.label || "");
+ return label.includes("鍋囨湡浣欓") || field?.key === "leaveBalanceDays";
+}
+
+function isLeaveDurationField(field) {
+ const label = String(field?.label || "");
+ return label.includes("璇峰亣鏃堕暱") || field?.key === "leaveDurationDays";
+}
+
+function displayTemplateFields(fields = []) {
+ return (fields || []).filter((f) => !isLeaveBalanceField(f) && !isLeaveDurationField(f));
+}
+
+function findLeaveTimeTemplateField(fields = []) {
+ return (
+ fields.find((f) => f?.type === "datetimerange" && String(f?.label || "").includes("璇峰亣鏃堕棿")) ||
+ fields.find((f) => f?.type === "datetimerange" && f?.key === "dateRange") ||
+ fields.find((f) => f?.type === "datetimerange") ||
+ null
+ );
+}
+
+function findApplicantTemplateField(fields = []) {
+ return (
+ fields.find((f) => String(f?.label || "").includes("鐢宠浜�")) ||
+ fields.find((f) => f?.type === "select" && f?.optionSource === SELECT_OPTION_SOURCE.USER) ||
+ null
+ );
+}
+
+function resolveLeaveTimeRange(payload, leaveTimeField) {
+ if (!leaveTimeField?.key) return { start: "", end: "" };
+ const val = payload?.[leaveTimeField.key];
+ if (!Array.isArray(val) || val.length < 2) return { start: "", end: "" };
+ return { start: val[0] || "", end: val[1] || "" };
+}
+
+function computeLeaveDays(startStr, endStr) {
+ if (!startStr || !endStr) return null;
+ const t0 = dayjs(startStr);
+ const t1 = dayjs(endStr);
+ if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
+ const days = t1.diff(t0, "millisecond") / (24 * 60 * 60 * 1000);
+ return Math.round(days * 100) / 100;
+}
+
+function leaveDurationDisplay(form) {
+ const leaveTimeField = findLeaveTimeTemplateField(form.formFieldDefs);
+ const { start, end } = resolveLeaveTimeRange(form.formPayload, leaveTimeField);
+ const d = computeLeaveDays(start, end);
+ return d == null ? "" : String(d);
+}
+
+const searchForm = reactive({
+ instanceNo: "",
+ applicantKeyword: "",
+});
+
+function validateLeaveBeforeSave() {
+ const leaveTimeField = findLeaveTimeTemplateField(submitForm.formFieldDefs);
+ const { start, end } = resolveLeaveTimeRange(submitForm.formPayload, leaveTimeField);
+ if (computeLeaveDays(start, end) == null) {
+ ElMessage.warning("璇锋鏌ユā鏉夸腑鐨勮鍋囨椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�");
+ throw new Error("invalid leave time");
+ }
+}
+
+const mod = useApprovalInstanceModule({
+ moduleKey: APPROVAL_MODULE_KEYS.LEAVE,
+ beforeSave: validateLeaveBeforeSave,
+ extraFormRules: {
+ leaveBalanceDays: [{ required: true, message: "璇峰~鍐欏亣鏈熶綑棰�", trigger: "blur" }],
+ },
+});
+
+const {
+ tableData,
+ tableLoading,
+ page,
+ detailDialog,
+ detailRow,
+ submitDialog,
+ submitEditRow,
+ submitForm,
+ submitFormRef,
+ submitSaving,
+ isSubmitEdit,
+ activeTemplate,
+ submitFormFields,
+ submitFormRules,
+ submitDialogTitle,
+ templateBindVisible,
+ handleQuery,
+ initModuleList,
+ pagination,
+ openAddWithTemplate,
+ onTemplateBound,
+ onTemplateBindClosed,
+ openEditFromDetail,
+ submitInstanceForm,
+ buildTableActions,
+} = mod;
+
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
+const allUsersCache = ref([]);
+
+const applicantTemplateField = computed(() =>
+ findApplicantTemplateField(submitForm.formFieldDefs)
+);
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload && Array.isArray(payload.data)) return payload.data;
+ return [];
+}
+
+async function loadUserPool() {
+ try {
+ const res = await userListNoPageByTenantId();
+ allUsersCache.value = unwrapArray(res);
+ } catch {
+ allUsersCache.value = [];
+ }
+}
+
+watch(
+ () => submitDialog.visible,
+ (v) => {
+ if (!v) return;
+ if (submitForm.leaveBalanceDays == null && isSubmitEdit.value) {
+ submitForm.leaveBalanceDays =
+ submitEditRow.value?.formPayload?.leaveBalanceDays ??
+ submitEditRow.value?.leaveBalanceDays;
+ }
+ if (submitForm.leaveBalanceDays == null && !isSubmitEdit.value) {
+ submitForm.leaveBalanceDays = undefined;
+ }
+ }
+);
+
+watch(
+ () => {
+ const key = applicantTemplateField.value?.key;
+ return key ? submitForm.formPayload[key] : undefined;
+ },
+ async (uid) => {
+ if (!applicantTemplateField.value || !uid) return;
+ if (!allUsersCache.value.length) await loadUserPool();
+ }
+);
+
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, {
+ moduleKey: APPROVAL_MODULE_KEYS.LEAVE,
+});
+
+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_title {
+ font-size: 14px;
+ color: var(--el-text-color-regular);
+}
+</style>
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue
new file mode 100644
index 0000000..9e3ada5
--- /dev/null
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue
@@ -0,0 +1,360 @@
+<!-- 鍔犵彮鐢宠妯″潡鍐咃細鍙鍒犲鎵硅妭鐐癸紝姣忚妭鐐瑰繀閫� 1 浜� -->
+<template>
+ <div class="afe">
+ <div v-if="innerList.length" class="afe-flow">
+ <div v-for="(item, index) in innerList" :key="item._uid" class="afe-flow-item">
+ <div class="afe-card" :class="{ 'afe-card--empty': !item.approverId }">
+ <div class="afe-badge">{{ index + 1 }}</div>
+ <div class="afe-avatar-wrap">
+ <div
+ class="afe-avatar"
+ :class="{ 'afe-avatar--on': item.approverId }"
+ :style="item.approverId ? { backgroundColor: avatarColor(item.approverName) } : {}"
+ >
+ <span v-if="item.approverId">{{ (item.approverName || '?').charAt(0) }}</span>
+ <el-icon v-else :size="22"><User /></el-icon>
+ </div>
+ <div class="afe-level">{{ levelText(index) }}</div>
+ </div>
+ <div class="afe-select">
+ <el-select
+ v-model="item.approverId"
+ placeholder="璇烽�夋嫨瀹℃壒浜�"
+ filterable
+ clearable
+ style="width: 100%"
+ @change="(v) => onPick(v, item)"
+ >
+ <el-option
+ v-for="u in userOptions"
+ :key="String(u.userId ?? u.id)"
+ :label="optionLabel(u)"
+ :value="u.userId ?? u.id"
+ />
+ </el-select>
+ </div>
+ <div class="afe-actions">
+ <el-button type="primary" circle size="small" :disabled="index === 0" title="鍓嶇Щ" @click="moveLeft(index)">
+ <el-icon><ArrowLeft /></el-icon>
+ </el-button>
+ <el-button
+ type="primary"
+ circle
+ size="small"
+ :disabled="index === innerList.length - 1"
+ title="鍚庣Щ"
+ @click="moveRight(index)"
+ >
+ <el-icon><ArrowRight /></el-icon>
+ </el-button>
+ <el-button type="danger" circle size="small" title="鍒犻櫎鑺傜偣" @click="remove(index)">
+ <el-icon><Delete /></el-icon>
+ </el-button>
+ </div>
+ </div>
+ <div v-if="index < innerList.length - 1" class="afe-conn">
+ <div class="afe-conn-line"></div>
+ <el-icon class="afe-conn-icon"><ArrowRight /></el-icon>
+ </div>
+ </div>
+
+ <div class="afe-add-wrap">
+ <div class="afe-conn" v-if="innerList.length">
+ <div class="afe-conn-line"></div>
+ <el-icon class="afe-conn-icon"><ArrowRight /></el-icon>
+ </div>
+ <div class="afe-add-card" @click="addNode">
+ <div class="afe-add-icon"><el-icon :size="26"><Plus /></el-icon></div>
+ <span>鏂板鑺傜偣</span>
+ </div>
+ </div>
+ </div>
+
+ <div v-else class="afe-empty">
+ <el-icon :size="44" color="#c0c4cc"><User /></el-icon>
+ <p>鏆傛棤瀹℃壒鑺傜偣</p>
+ <el-button type="primary" @click="addNode">娣诲姞绗竴涓妭鐐�</el-button>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ArrowLeft, ArrowRight, Delete, Plus, User } from "@element-plus/icons-vue";
+import { ref, watch } from "vue";
+
+const props = defineProps({
+ modelValue: { type: Array, default: () => [] },
+ /** 涓庣埗椤� userList 缁撴瀯涓�鑷达細userId / id銆乶ickName銆乽serName */
+ userOptions: { type: Array, default: () => [] },
+});
+
+const emit = defineEmits(["update:modelValue"]);
+
+const innerList = ref([]);
+
+const palette = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C", "#9B59B6", "#1ABC9C"];
+
+function avatarColor(name) {
+ if (!name) return "#c0c4cc";
+ let h = 0;
+ for (let i = 0; i < name.length; i++) h = name.charCodeAt(i) + ((h << 5) - h);
+ return palette[Math.abs(h) % palette.length];
+}
+
+function levelText(i) {
+ const t = ["绗竴绾�", "绗簩绾�", "绗笁绾�", "绗洓绾�", "绗簲绾�", "绗叚绾�", "绗竷绾�", "绗叓绾�"];
+ return t[i] || `绗�${i + 1}绾;
+}
+
+function optionLabel(u) {
+ const nick = u.nickName || "";
+ const un = u.userName || "";
+ if (nick && un && nick !== un) return `${nick}锛�${un}锛塦;
+ return nick || un || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
+}
+
+function newUid() {
+ return `n_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
+}
+
+function mapIn(rows) {
+ if (!Array.isArray(rows)) return [];
+ return rows.map((r, i) => ({
+ _uid: newUid(),
+ approverId: r.approverId ?? r.approver_id ?? null,
+ approverName: r.approverName ?? r.approver_name ?? "",
+ sortOrder: r.sortOrder ?? r.nodeOrder ?? i + 1,
+ nodeOrder: r.nodeOrder ?? r.sortOrder ?? i + 1,
+ roleName: r.roleName ?? "",
+ roleCode: r.roleCode ?? "",
+ }));
+}
+
+function publicShape(rows) {
+ const arr = Array.isArray(rows) ? rows : [];
+ return arr.map((r, i) => ({
+ approverId: r.approverId ?? null,
+ approverName: r.approverName ?? "",
+ roleName: r.roleName ?? "",
+ roleCode: r.roleCode ?? "",
+ sortOrder: i + 1,
+ }));
+}
+
+function emitOut() {
+ const out = innerList.value.map((r, i) => ({
+ approverId: r.approverId ?? null,
+ approverName: r.approverName ?? "",
+ sortOrder: i + 1,
+ nodeOrder: i + 1,
+ roleName: r.roleName ?? "",
+ roleCode: r.roleCode ?? "",
+ }));
+ emit("update:modelValue", out);
+}
+
+watch(
+ () => props.modelValue,
+ (v) => {
+ const next = publicShape(v || []);
+ if (JSON.stringify(next) === JSON.stringify(publicShape(innerList.value))) return;
+ innerList.value = mapIn(v || []);
+ },
+ { deep: true, immediate: true }
+);
+
+function findUser(id) {
+ if (id == null || id === "") return null;
+ return props.userOptions.find((u) => String(u.userId ?? u.id) === String(id)) ?? null;
+}
+
+function onPick(userId, row) {
+ if (!userId) {
+ row.approverName = "";
+ emitOut();
+ return;
+ }
+ const u = findUser(userId);
+ row.approverName = u ? u.nickName || u.userName || "" : "";
+ emitOut();
+}
+
+function addNode() {
+ innerList.value.push({
+ _uid: newUid(),
+ approverId: null,
+ approverName: "",
+ roleName: "",
+ roleCode: "",
+ });
+ emitOut();
+}
+
+function remove(index) {
+ innerList.value.splice(index, 1);
+ emitOut();
+}
+
+function moveLeft(index) {
+ if (index < 1) return;
+ const t = innerList.value[index];
+ innerList.value[index] = innerList.value[index - 1];
+ innerList.value[index - 1] = t;
+ emitOut();
+}
+
+function moveRight(index) {
+ if (index >= innerList.value.length - 1) return;
+ const t = innerList.value[index];
+ innerList.value[index] = innerList.value[index + 1];
+ innerList.value[index + 1] = t;
+ emitOut();
+}
+</script>
+
+<style scoped>
+.afe {
+ width: 100%;
+}
+.afe-flow {
+ display: flex;
+ align-items: flex-start;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ padding: 6px 0 10px;
+ gap: 0;
+}
+.afe-flow-item {
+ display: flex;
+ align-items: center;
+}
+.afe-card {
+ width: 200px;
+ flex-shrink: 0;
+ border: 2px solid var(--el-border-color);
+ border-radius: 12px;
+ padding: 14px 12px 12px;
+ position: relative;
+ background: var(--el-bg-color);
+}
+.afe-card--empty {
+ border-style: dashed;
+ background: var(--el-fill-color-lighter);
+}
+.afe-badge {
+ position: absolute;
+ top: -8px;
+ left: 12px;
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ background: var(--el-color-primary);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.afe-avatar-wrap {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: 6px 0 10px;
+}
+.afe-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: var(--el-fill-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--el-text-color-placeholder);
+ margin-bottom: 6px;
+ font-size: 18px;
+ font-weight: 600;
+}
+.afe-avatar--on {
+ color: #fff;
+}
+.afe-level {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+}
+.afe-select {
+ margin-bottom: 10px;
+}
+.afe-actions {
+ display: flex;
+ justify-content: center;
+ gap: 8px;
+ padding-top: 10px;
+ border-top: 1px solid var(--el-border-color-lighter);
+}
+.afe-conn {
+ display: flex;
+ align-items: center;
+ width: 40px;
+ flex-shrink: 0;
+ align-self: center;
+}
+.afe-conn-line {
+ flex: 1;
+ height: 2px;
+ background: var(--el-border-color);
+}
+.afe-conn-icon {
+ font-size: 14px;
+ color: var(--el-text-color-placeholder);
+ margin-left: -2px;
+}
+.afe-add-wrap {
+ display: flex;
+ align-items: center;
+}
+.afe-add-card {
+ width: 120px;
+ min-height: 168px;
+ flex-shrink: 0;
+ border: 2px dashed var(--el-border-color);
+ border-radius: 12px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ cursor: pointer;
+ color: var(--el-text-color-regular);
+ font-size: 13px;
+ background: var(--el-fill-color-lighter);
+ transition: border-color 0.2s, background 0.2s;
+}
+.afe-add-card:hover {
+ border-color: var(--el-color-primary);
+ background: var(--el-color-primary-light-9);
+ color: var(--el-color-primary);
+}
+.afe-add-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: var(--el-color-primary);
+ color: #fff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.afe-empty {
+ text-align: center;
+ padding: 28px 16px;
+ border: 1px dashed var(--el-border-color);
+ border-radius: 12px;
+ background: var(--el-fill-color-lighter);
+}
+.afe-empty p {
+ margin: 10px 0 14px;
+ color: var(--el-text-color-secondary);
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
new file mode 100644
index 0000000..9b3d91e
--- /dev/null
+++ b/src/views/officeProcessAutomation/AttendManage/overtime-apply/index.vue
@@ -0,0 +1,260 @@
+<!--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="warning" plain @click="handleExport">瀵煎嚭</el-button>
+ <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"
+ flow-attachments-only
+ @submit="onSubmit"
+ >
+ <template #before="{ form, fields }">
+ <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" />
+ <el-row :gutter="24">
+ <el-col :span="12">
+ <el-form-item label="鍔犵彮鏃堕暱">
+ <el-input :model-value="overtimeHoursDisplay(form)" readonly placeholder="鏍规嵁妯℃澘涓姞鐝椂闂磋嚜鍔ㄨ绠�">
+ <template #append>灏忔椂</template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </template>
+ </ApprovalInstanceSubmitDialog>
+
+ <ApprovalTemplateBindDialog
+ v-model:visible="templateBindVisible"
+ :module-key="APPROVAL_MODULE_KEYS.OVERTIME"
+ 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 dayjs from "dayjs";
+import { ElMessage } from "element-plus";
+import { getCurrentInstance, onMounted, reactive, ref } from "vue";
+import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.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 OVERTIME_TYPE_OPTIONS = [
+ { label: "宸ヤ綔鏃ュ姞鐝�", value: "weekday" },
+ { label: "浼戞伅鏃ュ姞鐝�", value: "weekend" },
+ { label: "娉曞畾鑺傚亣鏃ュ姞鐝�", value: "holiday" },
+];
+
+function isOvertimeDurationField(field) {
+ const label = String(field?.label || "");
+ return label.includes("鍔犵彮鏃堕暱") || field?.key === "overtimeHours";
+}
+
+function displayTemplateFields(fields = []) {
+ return (fields || []).filter((f) => !isOvertimeDurationField(f));
+}
+
+function findOvertimeTimeTemplateField(fields = []) {
+ return (
+ fields.find((f) => f?.type === "datetimerange" && String(f?.label || "").includes("鍔犵彮鏃堕棿")) ||
+ fields.find((f) => f?.type === "datetimerange") ||
+ null
+ );
+}
+
+function resolveOvertimeTimeRange(payload, overtimeTimeField) {
+ if (!overtimeTimeField?.key) return { start: "", end: "" };
+ const val = payload?.[overtimeTimeField.key];
+ if (!Array.isArray(val) || val.length < 2) return { start: "", end: "" };
+ return { start: val[0] || "", end: val[1] || "" };
+}
+
+function computeOvertimeHours(startStr, endStr) {
+ if (!startStr || !endStr) return null;
+ const t0 = dayjs(startStr);
+ const t1 = dayjs(endStr);
+ if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
+ return Math.round((t1.diff(t0, "millisecond") / 3600000) * 100) / 100;
+}
+
+function overtimeHoursDisplay(form) {
+ const field = findOvertimeTimeTemplateField(form.formFieldDefs);
+ const { start, end } = resolveOvertimeTimeRange(form.formPayload, field);
+ const h = computeOvertimeHours(start, end);
+ return h == null ? "" : String(h);
+}
+
+const { proxy } = getCurrentInstance();
+
+const searchForm = reactive({
+ instanceNo: "",
+ applicantKeyword: "",
+});
+
+const mod = useApprovalInstanceModule({
+ moduleKey: APPROVAL_MODULE_KEYS.OVERTIME,
+ beforeSave: validateOvertimeBeforeSave,
+});
+
+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();
+
+function validateOvertimeBeforeSave() {
+ const field = findOvertimeTimeTemplateField(submitForm.formFieldDefs);
+ const { start, end } = resolveOvertimeTimeRange(submitForm.formPayload, field);
+ if (computeOvertimeHours(start, end) == null) {
+ ElMessage.warning("璇锋鏌ユā鏉夸腑鐨勫姞鐝椂闂达紝缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�");
+ throw new Error("invalid overtime time");
+ }
+}
+
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, {
+ moduleKey: APPROVAL_MODULE_KEYS.OVERTIME,
+});
+
+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 ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
+}
+
+function handleExport() {
+ const data = tableData.value;
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `鍔犵彮鐢宠瀵煎嚭_${dayjs().format("YYYYMMDDHHmmss")}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ proxy?.$modal?.msgSuccess?.(`宸插鍑� ${data.length} 鏉★紙褰撳墠椤靛垪琛ㄦ暟鎹級`);
+}
+
+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>
diff --git a/src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue b/src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue
new file mode 100644
index 0000000..d6d9ef4
--- /dev/null
+++ b/src/views/officeProcessAutomation/ContractManage/purchase-contract/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細閲囪喘鍚堝悓
+ 鐩綍鏍囪瘑锛欳ontractManage/purchase-contract锛坧urchase-contract 鈫� 涓枃锛氶噰璐悎鍚岋級
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue b/src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue
new file mode 100644
index 0000000..6be106a
--- /dev/null
+++ b/src/views/officeProcessAutomation/ContractManage/sale-contract/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細閿�鍞悎鍚�
+ 鐩綍鏍囪瘑锛欳ontractManage/sale-contract锛坰ale-contract 鈫� 涓枃锛氶攢鍞悎鍚岋級
+ 澶嶇敤椤甸潰锛欯/views/procurementManagement/procurementLedger/index.vue锛堥噰璐彴璐︼紱鏂囦欢鍚� index.vue 鈫� 鍏ュ彛椤碉級
+-->
+<template>
+ <ProcurementLedger />
+</template>
+
+<script setup>
+import ProcurementLedger from '@/views/procurementManagement/procurementLedger/index.vue'
+</script>
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/components/NewsDetailPanel.vue b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/components/NewsDetailPanel.vue
new file mode 100644
index 0000000..1124472
--- /dev/null
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/components/NewsDetailPanel.vue
@@ -0,0 +1,169 @@
+<!-- EnterpriseNews锛氳鎯呭彧璇婚潰鏉匡紙鍚簰鍔級 -->
+<template>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鏂伴椈缂栧彿">{{ row.newsNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">
+ <el-tag :type="publishStatusTag(row.newsStatus ?? row.publishStatus)" size="small">
+ {{ publishStatusLabel(row.newsStatus ?? row.publishStatus) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏂伴椈鍒嗙被">
+ <span class="type-badge" :style="{ color: newsTypeColor(row.newsType) }">
+ {{ newsTypeLabel(row.newsType) }}
+ </span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎺掔増妯℃澘">{{ layoutTemplateLabel(row.layoutTemplate) }}</el-descriptions-item>
+ <el-descriptions-item label="鏍囬" :span="2">{{ row.title || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鎽樿" :span="2">{{ row.summary || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="闃呰鑼冨洿">{{ readScopeLabel(row.readScope) }}</el-descriptions-item>
+ <el-descriptions-item label="闃呰鐜�">
+ {{ readRate(row) }}%锛堟湭璇� {{ unreadCount }} 浜猴級
+ </el-descriptions-item>
+ <el-descriptions-item label="缂栬緫鏉冮檺">{{ publishRoleLabel(row.editorRole) }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳瑙掕壊">{{ publishRoleLabel(row.reviewerRole) }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戝竷浜�">{{ row.publisherName || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戝竷鏃堕棿">{{ row.publishTime || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="褰撳墠鐗堟湰">v{{ row.versionNo || 1 }}</el-descriptions-item>
+ <el-descriptions-item label="闇�闃呰纭">
+ {{ row.requireReadConfirm ? "鏄�" : "鍚�" }}
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <el-divider content-position="left">姝f枃鍐呭</el-divider>
+ <div v-if="row.contentHtml" class="news-html-body" v-html="row.contentHtml" />
+ <el-empty v-else description="鏆傛棤姝f枃" :image-size="48" />
+
+ <template v-if="row.mediaList?.length">
+ <el-divider content-position="left">鍥鹃泦 / 瑙嗛</el-divider>
+ <div class="media-grid">
+ <div v-for="(m, i) in row.mediaList" :key="i" class="media-item">
+ <el-tag size="small" type="info">{{ m.type === "video" ? "瑙嗛" : "鍥剧墖" }}</el-tag>
+ <span class="media-name">{{ m.name }}</span>
+ </div>
+ </div>
+ </template>
+
+ <el-divider content-position="left">闄勪欢</el-divider>
+ <template v-if="row.attachmentList?.length">
+ <el-tag
+ v-for="(f, i) in row.attachmentList"
+ :key="i"
+ class="file-tag"
+ type="info"
+ @click="openFile(f)"
+ >
+ {{ f.name }}
+ </el-tag>
+ </template>
+ <el-empty v-else description="鏆傛棤闄勪欢" :image-size="48" />
+
+ <template v-if="row.newsType === 'culture' && (row.publishStatus === 'PUBLISHED' || row.publishStatus === 'published')">
+ <el-divider content-position="left">浜掑姩锛堢偣璧� {{ likeCount }} 路 璇勮 {{ commentCount }}锛�</el-divider>
+ <div class="interaction-bar">
+ <el-button type="primary" plain size="small" @click="$emit('like')">
+ {{ likedByMe ? "鍙栨秷鐐硅禐" : "鐐硅禐" }}
+ </el-button>
+ </div>
+ <el-input
+ v-model="commentDraft"
+ type="textarea"
+ :rows="2"
+ maxlength="300"
+ show-word-limit
+ placeholder="鍐欎笅浣犵殑璇勮鈥�"
+ class="mb8"
+ />
+ <el-button type="primary" size="small" @click="submitComment">鍙戣〃璇勮</el-button>
+ <el-timeline v-if="row.comments?.length" class="comment-timeline mt12">
+ <el-timeline-item v-for="c in row.comments" :key="c.id" :timestamp="c.time">
+ <strong>{{ c.name }}</strong>锛歿{ c.content }}
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤璇勮" :image-size="40" />
+ </template>
+</template>
+
+<script setup>
+import { computed, ref } from "vue";
+import {
+ newsTypeLabel,
+ newsTypeColor,
+ publishStatusLabel,
+ publishStatusTag,
+ layoutTemplateLabel,
+ readScopeLabel,
+ publishRoleLabel,
+ readRate,
+ getUnreadEmployees,
+} from "../enterpriseNewsUtils.js";
+
+const props = defineProps({
+ row: { type: Object, default: () => ({}) },
+});
+
+const emit = defineEmits(["like", "comment"]);
+
+const commentDraft = ref("");
+
+const unreadCount = computed(() => getUnreadEmployees(props.row).length);
+const likeCount = computed(() => props.row?.likes?.length || 0);
+const commentCount = computed(() => props.row?.comments?.length || 0);
+const likedByMe = computed(() => (props.row?.likes || []).some((l) => l.userId === "u1"));
+
+function openFile(f) {
+ const url = f?.url || f?.downloadURL;
+ if (url) window.open(url, "_blank");
+}
+
+function submitComment() {
+ emit("comment", commentDraft.value);
+ commentDraft.value = "";
+}
+</script>
+
+<style scoped>
+.type-badge {
+ font-weight: 600;
+}
+.news-html-body {
+ padding: 12px 16px;
+ background: var(--el-fill-color-light);
+ border-radius: 6px;
+ line-height: 1.7;
+ max-height: 320px;
+ overflow-y: auto;
+}
+.media-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+.media-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: var(--el-fill-color-lighter);
+ border-radius: 4px;
+}
+.media-name {
+ font-size: 13px;
+}
+.file-tag {
+ margin: 0 8px 8px 0;
+ cursor: pointer;
+}
+.interaction-bar {
+ margin-bottom: 8px;
+}
+.comment-timeline {
+ max-height: 200px;
+ overflow-y: auto;
+}
+.mb8 {
+ margin-bottom: 8px;
+}
+.mt12 {
+ margin-top: 12px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js
new file mode 100644
index 0000000..b870ab7
--- /dev/null
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsApprovalBridge.js
@@ -0,0 +1,11 @@
+/** @deprecated 璇蜂娇鐢� enterpriseNewsMappers.js */
+export {
+ ENTERPRISE_NEWS_PAYLOAD_KEY,
+ buildEnterpriseNewsSaveDto,
+ buildEnterpriseNewsTableColumns,
+ canEditEnterpriseNewsRow,
+ extractEnterpriseNewsFromRow,
+ mapApiRowToNewsForm,
+ mapEnterpriseNewsFromApi,
+ syncNewsFormToSubmitPayload,
+} from "./enterpriseNewsMappers.js";
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsMappers.js b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsMappers.js
new file mode 100644
index 0000000..27cb9bc
--- /dev/null
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsMappers.js
@@ -0,0 +1,221 @@
+import {
+ createEmptyForm,
+ normalizeEnterpriseNewsStatus,
+ publishStatusLabel,
+ publishStatusTag,
+} from "./enterpriseNewsUtils.js";
+
+/** formPayload 涓瓨鏀惧畬鏁翠紒涓氭柊闂讳笟鍔℃暟鎹殑閿紙瀹℃壒瀹炰緥淇濆瓨鐢級 */
+export const ENTERPRISE_NEWS_PAYLOAD_KEY = "enterpriseNews";
+
+const READ_SCOPE_FROM_API = {
+ all: "all",
+ dept: "department",
+ department: "department",
+ custom: "custom",
+ management: "management",
+};
+
+const READ_SCOPE_TO_API = {
+ all: "all",
+ department: "dept",
+ dept: "dept",
+ custom: "custom",
+ management: "all",
+};
+
+export function mapReadScopeFromApi(scope) {
+ const key = String(scope ?? "").trim().toLowerCase();
+ return READ_SCOPE_FROM_API[key] || key || "all";
+}
+
+export function mapReadScopeToApi(scope) {
+ return READ_SCOPE_TO_API[scope] || scope || "all";
+}
+
+export function unwrapEnterpriseNewsPage(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") {
+ return { records: [], total: 0 };
+ }
+ if (Array.isArray(data.records)) {
+ return { records: data.records, total: Number(data.total ?? 0) };
+ }
+ const nested = data.data;
+ if (nested && typeof nested === "object" && Array.isArray(nested.records)) {
+ return { records: nested.records, total: Number(nested.total ?? 0) };
+ }
+ return { records: [], total: 0 };
+}
+
+/** 缁勮 listPage 鏌ヨ鍙傛暟 */
+export function buildEnterpriseNewsListParams({ page, searchForm }) {
+ const params = {
+ current: page.current,
+ size: page.size,
+ };
+ const kw = (searchForm?.keyword || "").trim();
+ if (kw) params.title = kw;
+ if (searchForm?.newsType) params.category = searchForm.newsType;
+ if (searchForm?.status) params.status = searchForm.status;
+ const range = searchForm?.createTimeRange;
+ if (Array.isArray(range) && range[0]) {
+ params.createTimeStart = range[0];
+ }
+ if (Array.isArray(range) && range[1]) {
+ params.createTimeEnd = range[1];
+ }
+ return params;
+}
+
+/** 鎺ュ彛 EnterpriseNewsVo 鈫� 鍒楄〃琛� */
+export function mapEnterpriseNewsFromApi(row) {
+ if (!row) return {};
+ const newsStatus = normalizeEnterpriseNewsStatus(row.status);
+ return {
+ ...row,
+ newsNo: row.id != null ? String(row.id) : "鈥�",
+ newsType: row.category || "",
+ contentHtml: row.content || "",
+ publisherName: row.createUserName || "鈥�",
+ publishTime: row.createTime || "",
+ updateTime: row.updateTime || "",
+ newsStatus,
+ requireReadConfirm: row.isRequired === "1" || row.isRequired === 1,
+ readScope: mapReadScopeFromApi(row.readScope),
+ readCount: row.readCount ?? 0,
+ requiredReadCount: row.requiredReadCount ?? 0,
+ };
+}
+
+/** 鏄惁鍏佽淇敼锛堣崏绋裤�侀┏鍥炲彲鏀癸級 */
+export function canEditEnterpriseNewsRow(row) {
+ const status = normalizeEnterpriseNewsStatus(row?.newsStatus ?? row?.status);
+ return status === "DRAFT" || status === "REJECTED";
+}
+
+/** 鎺ュ彛琛� / 璇︽儏 鈫� 琛ㄥ崟 */
+export function mapApiRowToNewsForm(row) {
+ if (!row) return createEmptyForm();
+ return {
+ ...createEmptyForm(),
+ id: row.id != null ? String(row.id) : "",
+ newsNo: row.id != null ? String(row.id) : "",
+ title: row.title || "",
+ summary: row.summary || "",
+ contentHtml: row.content || row.contentHtml || "",
+ newsType: row.newsType || row.category || "announcement",
+ readScope: mapReadScopeFromApi(row.readScope),
+ requireReadConfirm: Boolean(row.requireReadConfirm ?? row.isRequired === "1"),
+ publisherName: row.createUserName || row.publisherName || "",
+ publishStatus: normalizeEnterpriseNewsStatus(row.newsStatus ?? row.status),
+ templateId: row.templateId,
+ templateName: row.templateName || "",
+ targetDeptIds: [...(row.deptIds || row.targetDeptIds || [])],
+ targetUserIds: [...(row.userIds || row.targetUserIds || [])],
+ };
+}
+
+/** 瀹℃壒瀹炰緥琛� formPayload 鈫� 琛ㄥ崟锛堝吋瀹规棫鏁版嵁锛� */
+export function extractEnterpriseNewsFromRow(row) {
+ if (!row?.formPayload && !row?.formFieldDefs && !row?.instanceNo) {
+ return mapApiRowToNewsForm(row);
+ }
+ const payload = row?.formPayload || {};
+ const raw = payload[ENTERPRISE_NEWS_PAYLOAD_KEY];
+ if (raw && typeof raw === "object") {
+ return { ...createEmptyForm(), ...raw };
+ }
+ return {
+ ...createEmptyForm(),
+ title: payload.title || row?.title || "",
+ summary: payload.summary || "",
+ newsType: payload.newsType || row?.category || "announcement",
+ contentHtml: payload.contentHtml || row?.content || "",
+ };
+}
+
+export function syncNewsFormToSubmitPayload(newsForm, submitForm) {
+ const snapshot = JSON.parse(JSON.stringify(newsForm));
+ submitForm.formPayload = {
+ ...(submitForm.formPayload || {}),
+ [ENTERPRISE_NEWS_PAYLOAD_KEY]: snapshot,
+ title: snapshot.title,
+ summary: snapshot.summary,
+ };
+}
+
+function toIdList(ids) {
+ if (!Array.isArray(ids) || !ids.length) return undefined;
+ const list = ids
+ .map((id) => (typeof id === "number" ? id : Number(id)))
+ .filter((n) => !Number.isNaN(n));
+ return list.length ? list : undefined;
+}
+
+/** 琛ㄥ崟 鈫� POST /enterpriseNews/save 璇锋眰浣� */
+export function buildEnterpriseNewsSaveDto(newsForm, { status } = {}) {
+ const dto = {
+ title: (newsForm.title || "").trim(),
+ summary: newsForm.summary || "",
+ content: newsForm.contentHtml || "",
+ category: newsForm.newsType || "",
+ readScope: mapReadScopeToApi(newsForm.readScope),
+ isRequired: newsForm.requireReadConfirm ? "1" : "0",
+ status: normalizeEnterpriseNewsStatus(status ?? newsForm.publishStatus),
+ };
+
+ const rawId = newsForm.id;
+ if (rawId != null && rawId !== "") {
+ const id = Number(rawId);
+ if (!Number.isNaN(id)) dto.id = id;
+ }
+
+ const deptIds = toIdList(newsForm.targetDeptIds);
+ if (deptIds) dto.deptIds = deptIds;
+
+ const userIds = toIdList(newsForm.targetUserIds);
+ if (userIds) dto.userIds = userIds;
+
+ const templateId = newsForm.templateId;
+ if (templateId != null && templateId !== "") {
+ const tid = Number(templateId);
+ if (!Number.isNaN(tid)) dto.templateId = tid;
+ }
+ if (newsForm.templateName) dto.templateName = newsForm.templateName;
+
+ return dto;
+}
+
+export function buildEnterpriseNewsTableColumns(buildTableActions) {
+ return [
+ { label: "缂栧彿", prop: "newsNo", width: 120 },
+ { label: "鏍囬", prop: "title", minWidth: 180, showOverflowTooltip: true },
+ {
+ label: "鍒嗙被",
+ prop: "newsType",
+ width: 100,
+ dataType: "slot",
+ slot: "newsType",
+ },
+ {
+ label: "鐘舵��",
+ prop: "newsStatus",
+ width: 100,
+ dataType: "tag",
+ formatData: (v) => publishStatusLabel(v),
+ formatType: (v) => publishStatusTag(v),
+ },
+ { label: "鍒涘缓浜�", prop: "publisherName", width: 110 },
+ { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 170 },
+ { label: "鏇存柊鏃堕棿", prop: "updateTime", width: 170 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 220,
+ operation: buildTableActions(),
+ },
+ ];
+}
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsUtils.js b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsUtils.js
new file mode 100644
index 0000000..9bc29c3
--- /dev/null
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/enterpriseNewsUtils.js
@@ -0,0 +1,207 @@
+import dayjs from "dayjs";
+
+/** 鏂伴椈鍒嗙被锛氱粺涓�淇℃伅鍑哄彛 */
+export const NEWS_TYPE_OPTIONS = [
+ { value: "announcement", label: "浼佷笟鍏憡", color: "#409eff" },
+ { value: "policy", label: "鏀跨瓥瑙h", color: "#e6a23c" },
+ { value: "industry", label: "琛屼笟鍔ㄦ��", color: "#909399" },
+ { value: "culture", label: "鏂囧寲娲诲姩", color: "#67c23a" },
+];
+
+/** 浼佷笟鏂伴椈鐘舵�侊紙涓庡悗绔灇涓句竴鑷达級 */
+export const PUBLISH_STATUS_OPTIONS = [
+ { value: "DRAFT", label: "鑽夌", tag: "info" },
+ { value: "PENDING", label: "寰呭鎵�", tag: "warning" },
+ { value: "PUBLISHED", label: "宸插彂甯�", tag: "success" },
+ { value: "REJECTED", label: "椹冲洖", tag: "danger" },
+ { value: "OFFLINE", label: "宸蹭笅绾�", tag: "info" },
+];
+
+/** 浼佷笟鏂伴椈鍒楄〃绛涢�� */
+export const ENTERPRISE_NEWS_STATUS_SEARCH_OPTIONS = [...PUBLISH_STATUS_OPTIONS];
+
+const LEGACY_PUBLISH_STATUS_MAP = {
+ draft: "DRAFT",
+ pending_review: "PENDING",
+ published: "PUBLISHED",
+ archived: "OFFLINE",
+};
+
+/** 鍚庣鏁板瓧鐘舵�佺爜 鈫� 鏋氫妇锛�0 鑽夌 1 寰呭鎵� 2 宸插彂甯� 3 椹冲洖 4 宸蹭笅绾匡級 */
+const ENTERPRISE_NEWS_STATUS_NUMERIC_MAP = {
+ 0: "DRAFT",
+ 1: "PENDING",
+ 2: "PUBLISHED",
+ 3: "REJECTED",
+ 4: "OFFLINE",
+};
+
+const ENTERPRISE_NEWS_STATUS_LABEL_MAP = {
+ 鑽夌: "DRAFT",
+ 寰呭鎵�: "PENDING",
+ 宸插彂甯�: "PUBLISHED",
+ 椹冲洖: "REJECTED",
+ 宸查┏鍥�: "REJECTED",
+ 宸蹭笅绾�: "OFFLINE",
+};
+
+/** 缁熶竴涓哄悗绔姸鎬佹灇涓惧�� */
+export function normalizeEnterpriseNewsStatus(v) {
+ if (v == null || v === "") return "DRAFT";
+ if (typeof v === "number" || (typeof v === "string" && /^\d+$/.test(v.trim()))) {
+ const numKey = ENTERPRISE_NEWS_STATUS_NUMERIC_MAP[Number(v)];
+ if (numKey) return numKey;
+ }
+ const raw = String(v).trim();
+ if (ENTERPRISE_NEWS_STATUS_LABEL_MAP[raw]) {
+ return ENTERPRISE_NEWS_STATUS_LABEL_MAP[raw];
+ }
+ const upper = raw.toUpperCase();
+ if (upper === "APPROVED") return "PUBLISHED";
+ const hit = PUBLISH_STATUS_OPTIONS.find((x) => x.value === upper);
+ if (hit) return hit.value;
+ const legacy = LEGACY_PUBLISH_STATUS_MAP[raw.toLowerCase()];
+ if (legacy) return legacy;
+ return upper;
+}
+
+/** 鎺掔増妯℃澘 */
+export const LAYOUT_TEMPLATE_OPTIONS = [
+ { value: "standard", label: "鏍囧噯鍥炬枃" },
+ { value: "policy", label: "鏀跨瓥鏉℃枃" },
+ { value: "gallery", label: "鍥鹃泦鐩稿唽" },
+ { value: "briefing", label: "绠�鎶ユ憳瑕�" },
+];
+
+/** 闃呰鍙鑼冨洿 */
+export const READ_SCOPE_OPTIONS = [
+ { value: "all", label: "鍏ㄥ憳鍙" },
+ { value: "management", label: "绠$悊灞�" },
+ { value: "department", label: "鎸囧畾閮ㄩ棬" },
+ { value: "custom", label: "鑷畾涔夊悕鍗�" },
+];
+
+/** 缂栬緫/瀹℃牳瑙掕壊锛堝彂甯冩潈闄愶級 */
+export const PUBLISH_ROLE_OPTIONS = [
+ { value: "hr", label: "HR锛堜汉浜嬫斂绛栵級" },
+ { value: "admin", label: "绠$悊鍛橈紙澶栭儴鏂伴椈瀹℃牳锛�" },
+ { value: "dept_manager", label: "閮ㄩ棬璐熻矗浜�" },
+ { value: "editor", label: "鍐呭缂栬緫" },
+];
+
+/** 鐩爣鍙椾紬锛堝鎺ョ粍缁囨灦鏋� API 鍓嶄负绌猴級 */
+export const MOCK_AUDIENCE = [];
+
+const DEPT_OPTIONS = [
+ { value: "101", label: "鐮斿彂閮�" },
+ { value: "102", label: "閿�鍞儴" },
+ { value: "103", label: "琛屾斂閮�" },
+ { value: "104", label: "璐㈠姟閮�" },
+ { value: "105", label: "鎬荤粡鍔�" },
+ { value: "106", label: "浜哄姏璧勬簮閮�" },
+];
+
+export { DEPT_OPTIONS };
+
+export function newsTypeLabel(v) {
+ return NEWS_TYPE_OPTIONS.find((x) => x.value === v)?.label || v || "鈥�";
+}
+
+export function newsTypeColor(v) {
+ return NEWS_TYPE_OPTIONS.find((x) => x.value === v)?.color || "#909399";
+}
+
+export function publishStatusLabel(v) {
+ const key = normalizeEnterpriseNewsStatus(v);
+ return PUBLISH_STATUS_OPTIONS.find((x) => x.value === key)?.label || v || "鈥�";
+}
+
+export function publishStatusTag(v) {
+ const key = normalizeEnterpriseNewsStatus(v);
+ return PUBLISH_STATUS_OPTIONS.find((x) => x.value === key)?.tag || "info";
+}
+
+export function layoutTemplateLabel(v) {
+ return LAYOUT_TEMPLATE_OPTIONS.find((x) => x.value === v)?.label || v || "鈥�";
+}
+
+export function readScopeLabel(v) {
+ return READ_SCOPE_OPTIONS.find((x) => x.value === v)?.label || v || "鈥�";
+}
+
+export function publishRoleLabel(v) {
+ return PUBLISH_ROLE_OPTIONS.find((x) => x.value === v)?.label || v || "鈥�";
+}
+
+export function createEmptyForm() {
+ return {
+ id: "",
+ newsNo: "",
+ title: "",
+ summary: "",
+ newsType: "announcement",
+ layoutTemplate: "standard",
+ contentHtml: "",
+ coverImage: "",
+ mediaList: [],
+ attachmentList: [],
+ editorRole: "hr",
+ reviewerRole: "admin",
+ readScope: "all",
+ targetDeptIds: [],
+ targetUserIds: [],
+ publishStatus: "DRAFT",
+ publisherName: "",
+ publishTime: "",
+ readRecords: [],
+ remindLogs: [],
+ likes: [],
+ comments: [],
+ versions: [],
+ versionNo: 1,
+ requireReadConfirm: false,
+ templateId: null,
+ templateName: "",
+ };
+}
+
+/** 鎸夐槄璇昏寖鍥磋В鏋愮洰鏍囧彈浼� */
+export function resolveTargetAudience(row) {
+ const scope = row.readScope || "all";
+ if (scope === "management") {
+ return MOCK_AUDIENCE.filter((u) => u.isManagement);
+ }
+ if (scope === "department" && row.targetDeptIds?.length) {
+ const names = DEPT_OPTIONS.filter((d) => row.targetDeptIds.includes(d.value)).map((d) => d.label);
+ return MOCK_AUDIENCE.filter((u) => names.includes(u.deptName));
+ }
+ if (scope === "custom" && row.targetUserIds?.length) {
+ return MOCK_AUDIENCE.filter((u) => row.targetUserIds.includes(u.userId));
+ }
+ return [...MOCK_AUDIENCE];
+}
+
+export function getUnreadEmployees(row) {
+ const audience = resolveTargetAudience(row);
+ const readSet = new Set(
+ (row.readRecords || []).filter((r) => r.readAt).map((r) => r.userId)
+ );
+ return audience.filter((u) => !readSet.has(u.userId));
+}
+
+export function readRate(row) {
+ const audience = resolveTargetAudience(row);
+ if (!audience.length) return 0;
+ const readCount = (row.readRecords || []).filter((r) => r.readAt).length;
+ return Math.round((readCount / audience.length) * 100);
+}
+
+export function validateNewsForm(form) {
+ const title = (form.title || "").trim();
+ if (!title) return { ok: false, message: "璇峰~鍐欐柊闂绘爣棰�" };
+ if (!form.newsType) return { ok: false, message: "璇烽�夋嫨鏂伴椈鍒嗙被" };
+ if (form.readScope === "department" && !(form.targetDeptIds || []).length) {
+ return { ok: false, message: "璇烽�夋嫨鍙閮ㄩ棬" };
+ }
+ return { ok: true, title };
+}
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue
new file mode 100644
index 0000000..f263a41
--- /dev/null
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/index.vue
@@ -0,0 +1,566 @@
+<!--OA妯″潡锛欵nterpriseNews 浼佷笟鏂伴椈锛坙istPage|save|update|delete锛屾柊寤轰繚鐣欏鎵规ā鏉匡級-->
+<template>
+ <div class="app-container enterprise-news-page">
+ <div class="search_form mb20">
+ <div class="search_fields">
+ <span class="search_title">鍏抽敭璇嶏細</span>
+ <el-input
+ v-model="searchForm.keyword"
+ style="width: 200px"
+ placeholder="鏍囬"
+ clearable
+ :prefix-icon="Search"
+ @keyup.enter="onSearch"
+ />
+ <span class="search_title" style="margin-left: 12px">鍒嗙被锛�</span>
+ <el-select v-model="searchForm.newsType" placeholder="鍏ㄩ儴" clearable style="width: 140px">
+ <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ <span class="search_title" style="margin-left: 12px">鐘舵�侊細</span>
+ <el-select v-model="searchForm.status" placeholder="鍏ㄩ儴" clearable style="width: 120px">
+ <el-option
+ v-for="opt in ENTERPRISE_NEWS_STATUS_SEARCH_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ <span class="search_title" style="margin-left: 12px">鍒涘缓鏃堕棿锛�</span>
+ <el-date-picker
+ v-model="searchForm.createTimeRange"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮�"
+ end-placeholder="缁撴潫"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 260px"
+ clearable
+ />
+ <el-button type="primary" :icon="Search" class="ml10" @click="onSearch">鎼滅储</el-button>
+ <el-button :icon="RefreshRight" @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div class="search_actions">
+ <el-button type="primary" :icon="Plus" @click="openAddWithTemplate">鏂板缓鏂伴椈</el-button>
+ </div>
+ </div>
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="false"
+ :tableLoading="tableLoading"
+ :total="page.total"
+ @pagination="onPagination"
+ >
+ <template #newsType="{ row }">
+ <span class="news-type-tag" :style="{ color: newsTypeColor(row.newsType) }">
+ {{ newsTypeLabel(row.newsType) }}
+ </span>
+ </template>
+ </PIMTable>
+ </div>
+
+ <ApprovalTemplateBindDialog
+ v-model:visible="templateBindVisible"
+ :module-key="APPROVAL_MODULE_KEYS.ENTERPRISE_NEWS"
+ skip-form-confirm
+ @confirm="onTemplateBound"
+ @closed="onTemplateBindClosed"
+ />
+
+ <el-dialog
+ v-model="newsFormDialog.visible"
+ :title="newsFormDialog.title"
+ width="960px"
+ append-to-body
+ destroy-on-close
+ class="news-form-dialog"
+ @closed="onNewsFormClosed"
+ >
+ <el-form
+ ref="newsFormRef"
+ :model="newsForm"
+ :rules="newsFormRules"
+ label-width="110px"
+ :disabled="newsFormDialog.readonly"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鏂伴椈鍒嗙被" prop="newsType">
+ <el-select v-model="newsForm.newsType" placeholder="璇烽�夋嫨" style="width: 100%">
+ <el-option v-for="opt in NEWS_TYPE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎺掔増妯℃澘">
+ <el-select v-model="newsForm.layoutTemplate" style="width: 100%">
+ <el-option
+ v-for="opt in LAYOUT_TEMPLATE_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鏍囬" prop="title">
+ <el-input v-model="newsForm.title" placeholder="鏂伴椈鏍囬" maxlength="100" show-word-limit />
+ </el-form-item>
+ <el-form-item label="鎽樿">
+ <el-input v-model="newsForm.summary" type="textarea" :rows="2" maxlength="300" show-word-limit />
+ </el-form-item>
+ <el-form-item label="姝f枃" prop="contentHtml">
+ <Editor v-model="newsForm.contentHtml" :min-height="280" />
+ </el-form-item>
+ <el-form-item label="闄勪欢">
+ <FileUpload v-model:file-list="newsForm.attachmentList" :limit="10" button-text="涓婁紶 PDF / 鏂囨。" />
+ </el-form-item>
+ <el-form-item v-if="newsForm.layoutTemplate === 'gallery'" label="鍥鹃泦/瑙嗛">
+ <el-input
+ v-model="galleryInput"
+ placeholder="杈撳叆璧勬簮鍚嶇О鍚庡洖杞︽坊鍔狅紙婕旂ず锛�"
+ @keyup.enter="addGalleryItem"
+ />
+ <el-tag
+ v-for="(m, i) in newsForm.mediaList"
+ :key="i"
+ closable
+ class="media-tag"
+ @close="newsForm.mediaList.splice(i, 1)"
+ >
+ {{ m.name }}
+ </el-tag>
+ </el-form-item>
+
+ <el-divider content-position="left">鏉冮檺绠℃帶</el-divider>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="缂栬緫瑙掕壊">
+ <el-select v-model="newsForm.editorRole" style="width: 100%">
+ <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹℃牳瑙掕壊">
+ <el-select v-model="newsForm.reviewerRole" style="width: 100%">
+ <el-option v-for="opt in PUBLISH_ROLE_OPTIONS" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="闃呰鑼冨洿" prop="readScope">
+ <el-radio-group v-model="newsForm.readScope">
+ <el-radio v-for="opt in READ_SCOPE_OPTIONS" :key="opt.value" :value="opt.value">
+ {{ opt.label }}
+ </el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="newsForm.readScope === 'department'" label="鍙閮ㄩ棬">
+ <el-select v-model="newsForm.targetDeptIds" multiple placeholder="閫夋嫨閮ㄩ棬" style="width: 100%">
+ <el-option v-for="d in DEPT_OPTIONS" :key="d.value" :label="d.label" :value="d.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏀跨瓥绫诲繀璇�">
+ <el-switch v-model="newsForm.requireReadConfirm" active-text="闇�闃呰纭锛堜究浜庣粺璁℃湭璇伙級" />
+ </el-form-item>
+
+ <template v-if="hasApprovalTemplate">
+ <el-divider content-position="left">瀹℃壒娴佺▼</el-divider>
+ <el-form-item label="瀹℃壒妯℃澘">
+ <span class="template-name">{{ approvalTemplateLabel }}</span>
+ </el-form-item>
+ <el-form-item v-if="activeTemplate" label="瀹℃壒娴佺▼" required>
+ <TemplateFlowEditor v-model="submitForm.flowNodes" :user-options="flowUserOptions" />
+ <p class="section-tip">娴佺▼涓庡鎵逛汉鐢辨ā鏉块缃紝鍙寜闇�寰皟鑺傜偣瀹℃壒浜恒��</p>
+ </el-form-item>
+ </template>
+ <el-alert
+ v-else-if="!isNewsEdit"
+ type="warning"
+ show-icon
+ :closable="false"
+ title="璇峰厛閫氳繃銆屾柊寤烘柊闂汇�嶉�夋嫨瀹℃壒妯℃澘"
+ />
+ </el-form>
+ <template v-if="!newsFormDialog.readonly" #footer>
+ <el-button @click="newsFormDialog.visible = false">鍙� 娑�</el-button>
+ <el-button :loading="newsSaving" @click="onNewsSave('draft')">瀛樿崏绋�</el-button>
+ <el-button type="warning" :loading="newsSaving" @click="onNewsSave('submit_review')">
+ 鎻愪氦瀹℃牳
+ </el-button>
+ <el-button type="primary" :loading="newsSaving" @click="onNewsSave('submit_review')">
+ 淇� 瀛�
+ </el-button>
+ </template>
+ </el-dialog>
+
+ <el-dialog v-model="detailDialog.visible" title="鏂伴椈璇︽儏" width="880px" append-to-body destroy-on-close>
+ <NewsDetailPanel :row="detailNewsRow" />
+ <template #footer>
+ <el-button v-if="canEditEnterpriseNewsRow(detailRow)" type="primary" @click="openNewsEditFromDetail">
+ 淇敼
+ </el-button>
+ <el-button @click="detailDialog.visible = false">鍏� 闂�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { Plus, RefreshRight, Search } from "@element-plus/icons-vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import {
+ deleteEnterpriseNews,
+ saveEnterpriseNews,
+ updateEnterpriseNews,
+} from "@/api/officeProcessAutomation/enterpriseNews.js";
+import { computed, onMounted, reactive, ref } from "vue";
+import Editor from "@/components/Editor/index.vue";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import ApprovalTemplateBindDialog from "../../ApproveManage/approve-shared/components/ApprovalTemplateBindDialog.vue";
+import TemplateFlowEditor from "../../ApproveManage/approve-template/components/TemplateFlowEditor.vue";
+import {
+ applyBindingToForm,
+ validateTemplateBinding,
+} from "../../ApproveManage/approve-shared/approvalTemplateBindingUtils.js";
+import { createEmptySubmitForm } from "../../ApproveManage/approve-list/approveListConstants.js";
+import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
+import { useFlowUserOptions } from "../../ApproveManage/approve-shared/useFlowUserOptions.js";
+import NewsDetailPanel from "./components/NewsDetailPanel.vue";
+import {
+ NEWS_TYPE_OPTIONS,
+ LAYOUT_TEMPLATE_OPTIONS,
+ READ_SCOPE_OPTIONS,
+ PUBLISH_ROLE_OPTIONS,
+ DEPT_OPTIONS,
+ createEmptyForm,
+ ENTERPRISE_NEWS_STATUS_SEARCH_OPTIONS,
+ newsTypeColor,
+ newsTypeLabel,
+ validateNewsForm,
+} from "./enterpriseNewsUtils.js";
+import {
+ buildEnterpriseNewsSaveDto,
+ buildEnterpriseNewsTableColumns,
+ canEditEnterpriseNewsRow,
+ mapApiRowToNewsForm,
+} from "./enterpriseNewsMappers.js";
+import { useEnterpriseNewsList } from "./useEnterpriseNewsList.js";
+
+const searchForm = reactive({
+ keyword: "",
+ newsType: "",
+ status: "",
+ createTimeRange: null,
+});
+
+const newsFormDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
+const detailDialog = reactive({ visible: false });
+const detailRow = ref({});
+const newsForm = reactive(createEmptyForm());
+const newsFormRef = ref();
+const galleryInput = ref("");
+
+const newsFormRules = {
+ title: [{ required: true, message: "璇疯緭鍏ユ柊闂绘爣棰�", trigger: "blur" }],
+ newsType: [{ required: true, message: "璇烽�夋嫨鏂伴椈鍒嗙被", trigger: "change" }],
+ readScope: [{ required: true, message: "璇烽�夋嫨闃呰鑼冨洿", trigger: "change" }],
+};
+
+const newsList = useEnterpriseNewsList();
+const { tableData, tableLoading, page, handleQuery: fetchNewsList, pagination: paginateNewsList } =
+ newsList;
+
+const submitForm = reactive(createEmptySubmitForm(""));
+const templateBindVisible = ref(false);
+const pendingTemplateBinding = ref(null);
+const newsSaving = ref(false);
+
+const isNewsEdit = computed(() => newsFormDialog.mode === "edit");
+const activeTemplate = computed(() => submitForm.templateSnapshot || null);
+const hasApprovalTemplate = computed(
+ () => Boolean(activeTemplate.value || newsForm.templateId)
+);
+const approvalTemplateLabel = computed(
+ () =>
+ activeTemplate.value?.label ||
+ newsForm.templateName ||
+ submitForm.templateName ||
+ "鈥�"
+);
+
+const { flowUserOptions, loadFlowUsers } = useFlowUserOptions();
+
+function openAddWithTemplate() {
+ pendingTemplateBinding.value = null;
+ templateBindVisible.value = true;
+}
+
+function onTemplateBound(binding) {
+ pendingTemplateBinding.value = binding;
+}
+
+function resetSubmitForm() {
+ Object.assign(submitForm, createEmptySubmitForm(""));
+}
+
+const detailNewsRow = computed(() => mapApiRowToNewsForm(detailRow.value));
+
+const tableColumn = ref(
+ buildEnterpriseNewsTableColumns(() => [
+ { name: "璇︽儏", type: "text", clickFun: (row) => openNewsDetail(row) },
+ {
+ name: "淇敼",
+ type: "text",
+ disabled: (row) => !canEditEnterpriseNewsRow(row),
+ clickFun: (row) => openNewsEdit(row),
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ disabled: (row) => !canEditEnterpriseNewsRow(row),
+ clickFun: (row) => handleNewsDelete(row),
+ },
+ ])
+);
+
+function resetNewsForm(target = createEmptyForm()) {
+ Object.assign(newsForm, createEmptyForm(), target);
+}
+
+function openNewsFormDialog(mode, row) {
+ newsFormDialog.mode = mode;
+ newsFormDialog.readonly = mode === "view";
+ newsFormDialog.title =
+ mode === "add" ? "鏂板缓浼佷笟鏂伴椈" : mode === "edit" ? "缂栬緫浼佷笟鏂伴椈" : "鏌ョ湅浼佷笟鏂伴椈";
+ if (mode === "add") {
+ resetNewsForm();
+ } else if (row) {
+ resetNewsForm(mapApiRowToNewsForm(row));
+ }
+ newsFormDialog.visible = true;
+}
+
+function onTemplateBindClosed() {
+ const binding = pendingTemplateBinding.value;
+ if (!binding) return;
+ pendingTemplateBinding.value = null;
+ resetSubmitForm();
+ applyBindingToForm(submitForm, binding);
+ if (binding.templateId) {
+ newsForm.templateId = binding.templateId;
+ newsForm.templateName = binding.templateName || "";
+ }
+ openNewsFormDialog("add");
+}
+
+function openNewsEdit(row) {
+ if (!canEditEnterpriseNewsRow(row)) {
+ ElMessage.warning("褰撳墠鐘舵�佷笉鍙慨鏀�");
+ return;
+ }
+ resetSubmitForm();
+ if (row?.templateId != null) {
+ submitForm.templateId = row.templateId;
+ submitForm.templateName = row.templateName || "";
+ }
+ openNewsFormDialog("edit", row);
+}
+
+function openNewsDetail(row) {
+ detailRow.value = { ...row };
+ detailDialog.visible = true;
+}
+
+function openNewsEditFromDetail() {
+ const row = detailRow.value;
+ detailDialog.visible = false;
+ openNewsEdit(row);
+}
+
+async function handleNewsDelete(row) {
+ if (!canEditEnterpriseNewsRow(row)) {
+ ElMessage.warning("褰撳墠鐘舵�佷笉鍙垹闄�");
+ return;
+ }
+ if (row?.id == null || row.id === "") {
+ ElMessage.warning("鏃犳硶鍒犻櫎锛氱己灏戞柊闂� ID");
+ return;
+ }
+ const title = (row.title || "").trim() || "璇ユ潯鏂伴椈";
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゃ��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
+ } catch {
+ return;
+ }
+ try {
+ await deleteEnterpriseNews([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await fetchNewsList(searchForm);
+ } catch {
+ /* 閿欒鐢辫姹傛嫤鎴櫒鎻愮ず */
+ }
+}
+
+function onNewsFormClosed() {
+ newsFormRef.value?.resetFields?.();
+}
+
+function addGalleryItem() {
+ const name = (galleryInput.value || "").trim();
+ if (!name) return;
+ newsForm.mediaList = newsForm.mediaList || [];
+ newsForm.mediaList.push({ type: "image", name, url: "" });
+ galleryInput.value = "";
+}
+
+async function onNewsSave(action = "submit_review") {
+ try {
+ await newsFormRef.value?.validate();
+ } catch {
+ ElMessage.warning("璇峰畬鍠勮〃鍗曞繀濉」鍚庡啀淇濆瓨");
+ return;
+ }
+ const v = validateNewsForm(newsForm);
+ if (!v.ok) {
+ ElMessage.warning(v.message);
+ return;
+ }
+ const status = action === "draft" ? "DRAFT" : "PENDING";
+ newsForm.publishStatus = status;
+
+ if (!isNewsEdit.value) {
+ const templateId = newsForm.templateId || submitForm.templateId;
+ if (!templateId) {
+ ElMessage.warning("璇峰厛閫夋嫨瀹℃壒妯℃澘");
+ return;
+ }
+ if (!newsForm.templateId) newsForm.templateId = templateId;
+ if (!newsForm.templateName && submitForm.templateName) {
+ newsForm.templateName = submitForm.templateName;
+ }
+ if (action !== "draft") {
+ const bindingCheck = validateTemplateBinding({ flowNodes: submitForm.flowNodes });
+ if (!bindingCheck.ok) {
+ ElMessage.warning(bindingCheck.message);
+ return;
+ }
+ }
+ } else if (!newsForm.templateId && submitForm.templateId) {
+ newsForm.templateId = submitForm.templateId;
+ newsForm.templateName = submitForm.templateName || newsForm.templateName;
+ }
+
+ const dto = buildEnterpriseNewsSaveDto(newsForm, { status });
+ if (isNewsEdit.value) {
+ if (dto.id == null) {
+ ElMessage.warning("鏃犳硶淇敼锛氱己灏戞柊闂� ID");
+ return;
+ }
+ }
+
+ if (newsSaving.value) return;
+ newsSaving.value = true;
+ try {
+ if (isNewsEdit.value) {
+ await updateEnterpriseNews(dto);
+ } else {
+ await saveEnterpriseNews(dto);
+ }
+ newsFormDialog.visible = false;
+ const msg =
+ action === "draft" ? "宸蹭繚瀛樿崏绋�" : isNewsEdit.value ? "淇敼鎴愬姛" : "宸叉彁浜ゅ鏍�";
+ ElMessage.success(msg);
+ if (!isNewsEdit.value) page.current = 1;
+ await fetchNewsList(searchForm);
+ } catch {
+ /* 閿欒鐢辫姹傛嫤鎴櫒鎻愮ず */
+ } finally {
+ newsSaving.value = false;
+ }
+}
+
+function onSearch() {
+ fetchNewsList(searchForm);
+}
+
+function resetSearch() {
+ searchForm.keyword = "";
+ searchForm.newsType = "";
+ searchForm.status = "";
+ searchForm.createTimeRange = null;
+ onSearch();
+}
+
+function onPagination(obj) {
+ paginateNewsList(obj, searchForm);
+}
+
+onMounted(() => {
+ loadFlowUsers();
+ fetchNewsList(searchForm);
+});
+</script>
+
+<style scoped>
+.enterprise-news-page .search_form {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 12px;
+}
+.search_fields {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px;
+}
+.search_actions {
+ flex-shrink: 0;
+}
+.search_title {
+ font-size: 14px;
+ color: var(--el-text-color-regular);
+}
+.news-type-tag {
+ font-weight: 600;
+ font-size: 13px;
+}
+.media-tag {
+ margin: 6px 8px 0 0;
+}
+.template-name {
+ font-weight: 600;
+ color: var(--el-text-color-primary);
+}
+.section-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin: 8px 0 0;
+ line-height: 1.5;
+}
+.mb20 {
+ margin-bottom: 20px;
+}
+.ml10 {
+ margin-left: 10px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/EnterpriseNews/news-manage/useEnterpriseNewsList.js b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/useEnterpriseNewsList.js
new file mode 100644
index 0000000..66aef1e
--- /dev/null
+++ b/src/views/officeProcessAutomation/EnterpriseNews/news-manage/useEnterpriseNewsList.js
@@ -0,0 +1,55 @@
+import { listEnterpriseNewsPage } from "@/api/officeProcessAutomation/enterpriseNews.js";
+import { ElMessage } from "element-plus";
+import { reactive, ref } from "vue";
+import {
+ buildEnterpriseNewsListParams,
+ mapEnterpriseNewsFromApi,
+ unwrapEnterpriseNewsPage,
+} from "./enterpriseNewsMappers.js";
+
+/** 浼佷笟鏂伴椈鍒楄〃锛氬垎椤垫煡璇� /enterpriseNews/listPage */
+export function useEnterpriseNewsList() {
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ let lastSearchForm = null;
+
+ async function fetchList(searchForm = {}) {
+ tableLoading.value = true;
+ try {
+ const res = await listEnterpriseNewsPage(
+ buildEnterpriseNewsListParams({ page, searchForm })
+ );
+ const { records, total } = unwrapEnterpriseNewsPage(res);
+ tableData.value = records.map(mapEnterpriseNewsFromApi);
+ page.total = total;
+ } catch {
+ tableData.value = [];
+ page.total = 0;
+ ElMessage.error("浼佷笟鏂伴椈鍒楄〃鍔犺浇澶辫触");
+ } finally {
+ tableLoading.value = false;
+ }
+ }
+
+ function handleQuery(searchForm) {
+ lastSearchForm = searchForm;
+ page.current = 1;
+ return fetchList(searchForm);
+ }
+
+ function pagination({ page: p, limit }, searchForm) {
+ page.current = p;
+ page.size = limit;
+ return fetchList(searchForm ?? lastSearchForm ?? {});
+ }
+
+ return {
+ tableData,
+ tableLoading,
+ page,
+ fetchList,
+ handleQuery,
+ pagination,
+ };
+}
diff --git a/src/views/officeProcessAutomation/HrManage/post-manage/index.vue b/src/views/officeProcessAutomation/HrManage/post-manage/index.vue
new file mode 100644
index 0000000..a57137c
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/post-manage/index.vue
@@ -0,0 +1,292 @@
+<!--OA妯″潡锛氬矖浣嶇鐞�-->
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="宀椾綅缂栫爜" prop="postCode">
+ <el-input
+ v-model="queryParams.postCode"
+ placeholder="璇疯緭鍏ュ矖浣嶇紪鐮�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="宀椾綅鍚嶇О" prop="postName">
+ <el-input
+ v-model="queryParams.postName"
+ placeholder="璇疯緭鍏ュ矖浣嶅悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="宀椾綅鐘舵��" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:post:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:post:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:post:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['system:post:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="宀椾綅缂栧彿" align="center" prop="postId" />
+ <el-table-column label="宀椾綅缂栫爜" align="center" prop="postCode" />
+ <el-table-column label="宀椾綅鍚嶇О" align="center" prop="postName" />
+ <el-table-column label="宀椾綅鎺掑簭" align="center" prop="postSort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:post:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:post:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀瑰矖浣嶅璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="postRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="宀椾綅鍚嶇О" prop="postName">
+ <el-input v-model="form.postName" placeholder="璇疯緭鍏ュ矖浣嶅悕绉�" />
+ </el-form-item>
+ <el-form-item label="宀椾綅缂栫爜" prop="postCode">
+ <el-input v-model="form.postCode" placeholder="璇疯緭鍏ョ紪鐮佸悕绉�" />
+ </el-form-item>
+ <el-form-item label="宀椾綅椤哄簭" prop="postSort">
+ <el-input-number v-model="form.postSort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="宀椾綅鐘舵��" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Post">
+import { listPost, addPost, delPost, getPost, updatePost } from "@/api/system/post"
+import {onMounted} from "vue";
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const postList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ postCode: undefined,
+ postName: undefined,
+ status: undefined
+ },
+ rules: {
+ postName: [{ required: true, message: "宀椾綅鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ postCode: [{ required: true, message: "宀椾綅缂栫爜涓嶈兘涓虹┖", trigger: "blur" }],
+ postSort: [{ required: true, message: "宀椾綅椤哄簭涓嶈兘涓虹┖", trigger: "blur" }],
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ宀椾綅鍒楄〃 */
+function getList() {
+ loading.value = true
+ listPost(queryParams.value).then(response => {
+ postList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ postId: undefined,
+ postCode: undefined,
+ postName: undefined,
+ postSort: 0,
+ status: "0",
+ remark: undefined
+ }
+ proxy.resetForm("postRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.postId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞宀椾綅"
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const postId = row.postId || ids.value
+ getPost(postId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼宀椾綅"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["postRef"].validate(valid => {
+ if (valid) {
+ if (form.value.postId != undefined) {
+ updatePost(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addPost(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const postIds = row.postId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎宀椾綅缂栧彿涓�"' + postIds + '"鐨勬暟鎹」锛�').then(function() {
+ return delPost(postIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/post/export", {
+ ...queryParams.value
+ }, `post_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
diff --git a/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
new file mode 100644
index 0000000..291d57d
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/regular-apply/index.vue
@@ -0,0 +1,174 @@
+<!--OA妯″潡锛氳浆姝g敵璇�-->
+<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.applicantName"
+ 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>
+ <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.REGULAR"
+ 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: "",
+ applicantName: "",
+});
+
+const mod = useApprovalInstanceModule({
+ moduleKey: APPROVAL_MODULE_KEYS.REGULAR,
+ buildExtraListParams(sf) {
+ const extra = {};
+ const name = (sf?.applicantName || "").trim();
+ if (name) extra.applicantName = name;
+ return extra;
+ },
+});
+
+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.REGULAR,
+});
+
+function onSearch() {
+ handleQuery(searchForm);
+}
+
+function resetSearch() {
+ searchForm.instanceNo = "";
+ searchForm.applicantName = "";
+ 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_title {
+ font-size: 14px;
+ color: var(--el-text-color-regular);
+}
+</style>
diff --git a/src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue b/src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue
new file mode 100644
index 0000000..86c59ce
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/resign-apply/components/formDia.vue
@@ -0,0 +1,347 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板绂昏亴' : '缂栬緫绂昏亴'"
+ width="70%"
+ @close="closeDia"
+ >
+ <!-- 鍛樺伐淇℃伅灞曠ず鍖哄煙 -->
+ <div class="info-section">
+ <div class="info-title">鍛樺伐淇℃伅</div>
+ <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="濮撳悕锛�" prop="staffOnJobId">
+ <el-select v-model="form.staffOnJobId"
+ placeholder="璇烽�夋嫨浜哄憳"
+ style="width: 100%"
+ :disabled="operationType === 'edit'"
+ @change="handleSelect">
+ <el-option
+ v-for="item in personList"
+ :key="item.id"
+ :label="item.staffName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐缂栧彿锛�">
+ {{ currentStaffRecord.staffNo || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鎬у埆锛�">
+ {{ currentStaffRecord.sex || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎴风睄浣忓潃锛�">
+ {{ currentStaffRecord.nativePlace || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="宀椾綅锛�">
+ {{ currentStaffRecord.postName || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐜颁綇鍧�锛�">
+ {{ currentStaffRecord.adress || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绗竴瀛﹀巻锛�">
+ {{ currentStaffRecord.firstStudy || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓撲笟锛�">
+ {{ currentStaffRecord.profession || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="骞撮緞锛�">
+ {{ currentStaffRecord.age || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽锛�">
+ {{ currentStaffRecord.phone || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绱ф�ヨ仈绯讳汉锛�">
+ {{ currentStaffRecord.emergencyContact || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绱ф�ヨ仈绯讳汉鑱旂郴鐢佃瘽锛�">
+ {{ currentStaffRecord.emergencyContactPhone || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绂昏亴鏃ユ湡锛�" prop="leaveDate">
+ <el-date-picker
+ v-model="form.leaveDate"
+ type="date"
+ :disabled="operationType === 'edit'"
+ :disabled-date="disabledFutureDate"
+ placeholder="璇烽�夋嫨绂昏亴鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绂昏亴鍘熷洜锛�" prop="reason">
+ <el-select v-model="form.reason" placeholder="璇烽�夋嫨绂昏亴鍘熷洜" style="width: 100%" @change="handleSelectDimissionReason">
+ <el-option
+ v-for="(item, index) in dimissionReasonOptions"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="澶囨敞锛�" prop="remark" v-if="form.reason === 'other'">
+ <el-input
+ v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="澶囨敞"
+ maxlength="500"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+<!-- <el-row :gutter="30">-->
+<!-- <el-col :span="12">-->
+<!-- <div class="info-item">-->
+<!-- <span class="info-label">绂昏亴鍘熷洜锛�</span>-->
+<!-- <el-select v-model="form.reason" placeholder="璇烽�夋嫨浜哄憳" style="width: 100%" @change="handleSelect">-->
+<!-- <el-option-->
+<!-- v-for="(item, index) in dimissionReasonOptions"-->
+<!-- :key="index"-->
+<!-- :label="item.label"-->
+<!-- :value="item.value"-->
+<!-- />-->
+<!-- </el-select>-->
+<!-- </div>-->
+<!-- </el-col>-->
+<!-- <el-col :span="12">-->
+<!-- <div class="info-item">-->
+<!-- <span class="info-label">鍛樺伐缂栧彿锛�</span>-->
+<!-- <span class="info-value">{{ form.staffNo || '-' }}</span>-->
+<!-- </div>-->
+<!-- </el-col>-->
+<!-- </el-row>-->
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, toRefs, getCurrentInstance} from "vue";
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+import {createStaffLeave, updateStaffLeave} from "@/api/personnelManagement/staffLeave.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const getTodayDate = () => {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = `${now.getMonth() + 1}`.padStart(2, '0');
+ const day = `${now.getDate()}`.padStart(2, '0');
+ return `${year}-${month}-${day}`;
+};
+
+const disabledFutureDate = (time) => {
+ const todayEnd = new Date();
+ todayEnd.setHours(23, 59, 59, 999);
+ return time.getTime() > todayEnd.getTime();
+};
+const data = reactive({
+ form: {
+ staffOnJobId: undefined,
+ leaveDate: "",
+ reason: "",
+ remark: "",
+ },
+ rules: {
+ staffName: [{ required: true, message: "璇烽�夋嫨浜哄憳" }],
+ leaveDate: [{ required: true, message: "璇烽�夋嫨绂昏亴鏃ユ湡", trigger: "change" }],
+ reason: [{ required: true, message: "璇烽�夋嫨绂昏亴鍘熷洜"}],
+ },
+ dimissionReasonOptions: [
+ {label: '钖祫寰呴亣', value: 'salary'},
+ {label: '鑱屼笟鍙戝睍', value: 'career_development'},
+ {label: '宸ヤ綔鐜', value: 'work_environment'},
+ {label: '涓汉鍘熷洜', value: 'personal_reason'},
+ {label: '鍏朵粬', value: 'other'},
+ ],
+ currentStaffRecord: {},
+});
+const { form, rules, dimissionReasonOptions, currentStaffRecord } = toRefs(data);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === 'edit') {
+ currentStaffRecord.value = row
+ form.value.staffOnJobId = row.staffOnJobId
+ form.value.leaveDate = row.leaveDate
+ form.value.reason = row.reason
+ form.value.remark = row.remark
+ personList.value = [
+ {
+ staffName: row.staffName,
+ id: row.staffOnJobId,
+ }
+ ]
+ } else {
+ form.value.leaveDate = getTodayDate()
+ getList()
+ }
+}
+
+const handleSelectDimissionReason = (val) => {
+ if (val === 'other') {
+ form.value.remark = ''
+ }
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ form.value.staffState = 0
+ if (form.value.reason !== 'other') {
+ form.value.remark = ''
+ }
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ createStaffLeave(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ } else {
+ updateStaffLeave(currentStaffRecord.value.id, form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ }
+ })
+
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ // 琛ㄥ崟宸叉敞閲婏紝鎵嬪姩閲嶇疆琛ㄥ崟鏁版嵁
+ form.value = {
+ staffOnJobId: undefined,
+ leaveDate: "",
+ reason: "",
+ remark: "",
+ };
+ dialogFormVisible.value = false;
+ emit('close')
+};
+
+const personList = ref([]);
+
+/**
+ * 鑾峰彇褰撳墠鍦ㄨ亴浜哄憳鍒楄〃
+ */
+const getList = () => {
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1
+ }).then(res => {
+ personList.value = res.data.records || []
+ })
+};
+
+const handleSelect = (val) => {
+ let obj = personList.value.find(item => item.id === val)
+ currentStaffRecord.value = {}
+ if (obj) {
+ // 淇濈暀绂昏亴鏃ユ湡鍜岀鑱屽師鍥狅紝鍙洿鏂板憳宸ヤ俊鎭�
+ currentStaffRecord.value = obj
+ }
+}
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+.info-section {
+ background: #f5f7fa;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.info-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #e4e7ed;
+}
+
+.info-item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16px;
+ min-height: 32px;
+}
+
+.info-label {
+ min-width: 140px;
+ color: #606266;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.info-value {
+ flex: 1;
+ color: #303133;
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/HrManage/resign-apply/index.vue b/src/views/officeProcessAutomation/HrManage/resign-apply/index.vue
new file mode 100644
index 0000000..2b20970
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/resign-apply/index.vue
@@ -0,0 +1,220 @@
+<!--OA妯″潡锛氱鑱岀敵璇�-->
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">濮撳悕锛�</span>
+ <el-input
+ v-model="searchForm.staffName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ鍚嶆悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板绂昏亴</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import {onMounted, ref} from "vue";
+import FormDia from "@/views/personnelManagement/dimission/components/formDia.vue";
+import { findStaffLeaveListPage } from "@/api/personnelManagement/staffLeave.js";
+import {ElMessageBox} from "element-plus";
+
+const data = reactive({
+ searchForm: {
+ staffName: "",
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "staffState",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 0) {
+ return "绂昏亴";
+ } else if (params == 1) {
+ return "鍦ㄨ亴";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params == 0) {
+ return "danger";
+ } else if (params == 1) {
+ return "primary";
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ label: "绂昏亴鏃ユ湡",
+ prop: "leaveDate",
+ },
+ {
+ label: "鍛樺伐缂栧彿",
+ prop: "staffNo",
+ },
+ {
+ label: "濮撳悕",
+ prop: "staffName",
+ },
+ {
+ label: "鎬у埆",
+ prop: "sex",
+ },
+ {
+ label: "鎴风睄浣忓潃",
+ prop: "nativePlace",
+ },
+ {
+ label: "閮ㄩ棬",
+ prop: "deptName",
+ },
+ {
+ label: "宀椾綅",
+ prop: "postName",
+ },
+ {
+ label: "鐜颁綇鍧�",
+ prop: "adress",
+ width:200
+ },
+ {
+ label: "绗竴瀛﹀巻",
+ prop: "firstStudy",
+ },
+ {
+ label: "涓撲笟",
+ prop: "profession",
+ width:100
+ },
+ {
+ label: "骞撮緞",
+ prop: "age",
+ },
+ {
+ label: "鑱旂郴鐢佃瘽",
+ prop: "phone",
+ width:150
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉",
+ prop: "emergencyContact",
+ width: 120
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉鐢佃瘽",
+ prop: "emergencyContactPhone",
+ width:150
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const formDia = ref()
+const { proxy } = getCurrentInstance()
+
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ findStaffLeaveListPage({...page, ...searchForm.value}).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/staff/staffLeave/export", {}, "浜哄憳绂昏亴.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue
new file mode 100644
index 0000000..0aa4f06
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/components/BasicInfoSection.vue
@@ -0,0 +1,181 @@
+<template>
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鍩烘湰淇℃伅
+ </span>
+ </template>
+
+ <el-row :gutter="24">
+ <el-col :span="5">
+ <el-form-item label="鍛樺伐缂栧彿" prop="staffNo">
+ <el-input
+ v-model="form.staffNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ :disabled="operationType !== 'add'"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="濮撳悕" prop="staffName">
+ <el-input
+ v-model="form.staffName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="鍒悕" prop="alias">
+ <el-input
+ v-model="form.alias"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="鎵嬫満" prop="phone">
+ <el-input
+ v-model="form.phone"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="11"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select
+ v-model="form.sex"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="鐢�" value="鐢�" />
+ <el-option label="濂�" value="濂�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="24">
+ <el-col :span="5">
+ <el-form-item label="鍑虹敓鏃ユ湡" prop="birthDate">
+ <el-date-picker
+ v-model="form.birthDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="骞撮緞" prop="age">
+ <el-input-number
+ v-model="form.age"
+ :min="0"
+ :max="150"
+ :precision="0"
+ :step="1"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="绫嶈疮" prop="nativePlace">
+ <el-input
+ v-model="form.nativePlace"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="姘戞棌" prop="nation">
+ <el-input
+ v-model="form.nation"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="濠氬Щ鐘跺喌" prop="maritalStatus">
+ <el-select
+ v-model="form.maritalStatus"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="鏈" value="鏈" />
+ <el-option label="宸插" value="宸插" />
+ <el-option label="绂诲紓" value="绂诲紓" />
+ <el-option label="涓у伓" value="涓у伓" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="24">
+ <el-col :span="10">
+ <el-form-item label="瑙掕壊" prop="roleId">
+ <el-select
+ v-model="form.roleId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in roleOptions"
+ :key="item.roleId"
+ :label="item.roleName"
+ :value="item.roleId"
+ :disabled="item.status == 1"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+ operationType: { type: String, default: "add" },
+ roleOptions: { type: Array, default: () => [] },
+});
+
+const { form, operationType, roleOptions } = toRefs(props);
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+</style>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue
new file mode 100644
index 0000000..c1470e7
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/components/EducationWorkSection.vue
@@ -0,0 +1,263 @@
+<template>
+ <div>
+ <!-- 鏁欒偛缁忓巻 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鏁欒偛缁忓巻
+ </span>
+ </template>
+ <el-table :data="form.staffEducationList" border>
+ <el-table-column label="瀛﹀巻" prop="education" width="120">
+ <template #default="{ row }">
+ <el-select
+ v-model="row.education"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="涓笓鍙婁互涓�" value="secondary" />
+ <el-option label="澶т笓" value="junior_college" />
+ <el-option label="鏈" value="bachelor" />
+ <el-option label="纭曞+" value="master" />
+ <el-option label="鍗氬+鍙婁互涓�" value="doctor" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="姣曚笟闄㈡牎" prop="schoolName" min-width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.schoolName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="30"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏ュ鏃堕棿" prop="enrollTime" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.enrollTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="姣曚笟鏃堕棿" prop="graduateTime" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.graduateTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="涓撲笟" prop="major" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.major"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="瀛︿綅" prop="degree" width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.degree"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffEducationList.length > 1"
+ type="primary"
+ link
+ @click="removeEducationRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addEducationRow">鏂板缓涓�琛�</div>
+ </el-card>
+
+ <!-- 宸ヤ綔缁忓巻 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 宸ヤ綔缁忓巻
+ </span>
+ </template>
+ <el-table :data="form.staffWorkExperienceList" border>
+ <el-table-column label="鍓嶅叕鍙�" prop="formerCompany" min-width="180">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerCompany"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="30"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍓嶅叕鍙搁儴闂�" prop="formerDept" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerDept"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍓嶅叕鍙歌亴浣�" prop="formerPosition" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerPosition"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="寮�濮嬫棩鏈�" prop="startDate" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.startDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="缁撴潫鏃ユ湡" prop="endDate" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.endDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ヤ綔鎻忚堪" prop="workDesc" min-width="220">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.workDesc"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="500"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffWorkExperienceList.length > 1"
+ type="primary"
+ link
+ @click="removeWorkRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addWorkRow">鏂板缓涓�琛�</div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+});
+
+const emit = defineEmits(["update:form"]);
+
+const { form } = toRefs(props);
+
+const addEducationRow = () => {
+ form.value.staffEducationList.push({
+ education: "",
+ schoolName: "",
+ enrollTime: "",
+ graduateTime: "",
+ major: "",
+ degree: "",
+ });
+};
+
+const removeEducationRow = (index) => {
+ if (form.value.staffEducationList.length <= 1) return;
+ form.value.staffEducationList.splice(index, 1);
+};
+
+const addWorkRow = () => {
+ form.value.staffWorkExperienceList.push({
+ formerCompany: "",
+ formerDept: "",
+ formerPosition: "",
+ startDate: "",
+ endDate: "",
+ workDesc: "",
+ });
+};
+
+const removeWorkRow = (index) => {
+ if (form.value.staffWorkExperienceList.length <= 1) return;
+ form.value.staffWorkExperienceList.splice(index, 1);
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.table-add-row {
+ margin-top: 8px;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+}
+</style>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue
new file mode 100644
index 0000000..bd63608
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/components/EmergencyAndAttachmentSection.vue
@@ -0,0 +1,115 @@
+<template>
+ <div>
+ <!-- 绱ф�ヨ仈绯讳汉 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 绱ф�ヨ仈绯讳汉
+ </span>
+ </template>
+ <el-table :data="form.staffEmergencyContactList" border>
+ <el-table-column label="绱ф�ヨ仈绯讳汉濮撳悕" prop="contactName" min-width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉鍏崇郴" prop="contactRelation" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactRelation"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉鎵嬫満" prop="contactPhone" width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactPhone"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="11"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉浣忓潃" prop="contactAddress" min-width="220">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactAddress"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffEmergencyContactList.length > 1"
+ type="primary"
+ link
+ @click="removeEmergencyRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addEmergencyRow">鏂板缓涓�琛�</div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true }
+});
+
+const { form } = toRefs(props);
+
+const addEmergencyRow = () => {
+ form.value.staffEmergencyContactList.push({
+ contactName: "",
+ contactRelation: "",
+ contactPhone: "",
+ contactAddress: "",
+ });
+};
+
+const removeEmergencyRow = (index) => {
+ if (form.value.staffEmergencyContactList.length <= 1) return;
+ form.value.staffEmergencyContactList.splice(index, 1);
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.table-add-row {
+ margin-top: 8px;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+}
+</style>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue
new file mode 100644
index 0000000..be33436
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/components/JobInfoSection.vue
@@ -0,0 +1,176 @@
+<template>
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鍦ㄨ亴淇℃伅
+ </span>
+ </template>
+
+ <!-- 绗竴琛岋細鍚堝悓寮�濮� / 鍚堝悓缁撴潫 / 璇曠敤鏈� / 杞 -->
+ <el-row :gutter="24">
+ <el-col :span="6">
+ <el-form-item label="鍏ヨ亴鏃ユ湡" prop="contractStartTime">
+ <el-date-picker
+ v-model="form.contractStartTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item
+ label="鍚堝悓缁撴潫鏃ユ湡"
+ prop="contractEndTime"
+ required
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鍚堝悓缁撴潫鏃ユ湡',
+ trigger: 'change',
+ },
+ ]"
+ >
+ <el-date-picker
+ v-model="form.contractEndTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璇曠敤鏈燂紙鏈堬級" prop="probationPeriod">
+ <el-input-number
+ v-model="form.proTerm"
+ :min="0"
+ :max="24"
+ :precision="0"
+ :step="1"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="杞鏃ユ湡" prop="positiveDate">
+ <el-date-picker
+ v-model="form.positiveDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 绗簩琛岋細閮ㄩ棬 / 宀椾綅 / 鍩烘湰宸ヨ祫 -->
+ <el-row :gutter="24">
+ <el-col :span="8">
+ <el-form-item label="閮ㄩ棬" prop="sysDeptId">
+ <el-tree-select
+ v-model="form.sysDeptId"
+ :data="deptOptions"
+ check-strictly
+ :render-after-expand="false"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="宀椾綅" prop="sysPostId">
+ <el-select
+ v-model="form.sysPostId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in postOptions"
+ :key="item.postId"
+ :label="item.postName"
+ :value="item.postId"
+ :disabled="item.status === '1'"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍩烘湰宸ヨ祫" prop="basicSalary">
+ <el-input-number
+ v-model="form.basicSalary"
+ :min="0"
+ :max="999999"
+ :precision="2"
+ :step="100"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+ postOptions: { type: Array, default: () => [] },
+ deptOptions: { type: Array, default: () => [] },
+});
+
+const { form, postOptions, deptOptions } = toRefs(props);
+
+// 璁$畻鍚堝悓骞撮檺
+const calculateContractTerm = () => {
+ if (form.value.contractStartTime && form.value.contractEndTime) {
+ const startDate = new Date(form.value.contractStartTime);
+ const endDate = new Date(form.value.contractEndTime);
+
+ if (endDate > startDate) {
+ // 璁$畻骞翠唤宸�
+ const yearDiff = endDate.getFullYear() - startDate.getFullYear();
+ const monthDiff = endDate.getMonth() - startDate.getMonth();
+ const dayDiff = endDate.getDate() - startDate.getDate();
+
+ let years = yearDiff;
+
+ // 濡傛灉缁撴潫鏃ユ湡鐨勬湀鏃ュ皬浜庡紑濮嬫棩鏈熺殑鏈堟棩锛屽垯鍑忓幓1骞�
+ if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
+ years = yearDiff - 1;
+ }
+
+ form.value.contractTerm = Math.max(0, years);
+ } else {
+ form.value.contractTerm = 0;
+ }
+ } else {
+ form.value.contractTerm = 0;
+ }
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+</style>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue
new file mode 100644
index 0000000..2ad06fb
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/components/NewOrEditFormDia.vue
@@ -0,0 +1,304 @@
+<template>
+ <FormDialog
+ v-model="dialogFormVisible"
+ :operation-type="operationType"
+ :title="dialogTitle"
+ width="90%"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia"
+ >
+ <div class="form-dia-body">
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ >
+ <BasicInfoSection
+ :form="form"
+ :operation-type="operationType"
+ :role-options="roleOptions"
+ />
+ <JobInfoSection
+ :form="form"
+ :post-options="postOptions"
+ :dept-options="deptOptions"
+ />
+ <EducationWorkSection :form="form" />
+ <EmergencyAndAttachmentSection :form="form" />
+ </el-form>
+ </div>
+ </FormDialog>
+</template>
+
+<script setup>
+import {
+ ref,
+ reactive,
+ toRefs,
+ onMounted,
+ getCurrentInstance,
+ nextTick,
+} from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { findPostOptions } from "@/api/system/post.js";
+import { deptTreeSelect, getUser } from "@/api/system/user.js";
+import {
+ staffOnJobInfo,
+ createStaffOnJob,
+ updateStaffOnJob,
+} from "@/api/personnelManagement/staffOnJob.js";
+
+import BasicInfoSection from "./BasicInfoSection.vue";
+import JobInfoSection from "./JobInfoSection.vue";
+import EducationWorkSection from "./EducationWorkSection.vue";
+import EmergencyAndAttachmentSection from "./EmergencyAndAttachmentSection.vue";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(["close"]);
+
+const dialogFormVisible = ref(false);
+const operationType = ref("add");
+const id = ref(0);
+const formRef = ref(null);
+
+const dialogTitle = () =>
+ operationType.value === "add" ? "鏂板鍏ヨ亴" : "缂栬緫浜哄憳";
+
+const createEmptyEducation = () => ({
+ education: "",
+ schoolName: "",
+ enrollTime: "",
+ graduateTime: "",
+ major: "",
+ degree: "",
+});
+
+const createEmptyWork = () => ({
+ formerCompany: "",
+ formerDept: "",
+ formerPosition: "",
+ startDate: "",
+ endDate: "",
+ workDesc: "",
+});
+
+const createEmptyEmergency = () => ({
+ contactName: "",
+ contactRelation: "",
+ contactPhone: "",
+ contactAddress: "",
+});
+
+const createDefaultForm = () => ({
+ id: undefined,
+ // 鍩烘湰淇℃伅
+ staffNo: "",
+ staffName: "",
+ alias: "",
+ phone: "",
+ sex: "",
+ birthDate: "",
+ age: undefined,
+ nativePlace: "",
+ nation: "",
+ maritalStatus: "",
+ politicalStatus: "",
+ firstWorkDate: "",
+ workingYears: undefined,
+ idCardNo: "",
+ hukouType: "",
+ email: "",
+ currentAddress: "",
+ // 鍦ㄨ亴淇℃伅
+ contractStartTime: "",
+ contractEndTime: "",
+ proTerm: undefined,
+ positiveDate: "",
+ sysDeptId: undefined,
+ sysPostId: undefined,
+ basicSalary: undefined,
+ // 閾惰鍗′俊鎭�
+ bankName: "",
+ bankCardNo: "",
+ // 鏁欒偛缁忓巻
+ staffEducationList: [createEmptyEducation()],
+ // 宸ヤ綔缁忓巻
+ staffWorkExperienceList: [createEmptyWork()],
+ // 绱ф�ヨ仈绯讳汉
+ staffEmergencyContactList: [createEmptyEmergency()],
+ // 瑙掕壊锛堝崟閫夛級
+ roleId: undefined,
+});
+
+const state = reactive({
+ form: createDefaultForm(),
+ rules: {
+ staffNo: [{ required: true, message: "璇疯緭鍏ュ憳宸ョ紪鍙�", trigger: "blur" }],
+ staffName: [{ required: true, message: "璇疯緭鍏ュ鍚�", trigger: "blur" }],
+ phone: [{ required: true, message: "璇疯緭鍏ユ墜鏈�", trigger: "blur" }],
+ sex: [{ required: true, message: "璇烽�夋嫨鎬у埆", trigger: "change" }],
+ birthDate: [
+ { required: true, message: "璇烽�夋嫨鍑虹敓鏃ユ湡", trigger: "change" },
+ ],
+ contractStartTime: [
+ { required: true, message: "璇烽�夋嫨鍏ヨ亴鏃ユ湡", trigger: "change" },
+ ],
+ contractEndTime: [
+ { required: true, message: "璇烽�夋嫨鍚堝悓缁撴潫鏃ユ湡", trigger: "change" },
+ ],
+ sysDeptId: [
+ { required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" },
+ ],
+ roleId: [{ required: true, message: "璇烽�夋嫨瑙掕壊", trigger: "change" }],
+ },
+ postOptions: [],
+ deptOptions: [],
+});
+
+const { form, rules, postOptions, deptOptions } = toRefs(state);
+const roleOptions = ref([]);
+
+const resetForm = () => {
+ Object.assign(form.value, createDefaultForm());
+ nextTick(() => {
+ formRef.value?.clearValidate();
+ });
+};
+
+const fetchPostOptions = () => {
+ findPostOptions().then((res) => {
+ postOptions.value = res.data || [];
+ });
+};
+
+const fetchDeptOptions = () => {
+ deptTreeSelect().then((response) => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data || []))
+ );
+ });
+};
+
+const fetchRoleOptions = () => {
+ getUser().then((res) => {
+ roleOptions.value = res.roles || [];
+ });
+};
+
+function filterDisabledDept(deptList) {
+ return deptList.filter((dept) => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+}
+
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ fetchPostOptions();
+ fetchDeptOptions();
+ fetchRoleOptions();
+ resetForm();
+ if (type === "edit" && row?.id) {
+ id.value = row.id;
+ staffOnJobInfo(id.value, {}).then((res) => {
+ const d = res.data || {};
+ Object.assign(form.value, {
+ ...form.value,
+ ...d,
+ });
+ if (
+ !Array.isArray(form.value.staffEducationList) ||
+ !form.value.staffEducationList.length
+ ) {
+ form.value.staffEducationList = [createEmptyEducation()];
+ }
+ if (
+ !Array.isArray(form.value.staffWorkExperienceList) ||
+ !form.value.staffWorkExperienceList.length
+ ) {
+ form.value.staffWorkExperienceList = [createEmptyWork()];
+ }
+ if (
+ !Array.isArray(form.value.staffEmergencyContactList) ||
+ !form.value.staffEmergencyContactList.length
+ ) {
+ form.value.staffEmergencyContactList = [createEmptyEmergency()];
+ }
+ if (form.value.sysPostId === 0) {
+ form.value.sysPostId = undefined;
+ }
+ if (form.value.sysDeptId === 0) {
+ form.value.sysDeptId = undefined;
+ }
+ });
+ }
+};
+
+onMounted(() => {
+ fetchPostOptions();
+ fetchDeptOptions();
+});
+
+const submitForm = () => {
+ if (!form.value.sysPostId) {
+ form.value.sysPostId = undefined;
+ }
+ if (!form.value.sysDeptId) {
+ form.value.sysDeptId = undefined;
+ }
+ // 鍏煎鍚庣鍙兘浠嶄娇鐢� roleIds 鏁扮粍
+ form.value.roleIds = form.value.roleId ? [form.value.roleId] : [];
+ formRef.value?.validate((valid) => {
+ if (valid) {
+ if (operationType.value === "add") {
+ createStaffOnJob(form.value).then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ } else {
+ updateStaffOnJob(id.value, form.value).then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ }
+ }
+ });
+};
+
+const closeDia = () => {
+ formRef.value?.resetFields();
+ dialogFormVisible.value = false;
+ emit("close");
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+.form-dia-body {
+ padding: 0;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.form-card {
+ margin-bottom: 16px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue
new file mode 100644
index 0000000..9c2acfc
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/components/RenewContract.vue
@@ -0,0 +1,141 @@
+<template>
+ <el-dialog
+ v-model="isShow"
+ title="缁鍚堝悓"
+ width="800px"
+ @close="closeModal"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-form-item label="鍚堝悓寮�濮嬫棩鏈燂細" prop="contractStartTime">
+ <el-date-picker
+ v-model="form.contractStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ <el-form-item label="鍚堝悓缁撴潫鏃ユ湡锛�" prop="contractEndTime">
+ <el-date-picker
+ v-model="form.contractEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ <el-form-item label="鍚堝悓骞撮檺锛�" prop="contractTerm">
+ <el-input-number v-model="form.contractTerm" :precision="0" :step="1" style="width: 100%" :disabled="true"/>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+// 缁鍚堝悓
+import { renewContract } from "@/api/personnelManagement/staffOnJob.js";
+import {computed, getCurrentInstance,} from "vue";
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+const data = reactive({
+ form: {
+ contractTerm: 0,
+ contractStartTime: "",
+ contractEndTime: "",
+ },
+ rules: {
+ contractTerm: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ contractStartTime: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ contractEndTime: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ }
+});
+const { form, rules } = toRefs(data);
+let { proxy } = getCurrentInstance()
+
+const props = defineProps({
+ id: {
+ type: Number,
+ default: 0,
+ },
+
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+})
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+// 璁$畻鍚堝悓骞撮檺
+const calculateContractTerm = () => {
+ if (form.value.contractStartTime && form.value.contractEndTime) {
+ const startDate = new Date(form.value.contractStartTime);
+ const endDate = new Date(form.value.contractEndTime);
+
+ if (endDate > startDate) {
+ // 璁$畻骞翠唤宸�
+ const yearDiff = endDate.getFullYear() - startDate.getFullYear();
+ const monthDiff = endDate.getMonth() - startDate.getMonth();
+ const dayDiff = endDate.getDate() - startDate.getDate();
+
+ let years = yearDiff;
+
+ // 濡傛灉缁撴潫鏃ユ湡鐨勬湀鏃ュ皬浜庡紑濮嬫棩鏈熺殑鏈堟棩锛屽垯鍑忓幓1骞�
+ if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
+ years = yearDiff - 1;
+ }
+
+ form.value.contractTerm = Math.max(0, years);
+ } else {
+ form.value.contractTerm = 0;
+ }
+ } else {
+ form.value.contractTerm = 0;
+ }
+};
+
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ renewContract(props.id, form.value).then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("缁鍚堝悓鎴愬姛");
+ emit('completed');
+ closeModal();
+ }
+ })
+ }
+ })
+}
+
+// 鍏抽棴寮规
+const closeModal = () => {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ form.value = {
+ contractTerm: 0,
+ contractStartTime: "",
+ contractEndTime: "",
+ };
+ isShow.value = false;
+};
+</script>
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue
new file mode 100644
index 0000000..5d0b261
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/components/Show.vue
@@ -0,0 +1,69 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="璇︽儏"
+ width="70%"
+ @close="closeDia"
+ >
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ height="600"
+ ></PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {staffOnJobInfo} from "@/api/personnelManagement/staffOnJob.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const tableColumn = ref([
+ {
+ label: "鍚堝悓寮�濮嬫棩鏈�",
+ prop: "contractStartTime",
+ },
+ {
+ label: "鍚堝悓缁撴潫鏃ユ湡",
+ prop: "contractEndTime",
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === 'edit') {
+ staffOnJobInfo({staffNo: row.staffNo}).then(res => {
+ tableData.value = res.data
+ })
+ }
+}
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-archive/index.vue b/src/views/officeProcessAutomation/HrManage/staff-archive/index.vue
new file mode 100644
index 0000000..66cec7a
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-archive/index.vue
@@ -0,0 +1,360 @@
+<!--OA妯″潡锛氬憳宸ユ。妗�-->
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">濮撳悕锛�</span>
+ <el-input
+ v-model="searchForm.staffName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ鍚嶆悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <span class="search_title search_title2">閮ㄩ棬锛�</span>
+ <el-tree-select
+ v-model="searchForm.sysDeptId"
+ :data="deptOptions"
+ check-strictly
+ :render-after-expand="false"
+ style="width: 240px"
+ placeholder="璇烽�夋嫨"
+ />
+ <span class="search_title search_title2">鍏ヨ亴鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.contractStartTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="openFormNewOrEditFormDia('add')">鏂板鍏ヨ亴</el-button>
+ <el-button type="info" @click="handleImport">瀵煎叆</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <show-form-dia ref="formDia" @close="handleQuery"></show-form-dia>
+ <new-or-edit-form-dia ref="formDiaNewOrEditFormDia" @close="handleQuery"></new-or-edit-form-dia>
+ <renew-contract
+ v-if="isShowRenewContractModal"
+ v-model:visible="isShowRenewContractModal"
+ :id="id"
+ @completed="handleQuery"
+ />
+
+ <!-- 瀵煎叆瀵硅瘽妗� -->
+ <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url"
+ :disabled="upload.isUploading"
+ :on-progress="handleFileUploadProgress"
+ :on-success="handleFileSuccess"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">涓嬭浇妯℃澘</el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { Search, UploadFilled } from "@element-plus/icons-vue";
+import {onMounted, ref} from "vue";
+import {ElMessageBox} from "element-plus";
+import { deptTreeSelect } from "@/api/system/user.js";
+import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
+import { getToken } from "@/utils/auth";
+import dayjs from "dayjs";
+
+const NewOrEditFormDia = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue"));
+const ShowFormDia = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/Show.vue"));
+const RenewContract = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
+
+const data = reactive({
+ searchForm: {
+ staffName: "",
+ entryDate: undefined, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ deptOptions: [],
+});
+const { searchForm, deptOptions } = toRefs(data);
+const isShowRenewContractModal = ref(false);
+const id = ref(0);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "staffState",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 0) {
+ return "绂昏亴";
+ } else if (params == 1) {
+ return "鍦ㄨ亴";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params == 0) {
+ return "danger";
+ } else if (params == 1) {
+ return "primary";
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ label: "鍛樺伐缂栧彿",
+ prop: "staffNo",
+ },
+ {
+ label: "濮撳悕",
+ prop: "staffName",
+ },
+ {
+ label: "鍒悕",
+ prop: "alias",
+ },
+ {
+ label: "鎵嬫満",
+ prop: "phone",
+ width: 150,
+ },
+ {
+ label: "鎬у埆",
+ prop: "sex",
+ },
+ {
+ label: "鍑虹敓鏃ユ湡",
+ prop: "birthDate",
+ width: 120,
+ },
+ {
+ label: "鍏ヨ亴鏃ユ湡",
+ prop: "contractStartTime",
+ width: 120,
+ },
+ {
+ label: "骞撮緞",
+ prop: "age",
+ },
+ {
+ label: "绫嶈疮",
+ prop: "nativePlace",
+ },
+ {
+ label: "姘戞棌",
+ prop: "nation",
+ width: 100,
+ },
+ {
+ label: "濠氬Щ鐘跺喌",
+ prop: "maritalStatus",
+ width: 100,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: 180,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openFormNewOrEditFormDia("edit", row);
+ },
+ },
+ {
+ name: "缁鍚堝悓",
+ type: "text",
+ showHide: row => row.staffState === 1,
+ clickFun: (row) => {
+ isShowRenewContractModal.value = true;
+ id.value = row.id;
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0
+});
+const formDia = ref()
+const formDiaNewOrEditFormDia = ref()
+const { proxy } = getCurrentInstance()
+
+// 瀵煎叆鐩稿叧
+const uploadRef = ref(null)
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞�
+ open: false,
+ // 寮瑰嚭灞傛爣棰�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import"
+})
+
+const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ fetchDeptOptions();
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined
+ staffOnJobListPage({...params}).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+const openFormNewOrEditFormDia = (type, row) => {
+ nextTick(() => {
+ formDiaNewOrEditFormDia.value?.openDialog(type, row)
+ })
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/staff/staffOnJob/export", {staffState: 1}, "鍛樺伐鍙拌处.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎叆鎸夐挳鎿嶄綔
+const handleImport = () => {
+ upload.title = "鍛樺伐瀵煎叆"
+ upload.open = true
+}
+
+// 涓嬭浇妯℃澘鎿嶄綔
+const importTemplate = () => {
+ proxy.download("/staff/staffOnJob/downloadTemplate", {}, `鍛樺伐瀵煎叆妯℃澘_${new Date().getTime()}.xlsx`)
+}
+
+// 鏂囦欢涓婁紶涓鐞�
+const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true
+}
+
+// 鏂囦欢涓婁紶鎴愬姛澶勭悊
+const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false
+ upload.isUploading = false
+ proxy.$refs["uploadRef"].handleRemove(file)
+ proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "瀵煎叆缁撴灉", { dangerouslyUseHTMLString: true })
+ getList()
+}
+
+// 鎻愪氦涓婁紶鏂囦欢
+const submitFileForm = () => {
+ proxy.$refs["uploadRef"].submit()
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+.search_title2 {
+ margin-left: 10px;
+}
+</style>
+
diff --git a/src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue b/src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue
new file mode 100644
index 0000000..3db1bee
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-contract/components/formDia.vue
@@ -0,0 +1,93 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible"
+ title="璇︽儏"
+ width="70%"
+ @close="closeDia">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ height="600"></PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <FileList v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ :record-type="'staff_contract'"
+ :record-id="recordId" />
+ </div>
+</template>
+
+<script setup>
+ import { ref, defineAsyncComponent, getCurrentInstance } from "vue";
+ import { findStaffContractListPage } from "@/api/personnelManagement/staffContract.js";
+ const FileList = defineAsyncComponent(() =>
+ import("@/components/Dialog/FileList.vue")
+ );
+ const { proxy } = getCurrentInstance();
+ const emit = defineEmits(["close"]);
+ const fileDialogVisible = ref(false);
+ const recordId = ref(0);
+ const dialogFormVisible = ref(false);
+ const operationType = ref("");
+ const tableColumn = ref([
+ {
+ label: "鍚堝悓骞撮檺",
+ prop: "contractTerm",
+ },
+ {
+ label: "鍚堝悓寮�濮嬫棩鏈�",
+ prop: "contractStartTime",
+ },
+ {
+ label: "鍚堝悓缁撴潫鏃ユ湡",
+ prop: "contractEndTime",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 120,
+ operation: [
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ recordId.value = row.id;
+ fileDialogVisible.value = true;
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+
+ // 鎵撳紑寮规
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === "edit") {
+ findStaffContractListPage({ staffOnJobId: row.id }).then(res => {
+ tableData.value = res.data.records;
+ });
+ }
+ };
+
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit("close");
+ };
+ defineExpose({
+ openDialog,
+ });
+</script>
+
+<style scoped>
+</style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue b/src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue
new file mode 100644
index 0000000..02f9cef
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-contract/filesDia.vue
@@ -0,0 +1,197 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ style="display: inline;margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ :page="page"
+ @selection-change="handleSelectionChange"
+ height="500"
+ @pagination="paginationSearch"
+ :total="page.total"
+ >
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import filePreview from '@/components/filePreview/index.vue'
+import {
+ fileAdd,
+ fileDel,
+ fileListPage
+} from "@/api/financialManagement/revenueManagement.js";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const filePreviewRef = ref()
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: (row) => {
+ lookFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const accountType = ref('')
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row,type) => {
+ accountType.value = type;
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const fileRow = {}
+ fileRow.name = res.data.originalName
+ fileRow.url = res.data.tempPath
+ uploadFile(fileRow)
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+function uploadFile(file) {
+ file.accountId = currentId.value;
+ file.accountType = accountType.value;
+ fileAdd(file).then(res => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ fileDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 棰勮闄勪欢
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/HrManage/staff-contract/index.vue b/src/views/officeProcessAutomation/HrManage/staff-contract/index.vue
new file mode 100644
index 0000000..8186bdd
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/staff-contract/index.vue
@@ -0,0 +1,296 @@
+<!--OA妯″潡锛氬憳宸ュ悎鍚�-->
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">濮撳悕锛�</span>
+ <el-input v-model="searchForm.staffName" style="width: 240px" placeholder="璇疯緭鍏ュ鍚嶆悳绱�" @change="handleQuery"
+ clearable :prefix-icon="Search" />
+ <span style="margin-left: 10px" class="search_title">鍚堝悓缁撴潫鏃ユ湡锛�</span>
+ <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+ placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true"
+ @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+
+ <!-- 鍚堝悓瀵煎叆瀵硅瘽妗� -->
+ <el-dialog
+ :title="upload.title"
+ v-model="upload.open"
+ width="400px"
+ append-to-body
+ >
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url + '?updateSupport=' + upload.updateSupport"
+ :disabled="upload.isUploading"
+ :on-progress="handleFileUploadProgress"
+ :on-success="handleFileSuccess"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <files-dia ref="filesDia"></files-dia>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import { onMounted, ref } from "vue";
+import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue";
+import { ElMessageBox } from "element-plus";
+import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
+import dayjs from "dayjs";
+import { getToken } from "@/utils/auth.js";
+import FilesDia from "./filesDia.vue";
+const data = reactive({
+ searchForm: {
+ staffName: "",
+ entryDate: null, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "staffState",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 0) {
+ return "绂昏亴";
+ } else if (params == 1) {
+ return "鍦ㄨ亴";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params == 0) {
+ return "danger";
+ } else if (params == 1) {
+ return "primary";
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ label: "鍛樺伐缂栧彿",
+ prop: "staffNo",
+ },
+ {
+ label: "濮撳悕",
+ prop: "staffName",
+ },
+ {
+ label: "鎬у埆",
+ prop: "sex",
+ },
+ {
+ label: "鎴风睄浣忓潃",
+ prop: "nativePlace",
+ },
+ {
+ label: "宀椾綅",
+ prop: "postName",
+ },
+ {
+ label: "鐜颁綇鍧�",
+ prop: "adress",
+ width: 200
+ },
+ {
+ label: "绗竴瀛﹀巻",
+ prop: "firstStudy",
+ },
+ {
+ label: "涓撲笟",
+ prop: "profession",
+ width: 100
+ },
+ {
+ label: "骞撮緞",
+ prop: "age",
+ },
+ {
+ label: "鑱旂郴鐢佃瘽",
+ prop: "phone",
+ width: 150
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉",
+ prop: "emergencyContact",
+ width: 120
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉鐢佃瘽",
+ prop: "emergencyContactPhone",
+ width: 150
+ },
+ {
+ label: "鍚堝悓缁撴潫鏃ユ湡",
+ prop: "contractExpireTime",
+ width: 120
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: 120,
+ operation: [
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ }
+ ],
+ },
+]);
+const filesDia = ref()
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const formDia = ref()
+const { proxy } = getCurrentInstance()
+
+const changeDaterange = (value) => {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ getList();
+};
+// 鎵撳紑闄勪欢寮规
+const openFilesFormDia = (row) => {
+ nextTick(() => {
+ filesDia.value?.openDialog( row,'鍚堝悓')
+ })
+};
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined
+ params.staffState = 1
+ staffOnJobListPage(params).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/staff/staffOnJob/export", {staffState: 1}, "鍚堝悓绠$悊.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙鍚堝悓瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙鍚堝悓瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+ updateSupport: 1,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import",
+});
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImport() {
+ upload.title = "鍚堝悓瀵煎叆";
+ upload.open = true;
+}
+/** 鎻愪氦涓婁紶鏂囦欢 */
+function submitFileForm() {
+ proxy.$refs["uploadRef"].submit();
+}
+/**鏂囦欢涓婁紶涓鐞� */
+const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true;
+};
+/** 鏂囦欢涓婁紶鎴愬姛澶勭悊 */
+const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false;
+ upload.isUploading = false;
+ proxy.$refs["uploadRef"].handleRemove(file);
+ getList();
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
+
diff --git a/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue b/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
new file mode 100644
index 0000000..f731d57
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/transfer-apply/index.vue
@@ -0,0 +1,347 @@
+<!--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-select
+ v-model="searchForm.applicantId"
+ filterable
+ remote
+ clearable
+ reserve-keyword
+ placeholder="璇烽�夋嫨鎴栨悳绱㈢敵璇蜂汉"
+ style="width: 220px"
+ :remote-method="remoteSearchApplicant"
+ :loading="applicantSearchLoading"
+ >
+ <el-option
+ v-for="u in applicantSearchOptions"
+ :key="u.userId"
+ :label="userSelectLabel(u)"
+ :value="u.userId"
+ />
+ </el-select>
+ <el-button type="primary" style="margin-left: 10px" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div>
+ <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"
+ flow-attachments-only
+ @submit="onSubmit"
+ >
+ <template #before="{ form, fields }">
+ <FormPayloadFields :fields="displayTemplateFields(fields)" :form-payload="form.formPayload" />
+ <el-form-item label="鍘熷矖浣�">
+ <el-input :model-value="originalPostName" placeholder="閫夋嫨鐢宠浜哄悗鑷姩甯﹀嚭" disabled />
+ </el-form-item>
+ </template>
+ </ApprovalInstanceSubmitDialog>
+
+ <ApprovalTemplateBindDialog
+ v-model:visible="templateBindVisible"
+ :module-key="APPROVAL_MODULE_KEYS.TRANSFER"
+ skip-form-confirm
+ @confirm="onTemplateBound"
+ @closed="onTemplateBindClosed"
+ />
+
+ <ApprovalInstanceDetailDialog
+ v-model="detailDialog.visible"
+ title="璋冨矖鐢宠璇︽儏"
+ :row="detailRow"
+ @edit="openEditFromDetail"
+ />
+ </div>
+</template>
+
+<script setup>
+import { findPostOptions } from "@/api/system/post.js";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { ElMessage } from "element-plus";
+import { computed, onMounted, reactive, ref, watch } from "vue";
+import FormPayloadFields from "../../ApproveManage/approve-list/components/FormPayloadFields.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";
+import { SELECT_OPTION_SOURCE } from "../../ApproveManage/approve-template/selectOptionSource.js";
+
+function isOriginalPostField(field) {
+ const label = String(field?.label || "");
+ return (
+ label.includes("鍘熷矖浣�") ||
+ field?.key === "originalPost" ||
+ field?.key === "originalPostName" ||
+ field?.key === "originalPostId"
+ );
+}
+
+function displayTemplateFields(fields = []) {
+ return (fields || []).filter((f) => !isOriginalPostField(f));
+}
+
+function findApplicantTemplateField(fields = []) {
+ return (
+ fields.find((f) => String(f?.label || "").includes("鐢宠浜�")) ||
+ fields.find((f) => f?.type === "select" && f?.optionSource === SELECT_OPTION_SOURCE.USER) ||
+ null
+ );
+}
+
+const searchForm = reactive({
+ instanceNo: "",
+ applicantId: "",
+});
+
+const mod = useApprovalInstanceModule({
+ moduleKey: APPROVAL_MODULE_KEYS.TRANSFER,
+});
+
+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 allUsersCache = ref([]);
+const postIdToName = ref({});
+const targetPostOptions = ref([]);
+const applicantSearchLoading = ref(false);
+const applicantSearchOptions = ref([]);
+const originalPostName = ref("");
+
+const applicantTemplateField = computed(() =>
+ findApplicantTemplateField(submitForm.formFieldDefs)
+);
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload && Array.isArray(payload.data)) return payload.data;
+ if (payload && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+function userSelectLabel(u) {
+ const nick = u.nickName || "";
+ const name = u.userName || "";
+ if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
+ return nick || name || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
+}
+
+function firstPostId(user) {
+ if (!user) return undefined;
+ if (Array.isArray(user.postIds) && user.postIds.length) return user.postIds[0];
+ if (user.postId != null && user.postId !== "") return user.postId;
+ return undefined;
+}
+
+function resolveOriginalPost(user) {
+ if (!user) return { originalPostName: "" };
+ const nameStr = (user.postName ?? user.postname ?? "").toString().trim();
+ if (nameStr) return { originalPostName: nameStr };
+ if (Array.isArray(user.posts) && user.posts.length) {
+ return { originalPostName: (user.posts[0].postName ?? "").toString() || "鏈懡鍚嶅矖浣�" };
+ }
+ const pid = firstPostId(user);
+ if (pid != null && pid !== "") {
+ const n = postIdToName.value[String(pid)] || "";
+ return { originalPostName: n || "褰撳墠宀椾綅锛堟湭鍦ㄥ矖浣嶅瓧鍏镐腑锛�" };
+ }
+ return { originalPostName: "鏈垎閰嶅矖浣�" };
+}
+
+function userById(id) {
+ if (id == null || id === "") return undefined;
+ return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
+}
+
+function filterUsersByQuery(query) {
+ const list = allUsersCache.value.filter((u) => isActiveUser(u));
+ const q = (query || "").trim().toLowerCase();
+ if (!q) return [...list];
+ return list.filter((u) => {
+ const nick = (u.nickName || "").toLowerCase();
+ const uname = (u.userName || "").toLowerCase();
+ const phone = (u.phonenumber || u.phone || "").toString();
+ return nick.includes(q) || uname.includes(q) || phone.includes(q);
+ });
+}
+
+async function loadUserPool() {
+ try {
+ const res = await userListNoPageByTenantId();
+ allUsersCache.value = unwrapArray(res);
+ } catch {
+ allUsersCache.value = [];
+ }
+}
+
+async function loadPostOptions() {
+ try {
+ const res = await findPostOptions();
+ const rows = res.data ?? res.rows ?? [];
+ targetPostOptions.value = Array.isArray(rows) ? rows : [];
+ } catch {
+ targetPostOptions.value = [];
+ }
+ const m = {};
+ for (const p of targetPostOptions.value) {
+ const id = p.postId ?? p.value ?? p.id;
+ if (id != null && id !== "") m[String(id)] = p.postName ?? p.label ?? "";
+ }
+ postIdToName.value = m;
+}
+
+async function remoteSearchApplicant(query) {
+ applicantSearchLoading.value = true;
+ try {
+ if (!allUsersCache.value.length) await loadUserPool();
+ applicantSearchOptions.value = filterUsersByQuery(query);
+ } finally {
+ applicantSearchLoading.value = false;
+ }
+}
+
+function syncOriginalPostFromApplicant(uid) {
+ const u = userById(uid);
+ originalPostName.value = resolveOriginalPost(u).originalPostName;
+}
+
+watch(
+ () => {
+ const key = applicantTemplateField.value?.key;
+ return key ? submitForm.formPayload[key] : undefined;
+ },
+ async (uid) => {
+ if (!applicantTemplateField.value) return;
+ if (!allUsersCache.value.length) await loadUserPool();
+ syncOriginalPostFromApplicant(uid);
+ }
+);
+
+watch(
+ () => submitDialog.visible,
+ async (v) => {
+ if (!v) return;
+ const key = applicantTemplateField.value?.key;
+ if (key && submitForm.formPayload[key]) {
+ syncOriginalPostFromApplicant(submitForm.formPayload[key]);
+ }
+ }
+);
+
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, {
+ moduleKey: APPROVAL_MODULE_KEYS.TRANSFER,
+});
+
+function onSearch() {
+ handleQuery(searchForm);
+}
+
+async function resetSearch() {
+ searchForm.instanceNo = "";
+ searchForm.applicantId = "";
+ onSearch();
+ await remoteSearchApplicant("");
+}
+
+function onPagination(obj) {
+ pagination(obj, searchForm);
+}
+
+async function onSubmit() {
+ const ok = await submitInstanceForm({ skipValidate: true });
+ if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
+}
+
+onMounted(async () => {
+ await Promise.all([loadUserPool(), loadPostOptions()]);
+ loadFlowUsers();
+ await remoteSearchApplicant("");
+ 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_title {
+ font-size: 14px;
+ color: var(--el-text-color-regular);
+}
+</style>
diff --git a/src/views/officeProcessAutomation/HrManage/work-handover/index.vue b/src/views/officeProcessAutomation/HrManage/work-handover/index.vue
new file mode 100644
index 0000000..a9b96ae
--- /dev/null
+++ b/src/views/officeProcessAutomation/HrManage/work-handover/index.vue
@@ -0,0 +1,249 @@
+<!--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-select
+ v-model="searchForm.applicantId"
+ filterable
+ remote
+ clearable
+ reserve-keyword
+ placeholder="璇烽�夋嫨鎴栨悳绱㈢敵璇蜂汉"
+ style="width: 220px"
+ :remote-method="remoteSearchApplicant"
+ :loading="applicantSearchLoading"
+ >
+ <el-option
+ v-for="u in applicantSearchOptions"
+ :key="u.userId"
+ :label="userSelectLabel(u)"
+ :value="u.userId"
+ />
+ </el-select>
+ <el-button type="primary" style="margin-left: 10px" @click="onSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div>
+ <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.WORK_HANDOVER"
+ skip-form-confirm
+ @confirm="onTemplateBound"
+ @closed="onTemplateBindClosed"
+ />
+
+ <ApprovalInstanceDetailDialog
+ v-model="detailDialog.visible"
+ title="宸ヤ綔浜ゆ帴璇︽儏"
+ :row="detailRow"
+ @edit="openEditFromDetail"
+ />
+ </div>
+</template>
+
+<script setup>
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { ElMessage } from "element-plus";
+import { onMounted, reactive, ref } 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 handoverStatusOptions = [
+ { value: "in_progress", label: "杩涜涓�" },
+ { value: "completed", label: "宸插畬鎴�" },
+ { value: "returned", label: "宸查��鍥�" },
+];
+
+const handoverTypeOptions = [
+ { value: "resignation", label: "绂昏亴浜ゆ帴" },
+ { value: "transfer", label: "璋冨矖浜ゆ帴" },
+];
+
+const searchForm = reactive({
+ instanceNo: "",
+ applicantId: "",
+});
+
+const mod = useApprovalInstanceModule({
+ moduleKey: APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+});
+
+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 allUsersCache = ref([]);
+const applicantSearchOptions = ref([]);
+const applicantSearchLoading = ref(false);
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload && Array.isArray(payload.data)) return payload.data;
+ if (payload && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+function userSelectLabel(u) {
+ const nick = u.nickName || "";
+ const name = u.userName || "";
+ if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
+ return nick || name || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
+}
+
+function filterUsersByQuery(query) {
+ const list = allUsersCache.value.filter((u) => isActiveUser(u));
+ const q = (query || "").trim().toLowerCase();
+ if (!q) return list.slice(0, 50);
+ return list
+ .filter((u) => {
+ const nick = (u.nickName || "").toLowerCase();
+ const name = (u.userName || "").toLowerCase();
+ const id = String(u.userId ?? u.id ?? "");
+ return nick.includes(q) || name.includes(q) || id.includes(q);
+ })
+ .slice(0, 50);
+}
+
+async function loadUserPool() {
+ try {
+ const res = await userListNoPageByTenantId();
+ allUsersCache.value = unwrapArray(res);
+ } catch {
+ allUsersCache.value = [];
+ }
+}
+
+async function remoteSearchApplicant(query) {
+ applicantSearchLoading.value = true;
+ try {
+ if (!allUsersCache.value.length) await loadUserPool();
+ applicantSearchOptions.value = filterUsersByQuery(query);
+ } finally {
+ applicantSearchLoading.value = false;
+ }
+}
+
+const tableColumn = buildInstanceTableColumns(tableData, buildTableActions, {
+ moduleKey: APPROVAL_MODULE_KEYS.WORK_HANDOVER,
+});
+
+function onSearch() {
+ handleQuery(searchForm);
+}
+
+async function resetSearch() {
+ searchForm.instanceNo = "";
+ searchForm.applicantId = "";
+ onSearch();
+ await remoteSearchApplicant("");
+}
+
+function onPagination(obj) {
+ pagination(obj, searchForm);
+}
+
+async function onSubmit() {
+ const ok = await submitInstanceForm({ skipValidate: true });
+ if (ok) ElMessage.success(isSubmitEdit.value ? "淇敼鎴愬姛" : "鎻愪氦鎴愬姛");
+}
+
+onMounted(async () => {
+ await loadUserPool();
+ loadFlowUsers();
+ await remoteSearchApplicant("");
+ 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_title {
+ font-size: 14px;
+ color: var(--el-text-color-regular);
+}
+</style>
diff --git a/src/views/officeProcessAutomation/NoticeAnnouncement/notice-manage/index.vue b/src/views/officeProcessAutomation/NoticeAnnouncement/notice-manage/index.vue
new file mode 100644
index 0000000..3f65cb7
--- /dev/null
+++ b/src/views/officeProcessAutomation/NoticeAnnouncement/notice-manage/index.vue
@@ -0,0 +1,12 @@
+<!--
+ 妯″潡涓枃鍚嶏細閫氱煡鍏憡
+ 鐩綍鏍囪瘑锛歂oticeAnnouncement/notice-manage
+ 澶嶇敤椤甸潰锛欯/views/collaborativeApproval/noticeManagement/index.vue锛堝崗鍚屽鎵�-閫氱煡鍏憡锛�
+-->
+<template>
+ <NoticeManagement />
+</template>
+
+<script setup>
+import NoticeManagement from "@/views/collaborativeApproval/noticeManagement/index.vue";
+</script>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue
new file mode 100644
index 0000000..4db16a7
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue
@@ -0,0 +1,74 @@
+<!-- 璐圭敤鎶ラ攢锛氳鎯呭彧璇婚潰鏉� -->
+<template>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鎶ラ攢鍗曞彿">{{ row.reimburseNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ラ攢鐘舵��">
+ <el-tag :type="statusTagType(row.approvalResult)" size="small">{{ statusLabel(row.approvalResult) }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="璐圭敤绫诲瀷">{{ expenseCategoryLabel(row.expenseCategory) }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠鏃堕棿">{{ row.applyTime || row.createTime || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍛樺伐缂栧彿">{{ row.employeeNo || row.applicantNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍛樺伐濮撳悕">{{ row.employeeName || row.applicantName || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ラ攢鍘熷洜" :span="2">{{ row.reimburseReason || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ラ攢閲戦">{{ row.applyAmount != null ? `${row.applyAmount} 鍏僠 : "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鏀舵浜�">{{ row.payee || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鏀舵璐﹀彿">{{ row.payeeAccount || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="寮�鎴锋敮琛�">{{ row.bankBranch || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item v-if="row.rejectReason" label="椹冲洖鍘熷洜" :span="2">
+ <span class="reject-text">{{ row.rejectReason }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿" :span="2">{{ row.createTime || "鈥�" }}</el-descriptions-item>
+ </el-descriptions>
+
+ <el-divider content-position="left">鎶ラ攢鏄庣粏</el-divider>
+ <el-table v-if="row.expenseDetails?.length" :data="row.expenseDetails" border size="small">
+ <el-table-column type="index" label="搴忓彿" width="55" align="center" />
+ <el-table-column prop="invoiceDate" label="鍙戠エ鏃ユ湡" width="120" />
+ <el-table-column label="璐圭敤绉戠洰" width="100">
+ <template #default="{ row: d }">{{ expenseSubjectLabel(d.expenseSubject) }}</template>
+ </el-table-column>
+ <el-table-column prop="amount" label="閲戦" width="100" />
+ <el-table-column prop="description" label="鎻忚堪" min-width="140" show-overflow-tooltip />
+ </el-table>
+ <el-empty v-else description="鏆傛棤鏄庣粏" :image-size="48" />
+
+ <el-divider content-position="left">鍙戠エ闄勪欢</el-divider>
+ <template v-if="attachmentFiles.length">
+ <el-tag v-for="(f, i) in attachmentFiles" :key="i" class="file-tag" type="info" @click="openFile(f)">
+ {{ f.name }}
+ </el-tag>
+ </template>
+ <el-empty v-else description="鏆傛棤闄勪欢" :image-size="48" />
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { expenseCategoryLabel, expenseSubjectLabel, statusLabel, statusTagType } from "../costReimburseUtils.js";
+
+const props = defineProps({
+ row: { type: Object, default: () => ({}) },
+});
+
+const attachmentFiles = computed(() => {
+ const list =
+ props.row?.attachmentList ||
+ props.row?.storageBlobVOList ||
+ props.row?.invoiceAttachments;
+ return Array.isArray(list) ? list : [];
+});
+
+function openFile(f) {
+ const url = f?.url || f?.downloadURL || f?.previewURL;
+ if (url) window.open(url, "_blank");
+}
+</script>
+
+<style scoped>
+.reject-text {
+ color: var(--el-color-danger);
+}
+.file-tag {
+ margin: 0 8px 8px 0;
+ cursor: pointer;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
new file mode 100644
index 0000000..1736b3e
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
@@ -0,0 +1,313 @@
+import dayjs from "dayjs";
+
+/** 璐圭敤鎶ラ攢澶х被 */
+export const EXPENSE_CATEGORY_OPTIONS = [
+ { label: "宸梾", value: "travel" },
+ { label: "鍔炲叕閲囪喘", value: "office_procurement" },
+ { label: "涓氬姟鎷涘緟", value: "business_entertainment" },
+ { label: "浜ら�氳垂", value: "transport" },
+ { label: "閫氳璐�", value: "communication" },
+ { label: "鍏朵粬", value: "other" },
+];
+
+/** 鏄庣粏璐圭敤绉戠洰 */
+export const EXPENSE_SUBJECT_OPTIONS = [
+ { label: "浜ら�氳垂", value: "transport" },
+ { label: "浣忓璐�", value: "hotel" },
+ { label: "椁愰ギ璐�", value: "meal" },
+ { label: "鍔炲叕鐢ㄥ搧", value: "office_supply" },
+ { label: "鎷涘緟璐�", value: "entertainment" },
+ { label: "閫氳璐�", value: "phone" },
+ { label: "鍏朵粬", value: "other" },
+];
+
+/** 鍒嗙被濉姤妯℃澘锛堜竴閿皟鐢級 */
+export const CATEGORY_TEMPLATES = {
+ travel: {
+ label: "宸梾璐圭敤",
+ reason: "鍥犲叕鍑哄樊浜х敓鐨勪氦閫氥�佷綇瀹裤�侀楗瓑璐圭敤鎶ラ攢銆�",
+ details: [
+ { expenseSubject: "transport", description: "寰�杩斾氦閫氳垂" },
+ { expenseSubject: "hotel", description: "浣忓璐�" },
+ { expenseSubject: "meal", description: "鍑哄樊椁愰ギ" },
+ ],
+ },
+ office_procurement: {
+ label: "鍔炲叕閲囪喘",
+ reason: "閮ㄩ棬鏃ュ父鍔炲叕鐢ㄥ搧銆佽�楁潗閲囪喘鎶ラ攢銆�",
+ details: [
+ { expenseSubject: "office_supply", description: "鍔炲叕鐢ㄥ搧閲囪喘" },
+ { expenseSubject: "office_supply", description: "鎵撳嵃鑰楁潗" },
+ ],
+ },
+ business_entertainment: {
+ label: "涓氬姟鎷涘緟",
+ reason: "瀹㈡埛鎺ュ緟銆佸晢鍔″璇风瓑璐圭敤鎶ラ攢銆�",
+ details: [
+ { expenseSubject: "entertainment", description: "瀹㈡埛鎺ュ緟椁愯垂" },
+ { expenseSubject: "entertainment", description: "鍟嗗姟绀煎搧" },
+ ],
+ },
+ transport: {
+ label: "浜ら�氳垂",
+ reason: "甯傚唴閫氬嫟銆佹墦杞︺�佸仠杞︾瓑浜ら�氳垂鐢ㄦ姤閿�銆�",
+ details: [{ expenseSubject: "transport", description: "甯傚唴浜ら��" }],
+ },
+ communication: {
+ label: "閫氳璐�",
+ reason: "鍥犲叕閫氳銆佹祦閲忋�佽瘽璐硅ˉ璐存姤閿�銆�",
+ details: [{ expenseSubject: "phone", description: "璇濊垂/娴侀噺" }],
+ },
+ other: {
+ label: "鍏朵粬璐圭敤",
+ reason: "鍏朵粬鍥犲叕鏀嚭璐圭敤鎶ラ攢銆�",
+ details: [{ expenseSubject: "other", description: "鍏朵粬璐圭敤" }],
+ },
+};
+
+/** 瀹℃壒瑙掕壊灞曠ず鍚嶏紙鑺傜偣瀹℃壒浜洪』鍦ㄥ墠绔�夋嫨锛� */
+export const APPROVAL_ROLE_LABELS = {
+ direct_supervisor: "鐩村睘涓婄骇",
+ dept_manager: "閮ㄩ棬缁忕悊",
+ cfo: "璐㈠姟鎬荤洃",
+ compliance: "鍚堣瀹℃牳",
+};
+
+/** 鎸夐噾棰濋璁惧鎵归摼 */
+export const APPROVAL_AMOUNT_RULES = [
+ {
+ maxAmount: 500,
+ description: "500鍏冧互鍐咃細鐩村睘涓婄骇瀹℃壒",
+ roles: ["direct_supervisor"],
+ },
+ {
+ maxAmount: 5000,
+ description: "500锝�5000鍏冿細鐩村睘涓婄骇 + 閮ㄩ棬缁忕悊",
+ roles: ["direct_supervisor", "dept_manager"],
+ },
+ {
+ maxAmount: Infinity,
+ description: "瓒�5000鍏冿細鐩村睘涓婄骇 + 閮ㄩ棬缁忕悊 + 璐㈠姟鎬荤洃澶嶆牳",
+ roles: ["direct_supervisor", "dept_manager", "cfo"],
+ },
+];
+
+/** 閮ㄥ垎鍝佺被棰濆瀹℃壒鑺傜偣 */
+export const CATEGORY_EXTRA_APPROVAL = {
+ business_entertainment: ["compliance"],
+ office_procurement: [],
+};
+
+export function expenseCategoryLabel(v) {
+ return EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === v)?.label || "鈥�";
+}
+
+export function expenseSubjectLabel(v) {
+ return EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === v)?.label || "鈥�";
+}
+
+export function statusLabel(v) {
+ if (v === "draft") return "鑽夌";
+ if (v === "approved") return "宸查�氳繃";
+ if (v === "paid") return "宸蹭粯娆�";
+ if (v === "rejected") return "宸查┏鍥�";
+ if (v === "cancelled") return "宸叉挙鍥�";
+ return "瀹℃牳涓�";
+}
+
+export function statusTagType(v) {
+ if (v === "draft") return "info";
+ if (v === "approved" || v === "paid") return "success";
+ if (v === "rejected") return "danger";
+ if (v === "cancelled") return "info";
+ return "warning";
+}
+
+export { formatApprovalFlowSummary } from "../shared/finReimbursementMappers.js";
+
+export function resolveApprovalRoles(amount, expenseCategory) {
+ const amt = Number(amount) || 0;
+ let roles = [];
+ for (const rule of APPROVAL_AMOUNT_RULES) {
+ if (amt <= rule.maxAmount) {
+ roles = [...rule.roles];
+ break;
+ }
+ }
+ if (!roles.length) roles = ["direct_supervisor"];
+ const extra = CATEGORY_EXTRA_APPROVAL[expenseCategory] || [];
+ extra.forEach((r) => {
+ if (!roles.includes(r)) roles.push(r);
+ });
+ return roles;
+}
+
+export function buildAutoApprovalFlow(amount, expenseCategory, previousNodes = []) {
+ const roles = resolveApprovalRoles(amount, expenseCategory);
+ const prevByRole = new Map();
+ (previousNodes || []).forEach((n, idx) => {
+ if (n?.roleKey) prevByRole.set(n.roleKey, n);
+ else if (n?.approverId != null && n.approverId !== "") {
+ prevByRole.set(`__idx_${idx}`, n);
+ }
+ });
+ return roles.map((role, i) => {
+ const prev = prevByRole.get(role) || prevByRole.get(`__idx_${i}`);
+ const hasApprover = prev?.approverId != null && prev.approverId !== "";
+ return {
+ approverId: hasApprover ? prev.approverId : null,
+ approverName: hasApprover
+ ? prev.approverName || ""
+ : APPROVAL_ROLE_LABELS[role] || role,
+ roleKey: role,
+ signMode: prev?.signMode || "countersign",
+ sortOrder: i + 1,
+ nodeOrder: i + 1,
+ nodeStatus: i === 0 ? "process" : "wait",
+ approveOpinion: "",
+ approveTime: "",
+ };
+ });
+}
+
+export function getApprovalRuleHint(amount, expenseCategory) {
+ const amt = Number(amount) || 0;
+ const rule = APPROVAL_AMOUNT_RULES.find((r) => amt <= r.maxAmount) || APPROVAL_AMOUNT_RULES[APPROVAL_AMOUNT_RULES.length - 1];
+ const extra = CATEGORY_EXTRA_APPROVAL[expenseCategory] || [];
+ const extraText = extra.length
+ ? `锛�${expenseCategoryLabel(expenseCategory)}绫诲彟闇�锛�${extra.map((r) => APPROVAL_ROLE_LABELS[r] || r).join("銆�")}`
+ : "";
+ return `${rule.description}${extraText}`;
+}
+
+export function createEmptyExpenseDetail() {
+ return {
+ id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
+ invoiceDate: "",
+ expenseSubject: "",
+ amount: undefined,
+ description: "",
+ };
+}
+
+export function createEmptyForm() {
+ return {
+ id: undefined,
+ reimburseNo: "",
+ applicantId: "",
+ employeeNo: "",
+ employeeName: "",
+ expenseCategory: "",
+ reimburseReason: "",
+ applyAmount: undefined,
+ payee: "",
+ payeeAccount: "",
+ bankBranch: "",
+ expenseDetails: [],
+ attachmentList: [],
+ approvalFlowNodes: [],
+ currentNodeIndex: 0,
+ approvalResult: "pending",
+ rejectReason: "",
+ deptId: "",
+ deptName: "",
+ };
+}
+
+export function applyCategoryTemplate(form, category) {
+ const tpl = CATEGORY_TEMPLATES[category];
+ if (!tpl) return;
+ form.expenseCategory = category;
+ if (!form.reimburseReason?.trim()) form.reimburseReason = tpl.reason;
+ form.expenseDetails = (tpl.details || []).map((d) => ({
+ ...createEmptyExpenseDetail(),
+ expenseSubject: d.expenseSubject,
+ description: d.description,
+ invoiceDate: dayjs().format("YYYY-MM-DD"),
+ }));
+}
+
+export function initApprovalFlowNodes(nodes) {
+ return (nodes || []).map((n, i) => ({
+ ...n,
+ sortOrder: i + 1,
+ nodeOrder: i + 1,
+ nodeStatus: i === 0 ? "process" : "wait",
+ approveOpinion: n.approveOpinion || "",
+ approveTime: n.approveTime || "",
+ }));
+}
+
+export function advanceApprovalFlow(row, opinion) {
+ const nodes = [...(row.approvalFlowNodes || [])];
+ const idx = row.currentNodeIndex ?? 0;
+ if (!nodes.length) return { nodes, currentNodeIndex: idx, approvalResult: row.approvalResult };
+ const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ nodes[idx] = {
+ ...nodes[idx],
+ nodeStatus: "finish",
+ approveOpinion: opinion || "鍚屾剰",
+ approveTime: now,
+ };
+ const next = idx + 1;
+ if (next >= nodes.length) {
+ return { nodes, currentNodeIndex: idx, approvalResult: "approved", rejectReason: "" };
+ }
+ nodes[next] = { ...nodes[next], nodeStatus: "process" };
+ return { nodes, currentNodeIndex: next, approvalResult: "pending", rejectReason: "" };
+}
+
+export function rejectApprovalFlow(row, opinion) {
+ const nodes = [...(row.approvalFlowNodes || [])];
+ const idx = row.currentNodeIndex ?? 0;
+ const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ const reason = (opinion || "").trim() || "椹冲洖";
+ if (nodes[idx]) {
+ nodes[idx] = {
+ ...nodes[idx],
+ nodeStatus: "error",
+ approveOpinion: reason,
+ approveTime: now,
+ };
+ }
+ return { nodes, currentNodeIndex: idx, approvalResult: "rejected", rejectReason: reason };
+}
+
+export function normalizeImportedRow(raw, idx) {
+ const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`;
+ const expenseDetails = Array.isArray(raw.expenseDetails) ? raw.expenseDetails : [];
+ const applyAmount = raw.applyAmount ?? expenseDetails.reduce((s, d) => s + (Number(d.amount) || 0), 0);
+ const expenseCategory = raw.expenseCategory || "other";
+ const approvalFlowNodes =
+ Array.isArray(raw.approvalFlowNodes) && raw.approvalFlowNodes.length
+ ? raw.approvalFlowNodes
+ : buildAutoApprovalFlow(applyAmount, expenseCategory);
+
+ return {
+ id,
+ reimburseNo: raw.reimburseNo || `CR${dayjs().format("YYYYMMDD")}${String(idx).padStart(4, "0")}`,
+ applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`,
+ employeeNo: raw.employeeNo ?? raw.applicantNo ?? "",
+ employeeName: raw.employeeName ?? raw.applicantName ?? "鏈煡",
+ applicantNo: raw.employeeNo ?? raw.applicantNo ?? "",
+ applicantName: raw.employeeName ?? raw.applicantName ?? "鏈煡",
+ expenseCategory,
+ reimburseReason: raw.reimburseReason ?? "",
+ applyAmount,
+ payee: raw.payee ?? "",
+ payeeAccount: raw.payeeAccount ?? "",
+ bankBranch: raw.bankBranch ?? "",
+ expenseDetails,
+ attachmentList: Array.isArray(raw.attachmentList) ? raw.attachmentList : [],
+ invoiceAttachments: Array.isArray(raw.invoiceAttachments) ? raw.invoiceAttachments : [],
+ approvalFlowNodes,
+ currentNodeIndex: raw.currentNodeIndex ?? 0,
+ approvalResult: ["pending", "approved", "rejected"].includes(raw.approvalResult) ? raw.approvalResult : "pending",
+ rejectReason: raw.rejectReason ?? "",
+ approvalRecords: Array.isArray(raw.approvalRecords) ? raw.approvalRecords : [],
+ applyTime: raw.applyTime || raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ deptId: raw.deptId ?? "",
+ deptName: raw.deptName ?? "",
+ };
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
new file mode 100644
index 0000000..c9da4fc
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -0,0 +1,550 @@
+<!--OA妯″潡锛氳垂鐢ㄦ姤閿�锛堝垪琛� /finReimbursement/listPage锛宺eimbursementType=2锛�-->
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div class="search_fields">
+ <span class="search_title">鐢宠浜猴細</span>
+ <el-input
+ v-model="searchForm.applicantKeyword"
+ style="width: 220px"
+ placeholder="濮撳悕鎴栫紪鍙�"
+ clearable
+ :prefix-icon="Search"
+ @keyup.enter="handleQuery"
+ />
+ <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div class="search_actions">
+ <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>
+ <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>
+ <el-button type="primary" @click="openFormDialog('add')">鏂板璐圭敤鎶ラ攢</el-button>
+ </div>
+ </div>
+
+ <input ref="importInputRef" type="file" accept="application/json,.json" class="sr-only-input" @change="onImportFile" />
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="false"
+ :tableLoading="tableLoading"
+ :total="page.total"
+ @pagination="pagination"
+ />
+ </div>
+
+ <!-- 鏂板 / 缂栬緫 -->
+ <el-dialog
+ v-model="formDialog.visible"
+ :title="formDialog.title"
+ width="1120px"
+ append-to-body
+ destroy-on-close
+ class="cost-reimburse-form-dialog"
+ @closed="onFormClosed"
+ >
+ <el-alert type="info" show-icon :closable="false" class="mb16">
+ <template #title>鍏ㄥ搧绫昏垂鐢ㄦ姤閿� 路 鍒嗙被妯℃澘涓�閿~鎶�</template>
+ <template #default>
+ 鏀寔宸梾銆佸姙鍏噰璐�佷笟鍔℃嫑寰呫�佷氦閫氳垂銆侀�氳璐圭瓑锛涙寜閲戦鑷姩鍖归厤瀹℃壒閾撅紙500鍏冨唴鐩村睘涓婄骇锛岃秴5000鍏冭储鍔℃�荤洃澶嶆牳锛夈��
+ </template>
+ </el-alert>
+
+ <div v-if="!formDialog.readonly" class="template-bar mb16">
+ <span class="template-label">鍒嗙被妯℃澘锛�</span>
+ <el-button
+ v-for="(tpl, key) in CATEGORY_TEMPLATES"
+ :key="key"
+ size="small"
+ :type="form.expenseCategory === key ? 'primary' : 'default'"
+ plain
+ @click="applyTemplate(key)"
+ >
+ {{ tpl.label }}
+ </el-button>
+ </div>
+
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="formRules"
+ label-width="120px"
+ class="cost-reimburse-form"
+ :disabled="formDialog.readonly"
+ >
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">鍩烘湰淇℃伅</span></template>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐缂栧彿">
+ <el-input v-model="form.employeeNo" readonly placeholder="閫夋嫨鍛樺伐鍚庤嚜鍔ㄥ甫鍑�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐濮撳悕" prop="applicantId">
+ <el-select
+ v-model="form.applicantId"
+ filterable
+ remote
+ clearable
+ reserve-keyword
+ placeholder="璇烽�夋嫨鎴栨悳绱㈠憳宸�"
+ style="width: 100%"
+ :remote-method="remoteSearchApplicantForm"
+ :loading="applicantFormSearchLoading"
+ @change="onApplicantChange"
+ >
+ <el-option
+ v-for="u in applicantFormOptions"
+ :key="u.userId"
+ :label="userSelectLabel(u)"
+ :value="u.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璐圭敤绫诲瀷" prop="expenseCategory">
+ <el-select
+ v-model="form.expenseCategory"
+ placeholder="璇烽�夋嫨璐圭敤绫诲瀷"
+ style="width: 100%"
+ @change="onExpenseCategoryChange"
+ >
+ <el-option
+ v-for="opt in EXPENSE_CATEGORY_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎶ラ攢鐘舵��">
+ <el-tag
+ :type="form.approvalResult === 'approved' ? 'success' : form.approvalResult === 'rejected' ? 'danger' : 'warning'"
+ effect="plain"
+ >
+ {{
+ form.approvalResult === "approved"
+ ? "宸查�氳繃"
+ : form.approvalResult === "rejected"
+ ? "宸查┏鍥�"
+ : "瀹℃牳涓�"
+ }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鎶ラ攢鍘熷洜" prop="reimburseReason">
+ <el-input
+ v-model="form.reimburseReason"
+ type="textarea"
+ :rows="3"
+ placeholder="璇峰~鍐欐姤閿�鍘熷洜"
+ maxlength="2000"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎶ラ攢閲戦" prop="applyAmount">
+ <div class="amount-row">
+ <el-input-number
+ v-model="form.applyAmount"
+ :min="0"
+ :precision="2"
+ controls-position="right"
+ class="amount-input"
+ @change="autoAssignApprovalFlow"
+ />
+ <el-button v-if="!formDialog.readonly" type="primary" link @click="syncApplyAmountFromDetails">
+ 鎸夋槑缁嗘眹鎬� {{ detailTotalAmount }} 鍏�
+ </el-button>
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header>
+ <div class="card-header-row">
+ <span class="card-header-title">鎶ラ攢鏄庣粏</span>
+ <el-button v-if="!formDialog.readonly" type="primary" plain size="small" @click="addExpenseDetail">
+ 鏂板鏄庣粏
+ </el-button>
+ </div>
+ </template>
+
+ <el-table :data="form.expenseDetails" border size="small" class="detail-table">
+ <el-table-column type="index" label="搴忓彿" width="55" align="center" />
+ <el-table-column label="鍙戠エ鏃ユ湡" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-if="!formDialog.readonly"
+ v-model="row.invoiceDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ size="small"
+ style="width: 100%"
+ />
+ <span v-else>{{ row.invoiceDate || "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="璐圭敤绉戠洰" width="130">
+ <template #default="{ row }">
+ <el-select v-if="!formDialog.readonly" v-model="row.expenseSubject" size="small" style="width: 100%">
+ <el-option
+ v-for="opt in EXPENSE_SUBJECT_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ <span v-else>{{ expenseSubjectLabel(row.expenseSubject) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" width="120">
+ <template #default="{ row }">
+ <el-input-number
+ v-if="!formDialog.readonly"
+ v-model="row.amount"
+ :min="0"
+ :precision="2"
+ size="small"
+ controls-position="right"
+ style="width: 100%"
+ @change="onDetailAmountChange"
+ />
+ <span v-else>{{ row.amount ?? "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻忚堪" min-width="140">
+ <template #default="{ row }">
+ <el-input v-if="!formDialog.readonly" v-model="row.description" size="small" placeholder="璇存槑" />
+ <span v-else>{{ row.description || "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column v-if="!formDialog.readonly" label="鎿嶄綔" width="70" align="center">
+ <template #default="{ $index }">
+ <el-button type="danger" link size="small" @click="removeExpenseDetail($index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">鏀舵淇℃伅</span></template>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鏀舵浜�" prop="payee">
+ <el-input v-model="form.payee" placeholder="璇疯緭鍏ユ敹娆句汉" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏀舵璐﹀彿" prop="payeeAccount">
+ <el-input v-model="form.payeeAccount" placeholder="閾惰鍗″彿" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="寮�鎴锋敮琛�" prop="bankBranch">
+ <el-input v-model="form.bankBranch" placeholder="寮�鎴锋敮琛屽叏绉�" maxlength="100" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">闄勪欢锛堝彂绁級</span></template>
+ <el-form-item label-width="0" class="attachment-form-item">
+ <div class="upload-block">
+ <FileUpload v-model:file-list="form.attachmentList" :limit="20" button-text="鐐瑰嚮閫夋嫨鏂囦欢" />
+ </div>
+ </el-form-item>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header>
+ <div class="card-header-row">
+ <span class="card-header-title">瀹℃壒娴佺▼</span>
+ <el-button v-if="!formDialog.readonly" type="primary" link size="small" @click="autoAssignApprovalFlow">
+ 鎸夎鍒欓噸鏂板垎閰�
+ </el-button>
+ </div>
+ </template>
+ <el-alert type="success" :title="approvalRuleHint" show-icon :closable="false" class="mb12" />
+ <el-form-item prop="approvalFlowNodes" label-width="0">
+ <ApprovalFlowEditor
+ v-if="!formDialog.readonly"
+ v-model="form.approvalFlowNodes"
+ :user-options="flowUserOptions"
+ @update:model-value="onApprovalFlowChange"
+ />
+ <ApprovalFlowProgress v-else :nodes="form.approvalFlowNodes" :current-index="form.currentNodeIndex" />
+ <p v-if="!formDialog.readonly" class="flow-tip">绯荤粺宸叉寜閲戦涓庤垂鐢ㄧ被鍨嬭嚜鍔ㄥ垎閰嶅鎵逛汉锛屽彲鎵嬪姩璋冩暣銆�</p>
+ </el-form-item>
+ </el-card>
+ </el-form>
+ <template #footer>
+ <el-button
+ v-if="!formDialog.readonly"
+ type="primary"
+ :loading="submitSaving"
+ @click="submitForm"
+ >
+ 鎻� 浜�
+ </el-button>
+ <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "鍏� 闂�" : "鍙� 娑�" }}</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 璇︽儏 -->
+ <el-dialog v-model="detailDialog.visible" title="璐圭敤鎶ラ攢璇︽儏" width="900px" append-to-body destroy-on-close>
+ <div v-loading="detailLoading">
+ <DetailPanel :row="detailRow" />
+ <el-divider content-position="left">瀹℃壒娴佺▼</el-divider>
+ <ApprovalFlowProgress
+ :nodes="detailRow.approvalFlowProgressNodes ?? detailRow.approvalFlowNodes"
+ :current-index="detailRow.currentNodeIndex ?? 0"
+ />
+ <el-divider content-position="left">瀹℃壒璁板綍</el-divider>
+ <el-timeline v-if="detailRow.approvalRecords?.length">
+ <el-timeline-item
+ v-for="(rec, i) in detailRow.approvalRecords"
+ :key="i"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+ :timestamp="rec.time"
+ >
+ {{ rec.operatorName }} 鈥� {{ approvalActionLabel(rec.result) }}锛歿{ rec.opinion || "鏃犳剰瑙�" }}
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+ </div>
+ <template #footer>
+ <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 瀹℃壒 -->
+ <el-dialog
+ v-model="approveDialog.visible"
+ title="璐圭敤鎶ラ攢瀹℃壒"
+ width="1000px"
+ append-to-body
+ destroy-on-close
+ @closed="approveOpinion = ''"
+ >
+ <DetailPanel :row="approveDialog.row" />
+ <el-divider content-position="left">娴佺▼杩涘害</el-divider>
+ <ApprovalFlowProgress
+ :nodes="approveDialog.row?.approvalFlowProgressNodes ?? approveDialog.row?.approvalFlowNodes"
+ :current-index="approveDialog.row?.currentNodeIndex ?? 0"
+ />
+ <el-form label-width="100px" class="mt16">
+ <el-form-item label="瀹℃壒鎰忚" required>
+ <el-input
+ v-model="approveOpinion"
+ type="textarea"
+ :rows="3"
+ maxlength="500"
+ show-word-limit
+ placeholder="閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏叿浣撳師鍥狅紙濡傦細鍙戠エ妯$硦闇�閲嶄紶锛�"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="success" @click="submitApprove('approved')">閫� 杩�</el-button>
+ <el-button type="danger" @click="submitApprove('rejected')">椹� 鍥�</el-button>
+ <el-button @click="approveDialog.visible = false">鍙� 娑�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue";
+import ApprovalFlowProgress from "../travel-reimburse/components/ApprovalFlowProgress.vue";
+import DetailPanel from "./components/DetailPanel.vue";
+import { useCostReimburse } from "./useCostReimburse.js";
+
+const cr = useCostReimburse();
+const {
+ Search,
+ EXPENSE_CATEGORY_OPTIONS,
+ CATEGORY_TEMPLATES,
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseSubjectLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ importInputRef,
+ formRef,
+ form,
+ formDialog,
+ formRules,
+ detailDialog,
+ detailLoading,
+ detailRow,
+ approveDialog,
+ approveOpinion,
+ applicantFormSearchLoading,
+ applicantFormOptions,
+ flowUserOptions,
+ detailTotalAmount,
+ approvalRuleHint,
+ handleQuery,
+ resetSearch,
+ pagination,
+ remoteSearchApplicantForm,
+ userSelectLabel,
+ onApplicantChange,
+ onExpenseCategoryChange,
+ applyTemplate,
+ onDetailAmountChange,
+ onApprovalFlowChange,
+ addExpenseDetail,
+ removeExpenseDetail,
+ syncApplyAmountFromDetails,
+ autoAssignApprovalFlow,
+ openFormDialog,
+ onFormClosed,
+ submitForm,
+ submitSaving,
+ approvalActionLabel,
+ submitApprove,
+ handleExport,
+ handleImportClick,
+ onImportFile,
+} = cr;
+</script>
+
+<style scoped>
+.mb20 {
+ margin-bottom: 20px;
+}
+.mb16 {
+ margin-bottom: 16px;
+}
+.mb12 {
+ margin-bottom: 12px;
+}
+.mt16 {
+ margin-top: 16px;
+}
+.search_form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.search_fields {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 4px;
+}
+.search_actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.search_title {
+ font-size: 14px;
+ color: var(--el-text-color-regular);
+}
+.sr-only-input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+.template-bar {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+.template-label {
+ font-size: 14px;
+ color: var(--el-text-color-secondary);
+ flex-shrink: 0;
+}
+.form-section {
+ margin-bottom: 16px;
+ border: 1px solid var(--el-border-color-lighter);
+}
+.form-section :deep(.el-card__header) {
+ padding: 12px 16px;
+ background: var(--el-fill-color-lighter);
+}
+.form-section :deep(.el-card__body) {
+ padding: 16px 16px 4px;
+}
+.card-header-title {
+ font-size: 15px;
+ font-weight: 600;
+}
+.card-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.amount-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+}
+.amount-input {
+ flex: 1;
+ min-width: 160px;
+}
+.attachment-form-item {
+ margin-bottom: 0;
+}
+.detail-table {
+ margin-bottom: 0;
+}
+.upload-block {
+ width: 100%;
+}
+.flow-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin-top: 8px;
+}
+.cost-reimburse-form-dialog :deep(.el-dialog__body) {
+ padding-top: 12px;
+}
+.cost-reimburse-form :deep(.el-form-item) {
+ margin-bottom: 18px;
+}
+.cost-reimburse-form :deep(.el-input-number) {
+ width: 100%;
+}
+.cost-reimburse-form :deep(.el-row) {
+ margin-bottom: 0;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
new file mode 100644
index 0000000..3b90a3b
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
@@ -0,0 +1,634 @@
+import { Search } from "@element-plus/icons-vue";
+import dayjs from "dayjs";
+import {
+ deleteFinReimbursement,
+ getFinReimbursementDetail,
+ listFinReimbursementPage,
+ persistFinReimbursement,
+} from "@/api/officeProcessAutomation/finReimbursement.js";
+import { ElMessageBox } from "element-plus";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
+import {
+ buildCostReimbursementSaveDto,
+ buildFinReimbursementListParams,
+ filterReimbursementRowsBySearch,
+ hasActiveReimbursementSearch,
+ canDeleteReimbursementRow,
+ canEditReimbursementRow,
+ enrichReimbursementListRowsWithApprovalFlow,
+ filterRowsByReimbursementType,
+ FIN_REIMBURSEMENT_TYPE,
+ mapCostReimbursementRow,
+ mapFinReimbursementDetailRow,
+ resolveReimbursementDeleteId,
+ unwrapFinReimbursementDetail,
+ unwrapFinReimbursementPage,
+ validateReimbursementApprovalNodes,
+ validateReimbursementPersistDto,
+} from "../shared/finReimbursementMappers.js";
+import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js";
+import {
+ EXPENSE_CATEGORY_OPTIONS,
+ CATEGORY_TEMPLATES,
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseCategoryLabel,
+ expenseSubjectLabel,
+ statusLabel,
+ statusTagType,
+ formatApprovalFlowSummary,
+ buildAutoApprovalFlow,
+ getApprovalRuleHint,
+ createEmptyExpenseDetail,
+ createEmptyForm,
+ applyCategoryTemplate,
+ initApprovalFlowNodes,
+ advanceApprovalFlow,
+ rejectApprovalFlow,
+ normalizeImportedRow,
+} from "./costReimburseUtils.js";
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+function demoFlowNodes(amount = 1200, category = "transport") {
+ return buildAutoApprovalFlow(amount, category);
+}
+
+export function useCostReimburse() {
+ const { proxy } = getCurrentInstance();
+
+ const allRows = ref([]);
+
+ const searchForm = reactive({
+ applicantKeyword: "",
+ });
+ const tableLoading = ref(false);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const importInputRef = ref(null);
+ const allUsersCache = ref([]);
+ const applicantFormSearchLoading = ref(false);
+ const applicantFormOptions = ref([]);
+ const formRef = ref();
+ const form = reactive(createEmptyForm());
+ const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
+ const detailDialog = reactive({ visible: false });
+ const detailLoading = ref(false);
+ const detailRow = ref({});
+ const approveDialog = reactive({ visible: false, row: null });
+ const approveOpinion = ref("");
+ const submitSaving = ref(false);
+
+ const tableData = computed(() =>
+ allRows.value.map((r) => ({
+ ...r,
+ approvalFlowSummary: formatApprovalFlowSummary(r),
+ }))
+ );
+
+ async function fetchList() {
+ tableLoading.value = true;
+ try {
+ const res = await listFinReimbursementPage(
+ buildFinReimbursementListParams({
+ page,
+ searchForm,
+ reimbursementType: FIN_REIMBURSEMENT_TYPE.COST,
+ })
+ );
+ const { records, total } = unwrapFinReimbursementPage(res);
+ const filtered = filterRowsByReimbursementType(
+ records,
+ FIN_REIMBURSEMENT_TYPE.COST
+ );
+ let mapped = filtered.map(mapCostReimbursementRow);
+ mapped = await enrichReimbursementListRowsWithApprovalFlow(
+ mapped,
+ FIN_REIMBURSEMENT_TYPE.COST
+ );
+ if (hasActiveReimbursementSearch(searchForm)) {
+ mapped = filterReimbursementRowsBySearch(mapped, searchForm);
+ }
+ allRows.value = mapped;
+ const dropped = records.length - filtered.length;
+ let nextTotal =
+ dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total);
+ if (hasActiveReimbursementSearch(searchForm)) {
+ nextTotal = mapped.length;
+ }
+ page.total = nextTotal;
+ } catch {
+ allRows.value = [];
+ page.total = 0;
+ proxy?.$modal?.msgError?.("璐圭敤鎶ラ攢鍒楄〃鍔犺浇澶辫触");
+ } finally {
+ tableLoading.value = false;
+ }
+ }
+
+ const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
+
+ const detailTotalAmount = computed(() => {
+ const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
+ return Math.round(sum * 100) / 100;
+ });
+
+ const approvalRuleHint = computed(() =>
+ getApprovalRuleHint(form.applyAmount ?? detailTotalAmount.value, form.expenseCategory)
+ );
+
+ const tableColumn = ref([
+ { label: "鎶ラ攢鍗曞彿", prop: "reimburseNo", width: 150 },
+ { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
+ { label: "鐢宠浜�", prop: "applicantName", minWidth: 90 },
+ { label: "鎶ラ攢閲戦(鍏�)", prop: "applyAmount", width: 110 },
+ { label: "鎶ラ攢鍘熷洜", prop: "reimburseReason", minWidth: 160, showOverflowTooltip: true },
+ { label: "鐢宠鏃堕棿", prop: "applyTime", width: 165 },
+ { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 165 },
+ {
+ label: "鎶ラ攢鐘舵��",
+ prop: "approvalResult",
+ width: 100,
+ dataType: "tag",
+ formatData: (v) => statusLabel(v),
+ formatType: (v) => statusTagType(v),
+ },
+ {
+ label: "瀹℃壒娴佺▼",
+ prop: "approvalFlowSummary",
+ minWidth: 200,
+ showOverflowTooltip: true,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 220,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: (row) => !canEditReimbursementRow(row),
+ clickFun: (row) => openFormDialog("edit", row),
+ },
+ { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ disabled: (row) => !canDeleteReimbursementRow(row),
+ clickFun: (row) => confirmRemoveRow(row),
+ },
+ ],
+ },
+ ]);
+
+ const formRules = {
+ applicantId: [{ required: true, message: "璇烽�夋嫨鍛樺伐", trigger: "change" }],
+ expenseCategory: [{ required: true, message: "璇烽�夋嫨璐圭敤绫诲瀷", trigger: "change" }],
+ reimburseReason: [{ required: true, message: "璇峰~鍐欐姤閿�鍘熷洜", trigger: "blur" }],
+ applyAmount: [{ required: true, message: "璇峰~鍐欐姤閿�閲戦", trigger: "blur" }],
+ payee: [{ required: true, message: "璇峰~鍐欐敹娆句汉", trigger: "blur" }],
+ payeeAccount: [{ required: true, message: "璇峰~鍐欐敹娆捐处鍙�", trigger: "blur" }],
+ bankBranch: [{ required: true, message: "璇峰~鍐欏紑鎴锋敮琛�", trigger: "blur" }],
+ approvalFlowNodes: [
+ {
+ validator: (_r, _v, cb) => {
+ const nodes = form.approvalFlowNodes || [];
+ if (!nodes.length) {
+ cb(new Error("璇疯嚦灏戦厤缃竴涓鎵硅妭鐐�"));
+ return;
+ }
+ if (nodes.some((n) => n.approverId == null || n.approverId === "")) {
+ cb(new Error("姣忎釜鑺傜偣椤婚�夋嫨瀹℃壒浜�"));
+ return;
+ }
+ cb();
+ },
+ trigger: "change",
+ },
+ ],
+ };
+
+ async function loadUserPool() {
+ try {
+ allUsersCache.value = unwrapArray(await userListNoPageByTenantId());
+ } catch {
+ allUsersCache.value = [];
+ }
+ }
+
+ function userSelectLabel(u) {
+ const nick = u.nickName || "";
+ const name = u.userName || "";
+ if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
+ return nick || name || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
+ }
+
+ function userById(id) {
+ return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
+ }
+
+ function employeeNoFromUser(u) {
+ if (!u) return "";
+ return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
+ }
+
+ function filterUsersByQuery(query) {
+ const list = allUsersCache.value.filter(isActiveUser);
+ const q = (query || "").trim().toLowerCase();
+ if (!q) return [...list];
+ return list.filter((u) => {
+ const nick = (u.nickName || "").toLowerCase();
+ const uname = (u.userName || "").toLowerCase();
+ return nick.includes(q) || uname.includes(q);
+ });
+ }
+
+ async function remoteSearchApplicantForm(query) {
+ applicantFormSearchLoading.value = true;
+ try {
+ if (!allUsersCache.value.length) await loadUserPool();
+ applicantFormOptions.value = filterUsersByQuery(query);
+ } finally {
+ applicantFormSearchLoading.value = false;
+ }
+ }
+
+ function onApplicantChange(uid) {
+ const u = userById(uid);
+ if (u) {
+ form.employeeName = u.nickName || u.userName || "";
+ form.employeeNo = employeeNoFromUser(u);
+ form.payee = form.payee || form.employeeName;
+ form.deptId = String(u.deptId ?? u.sysDeptId ?? "");
+ form.deptName = u.dept?.deptName ?? u.deptName ?? "";
+ } else {
+ form.employeeName = "";
+ form.employeeNo = "";
+ }
+ }
+
+ function autoAssignApprovalFlow() {
+ const amount = Number(form.applyAmount) || detailTotalAmount.value || 0;
+ form.approvalFlowNodes = buildAutoApprovalFlow(
+ amount,
+ form.expenseCategory || "other",
+ form.approvalFlowNodes
+ );
+ nextTick(() => formRef.value?.validateField?.("approvalFlowNodes"));
+ }
+
+ function onExpenseCategoryChange(val) {
+ if (val && !(form.expenseDetails || []).length) {
+ applyCategoryTemplate(form, val);
+ syncApplyAmountFromDetails();
+ }
+ autoAssignApprovalFlow();
+ }
+
+ function applyTemplate(category) {
+ applyCategoryTemplate(form, category);
+ syncApplyAmountFromDetails();
+ autoAssignApprovalFlow();
+ proxy?.$modal?.msgSuccess?.(`宸插簲鐢ㄣ��${CATEGORY_TEMPLATES[category]?.label || category}銆嶅~鎶ユā鏉縛);
+ }
+
+ function onDetailAmountChange() {
+ syncApplyAmountFromDetails();
+ autoAssignApprovalFlow();
+ }
+
+ function onApprovalFlowChange() {
+ nextTick(() => formRef.value?.validateField?.("approvalFlowNodes"));
+ }
+
+ function addExpenseDetail() {
+ form.expenseDetails.push(createEmptyExpenseDetail());
+ }
+
+ function removeExpenseDetail(index) {
+ form.expenseDetails.splice(index, 1);
+ syncApplyAmountFromDetails();
+ autoAssignApprovalFlow();
+ }
+
+ function syncApplyAmountFromDetails() {
+ form.applyAmount = detailTotalAmount.value;
+ }
+
+ function handleQuery() {
+ page.current = 1;
+ return fetchList();
+ }
+
+ function resetSearch() {
+ searchForm.applicantKeyword = "";
+ handleQuery();
+ }
+
+ function pagination(obj) {
+ page.current = obj.page;
+ page.size = obj.limit;
+ return fetchList();
+ }
+
+ async function loadCostDetailRow(row) {
+ const id = resolveReimbursementDeleteId(row);
+ if (id == null) {
+ throw new Error("missing id");
+ }
+ const res = await getFinReimbursementDetail(id);
+ const raw = unwrapFinReimbursementDetail(res);
+ return mapFinReimbursementDetailRow(raw, FIN_REIMBURSEMENT_TYPE.COST);
+ }
+
+ async function openDetail(row) {
+ const id = resolveReimbursementDeleteId(row);
+ if (id == null) {
+ proxy?.$modal?.msgWarning?.("鏃犳硶鏌ョ湅璇︽儏锛氱己灏戞姤閿�鍗� ID");
+ return;
+ }
+ detailDialog.visible = true;
+ detailLoading.value = true;
+ detailRow.value = { ...row };
+ try {
+ detailRow.value = await loadCostDetailRow(row);
+ } catch {
+ proxy?.$modal?.msgError?.("鍔犺浇璇︽儏澶辫触");
+ detailDialog.visible = false;
+ } finally {
+ detailLoading.value = false;
+ }
+ }
+
+ async function confirmRemoveRow(row) {
+ const id = resolveReimbursementDeleteId(row);
+ if (id == null) {
+ proxy?.$modal?.msgWarning?.("鏃犳硶鍒犻櫎锛氱己灏戞姤閿�鍗� ID");
+ return;
+ }
+ const title = row.reimburseNo || row.billNo || row.reimburseReason || "璇ユ姤閿�鍗�";
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゃ��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
+ } catch {
+ return;
+ }
+ try {
+ await deleteFinReimbursement([id]);
+ proxy?.$modal?.msgSuccess?.("鍒犻櫎鎴愬姛");
+ if (detailDialog.visible && resolveReimbursementDeleteId(detailRow.value) === id) {
+ detailDialog.visible = false;
+ }
+ await handleQuery();
+ } catch {
+ proxy?.$modal?.msgError?.("鍒犻櫎澶辫触");
+ }
+ }
+
+ function openApprove(row) {
+ approveDialog.row = { ...row };
+ approveDialog.visible = true;
+ }
+
+ function approvalActionLabel(v) {
+ if (v === "approved") return "閫氳繃";
+ if (v === "rejected") return "椹冲洖";
+ return "鎻愪氦";
+ }
+
+ async function openFormDialog(mode, row) {
+ formDialog.mode = mode;
+ formDialog.readonly = false;
+ formDialog.title = mode === "add" ? "鏂板璐圭敤鎶ラ攢" : "缂栬緫璐圭敤鎶ラ攢";
+ if (!allUsersCache.value.length) await loadUserPool();
+ Object.assign(form, createEmptyForm());
+ if (mode === "edit" && row) {
+ let editRow = row;
+ try {
+ editRow = await loadCostDetailRow(row);
+ } catch {
+ proxy?.$modal?.msgError?.("鍔犺浇鎶ラ攢璇︽儏澶辫触");
+ return;
+ }
+ Object.assign(form, {
+ ...JSON.parse(JSON.stringify(editRow)),
+ reimbursementId: editRow.reimbursementId ?? editRow.id,
+ attachmentList: JSON.parse(JSON.stringify(editRow.attachmentList || editRow.invoiceAttachments || [])),
+ approvalFlowNodes: JSON.parse(JSON.stringify(editRow.approvalFlowNodes || [])),
+ expenseDetails: JSON.parse(JSON.stringify(editRow.expenseDetails || [])),
+ });
+ const u = userById(editRow.applicantId);
+ applicantFormOptions.value = u
+ ? [u]
+ : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }];
+ } else {
+ form.approvalFlowNodes = buildAutoApprovalFlow(0, "other");
+ remoteSearchApplicantForm("");
+ }
+ formDialog.visible = true;
+ nextTick(() => {
+ formRef.value?.clearValidate?.();
+ });
+ }
+
+ function onFormClosed() {
+ formRef.value?.resetFields?.();
+ }
+
+ async function submitForm() {
+ try {
+ await formRef.value?.validate?.();
+ } catch {
+ return;
+ }
+ if (!(form.expenseDetails || []).length) {
+ proxy?.$modal?.msgWarning?.("璇疯嚦灏戞坊鍔犱竴鏉℃姤閿�鏄庣粏");
+ return;
+ }
+ syncApplyAmountFromDetails();
+
+ if (submitSaving.value) return;
+ const isEdit = formDialog.mode === "edit";
+ const dto = buildCostReimbursementSaveDto(form);
+ const check = validateReimbursementPersistDto(dto, isEdit);
+ if (!check.ok) {
+ proxy?.$modal?.msgWarning?.(check.message);
+ return;
+ }
+ const nodeCheck = validateReimbursementApprovalNodes(dto);
+ if (!nodeCheck.ok) {
+ proxy?.$modal?.msgWarning?.(nodeCheck.message);
+ return;
+ }
+ submitSaving.value = true;
+ try {
+ await persistFinReimbursement(dto, isEdit);
+ proxy?.$modal?.msgSuccess?.(isEdit ? "淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ formDialog.visible = false;
+ await handleQuery();
+ } catch {
+ proxy?.$modal?.msgError?.(isEdit ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触");
+ } finally {
+ submitSaving.value = false;
+ }
+ }
+
+ async function submitApprove(result) {
+ const row = approveDialog.row;
+ if (!row) return;
+ if (result === "rejected" && !(approveOpinion.value || "").trim()) {
+ proxy?.$modal?.msgWarning?.("椹冲洖椤诲~鍐欏鎵规剰瑙侊紙濡傦細鍙戠エ妯$硦闇�閲嶄紶锛�");
+ return;
+ }
+ const idx = allRows.value.findIndex((r) => r.id === row.id);
+ if (idx === -1) return;
+ const cur = allRows.value[idx];
+ const operatorName = "褰撳墠瀹℃壒浜�";
+ const record = {
+ operatorName,
+ result,
+ opinion: approveOpinion.value || (result === "approved" ? "鍚屾剰" : "椹冲洖"),
+ time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ };
+ const records = [...(cur.approvalRecords || []), record];
+ let flowUpdate;
+ if (result === "approved") {
+ flowUpdate = advanceApprovalFlow(cur, approveOpinion.value);
+ } else {
+ flowUpdate = rejectApprovalFlow(cur, approveOpinion.value);
+ }
+ allRows.value[idx] = {
+ ...cur,
+ approvalFlowNodes: flowUpdate.nodes,
+ currentNodeIndex: flowUpdate.currentNodeIndex,
+ approvalResult: flowUpdate.approvalResult,
+ rejectReason: flowUpdate.rejectReason ?? cur.rejectReason,
+ approvalRecords: records,
+ };
+ proxy?.$modal?.msgSuccess?.(result === "approved" ? "宸查�氳繃" : "宸查┏鍥�");
+ approveDialog.visible = false;
+ handleQuery();
+ }
+
+ function handleExport() {
+ const data = allRows.value;
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `璐圭敤鎶ラ攢瀵煎嚭_${dayjs().format("YYYYMMDDHHmmss")}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ proxy?.$modal?.msgSuccess?.(`宸插鍑� ${data.length} 鏉);
+ }
+
+ function handleImportClick() {
+ importInputRef.value?.click?.();
+ }
+
+ function onImportFile(e) {
+ const file = e.target.files?.[0];
+ e.target.value = "";
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ try {
+ const parsed = JSON.parse(String(reader.result || ""));
+ const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data;
+ if (!Array.isArray(arr) || !arr.length) {
+ proxy?.$modal?.msgWarning?.("瀵煎叆鏍煎紡椤讳负璐圭敤鎶ラ攢 JSON 鏁扮粍");
+ return;
+ }
+ arr.forEach((raw, i) => allRows.value.unshift(normalizeImportedRow(raw, i)));
+ proxy?.$modal?.msgSuccess?.(`鎴愬姛瀵煎叆 ${arr.length} 鏉);
+ handleQuery();
+ } catch {
+ proxy?.$modal?.msgError?.("瑙f瀽澶辫触");
+ }
+ };
+ reader.readAsText(file, "utf-8");
+ }
+
+ onMounted(async () => {
+ loadUserPool();
+ await fetchList();
+ const editPayload = consumeReimburseEditFromApprove();
+ if (editPayload?.reimbursementId != null) {
+ await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId });
+ }
+ });
+
+ return {
+ Search,
+ EXPENSE_CATEGORY_OPTIONS,
+ CATEGORY_TEMPLATES,
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseCategoryLabel,
+ expenseSubjectLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ importInputRef,
+ formRef,
+ form,
+ formDialog,
+ formRules,
+ detailDialog,
+ detailLoading,
+ detailRow,
+ approveDialog,
+ approveOpinion,
+ applicantFormSearchLoading,
+ applicantFormOptions,
+ flowUserOptions,
+ detailTotalAmount,
+ approvalRuleHint,
+ handleQuery,
+ resetSearch,
+ pagination,
+ remoteSearchApplicantForm,
+ userSelectLabel,
+ onApplicantChange,
+ onExpenseCategoryChange,
+ applyTemplate,
+ onDetailAmountChange,
+ onApprovalFlowChange,
+ addExpenseDetail,
+ removeExpenseDetail,
+ syncApplyAmountFromDetails,
+ autoAssignApprovalFlow,
+ openFormDialog,
+ onFormClosed,
+ submitForm,
+ submitSaving,
+ openDetail,
+ approvalActionLabel,
+ submitApprove,
+ handleExport,
+ handleImportClick,
+ onImportFile,
+ };
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue b/src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue
new file mode 100644
index 0000000..724376d
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/components/FinReimburseApprovePanel.vue
@@ -0,0 +1,70 @@
+<!-- 宸梾/璐圭敤鎶ラ攢锛氬鎵瑰垪琛ㄥ唴璇︽儏/瀹℃壒寮圭獥鍐呭锛堜笌鎶ラ攢椤靛脊绐椾竴鑷达級 -->
+<template>
+ <div v-loading="loading">
+ <TravelDetailPanel v-if="isTravel" :row="reimburseRow" />
+ <CostDetailPanel v-else :row="reimburseRow" />
+
+ <el-divider content-position="left">娴佺▼杩涘害</el-divider>
+ <ApprovalFlowProgress
+ :nodes="reimburseRow.approvalFlowProgressNodes ?? reimburseRow.approvalFlowNodes"
+ :current-index="reimburseRow.currentNodeIndex ?? 0"
+ />
+
+ <template v-if="mode === 'detail'">
+ <el-divider content-position="left">瀹℃壒璁板綍锛堝叏娴佺▼鐣欑棔锛�</el-divider>
+ <el-timeline v-if="reimburseRow.approvalRecords?.length">
+ <el-timeline-item
+ v-for="(rec, i) in reimburseRow.approvalRecords"
+ :key="i"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+ :timestamp="rec.time"
+ >
+ {{ rec.operatorName }} 鈥� {{ actionLabel(rec.result) }}锛歿{ rec.opinion || "鏃犳剰瑙�" }}
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+ </template>
+
+ <el-form v-else label-width="100px" class="mt16">
+ <el-form-item label="瀹℃壒鎰忚">
+ <el-input
+ :model-value="approveOpinion"
+ type="textarea"
+ :rows="3"
+ maxlength="500"
+ show-word-limit
+ :placeholder="isTravel ? '閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏師鍥�' : '閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏叿浣撳師鍥狅紙濡傦細鍙戠エ妯$硦闇�閲嶄紶锛�'"
+ @update:model-value="$emit('update:approveOpinion', $event)"
+ />
+ </el-form-item>
+ </el-form>
+ </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { isTravelReimbursementType } from "../finReimbursementMappers.js";
+import ApprovalFlowProgress from "../../travel-reimburse/components/ApprovalFlowProgress.vue";
+import CostDetailPanel from "../../cost-reimburse/components/DetailPanel.vue";
+import TravelDetailPanel from "../../travel-reimburse/components/DetailPanel.vue";
+
+const props = defineProps({
+ mode: { type: String, default: "detail" },
+ moduleKey: { type: String, default: "" },
+ reimburseRow: { type: Object, default: () => ({}) },
+ loading: { type: Boolean, default: false },
+ approveOpinion: { type: String, default: "" },
+});
+
+defineEmits(["update:approveOpinion"]);
+
+const isTravel = computed(() =>
+ isTravelReimbursementType(props.reimburseRow?.reimbursementType ?? props.moduleKey)
+);
+
+function actionLabel(v) {
+ if (v === "approved") return "閫氳繃";
+ if (v === "rejected") return "椹冲洖";
+ return "鎻愪氦";
+}
+</script>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js
new file mode 100644
index 0000000..c72633b
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementDetailExtras.js
@@ -0,0 +1,160 @@
+import { formatDisplayTime } from "../../ApproveManage/approve-template/approveTemplateConstants.js";
+import {
+ mapRecordResultFromApi,
+ mapRecordsFromApi,
+ mapTasksToFlowNodes,
+} from "../../ApproveManage/approve-list/approveListConstants.js";
+
+function taskStatusToNodeStatus(taskStatus) {
+ const s = String(taskStatus ?? "").toUpperCase();
+ if (["APPROVED", "COMPLETED", "FINISHED", "PASSED", "AGREE"].includes(s)) {
+ return "finish";
+ }
+ if (["REJECTED", "REJECT", "REFUSE", "REFUSED"].includes(s)) {
+ return "error";
+ }
+ if (["PENDING", "IN_APPROVAL", "PROCESS", "PROCESSING"].includes(s)) {
+ return "process";
+ }
+ return "wait";
+}
+
+/** storageBlobVOList 鈫� 椤甸潰闄勪欢鍒楄〃 */
+export function mapReimbursementAttachments(source = {}) {
+ const list =
+ source.storageBlobVOList ||
+ source.storageBlobDTOs ||
+ source.storageBlobDTOS ||
+ source.storageBlobVOS ||
+ source.attachmentList ||
+ source.invoiceAttachments ||
+ [];
+ if (!Array.isArray(list)) return [];
+ return list.map((b, i) => ({
+ ...b,
+ id: b.id ?? b.blobId ?? `att_${i}`,
+ name:
+ b.fileName ||
+ b.originalFilename ||
+ b.originalFileName ||
+ b.blobName ||
+ b.name ||
+ "闄勪欢",
+ url:
+ b.url ||
+ b.fileUrl ||
+ b.downloadUrl ||
+ b.downloadURL ||
+ b.previewUrl ||
+ b.previewURL ||
+ b.link ||
+ "",
+ }));
+}
+
+/** 瀹℃壒璁板綍鏉ヨ嚜 tasks锛堟瘡鏉′换鍔′竴鏉$暀鐥曪級 */
+export function mapTasksToApprovalRecords(tasks) {
+ const list = Array.isArray(tasks) ? tasks : [];
+ return list
+ .map((t, index) => ({
+ id: t.id ?? index,
+ operatorName: t.approverName || t.operatorName || t.createUserName || "鈥�",
+ result: mapRecordResultFromApi(
+ t.approveAction ?? t.taskStatus ?? t.status
+ ),
+ opinion: t.approveComment || t.comment || t.opinion || "",
+ time: formatDisplayTime(
+ t.approveTime || t.finishTime || t.updateTime || t.createTime || ""
+ ),
+ levelNo: t.levelNo ?? t.taskLevel,
+ raw: t,
+ }))
+ .sort((a, b) => {
+ const la = Number(a.levelNo ?? 0);
+ const lb = Number(b.levelNo ?? 0);
+ if (la !== lb) return la - lb;
+ return String(a.time).localeCompare(String(b.time));
+ });
+}
+
+/** tasks 鈫� ApprovalFlowProgress 鑺傜偣 */
+export function mapTasksToApprovalFlowNodes(tasks) {
+ const grouped = mapTasksToFlowNodes(tasks);
+ return grouped.map((node, i) => {
+ const approvers = node.approvers || [];
+ const statuses = approvers.map(a =>
+ taskStatusToNodeStatus(a.taskStatus ?? a.status)
+ );
+ let nodeStatus = "wait";
+ if (statuses.includes("error")) nodeStatus = "error";
+ else if (statuses.length && statuses.every(s => s === "finish")) {
+ nodeStatus = "finish";
+ } else if (statuses.includes("process")) nodeStatus = "process";
+
+ const names = approvers.map(a => a.approverName).filter(Boolean).join("銆�");
+ const opinions = approvers
+ .map(a => a.approveComment)
+ .filter(Boolean)
+ .join("锛�");
+
+ return {
+ nodeOrder: node.nodeOrder ?? node.levelNo ?? i + 1,
+ sortOrder: node.nodeOrder ?? node.levelNo ?? i + 1,
+ approverName: names || "鈥�",
+ approveOpinion: opinions,
+ approveTime: approvers.find(a => a.approveTime)?.approveTime || "",
+ nodeStatus,
+ signMode: node.signMode,
+ };
+ });
+}
+
+export function computeApprovalFlowCurrentIndex(approvalFlowNodes = []) {
+ const list = approvalFlowNodes || [];
+ const processing = list.findIndex(n => n.nodeStatus === "process");
+ if (processing >= 0) return processing;
+ const errorIdx = list.findIndex(n => n.nodeStatus === "error");
+ if (errorIdx >= 0) return errorIdx;
+ return list.filter(n => n.nodeStatus === "finish").length;
+}
+
+/** 璇︽儏 DTO 琛ュ厖 tasks / 闄勪欢 / 瀹℃壒璁板綍 */
+export function applyFinReimbursementDetailEnrichment(mapped, raw = {}) {
+ if (!mapped || typeof mapped !== "object") return mapped;
+ const source = { ...raw, ...mapped };
+ const tasks = Array.isArray(source.tasks) ? source.tasks : [];
+ const attachments = mapReimbursementAttachments(source);
+ const approvalRecords = tasks.length
+ ? mapTasksToApprovalRecords(tasks)
+ : mapRecordsFromApi(source.records || source.approvalRecords);
+ /** 琛ㄥ崟缂栬緫鍥炴樉锛氫繚鐣� nodes 鏄犲皠锛堝惈 approverId锛夛紝鍕跨敤 tasks 瑕嗙洊 */
+ const approvalFlowNodes = Array.isArray(mapped.approvalFlowNodes)
+ ? mapped.approvalFlowNodes
+ : [];
+ /** 璇︽儏/杩涘害鏉″睍绀猴細鏈� tasks 鏃剁敤浠诲姟鐘舵�佽妭鐐� */
+ const approvalFlowProgressNodes = tasks.length
+ ? mapTasksToApprovalFlowNodes(tasks)
+ : approvalFlowNodes;
+ const currentNodeIndex = computeApprovalFlowCurrentIndex(
+ approvalFlowProgressNodes.length ? approvalFlowProgressNodes : approvalFlowNodes
+ );
+ const rejectReason =
+ approvalRecords.find(r => r.result === "rejected")?.opinion ||
+ source.rejectReason ||
+ "";
+
+ return {
+ ...mapped,
+ tasks,
+ storageBlobVOList: attachments,
+ attachmentList: attachments,
+ invoiceAttachments: attachments,
+ approvalRecords,
+ records: tasks.length ? tasks : source.records,
+ approvalFlowNodes,
+ approvalFlowProgressNodes,
+ currentNodeIndex,
+ rejectReason,
+ flowNodes: tasks.length ? mapTasksToFlowNodes(tasks) : mapped.flowNodes,
+ };
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js
new file mode 100644
index 0000000..2525f70
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js
@@ -0,0 +1,904 @@
+import dayjs from "dayjs";
+import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js";
+import { APPROVAL_MODULE_KEYS } from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
+import { mapSignModeToApi } from "../../ApproveManage/approve-template/approveTemplateConstants.js";
+import { EXPENSE_CATEGORY_OPTIONS } from "../cost-reimburse/costReimburseUtils.js";
+import { EXPENSE_SUBJECT_OPTIONS } from "../travel-reimburse/travelReimburseUtils.js";
+import { mapTasksToFlowNodes } from "../../ApproveManage/approve-list/approveListConstants.js";
+import { applyFinReimbursementDetailEnrichment } from "./finReimbursementDetailExtras.js";
+
+/** 鎶ラ攢绫诲瀷锛�1-宸梾鎶ラ攢锛�2-璐圭敤鎶ラ攢 */
+export const FIN_REIMBURSEMENT_TYPE = {
+ TRAVEL: "1",
+ COST: "2",
+};
+
+const REIMBURSEMENT_TYPE_LABEL = {
+ [FIN_REIMBURSEMENT_TYPE.TRAVEL]: "宸梾鎶ラ攢",
+ [FIN_REIMBURSEMENT_TYPE.COST]: "璐圭敤鎶ラ攢",
+};
+
+/** 褰掍竴鍖栨姤閿�绫诲瀷锛�1-宸梾锛�2-璐圭敤 */
+export function normalizeReimbursementType(val) {
+ const s = String(val ?? "").trim();
+ if (s === "1" || s === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+ return FIN_REIMBURSEMENT_TYPE.TRAVEL;
+ }
+ if (s === "2" || s === FIN_REIMBURSEMENT_TYPE.COST) {
+ return FIN_REIMBURSEMENT_TYPE.COST;
+ }
+ return "";
+}
+
+export function reimbursementTypeLabel(type) {
+ return REIMBURSEMENT_TYPE_LABEL[normalizeReimbursementType(type)] || "鈥�";
+}
+
+export function getModuleKeyByReimbursementType(type) {
+ const t = normalizeReimbursementType(type);
+ if (t === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+ return APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE;
+ }
+ if (t === FIN_REIMBURSEMENT_TYPE.COST) {
+ return APPROVAL_MODULE_KEYS.COST_REIMBURSE;
+ }
+ return "";
+}
+
+/** 浼樺厛鎺ュ彛 reimbursementType锛屽叾娆¢〉闈� moduleKey / 鍏ュ弬 */
+export function resolveReimbursementType(raw, fallback) {
+ const fromApi = normalizeReimbursementType(raw?.reimbursementType);
+ if (fromApi) return fromApi;
+ return (
+ normalizeReimbursementType(fallback) ||
+ getReimbursementTypeByModuleKey(fallback) ||
+ ""
+ );
+}
+
+export function isTravelReimbursementType(type) {
+ return (
+ resolveReimbursementType({ reimbursementType: type }, type) ===
+ FIN_REIMBURSEMENT_TYPE.TRAVEL
+ );
+}
+
+export function filterRowsByReimbursementType(rows, expectedType) {
+ const expected = normalizeReimbursementType(expectedType);
+ if (!expected) return rows || [];
+ return (rows || []).filter((row) => {
+ const t = resolveReimbursementType(row, expected);
+ return t === expected;
+ });
+}
+
+export function getReimbursementTypeByModuleKey(moduleKey) {
+ if (moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE) {
+ return FIN_REIMBURSEMENT_TYPE.TRAVEL;
+ }
+ if (moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE) {
+ return FIN_REIMBURSEMENT_TYPE.COST;
+ }
+ return "";
+}
+
+export function unwrapFinReimbursementPage(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") {
+ return { records: [], total: 0 };
+ }
+ if (Array.isArray(data.records)) {
+ return { records: data.records, total: Number(data.total ?? 0) };
+ }
+ const nested = data.data;
+ if (nested && typeof nested === "object" && Array.isArray(nested.records)) {
+ return { records: nested.records, total: Number(nested.total ?? 0) };
+ }
+ return { records: [], total: 0 };
+}
+
+/** 璇︽儏鎺ュ彛 data 瑙e寘 */
+export function unwrapFinReimbursementDetail(res) {
+ const data = res?.data ?? res;
+ if (!data || typeof data !== "object") return {};
+ if (data.billNo != null || data.id != null || data.reimbursementType != null) {
+ return data;
+ }
+ const nested = data.data;
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
+ return nested;
+ }
+ if (data.finReimbursementDto && typeof data.finReimbursementDto === "object") {
+ return data.finReimbursementDto;
+ }
+ return data;
+}
+
+/** 璇︽儏鏌ヨ鍙傛暟锛坬uery finReimbursementDto锛� */
+export function buildFinReimbursementDetailParams(id) {
+ const raw = id?.id != null ? id.id : id;
+ const n = toNumber(raw);
+ return { finReimbursementDto: { id: n != null ? n : raw } };
+}
+
+/** 璇︽儏 DTO 鈫� 椤甸潰琛岋紙鎸� reimbursementType 鏄犲皠锛屽惈 tasks / storageBlobVOList锛� */
+export function mapFinReimbursementDetailRow(raw, reimbursementTypeOrModuleKey) {
+ const type = resolveReimbursementType(raw, reimbursementTypeOrModuleKey);
+ let mapped = {};
+ if (type === FIN_REIMBURSEMENT_TYPE.TRAVEL) {
+ mapped = mapTravelReimbursementRow(raw);
+ } else if (type === FIN_REIMBURSEMENT_TYPE.COST) {
+ mapped = mapCostReimbursementRow(raw);
+ } else {
+ mapped = raw || {};
+ }
+
+ let formApprovalFlowNodes = mapNodesToFormFlow(resolveRowApiNodes(raw));
+ if (!formApprovalFlowNodes.length && Array.isArray(raw?.tasks) && raw.tasks.length) {
+ formApprovalFlowNodes = mapNodesToFormFlow(mapTasksToFlowNodes(raw.tasks));
+ }
+
+ const enriched = applyFinReimbursementDetailEnrichment(mapped, raw);
+ return {
+ ...enriched,
+ approvalFlowNodes: formApprovalFlowNodes.length
+ ? formApprovalFlowNodes
+ : enriched.approvalFlowNodes,
+ reimbursementType: type,
+ reimbursementTypeLabel: reimbursementTypeLabel(type),
+ moduleKey: getModuleKeyByReimbursementType(type),
+ };
+}
+
+/** 鍗曟嵁鐘舵�� 鈫� 椤甸潰 approvalResult锛堝吋瀹� statusLabel锛� */
+export function mapBillStatusToApprovalResult(billStatus) {
+ const upper = String(billStatus ?? "").trim().toUpperCase();
+ if (upper === "DRAFT") return "draft";
+ if (upper === "IN_APPROVAL") return "pending";
+ if (upper === "APPROVED") return "approved";
+ if (upper === "REJECTED") return "rejected";
+ if (upper === "WITHDRAWN") return "cancelled";
+ if (upper === "PAID") return "paid";
+ return "pending";
+}
+
+function pickApplicantQuery(searchForm = {}) {
+ const kw = (searchForm.applicantKeyword || "").trim();
+ if (!kw) return {};
+ // 鍗犱綅銆屽鍚嶆垨缂栧彿銆嶏細濮撳悕璧� applicantName锛涚紪鍙峰彟浼� applicantCode
+ const out = { applicantName: kw };
+ if (!/[\u4e00-\u9fa5]/.test(kw)) {
+ out.applicantCode = kw;
+ }
+ return out;
+}
+
+/** 鏄惁瀛樺湪鍒楄〃绛涢�夋潯浠讹紙浠呯敵璇蜂汉锛� */
+export function hasActiveReimbursementSearch(searchForm = {}) {
+ return Boolean((searchForm?.applicantKeyword || "").trim());
+}
+
+/** 鏈嶅姟绔湭鐢熸晥鏃讹紝鎸夌敵璇蜂汉鍋氬墠绔厹搴曠瓫閫� */
+export function filterReimbursementRowsBySearch(rows, searchForm = {}) {
+ const list = Array.isArray(rows) ? rows : [];
+ const kw = (searchForm?.applicantKeyword || "").trim().toLowerCase();
+ if (!kw) return list;
+
+ return list.filter((row) => {
+ const parts = [
+ row.applicantName,
+ row.employeeName,
+ row.applicantNo,
+ row.applicantCode,
+ row.employeeNo,
+ ]
+ .filter((v) => v != null && v !== "")
+ .map((v) => String(v).toLowerCase());
+ return parts.some((p) => p.includes(kw));
+ });
+}
+
+/** 鎵佸钩鍖栦负 Spring GET 鍙粦瀹氱殑 query锛坒inReimbursementDto.xxx锛屽嬁鐢ㄦ柟鎷彿锛� */
+function appendDotNotationQuery(target, prefix, fields) {
+ if (!fields || typeof fields !== "object") return;
+ for (const [key, value] of Object.entries(fields)) {
+ if (value == null || value === "") continue;
+ target[`${prefix}.${key}`] = value;
+ }
+}
+
+/** 缁勮 listPage 鏌ヨ鍙傛暟锛堟墎骞� page.* / finReimbursementDto.*锛屼笌 detail 鎺ュ彛涓�鑷达級 */
+export function buildFinReimbursementListParams({
+ page,
+ searchForm,
+ reimbursementType,
+ extraDto = {},
+}) {
+ const dto = {
+ reimbursementType,
+ ...pickApplicantQuery(searchForm),
+ ...(extraDto && typeof extraDto === "object" ? extraDto : {}),
+ };
+
+ const params = {
+ current: page.current,
+ size: page.size,
+ "page.current": page.current,
+ "page.size": page.size,
+ ...dto,
+ };
+ appendDotNotationQuery(params, "finReimbursementDto", dto);
+ return params;
+}
+
+function pickTravelField(obj, keys) {
+ if (!obj || typeof obj !== "object") return "";
+ for (const key of keys) {
+ const v = obj[key];
+ if (v != null && v !== "") return v;
+ }
+ return "";
+}
+
+/** 鍏煎 list/detail 澶氱宸梾瀛愬璞$粨鏋� */
+export function pickTravelFromRow(row) {
+ if (!row || typeof row !== "object") return {};
+ const nested =
+ (row.travel && typeof row.travel === "object" ? row.travel : null) ||
+ row.finReimbursementTravel ||
+ row.finReimbursementTravelDto ||
+ row.travelDto ||
+ row.travelVO ||
+ {};
+ const src =
+ nested && typeof nested === "object" && Object.keys(nested).length
+ ? nested
+ : row;
+ return {
+ startTime: pickTravelField(src, [
+ "startTime",
+ "travelStartTime",
+ "startDate",
+ "travelStartDate",
+ "departureTime",
+ ]),
+ endTime: pickTravelField(src, [
+ "endTime",
+ "travelEndTime",
+ "endDate",
+ "travelEndDate",
+ "returnTime",
+ ]),
+ travelDays: src.travelDays,
+ departureCity: pickTravelField(src, [
+ "departureCity",
+ "departurePlace",
+ "departure",
+ ]),
+ destinationCity: pickTravelField(src, [
+ "destinationCity",
+ "destination",
+ "destinationPlace",
+ ]),
+ hotelStandard: src.hotelStandard,
+ lodgingDays: src.lodgingDays ?? src.hotelDays,
+ mealAllowance: src.mealAllowance ?? src.livingSubsidy,
+ transportAllowance: src.transportAllowance ?? src.transportSubsidy,
+ lodgingLimit: src.lodgingLimit,
+ withinStandard: src.withinStandard,
+ standardTag: src.standardTag || "",
+ id: src.id,
+ reimbursementId: src.reimbursementId,
+ };
+}
+
+/** 鍒楄〃/璇︽儏鏃堕棿灞曠ず锛圛SO 鈫� YYYY-MM-DD HH:mm:ss锛� */
+export function formatReimbursementDateTime(val) {
+ if (val == null || val === "") return "";
+ const d = dayjs(val);
+ if (!d.isValid()) return String(val);
+ const raw = String(val);
+ const hasTime = raw.includes("T") || /:\d{2}/.test(raw);
+ return hasTime ? d.format("YYYY-MM-DD HH:mm:ss") : d.format("YYYY-MM-DD");
+}
+
+/** 鎺ュ彛琛� 鈫� 宸梾鎶ラ攢鍒楄〃琛岋紙鍏煎 useTravelReimburse 瀛楁锛� */
+export function mapTravelReimbursementRow(row) {
+ if (!row) return {};
+ const travel = pickTravelFromRow(row);
+ const details = Array.isArray(row.details) ? row.details : [];
+
+ const base = {
+ ...row,
+ id: row.id,
+ reimbursementId: row.id,
+ approvalInstanceId: row.approvalInstanceId,
+ reimburseNo: row.billNo || "",
+ applicantId: row.applicantId,
+ applicantNo: row.applicantCode || "",
+ applicantName: row.applicantName || "",
+ employeeNo: row.applicantCode || "",
+ employeeName: row.applicantName || "",
+ applicantDeptName: row.applicantDeptName || "",
+ reimburseReason: row.reason || "",
+ travelStartTime: formatReimbursementDateTime(travel.startTime),
+ travelEndTime: formatReimbursementDateTime(travel.endTime),
+ travelDays: travel.travelDays,
+ departurePlace: travel.departureCity || "",
+ destination: travel.destinationCity || "",
+ hotelStandard: travel.hotelStandard,
+ hotelDays: travel.lodgingDays,
+ livingSubsidy: travel.mealAllowance,
+ transportSubsidy: travel.transportAllowance,
+ lodgingLimit: travel.lodgingLimit,
+ needSpecialApproval: travel.withinStandard === "0" || travel.withinStandard === 0,
+ standardTag: travel.standardTag || "",
+ applyAmount: row.applyAmount,
+ payee: row.payeeName || "",
+ payeeAccount: row.payeeAccount || "",
+ payeeBank: row.payeeBank || "",
+ billStatus: row.billStatus,
+ approvalResult: mapBillStatusToApprovalResult(row.billStatus),
+ createTime: formatReimbursementDateTime(row.createTime),
+ expenseDetails: details.map((d) => ({
+ ...d,
+ expenseSubject: d.expenseCategory,
+ })),
+ travel:
+ row.travel && typeof row.travel === "object" && Object.keys(row.travel).length
+ ? row.travel
+ : travel,
+ details,
+ nodes: resolveRowApiNodes(row),
+ approvalFlowNodes: mapNodesToFormFlow(resolveRowApiNodes(row)),
+ tasks: row.tasks || [],
+ approvalFlowSummary: buildApprovalFlowSummaryForRow(row),
+ };
+ return base;
+}
+
+/** 鎺ュ彛琛� 鈫� 璐圭敤鎶ラ攢鍒楄〃琛岋紙鍏煎 useCostReimburse 瀛楁锛� */
+export function mapCostReimbursementRow(row) {
+ if (!row) return {};
+ const details = Array.isArray(row.details) ? row.details : [];
+ const apiNodes = resolveRowApiNodes(row);
+ const approvalFlowNodes = mapNodesToFormFlow(apiNodes);
+
+ return {
+ ...row,
+ id: row.id,
+ reimbursementId: row.id,
+ approvalInstanceId: row.approvalInstanceId,
+ reimburseNo: row.billNo || "",
+ applicantId: row.applicantId,
+ applicantNo: row.applicantCode || "",
+ applicantName: row.applicantName || "",
+ employeeNo: row.applicantCode || "",
+ employeeName: row.applicantName || "",
+ applicantDeptName: row.applicantDeptName || "",
+ reimburseReason: row.reason || "",
+ expenseCategory: row.expenseType || "",
+ applyAmount: row.applyAmount,
+ applyTime: formatReimbursementDateTime(row.createTime),
+ payee: row.payeeName || "",
+ payeeAccount: row.payeeAccount || "",
+ bankBranch: row.payeeBank || "",
+ billStatus: row.billStatus,
+ approvalResult: mapBillStatusToApprovalResult(row.billStatus),
+ createTime: formatReimbursementDateTime(row.createTime),
+ expenseDetails: details.map((d) => ({
+ ...d,
+ expenseSubject: d.expenseCategory,
+ })),
+ details,
+ nodes: apiNodes,
+ approvalFlowNodes,
+ tasks: row.tasks || [],
+ approvalFlowSummary: buildApprovalFlowSummaryForRow({
+ ...row,
+ nodes: apiNodes,
+ approvalFlowNodes,
+ }),
+ };
+}
+
+function toNumber(val) {
+ if (val == null || val === "") return undefined;
+ const n = Number(val);
+ return Number.isNaN(n) ? undefined : n;
+}
+
+function expenseSubjectToCategory(subject) {
+ const hit = EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === subject);
+ return hit?.label || subject || "";
+}
+
+function expenseCategoryToType(category) {
+ const hit = EXPENSE_CATEGORY_OPTIONS.find((x) => x.value === category);
+ return hit?.label || category || "";
+}
+
+/** 鍒楄〃/璇︽儏琛屼笂鐨勫鎵硅妭鐐癸紙listPage 甯镐笉杩斿洖锛岄渶璇︽儏琛ュ叏锛� */
+export function resolveRowApiNodes(row) {
+ if (!row || typeof row !== "object") return [];
+ const list =
+ row.nodes ||
+ row.flowNodes ||
+ row.approveNodes ||
+ row.finReimbursementNodes ||
+ row.nodeList ||
+ row.reimbursementNodeList ||
+ [];
+ return Array.isArray(list) ? list : [];
+}
+
+function sortFlowNodesByLevel(nodes = []) {
+ return [...(Array.isArray(nodes) ? nodes : [])].sort((a, b) => {
+ const la = Number(a?.levelNo ?? a?.nodeOrder ?? a?.sortOrder ?? 0);
+ const lb = Number(b?.levelNo ?? b?.nodeOrder ?? b?.sortOrder ?? 0);
+ return la - lb;
+ });
+}
+
+function formatApiNodeApproverLabel(node, index) {
+ if (!node || typeof node !== "object") return "";
+ const approvers = Array.isArray(node.approvers) ? node.approvers : [];
+ const names = approvers
+ .map((a) => (a?.approverName || "").trim())
+ .filter(Boolean);
+ if (names.length) return names.join("/");
+ return (node.approverName || "").trim() || `鑺傜偣${index + 1}`;
+}
+
+/** 鎺ュ彛 nodes 鈫� 椤甸潰瀹℃壒娴侊紙鍗曞鎵逛汉鑺傜偣锛� */
+export function mapNodesToFormFlow(nodes = []) {
+ return sortFlowNodesByLevel(nodes).map((n, i) => {
+ const approvers = Array.isArray(n.approvers) ? n.approvers : [];
+ const first = approvers[0] || null;
+ const names = approvers
+ .map((a) => (a?.approverName || "").trim())
+ .filter(Boolean);
+ return {
+ ...n,
+ nodeOrder: n.levelNo ?? n.nodeOrder ?? i + 1,
+ signMode: String(n.approveType || "").toUpperCase() === "OR" ? "or_sign" : "countersign",
+ approverId:
+ toNumber(first?.approverId ?? n.approverId) ??
+ first?.approverId ??
+ n.approverId ??
+ null,
+ approverName:
+ names.join("銆�") || first?.approverName || n.approverName || "",
+ nodeStatus: n.nodeStatus,
+ };
+ });
+}
+
+function formatTasksToFlowSummary(tasks = []) {
+ const list = sortFlowNodesByLevel(
+ (Array.isArray(tasks) ? tasks : []).map((t, i) => ({
+ levelNo: t.levelNo ?? t.taskLevel ?? i + 1,
+ approverName:
+ (t.approverName || t.operatorName || t.createUserName || "").trim() ||
+ "",
+ }))
+ );
+ const parts = list.map((t) => t.approverName).filter(Boolean);
+ return parts.length ? parts.join(" 鈫� ") : "";
+}
+
+function buildApprovalFlowSummaryForRow(row) {
+ const apiNodes = sortFlowNodesByLevel(resolveRowApiNodes(row));
+ let flowNodes =
+ row?.approvalFlowNodes?.length > 0
+ ? sortFlowNodesByLevel(row.approvalFlowNodes)
+ : mapNodesToFormFlow(apiNodes);
+
+ if (!flowNodes.length && apiNodes.length) {
+ const line = apiNodes
+ .map((n, i) => formatApiNodeApproverLabel(n, i))
+ .filter(Boolean)
+ .join(" 鈫� ");
+ if (line) return line;
+ }
+
+ if (!flowNodes.length) {
+ const fromTasks = formatTasksToFlowSummary(row?.tasks);
+ if (fromTasks) return fromTasks;
+ return "鈥�";
+ }
+
+ return flowNodes
+ .map((n, i) => {
+ const name = (n.approverName || "").trim() || `鑺傜偣${i + 1}`;
+ if (n.nodeStatus === "finish") return `${name}鉁揱;
+ if (n.nodeStatus === "error") return `${name}鉁梎;
+ if (n.nodeStatus === "process") return `${name}鈥;
+ return name;
+ })
+ .join(" 鈫� ");
+}
+
+/** 鍒楄〃銆屽鎵规祦绋嬨�嶅垪鏂囨 */
+export function formatApprovalFlowSummary(row) {
+ return buildApprovalFlowSummaryForRow(row);
+}
+
+/** listPage 甯镐笉甯﹀畬鏁� nodes锛屽垪琛ㄥ姞杞藉悗缁熶竴鎷夎鎯呰ˉ鍏ㄥ绾у鎵规祦绋� */
+export async function enrichReimbursementListRowsWithApprovalFlow(
+ rows,
+ reimbursementType
+) {
+ const list = Array.isArray(rows) ? rows : [];
+ if (!list.length) return list;
+
+ const needIds = list
+ .map((r) => resolveReimbursementDeleteId(r))
+ .filter((id) => id != null);
+
+ if (!needIds.length) return list;
+
+ const detailById = new Map();
+ await Promise.all(
+ needIds.map(async (id) => {
+ try {
+ const res = await getFinReimbursementDetail(id);
+ detailById.set(String(id), unwrapFinReimbursementDetail(res));
+ } catch {
+ /* 鍗曡澶辫触涓嶅奖鍝嶅垪琛� */
+ }
+ })
+ );
+
+ const mapRow =
+ reimbursementType === FIN_REIMBURSEMENT_TYPE.TRAVEL
+ ? mapTravelReimbursementRow
+ : mapCostReimbursementRow;
+
+ return list.map((row) => {
+ const id = resolveReimbursementDeleteId(row);
+ const detail = id != null ? detailById.get(String(id)) : null;
+ if (!detail) return row;
+ const merged = {
+ ...row,
+ ...detail,
+ id: row.id ?? detail.id,
+ reimbursementId: row.reimbursementId ?? row.id ?? detail.id,
+ reimbursementType: detail.reimbursementType ?? row.reimbursementType,
+ };
+ return mapRow(merged);
+ });
+}
+
+/** 琛ㄥ崟涓婄殑瀹℃壒娴侊紙鍏煎 approvalFlowNodes / nodes / flowNodes锛� */
+export function resolveFormApprovalFlowNodes(form) {
+ const list =
+ form?.approvalFlowNodes ?? form?.nodes ?? form?.flowNodes ?? [];
+ return Array.isArray(list) ? list : [];
+}
+
+/** 椤甸潰瀹℃壒鑺傜偣 鈫� 鎺ュ彛 nodes */
+export function mapApprovalFlowNodesToApi(nodes = [], templateId) {
+ const list = Array.isArray(nodes) ? nodes : [];
+ return list
+ .map((n, i) => {
+ let approvers = [];
+ if (Array.isArray(n.approvers) && n.approvers.length) {
+ approvers = n.approvers
+ .filter((a) => a?.approverId != null && a.approverId !== "")
+ .map((a, idx) => {
+ const item = {
+ approverId: toNumber(a.approverId) ?? a.approverId,
+ approverName: a.approverName || "",
+ sortNo: a.sortNo ?? idx + 1,
+ };
+ if (a.id != null) item.id = a.id;
+ if (a.nodeId != null) item.nodeId = a.nodeId;
+ if (a.templateId != null) item.templateId = a.templateId;
+ else if (templateId != null) item.templateId = templateId;
+ if (a.roleKey) item.roleKey = a.roleKey;
+ return item;
+ });
+ } else if (n.approverId != null && n.approverId !== "") {
+ const item = {
+ approverId: toNumber(n.approverId) ?? n.approverId,
+ approverName: n.approverName || "",
+ sortNo: 1,
+ };
+ if (n.roleKey) item.roleKey = n.roleKey;
+ approvers = [item];
+ }
+ if (!approvers.length) return null;
+
+ const node = {
+ levelNo: n.levelNo ?? n.nodeOrder ?? n.sortOrder ?? i + 1,
+ approveType: n.approveType || mapSignModeToApi(n.signMode),
+ approvers,
+ };
+ if (n.id != null) node.id = n.id;
+ if (n.templateId != null) node.templateId = n.templateId;
+ else if (templateId != null) node.templateId = templateId;
+ if (n.roleKey) node.roleKey = n.roleKey;
+ return node;
+ })
+ .filter(Boolean);
+}
+
+/** 淇濆瓨鍓嶆牎楠� nodes 宸查厤缃� */
+export function validateReimbursementApprovalNodes(dto) {
+ if (Array.isArray(dto?.nodes) && dto.nodes.length > 0) {
+ return { ok: true };
+ }
+ return { ok: false, message: "璇烽厤缃鎵规祦绋嬪苟閫夋嫨瀹℃壒浜�" };
+}
+
+function mapDetailsToApi(details = []) {
+ return (details || []).map((d, i) => {
+ const item = {
+ rowNo: d.rowNo ?? i + 1,
+ invoiceDate: d.invoiceDate || undefined,
+ expenseCategory: expenseSubjectToCategory(d.expenseSubject ?? d.expenseCategory),
+ amount: toNumber(d.amount),
+ description: d.description || "",
+ invoiceNo: d.invoiceNo || undefined,
+ invoiceType: d.invoiceType || undefined,
+ invoiceAmount: toNumber(d.invoiceAmount),
+ taxRate: toNumber(d.taxRate),
+ taxAmount: toNumber(d.taxAmount),
+ remark: d.remark || undefined,
+ };
+ if (d.id != null && !String(d.id).startsWith("ed_")) {
+ item.id = toNumber(d.id) ?? d.id;
+ }
+ if (d.reimbursementId != null) item.reimbursementId = toNumber(d.reimbursementId);
+ return item;
+ });
+}
+
+function sumDetailAmount(details = []) {
+ const sum = (details || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
+ return Math.round(sum * 100) / 100;
+}
+
+/** 琛ㄥ崟闄勪欢鍒楄〃锛堝吋瀹瑰绉嶅瓧娈靛悕锛� */
+export function resolveFormAttachmentList(form) {
+ const list =
+ form?.attachmentList ??
+ form?.storageBlobDTOs ??
+ form?.storageBlobVOList ??
+ form?.invoiceAttachments ??
+ [];
+ return Array.isArray(list) ? list : [];
+}
+
+/** 椤甸潰闄勪欢 鈫� 淇濆瓨 DTO锛坰torageBlobVOList / storageBlobDTOs锛� */
+export function mapFormAttachmentsToApi(list = [], reimbursementId) {
+ const rid =
+ reimbursementId != null
+ ? toNumber(reimbursementId) ?? reimbursementId
+ : undefined;
+
+ return (list || [])
+ .map((item, i) => {
+ if (!item) return null;
+ const url =
+ item.url ||
+ item.fileUrl ||
+ item.downloadUrl ||
+ item.downloadURL ||
+ item.previewUrl ||
+ item.previewURL ||
+ item.link ||
+ "";
+ const name =
+ item.fileName ||
+ item.originalFilename ||
+ item.originalFileName ||
+ item.blobName ||
+ item.name ||
+ `闄勪欢${i + 1}`;
+
+ const idRaw = item.id ?? item.blobId;
+ const isTempId =
+ idRaw != null &&
+ /^(inv_|att_|ed_|local_)/.test(String(idRaw));
+
+ if (!url && (idRaw == null || isTempId)) return null;
+
+ const blob = {
+ fileName: name,
+ originalFilename: name,
+ fileUrl: url || undefined,
+ url: url || undefined,
+ };
+
+ if (idRaw != null && !isTempId) {
+ const n = toNumber(idRaw);
+ blob.id = n != null ? n : idRaw;
+ blob.blobId = blob.id;
+ }
+ if (rid != null) blob.reimbursementId = rid;
+ return blob;
+ })
+ .filter(Boolean);
+}
+
+function applyStorageBlobsToSaveDto(dto, form) {
+ const blobs = mapFormAttachmentsToApi(
+ resolveFormAttachmentList(form),
+ dto?.id ?? form?.reimbursementId ?? form?.id
+ );
+ if (blobs.length) {
+ dto.storageBlobVOList = blobs;
+ dto.storageBlobDTOs = blobs;
+ }
+ return dto;
+}
+
+/** 淇敼鏃惰ˉ榻愪富琛ㄤ笌瀛愯〃鍏宠仈 ID */
+function applyReimbursementRelations(dto) {
+ const rid = dto?.id;
+ if (rid == null) return dto;
+ if (dto.travel && typeof dto.travel === "object") {
+ dto.travel.reimbursementId = rid;
+ }
+ if (Array.isArray(dto.details)) {
+ dto.details.forEach((d) => {
+ d.reimbursementId = rid;
+ });
+ }
+ const blobLists = [dto.storageBlobVOList, dto.storageBlobDTOs].filter(Array.isArray);
+ blobLists.forEach((list) => {
+ list.forEach((b) => {
+ b.reimbursementId = rid;
+ });
+ });
+ return dto;
+}
+
+function resolveReimbursementId(form) {
+ const rawId = form?.reimbursementId ?? form?.id;
+ if (rawId == null || rawId === "" || String(rawId).startsWith("local_")) {
+ return undefined;
+ }
+ return toNumber(rawId) ?? rawId;
+}
+
+/** 宸梾琛ㄥ崟 鈫� FinReimbursementDto */
+export function buildTravelReimbursementSaveDto(form, { computeTravelDays } = {}) {
+ const details = mapDetailsToApi(form.expenseDetails);
+ const detailTotal = sumDetailAmount(form.expenseDetails);
+ const applyAmount = toNumber(form.applyAmount) ?? detailTotal;
+ const travelDays =
+ form.travelDays != null
+ ? toNumber(form.travelDays)
+ : computeTravelDays?.(form.travelStartTime, form.travelEndTime);
+
+ const dto = {
+ reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL,
+ expenseType: "宸梾璐�",
+ applicantId: toNumber(form.applicantId),
+ applicantCode: form.employeeNo || form.applicantNo || "",
+ applicantName: form.employeeName || form.applicantName || "",
+ applicantDeptId: toNumber(form.applicantDeptId),
+ applicantDeptName: form.applicantDeptName || form.deptName || "",
+ reason: (form.reimburseReason || "").trim(),
+ applyAmount,
+ detailTotalAmount: detailTotal,
+ payeeName: form.payee || "",
+ payeeAccount: form.payeeAccount || undefined,
+ payeeBank: form.payeeBank || undefined,
+ billStatus: "IN_APPROVAL",
+ deptId: toNumber(form.deptId),
+ travel: {
+ startTime: form.travelStartTime || undefined,
+ endTime: form.travelEndTime || undefined,
+ travelDays,
+ departureCity: form.departurePlace || "",
+ destinationCity: form.destination || "",
+ hotelStandard: toNumber(form.hotelStandard),
+ lodgingDays: toNumber(form.hotelDays),
+ mealAllowance: toNumber(form.livingSubsidy),
+ transportAllowance: toNumber(form.transportSubsidy),
+ lodgingLimit: toNumber(form.lodgingLimit),
+ standardTag: form.standardTag || (form.needSpecialApproval ? "瓒呮爣鐗规壒" : "鍦ㄦ爣鍑嗚寖鍥村唴"),
+ withinStandard: form.needSpecialApproval ? "0" : "1",
+ },
+ details,
+ nodes: mapApprovalFlowNodesToApi(
+ resolveFormApprovalFlowNodes(form),
+ form.templateId
+ ),
+ };
+
+ const id = resolveReimbursementId(form);
+ if (id != null) dto.id = id;
+ if (form.billNo || form.reimburseNo) {
+ dto.billNo = form.billNo || form.reimburseNo;
+ }
+ if (form.approvalInstanceId != null) {
+ dto.approvalInstanceId = toNumber(form.approvalInstanceId);
+ }
+ if (form.approveProcessId != null) {
+ dto.approveProcessId = toNumber(form.approveProcessId);
+ }
+ if (form.travel?.id != null) dto.travel.id = toNumber(form.travel.id);
+
+ applyStorageBlobsToSaveDto(dto, form);
+ return applyReimbursementRelations(dto);
+}
+
+/** 璐圭敤琛ㄥ崟 鈫� FinReimbursementDto */
+export function buildCostReimbursementSaveDto(form) {
+ const details = mapDetailsToApi(form.expenseDetails);
+ const detailTotal = sumDetailAmount(form.expenseDetails);
+ const applyAmount = toNumber(form.applyAmount) ?? detailTotal;
+
+ const dto = {
+ reimbursementType: FIN_REIMBURSEMENT_TYPE.COST,
+ expenseType: expenseCategoryToType(form.expenseCategory),
+ applicantId: toNumber(form.applicantId),
+ applicantCode: form.employeeNo || form.applicantNo || "",
+ applicantName: form.employeeName || form.applicantName || "",
+ applicantDeptId: toNumber(form.applicantDeptId),
+ applicantDeptName: form.applicantDeptName || form.deptName || "",
+ reason: (form.reimburseReason || "").trim(),
+ applyAmount,
+ detailTotalAmount: detailTotal,
+ payeeName: form.payee || "",
+ payeeAccount: form.payeeAccount || "",
+ payeeBank: form.bankBranch || form.payeeBank || "",
+ billStatus: "IN_APPROVAL",
+ deptId: toNumber(form.deptId),
+ details,
+ nodes: mapApprovalFlowNodesToApi(
+ resolveFormApprovalFlowNodes(form),
+ form.templateId
+ ),
+ };
+
+ const id = resolveReimbursementId(form);
+ if (id != null) dto.id = id;
+ if (form.billNo || form.reimburseNo) {
+ dto.billNo = form.billNo || form.reimburseNo;
+ }
+ if (form.approvalInstanceId != null) {
+ dto.approvalInstanceId = toNumber(form.approvalInstanceId);
+ }
+ if (form.approveProcessId != null) {
+ dto.approveProcessId = toNumber(form.approveProcessId);
+ }
+
+ applyStorageBlobsToSaveDto(dto, form);
+ return applyReimbursementRelations(dto);
+}
+
+/** 鍒楄〃琛屼富閿紙鍒犻櫎/淇敼鐢� fin_reimbursement.id锛� */
+export function resolveReimbursementDeleteId(row) {
+ const raw = row?.reimbursementId ?? row?.id;
+ if (raw == null || raw === "" || String(raw).startsWith("local_")) {
+ return undefined;
+ }
+ const n = toNumber(raw);
+ return n != null ? n : raw;
+}
+
+/** 鏄惁鍏佽鍒犻櫎锛堝鎵逛腑銆佸凡閫氳繃銆佸凡浠樻涓嶅彲鍒狅級 */
+export function canDeleteReimbursementRow(row) {
+ const key = mapBillStatusToApprovalResult(
+ row?.billStatus ?? row?.approvalResult ?? row?.status
+ );
+ return key !== "pending" && key !== "approved" && key !== "paid";
+}
+
+/** 鏄惁鍏佽缂栬緫锛堜笌鍒犻櫎瑙勫垯涓�鑷达級 */
+export function canEditReimbursementRow(row) {
+ return canDeleteReimbursementRow(row);
+}
+
+/** 淇敼鍦烘櫙蹇呴』甯︿富閿� ID */
+export function validateReimbursementPersistDto(dto, isEdit) {
+ if (!isEdit) return { ok: true };
+ if (dto?.id != null && dto.id !== "") return { ok: true };
+ return { ok: false, message: "鏃犳硶淇敼锛氱己灏戞姤閿�鍗� ID" };
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js b/src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js
new file mode 100644
index 0000000..664d646
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/shared/reimburseApproveBridge.js
@@ -0,0 +1,124 @@
+import { getFinReimbursementDetail } from "@/api/officeProcessAutomation/finReimbursement.js";
+import { matchBusinessTypeValue } from "../../ApproveManage/approve-list/approveListConstants.js";
+import {
+ APPROVAL_MODULE_KEYS,
+ getApprovalModuleConfig,
+} from "../../ApproveManage/approve-shared/approvalModuleRegistry.js";
+import {
+ getModuleKeyByReimbursementType,
+ mapFinReimbursementDetailRow,
+ resolveReimbursementType,
+ unwrapFinReimbursementDetail,
+} from "./finReimbursementMappers.js";
+
+export const REIMBURSE_EDIT_FROM_APPROVE_KEY = "oa_reimburse_edit_from_approve";
+
+const REIMBURSE_MODULE_KEYS = [
+ APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE,
+ APPROVAL_MODULE_KEYS.COST_REIMBURSE,
+];
+
+/** 瀹℃壒瀹炰緥鏄惁宸梾/璐圭敤鎶ラ攢 */
+export function inferReimburseModuleKeyFromInstance(row) {
+ if (!row) return "";
+ for (const moduleKey of REIMBURSE_MODULE_KEYS) {
+ const cfg = getApprovalModuleConfig(moduleKey);
+ if (!cfg) continue;
+ if (
+ cfg.businessType != null &&
+ cfg.businessType !== "" &&
+ matchBusinessTypeValue(row.businessType, cfg.businessType)
+ ) {
+ return moduleKey;
+ }
+ if (matchBusinessTypeValue(row.businessType, cfg.approvalType)) {
+ return moduleKey;
+ }
+ const text = `${row.templateName || ""}${row.title || ""}${row.businessName || ""}`;
+ if ((cfg.typeLabels || []).some((l) => l && text.includes(l))) {
+ return moduleKey;
+ }
+ }
+ return "";
+}
+
+export function isReimburseApprovalInstance(row) {
+ return Boolean(inferReimburseModuleKeyFromInstance(row));
+}
+
+/** 瀹℃壒瀹炰緥鍏宠仈鐨� fin_reimbursement.id */
+export function resolveFinReimbursementIdFromInstance(row) {
+ const raw = row?.businessId ?? row?.formPayload?.reimbursementId;
+ if (raw == null || raw === "") return undefined;
+ const n = Number(raw);
+ return Number.isNaN(n) ? raw : n;
+}
+
+/** 鎷夊彇鎶ラ攢璇︽儏骞舵槧灏勪负宸梾/璐圭敤椤甸潰琛岋紙浠ユ帴鍙� reimbursementType 涓哄噯锛� */
+export async function loadReimburseDetailForInstance(instanceRow, moduleKey) {
+ const mk = moduleKey || inferReimburseModuleKeyFromInstance(instanceRow);
+ const id = resolveFinReimbursementIdFromInstance(instanceRow);
+ if (id == null) {
+ throw new Error("missing reimbursement id");
+ }
+ const res = await getFinReimbursementDetail(id);
+ const raw = unwrapFinReimbursementDetail(res);
+ const reimburseRow = mapFinReimbursementDetailRow(raw, mk);
+ const reimbursementType = resolveReimbursementType(raw, mk);
+ const resolvedMk =
+ getModuleKeyByReimbursementType(reimbursementType) || mk;
+ return {
+ reimburseRow,
+ instanceRow,
+ moduleKey: resolvedMk,
+ reimbursementType,
+ };
+}
+
+export function stashReimburseEditFromApprove(moduleKey, reimbursementId) {
+ sessionStorage.setItem(
+ REIMBURSE_EDIT_FROM_APPROVE_KEY,
+ JSON.stringify({ moduleKey, reimbursementId })
+ );
+}
+
+export function consumeReimburseEditFromApprove() {
+ const raw = sessionStorage.getItem(REIMBURSE_EDIT_FROM_APPROVE_KEY);
+ if (!raw) return null;
+ sessionStorage.removeItem(REIMBURSE_EDIT_FROM_APPROVE_KEY);
+ try {
+ return JSON.parse(raw);
+ } catch {
+ return null;
+ }
+}
+
+/** 浠庡凡娉ㄥ唽璺敱瑙f瀽宸梾/璐圭敤鎶ラ攢鑿滃崟 path锛堥伩鍏嶅啓姝� path 瀵艰嚧 404锛� */
+export function resolveReimburseManageRoutePath(router, moduleKey) {
+ if (!router?.getRoutes) return "";
+ const needle =
+ moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE
+ ? "travel-reimburse"
+ : moduleKey === APPROVAL_MODULE_KEYS.COST_REIMBURSE
+ ? "cost-reimburse"
+ : "";
+ if (!needle) return "";
+ const labelHint =
+ moduleKey === APPROVAL_MODULE_KEYS.TRAVEL_REIMBURSE ? "宸梾" : "璐圭敤";
+ const hit = router.getRoutes().find((r) => {
+ const path = r.path || "";
+ if (path.includes(needle)) return true;
+ const title = r.meta?.title || "";
+ return title.includes(labelHint) && title.includes("鎶ラ攢");
+ });
+ return hit?.path || "";
+}
+
+export async function navigateToReimburseManageForEdit(router, moduleKey, reimbursementId) {
+ stashReimburseEditFromApprove(moduleKey, reimbursementId);
+ const path = resolveReimburseManageRoutePath(router, moduleKey);
+ if (!path) {
+ throw new Error("route not found");
+ }
+ await router.push(path);
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue
new file mode 100644
index 0000000..03a5fa3
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/ApprovalFlowProgress.vue
@@ -0,0 +1,49 @@
+<!-- 宸梾鎶ラ攢锛氬鎵规祦绋嬭繘搴﹀睍绀� -->
+<template>
+ <el-steps :active="activeStep" finish-status="success" align-center>
+ <el-step
+ v-for="(node, index) in sortedNodes"
+ :key="index"
+ :title="`鑺傜偣 ${index + 1}`"
+ :description="stepDescription(node)"
+ :status="stepStatus(node, index)"
+ />
+ </el-steps>
+</template>
+
+<script setup>
+import { computed } from "vue";
+
+const props = defineProps({
+ nodes: { type: Array, default: () => [] },
+ currentIndex: { type: Number, default: 0 },
+});
+
+const sortedNodes = computed(() => {
+ const list = props.nodes || [];
+ return [...list].sort((a, b) => (a.sortOrder ?? a.nodeOrder ?? 0) - (b.sortOrder ?? b.nodeOrder ?? 0));
+});
+
+const activeStep = computed(() => {
+ const list = sortedNodes.value;
+ if (!list.length) return 0;
+ const finished = list.filter((n) => n.nodeStatus === "finish").length;
+ const hasError = list.some((n) => n.nodeStatus === "error");
+ if (hasError) return Math.max(0, props.currentIndex);
+ return finished;
+});
+
+function stepDescription(node) {
+ const name = (node.approverName || "").trim() || "鏈寚瀹�";
+ const opinion = (node.approveOpinion || "").trim();
+ if (opinion) return `${name}锛�${opinion}`;
+ return name;
+}
+
+function stepStatus(node, index) {
+ if (node.nodeStatus === "error") return "error";
+ if (node.nodeStatus === "finish") return "success";
+ if (node.nodeStatus === "process" || index === props.currentIndex) return "process";
+ return "wait";
+}
+</script>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
new file mode 100644
index 0000000..2c1d8a4
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/components/DetailPanel.vue
@@ -0,0 +1,82 @@
+<!-- 宸梾鎶ラ攢锛氳鎯呭彧璇婚潰鏉� -->
+<template>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鎶ラ攢鍗曞彿">{{ row.reimburseNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐘舵��">
+ <el-tag :type="statusTagType(row.approvalResult)" size="small">{{ statusLabel(row.approvalResult) }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍛樺伐缂栧彿">{{ row.employeeNo || row.applicantNo || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍛樺伐濮撳悕">{{ row.employeeName || row.applicantName || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ラ攢鍘熷洜" :span="2">{{ row.reimburseReason || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍑哄樊寮�濮�">{{ row.travelStartTime || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍑哄樊缁撴潫">{{ row.travelEndTime || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鍑哄樊鍦�">{{ row.departurePlace || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐩殑鍦�">{{ row.destination || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="閰掑簵鏍囧噯">{{ row.hotelStandard != null ? `${row.hotelStandard} 鍏�/鏅歚 : "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="浣忓澶╂暟">{{ row.hotelDays ?? "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐢熸椿琛ヨ创">{{ row.livingSubsidy != null ? `${row.livingSubsidy} 鍏僠 : "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐢宠閲戦">{{ row.applyAmount != null ? `${row.applyAmount} 鍏僠 : "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鏀舵浜�">{{ row.payee || "鈥�" }}</el-descriptions-item>
+ <el-descriptions-item label="鐗规壒">
+ <el-tag :type="row.needSpecialApproval ? 'danger' : 'info'" size="small">
+ {{ row.needSpecialApproval ? "瓒呮敮闇�鐗规壒" : "鏍囧噯鍐�" }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item v-if="row.rejectReason" label="椹冲洖鍘熷洜" :span="2">
+ <span class="reject-text">{{ row.rejectReason }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿" :span="2">{{ row.createTime || "鈥�" }}</el-descriptions-item>
+ </el-descriptions>
+
+ <el-divider content-position="left">鎶ラ攢鏄庣粏</el-divider>
+ <el-table v-if="row.expenseDetails?.length" :data="row.expenseDetails" border size="small">
+ <el-table-column type="index" label="搴忓彿" width="55" align="center" />
+ <el-table-column prop="invoiceDate" label="鍙戠エ鏃ユ湡" width="120" />
+ <el-table-column label="璐圭敤绉戠洰" width="100">
+ <template #default="{ row: d }">{{ expenseSubjectLabel(d.expenseSubject) }}</template>
+ </el-table-column>
+ <el-table-column prop="amount" label="閲戦" width="100" />
+ <el-table-column prop="description" label="鎻忚堪" min-width="140" show-overflow-tooltip />
+ </el-table>
+ <el-empty v-else description="鏆傛棤鏄庣粏" :image-size="48" />
+
+ <el-divider content-position="left">鍙戠エ闄勪欢</el-divider>
+ <template v-if="attachmentFiles.length">
+ <el-tag v-for="(f, i) in attachmentFiles" :key="i" class="file-tag" type="info" @click="openFile(f)">
+ {{ f.name }}
+ </el-tag>
+ </template>
+ <el-empty v-else description="鏆傛棤闄勪欢" :image-size="48" />
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { expenseSubjectLabel, statusLabel, statusTagType } from "../travelReimburseUtils.js";
+
+const props = defineProps({
+ row: { type: Object, default: () => ({}) },
+});
+
+const attachmentFiles = computed(() => {
+ const list =
+ props.row?.attachmentList ||
+ props.row?.storageBlobVOList ||
+ props.row?.invoiceAttachments;
+ return Array.isArray(list) ? list : [];
+});
+
+function openFile(f) {
+ const url = f?.url || f?.downloadURL || f?.previewURL;
+ if (url) window.open(url, "_blank");
+}
+</script>
+
+<style scoped>
+.reject-text {
+ color: var(--el-color-danger);
+}
+.file-tag {
+ margin: 0 8px 8px 0;
+ cursor: pointer;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
new file mode 100644
index 0000000..17737e3
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/index.vue
@@ -0,0 +1,614 @@
+<!--OA妯″潡锛氬樊鏃呮姤閿�锛堝垪琛� /finReimbursement/listPage锛宺eimbursementType=1锛�-->
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">鐢宠浜猴細</span>
+ <el-input
+ v-model="searchForm.applicantKeyword"
+ style="width: 220px"
+ placeholder="濮撳悕鎴栫紪鍙�"
+ clearable
+ :prefix-icon="Search"
+ @keyup.enter="handleQuery"
+ />
+ <el-button type="primary" style="margin-left: 10px" @click="handleQuery">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </div>
+ <div class="search_actions">
+ <el-button type="success" plain @click="handleImportClick">瀵煎叆</el-button>
+ <el-button type="warning" plain @click="handleExport">瀵煎嚭</el-button>
+ <el-button type="primary" @click="openFormDialog('add')">鏂板宸梾鎶ラ攢</el-button>
+ </div>
+ </div>
+
+ <input ref="importInputRef" type="file" accept="application/json,.json" class="sr-only-input" @change="onImportFile" />
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="false"
+ :tableLoading="tableLoading"
+ :total="page.total"
+ @pagination="pagination"
+ />
+ </div>
+
+ <!-- 鏂板 / 缂栬緫 -->
+ <el-dialog
+ v-model="formDialog.visible"
+ :title="formDialog.title"
+ width="1120px"
+ append-to-body
+ destroy-on-close
+ class="travel-reimburse-form-dialog"
+ @closed="onFormClosed"
+ >
+ <el-alert
+ v-if="budgetHint.visible"
+ :title="budgetHint.title"
+ :type="budgetHint.type"
+ :description="budgetHint.description"
+ show-icon
+ :closable="false"
+ class="mb16"
+ />
+ <el-alert v-if="overBudgetWarnings.length" type="warning" show-icon :closable="false" class="mb16">
+ <template #title>宸梾鏍囧噯瓒呮敮鎻愰啋锛堥渶鐗规壒锛�</template>
+ <ul class="warn-list">
+ <li v-for="(w, i) in overBudgetWarnings" :key="i">{{ w }}</li>
+ </ul>
+ </el-alert>
+
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="formRules"
+ label-width="120px"
+ class="travel-reimburse-form"
+ :disabled="formDialog.readonly"
+ >
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">鍩烘湰淇℃伅</span></template>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐缂栧彿">
+ <el-input v-model="form.employeeNo" readonly placeholder="閫夋嫨鍛樺伐鍚庤嚜鍔ㄥ甫鍑�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐濮撳悕" prop="applicantId">
+ <el-select
+ v-model="form.applicantId"
+ filterable
+ remote
+ clearable
+ reserve-keyword
+ placeholder="璇烽�夋嫨鎴栨悳绱㈠憳宸�"
+ style="width: 100%"
+ :remote-method="remoteSearchApplicantForm"
+ :loading="applicantFormSearchLoading"
+ @change="onApplicantChange"
+ >
+ <el-option
+ v-for="u in applicantFormOptions"
+ :key="u.userId"
+ :label="userSelectLabel(u)"
+ :value="u.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="鎶ラ攢鍘熷洜" prop="reimburseReason">
+ <el-input
+ v-model="form.reimburseReason"
+ type="textarea"
+ :rows="3"
+ placeholder="璇峰~鍐欏嚭宸強鎶ラ攢鍘熷洜"
+ maxlength="2000"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍑哄樊寮�濮�" prop="travelStartTime">
+ <el-date-picker
+ v-model="form.travelStartTime"
+ type="datetime"
+ placeholder="寮�濮嬫椂闂�"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ @change="onTravelRangeChange"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍑哄樊缁撴潫" prop="travelEndTime">
+ <el-date-picker
+ v-model="form.travelEndTime"
+ type="datetime"
+ placeholder="缁撴潫鏃堕棿"
+ format="YYYY-MM-DD HH:mm:ss"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ style="width: 100%"
+ @change="onTravelRangeChange"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鍑哄樊澶╂暟">
+ <el-input :model-value="travelDaysDisplay" readonly>
+ <template #append>澶�</template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍑哄樊鍦�" prop="departurePlace">
+ <el-input v-model="form.departurePlace" placeholder="鍑哄彂鍩庡競" @blur="recalcTravelStandards" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鐩殑鍦�" prop="destination">
+ <el-input v-model="form.destination" placeholder="鐩殑鍩庡競" @blur="recalcTravelStandards" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header>
+ <div class="card-header-row">
+ <span class="card-header-title">宸梾鏍囧噯</span>
+ <el-text type="info" size="small">{{ travelTierLabel }} 路 鐢熸椿琛ヨ创寤鸿 {{ suggestedLivingSubsidy }} 鍏�</el-text>
+ </div>
+ </template>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="閰掑簵鏍囧噯">
+ <el-input-number
+ v-model="form.hotelStandard"
+ :min="0"
+ :precision="2"
+ controls-position="right"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浣忓澶╂暟">
+ <el-input-number
+ v-model="form.hotelDays"
+ :min="0"
+ :max="365"
+ :precision="0"
+ controls-position="right"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鐢熸椿琛ヨ创">
+ <el-input-number
+ v-model="form.livingSubsidy"
+ :min="0"
+ :precision="2"
+ controls-position="right"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="浜ら�氳ˉ璐�">
+ <el-input :model-value="String(suggestedTransportSubsidy)" readonly><template #append>鍏�</template></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="浣忓闄愰">
+ <el-input :model-value="String(suggestedHotelLimit)" readonly><template #append>鍏�</template></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鐗规壒鏍囪">
+ <el-tag :type="form.needSpecialApproval ? 'danger' : 'success'" effect="plain">
+ {{ form.needSpecialApproval ? "瓒呮敮闇�鐗规壒" : "鍦ㄦ爣鍑嗚寖鍥村唴" }}
+ </el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">閲戦涓庢敹娆�</span></template>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐢宠閲戦" prop="applyAmount">
+ <div class="amount-row">
+ <el-input-number v-model="form.applyAmount" :min="0" :precision="2" controls-position="right" class="amount-input" />
+ <el-button v-if="!formDialog.readonly" type="primary" link @click="syncApplyAmountFromDetails">
+ 鎸夋槑缁嗘眹鎬� {{ detailTotalAmount }} 鍏�
+ </el-button>
+ </div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏀舵浜�" prop="payee">
+ <el-input v-model="form.payee" placeholder="璇疯緭鍏ユ敹娆句汉" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header>
+ <div class="card-header-row">
+ <span class="card-header-title">鎶ラ攢鏄庣粏</span>
+ <el-button v-if="!formDialog.readonly" type="primary" plain size="small" @click="addExpenseDetail">鏂板鏄庣粏</el-button>
+ </div>
+ </template>
+
+ <el-table :data="form.expenseDetails" border size="small" class="detail-table">
+ <el-table-column type="index" label="搴忓彿" width="55" align="center" />
+ <el-table-column label="鍙戠エ鏃ユ湡" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-if="!formDialog.readonly"
+ v-model="row.invoiceDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ size="small"
+ style="width: 100%"
+ />
+ <span v-else>{{ row.invoiceDate || "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="璐圭敤绉戠洰" width="130">
+ <template #default="{ row }">
+ <el-select
+ v-if="!formDialog.readonly"
+ v-model="row.expenseSubject"
+ size="small"
+ style="width: 100%"
+ @change="recalcTravelStandards"
+ >
+ <el-option
+ v-for="opt in EXPENSE_SUBJECT_OPTIONS"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ <span v-else>{{ expenseSubjectLabel(row.expenseSubject) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="閲戦" width="120">
+ <template #default="{ row }">
+ <el-input-number
+ v-if="!formDialog.readonly"
+ v-model="row.amount"
+ :min="0"
+ :precision="2"
+ size="small"
+ controls-position="right"
+ style="width: 100%"
+ @change="onDetailAmountChange"
+ />
+ <span v-else>{{ row.amount ?? "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎻忚堪" min-width="140">
+ <template #default="{ row }">
+ <el-input v-if="!formDialog.readonly" v-model="row.description" size="small" placeholder="璇存槑" />
+ <span v-else>{{ row.description || "鈥�" }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column v-if="!formDialog.readonly" label="鎿嶄綔" width="70" align="center">
+ <template #default="{ $index }">
+ <el-button type="danger" link size="small" @click="removeExpenseDetail($index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">闄勪欢锛堝彂绁級</span></template>
+ <el-form-item label-width="0" class="attachment-form-item">
+ <div class="upload-block">
+ <FileUpload v-model:file-list="form.attachmentList" :limit="20" button-text="鐐瑰嚮閫夋嫨鏂囦欢" />
+ </div>
+ </el-form-item>
+ </el-card>
+
+ <el-card class="form-section" shadow="never">
+ <template #header><span class="card-header-title">瀹℃壒娴佺▼</span></template>
+ <el-form-item prop="approvalFlowNodes" label-width="0">
+ <ApprovalFlowEditor
+ v-if="!formDialog.readonly"
+ v-model="form.approvalFlowNodes"
+ :user-options="flowUserOptions"
+ @update:model-value="onApprovalFlowChange"
+ />
+ <ApprovalFlowProgress v-else :nodes="form.approvalFlowNodes" :current-index="form.currentNodeIndex" />
+ <p v-if="!formDialog.readonly" class="flow-tip">鑷冲皯淇濈暀涓�涓妭鐐癸紱瀹℃牳涓�佸凡閫氳繃鐨勫崟鎹笉鍙紪杈戙��</p>
+ </el-form-item>
+ </el-card>
+ </el-form>
+ <template #footer>
+ <el-button
+ v-if="!formDialog.readonly"
+ type="primary"
+ :loading="submitSaving"
+ @click="submitForm"
+ >
+ 鎻� 浜�
+ </el-button>
+ <el-button @click="formDialog.visible = false">{{ formDialog.readonly ? "鍏� 闂�" : "鍙� 娑�" }}</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 璇︽儏 -->
+ <el-dialog v-model="detailDialog.visible" title="宸梾鎶ラ攢璇︽儏" width="900px" append-to-body destroy-on-close>
+ <div v-loading="detailLoading">
+ <DetailPanel :row="detailRow" />
+ <ApprovalFlowProgress
+ class="mt16"
+ :nodes="detailRow.approvalFlowProgressNodes ?? detailRow.approvalFlowNodes"
+ :current-index="detailRow.currentNodeIndex ?? 0"
+ />
+ <el-divider content-position="left">瀹℃壒璁板綍锛堝叏娴佺▼鐣欑棔锛�</el-divider>
+ <el-timeline v-if="detailRow.approvalRecords?.length">
+ <el-timeline-item
+ v-for="(rec, i) in detailRow.approvalRecords"
+ :key="i"
+ :type="rec.result === 'approved' ? 'success' : rec.result === 'rejected' ? 'danger' : 'primary'"
+ :timestamp="rec.time"
+ >
+ {{ rec.operatorName }} 鈥� {{ approvalActionLabel(rec.result) }}锛歿{ rec.opinion || "鏃犳剰瑙�" }}
+ </el-timeline-item>
+ </el-timeline>
+ <el-empty v-else description="鏆傛棤瀹℃壒璁板綍" :image-size="60" />
+ </div>
+ <template #footer>
+ <el-button type="primary" @click="detailDialog.visible = false">鍏� 闂�</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 瀹℃壒 -->
+ <el-dialog
+ v-model="approveDialog.visible"
+ title="宸梾鎶ラ攢瀹℃壒"
+ width="1000px"
+ append-to-body
+ destroy-on-close
+ @closed="approveOpinion = ''"
+ >
+ <DetailPanel :row="approveDialog.row" />
+ <el-divider content-position="left">娴佺▼杩涘害</el-divider>
+ <ApprovalFlowProgress
+ :nodes="approveDialog.row?.approvalFlowProgressNodes ?? approveDialog.row?.approvalFlowNodes"
+ :current-index="approveDialog.row?.currentNodeIndex ?? 0"
+ />
+ <el-form label-width="100px" class="mt16">
+ <el-form-item label="瀹℃壒鎰忚">
+ <el-input
+ v-model="approveOpinion"
+ type="textarea"
+ :rows="3"
+ maxlength="500"
+ show-word-limit
+ placeholder="閫氳繃鍙暀绌猴紱椹冲洖璇峰~鍐欏師鍥�"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="success" @click="submitApprove('approved')">閫� 杩�</el-button>
+ <el-button type="danger" @click="submitApprove('rejected')">椹� 鍥�</el-button>
+ <el-button @click="approveDialog.visible = false">鍙� 娑�</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import ApprovalFlowEditor from "@/views/officeProcessAutomation/AttendManage/overtime-apply/components/ApprovalFlowEditor.vue";
+import ApprovalFlowProgress from "./components/ApprovalFlowProgress.vue";
+import DetailPanel from "./components/DetailPanel.vue";
+import { useTravelReimburse } from "./useTravelReimburse.js";
+
+const tr = useTravelReimburse();
+const {
+ Search,
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseSubjectLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ importInputRef,
+ formRef,
+ form,
+ formDialog,
+ formRules,
+ detailDialog,
+ detailLoading,
+ detailRow,
+ approveDialog,
+ approveOpinion,
+ applicantFormSearchLoading,
+ applicantFormOptions,
+ flowUserOptions,
+ travelDaysDisplay,
+ travelTierLabel,
+ suggestedLivingSubsidy,
+ suggestedTransportSubsidy,
+ suggestedHotelLimit,
+ detailTotalAmount,
+ overBudgetWarnings,
+ budgetHint,
+ handleQuery,
+ resetSearch,
+ pagination,
+ remoteSearchApplicantForm,
+ userSelectLabel,
+ onApplicantChange,
+ recalcTravelStandards,
+ onTravelRangeChange,
+ onDetailAmountChange,
+ onApprovalFlowChange,
+ addExpenseDetail,
+ removeExpenseDetail,
+ syncApplyAmountFromDetails,
+ openFormDialog,
+ onFormClosed,
+ submitForm,
+ submitSaving,
+ openDetail,
+ approvalActionLabel,
+ submitApprove,
+ handleExport,
+ handleImportClick,
+ onImportFile,
+} = tr;
+</script>
+
+<style scoped>
+.mb20 {
+ margin-bottom: 20px;
+}
+.mb16 {
+ margin-bottom: 16px;
+}
+.mb8 {
+ margin-bottom: 8px;
+}
+.mt16 {
+ margin-top: 16px;
+}
+.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);
+}
+.sr-only-input {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+.form-section {
+ margin-bottom: 16px;
+ border: 1px solid var(--el-border-color-lighter);
+}
+.form-section :deep(.el-card__header) {
+ padding: 12px 16px;
+ background: var(--el-fill-color-lighter);
+}
+.form-section :deep(.el-card__body) {
+ padding: 16px 16px 4px;
+}
+.card-header-title {
+ font-size: 15px;
+ font-weight: 600;
+}
+.card-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+}
+.amount-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+}
+.amount-input {
+ flex: 1;
+ min-width: 160px;
+}
+.w-full {
+ width: 100%;
+}
+.attachment-form-item {
+ margin-bottom: 0;
+}
+.detail-table {
+ margin-bottom: 0;
+}
+.section-title {
+ font-size: 15px;
+ font-weight: 600;
+ margin: 8px 0 12px;
+ color: var(--el-text-color-primary);
+ border-left: 3px solid var(--el-color-primary);
+ padding-left: 8px;
+}
+.field-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin-top: 4px;
+}
+.warn-list {
+ margin: 0;
+ padding-left: 18px;
+}
+.detail-toolbar {
+ margin-bottom: 8px;
+}
+.upload-block {
+ width: 100%;
+}
+.flow-tip {
+ font-size: 12px;
+ color: var(--el-text-color-secondary);
+ margin-top: 8px;
+}
+.sync-btn {
+ margin-top: 4px;
+}
+.travel-reimburse-form-dialog :deep(.el-dialog__body) {
+ padding-top: 12px;
+}
+.travel-reimburse-form :deep(.el-form-item) {
+ margin-bottom: 18px;
+}
+.travel-reimburse-form :deep(.el-input-number) {
+ width: 100%;
+}
+.travel-reimburse-form :deep(.el-row) {
+ margin-bottom: 0;
+}
+</style>
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js
new file mode 100644
index 0000000..6c94c61
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/travelReimburseUtils.js
@@ -0,0 +1,189 @@
+import dayjs from "dayjs";
+
+/** 璐圭敤绉戠洰 */
+export const EXPENSE_SUBJECT_OPTIONS = [
+ { label: "浜ら�氳垂", value: "transport" },
+ { label: "浣忓璐�", value: "hotel" },
+ { label: "椁愰ギ璐�", value: "meal" },
+ { label: "鍏朵粬", value: "other" },
+];
+
+const TIER1_CITIES = ["鍖椾含", "涓婃捣", "骞垮窞", "娣卞湷"];
+
+export function expenseSubjectLabel(v) {
+ return EXPENSE_SUBJECT_OPTIONS.find((x) => x.value === v)?.label || "鈥�";
+}
+
+export function statusLabel(v) {
+ if (v === "draft") return "鑽夌";
+ if (v === "approved") return "閫氳繃";
+ if (v === "paid") return "宸蹭粯娆�";
+ if (v === "rejected") return "椹冲洖";
+ if (v === "cancelled") return "宸叉挙鍥�";
+ return "瀹℃牳涓�";
+}
+
+export function statusTagType(v) {
+ if (v === "draft") return "info";
+ if (v === "approved" || v === "paid") return "success";
+ if (v === "rejected") return "danger";
+ if (v === "cancelled") return "info";
+ return "warning";
+}
+
+export function detectTravelTier(destination) {
+ const city = (destination || "").trim();
+ if (!city) return "tier3";
+ if (TIER1_CITIES.some((c) => city.includes(c))) return "tier1";
+ const tier2Keywords = ["鏉窞", "鍗椾含", "姝︽眽", "鎴愰兘", "閲嶅簡", "瑗垮畨", "澶╂触", "鑻忓窞", "闀挎矙", "閮戝窞"];
+ if (tier2Keywords.some((c) => city.includes(c))) return "tier2";
+ return "tier3";
+}
+
+export function getTravelStandardByTier(tier) {
+ const map = {
+ tier1: { hotelPerNight: 600, transportPerDay: 80, mealPerDay: 100, label: "涓�绾垮煄甯�" },
+ tier2: { hotelPerNight: 450, transportPerDay: 60, mealPerDay: 80, label: "浜岀嚎鍩庡競" },
+ tier3: { hotelPerNight: 350, transportPerDay: 40, mealPerDay: 60, label: "鍏朵粬鍩庡競" },
+ };
+ return map[tier] || map.tier3;
+}
+
+export function computeTravelDays(startStr, endStr) {
+ if (!startStr || !endStr) return null;
+ const t0 = dayjs(startStr);
+ const t1 = dayjs(endStr);
+ if (!t0.isValid() || !t1.isValid() || !t1.isAfter(t0)) return null;
+ const days = Math.ceil(t1.diff(t0, "day", true));
+ return Math.max(1, days);
+}
+
+export function createEmptyExpenseDetail() {
+ return {
+ id: `ed_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
+ invoiceDate: "",
+ expenseSubject: "",
+ amount: undefined,
+ description: "",
+ };
+}
+
+export function createEmptyForm() {
+ return {
+ id: undefined,
+ reimburseNo: "",
+ applicantId: "",
+ employeeNo: "",
+ employeeName: "",
+ reimburseReason: "",
+ travelStartTime: "",
+ travelEndTime: "",
+ travelDays: undefined,
+ departurePlace: "",
+ destination: "",
+ hotelStandard: undefined,
+ hotelDays: undefined,
+ livingSubsidy: undefined,
+ applyAmount: undefined,
+ payee: "",
+ expenseDetails: [],
+ attachmentList: [],
+ approvalFlowNodes: [],
+ currentNodeIndex: 0,
+ needSpecialApproval: false,
+ deptId: "",
+ deptName: "",
+ travelTier: "tier3",
+ };
+}
+
+export function initApprovalFlowNodes(nodes) {
+ return (nodes || []).map((n, i) => ({
+ ...n,
+ sortOrder: i + 1,
+ nodeOrder: i + 1,
+ nodeStatus: i === 0 ? "process" : "wait",
+ approveOpinion: n.approveOpinion || "",
+ approveTime: n.approveTime || "",
+ }));
+}
+
+export function advanceApprovalFlow(row, opinion) {
+ const nodes = [...(row.approvalFlowNodes || [])];
+ const idx = row.currentNodeIndex ?? 0;
+ if (!nodes.length) return { nodes, currentNodeIndex: idx, approvalResult: row.approvalResult };
+ const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ nodes[idx] = {
+ ...nodes[idx],
+ nodeStatus: "finish",
+ approveOpinion: opinion || "鍚屾剰",
+ approveTime: now,
+ };
+ const next = idx + 1;
+ if (next >= nodes.length) {
+ return { nodes, currentNodeIndex: idx, approvalResult: "approved" };
+ }
+ nodes[next] = { ...nodes[next], nodeStatus: "process" };
+ return { nodes, currentNodeIndex: next, approvalResult: "pending" };
+}
+
+export function rejectApprovalFlow(row, opinion) {
+ const nodes = [...(row.approvalFlowNodes || [])];
+ const idx = row.currentNodeIndex ?? 0;
+ const now = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ if (nodes[idx]) {
+ nodes[idx] = {
+ ...nodes[idx],
+ nodeStatus: "error",
+ approveOpinion: opinion || "椹冲洖",
+ approveTime: now,
+ };
+ }
+ return { nodes, currentNodeIndex: idx, approvalResult: "rejected", rejectReason: opinion || "椹冲洖" };
+}
+
+/** 閮ㄩ棬棰勭畻锛堝鎺ラ绠楃郴缁熷墠杩斿洖绌猴級 */
+export function mockDeptBudget(deptId) {
+ if (!deptId) return null;
+ return null;
+}
+
+export function normalizeImportedRow(raw, idx) {
+ const id = raw.id != null && String(raw.id).length ? `imp_${String(raw.id)}_${idx}` : `imp_${Date.now()}_${idx}`;
+ const travelDays =
+ raw.travelDays != null
+ ? Number(raw.travelDays)
+ : computeTravelDays(raw.travelStartTime, raw.travelEndTime);
+ return {
+ id,
+ reimburseNo: raw.reimburseNo || `TR${dayjs().format("YYYYMMDD")}${String(idx).padStart(4, "0")}`,
+ applicantId: raw.applicantId != null ? String(raw.applicantId) : `imp_user_${idx}`,
+ employeeNo: raw.employeeNo ?? raw.applicantNo ?? "",
+ employeeName: raw.employeeName ?? raw.applicantName ?? "鏈煡",
+ applicantNo: raw.employeeNo ?? raw.applicantNo ?? "",
+ applicantName: raw.employeeName ?? raw.applicantName ?? "鏈煡",
+ reimburseReason: raw.reimburseReason ?? "",
+ travelStartTime: raw.travelStartTime ?? "",
+ travelEndTime: raw.travelEndTime ?? "",
+ travelDays: travelDays == null || Number.isNaN(travelDays) ? 1 : travelDays,
+ departurePlace: raw.departurePlace ?? "",
+ destination: raw.destination ?? "",
+ hotelStandard: raw.hotelStandard,
+ hotelDays: raw.hotelDays,
+ livingSubsidy: raw.livingSubsidy,
+ applyAmount: raw.applyAmount ?? 0,
+ payee: raw.payee ?? "",
+ expenseDetails: Array.isArray(raw.expenseDetails) ? raw.expenseDetails : [],
+ invoiceAttachments: Array.isArray(raw.invoiceAttachments) ? raw.invoiceAttachments : [],
+ approvalFlowNodes: Array.isArray(raw.approvalFlowNodes) ? raw.approvalFlowNodes : [],
+ currentNodeIndex: raw.currentNodeIndex ?? 0,
+ approvalResult: ["pending", "approved", "rejected"].includes(raw.approvalResult) ? raw.approvalResult : "pending",
+ rejectReason: raw.rejectReason ?? "",
+ approvalRecords: Array.isArray(raw.approvalRecords) ? raw.approvalRecords : [],
+ needSpecialApproval: !!raw.needSpecialApproval,
+ deptId: raw.deptId ?? "",
+ deptName: raw.deptName ?? "",
+ travelTier: raw.travelTier || detectTravelTier(raw.destination),
+ createTime: raw.createTime || dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ };
+}
diff --git a/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
new file mode 100644
index 0000000..c92d88c
--- /dev/null
+++ b/src/views/officeProcessAutomation/ReimburseManage/travel-reimburse/useTravelReimburse.js
@@ -0,0 +1,696 @@
+import { Search } from "@element-plus/icons-vue";
+import dayjs from "dayjs";
+import {
+ deleteFinReimbursement,
+ getFinReimbursementDetail,
+ listFinReimbursementPage,
+ persistFinReimbursement,
+} from "@/api/officeProcessAutomation/finReimbursement.js";
+import { ElMessageBox } from "element-plus";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
+import {
+ buildFinReimbursementListParams,
+ filterReimbursementRowsBySearch,
+ hasActiveReimbursementSearch,
+ buildTravelReimbursementSaveDto,
+ canDeleteReimbursementRow,
+ canEditReimbursementRow,
+ enrichReimbursementListRowsWithApprovalFlow,
+ filterRowsByReimbursementType,
+ FIN_REIMBURSEMENT_TYPE,
+ mapFinReimbursementDetailRow,
+ mapTravelReimbursementRow,
+ resolveReimbursementDeleteId,
+ unwrapFinReimbursementDetail,
+ unwrapFinReimbursementPage,
+ validateReimbursementApprovalNodes,
+ validateReimbursementPersistDto,
+} from "../shared/finReimbursementMappers.js";
+import { consumeReimburseEditFromApprove } from "../shared/reimburseApproveBridge.js";
+import {
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseSubjectLabel,
+ statusLabel,
+ statusTagType,
+ detectTravelTier,
+ getTravelStandardByTier,
+ computeTravelDays,
+ createEmptyExpenseDetail,
+ createEmptyForm,
+ initApprovalFlowNodes,
+ advanceApprovalFlow,
+ rejectApprovalFlow,
+ mockDeptBudget,
+ normalizeImportedRow,
+} from "./travelReimburseUtils.js";
+
+function unwrapArray(payload) {
+ if (Array.isArray(payload)) return payload;
+ if (payload?.data && Array.isArray(payload.data)) return payload.data;
+ if (payload?.rows && Array.isArray(payload.rows)) return payload.rows;
+ return [];
+}
+
+function isActiveUser(u) {
+ if (u.delFlag === "2" || u.delFlag === 2) return false;
+ if (u.status == null) return true;
+ return String(u.status) === "0";
+}
+
+export function useTravelReimburse() {
+ const { proxy } = getCurrentInstance();
+
+ const allRows = ref([]);
+
+ const searchForm = reactive({ applicantKeyword: "" });
+ const tableLoading = ref(false);
+ const page = reactive({ current: 1, size: 10, total: 0 });
+ const importInputRef = ref(null);
+ const allUsersCache = ref([]);
+ const applicantFormSearchLoading = ref(false);
+ const applicantFormOptions = ref([]);
+ const formRef = ref();
+ const form = reactive(createEmptyForm());
+ const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
+ const detailDialog = reactive({ visible: false });
+ const detailLoading = ref(false);
+ const detailRow = ref({});
+ const approveDialog = reactive({ visible: false, row: null });
+ const approveOpinion = ref("");
+ const submitSaving = ref(false);
+
+ const tableData = computed(() => allRows.value);
+
+ async function fetchList() {
+ tableLoading.value = true;
+ try {
+ const res = await listFinReimbursementPage(
+ buildFinReimbursementListParams({
+ page,
+ searchForm,
+ reimbursementType: FIN_REIMBURSEMENT_TYPE.TRAVEL,
+ })
+ );
+ const { records, total } = unwrapFinReimbursementPage(res);
+ const filtered = filterRowsByReimbursementType(
+ records,
+ FIN_REIMBURSEMENT_TYPE.TRAVEL
+ );
+ let mapped = filtered.map(mapTravelReimbursementRow);
+ mapped = await enrichReimbursementListRowsWithApprovalFlow(
+ mapped,
+ FIN_REIMBURSEMENT_TYPE.TRAVEL
+ );
+ if (hasActiveReimbursementSearch(searchForm)) {
+ mapped = filterReimbursementRowsBySearch(mapped, searchForm);
+ }
+ allRows.value = mapped;
+ const dropped = records.length - filtered.length;
+ let nextTotal =
+ dropped > 0 ? Math.max(0, Number(total) - dropped) : Number(total);
+ if (hasActiveReimbursementSearch(searchForm)) {
+ nextTotal = mapped.length;
+ }
+ page.total = nextTotal;
+ } catch {
+ allRows.value = [];
+ page.total = 0;
+ proxy?.$modal?.msgError?.("宸梾鎶ラ攢鍒楄〃鍔犺浇澶辫触");
+ } finally {
+ tableLoading.value = false;
+ }
+ }
+
+ const flowUserOptions = computed(() => allUsersCache.value.filter(isActiveUser));
+
+ const travelDaysDisplay = computed(() => {
+ const d = computeTravelDays(form.travelStartTime, form.travelEndTime);
+ return d == null ? "" : String(d);
+ });
+
+ const travelTierLabel = computed(() => {
+ const std = getTravelStandardByTier(form.travelTier || detectTravelTier(form.destination));
+ return `鎸�${std.label}鏍囧噯`;
+ });
+
+ const suggestedLivingSubsidy = computed(() => {
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || form.hotelDays || 0;
+ const std = getTravelStandardByTier(form.travelTier);
+ return Math.round(std.mealPerDay * days * 100) / 100;
+ });
+
+ const suggestedTransportSubsidy = computed(() => {
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime) || 0;
+ const std = getTravelStandardByTier(form.travelTier);
+ return Math.round(std.transportPerDay * days * 100) / 100;
+ });
+
+ const suggestedHotelLimit = computed(() => {
+ const nights = form.hotelDays || 0;
+ const perNight = form.hotelStandard ?? getTravelStandardByTier(form.travelTier).hotelPerNight;
+ return Math.round(perNight * nights * 100) / 100;
+ });
+
+ const detailTotalAmount = computed(() => {
+ const sum = (form.expenseDetails || []).reduce((s, d) => s + (Number(d.amount) || 0), 0);
+ return Math.round(sum * 100) / 100;
+ });
+
+ const overBudgetWarnings = computed(() => buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value));
+
+ const budgetHint = computed(() => {
+ if (!form.deptId) return { visible: false };
+ const b = mockDeptBudget(form.deptId);
+ const apply = Number(form.applyAmount) || detailTotalAmount.value || 0;
+ const after = b.remainingAmount - apply;
+ return {
+ visible: true,
+ type: after < 0 ? "error" : "info",
+ title: `閮ㄩ棬棰勭畻鑱斿姩锛�${form.deptName || b.deptId}锛塦,
+ description: `骞村害棰勭畻 ${b.totalBudget} 鍏冿紝宸茬敤 ${b.usedAmount} 鍏冿紝鍓╀綑 ${b.remainingAmount} 鍏冿紱鏈崟鐢宠鍚庨璁″墿浣� ${Math.round(after * 100) / 100} 鍏冦�俙,
+ };
+ });
+
+ const tableColumn = ref([
+ { label: "鎶ラ攢鍗曞彿", prop: "reimburseNo", width: 150 },
+ { label: "鐢宠浜虹紪鍙�", prop: "applicantNo", width: 110 },
+ { label: "鐢宠浜�", prop: "applicantName", minWidth: 90 },
+ { label: "鍑哄樊寮�濮�", prop: "travelStartTime", width: 165 },
+ { label: "鍑哄樊缁撴潫", prop: "travelEndTime", width: 165 },
+ { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 165 },
+ {
+ label: "鐘舵��",
+ prop: "approvalResult",
+ width: 100,
+ dataType: "tag",
+ formatData: (v) => statusLabel(v),
+ formatType: (v) => statusTagType(v),
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 220,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: (row) => !canEditReimbursementRow(row),
+ clickFun: (row) => openFormDialog("edit", row),
+ },
+ { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ disabled: (row) => !canDeleteReimbursementRow(row),
+ clickFun: (row) => confirmRemoveRow(row),
+ },
+ ],
+ },
+ ]);
+
+ const formRules = {
+ applicantId: [{ required: true, message: "璇烽�夋嫨鍛樺伐", trigger: "change" }],
+ reimburseReason: [{ required: true, message: "璇峰~鍐欐姤閿�鍘熷洜", trigger: "blur" }],
+ travelStartTime: [{ required: true, message: "璇烽�夋嫨鍑哄樊寮�濮嬫椂闂�", trigger: "change" }],
+ travelEndTime: [
+ { required: true, message: "璇烽�夋嫨鍑哄樊缁撴潫鏃堕棿", trigger: "change" },
+ {
+ validator: (_r, val, cb) => {
+ if (!form.travelStartTime || !val) { cb(); return; }
+ if (computeTravelDays(form.travelStartTime, val) == null) cb(new Error("缁撴潫鏃堕棿椤绘櫄浜庡紑濮嬫椂闂�"));
+ else cb();
+ },
+ trigger: "change",
+ },
+ ],
+ departurePlace: [{ required: true, message: "璇峰~鍐欏嚭宸湴", trigger: "blur" }],
+ destination: [{ required: true, message: "璇峰~鍐欑洰鐨勫湴", trigger: "blur" }],
+ applyAmount: [{ required: true, message: "璇峰~鍐欑敵璇烽噾棰�", trigger: "blur" }],
+ payee: [{ required: true, message: "璇峰~鍐欐敹娆句汉", trigger: "blur" }],
+ approvalFlowNodes: [
+ {
+ validator: (_r, _v, cb) => {
+ const nodes = form.approvalFlowNodes || [];
+ if (!nodes.length) { cb(new Error("璇疯嚦灏戦厤缃竴涓鎵硅妭鐐�")); return; }
+ if (nodes.some((n) => n.approverId == null || n.approverId === "")) { cb(new Error("姣忎釜鑺傜偣椤婚�夋嫨瀹℃壒浜�")); return; }
+ cb();
+ },
+ trigger: "change",
+ },
+ ],
+ };
+
+ function buildOverBudgetWarnings(f, detailTotal, hotelLimit, transportLimit, mealLimit) {
+ const warnings = [];
+ const bySubject = { transport: 0, hotel: 0, meal: 0, other: 0 };
+ (f.expenseDetails || []).forEach((d) => {
+ const key = d.expenseSubject || "other";
+ bySubject[key] = (bySubject[key] || 0) + (Number(d.amount) || 0);
+ });
+ if (bySubject.transport > transportLimit && transportLimit > 0) {
+ warnings.push(`浜ら�氳垂 ${bySubject.transport} 鍏冭秴鍑烘爣鍑� ${transportLimit} 鍏僠);
+ }
+ if (bySubject.hotel > hotelLimit && hotelLimit > 0) {
+ warnings.push(`浣忓璐� ${bySubject.hotel} 鍏冭秴鍑洪檺棰� ${hotelLimit} 鍏僠);
+ }
+ if (bySubject.meal > mealLimit && mealLimit > 0) {
+ warnings.push(`椁愰ギ璐� ${bySubject.meal} 鍏冭秴鍑虹敓娲昏ˉ璐村缓璁� ${mealLimit} 鍏僠);
+ }
+ const std = getTravelStandardByTier(f.travelTier);
+ if (f.hotelStandard > std.hotelPerNight) {
+ warnings.push(`閰掑簵鏍囧噯 ${f.hotelStandard} 鍏�/鏅氶珮浜�${std.label}鏍囧噯 ${std.hotelPerNight} 鍏�/鏅歚);
+ }
+ const apply = Number(f.applyAmount) || detailTotal;
+ const standardTotal = transportLimit + hotelLimit + mealLimit;
+ if (apply > standardTotal && standardTotal > 0) {
+ warnings.push(`鐢宠鎬婚 ${apply} 鍏冮珮浜庡樊鏃呮爣鍑嗗悎璁$害 ${standardTotal} 鍏僠);
+ }
+ return warnings;
+ }
+
+ async function loadUserPool() {
+ try {
+ allUsersCache.value = unwrapArray(await userListNoPageByTenantId());
+ } catch {
+ allUsersCache.value = [];
+ }
+ }
+
+ function userSelectLabel(u) {
+ const nick = u.nickName || "";
+ const name = u.userName || "";
+ if (nick && name && nick !== name) return `${nick}锛�${name}锛塦;
+ return nick || name || `鐢ㄦ埛${u.userId ?? u.id ?? ""}`;
+ }
+
+ function userById(id) {
+ return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
+ }
+
+ function employeeNoFromUser(u) {
+ if (!u) return "";
+ return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
+ }
+
+ function filterUsersByQuery(query) {
+ const list = allUsersCache.value.filter(isActiveUser);
+ const q = (query || "").trim().toLowerCase();
+ if (!q) return [...list];
+ return list.filter((u) => {
+ const nick = (u.nickName || "").toLowerCase();
+ const uname = (u.userName || "").toLowerCase();
+ return nick.includes(q) || uname.includes(q);
+ });
+ }
+
+ async function remoteSearchApplicantForm(query) {
+ applicantFormSearchLoading.value = true;
+ try {
+ if (!allUsersCache.value.length) await loadUserPool();
+ applicantFormOptions.value = filterUsersByQuery(query);
+ } finally {
+ applicantFormSearchLoading.value = false;
+ }
+ }
+
+ function onApplicantChange(uid) {
+ const u = userById(uid);
+ if (u) {
+ form.employeeName = u.nickName || u.userName || "";
+ form.employeeNo = employeeNoFromUser(u);
+ form.payee = form.payee || form.employeeName;
+ form.deptId = String(u.deptId ?? u.sysDeptId ?? "");
+ form.deptName = u.dept?.deptName ?? u.deptName ?? "";
+ } else {
+ form.employeeName = "";
+ form.employeeNo = "";
+ }
+ }
+
+ function recalcTravelStandards() {
+ form.travelTier = detectTravelTier(form.destination);
+ const std = getTravelStandardByTier(form.travelTier);
+ if (form.hotelStandard == null || form.hotelStandard === 0) form.hotelStandard = std.hotelPerNight;
+ const days = computeTravelDays(form.travelStartTime, form.travelEndTime);
+ if (days != null) {
+ form.travelDays = days;
+ if (form.hotelDays == null) form.hotelDays = Math.max(0, days - 1);
+ if (form.livingSubsidy == null || form.livingSubsidy === 0) form.livingSubsidy = suggestedLivingSubsidy.value;
+ }
+ form.needSpecialApproval = buildOverBudgetWarnings(form, detailTotalAmount.value, suggestedHotelLimit.value, suggestedTransportSubsidy.value, suggestedLivingSubsidy.value).length > 0;
+ }
+
+ function onTravelRangeChange() {
+ recalcTravelStandards();
+ nextTick(() => formRef.value?.validateField?.("travelEndTime"));
+ }
+
+ function onDetailAmountChange() {
+ recalcTravelStandards();
+ }
+
+ function onApprovalFlowChange() {
+ nextTick(() => formRef.value?.validateField?.("approvalFlowNodes"));
+ }
+
+ function addExpenseDetail() {
+ form.expenseDetails.push(createEmptyExpenseDetail());
+ }
+
+ function removeExpenseDetail(index) {
+ form.expenseDetails.splice(index, 1);
+ recalcTravelStandards();
+ }
+
+ function mapAttachmentList(list) {
+ return (list || []).map((f, i) => ({
+ id: f.id ?? f.uid ?? `inv_${Date.now()}_${i}`,
+ name: f.name || f.fileName || f.originalFilename || "鏈懡鍚�",
+ url: f.url || f.downloadURL || f.previewURL || f.previewUrl || "",
+ }));
+ }
+
+ function syncApplyAmountFromDetails() {
+ form.applyAmount = detailTotalAmount.value;
+ recalcTravelStandards();
+ }
+
+ function handleQuery() {
+ page.current = 1;
+ return fetchList();
+ }
+
+ function resetSearch() {
+ searchForm.applicantKeyword = "";
+ handleQuery();
+ }
+
+ function pagination(obj) {
+ page.current = obj.page;
+ page.size = obj.limit;
+ return fetchList();
+ }
+
+ async function loadTravelDetailRow(row) {
+ const id = resolveReimbursementDeleteId(row);
+ if (id == null) {
+ throw new Error("missing id");
+ }
+ const res = await getFinReimbursementDetail(id);
+ const raw = unwrapFinReimbursementDetail(res);
+ return mapFinReimbursementDetailRow(raw, FIN_REIMBURSEMENT_TYPE.TRAVEL);
+ }
+
+ async function openDetail(row) {
+ const id = resolveReimbursementDeleteId(row);
+ if (id == null) {
+ proxy?.$modal?.msgWarning?.("鏃犳硶鏌ョ湅璇︽儏锛氱己灏戞姤閿�鍗� ID");
+ return;
+ }
+ detailDialog.visible = true;
+ detailLoading.value = true;
+ detailRow.value = { ...row };
+ try {
+ detailRow.value = await loadTravelDetailRow(row);
+ } catch {
+ proxy?.$modal?.msgError?.("鍔犺浇璇︽儏澶辫触");
+ detailDialog.visible = false;
+ } finally {
+ detailLoading.value = false;
+ }
+ }
+
+ async function confirmRemoveRow(row) {
+ const id = resolveReimbursementDeleteId(row);
+ if (id == null) {
+ proxy?.$modal?.msgWarning?.("鏃犳硶鍒犻櫎锛氱己灏戞姤閿�鍗� ID");
+ return;
+ }
+ const title = row.reimburseNo || row.billNo || row.reimburseReason || "璇ユ姤閿�鍗�";
+ try {
+ await ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ゃ��${title}銆嶅悧锛熷垹闄ゅ悗涓嶅彲鎭㈠銆俙,
+ "鍒犻櫎纭",
+ {
+ type: "warning",
+ confirmButtonText: "纭畾鍒犻櫎",
+ cancelButtonText: "鍙栨秷",
+ distinguishCancelAndClose: true,
+ autofocus: false,
+ }
+ );
+ } catch {
+ return;
+ }
+ try {
+ await deleteFinReimbursement([id]);
+ proxy?.$modal?.msgSuccess?.("鍒犻櫎鎴愬姛");
+ if (detailDialog.visible && resolveReimbursementDeleteId(detailRow.value) === id) {
+ detailDialog.visible = false;
+ }
+ await handleQuery();
+ } catch {
+ proxy?.$modal?.msgError?.("鍒犻櫎澶辫触");
+ }
+ }
+
+ function openApprove(row) {
+ approveDialog.row = { ...row };
+ approveDialog.visible = true;
+ }
+
+ function approvalActionLabel(v) {
+ if (v === "approved") return "閫氳繃";
+ if (v === "rejected") return "椹冲洖";
+ return "鎻愪氦";
+ }
+
+ async function openFormDialog(mode, row) {
+ formDialog.mode = mode;
+ formDialog.readonly = false;
+ formDialog.title = mode === "add" ? "鏂板宸梾鎶ラ攢" : "缂栬緫宸梾鎶ラ攢";
+ if (!allUsersCache.value.length) await loadUserPool();
+ Object.assign(form, createEmptyForm());
+ if (mode === "edit" && row) {
+ let editRow = row;
+ try {
+ editRow = await loadTravelDetailRow(row);
+ } catch {
+ proxy?.$modal?.msgError?.("鍔犺浇鎶ラ攢璇︽儏澶辫触");
+ return;
+ }
+ Object.assign(form, {
+ ...JSON.parse(JSON.stringify(editRow)),
+ reimbursementId: editRow.reimbursementId ?? editRow.id,
+ attachmentList: JSON.parse(JSON.stringify(editRow.attachmentList || editRow.invoiceAttachments || [])),
+ approvalFlowNodes: JSON.parse(JSON.stringify(editRow.approvalFlowNodes || [])),
+ expenseDetails: JSON.parse(JSON.stringify(editRow.expenseDetails || [])),
+ });
+ const u = userById(editRow.applicantId);
+ applicantFormOptions.value = u
+ ? [u]
+ : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }];
+ } else {
+ form.approvalFlowNodes = [{ approverId: null, approverName: "", sortOrder: 1, nodeOrder: 1 }];
+ remoteSearchApplicantForm("");
+ }
+ formDialog.visible = true;
+ nextTick(() => {
+ formRef.value?.clearValidate?.();
+ recalcTravelStandards();
+ });
+ }
+
+ function onFormClosed() {
+ formRef.value?.resetFields?.();
+ }
+
+ async function submitForm() {
+ try {
+ await formRef.value?.validate?.();
+ } catch {
+ return;
+ }
+ if (!(form.expenseDetails || []).length) {
+ proxy?.$modal?.msgWarning?.("璇疯嚦灏戞坊鍔犱竴鏉℃姤閿�鏄庣粏");
+ return;
+ }
+ recalcTravelStandards();
+ if (form.needSpecialApproval) {
+ try {
+ await proxy.$modal.confirm("瀛樺湪瓒呮敮椤癸紝鎻愪氦鍚庡皢鏍囪涓洪渶鐗规壒锛屾槸鍚︾户缁紵");
+ } catch {
+ return;
+ }
+ }
+ if (submitSaving.value) return;
+ const isEdit = formDialog.mode === "edit";
+ const dto = buildTravelReimbursementSaveDto(form, { computeTravelDays });
+ const check = validateReimbursementPersistDto(dto, isEdit);
+ if (!check.ok) {
+ proxy?.$modal?.msgWarning?.(check.message);
+ return;
+ }
+ const nodeCheck = validateReimbursementApprovalNodes(dto);
+ if (!nodeCheck.ok) {
+ proxy?.$modal?.msgWarning?.(nodeCheck.message);
+ return;
+ }
+ submitSaving.value = true;
+ try {
+ await persistFinReimbursement(dto, isEdit);
+ proxy?.$modal?.msgSuccess?.(isEdit ? "淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ formDialog.visible = false;
+ await handleQuery();
+ } catch {
+ proxy?.$modal?.msgError?.(isEdit ? "淇濆瓨澶辫触" : "鎻愪氦澶辫触");
+ } finally {
+ submitSaving.value = false;
+ }
+ }
+
+ async function submitApprove(result) {
+ const row = approveDialog.row;
+ if (!row) return;
+ if (result === "rejected" && !(approveOpinion.value || "").trim()) {
+ proxy?.$modal?.msgWarning?.("椹冲洖椤诲~鍐欏鎵规剰瑙�");
+ return;
+ }
+ const idx = allRows.value.findIndex((r) => r.id === row.id);
+ if (idx === -1) return;
+ const cur = allRows.value[idx];
+ const operatorName = "褰撳墠瀹℃壒浜�";
+ const record = {
+ operatorName,
+ result,
+ opinion: approveOpinion.value || (result === "approved" ? "鍚屾剰" : "椹冲洖"),
+ time: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ };
+ const records = [...(cur.approvalRecords || []), record];
+ let flowUpdate;
+ if (result === "approved") {
+ flowUpdate = advanceApprovalFlow(cur, approveOpinion.value);
+ } else {
+ flowUpdate = rejectApprovalFlow(cur, approveOpinion.value);
+ }
+ allRows.value[idx] = {
+ ...cur,
+ approvalFlowNodes: flowUpdate.nodes,
+ currentNodeIndex: flowUpdate.currentNodeIndex,
+ approvalResult: flowUpdate.approvalResult,
+ rejectReason: flowUpdate.rejectReason ?? cur.rejectReason,
+ approvalRecords: records,
+ };
+ proxy?.$modal?.msgSuccess?.(result === "approved" ? "宸查�氳繃" : "宸查┏鍥�");
+ approveDialog.visible = false;
+ handleQuery();
+ }
+
+ function handleExport() {
+ const data = allRows.value;
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `宸梾鎶ラ攢瀵煎嚭_${dayjs().format("YYYYMMDDHHmmss")}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ proxy?.$modal?.msgSuccess?.(`宸插鍑� ${data.length} 鏉);
+ }
+
+ function handleImportClick() {
+ importInputRef.value?.click?.();
+ }
+
+ function onImportFile(e) {
+ const file = e.target.files?.[0];
+ e.target.value = "";
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = () => {
+ try {
+ const parsed = JSON.parse(String(reader.result || ""));
+ const arr = Array.isArray(parsed) ? parsed : parsed?.rows || parsed?.data;
+ if (!Array.isArray(arr) || !arr.length) {
+ proxy?.$modal?.msgWarning?.("瀵煎叆鏍煎紡椤讳负宸梾鎶ラ攢 JSON 鏁扮粍");
+ return;
+ }
+ arr.forEach((raw, i) => allRows.value.unshift(normalizeImportedRow(raw, i)));
+ proxy?.$modal?.msgSuccess?.(`鎴愬姛瀵煎叆 ${arr.length} 鏉);
+ handleQuery();
+ } catch {
+ proxy?.$modal?.msgError?.("瑙f瀽澶辫触");
+ }
+ };
+ reader.readAsText(file, "utf-8");
+ }
+
+ onMounted(async () => {
+ loadUserPool();
+ await fetchList();
+ const editPayload = consumeReimburseEditFromApprove();
+ if (editPayload?.reimbursementId != null) {
+ await openFormDialog("edit", { reimbursementId: editPayload.reimbursementId });
+ }
+ });
+
+ return {
+ Search,
+ EXPENSE_SUBJECT_OPTIONS,
+ expenseSubjectLabel,
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ tableColumn,
+ importInputRef,
+ formRef,
+ form,
+ formDialog,
+ formRules,
+ detailDialog,
+ detailLoading,
+ detailRow,
+ approveDialog,
+ approveOpinion,
+ applicantFormSearchLoading,
+ applicantFormOptions,
+ flowUserOptions,
+ travelDaysDisplay,
+ travelTierLabel,
+ suggestedLivingSubsidy,
+ suggestedTransportSubsidy,
+ suggestedHotelLimit,
+ detailTotalAmount,
+ overBudgetWarnings,
+ budgetHint,
+ handleQuery,
+ resetSearch,
+ pagination,
+ remoteSearchApplicantForm,
+ userSelectLabel,
+ onApplicantChange,
+ recalcTravelStandards,
+ onTravelRangeChange,
+ onDetailAmountChange,
+ onApprovalFlowChange,
+ addExpenseDetail,
+ removeExpenseDetail,
+ syncApplyAmountFromDetails,
+ openFormDialog,
+ onFormClosed,
+ submitForm,
+ submitSaving,
+ openDetail,
+ confirmRemoveRow,
+ openApprove,
+ approvalActionLabel,
+ submitApprove,
+ handleExport,
+ handleImportClick,
+ onImportFile,
+ };
+}
diff --git a/src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue b/src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue
new file mode 100644
index 0000000..9dd4e90
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/dept-manage/index.vue
@@ -0,0 +1,291 @@
+<!--OA妯″潡锛氶儴闂ㄧ鐞�-->
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="閮ㄩ棬鍚嶇О" prop="deptName">
+ <el-input
+ v-model="queryParams.deptName"
+ placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="閮ㄩ棬鐘舵��" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:dept:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="Sort"
+ @click="toggleExpandAll"
+ >灞曞紑/鎶樺彔</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ <el-table
+ v-if="refreshTable"
+ v-loading="loading"
+ :data="deptList"
+ row-key="deptId"
+ :default-expand-all="isExpandAll"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ >
+ <el-table-column prop="deptName" label="閮ㄩ棬鍚嶇О" width="260"></el-table-column>
+ <el-table-column prop="orderNum" label="鎺掑簭" width="200"></el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="200">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']">鏂板</el-button>
+ <el-button v-if="scope.row.parentId != 0" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 娣诲姞鎴栦慨鏀归儴闂ㄥ璇濇 -->
+ <el-dialog :title="title" v-model="open" width="600px" append-to-body>
+ <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
+ <el-row>
+ <el-col :span="24" v-if="form.parentId !== 0">
+ <el-form-item label="涓婄骇閮ㄩ棬" prop="parentId">
+ <el-tree-select
+ v-model="form.parentId"
+ :data="deptOptions"
+ :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
+ value-key="deptId"
+ placeholder="閫夋嫨涓婄骇閮ㄩ棬"
+ check-strictly
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬鍚嶇О" prop="deptName">
+ <el-input v-model="form.deptName" placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄剧ず鎺掑簭" prop="orderNum">
+ <el-input-number v-model="form.orderNum" controls-position="right" :min="0"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="leader">
+ <el-input v-model="form.leader" placeholder="璇疯緭鍏ヨ礋璐d汉" maxlength="20" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="phone">
+ <el-input v-model="form.phone" placeholder="璇疯緭鍏ヨ仈绯荤數璇�" maxlength="11" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬缂栧彿" prop="deptNick">
+ <el-input v-model="form.deptNick" placeholder="璇疯緭鍏ラ儴闂ㄧ紪鍙�" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Dept">
+import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const deptList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const title = ref("")
+const deptOptions = ref([])
+const isExpandAll = ref(true)
+const refreshTable = ref(true)
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ deptName: undefined,
+ status: undefined
+ },
+ rules: {
+ parentId: [{ required: true, message: "涓婄骇閮ㄩ棬涓嶈兘涓虹┖", trigger: "blur" }],
+ deptName: [{ required: true, message: "閮ㄩ棬鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ orderNum: [{ required: true, message: "鏄剧ず鎺掑簭涓嶈兘涓虹┖", trigger: "blur" }],
+ email: [{ type: "email", message: "璇疯緭鍏ユ纭殑閭鍦板潃", trigger: ["blur", "change"] }],
+ phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur" }],
+ deptNick: [{ required: true, message: "閮ㄩ棬缂栧彿涓嶈兘涓虹┖", trigger: "blur" }],
+ },
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ閮ㄩ棬鍒楄〃 */
+function getList() {
+ loading.value = true
+ listDept(queryParams.value).then(response => {
+ deptList.value = proxy.handleTree(response.data, "deptId")
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ deptId: undefined,
+ parentId: undefined,
+ deptName: undefined,
+ orderNum: 0,
+ leader: undefined,
+ phone: undefined,
+ email: undefined,
+ status: "0",
+ deptNick: undefined,
+ }
+ proxy.resetForm("deptRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd(row) {
+ reset()
+ listDept().then(response => {
+ deptOptions.value = proxy.handleTree(response.data, "deptId")
+ })
+ if (row != undefined) {
+ form.value.parentId = row.deptId
+ }
+ open.value = true
+ title.value = "娣诲姞閮ㄩ棬"
+}
+
+/** 灞曞紑/鎶樺彔鎿嶄綔 */
+function toggleExpandAll() {
+ refreshTable.value = false
+ isExpandAll.value = !isExpandAll.value
+ nextTick(() => {
+ refreshTable.value = true
+ })
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ listDeptExcludeChild(row.deptId).then(response => {
+ deptOptions.value = proxy.handleTree(response.data, "deptId")
+ })
+ getDept(row.deptId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼閮ㄩ棬"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["deptRef"].validate(valid => {
+ if (valid) {
+ if (form.value.deptId != undefined) {
+ updateDept(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addDept(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鍚嶇О涓�"' + row.deptName + '"鐨勬暟鎹」?').then(function() {
+ return delDept(row.deptId)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+getList()
+</script>
+
diff --git a/src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue b/src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue
new file mode 100644
index 0000000..2701c1a
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/log-manage/index.vue
@@ -0,0 +1,315 @@
+<!--OA妯″潡锛氭棩蹇楃鐞�-->
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="鎿嶄綔鍦板潃" prop="operIp">
+ <el-input
+ v-model="queryParams.operIp"
+ placeholder="璇疯緭鍏ユ搷浣滃湴鍧�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绯荤粺妯″潡" prop="title">
+ <el-input
+ v-model="queryParams.title"
+ placeholder="璇疯緭鍏ョ郴缁熸ā鍧�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎿嶄綔浜哄憳" prop="operName">
+ <el-input
+ v-model="queryParams.operName"
+ placeholder="璇疯緭鍏ユ搷浣滀汉鍛�"
+ clearable
+ style="width: 240px;"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="businessType">
+ <el-select
+ v-model="queryParams.businessType"
+ placeholder="鎿嶄綔绫诲瀷"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_oper_type"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="鎿嶄綔鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_common_status"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎿嶄綔鏃堕棿" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['monitor:operlog:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ @click="handleClean"
+ v-hasPermi="['monitor:operlog:remove']"
+ >娓呯┖</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['monitor:operlog:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table ref="operlogRef" v-loading="loading" :data="operlogList" @selection-change="handleSelectionChange" :default-sort="defaultSort" @sort-change="handleSortChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="鏃ュ織缂栧彿" align="center" prop="operId" />
+ <el-table-column label="绯荤粺妯″潡" align="center" prop="title" :show-overflow-tooltip="true" />
+ <el-table-column label="鎿嶄綔绫诲瀷" align="center" prop="businessType">
+ <template #default="scope">
+ <dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔浜哄憳" align="center" width="110" prop="operName" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']" />
+ <el-table-column label="鎿嶄綔鍦板潃" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
+ <el-table-column label="鎿嶄綔鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_common_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔鏃ユ湡" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.operTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="娑堣�楁椂闂�" align="center" prop="costTime" width="110" :show-overflow-tooltip="true" sortable="custom" :sort-orders="['descending', 'ascending']">
+ <template #default="scope">
+ <span>{{ scope.row.costTime }}姣</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="View" @click="handleView(scope.row, scope.index)" v-hasPermi="['monitor:operlog:query']">璇︾粏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 鎿嶄綔鏃ュ織璇︾粏 -->
+ <el-dialog title="鎿嶄綔鏃ュ織璇︾粏" v-model="open" width="800px" append-to-body>
+ <el-form :model="form" label-width="100px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎿嶄綔妯″潡锛�">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
+ <el-form-item
+ label="鐧诲綍淇℃伅锛�"
+ >{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇锋眰鍦板潃锛�">{{ form.operUrl }}</el-form-item>
+ <el-form-item label="璇锋眰鏂瑰紡锛�">{{ form.requestMethod }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鎿嶄綔鏂规硶锛�">{{ form.method }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="璇锋眰鍙傛暟锛�">{{ form.operParam }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="杩斿洖鍙傛暟锛�">{{ form.jsonResult }}</el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎿嶄綔鐘舵�侊細">
+ <div v-if="form.status === 0">姝e父</div>
+ <div v-else-if="form.status === 1">澶辫触</div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="娑堣�楁椂闂达細">{{ form.costTime }}姣</el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎿嶄綔鏃堕棿锛�">{{ parseTime(form.operTime) }}</el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="寮傚父淇℃伅锛�" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="open = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Operlog">
+import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog"
+import {onMounted} from "vue";
+
+const { proxy } = getCurrentInstance()
+const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type","sys_common_status")
+
+const operlogList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const dateRange = ref([])
+const defaultSort = ref({ prop: "operTime", order: "descending" })
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ operIp: undefined,
+ title: undefined,
+ operName: undefined,
+ businessType: undefined,
+ status: undefined
+ }
+})
+
+const { queryParams, form } = toRefs(data)
+
+/** 鏌ヨ鐧诲綍鏃ュ織 */
+function getList() {
+ loading.value = true
+ list(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
+ operlogList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鎿嶄綔鏃ュ織绫诲瀷瀛楀吀缈昏瘧 */
+function typeFormat(row, column) {
+ return proxy.selectDictLabel(sys_oper_type.value, row.businessType)
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ queryParams.value.pageNum = 1
+ proxy.$refs["operlogRef"].sort(defaultSort.value.prop, defaultSort.value.order)
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.operId)
+ multiple.value = !selection.length
+}
+
+/** 鎺掑簭瑙﹀彂浜嬩欢 */
+function handleSortChange(column, prop, order) {
+ queryParams.value.orderByColumn = column.prop
+ queryParams.value.isAsc = column.order
+ getList()
+}
+
+/** 璇︾粏鎸夐挳鎿嶄綔 */
+function handleView(row) {
+ open.value = true
+ form.value = row
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const operIds = row.operId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鏃ュ織缂栧彿涓�"' + operIds + '"鐨勬暟鎹」?').then(function () {
+ return delOperlog(operIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 娓呯┖鎸夐挳鎿嶄綔 */
+function handleClean() {
+ proxy.$modal.confirm("鏄惁纭娓呯┖鎵�鏈夋搷浣滄棩蹇楁暟鎹」?").then(function () {
+ return cleanOperlog()
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("娓呯┖鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("monitor/operlog/export",{
+ ...queryParams.value,
+ }, `config_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue
new file mode 100644
index 0000000..a7546aa
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/user-manage/authRole.vue
@@ -0,0 +1,123 @@
+<template>
+ <div class="app-container">
+ <h4 class="form-header h4">鍩烘湰淇℃伅</h4>
+ <el-form :model="form" label-width="80px">
+ <el-row>
+ <el-col :span="8" :offset="2">
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickName">
+ <el-input v-model="form.nickName" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8" :offset="2">
+ <el-form-item label="鐧诲綍璐﹀彿" prop="userName">
+ <el-input v-model="form.userName" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <h4 class="form-header h4">瑙掕壊淇℃伅</h4>
+ <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)">
+ <el-table-column label="搴忓彿" width="55" type="index" align="center">
+ <template #default="scope">
+ <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column>
+ <el-table-column label="瑙掕壊缂栧彿" align="center" prop="roleId" />
+ <el-table-column label="瑙掕壊鍚嶇О" align="center" prop="roleName" />
+ <el-table-column label="鏉冮檺瀛楃" align="center" prop="roleKey" />
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
+
+ <el-form label-width="100px">
+ <div style="text-align: center;margin-left:-120px;margin-top:30px;">
+ <el-button type="primary" @click="submitForm()">鎻愪氦</el-button>
+ <el-button @click="close()">杩斿洖</el-button>
+ </div>
+ </el-form>
+ </div>
+</template>
+
+<script setup name="AuthRole">
+import { getAuthRole, updateAuthRole } from "@/api/system/user"
+
+const route = useRoute()
+const { proxy } = getCurrentInstance()
+
+const loading = ref(true)
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const roleIds = ref([])
+const roles = ref([])
+const form = ref({
+ nickName: undefined,
+ userName: undefined,
+ userId: undefined
+})
+
+/** 鍗曞嚮閫変腑琛屾暟鎹� */
+function clickRow(row) {
+ if (checkSelectable(row)) {
+ proxy.$refs["roleRef"].toggleRowSelection(row)
+ }
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ roleIds.value = selection.map(item => item.roleId)
+}
+
+/** 淇濆瓨閫変腑鐨勬暟鎹紪鍙� */
+function getRowKey(row) {
+ return row.roleId
+}
+
+// 妫�鏌ヨ鑹茬姸鎬�
+function checkSelectable(row) {
+ return row.status === "0" ? true : false
+}
+
+/** 鍏抽棴鎸夐挳 */
+function close() {
+ const obj = { path: "/system/user" }
+ proxy.$tab.closeOpenPage(obj)
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ const userId = form.value.userId
+ const rIds = roleIds.value.join(",")
+ updateAuthRole({ userId: userId, roleIds: rIds }).then(response => {
+ proxy.$modal.msgSuccess("鎺堟潈鎴愬姛")
+ close()
+ })
+}
+
+(() => {
+ const userId = route.params && route.params.userId
+ if (userId) {
+ loading.value = true
+ getAuthRole(userId).then(response => {
+ form.value = response.user
+ roles.value = response.roles
+ total.value = roles.value.length
+ nextTick(() => {
+ roles.value.forEach(row => {
+ if (row.flag) {
+ proxy.$refs["roleRef"].toggleRowSelection(row)
+ }
+ })
+ })
+ loading.value = false
+ })
+ }
+})()
+</script>
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue
new file mode 100644
index 0000000..97a06b1
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/user-manage/index.vue
@@ -0,0 +1,550 @@
+<!--OA妯″潡锛氱敤鎴风鐞�-->
+<template>
+ <div class="app-container">
+ <el-row :gutter="20" style="height: calc(100vh - 8em)">
+ <splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme">
+ <!--閮ㄩ棬鏁版嵁-->
+ <pane size="16">
+ <el-col style="padding: 10px">
+ <div class="head-container">
+ <el-input v-model="deptNames" placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�" clearable prefix-icon="Search" style="margin-bottom: 20px" />
+ </div>
+ <div class="head-container">
+ <el-tree :data="deptOptions" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" />
+ </div>
+ </el-col>
+ </pane>
+ <!--鐢ㄦ埛鏁版嵁-->
+ <pane size="84">
+ <el-col style="padding: 10px; height: 100%; display: flex; flex-direction: column;">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="鐧诲綍璐﹀彿" prop="userName">
+ <el-input v-model="queryParams.userName" placeholder="璇疯緭鍏ョ櫥褰曡处鍙�" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input v-model="queryParams.phonenumber" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="鐢ㄦ埛鐘舵��" clearable style="width: 240px">
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" style="width: 308px">
+ <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="寮�濮嬫棩鏈�" end-placeholder="缁撴潫鏃ユ湡"></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:user:add']">鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['system:user:edit']">淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['system:user:import']">瀵煎叆</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:user:export']">瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
+ </el-row>
+
+ <div style="flex: 1; overflow: hidden;">
+ <el-table v-loading="loading" :data="userList" height="100%" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
+ <el-table-column label="鐧诲綍璐﹀彿" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="鐢ㄦ埛鏄电О" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="閮ㄩ棬" align="center" key="deptNames" prop="deptNames" v-if="columns[3].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="鎵嬫満鍙风爜" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
+ <el-table-column label="鐘舵��" align="center" key="status" v-if="columns[5].visible">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ active-value="0"
+ inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" v-if="columns[6].visible" width="160">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="150" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="淇敼" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="閲嶇疆瀵嗙爜" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒嗛厤瑙掕壊" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-col>
+ </pane>
+ </splitpanes>
+ </el-row>
+
+ <!-- 娣诲姞鎴栦慨鏀圭敤鎴烽厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="600px" append-to-body>
+ <el-form :model="form" :rules="rules" ref="userRef" label-width="80px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item v-if="form.userId == undefined" label="鐧诲綍璐﹀彿" prop="userName">
+ <el-input v-model="form.userName" placeholder="璇疯緭鍏ョ敤鎴峰悕绉�" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item v-if="form.userId == undefined" label="鐢ㄦ埛瀵嗙爜" prop="password">
+ <el-input v-model="form.password" placeholder="璇疯緭鍏ョ敤鎴峰瘑鐮�" type="password" maxlength="20" show-password />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickName">
+ <el-input v-model="form.nickName" placeholder="璇疯緭鍏ョ敤鎴锋樀绉�" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select v-model="form.deptId" :data="enabledDeptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬" check-strictly />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="宀椾綅" prop="postIds">
+ <el-select v-model="form.postIds" multiple placeholder="璇烽�夋嫨">
+ <el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId" :disabled="item.status == 1"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙掕壊" prop="roleIds">
+ <el-select v-model="form.roleIds" multiple placeholder="璇烽�夋嫨">
+ <el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" :disabled="item.status == 1"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input v-model="form.phonenumber" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" maxlength="11" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ埛鎬у埆">
+ <el-select v-model="form.sex" placeholder="璇烽�夋嫨">
+ <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 鐢ㄦ埛瀵煎叆瀵硅瘽妗� -->
+ <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+ <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <div class="el-upload__tip">
+ <el-checkbox v-model="upload.updateSupport" />鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+ </div>
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">涓嬭浇妯℃澘</el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="User">
+import { getToken } from "@/utils/auth"
+import useAppStore from '@/store/modules/app'
+import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user"
+import { Splitpanes, Pane } from "splitpanes"
+import "splitpanes/dist/splitpanes.css"
+
+const router = useRouter()
+const appStore = useAppStore()
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable, sys_user_sex } = proxy.useDict("sys_normal_disable", "sys_user_sex")
+
+const userList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const dateRange = ref([])
+const deptNames = ref("")
+const deptOptions = ref(undefined)
+const enabledDeptOptions = ref(undefined)
+const initPassword = ref(undefined)
+const postOptions = ref([])
+const roleOptions = ref([])
+/*** 鐢ㄦ埛瀵煎叆鍙傛暟 */
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙鐢ㄦ埛瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙鐢ㄦ埛瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+ updateSupport: 0,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData"
+})
+// 鍒楁樉闅愪俊鎭�
+const columns = ref([
+ { key: 0, label: `鐢ㄦ埛缂栧彿`, visible: true },
+ { key: 1, label: `鐧诲綍璐﹀彿`, visible: true },
+ { key: 2, label: `鐢ㄦ埛鏄电О`, visible: true },
+ { key: 3, label: `閮ㄩ棬`, visible: true },
+ { key: 4, label: `鎵嬫満鍙风爜`, visible: true },
+ { key: 5, label: `鐘舵�乣, visible: true },
+ { key: 6, label: `鍒涘缓鏃堕棿`, visible: true }
+])
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ userName: undefined,
+ phonenumber: undefined,
+ status: undefined,
+ deptId: undefined
+ },
+ rules: {
+ userName: [{ required: true, message: "鐢ㄦ埛鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }, { min: 2, max: 20, message: "鐢ㄦ埛鍚嶇О闀垮害蹇呴』浠嬩簬 2 鍜� 20 涔嬮棿", trigger: "blur" }],
+ nickName: [{ required: true, message: "鐢ㄦ埛鏄电О涓嶈兘涓虹┖", trigger: "blur" }],
+ password: [{ required: true, message: "鐢ㄦ埛瀵嗙爜涓嶈兘涓虹┖", trigger: "blur" }, { min: 5, max: 20, message: "鐢ㄦ埛瀵嗙爜闀垮害蹇呴』浠嬩簬 5 鍜� 20 涔嬮棿", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > \" ' \\\ |", trigger: "blur" }],
+ email: [{ type: "email", message: "璇疯緭鍏ユ纭殑閭鍦板潃", trigger: ["blur", "change"] }],
+ phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur" }],
+ deptId: [{ required: true, message: "褰掑睘閮ㄩ棬涓嶈兘涓虹┖", trigger: "change" }],
+ postIds: [{ required: true, message: "宀椾綅涓嶈兘涓虹┖", trigger: "change" }],
+ roleIds: [{ required: true, message: "瑙掕壊涓嶈兘涓虹┖", trigger: "change" }]
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 閫氳繃鏉′欢杩囨护鑺傜偣 */
+const filterNode = (value, data) => {
+ if (!value) return true
+ return data.label.indexOf(value) !== -1
+}
+
+/** 鏍规嵁鍚嶇О绛涢�夐儴闂ㄦ爲 */
+watch(deptNames, val => {
+ proxy.$refs["deptTreeRef"].filter(val)
+})
+
+/** 鏌ヨ鐢ㄦ埛鍒楄〃 */
+function getList() {
+ loading.value = true
+ listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {
+ loading.value = false
+ userList.value = res.rows
+ total.value = res.total
+ })
+}
+
+/** 鏌ヨ閮ㄩ棬涓嬫媺鏍戠粨鏋� */
+function getDeptTree() {
+ deptTreeSelect().then(response => {
+ deptOptions.value = response.data
+ enabledDeptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
+ })
+}
+
+/** 杩囨护绂佺敤鐨勯儴闂� */
+function filterDisabledDept(deptList) {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children)
+ }
+ return true
+ })
+}
+
+/** 鑺傜偣鍗曞嚮浜嬩欢 */
+function handleNodeClick(data) {
+ queryParams.value.deptId = data.id
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ queryParams.value.deptId = undefined
+ proxy.$refs.deptTreeRef.setCurrentKey(null)
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const userIds = row.userId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鐢ㄦ埛缂栧彿涓�"' + userIds + '"鐨勬暟鎹」锛�').then(function () {
+ return delUser(userIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/user/export", {
+ ...queryParams.value,
+ },`user_${new Date().getTime()}.xlsx`)
+}
+
+/** 鐢ㄦ埛鐘舵�佷慨鏀� */
+function handleStatusChange(row) {
+ let text = row.status === "0" ? "鍚敤" : "鍋滅敤"
+ proxy.$modal.confirm('纭瑕�"' + text + '""' + row.userName + '"鐢ㄦ埛鍚�?').then(function () {
+ return changeUserStatus(row.userId, row.status)
+ }).then(() => {
+ proxy.$modal.msgSuccess(text + "鎴愬姛")
+ }).catch(function () {
+ row.status = row.status === "0" ? "1" : "0"
+ })
+}
+
+/** 鏇村鎿嶄綔 */
+function handleCommand(command, row) {
+ switch (command) {
+ case "handleResetPwd":
+ handleResetPwd(row)
+ break
+ case "handleAuthRole":
+ handleAuthRole(row)
+ break
+ default:
+ break
+ }
+}
+
+/** 璺宠浆瑙掕壊鍒嗛厤 */
+function handleAuthRole(row) {
+ const userId = row.userId
+ router.push("/system/user-auth/role/" + userId)
+}
+
+/** 閲嶇疆瀵嗙爜鎸夐挳鎿嶄綔 */
+function handleResetPwd(row) {
+ proxy.$prompt('璇疯緭鍏�"' + row.userName + '"鐨勬柊瀵嗙爜', "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ closeOnClickModal: false,
+ inputPattern: /^.{5,20}$/,
+ inputErrorMessage: "鐢ㄦ埛瀵嗙爜闀垮害蹇呴』浠嬩簬 5 鍜� 20 涔嬮棿",
+ inputValidator: (value) => {
+ if (/<|>|"|'|\||\\/.test(value)) {
+ return "涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > \" ' \\\ |"
+ }
+ },
+ }).then(({ value }) => {
+ resetUserPwd(row.userId, value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛锛屾柊瀵嗙爜鏄細" + value)
+ })
+ }).catch(() => {})
+}
+
+/** 閫夋嫨鏉℃暟 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.userId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImport() {
+ upload.title = "鐢ㄦ埛瀵煎叆"
+ upload.open = true
+}
+
+/** 涓嬭浇妯℃澘鎿嶄綔 */
+function importTemplate() {
+ proxy.download("system/user/importTemplate", {
+ }, `user_template_${new Date().getTime()}.xlsx`)
+}
+
+/**鏂囦欢涓婁紶涓鐞� */
+const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true
+}
+
+/** 鏂囦欢涓婁紶鎴愬姛澶勭悊 */
+const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false
+ upload.isUploading = false
+ proxy.$refs["uploadRef"].handleRemove(file)
+ proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "瀵煎叆缁撴灉", { dangerouslyUseHTMLString: true })
+ getList()
+}
+
+/** 鎻愪氦涓婁紶鏂囦欢 */
+function submitFileForm() {
+ proxy.$refs["uploadRef"].submit()
+}
+
+/** 閲嶇疆鎿嶄綔琛ㄥ崟 */
+function reset() {
+ form.value = {
+ userId: undefined,
+ deptId: undefined,
+ userName: undefined,
+ nickName: undefined,
+ password: undefined,
+ phonenumber: undefined,
+ email: undefined,
+ sex: undefined,
+ status: "0",
+ remark: undefined,
+ postIds: [],
+ roleIds: []
+ }
+ proxy.resetForm("userRef")
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ getUser().then(response => {
+ postOptions.value = response.posts
+ roleOptions.value = response.roles
+ open.value = true
+ title.value = "娣诲姞鐢ㄦ埛"
+ form.value.password = initPassword.value
+ })
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const userId = row.userId || ids.value
+ getUser(userId).then(response => {
+ form.value = response.data
+ postOptions.value = response.posts
+ roleOptions.value = response.roles
+ form.value.postIds = response.postIds
+ form.value.roleIds = response.roleIds
+ open.value = true
+ title.value = "淇敼鐢ㄦ埛"
+ form.password = ""
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["userRef"].validate(valid => {
+ if (valid) {
+ // 褰掑睘閮ㄩ棬铏界劧鏄崟閫夛紝浣嗗悗绔渶瑕佷紶鏁扮粍瀛楁 deptIds
+ const payload = {
+ ...form.value,
+ deptIds: form.value.deptId ? [form.value.deptId] : []
+ }
+ if (form.value.userId != undefined) {
+ updateUser(payload).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addUser(payload).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+getDeptTree()
+getList()
+</script>
+
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue
new file mode 100644
index 0000000..719a028
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/index.vue
@@ -0,0 +1,87 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="20">
+ <el-col :span="6" :xs="24">
+ <el-card class="box-card">
+ <template v-slot:header>
+ <div class="clearfix">
+ <span>涓汉淇℃伅</span>
+ </div>
+ </template>
+ <div>
+ <div class="text-center">
+ <userAvatar />
+ </div>
+ <ul class="list-group list-group-striped">
+ <li class="list-group-item">
+ <svg-icon icon-class="user" />鐢ㄦ埛鍚嶇О
+ <div class="pull-right">{{ state.user.userName }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="phone" />鎵嬫満鍙风爜
+ <div class="pull-right">{{ state.user.phonenumber }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="email" />鐢ㄦ埛閭
+ <div class="pull-right">{{ state.user.email }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="tree" />鎵�灞為儴闂�
+ <div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="peoples" />鎵�灞炶鑹�
+ <div class="pull-right">{{ state.roleGroup }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="date" />鍒涘缓鏃ユ湡
+ <div class="pull-right">{{ state.user.createTime }}</div>
+ </li>
+ </ul>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="18" :xs="24">
+ <el-card>
+ <template v-slot:header>
+ <div class="clearfix">
+ <span>鍩烘湰璧勬枡</span>
+ </div>
+ </template>
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="鍩烘湰璧勬枡" name="userinfo">
+ <userInfo :user="state.user" />
+ </el-tab-pane>
+ <el-tab-pane label="淇敼瀵嗙爜" name="resetPwd">
+ <resetPwd />
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup name="Profile">
+import userAvatar from "./userAvatar"
+import userInfo from "./userInfo"
+import resetPwd from "./resetPwd"
+import { getUserProfile } from "@/api/system/user"
+
+const activeTab = ref("userinfo")
+const state = reactive({
+ user: {},
+ roleGroup: {},
+ postGroup: {}
+})
+
+function getUser() {
+ getUserProfile().then(response => {
+ state.user = response.data
+ state.roleGroup = response.roleGroup
+ state.postGroup = response.postGroup
+ })
+}
+
+getUser()
+</script>
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue
new file mode 100644
index 0000000..73c6b18
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/resetPwd.vue
@@ -0,0 +1,59 @@
+<template>
+ <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
+ <el-form-item label="鏃у瘑鐮�" prop="oldPassword">
+ <el-input v-model="user.oldPassword" placeholder="璇疯緭鍏ユ棫瀵嗙爜" type="password" show-password />
+ </el-form-item>
+ <el-form-item label="鏂板瘑鐮�" prop="newPassword">
+ <el-input v-model="user.newPassword" placeholder="璇疯緭鍏ユ柊瀵嗙爜" type="password" show-password />
+ </el-form-item>
+ <el-form-item label="纭瀵嗙爜" prop="confirmPassword">
+ <el-input v-model="user.confirmPassword" placeholder="璇风‘璁ゆ柊瀵嗙爜" type="password" show-password/>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="submit">淇濆瓨</el-button>
+ <el-button type="danger" @click="close">鍏抽棴</el-button>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+import { updateUserPwd } from "@/api/system/user"
+
+const { proxy } = getCurrentInstance()
+
+const user = reactive({
+ oldPassword: undefined,
+ newPassword: undefined,
+ confirmPassword: undefined
+})
+
+const equalToPassword = (rule, value, callback) => {
+ if (user.newPassword !== value) {
+ callback(new Error("涓ゆ杈撳叆鐨勫瘑鐮佷笉涓�鑷�"))
+ } else {
+ callback()
+ }
+}
+
+const rules = ref({
+ oldPassword: [{ required: true, message: "鏃у瘑鐮佷笉鑳戒负绌�", trigger: "blur" }],
+ newPassword: [{ required: true, message: "鏂板瘑鐮佷笉鑳戒负绌�", trigger: "blur" }, { min: 6, max: 20, message: "闀垮害鍦� 6 鍒� 20 涓瓧绗�", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > \" ' \\\ |", trigger: "blur" }],
+ confirmPassword: [{ required: true, message: "纭瀵嗙爜涓嶈兘涓虹┖", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
+})
+
+/** 鎻愪氦鎸夐挳 */
+function submit() {
+ proxy.$refs.pwdRef.validate(valid => {
+ if (valid) {
+ updateUserPwd(user.oldPassword, user.newPassword).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ })
+ }
+ })
+}
+
+/** 鍏抽棴鎸夐挳 */
+function close() {
+ proxy.$tab.closePage()
+}
+</script>
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue
new file mode 100644
index 0000000..2594543
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userAvatar.vue
@@ -0,0 +1,168 @@
+<template>
+ <div class="user-info-head" @click="editCropper()">
+ <img :src="options.img" title="鐐瑰嚮涓婁紶澶村儚" class="img-circle img-lg" />
+ <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
+ <el-row>
+ <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+ <vue-cropper ref="cropper" :img="options.img" :info="true" :autoCrop="options.autoCrop"
+ :autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox"
+ :outputType="options.outputType" @realTime="realTime" v-if="visible" />
+ </el-col>
+ <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+ <div class="avatar-upload-preview">
+ <img :src="options.previews.url" :style="options.previews.img" />
+ </div>
+ </el-col>
+ </el-row>
+ <br />
+ <el-row>
+ <el-col :lg="2" :md="2">
+ <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
+ <el-button>
+ 閫夋嫨
+ <el-icon class="el-icon--right">
+ <Upload />
+ </el-icon>
+ </el-button>
+ </el-upload>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 2 }" :md="2">
+ <el-button icon="Plus" @click="changeScale(1)"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="Minus" @click="changeScale(-1)"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="RefreshRight" @click="rotateRight()"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 2, offset: 6 }" :md="2">
+ <el-button type="primary" @click="uploadImg()">鎻� 浜�</el-button>
+ </el-col>
+ </el-row>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import "vue-cropper/dist/index.css"
+import { VueCropper } from "vue-cropper"
+import { uploadAvatar } from "@/api/system/user"
+import useUserStore from "@/store/modules/user"
+
+const userStore = useUserStore()
+const { proxy } = getCurrentInstance()
+
+const open = ref(false)
+const visible = ref(false)
+const title = ref("淇敼澶村儚")
+
+//鍥剧墖瑁佸壀鏁版嵁
+const options = reactive({
+ img: userStore.avatar, // 瑁佸壀鍥剧墖鐨勫湴鍧�
+ autoCrop: true, // 鏄惁榛樿鐢熸垚鎴浘妗�
+ autoCropWidth: 200, // 榛樿鐢熸垚鎴浘妗嗗搴�
+ autoCropHeight: 200, // 榛樿鐢熸垚鎴浘妗嗛珮搴�
+ fixedBox: true, // 鍥哄畾鎴浘妗嗗ぇ灏� 涓嶅厑璁告敼鍙�
+ outputType: "png", // 榛樿鐢熸垚鎴浘涓篜NG鏍煎紡
+ filename: 'avatar', // 鏂囦欢鍚嶇О
+ previews: {} //棰勮鏁版嵁
+})
+
+/** 缂栬緫澶村儚 */
+function editCropper() {
+ open.value = true
+}
+
+/** 鎵撳紑寮瑰嚭灞傜粨鏉熸椂鐨勫洖璋� */
+function modalOpened() {
+ visible.value = true
+}
+
+/** 瑕嗙洊榛樿涓婁紶琛屼负 */
+function requestUpload() { }
+
+/** 鍚戝乏鏃嬭浆 */
+function rotateLeft() {
+ proxy.$refs.cropper.rotateLeft()
+}
+
+/** 鍚戝彸鏃嬭浆 */
+function rotateRight() {
+ proxy.$refs.cropper.rotateRight()
+}
+
+/** 鍥剧墖缂╂斁 */
+function changeScale(num) {
+ num = num || 1
+ proxy.$refs.cropper.changeScale(num)
+}
+
+/** 涓婁紶棰勫鐞� */
+function beforeUpload(file) {
+ if (file.type.indexOf("image/") == -1) {
+ proxy.$modal.msgError("鏂囦欢鏍煎紡閿欒锛岃涓婁紶鍥剧墖绫诲瀷,濡傦細JPG锛孭NG鍚庣紑鐨勬枃浠躲��")
+ } else {
+ const reader = new FileReader()
+ reader.readAsDataURL(file)
+ reader.onload = () => {
+ options.img = reader.result
+ options.filename = file.name
+ }
+ }
+}
+
+/** 涓婁紶鍥剧墖 */
+function uploadImg() {
+ proxy.$refs.cropper.getCropBlob(data => {
+ let formData = new FormData()
+ formData.append("avatarfile", data, options.filename)
+ uploadAvatar(formData).then(response => {
+ open.value = false
+ options.img = import.meta.env.VITE_APP_BASE_API + '/profile/' + response.imgUrl
+ userStore.avatar = options.img
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ visible.value = false
+ })
+ })
+}
+
+/** 瀹炴椂棰勮 */
+function realTime(data) {
+ options.previews = data
+}
+
+/** 鍏抽棴绐楀彛 */
+function closeDialog() {
+ options.img = userStore.avatar
+ options.visible = false
+}
+</script>
+
+<style lang='scss' scoped>
+.user-info-head {
+ position: relative;
+ display: inline-block;
+ height: 120px;
+}
+
+.user-info-head:hover:after {
+ content: "+";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ color: #eee;
+ background: rgba(0, 0, 0, 0.5);
+ font-size: 24px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ cursor: pointer;
+ line-height: 110px;
+ border-radius: 50%;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue
new file mode 100644
index 0000000..5099ffa
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysAdmin/user-manage/profile/userInfo.vue
@@ -0,0 +1,67 @@
+<template>
+ <el-form ref="userRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickName">
+ <el-input v-model="form.nickName" maxlength="30" />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input v-model="form.phonenumber" maxlength="11" />
+ </el-form-item>
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" maxlength="50" />
+ </el-form-item>
+ <el-form-item label="鎬у埆">
+ <el-radio-group v-model="form.sex">
+ <el-radio value="0">鐢�</el-radio>
+ <el-radio value="1">濂�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="submit">淇濆瓨</el-button>
+ <el-button type="danger" @click="close">鍏抽棴</el-button>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+import { updateUserProfile } from "@/api/system/user"
+
+const props = defineProps({
+ user: {
+ type: Object
+ }
+})
+
+const { proxy } = getCurrentInstance()
+
+const form = ref({})
+const rules = ref({
+ nickName: [{ required: true, message: "鐢ㄦ埛鏄电О涓嶈兘涓虹┖", trigger: "blur" }],
+ email: [{ required: true, message: "閭鍦板潃涓嶈兘涓虹┖", trigger: "blur" }, { type: "email", message: "璇疯緭鍏ユ纭殑閭鍦板潃", trigger: ["blur", "change"] }],
+ phonenumber: [{ required: true, message: "鎵嬫満鍙风爜涓嶈兘涓虹┖", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur" }],
+})
+
+/** 鎻愪氦鎸夐挳 */
+function submit() {
+ proxy.$refs.userRef.validate(valid => {
+ if (valid) {
+ updateUserProfile(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ props.user.phonenumber = form.value.phonenumber
+ props.user.email = form.value.email
+ })
+ }
+ })
+}
+
+/** 鍏抽棴鎸夐挳 */
+function close() {
+ proxy.$tab.closePage()
+}
+
+// 鍥炴樉褰撳墠鐧诲綍鐢ㄦ埛淇℃伅
+watch(() => props.user, user => {
+ if (user) {
+ form.value = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex }
+ }
+},{ immediate: true })
+</script>
diff --git a/src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue b/src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue
new file mode 100644
index 0000000..d5a90b0
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysMonitor/cache-monitor/index.vue
@@ -0,0 +1,134 @@
+<!--OA妯″潡锛氱紦瀛樼洃鎺�-->
+<template>
+ <div class="app-container">
+ <el-row :gutter="10">
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍩烘湰淇℃伅</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%">
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">Redis鐗堟湰</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_version }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">杩愯妯″紡</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.redis_mode == "standalone" ? "鍗曟満" : "闆嗙兢" }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">绔彛</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.tcp_port }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">瀹㈡埛绔暟</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.connected_clients }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">杩愯鏃堕棿(澶�)</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.uptime_in_days }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">浣跨敤鍐呭瓨</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.used_memory_human }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">浣跨敤CPU</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">鍐呭瓨閰嶇疆</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.maxmemory_human }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">AOF鏄惁寮�鍚�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.aof_enabled == "0" ? "鍚�" : "鏄�" }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">RDB鏄惁鎴愬姛</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.rdb_last_bgsave_status }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">Key鏁伴噺</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.dbSize">{{ cache.dbSize }} </div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">缃戠粶鍏ュ彛/鍑哄彛</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="cache.info">{{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><PieChart style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍛戒护缁熻</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <div ref="commandstats" style="height: 420px" />
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><Odometer style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍐呭瓨淇℃伅</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <div ref="usedmemory" style="height: 420px" />
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup name="Cache">
+import { getCache } from '@/api/monitor/cache'
+import * as echarts from 'echarts'
+
+const cache = ref([])
+const commandstats = ref(null)
+const usedmemory = ref(null)
+const { proxy } = getCurrentInstance()
+
+function getList() {
+ proxy.$modal.loading("姝e湪鍔犺浇缂撳瓨鐩戞帶鏁版嵁锛岃绋嶅�欙紒")
+ getCache().then(response => {
+ proxy.$modal.closeLoading()
+ cache.value = response.data
+
+ const commandstatsIntance = echarts.init(commandstats.value, "macarons")
+ commandstatsIntance.setOption({
+ tooltip: {
+ trigger: "item",
+ formatter: "{a} <br/>{b} : {c} ({d}%)"
+ },
+ series: [
+ {
+ name: "鍛戒护",
+ type: "pie",
+ roseType: "radius",
+ radius: [15, 95],
+ center: ["50%", "38%"],
+ data: response.data.commandStats,
+ animationEasing: "cubicInOut",
+ animationDuration: 1000
+ }
+ ]
+ })
+ const usedmemoryInstance = echarts.init(usedmemory.value, "macarons")
+ usedmemoryInstance.setOption({
+ tooltip: {
+ formatter: "{b} <br/>{a} : " + cache.value.info.used_memory_human
+ },
+ series: [
+ {
+ name: "宄板��",
+ type: "gauge",
+ min: 0,
+ max: 1000,
+ detail: {
+ formatter: cache.value.info.used_memory_human
+ },
+ data: [
+ {
+ value: parseFloat(cache.value.info.used_memory_human),
+ name: "鍐呭瓨娑堣��"
+ }
+ ]
+ }
+ ]
+ })
+ window.addEventListener("resize", () => {
+ commandstatsIntance.resize()
+ usedmemoryInstance.resize()
+ })
+ })
+}
+
+getList()
+</script>
+
diff --git a/src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue b/src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue
new file mode 100644
index 0000000..fe13414
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysMonitor/data-monitor/index.vue
@@ -0,0 +1,14 @@
+<!--OA妯″潡锛氭暟鎹洃鎺�-->
+<template>
+ <div>
+ <i-frame v-model:src="url"></i-frame>
+ </div>
+</template>
+
+<script setup>
+import iFrame from '@/components/iFrame'
+
+import { ref } from 'vue'
+
+const url = ref(import.meta.env.VITE_APP_BASE_API + '/druid/login.html')
+</script>
diff --git a/src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue b/src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue
new file mode 100644
index 0000000..053d55e
--- /dev/null
+++ b/src/views/officeProcessAutomation/SysMonitor/server-monitor/index.vue
@@ -0,0 +1,191 @@
+<!--OA妯″潡锛氭湇鍔″櫒鐩戞帶-->
+<template>
+ <div class="app-container">
+ <el-row :gutter="10">
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><Cpu style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">CPU</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <thead>
+ <tr>
+ <th class="el-table__cell is-leaf"><div class="cell">灞炴��</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鍊�</div></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鏍稿績鏁�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.cpuNum }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鐢ㄦ埛浣跨敤鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.used }}%</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">绯荤粺浣跨敤鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.sys }}%</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">褰撳墠绌洪棽鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.cpu">{{ server.cpu.free }}%</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="12" class="card-box">
+ <el-card>
+ <template #header><Tickets style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鍐呭瓨</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <thead>
+ <tr>
+ <th class="el-table__cell is-leaf"><div class="cell">灞炴��</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鍐呭瓨</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">JVM</div></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鎬诲唴瀛�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.total }}G</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.total }}M</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">宸茬敤鍐呭瓨</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.used}}G</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.used}}M</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鍓╀綑鍐呭瓨</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem">{{ server.mem.free }}G</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.free }}M</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">浣跨敤鐜�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.mem" :class="{'text-danger': server.mem.usage > 80}">{{ server.mem.usage }}%</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm" :class="{'text-danger': server.jvm.usage > 80}">{{ server.jvm.usage }}%</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">鏈嶅姟鍣ㄤ俊鎭�</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鏈嶅姟鍣ㄥ悕绉�</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">鎿嶄綔绯荤粺</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osName }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鏈嶅姟鍣↖P</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.computerIp }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">绯荤粺鏋舵瀯</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.osArch }}</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><CoffeeCup style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">Java铏氭嫙鏈轰俊鎭�</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;table-layout:fixed;">
+ <tbody>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">Java鍚嶇О</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.name }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">Java鐗堟湰</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.version }}</div></td>
+ </tr>
+ <tr>
+ <td class="el-table__cell is-leaf"><div class="cell">鍚姩鏃堕棿</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.startTime }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">杩愯鏃堕暱</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.runTime }}</div></td>
+ </tr>
+ <tr>
+ <td colspan="1" class="el-table__cell is-leaf"><div class="cell">瀹夎璺緞</div></td>
+ <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.home }}</div></td>
+ </tr>
+ <tr>
+ <td colspan="1" class="el-table__cell is-leaf"><div class="cell">椤圭洰璺緞</div></td>
+ <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.sys">{{ server.sys.userDir }}</div></td>
+ </tr>
+ <tr>
+ <td colspan="1" class="el-table__cell is-leaf"><div class="cell">杩愯鍙傛暟</div></td>
+ <td colspan="3" class="el-table__cell is-leaf"><div class="cell" v-if="server.jvm">{{ server.jvm.inputArgs }}</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="24" class="card-box">
+ <el-card>
+ <template #header><MessageBox style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">纾佺洏鐘舵��</span></template>
+ <div class="el-table el-table--enable-row-hover el-table--medium">
+ <table cellspacing="0" style="width: 100%;">
+ <thead>
+ <tr>
+ <th class="el-table__cell el-table__cell is-leaf"><div class="cell">鐩樼璺緞</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鏂囦欢绯荤粺</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鐩樼绫诲瀷</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鎬诲ぇ灏�</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">鍙敤澶у皬</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">宸茬敤澶у皬</div></th>
+ <th class="el-table__cell is-leaf"><div class="cell">宸茬敤鐧惧垎姣�</div></th>
+ </tr>
+ </thead>
+ <tbody v-if="server.sysFiles">
+ <tr v-for="(sysFile, index) in server.sysFiles" :key="index">
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.dirName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.sysTypeName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.typeName }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.total }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.free }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell">{{ sysFile.used }}</div></td>
+ <td class="el-table__cell is-leaf"><div class="cell" :class="{'text-danger': sysFile.usage > 80}">{{ sysFile.usage }}%</div></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import { getServer } from '@/api/monitor/server'
+import {onMounted} from "vue";
+
+const server = ref([])
+const { proxy } = getCurrentInstance()
+
+function getList() {
+ proxy.$modal.loading("姝e湪鍔犺浇鏈嶅姟鐩戞帶鏁版嵁锛岃绋嶅�欙紒")
+ getServer().then(response => {
+ server.value = response.data
+ proxy.$modal.closeLoading()
+ })
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/personnelManagement/analytics/index.vue b/src/views/personnelManagement/analytics/index.vue
new file mode 100644
index 0000000..6c5a1d6
--- /dev/null
+++ b/src/views/personnelManagement/analytics/index.vue
@@ -0,0 +1,701 @@
+<template>
+ <div class="app-container analytics-container" v-loading="loading">
+
+ <!-- 鍏抽敭鎸囨爣鍗$墖 -->
+ <el-row :gutter="20" class="metrics-cards">
+ <el-col :span="6" v-for="(item, index) in keyMetrics" :key="index">
+ <el-card class="metric-card" :class="item.type">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon :size="32">
+ <component :is="item.icon" />
+ </el-icon>
+ </div>
+ <div class="card-info">
+ <div class="card-number">
+ <el-skeleton-item v-if="loading" variant="text" style="width: 60px; height: 32px;" />
+ <span v-else>{{ item.value }}{{ item.unit }}</span>
+ </div>
+ <div class="card-label">{{ item.label }}</div>
+<!-- <div class="card-trend" :class="item.trend > 0 ? 'positive' : 'negative'" v-if="item.showTrend !== false">-->
+<!-- <el-icon>-->
+<!-- <component :is="item.trend > 0 ? 'ArrowUp' : 'ArrowDown'" />-->
+<!-- </el-icon>-->
+<!-- {{ Math.abs(item.trend) }}%-->
+<!-- </div>-->
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 鍥捐〃鍖哄煙 -->
+ <el-row :gutter="20" class="charts-section">
+ <!-- 鍛樺伐娴佸姩鐜囪秼鍔垮浘 -->
+ <el-col :span="12">
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span>鍛樺伐娴佸姩鐜囪秼鍔�</span>
+ <el-tag type="info">杩�12涓湀</el-tag>
+ </div>
+ </template>
+ <div class="chart-container">
+ <div ref="turnoverChartRef" class="chart"></div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <!-- 閮ㄩ棬浜哄憳鍒嗗竷 -->
+ <el-col :span="12">
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span>閮ㄩ棬浜哄憳鍒嗗竷</span>
+ <el-tag type="success">褰撳墠鐘舵��</el-tag>
+ </div>
+ </template>
+ <div class="chart-container">
+ <div ref="departmentChartRef" class="chart"></div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 绗簩琛屽浘琛� -->
+ <el-row :gutter="20" class="charts-section">
+ <!-- 鍛樺伐娴佸け鍘熷洜鍒嗘瀽 -->
+ <el-col :span="12">
+ <el-card class="chart-card">
+ <template #header>
+ <div class="card-header">
+ <span>鍛樺伐娴佸け鍘熷洜鍒嗘瀽</span>
+ <el-tag type="danger">骞村害缁熻</el-tag>
+ </div>
+ </template>
+ <div class="chart-container">
+ <div ref="attritionChartRef" class="chart"></div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import * as echarts from 'echarts'
+import {listDept} from "@/api/system/dept.js";
+import {
+ findStaffAnalysisMonthlyTurnoverRateFor12Months,
+ findStaffLeaveReasonAnalysis,
+ findStaffAnalysisTotalStatistic
+} from "@/api/personnelManagement/staffAnalytics.js";
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const autoRefreshEnabled = ref(true)
+const autoRefreshInterval = ref(null)
+
+// 鍥捐〃寮曠敤
+const turnoverChartRef = ref(null)
+const departmentChartRef = ref(null)
+const staffingChartRef = ref(null)
+const attritionChartRef = ref(null)
+
+// 鍥捐〃瀹炰緥
+let turnoverChart = null
+let departmentChart = null
+let staffingChart = null
+let attritionChart = null
+
+// 鑷姩鏇存柊闂撮殧锛�10鍒嗛挓锛�
+const AUTO_REFRESH_INTERVAL = 10 * 60 * 1000
+
+// 鍏抽敭鎸囨爣鏁版嵁
+const keyMetrics = ref([
+ {
+ label: '鍛樺伐娴佸姩鐜�',
+ value: 0,
+ unit: '%',
+ icon: 'TrendCharts',
+ type: 'primary',
+ trend: 0
+ },
+ {
+ label: '鍛樺伐娴佸け鐜�',
+ value: 0,
+ unit: '%',
+ icon: 'User',
+ type: 'danger',
+ trend: 0
+ },
+ {
+ label: '鍦ㄨ亴鍛樺伐鏁�',
+ value: 0,
+ unit: '浜�',
+ icon: 'PieChart',
+ type: 'warning',
+ trend: 0,
+ showTrend: false
+ }
+])
+
+// 閮ㄩ棬鏁版嵁
+const departmentData = ref([])
+// 鍛樺伐娴佸け鍘熷洜鍒嗘瀽鏁版嵁
+const staffLeaveReasons = ref([])
+// 12涓湀鍛樺伐娴佸姩娴佸け鐜囧垎鏋愭暟鎹�
+const turnoverRateStatistics = ref([])
+
+// 鑾峰彇閮ㄩ棬鏁版嵁
+const getDepartmentData = async () => {
+ try {
+ const res = await listDept()
+ if (res && res.data) {
+ departmentData.value = res.data
+ }
+ } catch (error) {
+ console.error('鑾峰彇閮ㄩ棬鏁版嵁澶辫触:', error)
+ }
+}
+
+const getStaffLeaveReasonAnalysis = async () => {
+ try {
+ const res = await findStaffLeaveReasonAnalysis()
+ if (res && res.data) {
+ staffLeaveReasons.value = res.data || []
+ }
+ } catch (error) {
+ console.error('鑾峰彇鍛樺伐娴佸け鍘熷洜鍒嗘瀽澶辫触:', error)
+ }
+}
+
+// 淇敼涓鸿繑鍥濸romise鐨勫紓姝ュ嚱鏁�
+const getMonthlyTurnoverRateFor12Months = async () => {
+ try {
+ const res = await findStaffAnalysisMonthlyTurnoverRateFor12Months()
+ if (res && res.data) {
+ turnoverRateStatistics.value = res.data || []
+ }
+ } catch (error) {
+ console.error('鑾峰彇12涓湀鍛樺伐娴佸姩娴佸け鐜囧垎鏋愭暟鎹け璐�:', error)
+ }
+}
+
+const getStaffAnalysisTotalStatistic = async () => {
+ try {
+ const res = await findStaffAnalysisTotalStatistic()
+ if (res && res.data) {
+ keyMetrics.value[0].value = res.data.totalFlowRate || 0
+ keyMetrics.value[1].value = res.data.totalTurnoverRate || 0
+ keyMetrics.value[2].value = res.data.currentOnJobCount || 0
+ }
+ } catch (error) {
+ console.error('鑾峰彇鍛樺伐鍒嗘瀽鎬荤粺璁℃暟鎹け璐�:', error)
+ }
+}
+
+// 鍚姩鑷姩鍒锋柊
+const startAutoRefresh = () => {
+ if (autoRefreshInterval.value) {
+ clearInterval(autoRefreshInterval.value)
+ }
+ if (autoRefreshEnabled.value) {
+ autoRefreshInterval.value = setInterval(() => {
+ refreshData()
+ }, AUTO_REFRESH_INTERVAL)
+ }
+}
+
+// 鍋滄鑷姩鍒锋柊
+const stopAutoRefresh = () => {
+ if (autoRefreshInterval.value) {
+ clearInterval(autoRefreshInterval.value)
+ autoRefreshInterval.value = null
+ }
+}
+
+// 鍒囨崲鑷姩鍒锋柊鐘舵��
+const toggleAutoRefresh = (value) => {
+ if (value) {
+ startAutoRefresh()
+ } else {
+ stopAutoRefresh()
+ }
+}
+
+// 淇敼涓哄紓姝ュ嚱鏁帮紝纭繚鏁版嵁鍔犺浇瀹屾垚鍚庡啀娓叉煋鍥捐〃
+const refreshData = async () => {
+ try {
+ loading.value = true
+
+ // 绛夊緟鎵�鏈夋暟鎹姞杞藉畬鎴�
+ await Promise.all([
+ getDepartmentData(),
+ getStaffLeaveReasonAnalysis(),
+ getMonthlyTurnoverRateFor12Months(),
+ getStaffAnalysisTotalStatistic()
+ ])
+
+ await nextTick()
+ renderAllCharts()
+
+ if (!autoRefreshEnabled.value) {
+ ElMessage.success('鏁版嵁鍒锋柊鎴愬姛')
+ }
+ } catch (error) {
+ console.error('鏁版嵁鍒锋柊澶辫触:', error)
+ ElMessage.error('鏁版嵁鍒锋柊澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鍒濆鍖栧浘琛�
+const initCharts = () => {
+ setTimeout(() => {
+ if (turnoverChartRef.value) {
+ turnoverChart = echarts.init(turnoverChartRef.value)
+ }
+ if (departmentChartRef.value) {
+ departmentChart = echarts.init(departmentChartRef.value)
+ }
+ if (staffingChartRef.value) {
+ staffingChart = echarts.init(staffingChartRef.value)
+ }
+ if (attritionChartRef.value) {
+ attritionChart = echarts.init(attritionChartRef.value)
+ }
+
+ // 鍒濆鍖栨椂涔熷厛鍔犺浇鏁版嵁鍐嶆覆鏌撳浘琛�
+ refreshData()
+ }, 300)
+}
+
+// 娓叉煋鎵�鏈夊浘琛�
+const renderAllCharts = () => {
+ renderTurnoverChart()
+ renderDepartmentChart()
+ renderStaffingChart()
+ renderAttritionChart()
+}
+
+// 淇敼涓轰娇鐢ˋPI杩斿洖鐨勫疄闄呮暟鎹�
+const renderTurnoverChart = () => {
+ if (!turnoverChart) return
+
+ // 浣跨敤API杩斿洖鐨勫疄闄呮暟鎹�
+ const months = turnoverRateStatistics.value.map(item => item.month)
+ const turnoverData = turnoverRateStatistics.value.map(item => item.flowRate || 0)
+ const attritionData = turnoverRateStatistics.value.map(item => item.turnoverRate || 0)
+
+ const option = {
+ title: {
+ text: '鍛樺伐娴佸姩鐜囪秼鍔�',
+ left: 'center',
+ textStyle: { fontSize: 16, fontWeight: 'normal' }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: { type: 'cross' }
+ },
+ legend: {
+ data: ['娴佸姩鐜�', '娴佸け鐜�'],
+ bottom: 10
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ top: '15%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: months,
+ boundaryGap: false
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: { formatter: '{value}%' }
+ },
+ series: [
+ {
+ name: '娴佸姩鐜�',
+ type: 'line',
+ data: turnoverData,
+ smooth: true,
+ lineStyle: { color: '#409EFF' },
+ itemStyle: { color: '#409EFF' }
+ },
+ {
+ name: '娴佸け鐜�',
+ type: 'line',
+ data: attritionData,
+ smooth: true,
+ lineStyle: { color: '#F56C6C' },
+ itemStyle: { color: '#F56C6C' }
+ }
+ ]
+ }
+
+ turnoverChart.setOption(option)
+}
+
+// 娓叉煋閮ㄩ棬浜哄憳鍒嗗竷鍥�
+const renderDepartmentChart = () => {
+ if (!departmentChart) return
+
+ const data = departmentData.value.map(item => ({
+ name: item.deptName,
+ value: item.staffCount
+ }))
+
+ const option = {
+ title: {
+ text: '閮ㄩ棬浜哄憳鍒嗗竷',
+ left: 'center',
+ textStyle: { fontSize: 16, fontWeight: 'normal' }
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{a} <br/>{b}: {c}浜� ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left',
+ top: 'middle'
+ },
+ series: [
+ {
+ name: '浜哄憳鏁伴噺',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ center: ['60%', '50%'],
+ data: data,
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ }
+ }
+ ]
+ }
+
+ departmentChart.setOption(option)
+}
+
+// 娓叉煋缂栧埗杈炬垚鐜囧浘
+const renderStaffingChart = () => {
+ if (!staffingChart) return
+
+ const departments = departmentData.value.map(item => item.deptName)
+ const rates = departmentData.value.map(item => item.staffingRate)
+
+ const option = {
+ title: {
+ text: '缂栧埗杈炬垚鐜�',
+ left: 'center',
+ textStyle: { fontSize: 16, fontWeight: 'normal' }
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' }
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '15%',
+ top: '15%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: departments,
+ axisLabel: { rotate: 45 }
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: { formatter: '{value}%' },
+ max: 100
+ },
+ series: [
+ {
+ name: '杈炬垚鐜�',
+ type: 'bar',
+ data: rates,
+ itemStyle: {
+ color: function(params) {
+ const value = params.value
+ if (value >= 90) return '#67C23A'
+ if (value >= 80) return '#E6A23C'
+ return '#F56C6C'
+ }
+ }
+ }
+ ]
+ }
+
+ staffingChart.setOption(option)
+}
+
+// 娓叉煋鍛樺伐娴佸け鍘熷洜鍒嗘瀽鍥�
+const renderAttritionChart = () => {
+ if (!attritionChart) return
+
+ const reasons = staffLeaveReasons.value.map(item => item.reasonText)
+ const data = staffLeaveReasons.value.map(item => item.count)
+
+ const option = {
+ title: {
+ text: '鍛樺伐娴佸け鍘熷洜鍒嗘瀽',
+ left: 'center',
+ textStyle: { fontSize: 16, fontWeight: 'normal' }
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{a} <br/>{b}: {c}浜� ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left',
+ top: 'middle'
+ },
+ series: [
+ {
+ name: '娴佸け浜烘暟',
+ type: 'pie',
+ radius: '50%',
+ center: ['60%', '50%'],
+ data: reasons.map((reason, index) => ({
+ name: reason,
+ value: data[index]
+ })),
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ }
+ }
+ ]
+ }
+
+ attritionChart.setOption(option)
+}
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ initCharts()
+ startAutoRefresh()
+})
+
+onUnmounted(() => {
+ stopAutoRefresh()
+})
+</script>
+
+<style scoped>
+.analytics-container {
+ padding: 20px;
+ min-height: 100vh;
+}
+
+.page-header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 12px;
+ color: white;
+}
+
+.page-header h2 {
+ color: white;
+ margin-bottom: 10px;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.page-header p {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 14px;
+ margin: 0 0 15px 0;
+}
+
+.header-controls {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+}
+
+.refresh-btn {
+ margin-left: 20px;
+}
+
+.metrics-cards {
+ margin-bottom: 30px;
+}
+
+.metric-card {
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ border: none;
+ overflow: hidden;
+}
+
+.metric-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+}
+
+.metric-card.primary {
+ border-left: 4px solid #409EFF;
+ background: linear-gradient(135deg, #409EFF 0%, #36a3f7 100%);
+}
+
+.metric-card.danger {
+ border-left: 4px solid #F56C6C;
+ background: linear-gradient(135deg, #F56C6C 0%, #f78989 100%);
+}
+
+.metric-card.success {
+ border-left: 4px solid #67C23A;
+ background: linear-gradient(135deg, #67C23A 0%, #85ce61 100%);
+}
+
+.metric-card.warning {
+ border-left: 4px solid #E6A23C;
+ background: linear-gradient(135deg, #E6A23C 0%, #ebb563 100%);
+}
+
+.card-content {
+ display: flex;
+ align-items: center;
+ padding: 20px;
+}
+
+.card-icon {
+ margin-right: 20px;
+ color: white;
+}
+
+.card-info {
+ flex: 1;
+}
+
+.card-number {
+ font-size: 32px;
+ font-weight: 600;
+ color: white;
+ margin-bottom: 5px;
+}
+
+.card-label {
+ font-size: 14px;
+ color: rgba(255, 255, 255, 0.9);
+ margin-bottom: 5px;
+}
+
+.card-trend {
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.card-trend.positive {
+ color: #67C23A;
+}
+
+.card-trend.negative {
+ color: #F56C6C;
+}
+
+.charts-section {
+ margin-bottom: 30px;
+}
+
+.chart-card {
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ border: none;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ color: #303133;
+ padding: 15px 20px;
+ border-bottom: 1px solid #ebeef5;
+}
+
+.chart-container {
+ height: 350px;
+ padding: 20px;
+}
+
+.chart {
+ width: 100%;
+ height: 100%;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .analytics-container {
+ padding: 10px;
+ }
+
+ .page-header {
+ padding: 15px;
+ }
+
+ .page-header h2 {
+ font-size: 24px;
+ }
+
+ .header-controls {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .refresh-btn {
+ margin-left: 0;
+ }
+
+ .metrics-cards .el-col {
+ margin-bottom: 15px;
+ }
+
+ .charts-section .el-col {
+ margin-bottom: 20px;
+ }
+
+ .chart-container {
+ height: 300px;
+ }
+}
+
+@media (max-width: 480px) {
+ .page-header h2 {
+ font-size: 20px;
+ }
+
+ .card-number {
+ font-size: 24px;
+ }
+
+ .chart-container {
+ height: 250px;
+ }
+}
+</style>
diff --git a/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue b/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
new file mode 100644
index 0000000..b17b234
--- /dev/null
+++ b/src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue
@@ -0,0 +1,511 @@
+<template>
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="700px"
+ :close-on-click-modal="false">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120px"
+ class="mt8">
+ <!-- 閮ㄩ棬閫夋嫨 -->
+ <el-form-item label="閮ㄩ棬"
+ prop="sysDeptId">
+ <el-tree-select v-model="form.sysDeptId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ check-strictly
+ style="width: 100%"
+ :disabled="['edit', 'view'].includes(operationType)" />
+ </el-form-item>
+ <!-- 鍦扮偣淇℃伅 -->
+ <!-- <el-form-item label="鍦扮偣鍚嶇О"
+ prop="locationName">
+ <el-input v-model="form.locationName"
+ placeholder="璇疯緭鍏ュ湴鐐瑰悕绉�"
+ :disabled="operationType === 'view'" />
+ </el-form-item> -->
+ <!-- 鎵撳崱鑼冨洿 -->
+ <el-form-item label="鐝"
+ prop="shift">
+ <el-select v-model="form.shift"
+ placeholder="璇烽�夋嫨鐝"
+ :disabled="operationType === 'view'"
+ style="width: 100%">
+ <el-option v-for="item in shifts_list"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎵撳崱鑼冨洿(m)"
+ prop="radius">
+ <el-input-number v-model="form.radius"
+ :min="10"
+ :max="1000"
+ :step="10"
+ placeholder="璇疯緭鍏ユ墦鍗¤寖鍥�"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ <!-- 楂樺痉鍦板浘閫夋嫨 -->
+ <el-form-item label="鎵撳崱浣嶇疆"
+ prop="longitude">
+ <div class="map-container">
+ <div class="map-header"
+ style="margin-bottom: 10px">
+ <!-- <el-button @click="getCurrentLocation">
+ <el-icon>
+ <Position />
+ </el-icon>
+ 褰撳墠浣嶇疆
+ </el-button> -->
+ <!-- <span style="margin-left: 10px; color: #909399;font-size: 12px;">鐐瑰嚮鍦板浘閫夋嫨浣嶇疆</span> -->
+ </div>
+ <div id="map-container"
+ class="map"
+ ref="mapContainer"></div>
+ <div class="coordinates-info mt10">
+ <el-input v-model="form.longitude"
+ readonly
+ placeholder="缁忓害"
+ style="width: 140px; margin-right: 10px" />
+ <el-input v-model="form.latitude"
+ readonly
+ placeholder="绾害"
+ style="width: 140px; margin-right: 10px" />
+ <!-- <el-input v-model="form.locationName"
+ placeholder="鍦扮偣鍚嶇О"
+ style="width: calc(100% - 290px)" /> -->
+ </div>
+ </div>
+ </el-form-item>
+ <el-form-item label="鍦扮偣鍚嶇О"
+ prop="locationName">
+ <el-input v-model="form.locationName"
+ :disabled="operationType === 'view'"
+ placeholder="璇疯緭鍏ュ湴鐐瑰悕绉�" />
+ </el-form-item>
+ <!-- 涓婁笅鐝椂闂� -->
+ <el-form-item label="涓婄彮鏃堕棿"
+ prop="startAt">
+ <el-time-picker v-model="form.startAt"
+ format="HH:mm"
+ value-format="HH:mm"
+ placeholder="璇烽�夋嫨涓婄彮鏃堕棿"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ <el-form-item label="涓嬬彮鏃堕棿"
+ prop="endAt">
+ <el-time-picker v-model="form.endAt"
+ format="HH:mm"
+ value-format="HH:mm"
+ :picker-options="{
+ minTime: form.startAt
+ }"
+ placeholder="璇烽�夋嫨涓嬬彮鏃堕棿"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary" @click="submitForm" v-if="operationType !== 'view'">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, watch, onMounted, nextTick } from "vue";
+ import { ElMessage } from "element-plus";
+ import { Position } from "@element-plus/icons-vue";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import { addAttendanceRule } from "@/api/personnelManagement/attendanceRules.js";
+ import { useDict } from "@/utils/dict";
+
+ const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false,
+ },
+ operationType: {
+ type: String,
+ default: "add",
+ },
+ row: {
+ type: Object,
+ default: () => ({}),
+ },
+ });
+ // const pickerOptions = ref({ minTime: form.value.startAt });
+
+ const emit = defineEmits(["update:modelValue", "close"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const dialogTitle = computed(() => {
+ if (props.operationType === "add") return "鏂板鐝";
+ if (props.operationType === "edit") return "缂栬緫鐝";
+ return "鏌ョ湅鐝";
+ });
+
+ // 鑾峰彇鐝瀛楀吀鍊�
+ const { shifts_list } = useDict("shifts_list");
+
+ // 琛ㄥ崟鏁版嵁
+ const formRef = ref();
+ const form = reactive({
+ id: "",
+ sysDeptId: "",
+ locationName: "",
+ longitude: "",
+ latitude: "",
+ radius: 100,
+ startAt: "09:00",
+ endAt: "18:00",
+ shift: "",
+ });
+
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ sysDeptId: [{ required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" }],
+ locationName: [
+ { required: true, message: "璇疯緭鍏ュ湴鐐瑰悕绉�", trigger: "blur" },
+ ],
+ longitude: [{ required: true, message: "璇烽�夋嫨鎵撳崱浣嶇疆", trigger: "blur" }],
+ latitude: [{ required: true, message: "璇烽�夋嫨鎵撳崱浣嶇疆", trigger: "blur" }],
+ shift: [{ required: true, message: "璇烽�夋嫨鐝", trigger: "change" }],
+ radius: [{ required: true, message: "璇疯緭鍏ユ墦鍗¤寖鍥�", trigger: "blur" }],
+ startAt: [{ required: true, message: "璇烽�夋嫨涓婄彮鏃堕棿", trigger: "change" }],
+ endAt: [
+ { required: true, message: "璇烽�夋嫨涓嬬彮鏃堕棿", trigger: "change" },
+ {
+ validator: (rule, value, callback) => {
+ if (form.startAt && value) {
+ const startParts = form.startAt.split(":");
+ const endParts = value.split(":");
+ const startTime =
+ parseInt(startParts[0]) * 60 + parseInt(startParts[1]);
+ const endTime = parseInt(endParts[0]) * 60 + parseInt(endParts[1]);
+ if (endTime <= startTime) {
+ callback(new Error("涓嬬彮鏃堕棿涓嶈兘鏃╀簬涓婄彮鏃堕棿"));
+ } else {
+ callback();
+ }
+ } else {
+ callback();
+ }
+ },
+ trigger: "change",
+ },
+ ],
+ };
+
+ // 閮ㄩ棬閫夐」
+ const deptOptions = ref([]);
+
+ // 鍦板浘鐩稿叧
+ const mapContainer = ref(null);
+ let map = null;
+ let marker = null;
+ let circle = null;
+
+ // 鑾峰彇閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ // 杩囨护绂佺敤鐨勯儴闂�
+ const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+
+ // 鍒濆鍖栧湴鍥�
+ const initMap = () => {
+ nextTick(() => {
+ if (window.AMap && mapContainer.value) {
+ // 鍒濆鍖栧湴鍥�
+ map = new window.AMap.Map(mapContainer.value, {
+ zoom: 16,
+ center: [116.397428, 39.90923], // 榛樿鍖椾含
+ });
+
+ // 娣诲姞鎺т欢
+ window.AMap.plugin(["AMap.ToolBar", "AMap.Scale"], function () {
+ map.addControl(new window.AMap.ToolBar());
+ map.addControl(new window.AMap.Scale());
+ });
+
+ // 娣诲姞鏍囪
+ marker = new window.AMap.Marker({
+ position: [116.397428, 39.90923],
+ draggable: true,
+ cursor: "move",
+ title: "鎷栨嫿瀹氫綅",
+ });
+ map.add(marker);
+
+ // 娣诲姞鍦嗗舰鑼冨洿
+ circle = new window.AMap.Circle({
+ center: [116.397428, 39.90923],
+ radius: form.radius,
+ strokeColor: "#3366FF",
+ strokeOpacity: 0.8,
+ strokeWeight: 2,
+ fillColor: "#3366FF",
+ fillOpacity: 0.2,
+ });
+ map.add(circle);
+
+ // 鐩戝惉鏍囪鎷栨嫿
+ marker.on("dragend", e => {
+ const position = e.lnglat;
+ const lng = position.getLng();
+ const lat = position.getLat();
+ form.longitude = lng;
+ form.latitude = lat;
+ updateCircle(position);
+ });
+
+ // 鐩戝惉鏍囪鎷栨嫿寮�濮�
+ marker.on("dragstart", () => {
+ map.setDefaultCursor("move");
+ });
+
+ // 鐩戝惉鏍囪鎷栨嫿缁撴潫
+ marker.on("dragend", () => {
+ map.setDefaultCursor("default");
+ });
+
+ // 鐩戝惉鍦板浘鐐瑰嚮
+ map.on("click", e => {
+ const position = e.lnglat;
+ const lng = position.getLng();
+ const lat = position.getLat();
+ form.longitude = lng;
+ form.latitude = lat;
+ updateMarker(position);
+ updateCircle(position);
+ });
+
+ // 灏濊瘯鑾峰彇褰撳墠浣嶇疆骞惰缃负鍦板浘涓績
+ if (navigator.geolocation && !form.longitude && !form.latitude) {
+ navigator.geolocation.getCurrentPosition(
+ position => {
+ console.log("鑾峰彇鍒板綋鍓嶄綅缃�:", position);
+ const { longitude, latitude } = position.coords;
+ const currentPosition = [longitude, latitude];
+ map.setCenter(currentPosition);
+ updateMarker(currentPosition);
+ updateCircle(currentPosition);
+ form.longitude = longitude;
+ form.latitude = latitude;
+ },
+ error => {
+ console.log("鑾峰彇浣嶇疆澶辫触锛屼娇鐢ㄩ粯璁や綅缃�");
+ }
+ );
+ } else if (form.longitude && form.latitude) {
+ // 濡傛灉鏈夋暟鎹紝璁剧疆鍒板湴鍥�
+ const position = [form.longitude, form.latitude];
+ map.setCenter(position);
+ updateMarker(position);
+ updateCircle(position);
+ }
+ }
+ });
+ };
+
+ // 鏇存柊鏍囪浣嶇疆
+ const updateMarker = position => {
+ if (marker) {
+ marker.setPosition(position);
+ }
+ };
+
+ // 鏇存柊鍦嗗舰鑼冨洿
+ const updateCircle = position => {
+ if (circle) {
+ circle.setCenter(position);
+ circle.setRadius(form.radius);
+ }
+ };
+
+ // 鑾峰彇褰撳墠浣嶇疆
+ const getCurrentLocation = () => {
+ if (navigator.geolocation) {
+ navigator.geolocation.getCurrentPosition(
+ position => {
+ const { longitude, latitude } = position.coords;
+ form.longitude = longitude;
+ form.latitude = latitude;
+ if (map) {
+ map.setCenter([longitude, latitude]);
+ updateMarker([longitude, latitude]);
+ updateCircle([longitude, latitude]);
+ }
+
+ // 閫嗗湴鐞嗙紪鐮佽幏鍙栧湴鍧�
+ if (window.AMap) {
+ // 鍔犺浇Geocoder鎻掍欢
+ window.AMap.plugin("AMap.Geocoder", function () {
+ const geocoder = new window.AMap.Geocoder();
+ geocoder.getAddress([longitude, latitude], (status, result) => {
+ if (status === "complete" && result.regeocode) {
+ form.locationName = result.regeocode.formattedAddress;
+ }
+ });
+ });
+ }
+ },
+ error => {
+ ElMessage.error("鑾峰彇浣嶇疆澶辫触锛岃鎵嬪姩閫夋嫨");
+ }
+ );
+ } else {
+ ElMessage.error("娴忚鍣ㄤ笉鏀寔鍦扮悊瀹氫綅");
+ }
+ };
+
+ // 鐩戝惉鍗婂緞鍙樺寲
+ watch(
+ () => form.radius,
+ newValue => {
+ if (circle) {
+ circle.setRadius(newValue);
+ }
+ }
+ );
+
+ // 鐩戝惉涓婄彮鏃堕棿鍙樺寲锛岃Е鍙戜笅鐝椂闂存牎楠�
+ watch(
+ () => form.startAt,
+ () => {
+ if (formRef.value && form.endAt) {
+ formRef.value.validateField("endAt");
+ }
+ }
+ );
+
+ // 鐩戝惉寮圭獥鏄剧ず
+ watch(
+ () => dialogVisible.value,
+ newValue => {
+ if (newValue) {
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form, {
+ id: "",
+ sysDeptId: "",
+ locationName: "",
+ longitude: "",
+ latitude: "",
+ radius: 100,
+ startAt: "09:00",
+ endAt: "18:00",
+ shift: "",
+ });
+
+ // 濡傛灉鏄紪杈戞垨鏌ョ湅锛屽~鍏呮暟鎹�
+ if (props.operationType !== "add" && props.row.id) {
+ // 澶勭悊鏃堕棿鏍煎紡锛岀‘淇濇槸HH:mm鏍煎紡
+ const rowData = { ...props.row };
+ if (rowData.startAt && rowData.startAt.includes(":")) {
+ rowData.startAt = rowData.startAt.split(":").slice(0, 2).join(":");
+ }
+ if (rowData.endAt && rowData.endAt.includes(":")) {
+ rowData.endAt = rowData.endAt.split(":").slice(0, 2).join(":");
+ }
+ Object.assign(form, rowData);
+ }
+
+ // 鍒濆鍖栧湴鍥�
+ setTimeout(() => {
+ initMap();
+ }, 100);
+ }
+ }
+ );
+
+ // 鎻愪氦琛ㄥ崟
+ const submitForm = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ const submitData = {
+ ...form,
+ // 杞崲鏃堕棿鏍煎紡锛岀‘淇濆彧淇濈暀鏃跺垎閮ㄥ垎
+ startAt: form.startAt
+ ? `${form.startAt.split(":").slice(0, 2).join(":")}`
+ : null,
+ endAt: form.endAt
+ ? `${form.endAt.split(":").slice(0, 2).join(":")}`
+ : null,
+ };
+
+ if (props.operationType === "add") {
+ addAttendanceRule(submitData).then(() => {
+ ElMessage.success("鏂板鎴愬姛");
+ emit("close");
+ });
+ } else if (props.operationType === "edit") {
+ addAttendanceRule(submitData).then(() => {
+ ElMessage.success("鏇存柊鎴愬姛");
+ emit("close");
+ });
+ }
+ }
+ });
+ };
+
+ // 鍒濆鍖�
+ onMounted(() => {
+ fetchDeptOptions();
+ });
+</script>
+
+<style scoped lang="scss">
+ .map-container {
+ width: 100%;
+ }
+
+ .map {
+ width: 100%;
+ height: 400px;
+ border: 1px solid #e4e7ed;
+ }
+
+ .coordinates-info {
+ display: flex;
+ gap: 10px;
+ }
+
+ .coordinates-display {
+ padding: 10px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ }
+
+ .mt10 {
+ margin-top: 10px;
+ }
+
+ .mt8 {
+ margin-top: 8px;
+ }
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue b/src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue
new file mode 100644
index 0000000..c47dd2e
--- /dev/null
+++ b/src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue
@@ -0,0 +1,314 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬鍜屾搷浣滄寜閽� -->
+ <div class="page-header">
+ <div class="title">鐝閰嶇疆</div>
+ <div class="actions">
+ <el-button type="primary"
+ @click="openForm('add')">
+ <el-icon>
+ <Plus />
+ </el-icon>
+ 鏂板鐝
+ </el-button>
+ </div>
+ </div>
+ <!-- 鏌ヨ鏉′欢 -->
+ <!-- <el-form :model="searchForm"
+ :inline="true"
+ class="search-form mb16">
+ <el-form-item label="閮ㄩ棬锛�"
+ prop="countId">
+ <el-tree-select v-model="searchForm.countId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ check-strictly
+ style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="鍦扮偣锛�"
+ prop="locationName">
+ <el-input v-model="searchForm.locationName"
+ placeholder="璇疯緭鍏ュ湴鐐瑰悕绉�"
+ clearable
+ style="width: 200px" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="fetchData">
+ <el-icon>
+ <Search />
+ </el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">
+ <el-icon>
+ <Refresh />
+ </el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form> -->
+ <!-- 鐝鍒楄〃 -->
+ <el-card shadow="never"
+ class="mb16">
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ height="calc(100vh - 18.5em)"
+ style="width: 100%"
+ row-key="id">
+ <el-table-column type="index"
+ label="搴忓彿"
+ width="60"
+ align="center" />
+ <el-table-column label="閮ㄩ棬">
+ <template #default="scope">
+ {{ getDeptNameById(scope.row.sysDeptId) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鐝">
+ <template #default="scope">
+ {{ getShiftNameByValue(scope.row.shift) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="locationName"
+ label="鍦扮偣鍚嶇О" />
+ <el-table-column prop="longitude"
+ label="缁忓害" />
+ <el-table-column prop="latitude"
+ label="绾害" />
+ <el-table-column prop="radius"
+ label="鎵撳崱鑼冨洿(m)" />
+ <el-table-column prop="startAt"
+ label="涓婄彮鏃堕棿">
+ <template #default="scope">
+ {{ scope.row.startAt }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="endAt"
+ label="涓嬬彮鏃堕棿">
+ <template #default="scope">
+ {{ scope.row.endAt }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="180"
+ fixed="right"
+ align="center">
+ <template #default="scope">
+ <el-button type="primary"
+ link
+ @click="openForm('edit', scope.row)">缂栬緫</el-button>
+ <el-button type="danger"
+ link
+ @click="handleDelete(scope.row.id)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination :total="page.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ class="mt10" />
+ </el-card>
+ <!-- 鏂板/缂栬緫鐝寮圭獥 -->
+ <rule-form ref="ruleFormRef"
+ v-model="dialogVisible"
+ :operation-type="operationType"
+ :row="currentRow"
+ @close="dialogVisible = false; fetchData()" />
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import {
+ Plus,
+ Edit,
+ Delete,
+ Search,
+ Refresh,
+ ArrowLeft,
+ } from "@element-plus/icons-vue";
+ import Pagination from "@/components/Pagination/index.vue";
+ import RuleForm from "./components/form.vue";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import {
+ getAttendanceRules,
+ deleteAttendanceRule,
+ } from "@/api/personnelManagement/attendanceRules.js";
+ import { useDict } from "@/utils/dict";
+
+ const { proxy } = getCurrentInstance();
+
+ // 琛ㄦ牸鏁版嵁
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+
+ // 鍒嗛〉鍙傛暟
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ // 鏌ヨ琛ㄥ崟
+ const searchForm = reactive({
+ countId: "",
+ locationName: "",
+ });
+
+ // 閮ㄩ棬閫夐」
+ const deptOptions = ref([]);
+ // 鑾峰彇鐝瀛楀吀鍊�
+ const { shifts_list } = useDict("shifts_list");
+
+ // 寮圭獥鎺у埗
+ const dialogVisible = ref(false);
+ const operationType = ref("add");
+ const currentRow = ref({});
+ const ruleFormRef = ref();
+
+ // 鏍煎紡鍖栨椂闂�
+ const formatTime = timestamp => {
+ if (!timestamp) return "";
+ const date = new Date(timestamp);
+ return `${String(date.getHours()).padStart(2, "0")}:${String(
+ date.getMinutes()
+ ).padStart(2, "0")}`;
+ };
+
+ // 鏍规嵁鐝鍊艰幏鍙栫彮娆″悕绉�
+ const getShiftNameByValue = value => {
+ if (!value) return "";
+ const shift = shifts_list.value.find(item => item.value === value);
+ return shift ? shift.label : value;
+ };
+
+ // 鑾峰彇閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ // 杩囨护绂佺敤鐨勯儴闂�
+ const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+
+ // 鏍规嵁閮ㄩ棬ID鏌ユ壘閮ㄩ棬鍚嶇О
+ const getDeptNameById = (deptId, deptList = deptOptions.value) => {
+ for (const dept of deptList) {
+ if (dept.id === deptId) {
+ return dept.label;
+ }
+ if (dept.children && dept.children.length) {
+ const name = getDeptNameById(deptId, dept.children);
+ if (name) {
+ return name;
+ }
+ }
+ }
+ return "";
+ };
+
+ // 鏌ヨ鐝鍒楄〃
+ const fetchData = () => {
+ tableLoading.value = true;
+ getAttendanceRules({ ...page, ...searchForm })
+ .then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鍒嗛〉鍙樻洿
+ const paginationChange = pagination => {
+ page.current = pagination.page;
+ page.size = pagination.limit;
+ fetchData();
+ };
+
+ // 閲嶇疆鎼滅储
+ const resetSearch = () => {
+ searchForm.countId = "";
+ searchForm.locationName = "";
+ fetchData();
+ };
+
+ // 鎵撳紑琛ㄥ崟
+ const openForm = (type, row = {}) => {
+ operationType.value = type;
+ currentRow.value = row;
+ dialogVisible.value = true;
+ };
+
+ // 鍒犻櫎鐝
+ const handleDelete = id => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ繖鏉$彮娆″悧锛�", "鍒犻櫎纭", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ deleteAttendanceRule([id]).then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ fetchData();
+ });
+ })
+ .catch(() => {
+ // 鍙栨秷鍒犻櫎
+ });
+ };
+
+ // 鍒濆鍖�
+ onMounted(() => {
+ fetchDeptOptions();
+ fetchData();
+ });
+</script>
+
+<style scoped lang="scss">
+ .page-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+
+ .title {
+ font-size: 18px;
+ font-weight: 600;
+ }
+
+ .actions {
+ display: flex;
+ gap: 10px;
+ }
+ }
+
+ .mb16 {
+ margin-bottom: 16px;
+ }
+
+ .mt10 {
+ margin-top: 10px;
+ }
+</style>
diff --git a/src/views/personnelManagement/attendanceCheckin/index.vue b/src/views/personnelManagement/attendanceCheckin/index.vue
new file mode 100644
index 0000000..168a71d
--- /dev/null
+++ b/src/views/personnelManagement/attendanceCheckin/index.vue
@@ -0,0 +1,512 @@
+<template>
+ <div class="app-container">
+ <!-- 鍛樺伐鎵撳崱鍖� -->
+ <!-- <el-card shadow="never"
+ class="mb16">
+ <div class="attendance-header">
+ <div>
+ <div class="title">鎵撳崱绛惧埌
+ </div>
+ <div class="sub-title">鏀寔涓�閿墦鍗★紝鑷姩璁板綍涓婁笅鐝椂闂�</div>
+ </div>
+ <div class="attendance-actions">
+ <div class="time-block">
+ <div class="label">褰撳墠鏃堕棿</div>
+ <div class="value">{{ nowTime }}</div>
+ </div>
+ <el-button type="primary"
+ size="large"
+ @click="handleCheckInOut"
+ :disabled="todayRecord.workEndAt">
+ {{ checkInOutText }}
+ </el-button>
+ </div>
+ </div>
+ <el-descriptions border
+ :column="4"
+ class="mt10">
+ <el-descriptions-item label="鍛樺伐濮撳悕">
+ {{ todayRecord.staffName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="宸ュ彿">
+ {{ todayRecord.staffNo }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵�灞為儴闂�">
+ {{ todayRecord.deptName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浠婃棩鐘舵��">
+ <el-tag :type="todayStatusTag"
+ size="small">
+ {{ todayStatusText }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="涓婄彮鏃堕棿">
+ {{ todayRecord?.workStartAt || '-' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓嬬彮鏃堕棿">
+ {{ todayRecord?.workEndAt || '-' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="宸ユ椂(灏忔椂)">
+ {{ todayRecord?.workHours ?? '-' }}
+ </el-descriptions-item>
+ <el-descriptions-item label="寮傚父鏍囪">
+ <span v-if="!todayRecord.id || todayRecord?.status === 0">-</span>
+ <el-tag v-else
+ type="danger"
+ size="small">
+ {{ todayRecord?.status ? getStatusText(todayRecord.status) : '-' }}
+ </el-tag>
+ </el-descriptions-item>
+ </el-descriptions>
+ </el-card> -->
+ <div class="attendance-operation">
+ <!-- 鏌ヨ鏉′欢锛堢鐞嗗憳鑰冨嫟鏃ユ姤锛� -->
+ <el-form :model="searchForm"
+ :inline="true"
+ class="search-form">
+ <el-form-item label="閮ㄩ棬锛�"
+ prop="deptId">
+ <el-tree-select v-model="searchForm.deptId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ check-strictly
+ style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="鏃ユ湡锛�"
+ prop="date">
+ <el-date-picker v-model="searchForm.date"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="fetchData">
+ <el-icon>
+ <Search />
+ </el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">
+ <el-icon>
+ <Refresh />
+ </el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ <el-button icon="Download"
+ @click="handleExport">
+ 瀵煎嚭鑰冨嫟鏃ユ姤
+ </el-button>
+ </div>
+ <!-- 鑰冨嫟鏃ユ姤琛ㄦ牸 -->
+ <div class="table_list">
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ style="width: 100%"
+ height="calc(100vh - 24em)"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ :row-class-name="rowClassName">
+ <el-table-column type="index"
+ label="搴忓彿"
+ width="60"
+ align="center" />
+ <el-table-column prop="date"
+ label="鏃ユ湡"
+ width="120" />
+ <el-table-column prop="deptName"
+ label="閮ㄩ棬"
+ width="140" />
+ <el-table-column prop="staffName"
+ label="濮撳悕"
+ width="120" />
+ <el-table-column prop="staffNo"
+ label="宸ュ彿"
+ width="120" />
+ <el-table-column prop="workStartAt"
+ label="涓婄彮鏃堕棿"
+ width="140" />
+ <el-table-column prop="workEndAt"
+ label="涓嬬彮鏃堕棿"
+ width="140" />
+ <el-table-column prop="workHours"
+ label="宸ユ椂(灏忔椂)"
+ align="center" />
+ <el-table-column prop="status"
+ label="鑰冨嫟鐘舵��"
+ align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.status === 0"
+ type="success"
+ size="small">
+ 姝e父
+ </el-tag>
+ <el-tag v-else
+ type="danger"
+ size="small">
+ <!-- {{ scope.row.status === 1 ? '杩熷埌' : scope.row.status === 2 ? '鏃╅��' : '杩熷埌銆佹棭閫�' }} -->
+ {{ getStatusText(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark"
+ label="澶囨敞"
+ show-overflow-tooltip />
+ </el-table>
+ <pagination :total="page.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
+ import { useRouter } from "vue-router";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import {
+ createPersonalAttendanceRecord,
+ findPersonalAttendanceRecords,
+ findTodayPersonalAttendanceRecord,
+ } from "@/api/personnelManagement/personalAttendanceRecords.js";
+ import Pagination from "@/components/Pagination/index.vue";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import { Refresh, Search, ArrowLeft } from "@element-plus/icons-vue";
+
+ const { proxy } = getCurrentInstance();
+ const router = useRouter();
+ const tableLoading = ref(false);
+ // 鍒嗛〉鍙傛暟
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ // 浠婃棩鏁版嵁
+ const todayRecord = ref({});
+
+ // 閮ㄩ棬閫夐」
+ const deptOptions = ref([]);
+
+ // 鏌ヨ琛ㄥ崟
+ const searchForm = reactive({
+ deptId: "",
+ date: "",
+ });
+
+ // 琛ㄦ牸鏁版嵁
+ const tableData = ref([]);
+
+ // 褰撳墠鏃堕棿灞曠ず
+ const nowTime = ref("");
+ let timer = null;
+
+ const updateNowTime = () => {
+ const now = new Date();
+ const Y = now.getFullYear();
+ const M = String(now.getMonth() + 1).padStart(2, "0");
+ const D = String(now.getDate()).padStart(2, "0");
+ const h = String(now.getHours()).padStart(2, "0");
+ const m = String(now.getMinutes()).padStart(2, "0");
+ const s = String(now.getSeconds()).padStart(2, "0");
+ nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
+ };
+
+ // 鎵撳崱鎸夐挳鏂囨湰
+ const checkInOutText = computed(() => {
+ if (!todayRecord.value || !todayRecord.value.workStartAt) {
+ return "涓婄彮鎵撳崱";
+ }
+ if (!todayRecord.value.workEndAt) {
+ return "涓嬬彮鎵撳崱";
+ }
+ return "浠婃棩宸叉墦鍗″畬鎴�";
+ });
+
+ // 浠婃棩鐘舵�佸睍绀�
+ const todayStatusTag = computed(() => {
+ if (!todayRecord.value.id) return "info";
+ if (todayRecord.value.status === 0) return "success";
+ return "danger";
+ });
+ const getStatusText = status => {
+ switch (status) {
+ case 0:
+ return "姝e父";
+ case 1:
+ return "杩熷埌";
+ case 2:
+ return "鏃╅��";
+ case 3:
+ return "杩熷埌銆佹棭閫�";
+ case 4:
+ return "缂哄嫟";
+ }
+ };
+
+ const todayStatusText = computed(() => {
+ if (!todayRecord.value.id) return "鏈墦鍗�";
+ switch (todayRecord.value.status) {
+ case 0:
+ return "姝e父";
+ case 1:
+ return "杩熷埌";
+ case 2:
+ return "鏃╅��";
+ case 3:
+ return "杩熷埌銆佹棭閫�";
+ case 4:
+ return "缂哄嫟";
+ }
+ });
+
+ // 琛屾牱寮忥細寮傚父楂樹寒
+ const rowClassName = ({ row }) => {
+ if (row.status === 1 || row.status === 2) {
+ return "row-abnormal";
+ }
+ return "";
+ };
+
+ // 鏌ヨ閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ /** 杩囨护绂佺敤鐨勯儴闂� */
+ function filterDisabledDept(deptList) {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ }
+
+ // 鏌ヨ
+ const fetchData = () => {
+ tableLoading.value = true;
+ findPersonalAttendanceRecords({ ...page, ...searchForm })
+ .then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鏌ヨ浠婃棩鎵撳崱淇℃伅
+ const fetchTodayData = () => {
+ // findTodayPersonalAttendanceRecord({}).then(res => {
+ // todayRecord.value = res.data;
+ // });
+ };
+
+ const paginationChange = pagination => {
+ page.current = pagination.page;
+ page.size = pagination.limit;
+ fetchData();
+ };
+
+ const resetSearch = () => {
+ searchForm.deptId = "";
+ searchForm.date = "";
+ fetchData();
+ };
+
+ const handleExport = () => {
+ ElMessageBox.confirm("鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/personalAttendanceRecords/export", {}, "鑰冨嫟璁板綍.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鑾峰彇褰撳墠浣嶇疆
+ const getCurrentLocation = () => {
+ return new Promise((resolve, reject) => {
+ if (!navigator.geolocation) {
+ reject(new Error("娴忚鍣ㄤ笉鏀寔鍦扮悊瀹氫綅"));
+ return;
+ }
+
+ // 妫�鏌ユ槸鍚︿娇鐢℉TTPS
+ const isSecureContext =
+ window.isSecureContext || window.location.protocol === "https:";
+ console.log(
+ "褰撳墠鍗忚:",
+ window.location.protocol,
+ "鏄惁瀹夊叏涓婁笅鏂�:",
+ isSecureContext
+ );
+
+ if (!isSecureContext) {
+ console.warn("褰撳墠涓嶆槸HTTPS鍗忚锛屽湴鐞嗕綅缃瓵PI鍙兘鍙楅檺");
+ }
+
+ navigator.geolocation.getCurrentPosition(
+ position => {
+ const { longitude, latitude } = position.coords;
+ console.log("鑾峰彇浣嶇疆鎴愬姛:", longitude, latitude);
+ resolve({ longitude, latitude });
+ },
+ error => {
+ console.log("鑾峰彇浣嶇疆澶辫触:", error);
+ let errorMessage = "鑾峰彇浣嶇疆澶辫触";
+
+ // 鏍规嵁閿欒绫诲瀷鎻愪緵鏇村叿浣撶殑鎻愮ず
+ switch (error.code) {
+ case error.PERMISSION_DENIED:
+ errorMessage =
+ "鐢ㄦ埛鎷掔粷浜嗕綅缃潈闄愯姹傦紝璇峰湪娴忚鍣ㄨ缃腑鍏佽浣嶇疆璁块棶";
+ break;
+ case error.POSITION_UNAVAILABLE:
+ errorMessage = "浣嶇疆淇℃伅涓嶅彲鐢紝璇锋鏌ヨ澶囧畾浣嶅姛鑳�";
+ break;
+ case error.TIMEOUT:
+ errorMessage = "鑾峰彇浣嶇疆瓒呮椂锛岃閲嶈瘯";
+ break;
+ case error.UNKNOWN_ERROR:
+ errorMessage = "鑾峰彇浣嶇疆鏃跺彂鐢熸湭鐭ラ敊璇�";
+ break;
+ default:
+ errorMessage = `鑾峰彇浣嶇疆澶辫触: ${error.message}`;
+ }
+
+ // 妫�鏌ユ槸鍚︽槸HTTPS闂
+ if (error.code === error.PERMISSION_DENIED && !isSecureContext) {
+ errorMessage += "锛堟敞鎰忥細鐢熶骇鐜闇�瑕佷娇鐢℉TTPS鍗忚鎵嶈兘鑾峰彇浣嶇疆锛�";
+ }
+
+ reject(new Error(errorMessage));
+ },
+ {
+ enableHighAccuracy: true,
+ timeout: 10000,
+ maximumAge: 0,
+ }
+ );
+ });
+ };
+
+ // 鎵撳崱
+ const handleCheckInOut = () => {
+ getCurrentLocation()
+ .then(location => {
+ console.log("浣嶇疆鎴愬姛");
+ createPersonalAttendanceRecord(location).then(res => {
+ fetchData();
+ fetchTodayData();
+ ElMessage.success("鎵撳崱鎴愬姛锛�");
+ });
+ })
+ .catch(error => {
+ // 鑾峰彇浣嶇疆澶辫触鏃讹紝浠嶅厑璁告墦鍗�
+ ElMessage.warning("鑾峰彇浣嶇疆澶辫触锛屽皢浣跨敤榛樿浣嶇疆鎵撳崱");
+ createPersonalAttendanceRecord({}).then(res => {
+ fetchData();
+ fetchTodayData();
+ ElMessage.success("鎵撳崱鎴愬姛锛�");
+ });
+ });
+ };
+
+ onMounted(() => {
+ updateNowTime();
+ timer = setInterval(updateNowTime, 1000);
+ // 榛樿灞曠ず褰撳ぉ鏁版嵁
+ const today = new Date();
+ const Y = today.getFullYear();
+ const M = String(today.getMonth() + 1).padStart(2, "0");
+ const D = String(today.getDate()).padStart(2, "0");
+ searchForm.date = `${Y}-${M}-${D}`;
+ fetchData();
+ fetchTodayData();
+ fetchDeptOptions();
+ });
+
+ onBeforeUnmount(() => {
+ if (timer) {
+ clearInterval(timer);
+ }
+ });
+</script>
+
+<style scoped lang="scss">
+.table_list {
+ margin-top: unset;
+}
+ .mb16 {
+ margin-bottom: 16px;
+ }
+
+ .attendance-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .attendance-header .title {
+ font-size: 18px;
+ font-weight: 600;
+ margin-bottom: 4px;
+ }
+
+ .attendance-header .sub-title {
+ font-size: 13px;
+ color: #909399;
+ }
+
+ .attendance-actions {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ .time-block {
+ text-align: right;
+ }
+
+ .time-block .label {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .time-block .value {
+ font-size: 18px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ :deep(.row-abnormal) {
+ background-color: #fff5f5;
+ }
+
+ .attendance-operation {
+ display: flex;
+ justify-content: space-between;
+ }
+</style>
+
diff --git a/src/views/personnelManagement/classsSheduling/index.vue b/src/views/personnelManagement/classsSheduling/index.vue
new file mode 100644
index 0000000..140ffba
--- /dev/null
+++ b/src/views/personnelManagement/classsSheduling/index.vue
@@ -0,0 +1,1283 @@
+<template>
+ <div class="class-page">
+ <div class="search-container">
+ <div class="search-form">
+ <div class="search-row">
+ <div class="search-item">
+ <label class="search-label">閫夋嫨鏃堕棿锛�</label>
+ <div class="search-input-group">
+ <el-date-picker v-model="query.year"
+ type="year"
+ size="small"
+ format="YYYY"
+ placeholder="閫夋嫨骞�"
+ @change="refreshTable()"
+ style="width: 90px"
+ :clearable="false" />
+ <el-select v-model="query.month"
+ clearable
+ placeholder="閫夋嫨鏈�"
+ style="width: 70px; margin-left: 8px"
+ size="small"
+ @change="refreshTable()">
+ <el-option v-for="item in monthOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </div>
+ </div>
+ <div class="search-item">
+ <el-input v-model="query.userName"
+ placeholder="璇疯緭鍏ヤ汉鍛樺悕绉�"
+ size="small"
+ style="width: 120px"
+ clearable
+ @keyup.enter="refreshTable()" />
+ </div>
+ <div class="search-item">
+ <el-tree-select v-model="query.sysDeptId"
+ :data="deptOptions"
+ :props="{ value: 'id', label: 'label', children: 'children' }"
+ value-key="id"
+ placeholder="璇烽�夋嫨閮ㄩ棬"
+ size="small"
+ clearable
+ @change="refreshTable()"
+ style="width: 140px" />
+ </div>
+ <div class="search-actions">
+ <el-button size="small"
+ type="primary"
+ @click="refreshTable()">
+ 鏌ヨ
+ </el-button>
+ <el-button size="small"
+ @click="refresh()"
+ style="margin-left: 8px">
+ 閲嶇疆
+ </el-button>
+ </div>
+ <div class="search-buttons">
+ <el-button size="small"
+ type="primary"
+ @click="configTime">
+ 鐝閰嶇疆
+ </el-button>
+ <el-button size="small"
+ type="success"
+ @click="handleDown"
+ :loading="downLoading"
+ style="margin-left: 8px">
+ 瀵煎嚭
+ </el-button>
+ <el-button size="small"
+ type="warning"
+ @click="schedulingVisible = true"
+ style="margin-left: 8px">
+ 鎺掔彮
+ </el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="scheduling-container"
+ v-loading="pageLoading">
+ <!-- 鏈堝害鎺掔彮 -->
+ <div class="scheduling-table"
+ v-show="query.month">
+ <div class="scheduling-left">
+ <div class="scheduling-header">
+ 浜哄憳鍚嶇О
+ </div>
+ <div class="scheduling-user"
+ :class="{ 'scheduling-user-hover': currentUserIndex == index }"
+ v-for="(item, index) in listForm"
+ :key="'e' + index"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="user-avatar">
+ {{ item.name ? item.name.charAt(0) : "" }}
+ </div>
+ <div class="user-details">
+ <h4 class="user-name">{{ item.name }}</h4>
+ <!-- <div class="user-stats">
+ <span class="stat-item">鏃�:{{ item.day0 }}</span>
+ <span class="stat-item">涓�:{{ item.day1 }}</span>
+ <span class="stat-item">澶�:{{ item.day2 }}</span>
+ <span class="stat-item">浼�:{{ item.day3 }}</span>
+ <span class="stat-item">鍋�:{{ item.day4 }}</span>
+ <span class="stat-item">宸�:{{ item.day6 }}</span>
+ </div> -->
+ <div class="user-total">
+ <span class="total-label">鍚堣鍑哄嫟:</span>
+ <span class="total-value">{{ item.monthlyAttendance.totalAttendance }}澶�</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="scheduling-right">
+ <div class="scheduling-calendar">
+ <div class="calendar-header">
+ <div class="calendar-header-item"
+ v-for="(item, index) in weeks"
+ :key="'b' + index">
+ <span class="week-number"
+ v-if="item.week == '鍛ㄦ棩'">{{ item.weekNum }}鍛�</span>
+ <div class="day-info">
+ <span class="day-number">{{ item.day }}</span>
+ <span class="day-week">{{ item.week.charAt(1) }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="calendar-body">
+ <div class="calendar-row"
+ v-for="(item, index) in listForm"
+ :key="'c' + index"
+ :class="{ 'calendar-row-hover': currentUserIndex == index }"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="calendar-cell"
+ v-for="(m, i) in item.list"
+ :key="'d' + i">
+ <el-dropdown trigger="click"
+ placement="bottom"
+ @command="(e) => handleCommand(e, m)"
+ class="shift-dropdown">
+ <div class="shift-box"
+ :class="{
+ 'shift-box-early': m.shift === '鏃╃彮',
+ 'shift-box-mid': m.shift === '涓彮',
+ 'shift-box-night': m.shift === '澶滅彮',
+ 'shift-box-rest': m.shift === '浼戞伅',
+ 'shift-box-leave': m.shift === '璇峰亣',
+ 'shift-box-other': m.shift === '澶�11',
+ 'shift-box-business': m.shift === '澶�12',
+ }">
+ <span class="shift-text">{{ getShiftNameByValue(m.shift) || '鈥�' }}</span>
+ </div>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item v-for="(n, j) in classType"
+ :key="'h' + j"
+ :command="n.id">{{ n.shift || '鈥�'
+ }}</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- 骞村害鎺掔彮 -->
+ <div class="yearly-table"
+ v-show="!query.month">
+ <div class="scheduling-left">
+ <div class="scheduling-header">
+ 浜哄憳鍚嶇О
+ </div>
+ <div class="scheduling-user"
+ :class="{ 'scheduling-user-hover': currentUserIndex == index }"
+ v-for="(item, index) in yearList"
+ :key="'e' + index"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="user-avatar">
+ {{ item.name ? item.name.charAt(0) : "" }}
+ </div>
+ <div class="user-details">
+ <h4 class="user-name">{{ item.name }}</h4>
+ <!-- <div class="user-stats">
+ <span class="stat-item">鏃�:{{ item.day0 }}</span>
+ <span class="stat-item">涓�:{{ item.day1 }}</span>
+ <span class="stat-item">澶�:{{ item.day2 }}</span>
+ <span class="stat-item">浼�:{{ item.day3 }}</span>
+ <span class="stat-item">鍋�:{{ item.day4 }}</span>
+ <span class="stat-item">宸�:{{ item.day6 }}</span>
+ </div> -->
+ <div class="user-total">
+ <span class="total-label">鍚堣鍑哄嫟:</span>
+ <span class="total-value">{{ item.work_time }}澶�</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="scheduling-right">
+ <div class="yearly-calendar">
+ <div class="yearly-header">
+ <div class="yearly-header-item"
+ v-for="(item, index) in monthList"
+ :key="'b' + index">
+ <span class="month-name">{{ item }}鏈�</span>
+ </div>
+ </div>
+ <div class="yearly-body">
+ <div class="yearly-row"
+ v-for="(item, index) in yearList"
+ :key="'c' + index"
+ :class="{ 'calendar-row-hover': currentUserIndex == index }"
+ @mouseenter="onMouseEnter(index)"
+ @mouseleave="currentUserIndex = null">
+ <div class="yearly-cell"
+ v-for="(m, i) in item.monthList"
+ :key="'d' + i">
+ <div class="monthly-attendance">
+ <span class="attendance-label">鍚堣鍑哄嫟锛�</span>
+ <span class="attendance-value">{{ m.totalMonthAttendance }}</span>
+ </div>
+ <!-- <div class="monthly-stats">
+ <span class="stat-item">鏃�:{{ m.day0 }}</span>
+ <span class="stat-item">涓�:{{ m.day1 }}</span>
+ <span class="stat-item">澶�:{{ m.day2 }}</span>
+ <span class="stat-item">浼�:{{ m.day3 }}</span>
+ <span class="stat-item">鍋�:{{ m.day4 }}</span>
+ <span class="stat-item">宸�:{{ m.day6 }}</span>
+ </div> -->
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div style="display: flex; justify-content: flex-end; margin-top: 10px; margin-right: 30px">
+ <el-pagination background
+ @current-change="currentChange"
+ :page-size="pageSize"
+ :current-page="currentPage"
+ layout="total, prev, pager, next, jumper"
+ :total="total">
+ </el-pagination>
+ </div>
+ <el-dialog title="鎺掔彮"
+ v-model="schedulingVisible"
+ width="400px">
+ <div class="search_thing">
+ <div class="search_label"
+ style="width: 90px">
+ <span style="color: red; margin-right: 4px">*</span>鍛ㄦ锛�
+ </div>
+ <div class="search_input">
+ <el-date-picker v-model="schedulingQuery.week"
+ type="week"
+ format="YYYY 绗� ww 鍛�"
+ placeholder="閫夋嫨鍛ㄦ"
+ style="width: 100%">
+ </el-date-picker>
+ </div>
+ </div>
+ <div class="search_thing">
+ <div class="search_label"
+ style="width: 90px">
+ <span style="color: red; margin-right: 4px">*</span>浜哄憳鍚嶇О锛�
+ </div>
+ <div class="search_input">
+ <el-select v-model="schedulingQuery.userId"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ multiple
+ clearable
+ collapse-tags>
+ <el-option v-for="item in personList"
+ :key="item.id"
+ :label="item.staffName"
+ :value="item.id">
+ </el-option>
+ </el-select>
+ </div>
+ </div>
+ <div class="search_thing">
+ <div class="search_label"
+ style="width: 90px">
+ <span style="color: red; margin-right: 4px">*</span>鐝锛�
+ </div>
+ <div class="search_input">
+ <el-select v-model="schedulingQuery.shift"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%">
+ <el-option v-for="item in classType"
+ :key="item.id"
+ :label="getShiftNameByValue(item.shift)"
+ :value="item.id">
+ </el-option>
+ </el-select>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="schedulingVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary"
+ @click="confirmScheduling"
+ :loading="loading">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, onMounted, getCurrentInstance } from "vue";
+ import { useRouter } from "vue-router";
+ import {
+ page,
+ pageYear,
+ add,
+ exportFile,
+ update,
+ staffOnJobListPage,
+ } from "@/api/personnelManagement/class";
+ import { deptTreeSelect } from "@/api/system/user.js";
+ import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js";
+ import { useDict } from "@/utils/dict";
+ const { proxy } = getCurrentInstance();
+ const router = useRouter();
+
+ // 鏌ヨ鏉′欢
+ const query = reactive({
+ userName: "",
+ sysDeptId: "",
+ year: new Date(),
+ month: new Date().getMonth() + 1,
+ });
+ // 鑾峰彇鐝瀛楀吀鍊�
+ const { shifts_list } = useDict("shifts_list");
+ // 鏈堜唤閫夐」
+ const monthOptions = [
+ { value: 1, label: "1鏈�" },
+ { value: 2, label: "2鏈�" },
+ { value: 3, label: "3鏈�" },
+ { value: 4, label: "4鏈�" },
+ { value: 5, label: "5鏈�" },
+ { value: 6, label: "6鏈�" },
+ { value: 7, label: "7鏈�" },
+ { value: 8, label: "8鏈�" },
+ { value: 9, label: "9鏈�" },
+ { value: 10, label: "10鏈�" },
+ { value: 11, label: "11鏈�" },
+ { value: 12, label: "12鏈�" },
+ ];
+
+ // 閮ㄩ棬鍒楄〃
+ const deptOptions = ref([]);
+
+ // 鍛ㄥ垪琛�
+ const weeks = ref([]);
+
+ // 鐝绫诲瀷
+ const classType = ref([]);
+
+ // 褰撳墠鐢ㄦ埛绱㈠紩
+ const currentUserIndex = ref(null);
+
+ // 鎺掔彮寮圭獥鏄剧ず鐘舵��
+ const schedulingVisible = ref(false);
+
+ // 浜哄憳鍒楄〃
+ const personList = ref([]);
+
+ // 鍔犺浇鐘舵��
+ const loading = ref(false);
+
+ // 鎺掔彮鏌ヨ鏉′欢
+ const schedulingQuery = reactive({
+ week: "",
+ userId: null,
+ shift: "",
+ });
+
+ // 鍒楄〃鏁版嵁
+ const listForm = ref([]);
+
+ // 褰撳墠椤�
+ const currentPage = ref(1);
+
+ // 姣忛〉鏉℃暟
+ const pageSize = ref(6);
+
+ // 鎬绘潯鏁�
+ const total = ref(3);
+
+ // 椤甸潰鍔犺浇鐘舵��
+ const pageLoading = ref(false);
+
+ // 鏈堜唤鍒楄〃
+ const monthList = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
+
+ // 骞村害鍒楄〃
+ const yearList = ref([]);
+
+ // 瀵煎嚭鍔犺浇鐘舵��
+ const downLoading = ref(false);
+
+ // 鑾峰彇閮ㄩ棬鍒楄〃
+ const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+
+ // 鏍规嵁鐝鍊艰幏鍙栫彮娆″悕绉�
+ const getShiftNameByValue = value => {
+ if (!value) return "";
+ const shift = shifts_list.value.find(item => item.value === value);
+ return shift ? shift.label : value;
+ };
+
+ // 杩囨护绂佺敤鐨勯儴闂�
+ const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+
+ // 鍒锋柊
+ const refresh = () => {
+ listForm.value = [];
+ yearList.value = [];
+ currentPage.value = 1;
+ query.userName = "";
+ query.sysDeptId = "";
+ query.year = new Date();
+ query.month = new Date().getMonth() + 1;
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ };
+
+ // 鍒锋柊琛ㄦ牸
+ const refreshTable = () => {
+ currentPage.value = 1;
+ if (query.month) {
+ listForm.value = [];
+ init();
+ } else {
+ yearList.value = [];
+ initYear();
+ }
+ };
+
+ // 椤电爜鏀瑰彉
+ const currentChange = num => {
+ currentPage.value = num;
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ };
+
+ // 鏁板瓧杞腑鏂�
+ const transFromNumber = num => {
+ let changeNum = ["闆�", "涓�", "浜�", "涓�", "鍥�", "浜�", "鍏�", "涓�", "鍏�", "涔�"];
+ let unit = ["", "鍗�", "鐧�", "鍗�", "涓�"];
+ num = parseInt(num);
+ let getWan = temp => {
+ let strArr = temp.toString().split("").reverse();
+ let newNum = "";
+ for (var i = 0; i < strArr.length; i++) {
+ newNum =
+ (i == 0 && strArr[i] == 0
+ ? ""
+ : i > 0 && strArr[i] == 0 && strArr[i - 1] == 0
+ ? ""
+ : changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i])) +
+ newNum;
+ }
+ return newNum;
+ };
+ let overWan = Math.floor(num / 10000);
+ let noWan = num % 10000;
+ if (noWan.toString().length < 4) noWan = "0" + noWan;
+ return overWan ? getWan(overWan) + "涓�" + getWan(noWan) : getWan(num);
+ };
+
+ // 鍒濆鍖栨湀搴︽暟鎹�
+ const init = () => {
+ pageLoading.value = true;
+ console.log(query.year, "query.year");
+ let year = query.year.getFullYear();
+ let month0 = query.month ? query.month : new Date().getMonth() + 1;
+ let month = month0 > 9 ? month0 : "0" + month0;
+ page({
+ size: pageSize.value,
+ current: currentPage.value,
+ time: year + "-" + month + "-01 00:00:00",
+ userName: query.userName,
+ sysDeptId: query.sysDeptId,
+ })
+ .then(res => {
+ pageLoading.value = false;
+ total.value = res.data.page.total;
+ listForm.value = res.data.page.records.map(item => {
+ for (let key in item.monthlyAttendance) {
+ let type = getDayByDic(key);
+ if (type != undefined || type != null) {
+ item[`day${type}`] = item.monthlyAttendance[key];
+ }
+ }
+ return item;
+ });
+ let headerList = res.data.headerList;
+ weeks.value = [];
+ headerList.forEach(item => {
+ let obj = {
+ weekNum: item.weekly,
+ week: item.headerTime.split(" ")[1],
+ day: item.headerTime.split(" ")[0],
+ };
+ weeks.value.push(obj);
+ });
+ })
+ .catch(() => {
+ pageLoading.value = false;
+ });
+ };
+
+ // 鍒濆鍖栧勾搴︽暟鎹�
+ const initYear = () => {
+ pageLoading.value = true;
+ let year = query.year.getFullYear();
+ pageYear({
+ size: pageSize.value,
+ current: currentPage.value,
+ time: year + "-01-01 00:00:00",
+ userName: query.userName,
+ sysDeptId: query.sysDeptId,
+ }).then(res => {
+ pageLoading.value = false;
+ total.value = res.data.total;
+ yearList.value = res.data.records.map(item => {
+ for (let key in item.year) {
+ let type = getDayByDic(key);
+ if (type != undefined || type != null) {
+ item[`day${type}`] = item.year[key];
+ }
+ }
+ item.monthList = [];
+ for (let m in item.month) {
+ let obj = {};
+ for (let key in item.month[m]) {
+ let type = getDayByDic(key);
+ if (type != undefined || type != null) {
+ obj[`day${type}`] = item.month[m][key];
+ }
+ }
+ obj.totalMonthAttendance = item.month[m].totalMonthAttendance;
+ item.monthList.push(obj);
+ }
+ return item;
+ });
+ });
+ };
+
+ // 榧犳爣杩涘叆
+ const onMouseEnter = index => {
+ currentUserIndex.value = index;
+ };
+
+ // 纭鎺掔彮
+ const confirmScheduling = () => {
+ if (!schedulingQuery.week) {
+ proxy.$modal.msgError("璇烽�夋嫨鍛ㄦ");
+ return;
+ }
+ let time = schedulingQuery.week.getTime();
+
+ // 鏍煎紡鍖栨棩鏈熶负 YYYY-MM-DD 鏍煎紡
+ const formatDate = date => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return `${year}-${month}-${day}`;
+ };
+
+ let startWeek =
+ formatDate(new Date(time - 24 * 60 * 60 * 1000)) + " 00:00:00";
+ let endWeek =
+ formatDate(new Date(time + 24 * 60 * 60 * 1000 * 5)) + " 00:00:00";
+
+ if (!schedulingQuery.userId || schedulingQuery.userId.length == 0) {
+ proxy.$modal.msgError("璇烽�夋嫨浜哄憳");
+ return;
+ }
+ if (!schedulingQuery.shift) {
+ proxy.$modal.msgError("璇烽�夋嫨鐝");
+ return;
+ }
+ loading.value = true;
+ add({
+ startWeek,
+ endWeek,
+ staffOnJobId: schedulingQuery.userId.join(","),
+ personalAttendanceLocationConfigId: schedulingQuery.shift,
+ })
+ .then(res => {
+ loading.value = false;
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ schedulingVisible.value = false;
+ schedulingQuery.week = "";
+ schedulingQuery.userId = null;
+ schedulingQuery.shift = "";
+ refresh();
+ })
+ .catch(err => {
+ loading.value = false;
+ });
+ };
+
+ // 鏃堕棿閰嶇疆
+ const configTime = () => {
+ // 璺宠浆鍒拌�冨嫟鎵撳崱椤甸潰
+ router.push({
+ path: "/personnelManagement/checkinRules",
+ });
+ };
+
+ // 鍒ゆ柇鏄惁涓虹┖瀵硅薄
+ const isObjectEmpty = obj => {
+ return Object.keys(obj).some(key => !obj[key]);
+ };
+
+ // 瀵煎嚭
+ const handleDown = () => {
+ let year = query.year.getFullYear();
+ let time = "";
+ if (query.month) {
+ let month = query.month > 9 ? query.month : "0" + query.month;
+ time = year + "-" + month + "-01 00:00:00";
+ } else {
+ time = year + "-01-01 00:00:00";
+ }
+ downLoading.value = true;
+ exportFile({
+ time,
+ userName: query.userName,
+ sysDeptId: query.sysDeptId,
+ isMonth: query.month ? true : false,
+ })
+ .then(res => {
+ proxy.$modal.msgSuccess("涓嬭浇鎴愬姛");
+ const blob =
+ res instanceof Blob
+ ? res
+ : new Blob([res], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ let fileName = "";
+ if (query.month) {
+ fileName = year + "-" + query.month + " 鐝淇℃伅";
+ } else {
+ fileName = year + " 鐝姹囨��";
+ }
+ proxy.$download.saveAs(blob, fileName + ".xlsx");
+ })
+ .catch(err => {
+ })
+ .finally(() => {
+ downLoading.value = false;
+ });
+ };
+ // 澶勭悊鍛戒护
+ const handleCommand = (e, m) => {
+ // if (e != m.shift) {
+ update({
+ id: m.id,
+ personalAttendanceLocationConfigId: e,
+ }).then(res => {
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ // m.shift = e;
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ });
+ // }
+ };
+ // 鏌ヨ瑙勫垯鍒楄〃
+ const fetchData = () => {
+ getAttendanceRules({ current: -1, size: -1 }).then(res => {
+ classType.value = res.data.records;
+ });
+ };
+ // 鑾峰彇鐢ㄦ埛
+ const getUsers = () => {
+ // selectUserCondition({ type: 1 }).then(res => {
+ // let arr = res.data;
+ // personList.value = arr;
+ // });
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1,
+ }).then(res => {
+ let arr = res.data.records;
+ personList.value = arr;
+ });
+ };
+
+ // 鏍规嵁瀛楀吀鑾峰彇鏃ユ湡
+ const getDayByDic = e => {
+ let obj = classType.value.find(m => m.shift == e);
+ if (obj) {
+ return obj.id;
+ }
+ };
+
+ // 鏍规嵁瀛楀吀鑾峰彇鐝
+ const getShiftByDic = e => {
+ let obj = classType.value.find(m => m.shift == e);
+ if (obj) {
+ return obj.shift;
+ }
+ return "鏃�";
+ };
+
+ // 鍒濆鍖�
+ onMounted(() => {
+ fetchData();
+ getUsers();
+ fetchDeptOptions();
+ if (query.month) {
+ init();
+ } else {
+ initYear();
+ }
+ monthList.value = [];
+ for (let i = 12; i > 0; i--) {
+ monthList.value.push(i);
+ }
+ monthList.value.reverse();
+ });
+</script>
+
+<style scoped>
+ .class-page {
+ padding: 16px;
+ }
+
+ .form_title {
+ height: 36px;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ font-weight: 800;
+ }
+
+ /* 鎼滅储鍖哄煙鏍峰紡 */
+ .search-container {
+ background: #f9fafb;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ }
+
+ .search-form {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .search-row {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: nowrap;
+ overflow-x: auto;
+ }
+
+ .search-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ }
+
+ .search-label {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ min-width: 65px;
+ text-align: right;
+ }
+
+ .search-input-group {
+ display: flex;
+ align-items: center;
+ }
+
+ .search-actions {
+ display: flex;
+ align-items: center;
+ margin-left: 8px;
+ }
+
+ .search-buttons {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex: 1;
+ }
+
+ /* 鍝嶅簲寮忚皟鏁� */
+ @media (max-width: 1200px) {
+ .search-row {
+ gap: 12px;
+ }
+
+ .search-item {
+ gap: 4px;
+ }
+
+ .search-label {
+ min-width: 60px;
+ font-size: 13px;
+ }
+
+ .search-actions {
+ margin-left: 4px;
+ }
+
+ .search-buttons {
+ margin-left: 12px;
+ }
+ }
+
+ @media (max-width: 992px) {
+ .search-row {
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ }
+
+ .search-buttons {
+ margin-left: 0;
+ margin-top: 12px;
+ width: 100%;
+ justify-content: flex-start;
+ }
+ }
+
+ /* 鎺掔彮瀹瑰櫒 */
+ .scheduling-container {
+ width: 100%;
+ min-height: calc(100vh - 280px);
+ background-color: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+ margin-bottom: 20px;
+ }
+
+ /* 鎺掔彮琛ㄦ牸 */
+ .scheduling-table {
+ display: flex;
+ width: 100%;
+ height: calc(100vh - 280px);
+ }
+
+ /* 宸︿晶浜哄憳淇℃伅 */
+ .scheduling-left {
+ width: 240px;
+ min-width: 240px;
+ background-color: #f9fafb;
+ border-right: 1px solid #e5e7eb;
+ }
+
+ /* 鍙充晶鎺掔彮鍐呭 */
+ .scheduling-right {
+ flex: 1;
+ overflow-x: auto;
+ }
+
+ /* 琛ㄥご */
+ .scheduling-header {
+ height: 48px;
+ line-height: 48px;
+ padding: 0 20px;
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ background-color: #f3f4f6;
+ border-bottom: 1px solid #e5e7eb;
+ }
+
+ /* 浜哄憳淇℃伅琛� */
+ .scheduling-user {
+ display: flex;
+ align-items: center;
+ padding: 10px 10px;
+ border-bottom: 1px solid #e5e7eb;
+ transition: all 0.3s ease;
+ height: 65px;
+ box-sizing: border-box;
+ }
+
+ .scheduling-user:hover,
+ .scheduling-user-hover {
+ background-color: rgba(59, 130, 246, 0.05);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ }
+
+ /* 鐢ㄦ埛澶村儚 */
+ .user-avatar {
+ width: 42px;
+ height: 42px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #3b82f6, #60a5fa);
+ color: #fff;
+ font-size: 18px;
+ font-weight: 600;
+ text-align: center;
+ line-height: 42px;
+ margin-right: 16px;
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
+ transition: all 0.3s ease;
+ }
+
+ .scheduling-user:hover .user-avatar {
+ transform: scale(1.05);
+ box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
+ }
+
+ /* 鐢ㄦ埛璇︽儏 */
+ .user-details {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 100%;
+ }
+
+ /* 鐢ㄦ埛鍚� */
+ .user-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ margin: 0 0 6px 0;
+ line-height: 1.2;
+ }
+
+ /* 鐢ㄦ埛缁熻 */
+ .user-stats {
+ /* display: flex; */
+ /* flex-wrap: wrap;
+ gap: 10px; */
+ margin-bottom: 4px;
+ }
+
+ .stat-item {
+ font-size: 12px;
+ color: #666;
+ /* background-color: #f9fafb; */
+ /* padding: 2px 8px; */
+ padding-right: 4px;
+ /* border-radius: 10px; */
+ /* border: 1px solid #e5e7eb; */
+ /* transition: all 0.3s ease; */
+ }
+
+ .scheduling-user:hover .stat-item {
+ background-color: rgba(59, 130, 246, 0.1);
+ border-color: rgba(59, 130, 246, 0.3);
+ }
+
+ /* 鍚堣鍑哄嫟 */
+ .user-total {
+ display: flex;
+ align-items: center;
+ }
+
+ .total-label {
+ font-size: 12px;
+ color: #666;
+ margin-right: 6px;
+ font-weight: 500;
+ }
+
+ .total-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: #3b82f6;
+ background-color: rgba(59, 130, 246, 0.1);
+ padding: 2px 10px;
+ border-radius: 10px;
+ border: 1px solid rgba(59, 130, 246, 0.2);
+ }
+
+ /* 鏃ュ巻澶撮儴 */
+ .calendar-header {
+ display: flex;
+
+ border-bottom: 1px solid #e5e7eb;
+ }
+
+ .calendar-header-item {
+ width: 50px;
+ min-width: 50px;
+ height: 48px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border-right: 1px solid #e5e7eb;
+ background-color: #f3f4f6;
+ position: relative;
+ }
+
+ .week-number {
+ position: absolute;
+ top: 6px;
+ font-size: 10px;
+ font-weight: 600;
+ color: #3b82f6;
+ background-color: #dbeafe;
+ padding: 3px 6px;
+ border-radius: 12px;
+ box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2);
+ transition: all 0.3s ease;
+ }
+
+ .week-number:hover {
+ background-color: #3b82f6;
+ color: #fff;
+ transform: translateY(-1px);
+ }
+
+ .day-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ .day-number {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ }
+
+ .day-week {
+ font-size: 12px;
+ color: #666;
+ }
+
+ /* 鏃ュ巻涓讳綋 */
+ .calendar-body {
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* 鏃ュ巻琛� */
+ .calendar-row {
+ display: flex;
+ border-bottom: 1px solid #e5e7eb;
+ transition: all 0.3s ease;
+ }
+
+ .calendar-row:hover,
+ .calendar-row-hover {
+ background-color: rgba(59, 130, 246, 0.03);
+ }
+
+ /* 鏃ュ巻鍗曞厓鏍� */
+ .calendar-cell {
+ width: 50px;
+ min-width: 50px;
+ height: 65px;
+ border-right: 1px solid #e5e7eb;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* 鐝涓嬫媺妗� */
+ .shift-dropdown {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* 鐝妗� */
+ .shift-box {
+ width: 90%;
+ height: 80%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ font-size: 12px;
+ font-weight: 500;
+ transition: all 0.3s ease;
+ }
+
+ .shift-box:hover {
+ transform: scale(1.05);
+ }
+
+ /* 鐝绫诲瀷鏍峰紡 */
+ .shift-box-early {
+ background: rgba(59, 130, 246, 0.15);
+ color: #3b82f6;
+ }
+
+ .shift-box-mid {
+ background: rgba(139, 92, 246, 0.15);
+ color: #8b5cf6;
+ }
+
+ .shift-box-night {
+ background: rgba(245, 158, 11, 0.15);
+ color: #f59e0b;
+ }
+
+ .shift-box-rest {
+ background: rgba(16, 185, 129, 0.15);
+ color: #10b981;
+ }
+
+ .shift-box-leave {
+ background: rgba(239, 68, 68, 0.15);
+ color: #ef4444;
+ }
+
+ .shift-box-other {
+ background: rgba(236, 72, 153, 0.15);
+ color: #ec4899;
+ }
+
+ .shift-box-business {
+ background: rgba(17, 24, 39, 0.15);
+ color: #111827;
+ }
+
+ /* 鐝鏂囨湰 */
+ .shift-text {
+ text-align: center;
+ }
+
+ /* 骞村害琛ㄦ牸 */
+ .yearly-table {
+ display: flex;
+ width: 100%;
+ height: calc(100vh - 280px);
+ }
+
+ /* 骞村害鏃ュ巻 */
+ .yearly-calendar {
+ width: 100%;
+ }
+
+ /* 骞村害琛ㄥご */
+ .yearly-header {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ background-color: #f3f4f6;
+ border-bottom: 1px solid #e5e7eb;
+ }
+
+ .yearly-header-item {
+ height: 48px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-right: 1px solid #e5e7eb;
+ }
+
+ .month-name {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ }
+
+ /* 骞村害涓讳綋 */
+ .yearly-body {
+ display: flex;
+ flex-direction: column;
+ }
+
+ /* 骞村害琛� */
+ .yearly-row {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ border-bottom: 1px solid #e5e7eb;
+ transition: all 0.3s ease;
+ }
+
+ /* 骞村害鍗曞厓鏍� */
+ .yearly-cell {
+ padding: 12px;
+ border-right: 1px solid #e5e7eb;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ height: 65px;
+ }
+
+ /* 鏈堝害鍑哄嫟 */
+ .monthly-attendance {
+ margin-bottom: 8px;
+ }
+
+ .attendance-label {
+ font-size: 12px;
+ color: #666;
+ margin-right: 4px;
+ }
+
+ .attendance-value {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ }
+
+ /* 鏈堝害缁熻 */
+ .monthly-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ justify-content: center;
+ }
+
+ .monthly-stats .stat-item {
+ font-size: 11px;
+ }
+
+ /* 婊氬姩鏉℃牱寮� */
+ .scheduling-right::-webkit-scrollbar {
+ height: 8px;
+ }
+
+ .scheduling-right::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ }
+
+ .scheduling-right::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 4px;
+ }
+
+ .scheduling-right::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+ }
+
+ .search_label {
+ font-size: 14px;
+ font-weight: 500;
+ color: #333;
+ margin-bottom: 8px;
+ margin-top: 12px;
+ }
+</style>
diff --git a/src/views/personnelManagement/contractManagement/components/formDia.vue b/src/views/personnelManagement/contractManagement/components/formDia.vue
new file mode 100644
index 0000000..3db1bee
--- /dev/null
+++ b/src/views/personnelManagement/contractManagement/components/formDia.vue
@@ -0,0 +1,93 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible"
+ title="璇︽儏"
+ width="70%"
+ @close="closeDia">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ height="600"></PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <FileList v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ :record-type="'staff_contract'"
+ :record-id="recordId" />
+ </div>
+</template>
+
+<script setup>
+ import { ref, defineAsyncComponent, getCurrentInstance } from "vue";
+ import { findStaffContractListPage } from "@/api/personnelManagement/staffContract.js";
+ const FileList = defineAsyncComponent(() =>
+ import("@/components/Dialog/FileList.vue")
+ );
+ const { proxy } = getCurrentInstance();
+ const emit = defineEmits(["close"]);
+ const fileDialogVisible = ref(false);
+ const recordId = ref(0);
+ const dialogFormVisible = ref(false);
+ const operationType = ref("");
+ const tableColumn = ref([
+ {
+ label: "鍚堝悓骞撮檺",
+ prop: "contractTerm",
+ },
+ {
+ label: "鍚堝悓寮�濮嬫棩鏈�",
+ prop: "contractStartTime",
+ },
+ {
+ label: "鍚堝悓缁撴潫鏃ユ湡",
+ prop: "contractEndTime",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 120,
+ operation: [
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ recordId.value = row.id;
+ fileDialogVisible.value = true;
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+
+ // 鎵撳紑寮规
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === "edit") {
+ findStaffContractListPage({ staffOnJobId: row.id }).then(res => {
+ tableData.value = res.data.records;
+ });
+ }
+ };
+
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit("close");
+ };
+ defineExpose({
+ openDialog,
+ });
+</script>
+
+<style scoped>
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/contractManagement/filesDia.vue b/src/views/personnelManagement/contractManagement/filesDia.vue
new file mode 100644
index 0000000..02f9cef
--- /dev/null
+++ b/src/views/personnelManagement/contractManagement/filesDia.vue
@@ -0,0 +1,197 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ style="display: inline;margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ :page="page"
+ @selection-change="handleSelectionChange"
+ height="500"
+ @pagination="paginationSearch"
+ :total="page.total"
+ >
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import filePreview from '@/components/filePreview/index.vue'
+import {
+ fileAdd,
+ fileDel,
+ fileListPage
+} from "@/api/financialManagement/revenueManagement.js";
+import Pagination from "@/components/PIMTable/Pagination.vue";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const filePreviewRef = ref()
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: (row) => {
+ lookFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const accountType = ref('')
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row,type) => {
+ accountType.value = type;
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ fileListPage({accountId: currentId.value,accountType:accountType.value, ...page}).then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const fileRow = {}
+ fileRow.name = res.data.originalName
+ fileRow.url = res.data.tempPath
+ uploadFile(fileRow)
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+function uploadFile(file) {
+ file.accountId = currentId.value;
+ file.accountType = accountType.value;
+ fileAdd(file).then(res => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ fileDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 棰勮闄勪欢
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/contractManagement/index.vue b/src/views/personnelManagement/contractManagement/index.vue
new file mode 100644
index 0000000..ae0087e
--- /dev/null
+++ b/src/views/personnelManagement/contractManagement/index.vue
@@ -0,0 +1,333 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">濮撳悕锛�</span>
+ <el-input v-model="searchForm.staffName" style="width: 240px" placeholder="璇疯緭鍏ュ鍚嶆悳绱�" @change="handleQuery"
+ clearable :prefix-icon="Search" />
+ <span style="margin-left: 10px" class="search_title">鍚堝悓缁撴潫鏃ユ湡锛�</span>
+ <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+ placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <!-- <el-button type="primary" @click="openForm('add')">鏂板鍏ヨ亴</el-button>-->
+<!-- <el-button type="info" @click="handleImport">瀵煎叆</el-button>-->
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <!-- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>-->
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id" :column="tableColumn" :tableData="tableData" :page="page" :isSelection="true"
+ @selection-change="handleSelectionChange" :tableLoading="tableLoading" @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ <renew-contract
+ v-if="isShowRenewContractModal"
+ v-model:visible="isShowRenewContractModal"
+ :id="id"
+ @completed="handleQuery"
+ />
+
+ <!-- 鍚堝悓瀵煎叆瀵硅瘽妗� -->
+ <el-dialog
+ :title="upload.title"
+ v-model="upload.open"
+ width="400px"
+ append-to-body
+ >
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url + '?updateSupport=' + upload.updateSupport"
+ :disabled="upload.isUploading"
+ :on-progress="handleFileUploadProgress"
+ :on-success="handleFileSuccess"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <!-- <el-link
+ type="primary"
+ :underline="false"
+ style="font-size: 12px; vertical-align: baseline"
+ @click="importTemplate"
+ >涓嬭浇妯℃澘</el-link
+ > -->
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <files-dia ref="filesDia"></files-dia>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import { onMounted, ref, defineAsyncComponent } from "vue";
+import FormDia from "@/views/personnelManagement/contractManagement/components/formDia.vue";
+const RenewContract = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/RenewContract.vue"));
+import { ElMessageBox } from "element-plus";
+import { staffOnJobListPage } from "@/api/personnelManagement/staffOnJob.js";
+import dayjs from "dayjs";
+import { getToken } from "@/utils/auth.js";
+import FilesDia from "./filesDia.vue";
+const data = reactive({
+ searchForm: {
+ staffName: "",
+ entryDate: null, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "staffState",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 0) {
+ return "绂昏亴";
+ } else if (params == 1) {
+ return "鍦ㄨ亴";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params == 0) {
+ return "danger";
+ } else if (params == 1) {
+ return "primary";
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ label: "鍛樺伐缂栧彿",
+ prop: "staffNo",
+ },
+ {
+ label: "濮撳悕",
+ prop: "staffName",
+ },
+ {
+ label: "鎬у埆",
+ prop: "sex",
+ },
+ {
+ label: "鎴风睄浣忓潃",
+ prop: "nativePlace",
+ },
+ {
+ label: "宀椾綅",
+ prop: "postName",
+ },
+ {
+ label: "鐜颁綇鍧�",
+ prop: "adress",
+ width: 200
+ },
+ {
+ label: "绗竴瀛﹀巻",
+ prop: "firstStudy",
+ },
+ {
+ label: "涓撲笟",
+ prop: "profession",
+ width: 100
+ },
+ {
+ label: "骞撮緞",
+ prop: "age",
+ },
+ {
+ label: "鑱旂郴鐢佃瘽",
+ prop: "phone",
+ width: 150
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉",
+ prop: "emergencyContact",
+ width: 120
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉鐢佃瘽",
+ prop: "emergencyContactPhone",
+ width: 150
+ },
+ // {
+ // label: "鍚堝悓骞撮檺",
+ // prop: "contractTerm",
+ // },
+ // {
+ // label: "鍚堝悓寮�濮嬫棩鏈�",
+ // prop: "contractStartTime",
+ // width: 120
+ // },
+ {
+ label: "鍚堝悓缁撴潫鏃ユ湡",
+ prop: "contractExpireTime",
+ width: 120
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: 160,
+ operation: [
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ {
+ name: "缁鍚堝悓",
+ type: "text",
+ showHide: row => row.staffState === 1,
+ clickFun: (row) => {
+ isShowRenewContractModal.value = true;
+ id.value = row.id;
+ },
+ }
+ ],
+ },
+]);
+const filesDia = ref()
+const isShowRenewContractModal = ref(false);
+const id = ref(0);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const formDia = ref()
+const { proxy } = getCurrentInstance()
+
+const changeDaterange = (value) => {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ getList();
+};
+// 鎵撳紑闄勪欢寮规
+const openFilesFormDia = (row) => {
+ console.log(row)
+ nextTick(() => {
+ filesDia.value?.openDialog( row,'鍚堝悓')
+ })
+};
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined
+ params.staffState = 1
+ staffOnJobListPage(params).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/staff/staffOnJob/export", {staffState: 1}, "鍚堝悓绠$悊.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙鍚堝悓瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙鍚堝悓瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+ updateSupport: 1,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import",
+});
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImport() {
+ upload.title = "鍚堝悓瀵煎叆";
+ upload.open = true;
+}
+/** 鎻愪氦涓婁紶鏂囦欢 */
+function submitFileForm() {
+ console.log(upload.url + '?updateSupport=' + upload.updateSupport)
+ proxy.$refs["uploadRef"].submit();
+}
+/**鏂囦欢涓婁紶涓鐞� */
+const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true;
+};
+/** 鏂囦欢涓婁紶鎴愬姛澶勭悊 */
+const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false;
+ upload.isUploading = false;
+ proxy.$refs["uploadRef"].handleRemove(file);
+ getList();
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/personnelManagement/dimission/components/formDia.vue b/src/views/personnelManagement/dimission/components/formDia.vue
new file mode 100644
index 0000000..86c59ce
--- /dev/null
+++ b/src/views/personnelManagement/dimission/components/formDia.vue
@@ -0,0 +1,347 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板绂昏亴' : '缂栬緫绂昏亴'"
+ width="70%"
+ @close="closeDia"
+ >
+ <!-- 鍛樺伐淇℃伅灞曠ず鍖哄煙 -->
+ <div class="info-section">
+ <div class="info-title">鍛樺伐淇℃伅</div>
+ <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="濮撳悕锛�" prop="staffOnJobId">
+ <el-select v-model="form.staffOnJobId"
+ placeholder="璇烽�夋嫨浜哄憳"
+ style="width: 100%"
+ :disabled="operationType === 'edit'"
+ @change="handleSelect">
+ <el-option
+ v-for="item in personList"
+ :key="item.id"
+ :label="item.staffName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐缂栧彿锛�">
+ {{ currentStaffRecord.staffNo || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鎬у埆锛�">
+ {{ currentStaffRecord.sex || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎴风睄浣忓潃锛�">
+ {{ currentStaffRecord.nativePlace || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="宀椾綅锛�">
+ {{ currentStaffRecord.postName || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐜颁綇鍧�锛�">
+ {{ currentStaffRecord.adress || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绗竴瀛﹀巻锛�">
+ {{ currentStaffRecord.firstStudy || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓撲笟锛�">
+ {{ currentStaffRecord.profession || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="骞撮緞锛�">
+ {{ currentStaffRecord.age || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽锛�">
+ {{ currentStaffRecord.phone || '-' }}
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绱ф�ヨ仈绯讳汉锛�">
+ {{ currentStaffRecord.emergencyContact || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绱ф�ヨ仈绯讳汉鑱旂郴鐢佃瘽锛�">
+ {{ currentStaffRecord.emergencyContactPhone || '-' }}
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绂昏亴鏃ユ湡锛�" prop="leaveDate">
+ <el-date-picker
+ v-model="form.leaveDate"
+ type="date"
+ :disabled="operationType === 'edit'"
+ :disabled-date="disabledFutureDate"
+ placeholder="璇烽�夋嫨绂昏亴鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绂昏亴鍘熷洜锛�" prop="reason">
+ <el-select v-model="form.reason" placeholder="璇烽�夋嫨绂昏亴鍘熷洜" style="width: 100%" @change="handleSelectDimissionReason">
+ <el-option
+ v-for="(item, index) in dimissionReasonOptions"
+ :key="index"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="澶囨敞锛�" prop="remark" v-if="form.reason === 'other'">
+ <el-input
+ v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="澶囨敞"
+ maxlength="500"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+<!-- <el-row :gutter="30">-->
+<!-- <el-col :span="12">-->
+<!-- <div class="info-item">-->
+<!-- <span class="info-label">绂昏亴鍘熷洜锛�</span>-->
+<!-- <el-select v-model="form.reason" placeholder="璇烽�夋嫨浜哄憳" style="width: 100%" @change="handleSelect">-->
+<!-- <el-option-->
+<!-- v-for="(item, index) in dimissionReasonOptions"-->
+<!-- :key="index"-->
+<!-- :label="item.label"-->
+<!-- :value="item.value"-->
+<!-- />-->
+<!-- </el-select>-->
+<!-- </div>-->
+<!-- </el-col>-->
+<!-- <el-col :span="12">-->
+<!-- <div class="info-item">-->
+<!-- <span class="info-label">鍛樺伐缂栧彿锛�</span>-->
+<!-- <span class="info-value">{{ form.staffNo || '-' }}</span>-->
+<!-- </div>-->
+<!-- </el-col>-->
+<!-- </el-row>-->
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, toRefs, getCurrentInstance} from "vue";
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+import {createStaffLeave, updateStaffLeave} from "@/api/personnelManagement/staffLeave.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const getTodayDate = () => {
+ const now = new Date();
+ const year = now.getFullYear();
+ const month = `${now.getMonth() + 1}`.padStart(2, '0');
+ const day = `${now.getDate()}`.padStart(2, '0');
+ return `${year}-${month}-${day}`;
+};
+
+const disabledFutureDate = (time) => {
+ const todayEnd = new Date();
+ todayEnd.setHours(23, 59, 59, 999);
+ return time.getTime() > todayEnd.getTime();
+};
+const data = reactive({
+ form: {
+ staffOnJobId: undefined,
+ leaveDate: "",
+ reason: "",
+ remark: "",
+ },
+ rules: {
+ staffName: [{ required: true, message: "璇烽�夋嫨浜哄憳" }],
+ leaveDate: [{ required: true, message: "璇烽�夋嫨绂昏亴鏃ユ湡", trigger: "change" }],
+ reason: [{ required: true, message: "璇烽�夋嫨绂昏亴鍘熷洜"}],
+ },
+ dimissionReasonOptions: [
+ {label: '钖祫寰呴亣', value: 'salary'},
+ {label: '鑱屼笟鍙戝睍', value: 'career_development'},
+ {label: '宸ヤ綔鐜', value: 'work_environment'},
+ {label: '涓汉鍘熷洜', value: 'personal_reason'},
+ {label: '鍏朵粬', value: 'other'},
+ ],
+ currentStaffRecord: {},
+});
+const { form, rules, dimissionReasonOptions, currentStaffRecord } = toRefs(data);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === 'edit') {
+ currentStaffRecord.value = row
+ form.value.staffOnJobId = row.staffOnJobId
+ form.value.leaveDate = row.leaveDate
+ form.value.reason = row.reason
+ form.value.remark = row.remark
+ personList.value = [
+ {
+ staffName: row.staffName,
+ id: row.staffOnJobId,
+ }
+ ]
+ } else {
+ form.value.leaveDate = getTodayDate()
+ getList()
+ }
+}
+
+const handleSelectDimissionReason = (val) => {
+ if (val === 'other') {
+ form.value.remark = ''
+ }
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ form.value.staffState = 0
+ if (form.value.reason !== 'other') {
+ form.value.remark = ''
+ }
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ createStaffLeave(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ } else {
+ updateStaffLeave(currentStaffRecord.value.id, form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ }
+ })
+
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ // 琛ㄥ崟宸叉敞閲婏紝鎵嬪姩閲嶇疆琛ㄥ崟鏁版嵁
+ form.value = {
+ staffOnJobId: undefined,
+ leaveDate: "",
+ reason: "",
+ remark: "",
+ };
+ dialogFormVisible.value = false;
+ emit('close')
+};
+
+const personList = ref([]);
+
+/**
+ * 鑾峰彇褰撳墠鍦ㄨ亴浜哄憳鍒楄〃
+ */
+const getList = () => {
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1
+ }).then(res => {
+ personList.value = res.data.records || []
+ })
+};
+
+const handleSelect = (val) => {
+ let obj = personList.value.find(item => item.id === val)
+ currentStaffRecord.value = {}
+ if (obj) {
+ // 淇濈暀绂昏亴鏃ユ湡鍜岀鑱屽師鍥狅紝鍙洿鏂板憳宸ヤ俊鎭�
+ currentStaffRecord.value = obj
+ }
+}
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+.info-section {
+ background: #f5f7fa;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+}
+
+.info-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 20px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #e4e7ed;
+}
+
+.info-item {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16px;
+ min-height: 32px;
+}
+
+.info-label {
+ min-width: 140px;
+ color: #606266;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.info-value {
+ flex: 1;
+ color: #303133;
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/personnelManagement/dimission/index.vue b/src/views/personnelManagement/dimission/index.vue
new file mode 100644
index 0000000..c2b8c3e
--- /dev/null
+++ b/src/views/personnelManagement/dimission/index.vue
@@ -0,0 +1,243 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">濮撳悕锛�</span>
+ <el-input
+ v-model="searchForm.staffName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ鍚嶆悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鏂板绂昏亴</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <!-- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button> -->
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import {onMounted, ref} from "vue";
+import FormDia from "@/views/personnelManagement/dimission/components/formDia.vue";
+import {findStaffLeaveListPage, batchDeleteStaffLeaves} from "@/api/personnelManagement/staffLeave.js";
+import {ElMessageBox} from "element-plus";
+
+const data = reactive({
+ searchForm: {
+ staffName: "",
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "staffState",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 0) {
+ return "绂昏亴";
+ } else if (params == 1) {
+ return "鍦ㄨ亴";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params == 0) {
+ return "danger";
+ } else if (params == 1) {
+ return "primary";
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ label: "绂昏亴鏃ユ湡",
+ prop: "leaveDate",
+ },
+ {
+ label: "鍛樺伐缂栧彿",
+ prop: "staffNo",
+ },
+ {
+ label: "濮撳悕",
+ prop: "staffName",
+ },
+ {
+ label: "鎬у埆",
+ prop: "sex",
+ },
+ {
+ label: "鎴风睄浣忓潃",
+ prop: "nativePlace",
+ },
+ {
+ label: "閮ㄩ棬",
+ prop: "deptName",
+ },
+ {
+ label: "宀椾綅",
+ prop: "postName",
+ },
+ {
+ label: "鐜颁綇鍧�",
+ prop: "adress",
+ width:200
+ },
+ {
+ label: "绗竴瀛﹀巻",
+ prop: "firstStudy",
+ },
+ {
+ label: "涓撲笟",
+ prop: "profession",
+ width:100
+ },
+ {
+ label: "骞撮緞",
+ prop: "age",
+ },
+ {
+ label: "鑱旂郴鐢佃瘽",
+ prop: "phone",
+ width:150
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉",
+ prop: "emergencyContact",
+ width: 120
+ },
+ {
+ label: "绱ф�ヨ仈绯讳汉鐢佃瘽",
+ prop: "emergencyContactPhone",
+ width:150
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const formDia = ref()
+const { proxy } = getCurrentInstance()
+
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ findStaffLeaveListPage({...page, ...searchForm.value}).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchDeleteStaffLeaves(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/staff/staffLeave/export", {}, "浜哄憳绂昏亴.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue b/src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue
new file mode 100644
index 0000000..0aa4f06
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/BasicInfoSection.vue
@@ -0,0 +1,181 @@
+<template>
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鍩烘湰淇℃伅
+ </span>
+ </template>
+
+ <el-row :gutter="24">
+ <el-col :span="5">
+ <el-form-item label="鍛樺伐缂栧彿" prop="staffNo">
+ <el-input
+ v-model="form.staffNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ :disabled="operationType !== 'add'"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="濮撳悕" prop="staffName">
+ <el-input
+ v-model="form.staffName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="鍒悕" prop="alias">
+ <el-input
+ v-model="form.alias"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="鎵嬫満" prop="phone">
+ <el-input
+ v-model="form.phone"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="11"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鎬у埆" prop="sex">
+ <el-select
+ v-model="form.sex"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="鐢�" value="鐢�" />
+ <el-option label="濂�" value="濂�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="24">
+ <el-col :span="5">
+ <el-form-item label="鍑虹敓鏃ユ湡" prop="birthDate">
+ <el-date-picker
+ v-model="form.birthDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="骞撮緞" prop="age">
+ <el-input-number
+ v-model="form.age"
+ :min="0"
+ :max="150"
+ :precision="0"
+ :step="1"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="绫嶈疮" prop="nativePlace">
+ <el-input
+ v-model="form.nativePlace"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="5">
+ <el-form-item label="姘戞棌" prop="nation">
+ <el-input
+ v-model="form.nation"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="濠氬Щ鐘跺喌" prop="maritalStatus">
+ <el-select
+ v-model="form.maritalStatus"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="鏈" value="鏈" />
+ <el-option label="宸插" value="宸插" />
+ <el-option label="绂诲紓" value="绂诲紓" />
+ <el-option label="涓у伓" value="涓у伓" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="24">
+ <el-col :span="10">
+ <el-form-item label="瑙掕壊" prop="roleId">
+ <el-select
+ v-model="form.roleId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in roleOptions"
+ :key="item.roleId"
+ :label="item.roleName"
+ :value="item.roleId"
+ :disabled="item.status == 1"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+ operationType: { type: String, default: "add" },
+ roleOptions: { type: Array, default: () => [] },
+});
+
+const { form, operationType, roleOptions } = toRefs(props);
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue b/src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue
new file mode 100644
index 0000000..c1470e7
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/EducationWorkSection.vue
@@ -0,0 +1,263 @@
+<template>
+ <div>
+ <!-- 鏁欒偛缁忓巻 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鏁欒偛缁忓巻
+ </span>
+ </template>
+ <el-table :data="form.staffEducationList" border>
+ <el-table-column label="瀛﹀巻" prop="education" width="120">
+ <template #default="{ row }">
+ <el-select
+ v-model="row.education"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option label="涓笓鍙婁互涓�" value="secondary" />
+ <el-option label="澶т笓" value="junior_college" />
+ <el-option label="鏈" value="bachelor" />
+ <el-option label="纭曞+" value="master" />
+ <el-option label="鍗氬+鍙婁互涓�" value="doctor" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="姣曚笟闄㈡牎" prop="schoolName" min-width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.schoolName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="30"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏ュ鏃堕棿" prop="enrollTime" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.enrollTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="姣曚笟鏃堕棿" prop="graduateTime" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.graduateTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="涓撲笟" prop="major" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.major"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="瀛︿綅" prop="degree" width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.degree"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffEducationList.length > 1"
+ type="primary"
+ link
+ @click="removeEducationRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addEducationRow">鏂板缓涓�琛�</div>
+ </el-card>
+
+ <!-- 宸ヤ綔缁忓巻 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 宸ヤ綔缁忓巻
+ </span>
+ </template>
+ <el-table :data="form.staffWorkExperienceList" border>
+ <el-table-column label="鍓嶅叕鍙�" prop="formerCompany" min-width="180">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerCompany"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="30"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍓嶅叕鍙搁儴闂�" prop="formerDept" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerDept"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍓嶅叕鍙歌亴浣�" prop="formerPosition" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.formerPosition"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="寮�濮嬫棩鏈�" prop="startDate" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.startDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="缁撴潫鏃ユ湡" prop="endDate" width="150">
+ <template #default="{ row }">
+ <el-date-picker
+ v-model="row.endDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ヤ綔鎻忚堪" prop="workDesc" min-width="220">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.workDesc"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="500"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffWorkExperienceList.length > 1"
+ type="primary"
+ link
+ @click="removeWorkRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addWorkRow">鏂板缓涓�琛�</div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+});
+
+const emit = defineEmits(["update:form"]);
+
+const { form } = toRefs(props);
+
+const addEducationRow = () => {
+ form.value.staffEducationList.push({
+ education: "",
+ schoolName: "",
+ enrollTime: "",
+ graduateTime: "",
+ major: "",
+ degree: "",
+ });
+};
+
+const removeEducationRow = (index) => {
+ if (form.value.staffEducationList.length <= 1) return;
+ form.value.staffEducationList.splice(index, 1);
+};
+
+const addWorkRow = () => {
+ form.value.staffWorkExperienceList.push({
+ formerCompany: "",
+ formerDept: "",
+ formerPosition: "",
+ startDate: "",
+ endDate: "",
+ workDesc: "",
+ });
+};
+
+const removeWorkRow = (index) => {
+ if (form.value.staffWorkExperienceList.length <= 1) return;
+ form.value.staffWorkExperienceList.splice(index, 1);
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.table-add-row {
+ margin-top: 8px;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue b/src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue
new file mode 100644
index 0000000..bd63608
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/EmergencyAndAttachmentSection.vue
@@ -0,0 +1,115 @@
+<template>
+ <div>
+ <!-- 绱ф�ヨ仈绯讳汉 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 绱ф�ヨ仈绯讳汉
+ </span>
+ </template>
+ <el-table :data="form.staffEmergencyContactList" border>
+ <el-table-column label="绱ф�ヨ仈绯讳汉濮撳悕" prop="contactName" min-width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉鍏崇郴" prop="contactRelation" min-width="140">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactRelation"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉鎵嬫満" prop="contactPhone" width="160">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactPhone"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="11"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绱ф�ヨ仈绯讳汉浣忓潃" prop="contactAddress" min-width="220">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.contactAddress"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button
+ v-if="form.staffEmergencyContactList.length > 1"
+ type="primary"
+ link
+ @click="removeEmergencyRow(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="table-add-row" @click="addEmergencyRow">鏂板缓涓�琛�</div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true }
+});
+
+const { form } = toRefs(props);
+
+const addEmergencyRow = () => {
+ form.value.staffEmergencyContactList.push({
+ contactName: "",
+ contactRelation: "",
+ contactPhone: "",
+ contactAddress: "",
+ });
+};
+
+const removeEmergencyRow = (index) => {
+ if (form.value.staffEmergencyContactList.length <= 1) return;
+ form.value.staffEmergencyContactList.splice(index, 1);
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.table-add-row {
+ margin-top: 8px;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue b/src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue
new file mode 100644
index 0000000..be33436
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue
@@ -0,0 +1,176 @@
+<template>
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title">
+ <span class="card-title-line">|</span>
+ 鍦ㄨ亴淇℃伅
+ </span>
+ </template>
+
+ <!-- 绗竴琛岋細鍚堝悓寮�濮� / 鍚堝悓缁撴潫 / 璇曠敤鏈� / 杞 -->
+ <el-row :gutter="24">
+ <el-col :span="6">
+ <el-form-item label="鍏ヨ亴鏃ユ湡" prop="contractStartTime">
+ <el-date-picker
+ v-model="form.contractStartTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item
+ label="鍚堝悓缁撴潫鏃ユ湡"
+ prop="contractEndTime"
+ required
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鍚堝悓缁撴潫鏃ユ湡',
+ trigger: 'change',
+ },
+ ]"
+ >
+ <el-date-picker
+ v-model="form.contractEndTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璇曠敤鏈燂紙鏈堬級" prop="probationPeriod">
+ <el-input-number
+ v-model="form.proTerm"
+ :min="0"
+ :max="24"
+ :precision="0"
+ :step="1"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="杞鏃ユ湡" prop="positiveDate">
+ <el-date-picker
+ v-model="form.positiveDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 绗簩琛岋細閮ㄩ棬 / 宀椾綅 / 鍩烘湰宸ヨ祫 -->
+ <el-row :gutter="24">
+ <el-col :span="8">
+ <el-form-item label="閮ㄩ棬" prop="sysDeptId">
+ <el-tree-select
+ v-model="form.sysDeptId"
+ :data="deptOptions"
+ check-strictly
+ :render-after-expand="false"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="宀椾綅" prop="sysPostId">
+ <el-select
+ v-model="form.sysPostId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in postOptions"
+ :key="item.postId"
+ :label="item.postName"
+ :value="item.postId"
+ :disabled="item.status === '1'"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鍩烘湰宸ヨ祫" prop="basicSalary">
+ <el-input-number
+ v-model="form.basicSalary"
+ :min="0"
+ :max="999999"
+ :precision="2"
+ :step="100"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-card>
+</template>
+
+<script setup>
+import { toRefs } from "vue";
+
+const props = defineProps({
+ form: { type: Object, required: true },
+ postOptions: { type: Array, default: () => [] },
+ deptOptions: { type: Array, default: () => [] },
+});
+
+const { form, postOptions, deptOptions } = toRefs(props);
+
+// 璁$畻鍚堝悓骞撮檺
+const calculateContractTerm = () => {
+ if (form.value.contractStartTime && form.value.contractEndTime) {
+ const startDate = new Date(form.value.contractStartTime);
+ const endDate = new Date(form.value.contractEndTime);
+
+ if (endDate > startDate) {
+ // 璁$畻骞翠唤宸�
+ const yearDiff = endDate.getFullYear() - startDate.getFullYear();
+ const monthDiff = endDate.getMonth() - startDate.getMonth();
+ const dayDiff = endDate.getDate() - startDate.getDate();
+
+ let years = yearDiff;
+
+ // 濡傛灉缁撴潫鏃ユ湡鐨勬湀鏃ュ皬浜庡紑濮嬫棩鏈熺殑鏈堟棩锛屽垯鍑忓幓1骞�
+ if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
+ years = yearDiff - 1;
+ }
+
+ form.value.contractTerm = Math.max(0, years);
+ } else {
+ form.value.contractTerm = 0;
+ }
+ } else {
+ form.value.contractTerm = 0;
+ }
+};
+</script>
+
+<style scoped>
+.form-card {
+ margin-bottom: 16px;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+</style>
+
diff --git a/src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue b/src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue
new file mode 100644
index 0000000..2ad06fb
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue
@@ -0,0 +1,304 @@
+<template>
+ <FormDialog
+ v-model="dialogFormVisible"
+ :operation-type="operationType"
+ :title="dialogTitle"
+ width="90%"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia"
+ >
+ <div class="form-dia-body">
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ >
+ <BasicInfoSection
+ :form="form"
+ :operation-type="operationType"
+ :role-options="roleOptions"
+ />
+ <JobInfoSection
+ :form="form"
+ :post-options="postOptions"
+ :dept-options="deptOptions"
+ />
+ <EducationWorkSection :form="form" />
+ <EmergencyAndAttachmentSection :form="form" />
+ </el-form>
+ </div>
+ </FormDialog>
+</template>
+
+<script setup>
+import {
+ ref,
+ reactive,
+ toRefs,
+ onMounted,
+ getCurrentInstance,
+ nextTick,
+} from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { findPostOptions } from "@/api/system/post.js";
+import { deptTreeSelect, getUser } from "@/api/system/user.js";
+import {
+ staffOnJobInfo,
+ createStaffOnJob,
+ updateStaffOnJob,
+} from "@/api/personnelManagement/staffOnJob.js";
+
+import BasicInfoSection from "./BasicInfoSection.vue";
+import JobInfoSection from "./JobInfoSection.vue";
+import EducationWorkSection from "./EducationWorkSection.vue";
+import EmergencyAndAttachmentSection from "./EmergencyAndAttachmentSection.vue";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(["close"]);
+
+const dialogFormVisible = ref(false);
+const operationType = ref("add");
+const id = ref(0);
+const formRef = ref(null);
+
+const dialogTitle = () =>
+ operationType.value === "add" ? "鏂板鍏ヨ亴" : "缂栬緫浜哄憳";
+
+const createEmptyEducation = () => ({
+ education: "",
+ schoolName: "",
+ enrollTime: "",
+ graduateTime: "",
+ major: "",
+ degree: "",
+});
+
+const createEmptyWork = () => ({
+ formerCompany: "",
+ formerDept: "",
+ formerPosition: "",
+ startDate: "",
+ endDate: "",
+ workDesc: "",
+});
+
+const createEmptyEmergency = () => ({
+ contactName: "",
+ contactRelation: "",
+ contactPhone: "",
+ contactAddress: "",
+});
+
+const createDefaultForm = () => ({
+ id: undefined,
+ // 鍩烘湰淇℃伅
+ staffNo: "",
+ staffName: "",
+ alias: "",
+ phone: "",
+ sex: "",
+ birthDate: "",
+ age: undefined,
+ nativePlace: "",
+ nation: "",
+ maritalStatus: "",
+ politicalStatus: "",
+ firstWorkDate: "",
+ workingYears: undefined,
+ idCardNo: "",
+ hukouType: "",
+ email: "",
+ currentAddress: "",
+ // 鍦ㄨ亴淇℃伅
+ contractStartTime: "",
+ contractEndTime: "",
+ proTerm: undefined,
+ positiveDate: "",
+ sysDeptId: undefined,
+ sysPostId: undefined,
+ basicSalary: undefined,
+ // 閾惰鍗′俊鎭�
+ bankName: "",
+ bankCardNo: "",
+ // 鏁欒偛缁忓巻
+ staffEducationList: [createEmptyEducation()],
+ // 宸ヤ綔缁忓巻
+ staffWorkExperienceList: [createEmptyWork()],
+ // 绱ф�ヨ仈绯讳汉
+ staffEmergencyContactList: [createEmptyEmergency()],
+ // 瑙掕壊锛堝崟閫夛級
+ roleId: undefined,
+});
+
+const state = reactive({
+ form: createDefaultForm(),
+ rules: {
+ staffNo: [{ required: true, message: "璇疯緭鍏ュ憳宸ョ紪鍙�", trigger: "blur" }],
+ staffName: [{ required: true, message: "璇疯緭鍏ュ鍚�", trigger: "blur" }],
+ phone: [{ required: true, message: "璇疯緭鍏ユ墜鏈�", trigger: "blur" }],
+ sex: [{ required: true, message: "璇烽�夋嫨鎬у埆", trigger: "change" }],
+ birthDate: [
+ { required: true, message: "璇烽�夋嫨鍑虹敓鏃ユ湡", trigger: "change" },
+ ],
+ contractStartTime: [
+ { required: true, message: "璇烽�夋嫨鍏ヨ亴鏃ユ湡", trigger: "change" },
+ ],
+ contractEndTime: [
+ { required: true, message: "璇烽�夋嫨鍚堝悓缁撴潫鏃ユ湡", trigger: "change" },
+ ],
+ sysDeptId: [
+ { required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" },
+ ],
+ roleId: [{ required: true, message: "璇烽�夋嫨瑙掕壊", trigger: "change" }],
+ },
+ postOptions: [],
+ deptOptions: [],
+});
+
+const { form, rules, postOptions, deptOptions } = toRefs(state);
+const roleOptions = ref([]);
+
+const resetForm = () => {
+ Object.assign(form.value, createDefaultForm());
+ nextTick(() => {
+ formRef.value?.clearValidate();
+ });
+};
+
+const fetchPostOptions = () => {
+ findPostOptions().then((res) => {
+ postOptions.value = res.data || [];
+ });
+};
+
+const fetchDeptOptions = () => {
+ deptTreeSelect().then((response) => {
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data || []))
+ );
+ });
+};
+
+const fetchRoleOptions = () => {
+ getUser().then((res) => {
+ roleOptions.value = res.roles || [];
+ });
+};
+
+function filterDisabledDept(deptList) {
+ return deptList.filter((dept) => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+}
+
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ fetchPostOptions();
+ fetchDeptOptions();
+ fetchRoleOptions();
+ resetForm();
+ if (type === "edit" && row?.id) {
+ id.value = row.id;
+ staffOnJobInfo(id.value, {}).then((res) => {
+ const d = res.data || {};
+ Object.assign(form.value, {
+ ...form.value,
+ ...d,
+ });
+ if (
+ !Array.isArray(form.value.staffEducationList) ||
+ !form.value.staffEducationList.length
+ ) {
+ form.value.staffEducationList = [createEmptyEducation()];
+ }
+ if (
+ !Array.isArray(form.value.staffWorkExperienceList) ||
+ !form.value.staffWorkExperienceList.length
+ ) {
+ form.value.staffWorkExperienceList = [createEmptyWork()];
+ }
+ if (
+ !Array.isArray(form.value.staffEmergencyContactList) ||
+ !form.value.staffEmergencyContactList.length
+ ) {
+ form.value.staffEmergencyContactList = [createEmptyEmergency()];
+ }
+ if (form.value.sysPostId === 0) {
+ form.value.sysPostId = undefined;
+ }
+ if (form.value.sysDeptId === 0) {
+ form.value.sysDeptId = undefined;
+ }
+ });
+ }
+};
+
+onMounted(() => {
+ fetchPostOptions();
+ fetchDeptOptions();
+});
+
+const submitForm = () => {
+ if (!form.value.sysPostId) {
+ form.value.sysPostId = undefined;
+ }
+ if (!form.value.sysDeptId) {
+ form.value.sysDeptId = undefined;
+ }
+ // 鍏煎鍚庣鍙兘浠嶄娇鐢� roleIds 鏁扮粍
+ form.value.roleIds = form.value.roleId ? [form.value.roleId] : [];
+ formRef.value?.validate((valid) => {
+ if (valid) {
+ if (operationType.value === "add") {
+ createStaffOnJob(form.value).then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ } else {
+ updateStaffOnJob(id.value, form.value).then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ }
+ }
+ });
+};
+
+const closeDia = () => {
+ formRef.value?.resetFields();
+ dialogFormVisible.value = false;
+ emit("close");
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+.form-dia-body {
+ padding: 0;
+}
+
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+
+.form-card {
+ margin-bottom: 16px;
+}
+
+.dialog-footer {
+ text-align: right;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/employeeRecord/components/RenewContract.vue b/src/views/personnelManagement/employeeRecord/components/RenewContract.vue
new file mode 100644
index 0000000..9c2acfc
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/RenewContract.vue
@@ -0,0 +1,141 @@
+<template>
+ <el-dialog
+ v-model="isShow"
+ title="缁鍚堝悓"
+ width="800px"
+ @close="closeModal"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-form-item label="鍚堝悓寮�濮嬫棩鏈燂細" prop="contractStartTime">
+ <el-date-picker
+ v-model="form.contractStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ <el-form-item label="鍚堝悓缁撴潫鏃ユ湡锛�" prop="contractEndTime">
+ <el-date-picker
+ v-model="form.contractEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ @change="calculateContractTerm"
+ />
+ </el-form-item>
+ <el-form-item label="鍚堝悓骞撮檺锛�" prop="contractTerm">
+ <el-input-number v-model="form.contractTerm" :precision="0" :step="1" style="width: 100%" :disabled="true"/>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+// 缁鍚堝悓
+import { renewContract } from "@/api/personnelManagement/staffOnJob.js";
+import {computed, getCurrentInstance,} from "vue";
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+const data = reactive({
+ form: {
+ contractTerm: 0,
+ contractStartTime: "",
+ contractEndTime: "",
+ },
+ rules: {
+ contractTerm: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ contractStartTime: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ contractEndTime: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ }
+});
+const { form, rules } = toRefs(data);
+let { proxy } = getCurrentInstance()
+
+const props = defineProps({
+ id: {
+ type: Number,
+ default: 0,
+ },
+
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+})
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+// 璁$畻鍚堝悓骞撮檺
+const calculateContractTerm = () => {
+ if (form.value.contractStartTime && form.value.contractEndTime) {
+ const startDate = new Date(form.value.contractStartTime);
+ const endDate = new Date(form.value.contractEndTime);
+
+ if (endDate > startDate) {
+ // 璁$畻骞翠唤宸�
+ const yearDiff = endDate.getFullYear() - startDate.getFullYear();
+ const monthDiff = endDate.getMonth() - startDate.getMonth();
+ const dayDiff = endDate.getDate() - startDate.getDate();
+
+ let years = yearDiff;
+
+ // 濡傛灉缁撴潫鏃ユ湡鐨勬湀鏃ュ皬浜庡紑濮嬫棩鏈熺殑鏈堟棩锛屽垯鍑忓幓1骞�
+ if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
+ years = yearDiff - 1;
+ }
+
+ form.value.contractTerm = Math.max(0, years);
+ } else {
+ form.value.contractTerm = 0;
+ }
+ } else {
+ form.value.contractTerm = 0;
+ }
+};
+
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ renewContract(props.id, form.value).then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("缁鍚堝悓鎴愬姛");
+ emit('completed');
+ closeModal();
+ }
+ })
+ }
+ })
+}
+
+// 鍏抽棴寮规
+const closeModal = () => {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ form.value = {
+ contractTerm: 0,
+ contractStartTime: "",
+ contractEndTime: "",
+ };
+ isShow.value = false;
+};
+</script>
diff --git a/src/views/personnelManagement/employeeRecord/components/Show.vue b/src/views/personnelManagement/employeeRecord/components/Show.vue
new file mode 100644
index 0000000..9220d45
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/components/Show.vue
@@ -0,0 +1,73 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="璇︽儏"
+ width="70%"
+ @close="closeDia"
+ >
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ height="600"
+ ></PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {staffOnJobInfo} from "@/api/personnelManagement/staffOnJob.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const tableColumn = ref([
+ // {
+ // label: "鍚堝悓骞撮檺",
+ // prop: "contractTerm",
+ // },
+ {
+ label: "鍚堝悓寮�濮嬫棩鏈�",
+ prop: "contractStartTime",
+ },
+ {
+ label: "鍚堝悓缁撴潫鏃ユ湡",
+ prop: "contractEndTime",
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === 'edit') {
+ staffOnJobInfo({staffNo: row.staffNo}).then(res => {
+ tableData.value = res.data
+ })
+ }
+}
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/employeeRecord/index.vue b/src/views/personnelManagement/employeeRecord/index.vue
new file mode 100644
index 0000000..a0699b0
--- /dev/null
+++ b/src/views/personnelManagement/employeeRecord/index.vue
@@ -0,0 +1,416 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">濮撳悕锛�</span>
+ <el-input
+ v-model="searchForm.staffName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ鍚嶆悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <span class="search_title search_title2">閮ㄩ棬锛�</span>
+ <el-tree-select
+ v-model="searchForm.sysDeptId"
+ :data="deptOptions"
+ check-strictly
+ :render-after-expand="false"
+ style="width: 240px"
+ placeholder="璇烽�夋嫨"
+ />
+ <span class="search_title search_title2">鍏ヨ亴鏃ユ湡锛�</span>
+ <el-date-picker
+ v-model="searchForm.contractStartTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ />
+ <!-- <span style="margin-left: 10px" class="search_title">鍚堝悓缁撴潫鏃ユ湡锛�</span> -->
+ <!-- <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+ placeholder="璇烽�夋嫨" clearable @change="changeDaterange" /> -->
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button type="primary" @click="openFormNewOrEditFormDia('add')">鏂板鍏ヨ亴</el-button>
+ <el-button type="info" @click="handleImport">瀵煎叆</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <!-- <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button> -->
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ >
+ <template #positiveDate="{ row }">
+ <span :class="getPositiveDateClass(row.positiveDate)">{{ row.positiveDate }}</span>
+ </template>
+ </PIMTable>
+ </div>
+ <show-form-dia ref="formDia" @close="handleQuery"></show-form-dia>
+ <new-or-edit-form-dia ref="formDiaNewOrEditFormDia" @close="handleQuery"></new-or-edit-form-dia>
+
+ <!-- 瀵煎叆瀵硅瘽妗� -->
+ <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+ <el-upload
+ ref="uploadRef"
+ :limit="1"
+ accept=".xlsx, .xls"
+ :headers="upload.headers"
+ :action="upload.url"
+ :disabled="upload.isUploading"
+ :on-progress="handleFileUploadProgress"
+ :on-success="handleFileSuccess"
+ :auto-upload="false"
+ drag
+ >
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">涓嬭浇妯℃澘</el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { Search, UploadFilled } from "@element-plus/icons-vue";
+import {onMounted, ref} from "vue";
+import {ElMessageBox} from "element-plus";
+import { deptTreeSelect } from "@/api/system/user.js";
+import {batchDeleteStaffOnJobs, staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+import { getToken } from "@/utils/auth";
+import dayjs from "dayjs";
+
+const NewOrEditFormDia = defineAsyncComponent(() => import("@/views/personnelManagement/employeeRecord/components/NewOrEditFormDia.vue"));
+const ShowFormDia = defineAsyncComponent(() => import( "@/views/personnelManagement/employeeRecord/components/Show.vue"));
+
+const data = reactive({
+ searchForm: {
+ staffName: "",
+ entryDate: undefined, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ deptOptions: [],
+});
+const { searchForm, deptOptions } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "staffState",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 0) {
+ return "绂昏亴";
+ } else if (params == 1) {
+ return "鍦ㄨ亴";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params == 0) {
+ return "danger";
+ } else if (params == 1) {
+ return "primary";
+ } else {
+ return null;
+ }
+ },
+ },
+ {
+ label: "鍛樺伐缂栧彿",
+ prop: "staffNo",
+ },
+ {
+ label: "濮撳悕",
+ prop: "staffName",
+ },
+ {
+ label: "鍒悕",
+ prop: "alias",
+ },
+ {
+ label: "鎵嬫満",
+ prop: "phone",
+ width: 150,
+ },
+ {
+ label: "鎬у埆",
+ prop: "sex",
+ },
+ {
+ label: "鍑虹敓鏃ユ湡",
+ prop: "birthDate",
+ width: 120,
+ },
+ {
+ label: "鍏ヨ亴鏃ユ湡",
+ prop: "contractStartTime",
+ width: 120,
+ },
+ {
+ label: "杞鏃ユ湡",
+ prop: "positiveDate",
+ width: 120,
+ dataType: "slot",
+ slot: "positiveDate",
+ },
+ {
+ label: "骞撮緞",
+ prop: "age",
+ },
+ {
+ label: "绫嶈疮",
+ prop: "nativePlace",
+ },
+ {
+ label: "姘戞棌",
+ prop: "nation",
+ width: 100,
+ },
+ {
+ label: "濠氬Щ鐘跺喌",
+ prop: "maritalStatus",
+ width: 100,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ width: 180,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openFormNewOrEditFormDia("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0
+});
+const formDia = ref()
+const formDiaNewOrEditFormDia = ref()
+const { proxy } = getCurrentInstance()
+
+// 瀵煎叆鐩稿叧
+const uploadRef = ref(null)
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞�
+ open: false,
+ // 寮瑰嚭灞傛爣棰�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/staff/staffOnJob/import"
+})
+
+// 鍒ゆ柇杞鏃ユ湡鏄惁鍦�7澶╁唴
+const getPositiveDateClass = (positiveDate) => {
+ if (!positiveDate) return '';
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const positive = new Date(positiveDate);
+ positive.setHours(0, 0, 0, 0);
+ const diffTime = positive - today;
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+ // 7澶╁唴杞锛堝寘鎷粖澶╋級鏄剧ず璀﹀憡鑹�
+ if (diffDays >= 0 && diffDays <= 7) {
+ return 'positive-date-warning';
+ }
+ return '';
+};
+
+const fetchDeptOptions = () => {
+ deptTreeSelect().then(response => {
+ console.log(response.data)
+ deptOptions.value = filterDisabledDept(
+ JSON.parse(JSON.stringify(response.data))
+ );
+ });
+ };
+const filterDisabledDept = deptList => {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false;
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children);
+ }
+ return true;
+ });
+ };
+const changeDaterange = (value) => {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ getList();
+};
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ fetchDeptOptions();
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined
+ staffOnJobListPage({...params}).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+const openFormNewOrEditFormDia = (type, row) => {
+ nextTick(() => {
+ formDiaNewOrEditFormDia.value?.openDialog(type, row)
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchDeleteStaffOnJobs(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/staff/staffOnJob/export", {staffState: 1}, "鍛樺伐鍙拌处.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎叆鎸夐挳鎿嶄綔
+const handleImport = () => {
+ upload.title = "鍛樺伐瀵煎叆"
+ upload.open = true
+}
+
+// 涓嬭浇妯℃澘鎿嶄綔
+const importTemplate = () => {
+ proxy.download("/staff/staffOnJob/downloadTemplate", {}, `鍛樺伐瀵煎叆妯℃澘_${new Date().getTime()}.xlsx`)
+}
+
+// 鏂囦欢涓婁紶涓鐞�
+const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true
+}
+
+// 鏂囦欢涓婁紶鎴愬姛澶勭悊
+const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false
+ upload.isUploading = false
+ proxy.$refs["uploadRef"].handleRemove(file)
+ if (response.code !== 200) {
+ proxy.$modal.msgError(response.msg)
+ } else {
+ proxy.$modal.msgSuccess(response.msg)
+ }
+ getList()
+}
+
+// 鎻愪氦涓婁紶鏂囦欢
+const submitFileForm = () => {
+ proxy.$refs["uploadRef"].submit()
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+.search_title2 {
+ margin-left: 10px;
+}
+
+.positive-date-warning {
+ color: #f56c6c;
+ font-weight: bold;
+}
+</style>
diff --git a/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue b/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue
new file mode 100644
index 0000000..48a41f0
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/components/auditDia.vue
@@ -0,0 +1,216 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ title="宸ヨ祫瀹℃牳"
+ width="900px"
+ @close="handleClose"
+ >
+ <!-- 宸ヨ祫琛ㄥ熀纭�淇℃伅 -->
+ <el-card shadow="never" style="margin-bottom: 16px;">
+ <template #header>
+ <span>宸ヨ祫琛ㄤ俊鎭�</span>
+ </template>
+ <el-descriptions :column="3" border>
+ <el-descriptions-item label="宸ヨ祫涓婚">{{ auditData?.salaryTitle || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="宸ヨ祫鏈堜唤">{{ auditData?.salaryMonth || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="宸ヨ祫鎬婚">楼 {{ formatMoney(auditData?.totalSalary) }}</el-descriptions-item>
+ <el-descriptions-item label="鏀粯閾惰">{{ auditData?.payBank || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳浜�">{{ auditData?.auditUserName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">{{ auditData?.remark || '-' }}</el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+
+ <!-- 鍛樺伐宸ヨ祫鏄庣粏 -->
+ <el-card shadow="never" style="margin-bottom: 16px;">
+ <template #header>
+ <span>鍛樺伐宸ヨ祫鏄庣粏</span>
+ </template>
+ <div v-if="!employeeList || employeeList.length === 0" style="text-align: center; padding: 20px; color: #909399;">
+ <div>鏆傛棤鍛樺伐宸ヨ祫鏄庣粏鏁版嵁</div>
+ <div style="font-size: 12px; margin-top: 5px;">鍛樺伐鏄庣粏鏁版嵁闇�瑕佸湪宸ヨ祫琛ㄧ敓鎴愭垨缂栬緫鏃舵墠浼氫繚瀛�</div>
+ </div>
+ <div v-else>
+ <el-table :data="employeeList" border max-height="300" style="width: 100%">
+ <el-table-column prop="staffName" label="鍛樺伐濮撳悕" width="100" />
+ <el-table-column prop="deptName" label="閮ㄩ棬" width="120" />
+ <el-table-column prop="basicSalary" label="鍩烘湰宸ヨ祫" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.basicSalary) }}</template>
+ </el-table-column>
+ <el-table-column prop="pieceSalary" label="璁′欢宸ヨ祫" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.pieceSalary) }}</template>
+ </el-table-column>
+ <el-table-column prop="hourlySalary" label="璁℃椂宸ヨ祫" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.hourlySalary) }}</template>
+ </el-table-column>
+ <el-table-column prop="otherIncome" label="鍏朵粬鏀跺叆" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.otherIncome) }}</template>
+ </el-table-column>
+ <el-table-column prop="socialPersonal" label="绀句繚涓汉" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.socialPersonal) }}</template>
+ </el-table-column>
+ <el-table-column prop="fundPersonal" label="鍏Н閲戜釜浜�" width="120" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.fundPersonal) }}</template>
+ </el-table-column>
+ <el-table-column prop="salaryTax" label="宸ヨ祫涓◣" width="100" align="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.salaryTax) }}</template>
+ </el-table-column>
+ <el-table-column prop="netSalary" label="瀹炲彂宸ヨ祫" width="100" align="right" fixed="right">
+ <template #default="{ row }">楼 {{ formatMoney(row.netSalary) }}</template>
+ </el-table-column>
+ </el-table>
+ <div style="margin-top: 10px; text-align: right; font-weight: bold;">
+ 宸ヨ祫鎬婚锛毬� {{ formatMoney(totalSalary) }}
+ </div>
+ </div>
+ </el-card>
+
+ <!-- 瀹℃牳鎿嶄綔 -->
+ <el-form label-position="top">
+ <el-form-item label="瀹℃牳缁撴灉" required>
+ <el-radio-group v-model="auditResult">
+ <el-radio :value="4">閫氳繃</el-radio>
+ <el-radio :value="2">涓嶉�氳繃</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <el-button type="primary" :loading="loading" @click="handleConfirm">
+ 纭畾
+ </el-button>
+ <el-button @click="handleClose">鍙栨秷</el-button>
+ </template>
+ </FormDialog>
+</template>
+
+<script setup>
+import { ref, computed, reactive, toRefs, getCurrentInstance, watch } from "vue";
+import { ElMessage } from "element-plus";
+import Cookies from "js-cookie";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { staffSalaryMainUpdate } from "@/api/personnelManagement/staffSalaryMain.js";
+
+const emit = defineEmits(["update:modelValue", "close", "success"]);
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ row: { type: Object, default: () => ({}) },
+});
+
+const { proxy } = getCurrentInstance();
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit("update:modelValue", val),
+});
+
+const loading = ref(false);
+const auditResult = ref(4); // 榛樿閫氳繃
+const auditData = ref({});
+const employeeList = ref([]);
+
+// 鐩戝惉row鏁版嵁鍙樺寲
+watch(() => props.row, (newRow) => {
+ if (newRow && Object.keys(newRow).length > 0) {
+ loadAuditData(newRow);
+ }
+}, { immediate: true });
+
+// 鏍煎紡鍖栭噾棰�
+const formatMoney = (value) => {
+ const num = Number(value) || 0;
+ return num.toFixed(2);
+};
+
+// 璁$畻宸ヨ祫鎬婚
+const totalSalary = computed(() => {
+ return employeeList.value.reduce((sum, e) => {
+ const salary = Number(e.netSalary) || 0;
+ return sum + salary;
+ }, 0);
+});
+
+// 鍔犺浇瀹℃牳鏁版嵁
+const loadAuditData = (row) => {
+ auditData.value = row || {};
+ auditResult.value = 4; // 榛樿閫夋嫨閫氳繃
+
+ // 鍔犺浇鍛樺伐宸ヨ祫鏄庣粏鏁版嵁
+ if (row?.staffSalaryDetailList && Array.isArray(row.staffSalaryDetailList)) {
+ employeeList.value = row.staffSalaryDetailList.map((e) => ({
+ staffName: e.staffName ?? "",
+ deptName: e.deptName ?? "",
+ basicSalary: Number(e.basicSalary) || 0,
+ pieceSalary: Number(e.pieceSalary) || 0,
+ hourlySalary: Number(e.hourlySalary) || 0,
+ otherIncome: Number(e.otherIncome) || 0,
+ socialPersonal: Number(e.socialPersonal) || 0,
+ fundPersonal: Number(e.fundPersonal) || 0,
+ salaryTax: Number(e.salaryTax) || 0,
+ netSalary: Number(e.netSalary) || 0,
+ }));
+ } else {
+ console.log('娌℃湁鎵惧埌鍛樺伐鏄庣粏鏁版嵁');
+ employeeList.value = [];
+ }
+};
+
+// 鎵撳紑寮圭獥
+const openDialog = (row) => {
+ loadAuditData(row);
+ dialogVisible.value = true;
+};
+
+// 鍏抽棴寮圭獥
+const handleClose = () => {
+ dialogVisible.value = false;
+ emit("close");
+};
+
+// 纭瀹℃牳
+const handleConfirm = () => {
+ try {
+ const row = auditData.value;
+ if (!row?.id) {
+ ElMessage.warning("鏁版嵁寮傚父锛岃閲嶈瘯");
+ return;
+ }
+
+ const username = Cookies.get("username") || "";
+ const userIdRaw = Cookies.get("userId");
+ const auditUserId = userIdRaw ? Number(userIdRaw) : undefined;
+
+ // 鏋勫缓瀹℃牳鏁版嵁
+ const submitData = {
+ id: row.id,
+ status: Number(auditResult.value) === 2 ? 2 : 4, // 2=涓嶉�氳繃 4=閫氳繃(寰呭彂鏀�)
+ auditUserId,
+ auditUserName: username,
+ };
+ loading.value = true;
+ staffSalaryMainUpdate(submitData)
+ .then(() => {
+ ElMessage.success("瀹℃牳鎴愬姛");
+ dialogVisible.value = false;
+ emit("success");
+ })
+ .catch((error) => {
+ console.error('瀹℃牳澶辫触:', error)
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ } catch (error) {
+ console.error('瀹℃牳澶勭悊寮傚父:', error);
+ loading.value = false;
+ }
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped>
+:deep(.el-descriptions__label) {
+ width: 100px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue b/src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue
new file mode 100644
index 0000000..b5fad15
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/components/bankSettingDia.vue
@@ -0,0 +1,188 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ operation-type="edit"
+ title="璁剧疆鍙戞斁閾惰涓嬫媺鏁版嵁"
+ width="640px"
+ @close="handleClose"
+ @confirm="handleConfirm"
+ @cancel="handleCancel"
+ >
+ <el-form ref="formRef" :model="form" label-position="top">
+ <el-row :gutter="16">
+ <el-col :span="24" style="display: flex; justify-content: end; gap: 10px;margin-bottom: 10px">
+ <el-button type="primary" @click="addBank">鏂板閾惰</el-button>
+ <el-button @click="resetToEmpty">娓呯┖</el-button>
+ </el-col>
+ </el-row>
+
+ <el-table :data="form.banks" border style="width: 100%">
+ <el-table-column label="閾惰鍚嶇О" min-width="260">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.bankName"
+ placeholder="渚嬪锛氫腑鍥藉伐鍟嗛摱琛�"
+ clearable
+ maxlength="50"
+ show-word-limit
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="90" align="center">
+ <template #default="{ $index }">
+ <el-button type="danger" link @click="removeBank($index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div style="margin-top: 10px; color: #909399; font-size: 12px">
+ 鎻愮ず锛氳繖閲岀淮鎶ょ殑鏄�滃彂鏀鹃摱琛屸�濅笅鎷夋閫夐」鏁版嵁锛涗繚瀛樺悗鍦ㄦ柊寤�/缂栬緫宸ヨ祫琛ㄤ腑鍙�夋嫨銆�
+ </div>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+import { computed, reactive, ref, toRefs, watch, getCurrentInstance } from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { bankAdd, bankDelete, bankList, bankUpdate } from "@/api/personnelManagement/bank.js";
+
+const emit = defineEmits(["update:modelValue", "close", "saved"]);
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+});
+
+const { proxy } = getCurrentInstance();
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit("update:modelValue", val),
+});
+
+const formRef = ref(null);
+
+const data = reactive({
+ form: {
+ banks: [],
+ },
+});
+
+const { form } = toRefs(data);
+
+function newKey() {
+ return Math.random().toString(36).slice(2);
+}
+
+const addBank = () => {
+ form.value.banks.push({
+ _key: newKey(),
+ id: undefined,
+ bankName: "",
+ _originBankName: "",
+ });
+};
+
+const removeBank = (index) => {
+ const row = form.value.banks?.[index];
+ if (!row) return;
+ // 鏈惤搴撶殑琛岋細鐩存帴绉婚櫎
+ if (!row.id) {
+ form.value.banks.splice(index, 1);
+ return;
+ }
+ // 宸茶惤搴擄細璋冪敤鍚庣鍒犻櫎
+ bankDelete([row.id]).then(() => {
+ proxy?.$modal?.msgSuccess?.("鍒犻櫎鎴愬姛");
+ form.value.banks.splice(index, 1);
+ emit("saved");
+ });
+};
+
+const resetToEmpty = () => {
+ if (!form.value.banks?.length) return;
+ const ids = form.value.banks.map((b) => b?.id).filter(Boolean);
+ // 鑻ュ叏閮ㄦ槸鏈繚瀛樿锛屽垯浠呮竻绌烘湰鍦�
+ if (!ids.length) {
+ form.value.banks = [];
+ return;
+ }
+ proxy?.$modal
+ ?.confirm?.("纭畾娓呯┖鎵�鏈夐摱琛屽悧锛�")
+ .then(() => bankDelete(ids))
+ .then(() => {
+ proxy?.$modal?.msgSuccess?.("娓呯┖鎴愬姛");
+ form.value.banks = [];
+ emit("saved");
+ })
+ .catch(() => {});
+};
+
+const loadSetting = () => {
+ return bankList().then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ form.value.banks = list.map((b) => ({
+ _key: newKey(),
+ id: b?.id,
+ bankName: b?.bankName ?? "",
+ _originBankName: b?.bankName ?? "",
+ }));
+ });
+};
+
+const openDialog = () => {
+ loadSetting();
+};
+
+watch(
+ () => dialogVisible.value,
+ (val) => {
+ if (val) openDialog();
+ }
+);
+
+const handleConfirm = () => {
+ const names = (form.value.banks || [])
+ .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
+ .filter((n) => n !== "");
+ const unique = Array.from(new Set(names));
+ if (!unique.length) {
+ proxy?.$modal?.msgWarning?.("璇疯嚦灏戞柊澧炰竴涓摱琛岄�夐」");
+ return;
+ }
+ if (unique.length !== names.length) {
+ proxy?.$modal?.msgWarning?.("閾惰鍚嶇О涓嶅彲閲嶅");
+ return;
+ }
+
+ const rows = form.value.banks.map((b) => ({
+ ...b,
+ bankName: b?.bankName == null ? "" : String(b.bankName).trim(),
+ }));
+
+ const toAdd = rows.filter((b) => !b.id && b.bankName);
+ const toUpdate = rows.filter((b) => b.id && b.bankName && b.bankName !== (b._originBankName ?? ""));
+
+ Promise.all([
+ ...toAdd.map((b) => bankAdd({ bankName: b.bankName })),
+ ...toUpdate.map((b) => bankUpdate({ id: b.id, bankName: b.bankName })),
+ ])
+ .then(() => loadSetting())
+ .then(() => {
+ proxy?.$modal?.msgSuccess?.("淇濆瓨鎴愬姛");
+ dialogVisible.value = false;
+ emit("saved", { options: unique });
+ });
+};
+
+const handleCancel = () => {
+ dialogVisible.value = false;
+};
+
+const handleClose = () => {
+ emit("close");
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped></style>
+
diff --git a/src/views/personnelManagement/monthlyStatistics/components/formDia.vue b/src/views/personnelManagement/monthlyStatistics/components/formDia.vue
new file mode 100644
index 0000000..36c2ec3
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/components/formDia.vue
@@ -0,0 +1,804 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ :title="operationType === 'add' ? '鏂板缓宸ヨ祫琛�' : '缂栬緫宸ヨ祫琛�'"
+ width="90%"
+ @close="closeDia"
+ >
+ <template #footer>
+ <el-button type="info" @click="saveDraft">淇濆瓨鑽夌</el-button>
+ <el-button type="primary" @click="submitForm">纭鎻愪氦</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </template>
+ <div class="form-dia-body">
+ <!-- 鍩虹璧勬枡 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title"><span class="card-title-line">|</span> 鍩虹璧勬枡</span>
+ <el-icon class="card-collapse"><ArrowUp /></el-icon>
+ </template>
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+ <el-row :gutter="24">
+ <el-col :span="6">
+ <el-form-item label="宸ヨ祫涓婚" prop="salaryTitle">
+ <el-input
+ v-model="form.salaryTitle"
+ placeholder="璇疯緭鍏�"
+ clearable
+ maxlength="20"
+ show-word-limit
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閫夋嫨閮ㄩ棬" prop="deptIds">
+ <el-select
+ v-model="form.deptIds"
+ placeholder="璇烽�夋嫨"
+ clearable
+ multiple
+ collapse-tags-tooltip
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in deptOptions"
+ :key="item.deptId"
+ :label="item.deptName"
+ :value="item.deptId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閫夋嫨宸ヨ祫鏈堜唤" prop="salaryMonth">
+ <el-date-picker
+ v-model="form.salaryMonth"
+ type="month"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨宸ヨ祫鏈堜唤"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="form.remark"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="24">
+ <el-col :span="6">
+ <el-form-item label="鏀粯閾惰" prop="payBank">
+ <el-select
+ v-model="form.payBank"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="b in bankOptions"
+ :key="b"
+ :label="b"
+ :value="b"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹℃牳浜�" prop="auditUserId">
+ <el-select
+ v-model="form.auditUserId"
+ placeholder="璇烽�夋嫨瀹℃牳浜�"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </el-card>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="toolbar">
+ <el-button type="primary" @click="handleGenerate">鐢熸垚宸ヨ祫琛�</el-button>
+ <el-button @click="handleClear">娓呯┖</el-button>
+ <el-button @click="handleBatchDelete">鍒犻櫎</el-button>
+ <el-button @click="handleTaxForm">涓◣琛�</el-button>
+ </div>
+
+ <!-- 鍛樺伐宸ヨ祫璇︽儏琛ㄦ牸 -->
+ <div class="employee-table-wrap">
+ <el-table
+ ref="employeeTableRef"
+ :data="employeeList"
+ border
+ max-height="400"
+ @selection-change="onEmployeeSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鍛樺伐濮撳悕" prop="staffName" minWidth="100" />
+ <el-table-column label="閮ㄩ棬" prop="deptName" minWidth="100" />
+ <el-table-column label="鍩烘湰宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.basicSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.basicSalary = parseNum(row.basicSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="璁′欢宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.pieceSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.pieceSalary = parseNum(row.pieceSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="璁℃椂宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.hourlySalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.hourlySalary = parseNum(row.hourlySalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏朵粬鏀跺叆" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.otherIncome"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.otherIncome = parseNum(row.otherIncome)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="绀句繚涓汉" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.socialPersonal"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.socialPersonal = parseNum(row.socialPersonal)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏Н閲戜釜浜�" minWidth="120">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.fundPersonal"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.fundPersonal = parseNum(row.fundPersonal)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍏朵粬鏀嚭" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.otherDeduct"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.otherDeduct = parseNum(row.otherDeduct)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ヨ祫涓◣" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.salaryTax"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.salaryTax = parseNum(row.salaryTax)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴斿彂宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.grossSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.grossSalary = parseNum(row.grossSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="搴旀墸宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.deductSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.deductSalary = parseNum(row.deductSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹炲彂宸ヨ祫" minWidth="110">
+ <template #default="{ row }">
+ <el-input
+ v-model.number="row.netSalary"
+ type="number"
+ placeholder="0"
+ size="small"
+ @input="row.netSalary = parseNum(row.netSalary)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" minWidth="120">
+ <template #default="{ row }">
+ <el-input
+ v-model="row.remark"
+ placeholder="璇疯緭鍏�"
+ size="small"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center" fixed="right">
+ <template #default="{ row }">
+ <el-button type="primary" link @click="removeEmployee(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div v-if="!employeeList.length" class="table-empty">鏆傛棤鏁版嵁</div>
+ <div v-else class="salary-total">
+ <span class="total-label">宸ヨ祫鎬婚锛�</span>
+ <span class="total-value">楼 {{ totalSalary.toFixed(2) }}</span>
+ </div>
+ </div>
+ </div>
+
+
+
+ <!-- 鏂板浜哄憳寮圭獥 -->
+ <el-dialog
+ v-model="addPersonVisible"
+ title="鏂板浜哄憳"
+ width="400px"
+ append-to-body
+ @close="addPersonClose"
+ >
+ <div class="add-person-tree">
+ <el-tree
+ ref="personTreeRef"
+ :data="deptStaffTree"
+ show-checkbox
+ node-key="id"
+ :props="{ label: 'label', children: 'children' }"
+ default-expand-all
+ />
+ </div>
+ <template #footer>
+ <el-button @click="addPersonVisible = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="confirmAddPerson">纭畾</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 涓◣琛ㄥ脊绐� -->
+ <el-dialog
+ v-model="taxDialogVisible"
+ title="涓◣琛�"
+ width="700px"
+ append-to-body
+ >
+ <div class="tax-desc">涓汉鎵�寰楃◣鍏嶅緛棰濓細5000鍏�</div>
+ <el-table :data="taxTableData" border style="width: 100%;margin-bottom: 20px;">
+ <el-table-column prop="level" label="绾ф暟" width="80" align="center" />
+ <el-table-column
+ prop="range"
+ label="鍏ㄥ勾搴旂撼绋庢墍寰楅/鍏�"
+ min-width="220"
+ />
+ <el-table-column
+ prop="rate"
+ label="绋庣巼(%)"
+ width="100"
+ align="center"
+ />
+ <el-table-column
+ prop="quickDeduction"
+ label="閫熺畻鎵i櫎鏁�/鍏�"
+ width="160"
+ align="center"
+ />
+ </el-table>
+ </el-dialog>
+ </FormDialog>
+</template>
+
+<script setup>
+import { ref, reactive, toRefs, computed, getCurrentInstance, nextTick } from "vue";
+import { ArrowUp } from "@element-plus/icons-vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { listDept } from "@/api/system/dept.js";
+import { staffOnJobList } from "@/api/personnelManagement/monthlyStatistics.js";
+import { bankList } from "@/api/personnelManagement/bank.js";
+import {
+ staffSalaryMainAdd,
+ staffSalaryMainUpdate,
+ staffSalaryMainCalculateSalary,
+} from "@/api/personnelManagement/staffSalaryMain.js";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+
+
+const emit = defineEmits(["update:modelValue", "close"]);
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ operationType: { type: String, default: "add" },
+ row: { type: Object, default: () => ({}) },
+});
+
+const { proxy } = getCurrentInstance();
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit("update:modelValue", val),
+});
+
+const formRef = ref(null);
+const employeeTableRef = ref(null);
+const personTreeRef = ref(null);
+const addPersonVisible = ref(false);
+const taxDialogVisible = ref(false);
+const deptOptions = ref([]);
+const deptStaffTree = ref([]);
+const employeeList = ref([]);
+const selectedEmployees = ref([]);
+const bankOptions = ref([]);
+const userList = ref([]);
+const taxTableData = ref([
+ { level: 1, range: "涓嶈秴杩�36000鍏�", rate: 3, quickDeduction: 0 },
+ { level: 2, range: "瓒呰繃36000-144000鍏�", rate: 10, quickDeduction: 2520 },
+ { level: 3, range: "瓒呰繃144000-300000鍏�", rate: 20, quickDeduction: 16920 },
+ { level: 4, range: "瓒呰繃300000-420000鍏�", rate: 25, quickDeduction: 31920 },
+ { level: 5, range: "瓒呰繃420000-660000鍏�", rate: 30, quickDeduction: 52920 },
+ { level: 6, range: "瓒呰繃660000-960000鍏�", rate: 35, quickDeduction: 85920 },
+ { level: 7, range: "瓒呰繃960000鍏�", rate: 45, quickDeduction: 181920 },
+]);
+
+function parseNum(v) {
+ if (v === "" || v == null) return 0;
+ const n = Number(v);
+ return isNaN(n) ? 0 : n;
+}
+
+// 鍩虹璧勬枡琛ㄥ崟
+const data = reactive({
+ form: {
+ id: undefined,
+ salaryTitle: "",
+ deptIds: [],
+ salaryMonth: "",
+ remark: "",
+ payBank: "",
+ auditUserId: undefined,
+ },
+ rules: {
+ salaryTitle: [{ required: true, message: "璇疯緭鍏ュ伐璧勪富棰�", trigger: "blur" }],
+ deptIds: [{ required: true, message: "璇烽�夋嫨閮ㄩ棬", trigger: "change" }],
+ salaryMonth: [{ required: true, message: "璇烽�夋嫨宸ヨ祫鏈堜唤", trigger: "change" }],
+ auditUserId: [{ required: true, message: "璇烽�夋嫨瀹℃牳浜�", trigger: "change" }],
+ },
+});
+const { form, rules } = toRefs(data);
+
+// 璁$畻宸ヨ祫鎬婚锛堟墍鏈夊憳宸ュ疄鍙戝伐璧勪箣鍜岋級
+const totalSalary = computed(() => {
+ return employeeList.value.reduce((sum, e) => sum + parseNum(e.netSalary), 0);
+});
+
+// 鏍规嵁瀹℃牳浜篒D鑾峰彇瀹℃牳浜哄悕绉�
+const auditUserName = computed(() => {
+ if (!form.value.auditUserId) return "";
+ const user = userList.value.find(u => u.userId === form.value.auditUserId);
+ return user ? user.nickName : "";
+});
+
+const loadBankOptions = () => {
+ return bankList().then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ bankOptions.value = list
+ .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
+ .filter((v) => v !== "");
+ });
+};
+
+const loadUserList = () => {
+ return userListNoPageByTenantId().then((res) => {
+ userList.value = res.data || [];
+ });
+};
+
+// 鎵佸钩鍖栭儴闂ㄦ爲渚涗笅鎷変娇鐢�
+function flattenDept(tree, list = []) {
+ if (!tree?.length) return list;
+ tree.forEach((node) => {
+ list.push({ deptId: node.deptId, deptName: node.deptName });
+ if (node.children?.length) flattenDept(node.children, list);
+ });
+ return list;
+}
+
+const loadDeptOptions = () => {
+ listDept().then((res) => {
+ const tree = res.data ?? [];
+ deptOptions.value = flattenDept(tree);
+ });
+};
+
+// 鏋勫缓 閮ㄩ棬-浜哄憳 鏍戯紙鐢ㄤ簬鏂板浜哄憳寮圭獥锛�
+const loadDeptStaffTree = () => {
+ Promise.all([listDept(), staffOnJobList()]).then(([deptRes, staffRes]) => {
+ const tree = deptRes.data ?? [];
+ const staffList = staffRes.data ?? [];
+ const deptMap = new Map();
+ function walk(nodes) {
+ nodes.forEach((node) => {
+ deptMap.set(node.deptId, {
+ id: "dept_" + node.deptId,
+ deptId: node.deptId,
+ label: node.deptName,
+ type: "dept",
+ children: [],
+ });
+ if (node.children?.length) walk(node.children);
+ });
+ }
+ walk(tree);
+ staffList.forEach((s) => {
+ const deptId = s.deptId ?? s.dept_id;
+ const node = deptMap.get(deptId);
+ if (node) {
+ node.children.push({
+ id: s.id ?? s.staffId,
+ staffId: s.id ?? s.staffId,
+ label: s.staffName ?? s.name,
+ type: "staff",
+ ...s,
+ });
+ }
+ });
+ deptStaffTree.value = Array.from(deptMap.values()).filter(
+ (n) => n.children && n.children.length > 0
+ );
+ });
+};
+
+const openDialog = (type, row) => {
+ nextTick(() => {
+ loadDeptOptions();
+ loadBankOptions();
+ loadUserList();
+ employeeList.value = [];
+ Object.assign(form.value, {
+ id: undefined,
+ salaryTitle: "",
+ deptIds: [],
+ salaryMonth: "",
+ remark: "",
+ payBank: "",
+ auditUserId: undefined,
+ });
+ // 缂栬緫锛氬垪琛ㄩ〉宸茶繑鍥炰富琛ㄥ瓧娈碉紱杩欓噷鍙仛鍥炴樉锛堟槑缁嗙敱鈥滅敓鎴愬伐璧勮〃/璁$畻宸ヨ祫鈥濆緱鍒帮級
+ if (type === "edit" && row?.id) {
+ form.value.id = row.id;
+ form.value.salaryTitle = row.salaryTitle ?? "";
+ // deptIds 鍚庣鏄瓧绗︿覆锛堝涓敤閫楀彿鍒嗛殧锛夛紱褰撳墠琛ㄥ崟浠嶆槸鍗曢�� deptId
+ form.value.deptIds = row.deptIds
+ ? String(row.deptIds).split(",").map((id) => Number(id.trim())).filter(Boolean)
+ : [];
+ form.value.salaryMonth = row.salaryMonth ?? "";
+ form.value.remark = row.remark ?? "";
+ form.value.payBank = row.payBank ?? "";
+ form.value.auditUserId = row.auditUserId ?? undefined;
+
+ // 濡傛灉鏈夊憳宸ユ槑缁嗘暟鎹紝鐩存帴鍙嶆樉
+ if (row.staffSalaryDetailList && row.staffSalaryDetailList.length > 0) {
+ employeeList.value = row.staffSalaryDetailList.map((e) => ({
+ staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
+ id: e.staffOnJobId ?? e.staffId ?? e.id,
+ staffName: e.staffName ?? "",
+ postName: e.postName ?? "",
+ deptName: e.deptName ?? "",
+ basicSalary: parseNum(e.basicSalary),
+ pieceSalary: parseNum(e.pieceSalary),
+ hourlySalary: parseNum(e.hourlySalary),
+ otherIncome: parseNum(e.otherIncome),
+ socialPersonal: parseNum(e.socialPersonal),
+ fundPersonal: parseNum(e.fundPersonal),
+ otherDeduct: parseNum(e.otherDeduct),
+ salaryTax: parseNum(e.salaryTax),
+ grossSalary: parseNum(e.grossSalary),
+ deductSalary: parseNum(e.deductSalary),
+ netSalary: parseNum(e.netSalary),
+ remark: e.remark ?? "",
+ }));
+ }
+ }
+ });
+};
+
+const openAddPerson = () => {
+ loadDeptStaffTree();
+ addPersonVisible.value = true;
+ nextTick(() => {
+ personTreeRef.value?.setCheckedKeys([]);
+ });
+};
+
+const addPersonClose = () => {};
+
+const confirmAddPerson = () => {
+ const tree = personTreeRef.value;
+ if (!tree) {
+ addPersonVisible.value = false;
+ return;
+ }
+ const checked = tree.getCheckedNodes();
+ const staffNodes = checked.filter((n) => n.type === "staff");
+ const existIds = new Set(employeeList.value.map((e) => e.staffId || e.id));
+ staffNodes.forEach((node) => {
+ const id = node.staffId ?? node.id;
+ if (existIds.has(id)) return;
+ existIds.add(id);
+ employeeList.value.push({
+ staffOnJobId: id,
+ id,
+ staffName: node.label,
+ postName: node.postName ?? node.post ?? "",
+ deptName: node.deptName ?? "",
+ basicSalary: 0,
+ pieceSalary: 0,
+ hourlySalary: 0,
+ otherIncome: 0,
+ socialPersonal: 0,
+ fundPersonal: 0,
+ otherDeduct: 0,
+ salaryTax: 0,
+ grossSalary: 0,
+ deductSalary: 0,
+ netSalary: 0,
+ remark: "",
+ });
+ });
+ addPersonVisible.value = false;
+};
+
+const removeEmployee = (row) => {
+ employeeList.value = employeeList.value.filter(
+ (e) => (e.staffOnJobId || e.id) !== (row.staffOnJobId || row.id)
+ );
+};
+
+const onEmployeeSelectionChange = (selection) => {
+ selectedEmployees.value = selection;
+};
+
+const handleBatchDelete = () => {
+ if (!selectedEmployees.value?.length) {
+ proxy.$modal.msgWarning("璇峰厛鍕鹃�夎鍒犻櫎鐨勫憳宸�");
+ return;
+ }
+ const ids = new Set(selectedEmployees.value.map((e) => e.staffOnJobId || e.id));
+ employeeList.value = employeeList.value.filter(
+ (e) => !ids.has(e.staffOnJobId || e.id)
+ );
+};
+
+const handleGenerate = () => {
+ if (!form.value.deptIds?.length) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨閮ㄩ棬");
+ return;
+ }
+ if (!form.value.salaryMonth) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨宸ヨ祫鏈堜唤");
+ return;
+ }
+ const payload = {
+ ids: form.value.deptIds,
+ date: form.value.salaryMonth,
+ };
+ staffSalaryMainCalculateSalary(payload).then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ if (!list.length) {
+ proxy.$modal.msgWarning("鏈绠楀埌宸ヨ祫鏁版嵁");
+ return;
+ }
+ employeeList.value = list.map((e) => ({
+ ...e,
+ staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
+ staffName: e.staffName,
+ postName: e.postName,
+ deptName: e.deptName,
+ basicSalary: parseNum(e.basicSalary),
+ pieceSalary: parseNum(e.pieceSalary),
+ hourlySalary: parseNum(e.hourlySalary),
+ otherIncome: parseNum(e.otherIncome),
+ socialPersonal: parseNum(e.socialPersonal),
+ fundPersonal: parseNum(e.fundPersonal),
+ otherDeduct: parseNum(e.otherDeduct),
+ salaryTax: parseNum(e.salaryTax),
+ grossSalary: parseNum(e.grossSalary),
+ deductSalary: parseNum(e.deductSalary),
+ netSalary: parseNum(e.netSalary),
+ remark: e.remark ?? "",
+ }));
+ proxy.$modal.msgSuccess("鐢熸垚鎴愬姛");
+ });
+};
+
+const handleClear = () => {
+ proxy.$modal.confirm("纭畾娓呯┖褰撳墠鍛樺伐鍒楄〃鍚楋紵").then(() => {
+ employeeList.value = [];
+ }).catch(() => {});
+};
+
+const handleTaxForm = () => {
+ taxDialogVisible.value = true;
+};
+
+const submitForm = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ saveData(3); // 纭鎻愪氦锛岀姸鎬佷负3锛堝緟瀹℃牳锛�
+ });
+};
+
+const saveDraft = () => {
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ saveData(1); // 淇濆瓨鑽夌锛岀姸鎬佷负1锛堣崏绋匡級
+ });
+};
+
+const saveData = (status) => {
+ const payload = {
+ id: form.value.id,
+ salaryTitle: form.value.salaryTitle,
+ deptIds: form.value.deptIds?.length ? form.value.deptIds.join(",") : "",
+ salaryMonth: form.value.salaryMonth,
+ remark: form.value.remark,
+ payBank: form.value.payBank,
+ auditUserId: form.value.auditUserId,
+ auditUserName: auditUserName.value,
+ totalSalary: totalSalary.value,
+ staffSalaryDetailList: employeeList.value.map((e) => ({
+ staffOnJobId: e.staffOnJobId ?? e.staffId ?? e.id,
+ staffName: e.staffName,
+ postName: e.postName ?? "",
+ deptName: e.deptName ?? "",
+ basicSalary: parseNum(e.basicSalary),
+ pieceSalary: parseNum(e.pieceSalary),
+ hourlySalary: parseNum(e.hourlySalary),
+ otherIncome: parseNum(e.otherIncome),
+ socialPersonal: parseNum(e.socialPersonal),
+ fundPersonal: parseNum(e.fundPersonal),
+ otherDeduct: parseNum(e.otherDeduct),
+ salaryTax: parseNum(e.salaryTax),
+ grossSalary: parseNum(e.grossSalary),
+ deductSalary: parseNum(e.deductSalary),
+ netSalary: parseNum(e.netSalary),
+ remark: e.remark ?? "",
+ })),
+ };
+ if (props.operationType === "add") {
+ staffSalaryMainAdd({ ...payload, status }).then(() => {
+ proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ } else {
+ staffSalaryMainUpdate({ ...payload, status }).then(() => {
+ proxy.$modal.msgSuccess(status === 1 ? "鑽夌淇濆瓨鎴愬姛" : "鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ }
+};
+
+const closeDia = () => {
+ dialogVisible.value = false;
+ emit("close");
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped>
+.form-dia-body {
+ padding: 0;
+}
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+.form-card {
+ margin-bottom: 16px;
+}
+.form-card :deep(.el-card__header) {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+}
+.card-title {
+ font-weight: 500;
+}
+.card-collapse {
+ color: #999;
+ cursor: pointer;
+}
+.toolbar {
+ margin-bottom: 16px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+.employee-table-wrap {
+ position: relative;
+ min-height: 120px;
+}
+.table-empty {
+ text-align: center;
+ padding: 24px;
+ color: #999;
+ font-size: 14px;
+}
+.add-person-tree {
+ max-height: 360px;
+ overflow-y: auto;
+ padding: 8px 0;
+}
+.tax-desc {
+ margin-bottom: 12px;
+ font-size: 14px;
+ color: #606266;
+}
+.dialog-footer {
+ text-align: right;
+}
+.salary-total {
+ margin-top: 16px;
+ padding: 12px 16px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ text-align: right;
+ font-size: 16px;
+}
+.salary-total .total-label {
+ color: #606266;
+ margin-right: 8px;
+}
+.salary-total .total-value {
+ color: #f56c6c;
+ font-weight: bold;
+ font-size: 18px;
+}
+</style>
diff --git a/src/views/personnelManagement/monthlyStatistics/index.vue b/src/views/personnelManagement/monthlyStatistics/index.vue
new file mode 100644
index 0000000..4ac9e97
--- /dev/null
+++ b/src/views/personnelManagement/monthlyStatistics/index.vue
@@ -0,0 +1,407 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">涓婚锛�</span>
+ <el-input
+ v-model="searchForm.salaryTitle"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヤ富棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ <span class="search_title ml10">鐘舵�侊細</span>
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" clearable style="width: 180px">
+ <el-option label="鑽夌" :value="1" />
+ <el-option label="瀹℃牳鏈�氳繃" :value="2" />
+ <el-option label="寰呭鏍�" :value="3" />
+ <el-option label="寰呭彂鏀�" :value="4" />
+ <el-option label="宸插彂鏀�" :value="5" />
+ </el-select>
+ <span class="search_title ml10">宸ヨ祫鏈堜唤锛�</span>
+ <el-date-picker
+ v-model="searchForm.salaryMonth"
+ type="month"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨宸ヨ祫鏈堜唤"
+ style="width: 180px"
+ clearable
+ @change="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-button type="primary" @click="openForm('add')">鏂板缓宸ヨ祫琛�</el-button>
+ <el-button @click="handleDelete">鍒犻櫎</el-button>
+ <el-button @click="openBankSetting">璁剧疆閾惰</el-button>
+ <el-button @click="handleExport">瀵煎嚭</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ :tableLoading="tableLoading"
+ @selection-change="handleSelectionChange"
+ @pagination="pagination"
+ :total="page.total"
+ />
+ </div>
+ <form-dia
+ v-model="dialogVisible"
+ :operation-type="operationType"
+ :row="currentRow"
+ ref="formDiaRef"
+ @close="handleQuery"
+ />
+ <bank-setting-dia
+ v-model="bankDialogVisible"
+ ref="bankDiaRef"
+ @saved="handleBankSaved"
+ />
+ <el-dialog v-model="issueDialogVisible" title="宸ヨ祫鍙戞斁" width="720px">
+ <el-form label-position="top">
+ <el-form-item label="鍙戞斁閾惰" required>
+ <el-select
+ v-model="issueForm.bank"
+ placeholder="璇烽�夋嫨鍙戞斁閾惰"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option v-for="b in issueBankOptions" :key="b" :label="b" :value="b" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary" :loading="issueLoading" @click="confirmIssue">
+ 纭畾
+ </el-button>
+ <el-button @click="issueDialogVisible = false">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ <audit-dia
+ v-model="auditDialogVisible"
+ :row="auditRow"
+ @close="auditDialogVisible = false"
+ @success="handleAuditSuccess"
+ />
+ </div>
+</template>
+
+<script setup>
+import {
+ onMounted,
+ computed,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+} from "vue";
+import { ElMessageBox } from "element-plus";
+import Cookies from "js-cookie";
+import FormDia from "./components/formDia.vue";
+import BankSettingDia from "./components/bankSettingDia.vue";
+import AuditDia from "./components/auditDia.vue";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import { bankList } from "@/api/personnelManagement/bank.js";
+import {
+ staffSalaryMainListPage,
+ staffSalaryMainDelete,
+ staffSalaryMainUpdate,
+} from "@/api/personnelManagement/staffSalaryMain.js";
+
+const data = reactive({
+ searchForm: {
+ salaryTitle: "",
+ status: "",
+ salaryMonth: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const tableColumn = ref([
+ { label: "宸ヨ祫涓婚", prop: "salaryTitle", minWidth: 140 },
+ { label: "宸ヨ祫鏈堜唤", prop: "salaryMonth", width: 120 },
+ {
+ label: "鐘舵��",
+ prop: "statusName",
+ width: 110,
+ dataType: "tag",
+ formatType: (status) => {
+ const statusMap = {
+ "鑽夌": "info",
+ "瀹℃牳鏈�氳繃": "danger",
+ "寰呭鏍�": "warning",
+ "寰呭彂鏀�": "primary",
+ "宸插彂鏀�": "success"
+ };
+ return statusMap[status] || "info";
+ }
+ },
+ { label: "宸ヨ祫鎬婚", prop: "totalSalary", width: 120 },
+ { label: "鏀粯閾惰", prop: "payBank", width: 120 },
+ { label: "瀹℃牳浜�", prop: "auditUserName", width: 110 },
+ { label: "澶囨敞", prop: "remark", minWidth: 120 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 180,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: (row) => Number(row?.status) !== 1 && Number(row?.status) !== 2,
+ clickFun: (row) => openForm("edit", row),
+ },
+ {
+ name: "瀹℃牳",
+ type: "text",
+ disabled: (row) => Number(row?.status) !== 3,
+ clickFun: (row) => openAudit(row),
+ },
+ {
+ name: "鍙戞斁",
+ type: "text",
+ disabled: (row) => Number(row?.status) !== 4,
+ clickFun: (row) => openIssue(row),
+ },
+ ],
+ },
+]);
+
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+const formDiaRef = ref(null);
+const dialogVisible = ref(false);
+const operationType = ref("add");
+const currentRow = ref({});
+const { proxy } = getCurrentInstance();
+const bankSetting = ref({});
+const bankDialogVisible = ref(false);
+const bankDiaRef = ref(null);
+const issueDialogVisible = ref(false);
+const issueLoading = ref(false);
+const issueRow = ref(null);
+const issueForm = reactive({ bank: "" });
+const auditDialogVisible = ref(false);
+const auditRow = ref(null);
+const auditDiaRef = ref(null);
+
+const issueBankOptions = computed(() => {
+ const options = Array.isArray(bankSetting.value?.options) ? bankSetting.value.options : [];
+ return options
+ .map((v) => (v == null ? "" : String(v).trim()))
+ .filter((v) => v !== "");
+});
+
+const statusName = (s) => {
+ const n = Number(s);
+ return (
+ {
+ 1: "鑽夌",
+ 2: "瀹℃牳鏈�氳繃",
+ 3: "寰呭鏍�",
+ 4: "寰呭彂鏀�",
+ 5: "宸插彂鏀�",
+ }[n] || "-"
+ );
+};
+
+const loadBankSetting = () => {
+ return bankList().then((res) => {
+ const list = Array.isArray(res?.data) ? res.data : [];
+ const options = list
+ .map((b) => (b?.bankName == null ? "" : String(b.bankName).trim()))
+ .filter((v) => v !== "");
+ bankSetting.value = { options, defaultBank: "" };
+ });
+};
+
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+const handleReset = () => {
+ searchForm.value.salaryTitle = "";
+ searchForm.value.status = "";
+ searchForm.value.salaryMonth = "";
+ page.current = 1;
+ getList();
+};
+
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ staffSalaryMainListPage({
+ ...searchForm.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then((res) => {
+ tableLoading.value = false;
+ const records = res.data?.records ?? res.data?.list ?? [];
+ console.log('鍒楄〃鎺ュ彛杩斿洖鏁版嵁:', records);
+ // 鍏煎鍚庣瀛楁锛氳嫢鎺ュ彛浠嶈繑鍥炲彴璐︾粨鏋勶紝鍙湪姝ゅ仛鏄犲皠
+ tableData.value = records.map((item) => ({
+ ...item,
+ salaryTitle: item.salaryTitle ?? "-",
+ salaryMonth: item.salaryMonth ?? "-",
+ statusName: statusName(item.status),
+ totalSalary: item.totalSalary ?? "-",
+ payBank: (item.payBank == null ? "" : String(item.payBank).trim()) || "-",
+ auditUserName: item.auditUserName ?? "-",
+ }));
+ page.total = res.data?.total ?? res.data?.count ?? 0;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+const openForm = (type, row) => {
+ operationType.value = type;
+ currentRow.value = row || {};
+ dialogVisible.value = true;
+ nextTick(() => {
+ formDiaRef.value?.openDialog(type, row);
+ });
+};
+
+const openBankSetting = () => {
+ bankDialogVisible.value = true;
+};
+
+const openAudit = (row) => {
+ console.log('鎵撳紑瀹℃牳锛屼紶鍏ョ殑鏁版嵁:', row);
+ auditRow.value = row || null;
+ auditDialogVisible.value = true;
+ nextTick(() => {
+ auditDiaRef.value?.openDialog(row);
+ });
+};
+
+const handleAuditSuccess = () => {
+ getList();
+};
+
+const openIssue = (row) => {
+ if (!issueBankOptions.value?.length) {
+ proxy?.$modal?.msgWarning?.("璇峰厛鍦ㄢ�滆缃摱琛屸�濅腑缁存姢鍙戞斁閾惰閫夐」");
+ return;
+ }
+ issueRow.value = row || null;
+ const current = row?.payBank && row.payBank !== "-" ? String(row.payBank).trim() : "";
+ issueForm.bank = current;
+ issueDialogVisible.value = true;
+};
+
+
+
+const confirmIssue = () => {
+ const bank = issueForm.bank ? String(issueForm.bank).trim() : "";
+ if (!bank) {
+ proxy?.$modal?.msgWarning?.("璇烽�夋嫨鍙戞斁閾惰");
+ return;
+ }
+ const row = issueRow.value;
+ if (!row?.id) {
+ issueDialogVisible.value = false;
+ return;
+ }
+ issueLoading.value = true;
+ staffSalaryMainUpdate({
+ id: row.id,
+ payBank: bank,
+ status: 5,
+ })
+ .then(() => {
+ proxy?.$modal?.msgSuccess?.("鍙戞斁鎴愬姛");
+ issueDialogVisible.value = false;
+ getList();
+ })
+ .finally(() => {
+ issueLoading.value = false;
+ });
+};
+
+const handleBankSaved = () => {
+ loadBankSetting();
+ getList();
+};
+
+const handleDelete = () => {
+ if (!selectedRows.value?.length) {
+ proxy.$modal.msgWarning("璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map((item) => item.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ staffSalaryMainDelete(ids).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {});
+};
+
+const handleExport = () => {
+ proxy.download(
+ "/compensationPerformance/export",
+ { ...searchForm.value, current: page.current, size: page.size },
+ "宸ヨ祫琛�.xlsx"
+ );
+};
+
+onMounted(() => {
+ loadBankSetting();
+ getList();
+});
+</script>
+
+<style scoped>
+.search_form {
+ margin-bottom: 20px;
+}
+.search_title {
+ font-weight: 500;
+ margin-right: 5px;
+}
+.ml10 {
+ margin-left: 10px;
+}
+.table_list {
+ margin-top: 20px;
+}
+</style>
diff --git a/src/views/personnelManagement/payrollManagement/components/formDia.vue b/src/views/personnelManagement/payrollManagement/components/formDia.vue
new file mode 100644
index 0000000..cf93559
--- /dev/null
+++ b/src/views/personnelManagement/payrollManagement/components/formDia.vue
@@ -0,0 +1,319 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板钖祫' : '缂栬緫钖祫'"
+ width="50%"
+ @close="closeDia"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鏈堜唤锛�" prop="payDate">
+ <el-date-picker
+ v-model="form.payDate"
+ type="month"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨鏈堜唤"
+ clearable
+ :disabled="operationType === 'edit'"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="濮撳悕锛�" prop="staffId">
+ <el-select v-model="form.staffId" placeholder="璇烽�夋嫨浜哄憳" style="width: 100%" @change="handleSelect" :disabled="operationType === 'edit'">
+ <el-option
+ v-for="item in personList"
+ :key="item.id"
+ :label="item.staffName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="搴斿嚭鍕ゅぉ鏁帮細" prop="shouldAttendedNum">
+ <el-input v-model="form.shouldAttendedNum" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹為檯鍑哄嫟澶╂暟锛�" prop="actualAttendedNum">
+ <el-input v-model="form.actualAttendedNum" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍩烘湰宸ヨ祫锛�" prop="basicSalary">
+ <el-input v-model="form.basicSalary" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="宀椾綅宸ヨ祫锛�" prop="postSalary">
+ <el-input v-model="form.postSalary" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍏ョ鑱岀己鍕ゆ墸娆撅細" prop="deductionAbsenteeism">
+ <el-input v-model="form.deductionAbsenteeism" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐥呭亣鎵f锛�" prop="sickLeaveDeductions">
+ <el-input v-model="form.sickLeaveDeductions" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="浜嬪亣鎵f锛�" prop="deductionPersonalLeave">
+ <el-input v-model="form.deductionPersonalLeave" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="蹇樿鎵撳崱鎵f锛�" prop="forgetClockDeduct">
+ <el-input v-model="form.forgetClockDeduct" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="缁╂晥寰楀垎锛�" prop="performanceScore">
+ <el-input v-model="form.performanceScore" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁╂晥宸ヨ祫锛�" prop="performancePay">
+ <el-input v-model="form.performancePay" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="搴斿彂鍚堣锛�" prop="payableWages">
+ <el-input v-model="form.payableWages" placeholder="璇疯緭鍏�" clearable type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绀句繚涓汉锛�" prop="socialSecurityIndividuals">
+ <el-input v-model="form.socialSecurityIndividuals" :precision="0" :step="1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绀句繚鍏徃锛�" prop="socialSecurityCompanies">
+ <el-input v-model="form.socialSecurityCompanies" :precision="0" :step="1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绀句繚鍚堣锛�" prop="socialSecurityTotal">
+ <el-input v-model="form.socialSecurityTotal" :precision="0" :step="1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍏Н閲戜釜浜猴細" prop="providentFundIndividuals">
+ <el-input v-model="form.providentFundIndividuals" :precision="0" :step="1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏Н閲戝叕鍙革細" prop="providentFundCompany">
+ <el-input v-model="form.providentFundCompany" :precision="0" :step="1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍏Н閲戝悎璁★細" prop="providentFundTotal">
+ <el-input v-model="form.providentFundTotal" :precision="0" :step="1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="搴旂◣宸ヨ祫锛�" prop="taxableWaget">
+ <el-input v-model="form.taxableWaget" :precision="0" :step="1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="涓汉鎵�寰楃◣锛�" prop="personalIncomeTax">
+ <el-input v-model="form.personalIncomeTax" :step="0.1" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹炲彂宸ヨ祫锛�" prop="actualWages">
+ <el-input v-model="form.actualWages" style="width: 100%" type="number"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {compensationAdd, compensationUpdate} from "@/api/personnelManagement/payrollManagement.js";
+import {staffOnJobInfo, staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const data = reactive({
+ form: {
+ payDate: "",
+ staffId: "",
+ name: "",
+ shouldAttendedNum: "",
+ actualAttendedNum: "",
+ basicSalary: "",
+ postSalary: "",
+ deductionAbsenteeism: "",
+ sickLeaveDeductions: "",
+ deductionPersonalLeave: "",
+ forgetClockDeduct: "",
+ performanceScore: "",
+ performancePay: "",
+ payableWages: "",
+ socialSecurityIndividuals: "",
+ socialSecurityCompanies: "",
+ socialSecurityTotal: "",
+ providentFundIndividuals: "",
+ providentFundCompany: "",
+ providentFundTotal: "",
+ taxableWaget: "",
+ personalIncomeTax: "",
+ actualWages: "",
+ },
+ rules: {
+ payDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" },],
+ staffId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" },],
+ staffName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ shouldAttendedNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ actualAttendedNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ basicSalary: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ postSalary: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ deductionAbsenteeism: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ sickLeaveDeductions: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ deductionPersonalLeave: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ forgetClockDeduct: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ performanceScore: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ performancePay: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ payableWages: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ socialSecurityIndividuals: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ socialSecurityCompanies: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ socialSecurityTotal: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ providentFundIndividuals: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ providentFundCompany: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ providentFundTotal: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ taxableWaget: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ personalIncomeTax: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ actualWages: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ },
+});
+const { form, rules } = toRefs(data);
+const personList = ref([]);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1
+ }).then(res => {
+ personList.value = res.data.records || []
+ })
+ form.value = {}
+ if (operationType.value === 'edit') {
+ staffOnJobInfo(row.staffId).then(res => {
+ form.value = {...row}
+ form.value.payDate = form.value.payDate + '-01'
+ })
+ }
+}
+const handleSelect = (value) => {
+ console.log('value', value)
+ const index = personList.value.findIndex(row => row.id === value)
+ if (index > -1) {
+ form.value.name = personList.value[index].staffName
+ }
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ form.value.staffState = 1
+ if (operationType.value === "add") {
+ compensationAdd(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ } else {
+ compensationUpdate(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ }
+ })
+}
+// 璁$畻鍚堝悓骞撮檺
+const calculateContractTerm = () => {
+ if (form.value.contractStartTime && form.value.contractEndTime) {
+ const startDate = new Date(form.value.contractStartTime);
+ const endDate = new Date(form.value.contractEndTime);
+
+ if (endDate > startDate) {
+ // 璁$畻骞翠唤宸�
+ const yearDiff = endDate.getFullYear() - startDate.getFullYear();
+ const monthDiff = endDate.getMonth() - startDate.getMonth();
+ const dayDiff = endDate.getDate() - startDate.getDate();
+
+ let years = yearDiff;
+
+ // 濡傛灉缁撴潫鏃ユ湡鐨勬湀鏃ュ皬浜庡紑濮嬫棩鏈熺殑鏈堟棩锛屽垯鍑忓幓1骞�
+ if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
+ years = yearDiff - 1;
+ }
+
+ form.value.contractTerm = Math.max(0, years);
+ } else {
+ form.value.contractTerm = 0;
+ }
+ } else {
+ form.value.contractTerm = 0;
+ }
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/personnelManagement/payrollManagement/index.vue b/src/views/personnelManagement/payrollManagement/index.vue
new file mode 100644
index 0000000..f17f42e
--- /dev/null
+++ b/src/views/personnelManagement/payrollManagement/index.vue
@@ -0,0 +1,306 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">濮撳悕锛�</span>
+ <el-input
+ v-model="searchForm.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ鍚嶆悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <span class="search_title ml10">鏈堜唤锛�</span>
+ <el-date-picker
+ v-model="searchForm.payDateStr"
+ type="month"
+ @change="handleQuery"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ placeholder="璇烽�夋嫨鏈堜唤"
+ style="width: 240px"
+ clearable
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ <div>
+ <el-button @click="handleExport" style="margin-right: 10px">瀵煎嚭</el-button>
+ <el-button type="primary" @click="openForm('add')">鏂板钖祫</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
+import FormDia from "@/views/personnelManagement/payrollManagement/components/formDia.vue";
+import {ElMessageBox} from "element-plus";
+import dayjs from "dayjs";
+import {compensationDelete, compensationListPage} from "@/api/personnelManagement/payrollManagement.js";
+
+const data = reactive({
+ searchForm: {
+ name: "",
+ payDateStr: "",
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "钖祫鏈堜唤",
+ prop: "payDate",
+ },
+ {
+ label: "濮撳悕",
+ prop: "name",
+ },
+ {
+ label: "搴斿嚭鍕ゅぉ鏁�",
+ prop: "shouldAttendedNum",
+ width:100
+ },
+ {
+ label: "瀹為檯鍑哄嫟澶╂暟",
+ prop: "actualAttendedNum",
+ width:110
+ },
+ {
+ label: "鍩烘湰宸ヨ祫",
+ prop: "basicSalary",
+ },
+ {
+ label: "宀椾綅宸ヨ祫",
+ prop: "postSalary",
+ width:100
+ },
+ {
+ label: "鍏ョ鑱岀己鍕ゆ墸娆�",
+ prop: "deductionAbsenteeism",
+ width:130
+ },
+ {
+ label: "鐥呭亣鎵f",
+ prop: "sickLeaveDeductions",
+ width:100
+ },
+ {
+ label: "浜嬪亣鎵f",
+ prop: "deductionPersonalLeave",
+ width:100
+ },
+ {
+ label: "蹇樿鎵撳崱鎵f",
+ prop: "forgetClockDeduct",
+ width:110
+ },
+ {
+ label: "缁╂晥寰楀垎",
+ prop: "performanceScore",
+ width:150
+ },
+ {
+ label: "缁╂晥宸ヨ祫",
+ prop: "performancePay",
+ width: 120
+ },
+ {
+ label: "搴斿彂鍚堣",
+ prop: "payableWages",
+ width:150
+ },
+ {
+ label: "绀句繚涓汉",
+ prop: "socialSecurityIndividuals",
+ },
+ {
+ label: "绀句繚鍏徃",
+ prop: "socialSecurityCompanies",
+ width: 120
+ },
+ {
+ label: "绀句繚鍚堣",
+ prop: "socialSecurityTotal",
+ width: 120
+ },
+ {
+ label: "鍏Н閲戜釜浜�",
+ prop: "providentFundIndividuals",
+ width: 120
+ },
+ {
+ label: "鍏Н閲戝叕鍙�",
+ prop: "providentFundCompany",
+ width: 120
+ },
+ {
+ label: "鍏Н閲戝悎璁�",
+ prop: "providentFundTotal",
+ width: 120
+ },
+ {
+ label: "搴旂◣宸ヨ祫",
+ prop: "taxableWaget",
+ },
+ {
+ label: "涓汉鎵�寰楃◣",
+ prop: "personalIncomeTax",
+ width: 120
+ },
+ {
+ label: "瀹炲彂宸ヨ祫",
+ prop: "actualWages",
+ width: 120
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: 'right',
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openForm("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const formDia = ref()
+const { proxy } = getCurrentInstance()
+
+const handleDateChange = (value,type) => {
+ searchForm.value.entryDateEnd = null
+ searchForm.value.entryDateStart = null
+ if(type === 1){
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value).format("YYYY-MM-DD");
+ }
+ }else{
+ if (value) {
+ searchForm.value.entryDateEnd = dayjs(value).format("YYYY-MM-DD");
+ }
+ }
+ getList();
+};
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ compensationListPage({...page, ...searchForm.value, staffState: 1}).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ compensationDelete(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/staff/staffJoinLeaveRecord/export", {staffState: 1}, "浜哄憳鍏ヨ亴.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎嚭钖祫绠$悊
+const handleExport = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/compensationPerformance/export", { ...searchForm.value, ...page }, "钖祫绠$悊.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/personnelManagement/scheduling/index.vue b/src/views/personnelManagement/scheduling/index.vue
new file mode 100644
index 0000000..19d2062
--- /dev/null
+++ b/src/views/personnelManagement/scheduling/index.vue
@@ -0,0 +1,622 @@
+<template>
+ <div class="app-container scheduling-container">
+ <!-- 绛涢�夊尯鍩� -->
+ <div class="filter-section">
+ <el-form :inline="true" :model="filterForm" class="filter-form">
+ <el-form-item label="鍛樺伐濮撳悕锛�">
+ <el-input
+ v-model="filterForm.staffName"
+ placeholder="璇疯緭鍏ュ憳宸ュ鍚�"
+ clearable
+ style="width: 150px"
+ />
+ </el-form-item>
+ <el-form-item label="鐝绫诲瀷锛�">
+ <el-select v-model="filterForm.shiftType" placeholder="璇烽�夋嫨鐝" clearable style="width: 120px">
+ <el-option v-for="item in shift_type" :label="item.label" :value="item.value" :key="item.value"/>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏃ユ湡鑼冨洿锛�">
+ <el-date-picker
+ v-model="filterForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 250px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleFilter">
+ <el-icon><Search/></el-icon>
+ 绛涢��
+ </el-button>
+ <el-button @click="resetFilter">
+ <el-icon><Refresh/></el-icon>
+ 閲嶇疆
+ </el-button>
+ <el-button @click="handleExport">
+ <el-icon><Download/></el-icon>
+ 瀵煎嚭
+ </el-button>
+ <el-button type="primary" @click="openScheduleDialog('add')">
+ <el-icon><Plus/></el-icon>
+ 鏂板鎺掔彮
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+
+ <!-- 鎺掔彮琛ㄦ牸 -->
+ <div class="table-section">
+ <el-table
+ :data="scheduleList"
+ border
+ :loading="tableLoading"
+ stripe
+ style="width: 100%"
+ height="calc(100vh - 18.5em)"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55"/>
+ <el-table-column prop="staffName" label="鍛樺伐濮撳悕" width="120"/>
+ <el-table-column prop="staffNo" label="鍛樺伐宸ュ彿" width="100"/>
+ <el-table-column prop="department" label="閮ㄩ棬" width="120">
+ <template #default="scope">
+ {{ (department_type.find(i => i.value === String(scope.row.department)) || {}).label }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="shiftType" label="鐝绫诲瀷" width="100">
+ <template #default="scope">
+ <el-tag :type="getShiftTagType(scope.row.shiftType)">
+ {{ (shift_type.find(i => i.value === String(scope.row.shiftType)) || {}).label }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="workDate" label="宸ヤ綔鏃ユ湡" width="120"/>
+ <el-table-column prop="startTime" label="寮�濮嬫椂闂�" width="100"/>
+ <el-table-column prop="endTime" label="缁撴潫鏃堕棿" width="100"/>
+ <el-table-column prop="workHours" label="宸ヤ綔鏃堕暱" width="100">
+ <template #default="scope">
+ {{ scope.row.workHours }}灏忔椂
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getStatusTagType(scope.row.status)">
+ {{ (schedule_status.find(i => i.value === String(scope.row.status)) || {}).label }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" min-width="150"/>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ size="small"
+ @click="openScheduleDialog('edit', scope.row)"
+ >
+ 缂栬緫
+ </el-button>
+ <el-button
+ type="danger"
+ size="small"
+ @click="handleDelete(scope.row)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-if="tableCount > 0"
+ :total="tableCount"
+ :page="filterForm.current"
+ :limit="filterForm.size"
+ @pagination="paginationChange"
+ />
+ </div>
+
+ <!-- 鎵归噺鎿嶄綔 -->
+ <div class="batch-actions" v-if="selectedRows.length > 0">
+ <el-button
+ type="danger"
+ @click="handleBatchDelete"
+ :disabled="selectedRows.length === 0"
+ >
+ 鎵归噺鍒犻櫎 ({{ selectedRows.length }})
+ </el-button>
+ </div>
+
+ <!-- 鎺掔彮鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog
+ v-model="scheduleDialog"
+ :title="dialogType === 'add' ? '鏂板鎺掔彮' : '缂栬緫鎺掔彮'"
+ width="700px"
+ @close="closeScheduleDialog"
+ >
+ <el-form
+ :model="scheduleForm"
+ :rules="scheduleRules"
+ ref="scheduleFormRef"
+ label-width="120px"
+ >
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐濮撳悕锛�" prop="staffId">
+ <el-select v-model="scheduleForm.staffId" placeholder="璇疯緭鍏ュ憳宸ュ鍚�" style="width: 100%"
+ @change="handleSelectStaff">
+ <el-option v-for="item in personList" :label="item.staffName" :value="item.id" :key="item.id"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍛樺伐宸ュ彿锛�" prop="staffNo">
+ <el-input :disabled="true" v-model="scheduleForm.staffNo" placeholder=""/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬锛�" prop="department">
+ <el-select v-model="scheduleForm.department" placeholder="璇烽�夋嫨閮ㄩ棬" style="width: 100%">
+ <el-option v-for="item in department_type" :label="item.label" :value="item.value" :key="item.value"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐝绫诲瀷锛�" prop="shiftType">
+ <el-select v-model="scheduleForm.shiftType" placeholder="璇烽�夋嫨鐝" style="width: 100%">
+ <el-option v-for="item in shift_type" :label="item.label" :value="item.value" :key="item.value"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="宸ヤ綔鏃ユ湡锛�" prop="workDate">
+ <el-date-picker
+ v-model="scheduleForm.workDate"
+ type="date"
+ placeholder="閫夋嫨宸ヤ綔鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵�侊細" prop="status">
+ <el-select v-model="scheduleForm.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%">
+ <el-option v-for="item in schedule_status" :label="item.label" :value="item.value" :key="item.value"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�濮嬫椂闂达細" prop="startTime">
+ <el-time-picker
+ v-model="scheduleForm.startTime"
+ placeholder="閫夋嫨寮�濮嬫椂闂�"
+ style="width: 100%"
+ format="HH:mm"
+ value-format="HH:mm"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁撴潫鏃堕棿锛�" prop="endTime">
+ <el-time-picker
+ v-model="scheduleForm.endTime"
+ placeholder="閫夋嫨缁撴潫鏃堕棿"
+ style="width: 100%"
+ format="HH:mm"
+ value-format="HH:mm"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞锛�" prop="remark">
+ <el-input
+ v-model="scheduleForm.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitScheduleForm">纭</el-button>
+ <el-button @click="closeScheduleDialog">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, computed, onMounted, getCurrentInstance} from 'vue'
+import {ElMessage, ElMessageBox} from 'element-plus'
+import {useDict} from "@/utils/dict.js"
+import {Plus, Download, Search, Refresh} from '@element-plus/icons-vue'
+import {save, del, delByIds, listPage} from "@/api/personnelManagement/scheduling.js"
+import dayjs from "dayjs";
+import pagination from "@/components/PIMTable/Pagination.vue";
+import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js";
+
+const { proxy } = getCurrentInstance();
+
+const tableCount = ref(0)
+// 鍝嶅簲寮忔暟鎹�
+const scheduleDialog = ref(false)
+const dialogType = ref('add')
+const selectedRows = ref([])
+const scheduleFormRef = ref()
+
+// 绛涢�夎〃鍗�
+const filterForm = reactive({
+ staffName: '',
+ shiftType: '',
+ dateRange: [],
+ current:1,
+ size: 10
+})
+
+// 鎺掔彮琛ㄥ崟
+const scheduleForm = reactive({
+ id: '',
+ staffId: '',
+ staffNo: '',
+ department: '',
+ shiftType: '',
+ workDate: '',
+ startTime: '',
+ endTime: '',
+ workStartTime: '',
+ workEndTime: '',
+ workHours: 0,
+ status: '',
+ remark: ''
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const scheduleRules = reactive({
+ staffId: [{required: true, message: '璇烽�夋嫨鍛樺伐', trigger: 'change'}],
+ department: [{required: true, message: '璇烽�夋嫨閮ㄩ棬', trigger: 'change'}],
+ shiftType: [{required: true, message: '璇烽�夋嫨鐝绫诲瀷', trigger: 'change'}],
+ workDate: [{required: true, message: '璇烽�夋嫨宸ヤ綔鏃ユ湡', trigger: 'change'}],
+ startTime: [{required: true, message: '璇烽�夋嫨寮�濮嬫椂闂�', trigger: 'change'}],
+ endTime: [{required: true, message: '璇烽�夋嫨缁撴潫鏃堕棿', trigger: 'change'}],
+ status: [{required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change'}]
+})
+const tableLoading = ref(false)
+
+//瀛楀吀
+const {department_type, schedule_status, shift_type} = useDict("department_type", "schedule_status", "shift_type")
+// 浜哄憳鍒楄〃
+const personList = ref([]);
+
+// 妯℃嫙鎺掔彮鏁版嵁
+const scheduleList = ref([])
+
+
+/**
+ * 鑾峰彇褰撳墠鍦ㄨ亴浜哄憳鍒楄〃
+ */
+const getPersonList = () => {
+ staffOnJobListPage({
+ current: -1,
+ size: -1,
+ staffState: 1
+ }).then(res => {
+ personList.value = res.data.records || []
+ })
+};
+const paginationChange = (obj) => {
+ filterForm.current = obj.page;
+ filterForm.size = obj.limit;
+ handleFilter();
+};
+
+const handleSelectStaff = (val) => {
+ let obj = personList.value.find(item => item.id === val)
+ scheduleForm.staffNo = obj.staffNo
+
+}
+
+// 鑾峰彇鐝鏍囩绫诲瀷
+const getShiftTagType = (shiftType) => {
+ const typeMap = Object.fromEntries(shift_type.value.map(i => [i.value, i.elTagType]))
+ return typeMap[shiftType] || 'info'
+}
+
+// 鑾峰彇鐘舵�佹爣绛剧被鍨�
+const getStatusTagType = (status) => {
+ const typeMap = Object.fromEntries(schedule_status.value.map(i => [i.value, i.elTagType]))
+ return typeMap[status] || 'info'
+}
+
+// 绛涢��
+const handleFilter = async () => {
+ tableLoading.value = true
+ let searchForm = {
+ ...filterForm,
+ ...(filterForm.dateRange.length > 0 && {
+ startDate: filterForm.dateRange[0],
+ endDate: filterForm.dateRange[1],
+ })
+ }
+ let resp = await listPage(searchForm)
+ tableCount.value = resp.data.total
+ scheduleList.value = resp.data.records.map(it => {
+ return {
+ ...it,
+ 'startTime': dayjs(it.workStartTime).format('HH:mm'),
+ 'endTime': dayjs(it.workEndTime).format('HH:mm'),
+ }
+ })
+ tableLoading.value = false
+
+}
+
+// 閲嶇疆绛涢��
+const resetFilter = () => {
+ filterForm.staffName = ''
+ filterForm.shiftType = ''
+ filterForm.dateRange = []
+}
+
+// 鎵撳紑鎺掔彮瀵硅瘽妗�
+const openScheduleDialog = (type, data) => {
+ dialogType.value = type
+ scheduleDialog.value = true
+ getPersonList()
+ if (type === 'edit' && data) {
+ // 缂栬緫妯″紡锛屽鍒舵暟鎹�
+ Object.assign(scheduleForm, {...data})
+ } else {
+ // 鏂板妯″紡锛岄噸缃〃鍗�
+ Object.keys(scheduleForm).forEach(key => {
+ scheduleForm[key] = ''
+ })
+ // scheduleForm.status = '宸插畨鎺�'
+ scheduleForm.workDate = new Date().toISOString().split('T')[0]
+ }
+}
+
+// 鍏抽棴鎺掔彮瀵硅瘽妗�
+const closeScheduleDialog = () => {
+ scheduleFormRef.value?.resetFields()
+ scheduleDialog.value = false
+}
+
+// 璁$畻宸ヤ綔鏃堕暱
+const calculateWorkHours = () => {
+ if (scheduleForm.workDate && scheduleForm.startTime && scheduleForm.endTime) {
+ // 浣跨敤 workDate 涓� startTime 鍜� endTime 缁勫悎
+ const startDateTime = new Date(`${scheduleForm.workDate} ${scheduleForm.startTime}`)
+ const endDateTime = new Date(`${scheduleForm.workDate} ${scheduleForm.endTime}`)
+
+ // 澶勭悊璺ㄥぉ鎯呭喌锛堢粨鏉熸椂闂存棭浜庡紑濮嬫椂闂达級
+ if (endDateTime < startDateTime) {
+ // 璺ㄥぉ锛屽皢缁撴潫鏃ユ湡鍔犱竴澶�
+ endDateTime.setDate(endDateTime.getDate() + 1)
+ }
+ // 璁$畻宸ヤ綔鏃堕暱锛堝皬鏃讹級
+ const diffMs = endDateTime - startDateTime
+ const diffHours = diffMs / (1000 * 60 * 60)
+ scheduleForm.workHours = Math.round(diffHours * 100) / 100
+ scheduleForm.workStartTime = dayjs(startDateTime).format("YYYY-MM-DD HH:mm:ss")
+ scheduleForm.workEndTime = dayjs(endDateTime).format("YYYY-MM-DD HH:mm:ss")
+
+ }
+}
+
+// 鎻愪氦鎺掔彮琛ㄥ崟
+const submitScheduleForm = async () => {
+ const valid = await scheduleFormRef.value.validate()
+ if (!valid) return
+
+ calculateWorkHours()
+ const newSchedule = {...scheduleForm}
+
+ try {
+ await save(newSchedule)
+ ElMessage.success('淇濆瓨鎺掔彮鎴愬姛')
+
+ handleFilter()
+ closeScheduleDialog()
+ } catch (err) {
+ ElMessage.error('淇濆瓨澶辫触')
+ }
+}
+
+// 鍒犻櫎鎺掔彮
+const handleDelete = (row) => {
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄� ${row.staffName} 鐨勬帓鐝褰曞悧锛焋,
+ '鍒犻櫎鎻愮ず',
+ {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ ).then(() => {
+ del(row.id)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ handleFilter()
+ }).catch(() => {
+ ElMessage.info('宸插彇娑堝垹闄�')
+ })
+}
+
+// 鎵归噺鍒犻櫎
+const handleBatchDelete = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸垹闄ょ殑璁板綍')
+ return
+ }
+
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉℃帓鐝褰曞悧锛焋,
+ '鎵归噺鍒犻櫎鎻愮ず',
+ {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }
+ ).then(() => {
+ delByIds(selectedRows.value.map(item => item.id))
+ handleFilter()
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+ }).catch(() => {
+ ElMessage.info('宸插彇娑堝垹闄�')
+ })
+}
+
+// 閫夋嫨鍙樺寲浜嬩欢
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection
+}
+
+// 瀵煎嚭
+const handleExport = () => {
+ let searchForm = {
+ ...filterForm,
+ ...(filterForm.dateRange.length > 0 && {
+ startDate: filterForm.dateRange[0],
+ endDate: filterForm.dateRange[1],
+ })
+ }
+ proxy.download('/staff/staffScheduling/export', {}, '浜哄憳鎺掔彮.xlsx')
+}
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ // 椤甸潰鍒濆鍖�
+ handleFilter()
+})
+</script>
+
+<style scoped>
+.scheduling-container {
+ padding: 20px;
+ background-color: #f5f7fa;
+ min-height: 100vh;
+}
+
+.page-header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 12px;
+ color: white;
+}
+
+.page-header h2 {
+ color: white;
+ margin-bottom: 10px;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.page-header p {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 14px;
+ margin: 0 0 15px 0;
+}
+
+.header-controls {
+ display: flex;
+ justify-content: center;
+ gap: 15px;
+}
+
+.filter-section {
+ background: white;
+ padding: 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.filter-form {
+ margin: 0;
+}
+
+.table-section {
+ background: white;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ margin-bottom: 20px;
+}
+
+.batch-actions {
+ background: white;
+ padding: 15px 20px;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.dialog-footer {
+ text-align: right;
+}
+
+:deep(.el-form-item__label) {
+ font-weight: 500;
+ color: #303133;
+}
+
+:deep(.el-input__wrapper) {
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
+}
+
+:deep(.el-input__wrapper:hover) {
+ box-shadow: 0 0 0 1px #c0c4cc inset;
+}
+
+:deep(.el-input__wrapper.is-focus) {
+ box-shadow: 0 0 0 1px #409eff inset;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .scheduling-container {
+ padding: 10px;
+ }
+
+ .page-header {
+ padding: 15px;
+ }
+
+ .page-header h2 {
+ font-size: 24px;
+ }
+
+ .header-controls {
+ flex-direction: column;
+ gap: 10px;
+ }
+}
+
+@media (max-width: 768px) {
+ .filter-form .el-form-item {
+ margin-bottom: 10px;
+ }
+}
+</style>
diff --git a/src/views/personnelManagement/selfService/index.vue b/src/views/personnelManagement/selfService/index.vue
new file mode 100644
index 0000000..647f149
--- /dev/null
+++ b/src/views/personnelManagement/selfService/index.vue
@@ -0,0 +1,800 @@
+<template>
+ <div class="app-container self-service-container">
+
+ <!-- 鍔熻兘瀵艰埅鍗$墖 -->
+ <el-row :gutter="20" class="nav-cards">
+ <el-col :span="6" v-for="(item, index) in navItems" :key="index">
+ <el-card class="nav-card" @click="handleNavClick(item.type)">
+ <div class="nav-content">
+ <el-icon :size="40" class="nav-icon">
+ <component :is="item.icon" />
+ </el-icon>
+ <h3>{{ item.title }}</h3>
+ <p>{{ item.desc }}</p>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 涓昏鍐呭鍖哄煙 -->
+ <div class="main-content">
+ <!-- 鑰冨嫟璁板綍 -->
+ <el-card v-if="currentView === 'attendance'" class="content-card">
+ <template #header>
+ <div class="card-header">
+ <span>涓汉鑰冨嫟璁板綍</span>
+ <el-button type="primary" @click="addAttendanceRecord">鏂板璁板綍</el-button>
+ </div>
+ </template>
+ <el-table :data="attendanceData" style="width: 100%" :loading="tableLoading">
+ <el-table-column prop="date" label="鏃ユ湡" />
+ <el-table-column prop="checkIn" label="绛惧埌鏃堕棿" />
+ <el-table-column prop="checkOut" label="绛鹃��鏃堕棿" />
+ <el-table-column prop="workHours" label="宸ヤ綔鏃堕暱" width="100" />
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === '姝e父' ? 'success' : 'danger'">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="150">
+ <template #default="scope">
+ <el-button size="small" @click="editAttendanceRecord(scope.row)">缂栬緫</el-button>
+ <el-button size="small" type="danger" @click="deleteAttendanceRecord(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 钖祫鍗� -->
+ <el-card v-if="currentView === 'salary'" class="content-card">
+ <template #header>
+ <div class="card-header">
+ <span>钖祫鍗曟煡璇�</span>
+ <el-date-picker v-model="payDateStr" type="month" placeholder="閫夋嫨鏈堜唤" value-format="YYYY-MM" format="YYYY-MM" @change="changMonth"/>
+ </div>
+ </template>
+ <el-table :data="salaryData" style="width: 100%">
+ <el-table-column prop="payDate" label="鏈堜唤" />
+ <el-table-column prop="basicSalary" label="鍩烘湰宸ヨ祫" />
+ <el-table-column prop="bonus" label="濂栭噾" />
+ <el-table-column prop="deduction" label="鎵f" />
+ <el-table-column prop="actualWages" label="瀹炲彂宸ヨ祫" />
+ <el-table-column prop="status" label="鐘舵��" >
+ <template #default="scope">
+ <el-tag :type="scope.row.status === '宸插彂鏀�' ? 'success' : 'warning'">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 鍋囨湡鐢宠 -->
+ <el-card v-if="currentView === 'leave'" class="content-card">
+ <template #header>
+ <div class="card-header">
+ <span>鍋囨湡鐢宠绠$悊</span>
+ <el-button type="primary" @click="openLeaveForm">鐢宠鍋囨湡</el-button>
+ </div>
+ </template>
+ <el-table :data="leaveData" style="width: 100%">
+ <el-table-column prop="type" label="鍋囨湡绫诲瀷" />
+ <el-table-column prop="startDate" label="寮�濮嬫棩鏈�" />
+ <el-table-column prop="endDate" label="缁撴潫鏃ユ湡" />
+ <el-table-column prop="days" label="澶╂暟" width="80" />
+ <el-table-column prop="reason" label="鐢宠鍘熷洜" />
+ <el-table-column prop="status" label="瀹℃壒鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="150">
+ <template #default="scope">
+ <el-button size="small" @click="editLeaveRecord(scope.row)">缂栬緫</el-button>
+ <el-button size="small" type="danger" @click="deleteLeaveRecord(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <!-- 涓汉淇℃伅 -->
+ <el-card v-if="currentView === 'profile'" class="content-card">
+ <template #header>
+ <div class="card-header">
+ <span>涓汉淇℃伅缁存姢</span>
+ <el-button type="primary" @click="editProfileForm">缂栬緫淇℃伅</el-button>
+ </div>
+ </template>
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="濮撳悕">{{ profile.name }}</el-descriptions-item>
+ <el-descriptions-item label="宸ュ彿">{{ profile.employeeId }}</el-descriptions-item>
+ <el-descriptions-item label="閮ㄩ棬">{{ profile.department }}</el-descriptions-item>
+ <el-descriptions-item label="鑱屼綅">{{ profile.position }}</el-descriptions-item>
+ <el-descriptions-item label="鍏ヨ亴鏃ユ湡">{{ profile.hireDate }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鐢佃瘽">{{ profile.phone }}</el-descriptions-item>
+ <el-descriptions-item label="閭">{{ profile.email }}</el-descriptions-item>
+ <el-descriptions-item label="鍦板潃">{{ profile.adress }}</el-descriptions-item>
+ </el-descriptions>
+ </el-card>
+ </div>
+
+ <!-- 鍋囨湡鐢宠寮圭獥 -->
+ <el-dialog v-model="showLeaveDialog" :title="leaveOperationType === 'add' ? '鐢宠鍋囨湡' : '缂栬緫鍋囨湡'" width="500px">
+ <el-form :model="leaveForm" label-width="100px">
+ <el-form-item label="鍋囨湡绫诲瀷">
+ <el-select v-model="leaveForm.type" placeholder="璇烽�夋嫨鍋囨湡绫诲瀷">
+ <el-option label="骞村亣" value="骞村亣" />
+ <el-option label="鐥呭亣" value="鐥呭亣" />
+ <el-option label="璋冧紤" value="璋冧紤" />
+ <el-option label="浜嬪亣" value="浜嬪亣" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="寮�濮嬫棩鏈�">
+ <el-date-picker v-model="leaveForm.startDate" type="date" placeholder="閫夋嫨寮�濮嬫棩鏈�" />
+ </el-form-item>
+ <el-form-item label="缁撴潫鏃ユ湡">
+ <el-date-picker v-model="leaveForm.endDate" type="date" placeholder="閫夋嫨缁撴潫鏃ユ湡" />
+ </el-form-item>
+ <el-form-item label="鐢宠鍘熷洜">
+ <el-input v-model="leaveForm.reason" type="textarea" rows="3" />
+ </el-form-item>
+ <!-- <el-form-item label="瀹℃壒鐘舵��">
+ <el-select v-model="leaveForm.status" placeholder="璇烽�夋嫨瀹℃壒鐘舵��">
+ <el-option label="瀹℃壒涓�" value="瀹℃壒涓�" />
+ <el-option label="宸查�氳繃" value="宸查�氳繃" />
+ <el-option label="宸叉嫆缁�" value="宸叉嫆缁�" />
+ </el-select>
+ </el-form-item> -->
+ </el-form>
+ <template #footer>
+ <el-button @click="showLeaveDialog = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="submitLeaveApplication">鎻愪氦鐢宠</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 鏂板-缂栬緫鑰冨嫟璁板綍寮圭獥 -->
+ <el-dialog v-model="showAttendanceDialog" :title="operationType === 'add' ? '鏂板鑰冨嫟璁板綍' : '缂栬緫鑰冨嫟璁板綍'" width="500px">
+ <el-form :model="attendanceForm" :rules="attendanceRules" ref="attendanceFormRef" label-width="100px">
+ <el-form-item label="鏃ユ湡" prop="date">
+ <el-date-picker v-model="attendanceForm.date" type="date" value-format="YYYY-MM-DD" format="YYYY-MM-DD" placeholder="閫夋嫨鏃ユ湡" />
+ </el-form-item>
+ <el-form-item label="绛惧埌鏃堕棿" prop="checkIn">
+ <el-time-picker v-model="attendanceForm.checkIn" placeholder="閫夋嫨绛惧埌鏃堕棿" format="HH:mm" value-format="HH:mm" />
+ </el-form-item>
+ <el-form-item label="绛鹃��鏃堕棿" prop="checkOut">
+ <el-time-picker v-model="attendanceForm.checkOut" placeholder="閫夋嫨绛鹃��鏃堕棿" format="HH:mm" value-format="HH:mm" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="attendanceForm.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option label="姝e父" value="姝e父" />
+ <el-option label="杩熷埌" value="杩熷埌" />
+ <el-option label="鏃╅��" value="鏃╅��" />
+ <el-option label="缂哄嫟" value="缂哄嫟" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="showAttendanceDialog = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="submitAttendance">鎻愪氦</el-button>
+ </template>
+ </el-dialog>
+
+ <!-- 涓汉淇℃伅缂栬緫寮圭獥 -->
+ <el-dialog v-model="editProfile" title="缂栬緫涓汉淇℃伅" width="500px">
+ <el-form :model="profileForm" label-width="100px">
+ <el-form-item label="濮撳悕">
+ <el-input v-model="profileForm.name" />
+ </el-form-item>
+ <el-form-item label="鑱旂郴鐢佃瘽">
+ <el-input v-model="profileForm.phone" />
+ </el-form-item>
+ <el-form-item label="閭">
+ <el-input v-model="profileForm.email" />
+ </el-form-item>
+ <el-form-item label="鍦板潃">
+ <el-input v-model="profileForm.adress" type="textarea" rows="2" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="editProfile = false">鍙栨秷</el-button>
+ <el-button type="primary" @click="saveProfile">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, watch, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+ Calendar,
+ Money,
+ Clock,
+ User
+} from '@element-plus/icons-vue'
+import { personalAttendanceRecordsListPage, personalAttendanceRecordsAdd, personalAttendanceRecordsUpdate, personalAttendanceRecordsDelete, holidayApplicationListPage, holidayApplicationAdd, holidayApplicationUpdate, holidayApplicationDelete } from '@/api/personnelManagement/selfService'
+import { compensationListPage, compensationAdd, compensationUpdate, compensationDelete } from '@/api/personnelManagement/payrollManagement'
+
+const { proxy } = getCurrentInstance()
+import { getUserProfile } from '@/api/system/user.js'
+import {staffOnJobListPage, updateStaffOnJob} from "@/api/personnelManagement/staffOnJob.js";
+
+const tableLoading = ref(false)
+// 鍒嗛〉鍙傛暟
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0
+})
+
+// 褰撳墠瑙嗗浘
+const currentView = ref('attendance')
+
+// 瀵艰埅椤�
+const navItems = [
+ { type: 'attendance', title: '鑰冨嫟璁板綍', desc: '鏌ヨ涓汉鑰冨嫟淇℃伅', icon: 'Calendar' },
+ { type: 'salary', title: '钖祫鍗�', desc: '鏌ョ湅钖祫鍙戞斁璁板綍', icon: 'Money' },
+ { type: 'leave', title: '鍋囨湡鐢宠', desc: '鍦ㄧ嚎鐢宠鍚勭被鍋囨湡', icon: 'Clock' },
+ { type: 'profile', title: '涓汉淇℃伅', desc: '缁存姢涓汉鍩烘湰淇℃伅', icon: 'User' }
+]
+
+// 鑰冨嫟鏁版嵁
+const attendanceData = ref([])
+
+// 钖祫鏁版嵁
+const salaryData = ref([])
+
+
+// 鍋囨湡鏁版嵁
+const leaveData = ref([])
+
+const currentUser = ref()
+const user= ref()
+// 涓汉淇℃伅
+const profile = ref({
+ id: '',
+ name: '',
+ employeeId: '',
+ department: '',
+ position: '',
+ hireDate: '',
+ phone: '',
+ email: '',
+ adress: ''
+ })
+
+// 寮圭獥鎺у埗
+const showLeaveDialog = ref(false)
+const editProfile = ref(false)
+const payDateStr = ref('')
+
+// 琛ㄥ崟鏁版嵁
+const leaveForm = reactive({
+ id: '',
+ type: '',
+ startDate: '',
+ endDate: '',
+ days: 0,
+ reason: '',
+ status: ''
+})
+const profileForm = reactive({
+ name: "",
+ email: "",
+ adress: "",
+ phone: "",
+})
+const joinForm = reactive({
+ id: "",
+ staffNo: "",
+ staffName: "",
+ email: "",
+ adress: "",
+ sex: "",
+ nativePlace: "",
+ postJob: "",
+ firstStudy: "",
+ profession: "",
+ age: 0,
+ phone: "",
+ emergencyContact: "",
+ emergencyContactPhone: "",
+ contractTerm: 0,
+ contractStartTime: "",
+ contractEndTime: "",
+ staffState: 1,
+})
+
+// 鏂板鑰冨嫟璁板綍锛氬脊绐椾笌琛ㄥ崟
+const operationType = ref('add')
+const leaveOperationType = ref('add')
+const showAttendanceDialog = ref(false)
+const attendanceFormRef = ref(null)
+const attendanceForm = reactive({
+ id: '',
+ date: '',
+ checkIn: '',
+ checkOut: '',
+ workHours: '',
+ status: '姝e父'
+})
+const attendanceRules = {
+ date: [{ required: true, message: '璇烽�夋嫨鏃ユ湡', trigger: 'change' }],
+ checkIn: [{ required: true, message: '璇烽�夋嫨绛惧埌鏃堕棿', trigger: 'change' }],
+ checkOut: [{ required: true, message: '璇烽�夋嫨绛鹃��鏃堕棿', trigger: 'change' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+}
+
+// 澶勭悊瀵艰埅鐐瑰嚮
+const handleNavClick = (type) => {
+ currentView.value = type
+}
+
+// 鑾峰彇鐘舵�佺被鍨�
+const getStatusType = (status) => {
+ const types = {
+ '宸查�氳繃': 'success',
+ '瀹℃壒涓�': 'warning',
+ '宸叉嫆缁�': 'danger'
+ }
+ return types[status] || 'info'
+}
+
+// 鏂板鑰冨嫟璁板綍锛堟墦寮�寮圭獥骞堕濉粯璁ゅ�硷級
+const addAttendanceRecord = () => {
+ operationType.value = 'add'
+ attendanceForm.date = new Date().toISOString().split('T')[0]
+ attendanceForm.checkIn = '09:00'
+ attendanceForm.checkOut = '18:00'
+ attendanceForm.status = '姝e父'
+ showAttendanceDialog.value = true
+}
+
+// 璁$畻宸ユ椂
+const computeWorkHours = (inStr, outStr) => {
+ const [inH, inM] = inStr.split(':').map(n => parseInt(n, 10))
+ const [outH, outM] = outStr.split(':').map(n => parseInt(n, 10))
+ const inMin = inH * 60 + inM
+ const outMin = outH * 60 + outM
+ const diff = Math.max(0, outMin - inMin)
+ const h = Math.floor(diff / 60)
+ const m = diff % 60
+ return m === 0 ? `${h}灏忔椂` : `${h}灏忔椂${m}鍒哷
+}
+
+// 缂栬緫鑰冨嫟璁板綍
+const editAttendanceRecord = (row) => {
+ operationType.value = 'edit'
+ Object.assign(attendanceForm, row)
+ showAttendanceDialog.value = true
+}
+// 鎻愪氦鏂板-缂栬緫鑰冨嫟璁板綍
+const submitAttendance = () => {
+ // if (!attendanceFormRef.value) return
+ const workHours = computeWorkHours(attendanceForm.checkIn, attendanceForm.checkOut)
+ const newRecord = {
+ date: attendanceForm.date,
+ checkIn: attendanceForm.checkIn,
+ checkOut: attendanceForm.checkOut,
+ workHours,
+ status: attendanceForm.status
+ }
+ if (operationType.value === 'add') {
+ personalAttendanceRecordsAdd(newRecord)
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success('鑰冨嫟璁板綍娣诲姞鎴愬姛')
+ getPersonalAttendanceRecordsList()
+ showAttendanceDialog.value = false
+ // 閲嶇疆琛ㄥ崟
+ attendanceForm.date = ''
+ attendanceForm.checkIn = ''
+ attendanceForm.checkOut = ''
+ attendanceForm.status = '姝e父'
+ }
+ }).catch(err => {
+ ElMessage.error('鑰冨嫟璁板綍娣诲姞澶辫触')
+ })
+ }else{
+ attendanceForm.workHours = computeWorkHours(attendanceForm.checkIn, attendanceForm.checkOut)
+ personalAttendanceRecordsUpdate(attendanceForm)
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success('鑰冨嫟璁板綍鏇存柊鎴愬姛')
+ getPersonalAttendanceRecordsList()
+ showAttendanceDialog.value = false
+ // 閲嶇疆琛ㄥ崟
+ attendanceForm.date = ''
+ attendanceForm.checkIn = ''
+ attendanceForm.checkOut = ''
+ attendanceForm.status = '姝e父'
+ }
+ }).catch(err => {
+ ElMessage.error('鑰冨嫟璁板綍鏇存柊澶辫触')
+ })
+ }
+ // attendanceFormRef.value.validate((valid) => {
+ // if (!valid) return
+
+
+ // })
+}
+// 鍒犻櫎鑰冨嫟璁板綍
+const deleteAttendanceRecord = (row) => {
+
+ ElMessageBox.confirm('纭畾鍒犻櫎璇ヨ�冨嫟璁板綍鍚楋紵', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ personalAttendanceRecordsDelete(row.id)
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success('鑰冨嫟璁板綍鍒犻櫎鎴愬姛')
+ getPersonalAttendanceRecordsList()
+ }
+ }).catch(err => {
+ ElMessage.error('鑰冨嫟璁板綍鍒犻櫎澶辫触')
+ })
+ }).catch(() => {
+ ElMessage({
+ type: 'info',
+ message: '宸插彇娑堝垹闄�'
+ })
+ })
+}
+// 鐢宠鍋囨湡
+const openLeaveForm = () => {
+ leaveOperationType.value = 'add'
+ showLeaveDialog.value = true
+ // leaveForm.type = ''
+ // leaveForm.startDate = ''
+ // leaveForm.endDate = ''
+ // leaveForm.days = 0
+ // leaveForm.reason = ''
+ // leaveForm.status = 'warning'
+}
+// 缂栬緫鍋囨湡璁板綍
+const editLeaveRecord = (row) => {
+ leaveOperationType.value = 'edit'
+ showLeaveDialog.value = true
+ Object.assign(leaveForm, row)
+ // ElMessage.info('缂栬緫鍔熻兘寮�鍙戜腑...')
+}
+
+// 鍒犻櫎鍋囨湡璁板綍
+const deleteLeaveRecord = (row) => {
+ ElMessageBox.confirm('纭畾鍒犻櫎璇ュ亣鏈熻褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ holidayApplicationDelete(row.id)
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success('鍋囨湡璁板綍鍒犻櫎鎴愬姛')
+ getHolidayApplicationList()
+ }
+ }).catch(err => {
+ ElMessage.error('鍋囨湡璁板綍鍒犻櫎澶辫触')
+ })
+ }).catch(() => {
+ ElMessage({
+ type: 'info',
+ message: '宸插彇娑堝垹闄�'
+ })
+ })
+}
+
+//璁$畻鍋囨湡澶╂暟
+const calculateDays = () => {
+ try {
+ if (leaveForm.startDate && leaveForm.endDate) {
+ const start = new Date(leaveForm.startDate)
+ const end = new Date(leaveForm.endDate)
+ leaveForm.startDate = start.toISOString().split('T')[0]
+ leaveForm.endDate = end.toISOString().split('T')[0]
+
+ if (isNaN(start.getTime()) || isNaN(end.getTime())) {
+ console.warn('鏃犳晥鐨勬棩鏈熸牸寮�')
+ return
+ }
+
+ const diffTime = Math.abs(end - start)
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1
+ leaveForm.days = diffDays
+ }
+ } catch (error) {
+ console.error('璁$畻澶╂暟澶辫触:', error)
+ }
+}
+
+// 鎻愪氦鍋囨湡鐢宠
+const submitLeaveApplication = () => {
+ if (leaveOperationType.value === 'add') {
+ if (!leaveForm.type || !leaveForm.startDate || !leaveForm.endDate || !leaveForm.reason) {
+ ElMessage.warning('璇峰~鍐欏畬鏁翠俊鎭�')
+ return
+ }
+ calculateDays()
+ const newLeave = {
+ type: leaveForm.type,
+ startDate: leaveForm.startDate,
+ endDate: leaveForm.endDate,
+ days: leaveForm.days, // 绠�鍗曡绠�
+ reason: leaveForm.reason,
+ status: '瀹℃壒涓�'
+ }
+
+ holidayApplicationAdd(newLeave)
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success('鍋囨湡鐢宠鎻愪氦鎴愬姛')
+ getHolidayApplicationList()
+ showLeaveDialog.value = false
+ // 閲嶇疆琛ㄥ崟
+ Object.keys(leaveForm).forEach(key => {
+ leaveForm[key] = ''
+ })
+ }
+ }).catch(err => {
+ ElMessage.error('鍋囨湡鐢宠鎻愪氦澶辫触')
+ })
+ }else{
+ calculateDays()
+ holidayApplicationUpdate(leaveForm)
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success('鍋囨湡鐢宠鏇存柊鎴愬姛')
+ getHolidayApplicationList()
+ showLeaveDialog.value = false
+ // 閲嶇疆琛ㄥ崟
+ Object.keys(leaveForm).forEach(key => {
+ leaveForm[key] = ''
+ })
+ }
+ }).catch(err => {
+ ElMessage.error('鍋囨湡鐢宠鏇存柊澶辫触')
+ })
+ }
+}
+
+// 鑾峰彇涓汉淇℃伅
+const getProfile = () => {
+ tableLoading.value = true;
+ getUserProfile().then(res => {
+ if (res.code === 200) {
+ currentUser.value = res.data
+ // console.log("----",currentUser.value)
+ //寰楀埌浜哄憳鍒楄〃
+ staffOnJobListPage({staffState: 1}).then(res => {
+ //绛涢�夊嚭鍜宑urrentUser鍚屽悕鐨勪汉鍛�
+ // let tableData = res.data.records
+ user.value = res.data.records.find(item => item.staffName === currentUser.value.userName)
+ // console.log("++++",user.value)
+ if(user.value){
+ profile.value.id=user.value.id
+ profile.value.name=user.value.staffName
+ profile.value.employeeId=user.value.staffNo
+ profile.value.phone=user.value.phone
+ profile.value.email=currentUser.value.email
+ profile.value.adress=user.value.adress
+ profile.value.position=user.value.postJob
+ profile.value.hireDate=user.value.createTime
+ profile.value.department=currentUser.value.deptNames
+ }
+ // console.log(profile.value)
+ // tableLoading.value = false;
+ }).catch(err => {})
+ }
+ }).catch(err => {
+ tableLoading.value = false;
+ ElMessage.error('鑾峰彇涓汉淇℃伅澶辫触')
+ })
+}
+// 淇濆瓨涓汉淇℃伅
+const saveProfile = async () => {
+ tableLoading.value = true;
+ try {
+ const userRes = await getUserProfile();
+ if (userRes.code === 200) {
+ currentUser.value = userRes.data;
+ const staffListRes = await staffOnJobListPage({ staffState: 1 });
+ user.value = staffListRes.data.records.find(item => item.staffName === currentUser.value.userName);
+ Object.assign(joinForm, user.value);
+ joinForm.staffName = profileForm.name;
+ joinForm.phone = profileForm.phone;
+ joinForm.email = profileForm.email;
+ joinForm.adress = profileForm.adress;
+ // 璋冪敤鏇存柊涓汉淇℃伅鐨勬帴鍙�
+ updateStaffOnJob(user.value.id, joinForm).then(res => {
+ if (res.code === 200) {
+ ElMessage.success('涓汉淇℃伅淇濆瓨鎴愬姛');
+ getProfile();
+ editProfile.value = false;
+ }
+ }).catch(err => {
+ ElMessage.error('涓汉淇℃伅淇濆瓨澶辫触');
+ })
+ }
+ } catch (err) {
+ ElMessage.error('鑾峰彇涓汉淇℃伅澶辫触');
+ } finally {
+ tableLoading.value = false;
+ }
+};
+
+// 缂栬緫涓汉淇℃伅
+const editProfileForm = () => {
+ editProfile.value = true;
+ Object.assign(profileForm, {
+ name: profile.value.name,
+ phone: profile.value.phone,
+ email: profile.value.email,
+ adress: profile.value.adress,
+ });
+};
+
+//鏈堜唤鏀瑰彉
+const changMonth = () => {
+ getCompensationList()
+}
+//鑾峰彇鑰冨嫟璁板綍鍒楄〃
+const getPersonalAttendanceRecordsList = async () => {
+ tableLoading.value = true
+ personalAttendanceRecordsListPage(page)
+ .then(res => {
+
+ attendanceData.value = res.data.records
+ page.value.total = res.data.total;
+ tableLoading.value = false;
+
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+}
+//钖祫鍗曟煡璇�
+const getCompensationList = async () => {
+ tableLoading.value = true
+ compensationListPage({...page,payDateStr:payDateStr.value})
+ .then(res => {
+ salaryData.value = res.data.records
+ //杩囨护鍑哄綋鍓嶆湀浠界殑鎵f鍚堣
+ salaryData.value.forEach(item => {
+ item.deduction =0 + item.deductionAbsenteeism+item.sickLeaveDeductions+item.deductionPersonalLeave+item.forgetClockDeduct,
+ item.bonus=0,
+ item.status='宸插彂鏀�'
+ })
+
+ page.value.total = res.data.total;
+ tableLoading.value = false;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+}
+//鑾峰彇鍋囨湡鐢宠鍒楄〃
+const getHolidayApplicationList = async () => {
+ tableLoading.value = true
+ holidayApplicationListPage(page)
+ .then(res => {
+ leaveData.value = res.data.records
+ page.value.total = res.data.total;
+ tableLoading.value = false;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+}
+onMounted(() => {
+ // 鍒濆鍖�
+ getPersonalAttendanceRecordsList()
+ getCompensationList()
+ getHolidayApplicationList()
+ getProfile()
+})
+</script>
+
+<style scoped>
+.self-service-container {
+ padding: 20px;
+ background-color: #f5f7fa;
+ min-height: 100vh;
+}
+
+.page-header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 12px;
+ color: white;
+}
+
+.page-header h2 {
+ color: white;
+ margin-bottom: 10px;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.page-header p {
+ color: rgba(255, 255, 255, 0.9);
+ font-size: 14px;
+ margin: 0;
+}
+
+.nav-cards {
+ margin-bottom: 30px;
+}
+
+.nav-card {
+ cursor: pointer;
+ transition: all 0.3s ease;
+ border-radius: 12px;
+ border: none;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.nav-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+}
+
+.nav-content {
+ text-align: center;
+ padding: 20px;
+}
+
+.nav-icon {
+ color: #409EFF;
+ margin-bottom: 15px;
+}
+
+.nav-content h3 {
+ margin: 0 0 10px 0;
+ color: #303133;
+ font-size: 18px;
+}
+
+.nav-content p {
+ margin: 0;
+ color: #909399;
+ font-size: 14px;
+}
+
+.main-content {
+ margin-bottom: 30px;
+}
+
+.content-card {
+ border-radius: 12px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ border: none;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: 600;
+ color: #303133;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 768px) {
+ .self-service-container {
+ padding: 10px;
+ }
+
+ .nav-cards .el-col {
+ margin-bottom: 15px;
+ }
+
+ .page-header h2 {
+ font-size: 24px;
+ }
+}
+</style>
diff --git a/src/views/personnelManagement/socialSecuritySet/components/formDia.vue b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
new file mode 100644
index 0000000..71f5e61
--- /dev/null
+++ b/src/views/personnelManagement/socialSecuritySet/components/formDia.vue
@@ -0,0 +1,470 @@
+<template>
+ <div>
+ <FormDialog
+ v-model="dialogFormVisible"
+ :operation-type="operationType"
+ :title="dialogTitle"
+ width="80%"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia"
+ >
+ <el-form ref="formRef" :model="form" :rules="rules" label-position="top">
+ <el-row :gutter="24">
+ <!-- 宸︿晶锛氶�傜敤浜哄憳 -->
+ <el-col :span="8">
+ <el-form-item label="閫傜敤浜哄憳锛�" prop="deptIds">
+ <div class="dept-checkbox-wrap">
+ <el-checkbox-group
+ v-model="form.deptIds"
+ :disabled="isDetail"
+ >
+ <div
+ v-for="dept in deptList"
+ :key="dept.deptId"
+ class="dept-checkbox-item"
+ >
+ <el-checkbox :value="dept.deptId">
+ {{ dept.deptName }}
+ <span v-if="dept.personCount != null" class="dept-count"
+ >{{ dept.personCount }}浜�</span
+ >
+ </el-checkbox>
+ </div>
+ </el-checkbox-group>
+ </div>
+ </el-form-item>
+ </el-col>
+ <!-- 鍙充晶锛氬熀纭�淇℃伅 + 淇濋櫓绫诲瀷 -->
+ <el-col :span="16">
+ <!-- 鍩虹淇℃伅 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title"><span class="card-title-line">|</span> 鍩虹淇℃伅</span>
+ <el-icon class="card-collapse"><ArrowUp /></el-icon>
+ </template>
+ <el-form-item label="鏂规鏍囬锛�" prop="title">
+ <el-input
+ v-model="form.title"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="isDetail"
+ />
+ </el-form-item>
+ <el-form-item label="澶囨敞锛�" prop="remark">
+ <el-input
+ v-model="form.remark"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="isDetail"
+ />
+ </el-form-item>
+ </el-card>
+
+ <!-- 淇濋櫓绫诲瀷 -->
+ <el-card class="form-card" shadow="never">
+ <template #header>
+ <span class="card-title"><span class="card-title-line">|</span> 淇濋櫓绫诲瀷</span>
+ <el-button
+ v-if="!isDetail"
+ type="primary"
+ size="small"
+ @click="addInsuranceBenefit"
+ >
+ 娣诲姞淇濋櫓绂忓埄
+ </el-button>
+ </template>
+ <el-row :gutter="16">
+ <el-col
+ v-for="(item, index) in form.insuranceBenefits"
+ :key="item._key"
+ :span="12"
+ >
+ <div class="insurance-benefit-card">
+ <div class="insurance-benefit-title">
+ 淇濋櫓绂忓埄{{ index + 1 }}
+ <el-button
+ v-if="!isDetail && form.insuranceBenefits.length > 1"
+ type="danger"
+ link
+ size="small"
+ class="card-delete-btn"
+ @click="removeInsuranceBenefit(index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <el-form-item
+ :prop="'insuranceBenefits.' + index + '.insuranceType'"
+ label="淇濋櫓绫诲瀷锛�"
+ label-width="100px"
+ >
+ <el-select
+ v-model="item.insuranceType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 100%"
+ :disabled="isDetail"
+ >
+ <el-option
+ v-for="opt in insuranceTypeOptions"
+ :key="opt.value"
+ :label="opt.label"
+ :value="opt.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="缂磋垂鍩烘暟锛�" label-width="100px">
+ <div class="base-salary-wrap">
+ <el-input
+ v-model="item.paymentBase"
+ placeholder="鏍规嵁鍩烘湰宸ヨ祫缂寸撼"
+ clearable
+ style="width: 120px"
+ type="number"
+ :disabled="isDetail || item.useBasicSalary"
+ @input="handlePaymentBaseInput(item)"
+ />
+ <el-checkbox
+ v-model="item.useBasicSalary"
+ @change="handleUseBasicSalaryChange(item)"
+ :disabled="isDetail"
+ >
+ 璋冪敤鍩烘湰宸ヨ祫
+ </el-checkbox>
+ </div>
+ </el-form-item>
+ <el-form-item label="涓汉缂磋垂姣斾緥锛�" label-width="100px">
+ <div class="personal-ratio-wrap">
+ <el-input
+ v-model="item.personalRatio"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 100px"
+ type="number"
+ :disabled="isDetail"
+ />
+ <span class="ratio-unit">(%)</span>
+ <span class="ratio-plus">+</span>
+ <el-input
+ v-model="item.personalFixed"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 100px"
+ type="number"
+ :disabled="isDetail"
+ />
+ </div>
+ </el-form-item>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, toRefs, getCurrentInstance, nextTick, computed } from "vue";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { ArrowUp } from "@element-plus/icons-vue";
+import { listDept } from "@/api/system/dept.js";
+import { socialSecurityAdd, socialSecurityUpdate } from "@/api/personnelManagement/socialSecuritySet.js";
+
+const emit = defineEmits(["close"]);
+const { proxy } = getCurrentInstance();
+
+const dialogFormVisible = ref(false);
+const operationType = ref("add");
+const formRef = ref(null);
+const deptList = ref([]);
+
+const isDetail = computed(() => operationType.value === "detail");
+
+const dialogTitle = () =>
+ operationType.value === "add"
+ ? "鏂板鏂规"
+ : operationType.value === "edit"
+ ? "缂栬緫鏂规"
+ : "鏂规璇︽儏";
+
+// 淇濋櫓绫诲瀷閫夐」锛堝彲鎸夊瓧鍏告浛鎹級
+const insuranceTypeOptions = [
+ { label: "鍏昏�佷繚闄�", value: "鍏昏�佷繚闄�" },
+ { label: "鍖荤枟淇濋櫓", value: "鍖荤枟淇濋櫓" },
+ { label: "澶变笟淇濋櫓", value: "澶变笟淇濋櫓" },
+ { label: "宸ヤ激淇濋櫓", value: "宸ヤ激淇濋櫓" },
+ { label: "鐢熻偛淇濋櫓", value: "鐢熻偛淇濋櫓" },
+ { label: "鍏Н閲�", value: "鍏Н閲�" },
+];
+
+const defaultBenefit = () => ({
+ _key: Math.random().toString(36).slice(2),
+ insuranceType: "",
+ paymentBase: "",
+ useBasicSalary: false,
+ personalRatio: "",
+ personalFixed: "",
+});
+
+const data = reactive({
+ form: {
+ id: undefined,
+ title: "",
+ remark: "",
+ deptIds: [],
+ insuranceBenefits: [defaultBenefit()],
+ },
+ rules: {
+ title: [{ required: true, message: "璇疯緭鍏ユ柟妗堟爣棰�", trigger: "blur" }],
+ deptIds: [
+ {
+ required: true,
+ type: "array",
+ min: 1,
+ message: "璇疯嚦灏戦�夋嫨涓�涓�傜敤閮ㄩ棬",
+ trigger: "change",
+ },
+ ],
+ },
+});
+const { form, rules } = toRefs(data);
+
+function flattenDept(tree, list = []) {
+ if (!tree || !tree.length) return list;
+ tree.forEach((node) => {
+ list.push({
+ deptId: node.deptId,
+ deptName: node.deptName,
+ personCount: node.personCount ?? null,
+ });
+ if (node.children && node.children.length) {
+ flattenDept(node.children, list);
+ }
+ });
+ return list;
+}
+
+const loadDeptList = () => {
+ listDept().then((res) => {
+ const tree = res.data ?? [];
+ deptList.value = flattenDept(tree);
+ });
+};
+
+const addInsuranceBenefit = () => {
+ form.value.insuranceBenefits.push(defaultBenefit());
+};
+
+const removeInsuranceBenefit = (index) => {
+ form.value.insuranceBenefits.splice(index, 1);
+};
+
+const handleUseBasicSalaryChange = (item) => {
+ if (item.useBasicSalary) {
+ item.paymentBase = "";
+ }
+};
+
+const handlePaymentBaseInput = (item) => {
+ if (item.paymentBase !== "" && item.paymentBase != null) {
+ item.useBasicSalary = false;
+ }
+};
+
+const resetForm = () => {
+ form.value = {
+ id: undefined,
+ title: "",
+ remark: "",
+ deptIds: [],
+ insuranceBenefits: [defaultBenefit()],
+ };
+};
+
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ loadDeptList();
+ resetForm();
+ if ((type === "edit" || type === "detail") && row) {
+ const d = row || {};
+ form.value.id = d.id;
+ form.value.title = d.title;
+ form.value.remark = d.remark ?? "";
+ // deptIds 鍚庣鍙兘鏄�楀彿鍒嗛殧瀛楃涓叉垨鏁扮粍锛岃繖閲岀粺涓�杞负鏁扮粍骞跺敖閲忚繕鍘熸暟鍊肩被鍨�
+ if (d.deptIds) {
+ form.value.deptIds = String(d.deptIds)
+ .split(",")
+ .filter((v) => v !== "")
+ .map((v) => {
+ const num = Number(v);
+ return Number.isNaN(num) ? v : num;
+ });
+ } else {
+ form.value.deptIds = [];
+ }
+ const detailList = d.schemeInsuranceDetailList || [];
+ form.value.insuranceBenefits =
+ detailList.length > 0
+ ? detailList.map((b) => ({
+ _key: Math.random().toString(36).slice(2),
+ insuranceType: b.insuranceType || "",
+ paymentBase: b.paymentBase ?? "",
+ useBasicSalary: b.useBasicSalary === 2,
+ personalRatio: b.personalRatio ?? "",
+ personalFixed: b.personalFixed ?? "",
+ }))
+ : [defaultBenefit()];
+ }
+};
+
+const submitForm = () => {
+ // 璇︽儏妯″紡涓嬩笉鎻愪氦锛屽彧鍏抽棴寮圭獥
+ if (operationType.value === "detail") {
+ closeDia();
+ return;
+ }
+ formRef.value?.validate((valid) => {
+ if (!valid) return;
+ const deptIds =
+ Array.isArray(form.value.deptIds) && form.value.deptIds.length
+ ? form.value.deptIds.join(",")
+ : "";
+ const schemeInsuranceDetailList = (form.value.insuranceBenefits || []).map(
+ ({ _key, ...rest }) => ({
+ ...rest,
+ useBasicSalary: rest.useBasicSalary ? 2 : 1,
+ })
+ );
+ const insuranceTypes = schemeInsuranceDetailList
+ .map((item) => item.insuranceType)
+ .filter((v) => v)
+ .join(",");
+ // 閮ㄩ棬鍚嶇О锛屽涓娇鐢ㄩ�楀彿闅斿紑锛堟牴鎹�変腑鐨� deptIds 涓� deptList 璁$畻锛�
+ const deptNames = (deptList.value || [])
+ .filter((d) =>
+ (form.value.deptIds || []).some(
+ (id) => String(id) === String(d.deptId)
+ )
+ )
+ .map((d) => d.deptName)
+ .join(",");
+ const submitData = {
+ id: form.value.id,
+ title: form.value.title,
+ remark: form.value.remark ?? "",
+ deptIds,
+ insuranceTypes,
+ deptNames,
+ schemeInsuranceDetailList,
+ };
+ if (operationType.value === "add") {
+ socialSecurityAdd(submitData).then(() => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ closeDia();
+ });
+ } else {
+ socialSecurityUpdate(submitData).then(() => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeDia();
+ });
+ }
+ });
+};
+
+const closeDia = () => {
+ proxy.resetForm?.("formRef");
+ dialogFormVisible.value = false;
+ emit("close");
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped>
+.card-title-line {
+ color: #f56c6c;
+ margin-right: 4px;
+}
+.form-card {
+ margin-bottom: 16px;
+}
+.form-card :deep(.el-card__header) {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 16px;
+}
+.card-title {
+ font-weight: 500;
+}
+.card-collapse {
+ color: #999;
+ cursor: pointer;
+}
+.dept-checkbox-wrap {
+ max-height: 320px;
+ overflow-y: auto;
+ padding: 8px 0;
+ border: 1px solid var(--el-border-color);
+ border-radius: 4px;
+ background: #fff;
+}
+.dept-checkbox-item {
+ padding: 6px 12px;
+}
+.dept-count {
+ color: #909399;
+ font-size: 12px;
+ margin-left: 4px;
+}
+.insurance-benefit-card {
+ border: 1px solid var(--el-border-color-lighter);
+ border-radius: 4px;
+ padding: 12px 16px;
+ margin-bottom: 12px;
+ background: #fafafa;
+}
+.insurance-benefit-title {
+ font-size: 14px;
+ margin-bottom: 12px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+.card-delete-btn {
+ margin-left: auto;
+}
+.checkbox-group-inline {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+}
+.base-salary-wrap {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 8px;
+}
+.base-salary-text {
+ color: #606266;
+ font-size: 14px;
+}
+.personal-ratio-wrap {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.ratio-unit,
+.ratio-plus {
+ color: #606266;
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/personnelManagement/socialSecuritySet/index.vue b/src/views/personnelManagement/socialSecuritySet/index.vue
new file mode 100644
index 0000000..1f3f104
--- /dev/null
+++ b/src/views/personnelManagement/socialSecuritySet/index.vue
@@ -0,0 +1,212 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">涓婚锛�</span>
+ <el-input
+ v-model="searchForm.title"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヤ富棰�"
+ clearable
+ @keyup.enter="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <div style="margin-bottom: 10px; display: flex; gap: 10px">
+ <el-button type="primary" @click="openForm('add')">鏂板鏂规</el-button>
+ <el-button
+ type="danger"
+ @click="handleBatchDelete"
+ :disabled="selectedRows.length === 0"
+ >
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ :total="page.total"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="pagination"
+ />
+ </div>
+ <form-dia ref="formDiaRef" @close="handleQuery" />
+ </div>
+</template>
+
+<script setup>
+import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
+import { ElMessageBox, ElMessage } from "element-plus";
+import FormDia from "./components/formDia.vue";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import {
+ socialSecurityListPage,
+ socialSecurityDelete,
+} from "@/api/personnelManagement/socialSecuritySet.js";
+
+const data = reactive({
+ searchForm: {
+ title: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+const tableColumn = ref([
+ { label: "涓婚", prop: "title", minWidth: 120 },
+ { label: "淇濋櫓绫诲瀷", prop: "insuranceTypes", width: 120 },
+ { label: "浣跨敤鑼冨洿", prop: "deptNames", width: 120 },
+ { label: "澶囨敞", prop: "remark", minWidth: 120 },
+ { label: "鍒涘缓鏃堕棿", prop: "createTime", width: 160 },
+ { label: "鍒涘缓浜�", prop: "createUserName", width: 100 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 180,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => openForm("edit", row),
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => openForm("detail", row),
+ },
+ {
+ name: "鍒犻櫎",
+ type: "text",
+ style: {
+ color: "#F56C6C",
+ },
+ clickFun: (row) => handleDelete(row),
+ },
+ ],
+ },
+]);
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+const selectedRows = ref([]);
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+const formDiaRef = ref(null);
+
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+const handleReset = () => {
+ searchForm.value.title = "";
+ page.current = 1;
+ getList();
+};
+
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ socialSecurityListPage({
+ ...searchForm.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data?.records ?? [];
+ page.total = res.data?.total ?? 0;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+const openForm = (type, row) => {
+ nextTick(() => {
+ formDiaRef.value?.openDialog(type, row);
+ });
+};
+
+// 鍒犻櫎鏂规锛岄�昏緫涓庡叾瀹冮〉闈繚鎸佷竴鑷达紙纭寮圭獥 + 璋冪敤鍒犻櫎鎺ュ彛 + 鍒锋柊鍒楄〃锛�
+const handleDelete = (row) => {
+ ElMessageBox.confirm(
+ `纭鍒犻櫎鏂规"${row.title}"鍚楋紵`,
+ "鍒犻櫎纭",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ socialSecurityDelete([row.id])
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+// 鎵归噺鍒犻櫎
+const handleBatchDelete = () => {
+ if (!selectedRows.value.length) return;
+ ElMessageBox.confirm(
+ `纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉℃柟妗堝悧锛焋,
+ "鎵归噺鍒犻櫎纭",
+ {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ const ids = selectedRows.value.map((item) => item.id);
+ socialSecurityDelete(ids)
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ ElMessage.info("宸插彇娑堝垹闄�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/procurementManagement/advancedPriceManagement/index.vue b/src/views/procurementManagement/advancedPriceManagement/index.vue
new file mode 100644
index 0000000..84bd160
--- /dev/null
+++ b/src/views/procurementManagement/advancedPriceManagement/index.vue
@@ -0,0 +1,773 @@
+<template>
+ <div class="app-container">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-card class="search-card" shadow="never">
+ <el-form :model="searchForm" :inline="true" label-width="100px">
+ <el-form-item label="鍟嗗搧鍚嶇О锛�">
+ <el-input v-model="searchForm.productName" placeholder="璇疯緭鍏ュ晢鍝佸悕绉�" clearable style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟嗭細">
+ <el-select v-model="searchForm.supplierId" placeholder="璇烽�夋嫨渚涘簲鍟�" clearable style="width: 200px">
+ <el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.name" :value="supplier.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch" :loading="loading">
+ <el-icon><Search /></el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="resetSearch">
+ <el-icon><Refresh /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 鍔熻兘鎸夐挳鍖哄煙 -->
+ <el-card class="action-card" shadow="never">
+ <div class="action-buttons">
+ <el-button type="primary" @click="openDialog('add')">
+ <el-icon><Plus /></el-icon>
+ 鏂板浠锋牸
+ </el-button>
+ <el-button type="success" @click="openBatchDiscountDialog">
+ <el-icon><Discount /></el-icon>
+ 鎵归噺鎶樻墸
+ </el-button>
+ <el-button type="info" @click="exportData">
+ <el-icon><Download /></el-icon>
+ 瀵煎嚭鏁版嵁
+ </el-button>
+ <el-button type="danger" @click="handleBatchDelete" :disabled="selectedRows.length === 0">
+ <el-icon><Delete /></el-icon>
+ 鎵归噺鍒犻櫎
+ </el-button>
+ </div>
+ </el-card>
+
+
+ <!-- 涓昏〃鏍� -->
+ <el-card class="table-card" shadow="never">
+ <el-table
+ :data="tableData"
+ border
+ v-loading="loading"
+ @selection-change="handleSelectionChange"
+ :default-sort="{ prop: 'updateTime', order: 'descending' }"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鍟嗗搧淇℃伅" min-width="200">
+ <template #default="{ row }">
+ <div class="product-info">
+ <div class="product-name">{{ row.productName }}</div>
+ <div class="product-spec">{{ row.specification }}</div>
+ <div class="product-code">缂栫爜: {{ row.productCode }}</div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="渚涘簲鍟�" prop="supplierName" width="150" />
+ <el-table-column label="鍩虹浠锋牸" width="120" align="right">
+ <template #default="{ row }">
+ <span class="price-text">楼{{ row.basePrice }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎶樻墸淇℃伅" width="150">
+ <template #default="{ row }">
+ <div v-if="row.discountType">
+ <el-tag :type="getDiscountTagType(row.discountType)" size="small">
+ {{ getDiscountText(row.discountType) }}
+ </el-tag>
+ <div class="discount-value">{{ row.discountValue }}{{ row.discountType === 'percentage' ? '%' : '鍏�' }}</div>
+ </div>
+ <span v-else class="no-discount">鏃犳姌鎵�</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹為檯浠锋牸" width="120" align="right">
+ <template #default="{ row }">
+ <span class="final-price">楼{{ calculateFinalPrice(row) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="浠锋牸鎺у埗" width="120">
+ <template #default="{ row }">
+ <div class="price-control">
+ <div v-if="row.minPrice" class="control-item">
+ 鏈�浣�: 楼{{ row.minPrice }}
+ </div>
+ <div v-if="row.maxPrice" class="control-item">
+ 鏈�楂�: 楼{{ row.maxPrice }}
+ </div>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" width="100" align="center">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+ <div v-if="isPriceWarning(row)" class="warning-indicator">
+ <el-icon color="#F56C6C"><Warning /></el-icon>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢熸晥鏃堕棿" prop="effectiveTime" width="180" />
+ <el-table-column label="鏇存柊鏃堕棿" prop="updateTime" width="180" sortable />
+ <el-table-column label="鎿嶄綔" width="250" align="center" fixed="right">
+ <template #default="{ row }">
+ <el-button type="primary" link @click="openDialog('edit', row)">
+ <el-icon><Edit /></el-icon>
+ 缂栬緫
+ </el-button>
+ <el-button type="danger" link @click="handleDelete(row)">
+ <el-icon><Delete /></el-icon>
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <div class="pagination-wrapper">
+ <el-pagination
+ v-model:current-page="pagination.current"
+ v-model:page-size="pagination.size"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </el-card>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <FormDialog v-model="dialogVisible" :title="dialogType === 'add' ? '鏂板浠锋牸' : '缂栬緫浠锋牸'" :width="'800px'" :operation-type="dialogType" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="formData" :rules="formRules" ref="formRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍟嗗搧鍚嶇О" prop="productName">
+ <el-select v-model="formData.productName" placeholder="璇烽�夋嫨鍟嗗搧" style="width: 100%" filterable>
+ <el-option v-for="product in productList" :key="product.id" :label="product.name" :value="product.name" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍟嗗搧缂栫爜" prop="productCode">
+ <el-input v-model="formData.productCode" placeholder="璇疯緭鍏ュ晢鍝佺紪鐮�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿" prop="specification">
+ <el-input v-model="formData.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟�" prop="supplierName">
+ <el-select v-model="formData.supplierName" placeholder="璇烽�夋嫨渚涘簲鍟�" style="width: 100%">
+ <el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.name" :value="supplier.name" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍩虹浠锋牸" prop="basePrice">
+ <el-input-number v-model="formData.basePrice" :min="0" :precision="2" placeholder="璇疯緭鍏ュ熀纭�浠锋牸" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅">
+ <el-input v-model="formData.unit" placeholder="璇疯緭鍏ュ崟浣�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 鎶樻墸璁剧疆 -->
+ <el-divider content-position="left">鎶樻墸璁剧疆</el-divider>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鎶樻墸绫诲瀷">
+ <el-select v-model="formData.discountType" placeholder="璇烽�夋嫨鎶樻墸绫诲瀷" style="width: 100%">
+ <el-option label="鏃犳姌鎵�" value="" />
+ <el-option label="鐧惧垎姣旀姌鎵�" value="percentage" />
+ <el-option label="鍥哄畾閲戦" value="fixed" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎶樻墸鍊�" v-if="formData.discountType && formData.discountType !== 'tiered'">
+ <el-input-number
+ v-model="formData.discountValue"
+ :min="0"
+ :max="formData.discountType === 'percentage' ? 100 : undefined"
+ :precision="2"
+ placeholder="璇疯緭鍏ユ姌鎵e��"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鎶樻墸鏈夋晥鏈�">
+ <el-date-picker
+ v-model="formData.discountEndTime"
+ type="datetime"
+ placeholder="閫夋嫨缁撴潫鏃堕棿"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <!-- 浠锋牸鎺у埗 -->
+ <el-divider content-position="left">浠锋牸鎺у埗</el-divider>
+ <el-row :gutter="20">
+ <el-col :span="8">
+ <el-form-item label="鏈�浣庝环鏍�">
+ <el-input-number v-model="formData.minPrice" :min="0" :precision="2" placeholder="鏈�浣庝环鏍�" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="鏈�楂樹环鏍�">
+ <el-input-number v-model="formData.maxPrice" :min="0" :precision="2" placeholder="鏈�楂樹环鏍�" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8">
+ <el-form-item label="棰勮闃堝��(%)">
+ <el-input-number v-model="formData.warningThreshold" :min="0" :max="100" :precision="1" placeholder="棰勮闃堝��" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐢熸晥鏃堕棿" prop="effectiveTime">
+ <el-date-picker v-model="formData.effectiveTime" type="datetime" placeholder="閫夋嫨鐢熸晥鏃堕棿" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶辨晥鏃堕棿">
+ <el-date-picker v-model="formData.expireTime" type="datetime" placeholder="閫夋嫨澶辨晥鏃堕棿" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="璋冧环鍘熷洜" prop="reason">
+ <el-select v-model="formData.reason" placeholder="璇烽�夋嫨璋冧环鍘熷洜" style="width: 100%">
+ <el-option label="甯傚満浠锋牸鍙樺姩" value="market" />
+ <el-option label="鎴愭湰鍙樺寲" value="cost" />
+ <el-option label="渚涘簲鍟嗚皟鏁�" value="supplier" />
+ <el-option label="瀛h妭鎬ц皟鏁�" value="seasonal" />
+ <el-option label="淇冮攢娲诲姩" value="promotion" />
+ <el-option label="鍏朵粬鍘熷洜" value="other" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="澶囨敞">
+ <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+
+ <!-- 鎵归噺鎶樻墸瀵硅瘽妗� -->
+ <FormDialog v-model="batchDiscountVisible" title="鎵归噺璁剧疆鎶樻墸" :width="'600px'" @close="batchDiscountVisible = false" @confirm="handleBatchDiscount" @cancel="batchDiscountVisible = false">
+ <el-form :model="batchDiscountForm" label-width="120px">
+ <el-form-item label="鎶樻墸绫诲瀷">
+ <el-select v-model="batchDiscountForm.discountType" placeholder="璇烽�夋嫨鎶樻墸绫诲瀷" style="width: 100%">
+ <el-option label="鐧惧垎姣旀姌鎵�" value="percentage" />
+ <el-option label="鍥哄畾閲戦" value="fixed" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎶樻墸鍊�">
+ <el-input-number
+ v-model="batchDiscountForm.discountValue"
+ :min="0"
+ :max="batchDiscountForm.discountType === 'percentage' ? 100 : undefined"
+ :precision="2"
+ placeholder="璇疯緭鍏ユ姌鎵e��"
+ style="width: 100%"
+ />
+ </el-form-item>
+ <el-form-item label="鐢熸晥鏃堕棿">
+ <el-date-picker v-model="batchDiscountForm.effectiveTime" type="datetime" placeholder="閫夋嫨鐢熸晥鏃堕棿" style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="澶辨晥鏃堕棿">
+ <el-date-picker v-model="batchDiscountForm.expireTime" type="datetime" placeholder="閫夋嫨澶辨晥鏃堕棿" style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="閫傜敤鍟嗗搧">
+ <div class="selected-items">
+ 宸查�夋嫨 {{ selectedRows.length }} 涓晢鍝�
+ </div>
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+
+ <!-- 浠锋牸鎺у埗瀵硅瘽妗� -->
+ <FormDialog v-model="priceControlVisible" title="浠锋牸鎺у埗璁剧疆" :width="'700px'" @close="priceControlVisible = false" @confirm="handlePriceControl" @cancel="priceControlVisible = false">
+ <el-form :model="priceControlForm" label-width="120px">
+ <el-form-item label="榛樿鏈�浣庝环鏍�">
+ <el-input-number v-model="priceControlForm.defaultMinPrice" :min="0" :precision="2" style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="榛樿鏈�楂樹环鏍�">
+ <el-input-number v-model="priceControlForm.defaultMaxPrice" :min="0" :precision="2" style="width: 200px" />
+ </el-form-item>
+ <el-form-item label="浠锋牸鍙樺姩闃堝��">
+ <el-input-number v-model="priceControlForm.changeThreshold" :min="0" :max="100" :precision="1" style="width: 200px" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+
+ </div>
+</template>
+
+<script setup>
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import {ref, reactive, computed, onMounted, getCurrentInstance} from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+ Search, Refresh, Plus, Discount, Setting, Download, Delete, Edit,
+ Warning
+} from '@element-plus/icons-vue'
+import { listPage, update, del, add } from '@/api/procurementManagement/advancedPriceManagement'
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const submitLoading = ref(false)
+const dialogVisible = ref(false)
+const batchDiscountVisible = ref(false)
+const priceControlVisible = ref(false)
+const dialogType = ref('add')
+const selectedRows = ref([])
+const formRef = ref()
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ productName: '',
+ supplierId: ''
+})
+
+const total = ref(0)
+
+// 鍒嗛〉
+const pagination = reactive({
+ current: 1,
+ size: 10
+})
+
+
+// 琛ㄥ崟鏁版嵁
+const formData = reactive({
+ productName: '',
+ productCode: '',
+ specification: '',
+ supplierName: '',
+ basePrice: 0,
+ unit: '',
+ discountType: '',
+ discountValue: 0,
+ discountEndTime: '',
+ tieredDiscount: [],
+ minPrice: null,
+ maxPrice: null,
+ warningThreshold: 10,
+ effectiveTime: '',
+ expireTime: '',
+ reason: '',
+ remark: ''
+})
+
+const tableData = ref([])
+
+// 鎵归噺鎶樻墸琛ㄥ崟
+const batchDiscountForm = reactive({
+ discountType: 'percentage',
+ discountValue: 0,
+ effectiveTime: '',
+ expireTime: ''
+})
+
+// 浠锋牸鎺у埗琛ㄥ崟
+const priceControlForm = reactive({
+ defaultMinPrice: 0,
+ defaultMaxPrice: 0,
+ changeThreshold: 10,
+})
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const formRules = {
+ productName: [{ required: true, message: '璇烽�夋嫨鍟嗗搧鍚嶇О', trigger: 'change' }],
+ productCode: [{ required: true, message: '璇疯緭鍏ュ晢鍝佺紪鐮�', trigger: 'blur' }],
+ supplierName: [{ required: true, message: '璇烽�夋嫨渚涘簲鍟�', trigger: 'change' }],
+ basePrice: [{ required: true, message: '璇疯緭鍏ュ熀纭�浠锋牸', trigger: 'blur' }],
+ effectiveTime: [{ required: true, message: '璇烽�夋嫨鐢熸晥鏃堕棿', trigger: 'change' }],
+ reason: [{ required: true, message: '璇烽�夋嫨璋冧环鍘熷洜', trigger: 'change' }]
+}
+
+const supplierList = ref([
+ { id: 1, name: '浼樿川浜旈噾渚涘簲鍟�' },
+ { id: 2, name: '閽㈡潗璐告槗鍏徃' },
+ { id: 3, name: '寤烘潗鎵瑰彂鍟�' }
+])
+
+const productList = ref([
+ { id: 1, name: '楂樺己搴﹁灪鏍�' },
+ { id: 2, name: '涓嶉攬閽㈢' },
+ { id: 3, name: '閾濆悎閲戝瀷鏉�' }
+])
+
+
+// 鏂规硶
+const calculateFinalPrice = (row) => {
+ let finalPrice = row.basePrice
+ if (row.discountType === 'percentage') {
+ finalPrice = row.basePrice * (1 - row.discountValue / 100)
+ } else if (row.discountType === 'fixed') {
+ finalPrice = row.basePrice - row.discountValue
+ }
+ return Math.max(finalPrice, 0)
+}
+
+const getDiscountTagType = (discountType) => {
+ const typeMap = {
+ percentage: 'success',
+ fixed: 'warning',
+ tiered: 'info'
+ }
+ return typeMap[discountType] || 'info'
+}
+
+const getDiscountText = (discountType) => {
+ const textMap = {
+ percentage: '鐧惧垎姣�',
+ fixed: '鍥哄畾閲戦'
+ }
+ return textMap[discountType] || '鏈煡'
+}
+
+const getStatusType = (status) => {
+ const statusMap = {
+ active: 'success',
+ pending: 'warning',
+ expired: 'info'
+ }
+ return statusMap[status] || 'info'
+}
+
+const getStatusText = (status) => {
+ const statusMap = {
+ active: '鏈夋晥',
+ pending: '寰呯敓鏁�',
+ expired: '宸茶繃鏈�'
+ }
+ return statusMap[status] || '鏈煡'
+}
+
+const isPriceWarning = (row) => {
+ if (!row.priceControl) return false
+ const finalPrice = calculateFinalPrice(row)
+ return finalPrice < row.priceControl.minPrice || finalPrice > row.priceControl.maxPrice
+}
+
+
+const handleSearch = () => {
+ loading.value = true
+ // 妯℃嫙API璋冪敤
+ listPage({ ...searchForm, ...pagination}).then(res => {
+ tableData.value = res.data.records
+ total.value = res.data.total
+ loading.value = false
+ })
+}
+
+const resetSearch = () => {
+ Object.assign(searchForm, {
+ productName: '',
+ supplierId: ''
+ })
+ handleSearch()
+}
+
+
+const openDialog = (type, row = {}) => {
+ dialogType.value = type
+ if (type === 'edit' && row.id) {
+ Object.assign(formData, {
+ ...row,
+ minPrice: row.priceControl?.minPrice,
+ maxPrice: row.priceControl?.maxPrice,
+ tieredDiscount: row.tieredDiscount || []
+ })
+ } else {
+ resetFormData()
+ }
+ dialogVisible.value = true
+}
+
+const resetFormData = () => {
+ Object.assign(formData, {
+ productName: '',
+ productCode: '',
+ specification: '',
+ supplierName: '',
+ basePrice: 0,
+ unit: '',
+ discountType: '',
+ discountValue: 0,
+ discountEndTime: '',
+ tieredDiscount: [],
+ minPrice: null,
+ maxPrice: null,
+ warningThreshold: 10,
+ effectiveTime: '',
+ expireTime: '',
+ reason: '',
+ remark: ''
+ })
+}
+
+const addTieredRow = () => {
+ formData.tieredDiscount.push({
+ minQty: 0,
+ maxQty: 0,
+ discount: 0
+ })
+}
+
+const removeTieredRow = (index) => {
+ formData.tieredDiscount.splice(index, 1)
+}
+
+const handleSubmit = async () => {
+ if (!formRef.value) return
+
+ try {
+ await formRef.value.validate()
+ submitLoading.value = true
+
+ if (dialogType.value === 'add') {
+ add(formData).then(res => {
+ if (res.code === 200){
+ ElMessage.success('鏂板鎴愬姛')
+ handleSearch()
+ }
+ })
+ } else {
+ update(formData).then(res => {
+ if (res.code === 200){
+ ElMessage.success('缂栬緫鎴愬姛')
+ handleSearch()
+ }
+ })
+ }
+
+ } catch (error) {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', error)
+ }finally {
+ dialogVisible.value = false
+ submitLoading.value = false
+ }
+}
+
+const openBatchDiscountDialog = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning('璇峰厛閫夋嫨瑕佽缃姌鎵g殑鍟嗗搧')
+ return
+ }
+ batchDiscountVisible.value = true
+}
+
+const handleBatchDiscount = () => {
+ // 鎵归噺璁剧疆鎶樻墸閫昏緫
+ selectedRows.value.forEach(row => {
+ row.discountType = batchDiscountForm.discountType
+ row.discountValue = batchDiscountForm.discountValue
+ update(row).then(res => {
+ handleSearch()
+ })
+ })
+ ElMessage.success('鎶樻墸璁剧疆鎴愬姛')
+ batchDiscountVisible.value = false
+
+}
+
+const openPriceControlDialog = () => {
+ priceControlVisible.value = true
+}
+
+const handlePriceControl = () => {
+ ElMessage.success('浠锋牸鎺у埗璁剧疆宸蹭繚瀛�')
+ priceControlVisible.value = false
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖鏉¤褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = [row.id]
+ del(ids).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ handleSearch()
+ }
+ })
+ })
+}
+
+const handleBatchDelete = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning('璇峰厛閫夋嫨瑕佸垹闄ょ殑璁板綍')
+ return
+ }
+
+ ElMessageBox.confirm(`纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 鏉¤褰曞悧锛焋, '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ del(selectedRows.value.map(item => item.id)).then(i =>{
+ if(i.code === 200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ handleSearch()
+ }
+ })
+ })
+}
+
+const handleSelectionChange = (rows) => {
+ selectedRows.value = rows
+}
+
+const handleSizeChange = (size) => {
+ pagination.size = size
+ handleSearch()
+}
+
+const handleCurrentChange = (page) => {
+ pagination.current = page
+ handleSearch()
+}
+const { proxy } = getCurrentInstance();
+
+const exportData = () => {
+ ElMessageBox.confirm("鍐呭灏嗚瀵煎嚭锛屾槸鍚︾‘璁ゅ鍑猴紵", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/procurementPriceManagement/export", {}, "閲囪喘浠锋牸绠$悊.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+}
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ handleSearch()
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+}
+
+.search-card, .action-card, .table-card {
+ margin-bottom: 20px;
+}
+
+.action-buttons {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+
+.product-info {
+ line-height: 1.4;
+}
+
+.product-name {
+ font-weight: bold;
+ color: #303133;
+}
+
+.product-spec, .product-code {
+ font-size: 12px;
+ color: #909399;
+}
+
+.price-text {
+ font-weight: bold;
+ color: #409EFF;
+}
+
+.final-price {
+ font-weight: bold;
+ color: #67C23A;
+ font-size: 16px;
+}
+
+.discount-value {
+ font-size: 12px;
+ color: #E6A23C;
+ margin-top: 2px;
+}
+
+.no-discount {
+ color: #C0C4CC;
+ font-size: 12px;
+}
+
+.price-control {
+ font-size: 12px;
+ line-height: 1.3;
+}
+
+.control-item {
+ color: #909399;
+}
+
+.warning-indicator {
+ margin-top: 2px;
+}
+
+.pagination-wrapper {
+ display: flex;
+ justify-content: end;
+ margin-top: 20px;
+}
+
+.selected-items {
+ color: #409EFF;
+ font-weight: bold;
+}
+
+
+.mt-2 {
+ margin-top: 8px;
+}
+
+.ml-2 {
+ margin-left: 8px;
+}
+
+:deep(.el-table) {
+ font-size: 13px;
+}
+
+:deep(.el-table th) {
+ background-color: #fafafa;
+}
+
+:deep(.el-card__body) {
+ padding: 15px;
+}
+
+:deep(.el-divider__text) {
+ font-weight: bold;
+ color: #409EFF;
+}
+</style>
diff --git a/src/views/procurementManagement/arrivalManagement/index.vue b/src/views/procurementManagement/arrivalManagement/index.vue
new file mode 100644
index 0000000..a1b5eed
--- /dev/null
+++ b/src/views/procurementManagement/arrivalManagement/index.vue
@@ -0,0 +1,237 @@
+<template>
+ <div class="app-container">
+ <el-card class="search-card" shadow="never">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="閲囪喘璁㈠崟鍙凤細">
+ <el-input v-model="searchForm.orderNo" placeholder="璇疯緭鍏ヨ鍗曞彿" clearable />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟嗗悕绉帮細">
+ <el-input v-model="searchForm.supplierName" placeholder="璇疯緭鍏ヤ緵搴斿晢鍚嶇О" clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <el-card class="table-card" shadow="never">
+ <div class="table-header">
+ <el-button type="primary" @click="openDialog('add')">鏂板鍒拌揣</el-button>
+ <el-button type="danger" @click="handleBatchDelete">鎵归噺鍒犻櫎</el-button>
+ </div>
+
+ <el-table :data="tableData" border v-loading="loading" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鍒拌揣鍗曞彿" prop="arrivalNo" width="180" />
+ <el-table-column label="閲囪喘璁㈠崟鍙�" prop="orderNo" width="180" />
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" />
+ <el-table-column label="鍒拌揣鐘舵��" prop="status" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒拌揣鏁伴噺" prop="arrivalQuantity" width="100" />
+ <el-table-column label="鍒拌揣鏃堕棿" prop="arrivalTime" width="180" />
+ <el-table-column label="鎿嶄綔" width="200" align="center">
+ <template #default="{ row }">
+ <el-button type="primary" link @click="openDialog('edit', row)">缂栬緫</el-button>
+ <el-button type="success" v-if="row.status === 'pending'" link @click="handleReceive(row)">鏀惰揣</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.current"
+ :limit="pagination.size"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <FormDialog v-model="dialogVisible" :title="dialogType === 'add' ? '鏂板鍒拌揣' : '缂栬緫鍒拌揣'" :width="'600px'" :operation-type="dialogType" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="formData" label-width="120px">
+ <el-form-item label="鍒拌揣鍗曞彿">
+ <el-input v-model="formData.arrivalNo" placeholder="鍒拌揣鍗曞彿" />
+ </el-form-item>
+ <el-form-item label="閲囪喘璁㈠崟鍙�">
+ <el-input v-model="formData.orderNo" placeholder="閲囪喘璁㈠崟鍙�" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟嗗悕绉�">
+ <el-input v-model="formData.supplierName" placeholder="渚涘簲鍟嗗悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍒拌揣鏁伴噺">
+ <el-input-number :min="0" v-model="formData.arrivalQuantity" placeholder="鍒拌揣鏁伴噺" />
+ </el-form-item>
+ <el-form-item label="澶囨敞">
+ <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import { ref, reactive,onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {listPage,add,update,del} from "@/api/procurementManagement/arrivalManagement.js"
+import Pagination from '@/components/PIMTable/Pagination.vue'
+
+onMounted(() => {
+ getList()
+})
+
+const tableData = ref([])
+
+const getList = () => {
+ loading.value = true
+ listPage({...searchForm,...pagination}).then(res =>{
+ if(res.code === 200){
+ tableData.value = res.data.records
+ total.value = res.data.total
+ loading.value = false
+ }
+ })
+}
+
+const pagination = reactive({
+ current: 1,
+ size: 10
+})
+
+const total = ref(0)
+
+const handleCurrentChange = (val) => {
+ pagination.current = val.page
+ pagination.size = val.limit
+ getList()
+}
+
+const loading = ref(false)
+const dialogVisible = ref(false)
+const dialogType = ref('add')
+const selectedRows = ref([])
+
+const searchForm = reactive({
+ orderNo: '',
+ supplierName: ''
+})
+
+const formData = reactive({
+ arrivalNo: '',
+ arrivalQuantity: 0,
+ orderNo: '',
+ supplierName: '',
+ remark: '',
+ status: 'pending'
+})
+
+const getStatusType = (status) => {
+ const statusMap = { pending: 'warning', received: 'success', stored: 'info' }
+ return statusMap[status] || 'info'
+}
+
+const getStatusText = (status) => {
+ const statusMap = { pending: '寰呮敹璐�', received: '宸叉敹璐�', stored: '宸插叆搴�' }
+ return statusMap[status] || '鏈煡'
+}
+
+const handleSearch = () => {
+ loading.value = true
+ getList()
+}
+
+const resetSearch = () => {
+ Object.assign(searchForm, { orderNo: '', supplierName: '' })
+}
+
+const openDialog = (type, row = {}) => {
+ dialogType.value = type
+ if (type === 'edit' && row.id) {
+ obj.id = row.id
+ Object.assign(formData, { orderNo: row.orderNo, supplierName: row.supplierName, remark: row.remark, arrivalQuantity: row.arrivalQuantity,arrivalNo: row.arrivalNo })
+ } else {
+ Object.assign(formData, { orderNo: '', supplierName: '', remark: '',arrivalQuantity: 0,arrivalNo: '' })
+ }
+ dialogVisible.value = true
+}
+
+const obj = reactive({
+ id:''
+})
+
+const handleSubmit = () => {
+ if (dialogType.value === 'add') {
+ add(formData).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鏂板鎴愬姛')
+ getList()
+ }
+ })
+ }else{
+ update({...formData, ...obj}).then(res => {
+ if(res.code === 200){
+ ElMessage.success('缂栬緫鎴愬姛')
+ getList()
+ }
+ })
+ }
+ dialogVisible.value = false
+}
+
+const handleReceive = (row) => {
+ row.status = 'received'
+ update(row).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鏀惰揣鎴愬姛')
+ getList()
+ }
+ })
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖鏉¤褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = [row.id]
+ del(ids).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getList()
+ }
+ })
+ })
+}
+
+const handleBatchDelete = () => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄ら�変腑鐨勮褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = selectedRows.value.map(item => item.id)
+ del(ids).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getList()
+ }
+ })
+ })
+}
+
+const handleSelectionChange = (rows) => {
+ selectedRows.value = rows
+}
+</script>
+
+<style scoped>
+.app-container { padding: 20px; }
+.search-card { margin-bottom: 20px; }
+.table-card { margin-bottom: 20px; }
+.table-header { margin-bottom: 20px; }
+</style>
diff --git a/src/views/procurementManagement/index.vue b/src/views/procurementManagement/index.vue
new file mode 100644
index 0000000..bf54384
--- /dev/null
+++ b/src/views/procurementManagement/index.vue
@@ -0,0 +1,418 @@
+<template>
+ <div class="app-container">
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <h2>閲囪喘绠$悊绯荤粺</h2>
+ <p>缁熶竴绠$悊閲囪喘鍏ㄦ祦绋嬶紝鎻愬崌閲囪喘鏁堢巼涓庤川閲�</p>
+ </div>
+
+ <!-- 鍔熻兘妯″潡鍗$墖 -->
+ <el-row :gutter="20" class="module-cards">
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/purchaseOrder')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#409EFF"><Document /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>閲囪喘璁㈠崟绠$悊</h3>
+ <p>鏂板缓銆佺紪杈戙�佸垹闄ら噰璐鍗曪紝閫夋嫨渚涘簲鍟嗭紝濉啓鍟嗗搧鏄庣粏</p>
+ <div class="card-stats">
+ <span>寰呭鏍�: {{ stats.pendingOrders }}</span>
+ <span>宸插鏍�: {{ stats.approvedOrders }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/arrivalManagement')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#67C23A"><Box /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>鍒拌揣绠$悊</h3>
+ <p>鑷姩鍏宠仈閲囪喘璁㈠崟锛屽綍鍏ュ埌璐у晢鍝佷俊鎭紝鏀寔鎵撳嵃鏌ョ湅</p>
+ <div class="card-stats">
+ <span>寰呮敹璐�: {{ stats.pendingArrivals }}</span>
+ <span>宸叉敹璐�: {{ stats.receivedArrivals }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/qualityInspection')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#E6A23C"><Search /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>璐ㄦ绠$悊</h3>
+ <p>鍒拌揣鍚庤嚜鍔ㄧ敓鎴愯川妫�鍗曪紝濉啓鍚堟牸涓庝笉鍚堟牸鍟嗗搧鏁伴噺鍙婂師鍥�</p>
+ <div class="card-stats">
+ <span>寰呰川妫�: {{ stats.pendingInspections }}</span>
+ <span>宸插畬鎴�: {{ stats.completedInspections }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20" class="module-cards">
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/returnManagement')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#F56C6C"><RefreshLeft /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>閫�璐х鐞�</h3>
+ <p>鐢熸垚閲囪喘閫�璐у崟鍜岃川妫�閫�璐у崟锛屾敮鎸佺瓫閫夋煡璇笌鍗曟嵁璇︽儏</p>
+ <div class="card-stats">
+ <span>寰呭鏍�: {{ stats.pendingReturns }}</span>
+ <span>宸插鏍�: {{ stats.approvedReturns }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/priceManagement')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#909399"><Money /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>浠锋牸绠$悊</h3>
+ <p>鏍规嵁鍟嗗搧鍙婂競鍦轰环鏍煎彉鍖栬繘琛岄噰璐环璋冩暣锛岃嚜鍔ㄦ洿鏂伴噰璐崟鎹�</p>
+ <div class="card-stats">
+ <span>鏈夋晥浠锋牸: {{ stats.activePrices }}</span>
+ <span>寰呯敓鏁�: {{ stats.pendingPrices }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/procurementPlan')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#9C27B0"><Calendar /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>閲囪喘璁″垝</h3>
+ <p>鏅鸿兘閲囪喘璁″垝閰嶇疆锛岃嚜鍔ㄨ绠楅噰璐渶姹傦紝鑰冭檻搴撳瓨鍜屽畨鍏ㄥ簱瀛�</p>
+ <div class="card-stats">
+ <span>娲昏穬璁″垝: {{ stats.activePlans }}</span>
+ <span>寰呰绠�: {{ stats.pendingCalculations }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20" class="module-cards">
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/procurementLedger')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#9C27B0"><List /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>閲囪喘鍙拌处</h3>
+ <p>鏌ョ湅閲囪喘鍘嗗彶璁板綍锛岀粺璁″垎鏋愰噰璐暟鎹紝鐢熸垚閲囪喘鎶ヨ〃</p>
+ <div class="card-stats">
+ <span>鎬昏鍗�: {{ stats.totalOrders }}</span>
+ <span>鎬婚噾棰�: 楼{{ stats.totalAmount.toFixed(2) }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <el-col :span="8">
+ <el-card class="module-card" shadow="hover" @click="navigateTo('/procurementManagement/procurementReport')">
+ <div class="card-content">
+ <div class="card-icon">
+ <el-icon size="48" color="#FF6B6B"><TrendCharts /></el-icon>
+ </div>
+ <div class="card-info">
+ <h3>閲囪喘鎶ヨ〃</h3>
+ <p>閲囪喘璁㈠崟鎵ц姹囨�汇�佹槑缁嗗垎鏋愩�佷笟鍔$粺璁°�佷緵搴斿晢渚涜揣姹囨��</p>
+ <div class="card-stats">
+ <span>鎶ヨ〃绫诲瀷: 4绉�</span>
+ <span>鏁版嵁鏇存柊: 瀹炴椂</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 缁熻姒傝 -->
+ <el-card class="stats-card" shadow="never">
+ <template #header>
+ <div class="card-header">
+ <span>閲囪喘缁熻姒傝</span>
+ <el-button type="primary" size="small" @click="refreshStats">鍒锋柊鏁版嵁</el-button>
+ </div>
+ </template>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-number">{{ stats.totalOrders }}</div>
+ <div class="stat-label">閲囪喘璁㈠崟鎬绘暟</div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-number">{{ stats.totalAmount.toFixed(2) }}</div>
+ <div class="stat-label">閲囪喘鎬婚噾棰�(涓囧厓)</div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-number">{{ stats.avgDeliveryTime }}</div>
+ <div class="stat-label">骞冲潎浜や粯澶╂暟</div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-item">
+ <div class="stat-number">{{ stats.qualityRate }}%</div>
+ <div class="stat-label">璐ㄦ鍚堟牸鐜�</div>
+ </div>
+ </el-col>
+ </el-row>
+ </el-card>
+
+ <!-- 鏈�杩戞椿鍔� -->
+ <el-card class="activity-card" shadow="never">
+ <template #header>
+ <span>鏈�杩戞椿鍔�</span>
+ </template>
+
+ <el-timeline>
+ <el-timeline-item
+ v-for="(activity, index) in recentActivities"
+ :key="index"
+ :timestamp="activity.time"
+ :type="activity.type"
+ >
+ {{ activity.content }}
+ </el-timeline-item>
+ </el-timeline>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Document, Box, Search, RefreshLeft, Money, List, Calendar, TrendCharts } from '@element-plus/icons-vue'
+
+const router = useRouter()
+
+// 缁熻鏁版嵁
+const stats = ref({
+ pendingOrders: 5,
+ approvedOrders: 25,
+ pendingArrivals: 3,
+ receivedArrivals: 18,
+ pendingInspections: 2,
+ completedInspections: 15,
+ pendingReturns: 1,
+ approvedReturns: 3,
+ activePrices: 45,
+ pendingPrices: 2,
+ activePlans: 8,
+ pendingCalculations: 3,
+ totalOrders: 30,
+ totalAmount: 125.8,
+ avgDeliveryTime: 7,
+ qualityRate: 96.5
+})
+
+// 鏈�杩戞椿鍔�
+const recentActivities = ref([
+ {
+ time: '2025-12-01 18:30',
+ content: '鏂板閲囪喘璁㈠崟 PO20241201004',
+ type: 'primary'
+ },
+ {
+ time: '2025-12-01 17:45',
+ content: '瀹屾垚璐ㄦ鍗� QI20241201002',
+ type: 'success'
+ },
+ {
+ time: '2025-12-01 16:20',
+ content: '鍒拌揣鍗� AR20241201003 宸叉敹璐�',
+ type: 'success'
+ },
+ {
+ time: '2025-12-01 15:15',
+ content: '浠锋牸璋冩暣锛氬晢鍝丅 浠� 楼80 璋冩暣涓� 楼75',
+ type: 'warning'
+ },
+ {
+ time: '2025-12-01 14:30',
+ content: '閫�璐у崟 RT20241201003 宸插鏍�',
+ type: 'info'
+ }
+])
+
+// 瀵艰埅鍒版寚瀹氶〉闈�
+const navigateTo = (path) => {
+ router.push(path)
+}
+
+// 鍒锋柊缁熻鏁版嵁
+const refreshStats = () => {
+ // 妯℃嫙鍒锋柊鏁版嵁
+ stats.value.pendingOrders = Math.floor(Math.random() * 10) + 1
+ stats.value.totalAmount = (Math.random() * 100 + 100).toFixed(1)
+}
+
+onMounted(() => {
+ // 椤甸潰鍔犺浇瀹屾垚鍚庣殑鍒濆鍖栭�昏緫
+})
+</script>
+
+<style scoped>
+.app-container {
+ padding: 20px;
+ background-color: #f5f7fa;
+ min-height: 100vh;
+}
+
+.page-header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 10px;
+ color: white;
+}
+
+.page-header h2 {
+ margin: 0 0 10px 0;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.page-header p {
+ margin: 0;
+ font-size: 16px;
+ opacity: 0.9;
+}
+
+.module-cards {
+ margin-bottom: 20px;
+}
+
+.module-card {
+ cursor: pointer;
+ transition: all 0.3s ease;
+ border: none;
+ border-radius: 12px;
+}
+
+.module-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+}
+
+.card-content {
+ display: flex;
+ align-items: center;
+ padding: 20px;
+}
+
+.card-icon {
+ margin-right: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 80px;
+ height: 80px;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ border-radius: 50%;
+ color: white;
+}
+
+.card-info h3 {
+ margin: 0 0 10px 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.card-info p {
+ margin: 0 0 15px 0;
+ font-size: 14px;
+ color: #606266;
+ line-height: 1.5;
+}
+
+.card-stats {
+ display: flex;
+ gap: 15px;
+}
+
+.card-stats span {
+ font-size: 12px;
+ color: #909399;
+ background-color: #f5f7fa;
+ padding: 4px 8px;
+ border-radius: 4px;
+}
+
+.stats-card {
+ margin-bottom: 20px;
+ border-radius: 12px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.stat-item {
+ text-align: center;
+ padding: 20px;
+}
+
+.stat-number {
+ font-size: 32px;
+ font-weight: 600;
+ color: #409EFF;
+ margin-bottom: 8px;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #606266;
+}
+
+.activity-card {
+ border-radius: 12px;
+}
+
+.el-timeline-item {
+ padding-bottom: 20px;
+}
+
+.el-timeline-item:last-child {
+ padding-bottom: 0;
+}
+</style>
diff --git a/src/views/procurementManagement/priceManagement/index.vue b/src/views/procurementManagement/priceManagement/index.vue
new file mode 100644
index 0000000..76a39ed
--- /dev/null
+++ b/src/views/procurementManagement/priceManagement/index.vue
@@ -0,0 +1,273 @@
+<template>
+ <div class="app-container">
+ <el-card class="search-card" shadow="never">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="鍟嗗搧鍚嶇О锛�">
+ <el-input v-model="searchForm.productName" placeholder="璇疯緭鍏ュ晢鍝佸悕绉�" clearable />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟嗗悕绉帮細">
+ <el-input v-model="searchForm.supplierName" placeholder="璇疯緭鍏ヤ緵搴斿晢鍚嶇О" clearable />
+ </el-form-item>
+ <el-form-item label="浠锋牸鐘舵�侊細">
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" clearable>
+ <el-option label="鏈夋晥" value="active" />
+ <el-option label="宸茶繃鏈�" value="expired" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <el-card class="table-card" shadow="never">
+ <div class="table-header">
+ <el-button type="primary" @click="openDialog('add')">鏂板浠锋牸</el-button>
+ <el-button type="success" @click="handleBatchUpdate">鎵归噺鏇存柊</el-button>
+ <el-button type="danger" @click="handleBatchDelete">鎵归噺鍒犻櫎</el-button>
+ </div>
+
+ <el-table :data="tableData" border v-loading="loading" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鍟嗗搧鍚嶇О" prop="productName" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specification" />
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" />
+ <el-table-column label="鍘熶环鏍�" prop="oldPrice" width="120">
+ <template #default="{ row }">楼{{ row.oldPrice.toFixed(2) }}</template>
+ </el-table-column>
+ <el-table-column label="鏂颁环鏍�" prop="newPrice" width="120">
+ <template #default="{ row }">楼{{ row.newPrice.toFixed(2) }}</template>
+ </el-table-column>
+ <el-table-column label="璋冧环骞呭害" prop="priceChange" width="120">
+ <template #default="{ row }">
+ <span :style="{ color: row.priceChange >= 0 ? '#f56c6c' : '#67c23a' }">
+ {{ row.priceChange >= 0 ? '+' : '' }}{{ row.priceChange.toFixed(2) }}%
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢熸晥鏃堕棿" prop="effectiveTime" width="180" />
+ <el-table-column label="鐘舵��" prop="status" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" align="center">
+ <template #default="{ row }">
+ <el-button type="primary" link @click="openDialog('edit', row)">缂栬緫</el-button>
+ <el-button type="success" link @click="handleApply(row)" v-if="row.status === 'pending'">搴旂敤</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <FormDialog v-model="dialogVisible" :title="dialogType === 'add' ? '鏂板浠锋牸' : '缂栬緫浠锋牸'" :width="'600px'" :operation-type="dialogType" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="formData" label-width="120px">
+ <el-form-item label="鍟嗗搧鍚嶇О">
+ <el-select v-model="formData.productName" placeholder="璇烽�夋嫨鍟嗗搧" style="width: 100%">
+ <el-option label="鍟嗗搧A" value="鍟嗗搧A" />
+ <el-option label="鍟嗗搧B" value="鍟嗗搧B" />
+ <el-option label="鍟嗗搧C" value="鍟嗗搧C" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿">
+ <el-input v-model="formData.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟嗗悕绉�">
+ <el-select v-model="formData.supplierName" placeholder="璇烽�夋嫨渚涘簲鍟�" style="width: 100%">
+ <el-option label="渚涘簲鍟咥" value="渚涘簲鍟咥" />
+ <el-option label="渚涘簲鍟咮" value="渚涘簲鍟咮" />
+ <el-option label="渚涘簲鍟咰" value="渚涘簲鍟咰" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍘熶环鏍�">
+ <el-input v-model="formData.oldPrice" placeholder="璇疯緭鍏ュ師浠锋牸" type="number" />
+ </el-form-item>
+ <el-form-item label="鏂颁环鏍�">
+ <el-input v-model="formData.newPrice" placeholder="璇疯緭鍏ユ柊浠锋牸" type="number" />
+ </el-form-item>
+ <el-form-item label="鐢熸晥鏃堕棿">
+ <el-date-picker v-model="formData.effectiveTime" type="datetime" placeholder="閫夋嫨鐢熸晥鏃堕棿" style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="璋冧环鍘熷洜">
+ <el-select v-model="formData.reason" placeholder="璇烽�夋嫨璋冧环鍘熷洜" style="width: 100%">
+ <el-option label="甯傚満浠锋牸鍙樺姩" value="market" />
+ <el-option label="鎴愭湰鍙樺寲" value="cost" />
+ <el-option label="渚涘簲鍟嗚皟鏁�" value="supplier" />
+ <el-option label="鍏朵粬鍘熷洜" value="other" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞">
+ <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+const loading = ref(false)
+const dialogVisible = ref(false)
+const dialogType = ref('add')
+const selectedRows = ref([])
+
+const searchForm = reactive({
+ productName: '',
+ supplierName: '',
+ status: ''
+})
+
+const formData = reactive({
+ productName: '',
+ specification: '',
+ supplierName: '',
+ oldPrice: 0,
+ newPrice: 0,
+ effectiveTime: '',
+ reason: '',
+ remark: ''
+})
+
+const mockData = [
+ {
+ id: 1,
+ productName: '鍟嗗搧A',
+ specification: '瑙勬牸1',
+ supplierName: '渚涘簲鍟咥',
+ oldPrice: 50.00,
+ newPrice: 55.00,
+ priceChange: 10.00,
+ effectiveTime: '2025-12-01 00:00:00',
+ status: 'active',
+ reason: '甯傚満浠锋牸鍙樺姩',
+ remark: '甯傚満浠锋牸涓婃定'
+ },
+ {
+ id: 2,
+ productName: '鍟嗗搧B',
+ specification: '瑙勬牸2',
+ supplierName: '渚涘簲鍟咮',
+ oldPrice: 80.00,
+ newPrice: 75.00,
+ priceChange: -6.25,
+ effectiveTime: '2025-12-01 00:00:00',
+ status: 'active',
+ reason: '鎴愭湰鍙樺寲',
+ remark: '鎴愭湰涓嬮檷'
+ }
+]
+
+const tableData = ref([...mockData])
+
+const getStatusType = (status) => {
+ const statusMap = { active: 'success', expired: 'info', pending: 'warning' }
+ return statusMap[status] || 'info'
+}
+
+const getStatusText = (status) => {
+ const statusMap = { active: '鏈夋晥', expired: '宸茶繃鏈�', pending: '寰呯敓鏁�' }
+ return statusMap[status] || '鏈煡'
+}
+
+const handleSearch = () => {
+ loading.value = true
+ setTimeout(() => { loading.value = false }, 500)
+}
+
+const resetSearch = () => {
+ Object.assign(searchForm, { productName: '', supplierName: '', status: '' })
+}
+
+const openDialog = (type, row = {}) => {
+ dialogType.value = type
+ if (type === 'edit' && row.id) {
+ Object.assign(formData, {
+ productName: row.productName,
+ specification: row.specification,
+ supplierName: row.supplierName,
+ oldPrice: row.oldPrice,
+ newPrice: row.newPrice,
+ effectiveTime: row.effectiveTime,
+ reason: row.reason,
+ remark: row.remark
+ })
+ } else {
+ Object.assign(formData, {
+ productName: '',
+ specification: '',
+ supplierName: '',
+ oldPrice: 0,
+ newPrice: 0,
+ effectiveTime: '',
+ reason: '',
+ remark: ''
+ })
+ }
+ dialogVisible.value = true
+}
+
+const handleSubmit = () => {
+ if (dialogType.value === 'add') {
+ const priceChange = ((formData.newPrice - formData.oldPrice) / formData.oldPrice) * 100
+ const newPrice = {
+ id: Date.now(),
+ productName: formData.productName,
+ specification: formData.specification,
+ supplierName: formData.supplierName,
+ oldPrice: formData.oldPrice,
+ newPrice: formData.newPrice,
+ priceChange: priceChange,
+ effectiveTime: formData.effectiveTime,
+ status: 'pending',
+ reason: formData.reason,
+ remark: formData.remark
+ }
+ tableData.value.unshift(newPrice)
+ ElMessage.success('鏂板鎴愬姛')
+ }
+ dialogVisible.value = false
+}
+
+const handleApply = (row) => {
+ row.status = 'active'
+ ElMessage.success('浠锋牸宸插簲鐢�')
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖鏉¤褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ const index = tableData.value.findIndex(item => item.id === row.id)
+ if (index !== -1) {
+ tableData.value.splice(index, 1)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ }
+ })
+}
+
+const handleBatchUpdate = () => {
+ ElMessage.success('鎵归噺鏇存柊鎴愬姛')
+}
+
+const handleBatchDelete = () => {
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+}
+
+const handleSelectionChange = (rows) => {
+ selectedRows.value = rows
+}
+</script>
+
+<style scoped>
+.app-container { padding: 20px; }
+.search-card { margin-bottom: 20px; }
+.table-card { margin-bottom: 20px; }
+.table-header { margin-bottom: 20px; }
+</style>
diff --git a/src/views/procurementManagement/procurementLedger/fileList.vue b/src/views/procurementManagement/procurementLedger/fileList.vue
new file mode 100644
index 0000000..945d6a1
--- /dev/null
+++ b/src/views/procurementManagement/procurementLedger/fileList.vue
@@ -0,0 +1,66 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="闄勪欢" width="40%" :before-close="handleClose">
+ <el-table :data="tableData" border height="40vh">
+ <el-table-column label="闄勪欢鍚嶇О" prop="name" min-width="400" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">涓嬭浇</el-button>
+ <el-button link type="primary" size="small" @click="lookFile(scope.row)">棰勮</el-button>
+ <el-button link type="danger" size="small" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import filePreview from '@/components/filePreview/index.vue'
+import { delCommonFile } from '@/api/publicApi/commonFile.js'
+
+const dialogVisible = ref(false)
+const tableData = ref([])
+const { proxy } = getCurrentInstance();
+const filePreviewRef = ref()
+const handleClose = () => {
+ dialogVisible.value = false
+}
+const open = (list) => {
+ dialogVisible.value = true
+ tableData.value = list
+}
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+// 鍒犻櫎闄勪欢
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎闄勪欢"${row.name}"鍚楋紵`, '鍒犻櫎纭', {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }).then(() => {
+ delCommonFile([row.id]).then(() => {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ // 浠庡垪琛ㄤ腑绉婚櫎宸插垹闄ょ殑闄勪欢
+ const index = tableData.value.findIndex(item => item.id === row.id)
+ if (index !== -1) {
+ tableData.value.splice(index, 1)
+ }
+ }).catch(() => {
+ ElMessage.error('鍒犻櫎澶辫触')
+ })
+ }).catch(() => {
+ proxy.$modal.msg('宸插彇娑堝垹闄�')
+ })
+}
+defineExpose({
+ open
+})
+</script>
+
+<style></style>
\ No newline at end of file
diff --git a/src/views/procurementManagement/procurementPlan/index.vue b/src/views/procurementManagement/procurementPlan/index.vue
new file mode 100644
index 0000000..ba9a38c
--- /dev/null
+++ b/src/views/procurementManagement/procurementPlan/index.vue
@@ -0,0 +1,856 @@
+<template>
+ <div class="app-container">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-card class="search-card"
+ shadow="never">
+ <el-form :model="searchForm"
+ :inline="true"
+ class="search-form">
+ <el-form-item label="璁″垝鍚嶇О">
+ <el-input v-model="searchForm.planName"
+ placeholder="璇疯緭鍏ヨ鍒掑悕绉�"
+ clearable />
+ </el-form-item>
+ <el-form-item label="鐘舵��">
+ <el-select v-model="searchForm.status"
+ placeholder="璇烽�夋嫨鐘舵��"
+ clearable
+ style="width: 150px">
+ <el-option label="鍚敤"
+ value="active" />
+ <el-option label="绂佺敤"
+ value="disabled" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleSearch">
+ <el-icon>
+ <Search />
+ </el-icon>
+ 鎼滅储
+ </el-button>
+ <el-button @click="handleReset">
+ <el-icon>
+ <Refresh />
+ </el-icon>
+ 閲嶇疆
+ </el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <el-card class="table-card"
+ shadow="never">
+ <div class="table-header">
+ <div class="table-title">閲囪喘璁″垝鍒楄〃</div>
+ <div class="table-actions">
+ <el-button type="primary"
+ @click="handleAdd">
+ <el-icon>
+ <Plus />
+ </el-icon>
+ 鏂板璁″垝
+ </el-button>
+ <el-button type="info"
+ @click="handleExport">
+ <el-icon>
+ <Download />
+ </el-icon>
+ 瀵煎嚭
+ </el-button>
+ </div>
+ </div>
+ <!-- 鏁版嵁琛ㄦ牸 -->
+ <el-table v-loading="loading"
+ :data="tableData"
+ stripe
+ border
+ style="width: 100%">
+ <el-table-column prop="planName"
+ label="璁″垝鍚嶇О"
+ min-width="150" />
+ <el-table-column prop="description"
+ label="鎻忚堪"
+ min-width="200"
+ show-overflow-tooltip />
+ <el-table-column prop="formula"
+ label="璁$畻鍏紡"
+ min-width="200"
+ show-overflow-tooltip>
+ <template #default="{ row }">
+ <el-tag type="info"
+ size="small">{{ row.formula }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="status"
+ label="鐘舵��"
+ width="80"
+ align="center">
+ <template #default="{ row }">
+ <el-tag :type="row.status === 'active' ? 'success' : 'info'"
+ size="small">
+ {{ row.status === 'active' ? '鍚敤' : '绂佺敤' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="updateTime"
+ label="鏈�鍚庤绠楁椂闂�"
+ width="160" />
+ <el-table-column label="鎿嶄綔"
+ width="200"
+ fixed="right"
+ align="center">
+ <template #default="{ row }">
+ <el-button type="primary"
+ link
+ @click="handleEdit(row)">缂栬緫</el-button>
+ <el-button type="success"
+ link
+ @click="handleCalculate(row)">璁$畻</el-button>
+ <el-button type="danger"
+ link
+ @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <div class="pagination-container">
+ <el-pagination v-model:current-page="pagination.current"
+ v-model:page-size="pagination.size"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange" />
+ </div>
+ </el-card>
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <FormDialog v-model="dialogVisible"
+ :title="dialogType === 'add' ? '鏂板閲囪喘璁″垝' : '缂栬緫閲囪喘璁″垝'"
+ :width="'1000px'"
+ :operation-type="dialogType"
+ :close-on-click-modal="false"
+ @close="dialogVisible = false"
+ @confirm="handleSubmit"
+ @cancel="dialogVisible = false">
+ <div class="form-container">
+ <!-- 鍩烘湰淇℃伅 -->
+ <div class="form-section">
+ <div class="section-title">鍩烘湰淇℃伅</div>
+ <el-form ref="formRef"
+ :model="formData"
+ :rules="formRules"
+ label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="缂栫爜"
+ prop="code">
+ <el-input v-model="formData.code"
+ placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�"
+ disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍚嶇О"
+ prop="planName"
+ required>
+ <el-input v-model="formData.planName"
+ placeholder="璇疯緭鍏ヨ鍒掑悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鎻忚堪"
+ prop="description">
+ <el-input v-model="formData.description"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヨ鍒掓弿杩�" />
+ </el-form-item>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐘舵��"
+ prop="status">
+ <el-select v-model="formData.status"
+ placeholder="璇烽�夋嫨鐘舵��"
+ style="width: 100%">
+ <el-option label="鍚敤"
+ value="active" />
+ <el-option label="绂佺敤"
+ value="disabled" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿" prop="createTime">
+ <el-date-picker v-model="formCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </div>
+ <!-- 璁$畻鍙傛暟 -->
+ <div class="form-section">
+ <div class="section-title">璁$畻鍙傛暟</div>
+ <el-tabs v-model="activeTab"
+ class="param-tabs">
+ <el-tab-pane label="闇�姹傚弬鏁�"
+ name="demand">
+ <div class="checkbox-group">
+ <el-checkbox v-model="formData.considerExistingStock">鑰冭檻鐜版湁搴撳瓨</el-checkbox>
+ <el-checkbox v-model="formData.warehouseControl">浠撳簱杩愯MRP鐨勬帶鍒�</el-checkbox>
+ <el-checkbox v-model="formData.calculateTotalDemand">璁$畻鎬婚渶姹�</el-checkbox>
+ <el-checkbox v-model="formData.considerSafetyStock">鑰冭檻瀹夊叏搴撳瓨</el-checkbox>
+ <el-checkbox v-model="formData.considerLockedStock">鑰冭檻閿佸簱</el-checkbox>
+ <el-checkbox v-model="formData.notConsiderMaterialAux">涓嶈�冭檻鐗╂枡杈呭姪灞炴��</el-checkbox>
+ <el-checkbox v-model="formData.negativeStockAsDemand">璐熷簱瀛樹綔涓洪渶姹�</el-checkbox>
+ </div>
+ </el-tab-pane>
+ <el-tab-pane label="璁$畻鍙傛暟"
+ name="calculation">
+ <div class="checkbox-group">
+ <el-checkbox v-model="formData.considerExistingStock">鑰冭檻鐜版湁搴撳瓨</el-checkbox>
+ <el-checkbox v-model="formData.warehouseControl">浠撳簱杩愯MRP鐨勬帶鍒�</el-checkbox>
+ <el-checkbox v-model="formData.calculateTotalDemand">璁$畻鎬婚渶姹�</el-checkbox>
+ <el-checkbox v-model="formData.considerSafetyStock">鑰冭檻瀹夊叏搴撳瓨</el-checkbox>
+ <el-checkbox v-model="formData.considerLockedStock">鑰冭檻閿佸簱</el-checkbox>
+ <el-checkbox v-model="formData.notConsiderMaterialAux">涓嶈�冭檻鐗╂枡杈呭姪灞炴��</el-checkbox>
+ <el-checkbox v-model="formData.negativeStockAsDemand">璐熷簱瀛樹綔涓洪渶姹�</el-checkbox>
+ </div>
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+ <!-- 姹囨�诲悎骞堕�夐」 -->
+ <div class="form-section">
+ <div class="section-title">姹囨�诲悎骞堕�夐」</div>
+ <div class="checkbox-group">
+ <el-checkbox v-model="formData.summaryMaterial">鐗╂枡</el-checkbox>
+ <el-checkbox v-model="formData.summaryAuxAttributes">杈呭姪灞炴��</el-checkbox>
+ <el-checkbox v-model="formData.summaryDemandDate">闇�姹傛棩鏈�</el-checkbox>
+ </div>
+ </div>
+ <!-- 璁$畻鍏紡 -->
+ <div class="form-section">
+ <div class="section-title">璁$畻鍏紡</div>
+ <div class="formula-input-section">
+ <el-form-item label="璁$畻鍏紡"
+ prop="formula"
+ required>
+ <el-input v-model="formData.formula"
+ placeholder="渚嬪: 棰勮鍑哄簱鏁伴噺 - 鐜版湁搴撳瓨 + 瀹夊叏搴撳瓨 - 棰勮鍏ュ簱鏁伴噺"
+ @input="validateFormula" />
+ </el-form-item>
+ <div class="formula-help">
+ <el-text type="info"
+ size="small">
+ 鏀寔鍙橀噺锛氶璁″嚭搴撴暟閲忋�佺幇鏈夊簱瀛樸�佸畨鍏ㄥ簱瀛樸�侀璁″叆搴撴暟閲�
+ </el-text>
+ </div>
+ </div>
+ </div>
+ </div>
+ </FormDialog>
+ <!-- 浜у搧閫夋嫨瀵硅瘽妗� -->
+ <FormDialog v-model="productSelectDialogVisible"
+ title="閫夋嫨浜у搧"
+ :width="'800px'"
+ :close-on-click-modal="false"
+ @close="productSelectDialogVisible = false"
+ @confirm="handleConfirmProductSelection"
+ @cancel="productSelectDialogVisible = false">
+ <div class="product-select">
+ <el-alert title="璇烽�夋嫨瑕佽绠楃殑浜у搧"
+ type="info"
+ :closable="false"
+ show-icon>
+ <template #default>
+ <p>閫夋嫨浜у搧鍚庯紝绯荤粺灏嗘牴鎹綋鍓嶈绠楀叕寮忓拰浜у搧搴撳瓨鎯呭喌杩涜璁$畻銆�</p>
+ </template>
+ </el-alert>
+ <el-table v-loading="productLoading"
+ :data="productList"
+ @selection-change="handleProductSelectionChange"
+ stripe
+ border
+ style="width: 100%; margin-top: 20px;">
+ <el-table-column type="selection"
+ width="55" />
+ <el-table-column prop="productCategory"
+ label="浜у搧澶х被"
+ min-width="150" />
+ <el-table-column prop="specificationModel"
+ label="瑙勬牸鍨嬪彿"
+ width="120" />
+ <el-table-column prop="inboundNum0"
+ label="鐜版湁搴撳瓨"
+ width="100"
+ align="right" />
+ <el-table-column prop="inboundNum"
+ label="瀹夊叏搴撳瓨"
+ width="100"
+ align="right" />
+ <el-table-column prop="inboundNum"
+ label="棰勮鍑哄簱"
+ width="100"
+ align="right" />
+ <el-table-column prop="inboundNum0"
+ label="棰勮鍏ュ簱"
+ width="100"
+ align="right" />
+ </el-table>
+ </div>
+ </FormDialog>
+ <!-- 璁$畻缁撴灉瀵硅瘽妗� -->
+ <FormDialog v-model="calculateDialogVisible"
+ title="閲囪喘璁$畻缁撴灉"
+ :width="'1000px'"
+ :close-on-click-modal="false"
+ @close="calculateDialogVisible = false"
+ @confirm="handleCreatePurchaseOrder"
+ @cancel="calculateDialogVisible = false">
+ <div class="calculate-result">
+ <el-alert title="璁$畻缁撴灉"
+ type="success"
+ :closable="false"
+ show-icon>
+ <template #default>
+ <p>鍩轰簬褰撳墠閰嶇疆鐨勮绠楀叕寮忓拰搴撳瓨鎯呭喌锛岀郴缁熷凡璁$畻鍑哄悇浜у搧鐨勯噰璐渶姹傘��</p>
+ </template>
+ </el-alert>
+ <el-table :data="calculateResult"
+ stripe
+ border
+ style="width: 100%; margin-top: 20px;">
+ <el-table-column prop="productCategory"
+ label="浜у搧澶х被"
+ min-width="150" />
+ <el-table-column prop="specificationModel"
+ label="瑙勬牸鍨嬪彿"
+ width="120" />
+ <el-table-column prop="inboundNum0"
+ label="鐜版湁搴撳瓨"
+ width="100"
+ align="right" />
+ <el-table-column prop="inboundNum"
+ label="瀹夊叏搴撳瓨"
+ width="100"
+ align="right" />
+ <el-table-column prop="inboundNum"
+ label="棰勮鍑哄簱鏁伴噺"
+ width="120"
+ align="right" />
+ <el-table-column prop="inboundNum0"
+ label="棰勮鍏ュ簱鏁伴噺"
+ width="120"
+ align="right" />
+ <el-table-column prop="weeklyNetDemand"
+ label="鎸夊懆鍑�闇�姹�"
+ width="120"
+ align="right">
+ <template #default="{ row }">
+ <el-tag :type="row.weeklyNetDemand > 0 ? 'warning' : 'success'"
+ size="small">
+ {{ row.weeklyNetDemand }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="suggestedPurchase"
+ label="寤鸿閲囪喘"
+ width="100"
+ align="right">
+ <template #default="{ row }">
+ <el-tag :type="row.suggestedPurchase > 0 ? 'danger' : 'success'"
+ size="small">
+ {{ row.suggestedPurchase }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import { ref, reactive, onMounted, getCurrentInstance, computed } from "vue";
+ import dayjs from "dayjs";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { Search, Refresh, Plus, Download } from "@element-plus/icons-vue";
+ import {
+ listPage,
+ add,
+ update,
+ del,
+ listPageCopy,
+ } from "@/api/procurementManagement/procurementPlan.js";
+
+ // 鍝嶅簲寮忔暟鎹�
+ const loading = ref(false);
+ const submitLoading = ref(false);
+ const dialogVisible = ref(false);
+ const productSelectDialogVisible = ref(false);
+ const calculateDialogVisible = ref(false);
+ const dialogType = ref("add");
+ const productLoading = ref(false);
+ const selectedProducts = ref([]);
+ const currentPlan = ref(null);
+
+ // 鎼滅储琛ㄥ崟
+ const searchForm = reactive({
+ planName: "",
+ status: "",
+ });
+
+ // 鍒嗛〉鏁版嵁
+ const pagination = reactive({
+ current: 1,
+ size: 20,
+ });
+
+ // 琛ㄥ崟鏁版嵁
+ const formData = reactive({
+ code: "",
+ planName: "",
+ description: "",
+ status: "",
+ isSystemPreset: false,
+ formula: "",
+ createTime: "",
+ // 璁$畻鍙傛暟
+ considerExistingStock: false,
+ warehouseControl: false,
+ calculateTotalDemand: false,
+ considerSafetyStock: false,
+ considerLockedStock: false,
+ notConsiderMaterialAux: false,
+ negativeStockAsDemand: false,
+ // 姹囨�诲悎骞堕�夐」
+ summaryMaterial: false,
+ summaryAuxAttributes: false,
+ summaryDemandDate: false,
+ });
+ const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
+
+ // 褰撳墠婵�娲荤殑鏍囩椤�
+ const activeTab = ref("demand");
+
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const formRules = {
+ planName: [{ required: true, message: "璇疯緭鍏ヨ鍒掑悕绉�", trigger: "blur" }],
+ status: [{ required: true, message: "璇烽�夋嫨鐘舵��", trigger: "change" }],
+ formula: [{ required: true, message: "璇疯緭鍏ヨ绠楀叕寮�", trigger: "blur" }],
+ };
+
+ // 琛ㄦ牸鏁版嵁
+ const tableData = ref([]);
+
+ // 浜у搧鍒楄〃鏁版嵁
+ const productList = ref([
+ {
+ id: 4,
+ productName: "浜у搧D",
+ productCode: "PD004",
+ existingStock: 90,
+ safetyStock: 40,
+ expectedOutbound: 160,
+ expectedInbound: 35,
+ },
+ ]);
+
+ // 璁$畻缁撴灉鏁版嵁
+ const calculateResult = ref([
+ {
+ productName: "浜у搧A",
+ existingStock: 100,
+ safetyStock: 50,
+ expectedOutbound: 200,
+ expectedInbound: 30,
+ weeklyNetDemand: 120,
+ suggestedPurchase: 150,
+ },
+ {
+ productName: "浜у搧B",
+ existingStock: 80,
+ safetyStock: 30,
+ expectedOutbound: 150,
+ expectedInbound: 20,
+ weeklyNetDemand: 100,
+ suggestedPurchase: 120,
+ },
+ ]);
+ const total = ref(0);
+ // 鏂规硶
+ const handleSearch = () => {
+ pagination.current = 1;
+ loadData();
+ };
+
+ const handleReset = () => {
+ Object.assign(searchForm, {
+ planName: "",
+ status: "",
+ });
+ handleSearch();
+ };
+
+ const loadData = () => {
+ loading.value = true;
+ listPage({ ...searchForm, ...pagination }).then(res => {
+ if (res.code === 200) {
+ tableData.value = res.data.records;
+ total.value = res.data.total;
+ loading.value = false;
+ }
+ });
+ };
+
+ const handleAdd = () => {
+ dialogType.value = "add";
+ resetForm();
+ formData.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ dialogVisible.value = true;
+ };
+
+ const handleEdit = row => {
+ dialogType.value = "edit";
+ Object.assign(formData, row);
+ dialogVisible.value = true;
+ };
+
+ const handleDelete = async row => {
+ try {
+ await ElMessageBox.confirm("纭畾瑕佸垹闄よ繖涓噰璐鍒掑悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ });
+ let ids = [row.id];
+ del(ids).then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ loadData();
+ }
+ });
+ } catch {
+ // 鐢ㄦ埛鍙栨秷鍒犻櫎
+ }
+ };
+
+ const handleSubmit = async () => {
+ try {
+ // 琛ㄥ崟楠岃瘉
+ if (!formData.planName || !formData.formula) {
+ ElMessage.error("璇峰~鍐欏繀濉」");
+ return;
+ }
+
+ submitLoading.value = true;
+
+ if (dialogType.value === "add") {
+ add(formData).then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鏂板鎴愬姛");
+ dialogVisible.value = false;
+ loadData();
+ }
+ });
+ } else {
+ // 缂栬緫
+ update(formData).then(res => {
+ if (res.code === 200) {
+ ElMessage.success("缂栬緫鎴愬姛");
+ dialogVisible.value = false;
+ loadData();
+ }
+ });
+ }
+ } catch (error) {
+ ElMessage.error("鎿嶄綔澶辫触");
+ } finally {
+ submitLoading.value = false;
+ }
+ };
+
+ const resetForm = () => {
+ Object.assign(formData, {
+ code: "",
+ planName: "",
+ description: "",
+ status: "",
+ isSystemPreset: false,
+ formula: "棰勮鍑哄簱鏁伴噺 - 鐜版湁搴撳瓨 + 瀹夊叏搴撳瓨 - 棰勮鍏ュ簱鏁伴噺",
+ // 璁$畻鍙傛暟
+ considerExistingStock: false,
+ warehouseControl: false,
+ calculateTotalDemand: false,
+ considerSafetyStock: false,
+ considerLockedStock: false,
+ notConsiderMaterialAux: false,
+ negativeStockAsDemand: false,
+ // 姹囨�诲悎骞堕�夐」
+ summaryMaterial: false,
+ summaryAuxAttributes: false,
+ summaryDemandDate: false,
+ });
+ activeTab.value = "demand";
+ };
+
+ const validateFormula = () => {
+ // 绠�鍗曠殑鍏紡楠岃瘉
+ const formula = formData.formula;
+ if (formula && !/^[a-zA-Z\u4e00-\u9fa5\s\*\+\-\/\(\)\d\.]+$/.test(formula)) {
+ ElMessage.warning("鍏紡鏍煎紡鍙兘涓嶆纭紝璇锋鏌�");
+ }
+ };
+
+ const handleCalculate = row => {
+ currentPlan.value = row;
+ productSelectDialogVisible.value = true;
+ loadProductList();
+ };
+
+ const loadProductList = () => {
+ productLoading.value = true;
+ // 妯℃嫙鍔犺浇浜у搧鏁版嵁
+ listPageCopy({ size: -1 }).then(res => {
+ if (res.code === 200) {
+ productList.value = res.data.records;
+ productLoading.value = false;
+ }
+ });
+ };
+
+ const handleProductSelectionChange = selection => {
+ selectedProducts.value = selection;
+ };
+
+ const handleConfirmProductSelection = () => {
+ if (selectedProducts.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佽绠楃殑浜у搧");
+ return;
+ }
+
+ ElMessage.success(`姝e湪璁$畻 ${currentPlan.value.planName} 鐨勯噰璐渶姹�...`);
+ productSelectDialogVisible.value = false;
+
+ // 鏍规嵁閫夋嫨鐨勪骇鍝佸拰璁$畻鍏紡杩涜璁$畻
+ calculateWithSelectedProducts();
+ };
+
+ const calculateWithSelectedProducts = () => {
+ // 妯℃嫙璁$畻杩囩▼
+ // 鏍规嵁閫夋嫨鐨勪骇鍝佹洿鏂拌绠楃粨鏋�
+ const result = selectedProducts.value.map(product => {
+ // 杩欓噷搴旇鏍规嵁瀹為檯鐨勮绠楀叕寮忚繘琛岃绠�
+ // 绀轰緥锛氶璁″嚭搴撴暟閲� - 鐜版湁搴撳瓨 + 瀹夊叏搴撳瓨 - 棰勮鍏ュ簱鏁伴噺
+ const weeklyNetDemand =
+ product.inboundNum -
+ product.inboundNum0 +
+ product.inboundNum -
+ product.inboundNum0;
+ const suggestedPurchase = Math.max(0, weeklyNetDemand);
+
+ return {
+ productCategory: product.productCategory,
+ specificationModel: product.specificationModel,
+ inboundNum0: product.inboundNum0,
+ inboundNum: product.inboundNum,
+ weeklyNetDemand: weeklyNetDemand,
+ suggestedPurchase: suggestedPurchase,
+ };
+ });
+
+ calculateResult.value = result;
+ calculateDialogVisible.value = true;
+ };
+
+ const handleCreatePurchaseOrder = () => {
+ calculateDialogVisible.value = false;
+ };
+ const { proxy } = getCurrentInstance();
+ const handleExport = () => {
+ ElMessageBox.confirm("鍐呭灏嗚瀵煎嚭锛屾槸鍚︾‘璁ゅ鍑猴紵", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/procurementPlan/export", {}, "閲囪喘璁″垝.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ const handleSizeChange = size => {
+ pagination.size = size;
+ loadData();
+ };
+
+ const handleCurrentChange = current => {
+ pagination.current = current;
+ loadData();
+ };
+
+ // 鐢熷懡鍛ㄦ湡
+ onMounted(() => {
+ loadData();
+ });
+</script>
+
+<style scoped>
+ .app-container {
+ padding: 20px;
+ }
+
+ .page-header {
+ margin-bottom: 20px;
+ }
+
+ .page-header h2 {
+ margin: 0 0 8px 0;
+ color: #303133;
+ font-size: 24px;
+ font-weight: 600;
+ }
+
+ .page-header p {
+ margin: 0;
+ color: #909399;
+ font-size: 14px;
+ }
+
+ .search-card {
+ margin-bottom: 20px;
+ }
+
+ .search-form {
+ margin-bottom: 0;
+ }
+
+ .table-card {
+ margin-bottom: 20px;
+ }
+
+ .table-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ }
+
+ .table-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .table-actions {
+ display: flex;
+ gap: 10px;
+ }
+
+ .pagination-container {
+ margin-top: 20px;
+ display: flex;
+ justify-content: end;
+ }
+
+ .form-container {
+ padding: 0 20px;
+ }
+
+ .formula-help {
+ margin-top: 5px;
+ }
+
+ .calculate-result {
+ padding: 20px 0;
+ }
+
+ .dialog-footer {
+ text-align: right;
+ }
+
+ :deep(.el-card__body) {
+ padding: 20px;
+ }
+
+ :deep(.el-table) {
+ font-size: 14px;
+ }
+
+ :deep(.el-form-item__label) {
+ font-weight: 500;
+ }
+
+ .form-container {
+ padding: 0;
+ }
+
+ .form-section {
+ margin-bottom: 24px;
+ border: 1px solid #e4e7ed;
+ border-radius: 6px;
+ overflow: hidden;
+ }
+
+ .section-title {
+ background-color: #f5f7fa;
+ padding: 12px 16px;
+ font-weight: 600;
+ color: #303133;
+ border-bottom: 1px solid #e4e7ed;
+ }
+
+ .form-section .el-form {
+ padding: 20px;
+ }
+
+ .param-tabs {
+ padding: 20px;
+ }
+
+ .param-tabs :deep(.el-tabs__header) {
+ margin-bottom: 20px;
+ }
+
+ .param-tabs :deep(.el-tabs__item.is-active) {
+ color: #f56c6c;
+ border-bottom-color: #f56c6c;
+ }
+
+ .checkbox-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ }
+
+ .checkbox-group .el-checkbox {
+ margin-right: 0;
+ margin-bottom: 8px;
+ }
+
+ .formula-input-section {
+ padding: 20px;
+ }
+
+ .formula-input-section .el-form-item {
+ margin-bottom: 12px;
+ }
+
+ .formula-help {
+ text-align: center;
+ margin-top: 8px;
+ }
+</style>
diff --git a/src/views/procurementManagement/procurementReport/index.vue b/src/views/procurementManagement/procurementReport/index.vue
new file mode 100644
index 0000000..26e682d
--- /dev/null
+++ b/src/views/procurementManagement/procurementReport/index.vue
@@ -0,0 +1,411 @@
+<template>
+ <div class="app-container">
+ <!-- 鏌ヨ鏉′欢 -->
+ <el-form :model="searchForm" :inline="true" class="search-form">
+ <el-form-item label="鏃堕棿鑼冨洿锛�">
+ <el-date-picker
+ v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 240px"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧澶х被锛�">
+ <el-tree-select
+ v-model="searchForm.productCategory"
+ placeholder="璇烽�夋嫨鍟嗗搧绫诲埆"
+ clearable
+ check-strictly
+ :data="productOptions"
+ :render-after-expand="false"
+ style="width: 200px"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch" :loading="loading">
+ 鏌ヨ
+ </el-button>
+ <el-button @click="resetSearch">
+ 閲嶇疆
+ </el-button>
+ <el-button type="info" @click="exportReport">
+ <el-icon><Download /></el-icon>
+ 瀵煎嚭
+ </el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鎶ヨ〃鍐呭 -->
+ <el-card class="report-content" shadow="never">
+ <!-- 閲囪喘涓氬姟姹囨�昏〃 -->
+ <div class="report-section">
+ <div class="section-header">
+ <h3>閲囪喘涓氬姟姹囨�昏〃</h3>
+ <div class="summary-stats">
+ <div class="stat-item">
+ <span class="stat-label">閲囪喘鎬婚锛�</span>
+ <span class="stat-value">楼{{ businessSummaryStats.totalAmount.toLocaleString() }}</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">鍟嗗搧绉嶇被锛�</span>
+ <span class="stat-value">{{ businessSummaryStats.productTypes }}</span>
+ </div>
+ <div class="stat-item">
+ <span class="stat-label">閫�娆炬�婚锛�</span>
+ <span class="stat-value">{{ businessSummaryStats.returnAmount }}</span>
+ </div>
+ </div>
+ </div>
+
+ <PIMTable
+ :table-data="businessSummaryData"
+ :column="tableColumns"
+ :table-loading="loading"
+ :is-selection="false"
+ :border="true"
+ :is-show-pagination="true"
+ :page="page"
+ @pagination="handlePagination"
+ />
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Download } from '@element-plus/icons-vue'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import { procurementBusinessSummaryListPage } from '@/api/procurementManagement/procurementReport'
+import { productTreeList } from '@/api/basicData/product'
+
+const { proxy } = getCurrentInstance()
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+ dateRange: [],
+ productCategory: ''
+})
+
+// 浜у搧绫诲埆鏍戦�夐」
+const productOptions = ref([])
+
+// 缁熻鏁版嵁
+const businessSummaryStats = ref({
+ totalAmount: 0,
+ productTypes: 0
+})
+
+// 琛ㄦ牸鍒楅厤缃紙鏍规嵁鍚庣瀛楁瀹氫箟锛�
+const tableColumns = ref([
+ {
+ label: '浜у搧澶х被',
+ prop: 'productCategory',
+ },
+ {
+ label: '瑙勬牸鍨嬪彿',
+ prop: 'specificationModel',
+ },
+ {
+ label: '閲囪喘鏁伴噺',
+ prop: 'purchaseNum',
+ width: 120,
+ formatData: (val) => {
+ return val ? parseFloat(val).toLocaleString() : '0'
+ }
+ },
+ {
+ label: '閫�璐ф暟閲�',
+ prop: 'returnQuantity',
+ width: 120,
+ formatData: (val) => {
+ return val ? parseFloat(val).toLocaleString() : '0'
+ }
+ },
+ {
+ label: '閫�璐ч噾棰�',
+ prop: 'returnAmount',
+ width: 120,
+ formatData: (val) => {
+ return val ? `楼${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '楼0.00'
+ }
+ },
+ {
+ label: '閫�娆惧偍閲�',
+ prop: 'purchaseAmount',
+ formatData: (val) => {
+ return val ? `楼${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '楼0.00'
+ }
+ },
+ {
+ label: '閲囪喘娆℃暟',
+ prop: 'purchaseTimes',
+ width: 100
+ },
+ {
+ label: '骞冲潎鍗曚环',
+ prop: 'averagePrice',
+ width: 120,
+ formatData: (val) => {
+ return val ? `楼${parseFloat(val).toFixed(2)}` : '楼0.00'
+ }
+ },
+ {
+ label: '渚涘簲鍟嗗悕绉�',
+ prop: 'supplierName',
+ },
+ {
+ label: '褰曞叆鏃ユ湡',
+ prop: 'entryDate',
+ width: 120
+ }
+])
+
+// 閲囪喘涓氬姟姹囨�昏〃鏁版嵁
+const businessSummaryData = ref([])
+
+// 鍒嗛〉鍙傛暟锛堝悗绔繑鍥烇細total/size/current/pages锛�
+const page = reactive({
+ total: 0,
+ current: 1,
+ size: 50,
+})
+
+// 杞崲浜у搧鏍戞暟鎹紝灏� id 鏀逛负 value
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id,
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+// 鑾峰彇浜у搧绫诲埆鏍戞暟鎹�
+const getProductOptions = () => {
+ return productTreeList().then((res) => {
+ productOptions.value = convertIdToValue(res)
+ }).catch((error) => {
+ console.error('鑾峰彇浜у搧鏍戝け璐�:', error)
+ ElMessage.error('鑾峰彇浜у搧绫诲埆澶辫触')
+ })
+}
+
+// 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+const findNodeLabelById = (nodes, id) => {
+ if (!id) return null
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === id) {
+ return nodes[i].label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const found = findNodeLabelById(nodes[i].children, id)
+ if (found) return found
+ }
+ }
+ return null
+}
+
+// 鏌ヨ鍒楄〃
+const handleSearch = async () => {
+ try {
+ loading.value = true
+ const params = {}
+
+ // 鏃堕棿鑼冨洿
+ if (searchForm.dateRange && searchForm.dateRange.length === 2) {
+ params.entryDateStart = searchForm.dateRange[0]
+ params.entryDateEnd = searchForm.dateRange[1]
+ }
+
+ // 浜у搧绫诲埆
+ if (searchForm.productCategory) {
+ const categoryName = findNodeLabelById(productOptions.value, searchForm.productCategory)
+ if (categoryName) {
+ params.productCategory = categoryName
+ }
+ }
+
+ // 鍒嗛〉鍙傛暟
+ params.current = page.current
+ params.size = page.size
+
+ const res = await procurementBusinessSummaryListPage(params)
+ if (res && res.data) {
+ // 鍏煎鍚庣鍙兘鐩存帴杩斿洖鏁扮粍/鎴栬繑鍥炲垎椤靛璞�
+ businessSummaryData.value = Array.isArray(res.data) ? res.data : (res.data.records || [])
+
+ if (!Array.isArray(res.data)) {
+ page.total = Number(res.data.total ?? 0)
+ page.current = Number(res.data.current ?? page.current)
+ page.size = Number(res.data.size ?? page.size)
+ }
+
+ // 璁$畻缁熻鏁版嵁
+ if (businessSummaryData.value.length > 0) {
+ businessSummaryStats.value.totalAmount = businessSummaryData.value.reduce((sum, item) => {
+ return sum + (parseFloat(item.purchaseAmount) || 0)
+ }, 0)
+ businessSummaryStats.value.returnAmount = businessSummaryData.value.reduce((sum, item) => {
+ return sum + (parseFloat(item.returnAmount) || 0)
+ }, 0)
+ businessSummaryStats.value.productTypes = businessSummaryData.value.length
+ } else {
+ businessSummaryStats.value = {
+ totalAmount: 0,
+ productTypes: 0
+ }
+ }
+ }
+ } catch (error) {
+ console.error('鏌ヨ澶辫触:', error)
+ } finally {
+ loading.value = false
+ }
+}
+
+// 缈婚〉/鍒囨崲姣忛〉鏉℃暟
+const handlePagination = ({ page: current, limit }) => {
+ page.current = current
+ page.size = limit
+ handleSearch()
+}
+
+const resetSearch = () => {
+ Object.assign(searchForm, {
+ dateRange: [],
+ productCategory: ''
+ })
+ page.current = 1
+ handleSearch()
+}
+
+const exportReport = () => {
+ const params = {}
+
+ // 鏃堕棿鑼冨洿
+ if (searchForm.dateRange && searchForm.dateRange.length === 2) {
+ params.entryDateStart = searchForm.dateRange[0]
+ params.entryDateEnd = searchForm.dateRange[1]
+ }
+
+ // 浜у搧绫诲埆
+ if (searchForm.productCategory) {
+ const categoryName = findNodeLabelById(productOptions.value, searchForm.productCategory)
+ if (categoryName) {
+ params.productCategory = categoryName
+ }
+ }
+
+ proxy.download("/procurementBusinessSummary/export", params, "閲囪喘涓氬姟姹囨�昏〃.xlsx")
+}
+
+
+onMounted(() => {
+ // 鍒濆鍖栦骇鍝佺被鍒爲
+ getProductOptions()
+
+ // 璁剧疆榛樿鏃堕棿鑼冨洿涓烘渶杩�30澶�
+ const endDate = new Date()
+ const startDate = new Date()
+ startDate.setDate(startDate.getDate() - 30)
+
+ searchForm.dateRange = [
+ startDate.toISOString().split('T')[0],
+ endDate.toISOString().split('T')[0]
+ ]
+
+ // 鍒濆鍔犺浇鏁版嵁
+ handleSearch()
+})
+</script>
+
+<style scoped>
+.page-header {
+ text-align: center;
+ margin-bottom: 20px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 10px;
+ color: white;
+}
+
+.page-header h2 {
+ margin: 0 0 10px 0;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.page-header p {
+ margin: 0;
+ font-size: 16px;
+ opacity: 0.9;
+}
+
+.report-content {
+ border-radius: 8px;
+}
+
+.report-section {
+ min-height: 400px;
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ padding-bottom: 15px;
+ border-bottom: 2px solid #e4e7ed;
+}
+
+.section-header h3 {
+ margin: 0;
+ font-size: 20px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.summary-stats {
+ display: flex;
+ gap: 30px;
+}
+
+.stat-item {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #606266;
+ margin-bottom: 5px;
+}
+
+.stat-value {
+ font-size: 18px;
+ font-weight: 600;
+ color: #409EFF;
+}
+
+.delay-text {
+ color: #F56C6C;
+ font-weight: 600;
+}
+
+
+</style>
diff --git a/src/views/procurementManagement/purchaseOrder/index.vue b/src/views/procurementManagement/purchaseOrder/index.vue
new file mode 100644
index 0000000..4f9812f
--- /dev/null
+++ b/src/views/procurementManagement/purchaseOrder/index.vue
@@ -0,0 +1,200 @@
+<template>
+ <div class="app-container">
+ <el-card class="search-card" shadow="never">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="渚涘簲鍟嗗悕绉帮細">
+ <el-input v-model="searchForm.supplierName" placeholder="璇疯緭鍏ヤ緵搴斿晢鍚嶇О" clearable />
+ </el-form-item>
+ <el-form-item label="璁㈠崟鐘舵�侊細">
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" clearable>
+ <el-option label="鑽夌" value="draft" />
+ <el-option label="寰呭鏍�" value="pending" />
+ <el-option label="宸插鏍�" value="approved" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <el-card class="table-card" shadow="never">
+ <div class="table-header">
+ <el-button type="primary" @click="openDialog('add')">鏂板璁㈠崟</el-button>
+ <el-button type="danger" @click="handleBatchDelete" :disabled="!selectedRows.length">鎵归噺鍒犻櫎</el-button>
+ </div>
+
+ <el-table :data="tableData" border v-loading="loading" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="璁㈠崟缂栧彿" prop="orderNo" width="180" />
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" />
+ <el-table-column label="璁㈠崟鐘舵��" prop="status" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎬婚噾棰�" prop="totalAmount" width="120">
+ <template #default="{ row }">楼{{ row.totalAmount.toFixed(2) }}</template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" prop="createTime" width="180" />
+ <el-table-column label="鎿嶄綔" width="200" align="center">
+ <template #default="{ row }">
+ <el-button type="primary" size="small" @click="openDialog('edit', row)">缂栬緫</el-button>
+ <el-button type="success" size="small" @click="viewDetails(row)">鏌ョ湅</el-button>
+ <el-button type="danger" size="small" @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <FormDialog v-model="dialogVisible" :title="dialogType === 'add' ? '鏂板閲囪喘璁㈠崟' : '缂栬緫閲囪喘璁㈠崟'" :width="'800px'" :operation-type="dialogType" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="formData" ref="formRef" label-width="120px">
+ <el-form-item label="渚涘簲鍟嗗悕绉�" prop="supplierName">
+ <el-select v-model="formData.supplierName" placeholder="璇烽�夋嫨渚涘簲鍟�" style="width: 100%">
+ <el-option label="渚涘簲鍟咥" value="渚涘簲鍟咥" />
+ <el-option label="渚涘簲鍟咮" value="渚涘簲鍟咮" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿">
+ <el-date-picker v-model="formCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="澶囨敞">
+ <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import { ref, reactive, computed } from 'vue'
+import dayjs from 'dayjs'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+const loading = ref(false)
+const dialogVisible = ref(false)
+const dialogType = ref('add')
+const selectedRows = ref([])
+
+const searchForm = reactive({
+ supplierName: '',
+ status: ''
+})
+
+const formData = reactive({
+ supplierName: '',
+ remark: '',
+ createTime: ''
+})
+const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(' ')[0] : ''),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format('HH:mm:ss')}` : ''
+ }
+})
+
+const mockData = [
+ {
+ id: 1,
+ orderNo: 'PO20241201001',
+ supplierName: '渚涘簲鍟咥',
+ status: 'approved',
+ totalAmount: 12500.00,
+ createTime: '2025-12-01 10:30:00',
+ remark: '甯歌閲囪喘'
+ }
+]
+
+const tableData = ref([...mockData])
+
+const getStatusType = (status) => {
+ const statusMap = { draft: 'info', pending: 'warning', approved: 'success' }
+ return statusMap[status] || 'info'
+}
+
+const getStatusText = (status) => {
+ const statusMap = { draft: '鑽夌', pending: '寰呭鏍�', approved: '宸插鏍�' }
+ return statusMap[status] || '鏈煡'
+}
+
+const handleSearch = () => {
+ loading.value = true
+ setTimeout(() => {
+ loading.value = false
+ }, 500)
+}
+
+const resetSearch = () => {
+ Object.assign(searchForm, { supplierName: '', status: '' })
+}
+
+const openDialog = (type, row = {}) => {
+ dialogType.value = type
+ if (type === 'edit' && row.id) {
+ Object.assign(formData, { supplierName: row.supplierName, remark: row.remark, createTime: row.createTime || '' })
+ } else {
+ Object.assign(formData, { supplierName: '', remark: '', createTime: dayjs().format('YYYY-MM-DD HH:mm:ss') })
+ }
+ dialogVisible.value = true
+}
+
+const handleSubmit = () => {
+ if (dialogType.value === 'add') {
+ const newOrder = {
+ id: Date.now(),
+ orderNo: '',
+ supplierName: formData.supplierName,
+ status: 'draft',
+ totalAmount: 0,
+ createTime: formData.createTime,
+ remark: formData.remark
+ }
+ tableData.value.unshift(newOrder)
+ ElMessage.success('鏂板鎴愬姛')
+ }
+ dialogVisible.value = false
+}
+
+const viewDetails = (row) => {
+ ElMessage.info('鏌ョ湅璇︽儏鍔熻兘')
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖鏉¤褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ const index = tableData.value.findIndex(item => item.id === row.id)
+ if (index !== -1) {
+ tableData.value.splice(index, 1)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ }
+ })
+}
+
+const handleBatchDelete = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸垹闄ょ殑璁板綍')
+ return
+ }
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+}
+
+const handleSelectionChange = (rows) => {
+ selectedRows.value = rows
+}
+</script>
+
+<style scoped>
+.app-container { padding: 20px; }
+.search-card { margin-bottom: 20px; }
+.table-card { margin-bottom: 20px; }
+.table-header { margin-bottom: 20px; }
+</style>
diff --git a/src/views/procurementManagement/purchaseReturnOrder/New.vue b/src/views/procurementManagement/purchaseReturnOrder/New.vue
new file mode 100644
index 0000000..9474b11
--- /dev/null
+++ b/src/views/procurementManagement/purchaseReturnOrder/New.vue
@@ -0,0 +1,808 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鏂板閲囪喘閫�璐�"
+ width="70%"
+ top="3vh"
+ @close="closeModal"
+ class="purchase-return-dialog"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef" :inline="true">
+ <div class="section-title">
+ <span class="title-dot"></span>
+ <span class="title-text">鍩烘湰淇℃伅</span>
+ </div>
+ <el-form-item
+ label="閫�鏂欏崟鍙�"
+ prop="no"
+ :rules="[
+ {
+ required: !formState.isDefaultNo,
+ message: '璇疯緭鍏ラ��鏂欏崟鍙�',
+ trigger: 'blur',
+ }
+ ]"
+ >
+ <el-input
+ v-model="formState.no"
+ :placeholder="formState.isDefaultNo ? '浣跨敤绯荤粺缂栧彿' : '璇疯緭鍏ラ��鏂欏崟鍙�'"
+ :disabled="formState.isDefaultNo"
+ style="width: 240px"
+ >
+ <template #append>
+ <el-checkbox v-model="formState.isDefaultNo" size="large" @change="handleChangeIsDefaultNo" />
+ </template>
+ </el-input>
+ </el-form-item>
+
+ <el-form-item
+ label="閫�璐ф柟寮�"
+ prop="returnType"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨閫�璐ф柟寮�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.returnType"
+ placeholder="璇烽�夋嫨閫�璐ф柟寮�"
+ style="width: 240px"
+ >
+ <el-option label="閫�璐ч��娆�" :value="0" />
+ <el-option label="鎷掓敹" :value="1" />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="渚涘簲鍟嗗悕绉�"
+ prop="supplierId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨渚涘簲鍟�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.supplierId"
+ placeholder="璇烽�夋嫨渚涘簲鍟�"
+ style="width: 240px"
+ filterable
+ @focus="fetchSupplierOptions"
+ @change="handleChangeSupplierId"
+ >
+ <el-option
+ v-for="item in supplierOptions"
+ :key="item.id"
+ :label="item.supplierName"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="椤圭洰闃舵"
+ prop="projectPhase"
+ >
+ <el-select
+ v-model="formState.projectPhase"
+ placeholder="璇烽�夋嫨椤圭洰闃舵"
+ style="width: 240px"
+ >
+ <el-option
+ v-for="item in projectStageOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="鍒朵綔鏃ユ湡"
+ prop="preparedAt"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鍒朵綔鏃ユ湡',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-date-picker
+ v-model="formState.preparedAt"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨鍒朵綔鏃ユ湡"
+ style="width: 240px"
+ clearable />
+ </el-form-item>
+
+ <el-form-item
+ label="鍒跺崟浜猴細"
+ prop="preparedUserId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鍒跺崟浜�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.preparedUserId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ style="width: 240px"
+ @focus="fetchUserOptions"
+ @change="formState.preparedUserName = userOptions.find(item => item.userId === formState.preparedUserId)?.nickName || ''"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="閫�鏂欎汉锛�"
+ prop="returnUserId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨閫�鏂欎汉',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.returnUserId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ default-first-option
+ style="width: 240px"
+ :reserve-keyword="false"
+ @focus="fetchUserOptions"
+ @change="formState.returnUserName = userOptions.find(item => item.userId === formState.returnUserId)?.nickName || ''"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="閲囪喘鍚堝悓鍙凤細"
+ prop="purchaseLedgerId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨閲囪喘鍚堝悓鍙�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.purchaseLedgerId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ default-first-option
+ style="width: 240px"
+ :reserve-keyword="false"
+ @change="handleChangePurchaseLedgerId"
+ >
+ <el-option
+ v-for="item in purchaseLedgerOptions"
+ :key="item.id"
+ :label="item.purchaseContractNumber"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item
+ label="澶囨敞锛�"
+ prop="remark"
+ >
+ <el-input style="width: 240px" v-model="formState.remark" :rows="1" type="textarea" placeholder="璇疯緭鍏ュ娉�"/>
+ </el-form-item>
+
+ <div style="margin:20px 0;min-width:0;">
+ <div class="section-title">
+ <span class="title-dot"></span>
+ <span class="title-text">浜у搧鍒楄〃</span>
+ </div>
+ <el-button type="primary" size="small" style="margin-bottom:20px" @click="isShowProductsModal = true" :disabled="!formState.purchaseLedgerId">娣诲姞浜у搧</el-button>
+ <el-table
+ :data="formState.purchaseReturnOrderProductsDtos"
+ border
+ max-height="400"
+ show-summary
+ :summary-method="summarizeChildrenTable"
+ style="width:100%;min-width:0;">
+ <el-table-column align="center"
+ type="selection"
+ width="55" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鍏ュ簱鍗曞彿"
+ prop="inboundBatches"
+ width="150" />
+ <el-table-column label="鎵规鍙�"
+ prop="batchNo"
+ width="150" />
+ <el-table-column label="浜у搧澶х被"
+ prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="specificationModel" />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ width="70" />
+ <el-table-column label="鏁伴噺"
+ prop="stockInNum"
+ width="100" />
+ <el-table-column label="鍙��璐ф暟閲�"
+ prop="unQuantity"
+ width="130" />
+ <el-table-column label="宸查��璐ф暟閲�"
+ width="130">
+ <template #default="scope">
+ {{ calcAlreadyReturned(scope.row) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�璐ф暟閲�"
+ prop="returnQuantity"
+ width="180">
+ <template #default="scope">
+ <el-input-number v-model="scope.row.returnQuantity"
+ controls-position="right"
+ :step="1"
+ :min="0"
+ :max="getReturnQtyMax(scope.row)"
+ :disabled="getReturnQtyMax(scope.row) <= 0"
+ @change="syncReturnTotal(scope.row)"
+ required
+ placeholder="璇疯緭鍏ラ��璐ф暟閲�" />
+ </template>
+ </el-table-column>
+ <!-- <el-table-column label="搴撳瓨棰勮鏁伴噺"
+ prop="warnNum"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="绋庣巼(%)"
+ prop="taxRate"
+ width="80" /> -->
+ <el-table-column label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="formattedNumber"
+ width="120" />
+ <el-table-column label="閫�璐ф�讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ width="120">
+ <template #default="scope">
+ {{ formatAmount(getReturnTotal(scope.row)) || '--' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁璐ㄦ"
+ prop="isChecked"
+ width="100">
+ <template #default="scope">
+ <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
+ {{ scope.row.isChecked ? '鏄�' : '鍚�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right"
+ label="鎿嶄綔"
+ width="100"
+ align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="danger"
+ size="small"
+ @click="delProduct(scope.$index)"
+ >
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <div class="section-title">
+ <span class="title-dot"></span>
+ <span class="title-text">璐圭敤淇℃伅</span>
+ </div>
+
+ <el-form-item
+ label="鏁村崟鎶樻墸棰濓細"
+ prop="totalDiscountAmount"
+ >
+ <el-input-number v-model="formState.totalDiscountAmount"
+ controls-position="right"
+ :step="0.01"
+ :precision="2"
+ style="width: 100%;"
+ @change="handleChangeTotalDiscountAmount"
+ placeholder="璇疯緭鍏ユ暣鍗曟姌鎵i"/>
+ </el-form-item>
+
+ <el-form-item
+ label="鏁村崟鎶樻墸鐜囷細"
+ prop="totalDiscountAmount"
+ >
+ <el-input v-model="formState.totalDiscountRate"
+ controls-position="right"
+ :step="0.01"
+ :precision="2"
+ style="width: 100%;"
+ @change="totalDiscount"
+ placeholder="璇疯緭鍏ユ暣鍗曟姌鎵g巼">
+ <template #append>
+ %
+ </template>
+ </el-input>
+ </el-form-item>
+
+ <el-form-item
+ label="鎴愪氦閲戦锛�"
+ prop="totalAmount"
+ :rules="[
+ {
+ required: true,
+ message: '璇疯緭鍏ユ垚浜ら噾棰�',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-input-number v-model="formState.totalAmount"
+ controls-position="right"
+ :step="0.01"
+ :precision="2"
+ style="width: 100%;"
+ placeholder="璇疯緭鍏ユ垚浜ら噾棰�"/>
+ </el-form-item>
+ <el-form-item label="鏀舵鏂瑰紡" prop="incomeType" :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨鏀舵鏂瑰紡',
+ trigger: 'change',
+ }
+ ]">
+ <el-select
+ style="width: 240px;"
+ v-model="formState.incomeType"
+ placeholder="璇烽�夋嫨"
+ clearable
+
+ >
+ <el-option :label="item.label" :value="item.value" v-for="(item,index) in payment_methods" :key="index" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <ProductList
+ v-if="isShowProductsModal"
+ v-model:visible="isShowProductsModal"
+ :purchase-ledger-id="formState.purchaseLedgerId"
+ @completed="handleAddProduct"
+ />
+
+ </div>
+</template>
+
+<script setup>
+import {ref, computed, getCurrentInstance, watch, defineAsyncComponent} from "vue";
+import {createPurchaseReturnOrder} from "@/api/procurementManagement/purchase_return_order.js";
+import {getOptions, purchaseList} from "@/api/procurementManagement/procurementLedger.js";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+const ProductList = defineAsyncComponent(() => import("@/views/procurementManagement/purchaseReturnOrder/ProductList.vue"));
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ }
+});
+let { proxy } = getCurrentInstance()
+const payment_methods = [
+ {
+ "label": "鐜伴噾",
+ "value": "0",
+ },
+ {
+ "label": "鏀エ",
+ "value": "1",
+ },
+ {
+ "label": "閾惰杞处",
+ "value": "2",
+ },
+ {
+ "label": "鍏朵粬",
+ "value": "3",
+ },
+]
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ no: '',
+ isDefaultNo: true,
+ returnType: 0,
+ incomeType: undefined,
+ remark: '',
+ supplierId: undefined,
+ projectId: undefined,
+ projectPhase: undefined,
+ preparedAt: undefined,
+ preparedUserId: undefined,
+ returnUserId: undefined,
+ purchaseLedgerId: undefined,
+ purchaseReturnOrderProductsDtos: [],
+ totalDiscountAmount: 0,
+ totalDiscountRate: undefined,
+ totalAmount: 0,
+});
+// 渚涘簲鍟嗛�夐」
+const supplierOptions = ref([])
+// 椤圭洰闃舵閫夐」
+const projectStageOptions = ref([
+ {
+ label: '绔嬮」',
+ value: 0,
+ },
+ {
+ label: '璁捐',
+ value: 1,
+ },
+ {
+ label: '閲囪喘',
+ value: 2,
+ },
+ {
+ label: '鐢熶骇',
+ value: 3,
+ },
+ {
+ label: '鍑鸿揣',
+ value: 4,
+ }
+])
+// 鐢ㄦ埛閫夐」
+const userOptions = ref([])
+// 閲囪喘鍙拌处閫夐」
+const purchaseLedgerOptions = ref([])
+// 鏄惁灞曠ず浜у搧鍒楄〃鏁版嵁
+const isShowProductsModal = ref(false)
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const formattedNumber = (row, column, cellValue) => {
+ return parseFloat(cellValue).toFixed(2);
+};
+
+const formatAmount = (value) => {
+ if (value === null || value === undefined || value === '') {
+ return '--'
+ }
+ const num = Number(value)
+ if (Number.isNaN(num)) {
+ return '--'
+ }
+ return num.toFixed(2)
+}
+
+const toNumber = (val) => {
+ const num = Number(val)
+ return Number.isNaN(num) ? 0 : num
+}
+
+/** 宸查��璐ф暟閲� = 鍏ュ簱琛屾�绘暟閲� 鈭� 褰撳墠鍙��璐ф暟閲忥紙鍓╀綑锛� */
+const calcAlreadyReturned = (row) => {
+ const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0)
+ const un = Number(row?.unQuantity ?? 0)
+ if (!Number.isFinite(total) || !Number.isFinite(un)) return 0
+ return Math.max(total - un, 0)
+}
+
+const getReturnTotal = (row) => {
+ const qty = toNumber(row?.returnQuantity)
+ const unitPrice = toNumber(row?.taxInclusiveUnitPrice)
+ const total = qty * unitPrice
+ return Number(total.toFixed(2))
+}
+
+const syncReturnTotal = (row) => {
+ if (!row) {
+ return
+ }
+ row.taxInclusiveTotalPrice = getReturnTotal(row)
+}
+
+const getBaseAmount = () => {
+ const rows = formState.value.purchaseReturnOrderProductsDtos || []
+ return rows.reduce((sum, item) => {
+ return sum + toNumber(item.taxInclusiveTotalPrice)
+ }, 0)
+}
+
+// 鍚屾鎶樻墸棰�
+const totalDiscount = () => {
+ const discountRate = toNumber(formState.value.totalDiscountRate)
+ if (discountRate < 0 || discountRate > 100) {
+ proxy.$modal.msgError("璇疯緭鍏�0-100涔嬮棿鐨勬姌鎵g巼")
+ return
+ }
+ const baseAmount = getBaseAmount()
+ // 鎶樻墸棰� = 浜у搧閫�璐ф�讳环鍚堣 * 鎶樻墸鐜�
+ formState.value.totalDiscountAmount = Number((baseAmount * (discountRate / 100)).toFixed(2))
+ syncTotalAmount()
+}
+
+const getReturnQtyMax = (row) => {
+ const max = Number(row?.unQuantity)
+ if (Number.isNaN(max) || max < 0) {
+ return 0
+ }
+ return max
+}
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const summarizeChildrenTable = (param) => {
+ return proxy.summarizeTable(
+ param,
+ [
+ "stockInNum",
+ "unQuantity",
+ "returnQuantity",
+ "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice",
+ "taxExclusiveTotalPrice",
+ ],
+ {
+ stockInNum: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ returnQuantity: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ unQuantity: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ }
+ );
+};
+
+const handleChangeTotalDiscountAmount= () => {
+ const discountAmount = toNumber(formState.value.totalDiscountAmount)
+ if (discountAmount < 0) {
+ proxy.$modal.msgError("鏁村崟鎶樻墸棰濅笉鑳藉皬浜�0")
+ formState.value.totalDiscountAmount = 0
+ }
+
+ const baseAmount = getBaseAmount()
+ const normalizedAmount = toNumber(formState.value.totalDiscountAmount)
+ if (baseAmount <= 0) {
+ formState.value.totalDiscountRate = 0
+ syncTotalAmount()
+ return
+ }
+
+ if (normalizedAmount > baseAmount) {
+ proxy.$modal.msgError("鏁村崟鎶樻墸棰濅笉鑳藉ぇ浜庝骇鍝侀��璐ф�讳环鍚堣")
+ formState.value.totalDiscountAmount = Number(baseAmount.toFixed(2))
+ }
+
+ const discountRate = (toNumber(formState.value.totalDiscountAmount) / baseAmount) * 100
+ formState.value.totalDiscountRate = Number(discountRate.toFixed(2))
+ syncTotalAmount()
+}
+
+const resetFeeInfo = () => {
+ formState.value.totalDiscountAmount = 0
+ formState.value.totalDiscountRate = undefined
+ formState.value.totalAmount = 0
+ formState.value.incomeType = undefined
+}
+
+const syncTotalAmount = () => {
+ const baseAmount = getBaseAmount()
+ const discount = toNumber(formState.value.totalDiscountAmount)
+ // 鎴愪氦閲戦 = 浜у搧閫�璐ф�讳环鍚堣 - 鎶樻墸棰�
+ formState.value.totalAmount = Number((baseAmount - discount).toFixed(2))
+}
+
+// 鑾峰彇渚涘簲鍟嗛�夐」
+const fetchSupplierOptions = () => {
+ if (supplierOptions.value.length > 0) {
+ return
+ }
+ getOptions().then((res) => {
+ supplierOptions.value = res.data;
+ });
+}
+
+
+// 鑾峰彇鐢ㄦ埛閫夐」
+const fetchUserOptions = () => {
+ if (userOptions.value.length > 0) {
+ return
+ }
+ userListNoPageByTenantId().then((res) => {
+ userOptions.value = res.data;
+ });
+}
+
+// 澶勭悊鏀瑰彉渚涘簲鍟嗘暟鎹�
+const handleChangeSupplierId = () => {
+ formState.value.purchaseLedgerId = undefined
+ formState.value.supplierName = supplierOptions.value.find(item => item.id === formState.value.supplierId)?.supplierName || ''
+ fetchPurchaseLedgerOptions()
+}
+
+// 鑾峰彇閲囪喘鍙拌处閫夐」
+const fetchPurchaseLedgerOptions = () => {
+ purchaseLedgerOptions.value = []
+ if (formState.value.supplierId) {
+ purchaseList({supplierId: formState.value.supplierId,approvalStatus:3}).then((res) => {
+ purchaseLedgerOptions.value = res.rows;
+ });
+ }
+}
+
+// 澶勭悊鏀瑰彉閲囪喘鍙拌处鏁版嵁锛堜笉璇锋眰鎺ュ彛鍥炴樉浜у搧锛屼骇鍝佷粎鍦ㄣ�屾坊鍔犱骇鍝併�嶅脊绐楀嬀閫夊悗鍐欏叆锛�
+const handleChangePurchaseLedgerId = () => {
+ resetFeeInfo()
+ formState.value.purchaseReturnOrderProductsDtos = []
+ syncTotalAmount()
+}
+
+// 澶勭悊鏀瑰彉鏄惁榛樿缂栧彿
+const handleChangeIsDefaultNo = (checked) => {
+ if (checked) {
+ formState.value.no = ''
+ }
+}
+
+// 澧炲姞浜у搧
+const handleAddProduct = (selectedRows) => {
+ const existingIds = new Set(formState.value.purchaseReturnOrderProductsDtos.map(item => item.id));
+ const newProducts = selectedRows.filter(item => !existingIds.has(item.id)).map(item => ({
+ ...item,
+ returnQuantity: undefined,
+ taxInclusiveTotalPrice: 0,
+ // salesLedgerProductId: item.salesLedgerProductId,
+ }));
+ formState.value.purchaseReturnOrderProductsDtos.push(...newProducts);
+ syncTotalAmount()
+}
+
+// 鍒犻櫎鍗曢」浜у搧
+const delProduct = (index) => {
+ formState.value.purchaseReturnOrderProductsDtos.splice(index, 1)
+ syncTotalAmount()
+}
+
+// 鎻愪氦琛ㄥ崟
+const handleSubmit = () => {
+ const productList = formState.value.purchaseReturnOrderProductsDtos || []
+
+ productList.forEach(syncReturnTotal)
+
+ if (productList.length === 0) {
+ proxy.$modal.msgError("璇峰厛閫夋嫨浜у搧")
+ return
+ }
+
+ // 閫愯鏍¢獙閫�璐ф暟閲忥細浠绘剰涓�琛屾湭濉�/闈炴硶/瓒呴檺閮戒笉鍏佽鎻愪氦
+ const invalidRowIndex = productList.findIndex((item) => {
+ const qty = Number(item.returnQuantity)
+ const maxQty = Number(item.unQuantity)
+
+ if (item.returnQuantity === null || item.returnQuantity === undefined || item.returnQuantity === "") {
+ return true
+ }
+ if (Number.isNaN(qty) || qty <= 0) {
+ return true
+ }
+ if (!Number.isNaN(maxQty) && maxQty > 0 && qty > maxQty) {
+ return true
+ }
+ return false
+ })
+
+ if (invalidRowIndex !== -1) {
+ proxy.$modal.msgError(`绗�${invalidRowIndex + 1}琛岄��璐ф暟閲忔湭濉啓鎴栦笉鍚堟硶`)
+ return
+ }
+
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ console.log(productList)
+ const submitPayload = {
+ ...formState.value,
+ purchaseReturnOrderProductsDtos: productList.map((row) => ({
+ ...row,
+ stockInRecordId: row.id,
+ })),
+ }
+ createPurchaseReturnOrder(submitPayload).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+watch(
+ () => formState.value.purchaseReturnOrderProductsDtos,
+ (rows) => {
+ (rows || []).forEach(syncReturnTotal)
+ syncTotalAmount()
+ },
+ { deep: true }
+)
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
+
+<style scoped lang="scss">
+.section-title {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20px;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ width: 100%;
+ clear: both;
+}
+
+.title-dot {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ background-color: #409EFF;
+ border-radius: 50%;
+ margin-right: 8px;
+}
+
+
+</style>
\ No newline at end of file
diff --git a/src/views/procurementManagement/purchaseReturnOrder/ProductList.vue b/src/views/procurementManagement/purchaseReturnOrder/ProductList.vue
new file mode 100644
index 0000000..27fae4a
--- /dev/null
+++ b/src/views/procurementManagement/purchaseReturnOrder/ProductList.vue
@@ -0,0 +1,191 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鏂板浜у搧"
+ width="1200"
+ @close="closeModal"
+ >
+ <div class="table_list" v-loading="tableLoading">
+ <el-table :data="tableData"
+ border
+ row-key="id"
+ @selection-change="handleChangeSelection">
+ <el-table-column align="center"
+ type="selection"
+ width="55" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鍏ュ簱鍗曞彿"
+ prop="inboundBatches"
+ width="150" />
+ <el-table-column label="鎵规鍙�"
+ prop="batchNo"
+ width="150" />
+ <el-table-column label="浜у搧澶х被"
+ prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="specificationModel" />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ width="70" />
+ <el-table-column label="鏁伴噺"
+ prop="stockInNum"
+ width="70" />
+ <el-table-column label="鍙��璐ф暟閲�"
+ prop="unQuantity"
+ width="130" />
+ <el-table-column label="宸查��璐ф暟閲�"
+ width="130">
+ <template #default="scope">
+ {{ calcAlreadyReturned(scope.row) }}
+ </template>
+ </el-table-column>
+ <!-- <el-table-column label="搴撳瓨棰勮鏁伴噺"
+ prop="warnNum"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="绋庣巼(%)"
+ prop="taxRate"
+ width="80" /> -->
+ <el-table-column label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <!-- <el-table-column label="鍚◣鎬讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150" /> -->
+ <el-table-column label="鏄惁璐ㄦ"
+ prop="isChecked"
+ width="150">
+ <template #default="scope">
+ <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
+ {{ scope.row.isChecked ? '鏄�' : '鍚�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" :disabled="selectedRows.length === 0" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {computed, ref, onMounted} from "vue";
+import {getPurchaseReturnOrderByPurchaseLedgerId} from "@/api/procurementManagement/purchase_return_order.js";
+import {ElMessage} from "element-plus";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ purchaseLedgerId: {
+ type: [Number, String],
+ required: true,
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const tableData = ref([])
+const selectedRows = ref([])
+const tableLoading = ref(false)
+const formattedNumber = (row, column, cellValue) => {
+ return parseFloat(cellValue).toFixed(2);
+};
+
+/** 宸查��璐ф暟閲� = 鍏ュ簱琛屾�绘暟閲� 鈭� 褰撳墠鍙��璐ф暟閲忥紙鍓╀綑锛� */
+const calcAlreadyReturned = (row) => {
+ const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0)
+ const un = Number(row?.unQuantity ?? 0)
+ if (!Number.isFinite(total) || !Number.isFinite(un)) return 0
+ return Math.max(total - un, 0)
+}
+
+const handleChangeSelection = (val) => {
+ selectedRows.value = val;
+}
+
+/** 涓� New.vue 涓噰璐彴璐﹀彉鏇存椂瑙f瀽 getByPurchaseLedgerId 鐨勮鍒欎竴鑷� */
+const parseProductRowsFromLedgerResponse = (res) => {
+ const payload = res?.data
+ let list = []
+ if (Array.isArray(payload)) {
+ list = payload
+ } else if (payload && typeof payload === 'object') {
+ const nested =
+ payload.purchaseReturnOrderProductsDtos ||
+ payload.purchaseReturnOrderProductsDetailVoList
+ list = Array.isArray(nested) ? nested : []
+ if (list.length && list[0]?.salesLedgerProduct) {
+ list = list.map((item) => ({ ...item, ...item.salesLedgerProduct }))
+ }
+ }
+ return list
+}
+
+const fetchData = () => {
+ if (props.purchaseLedgerId === undefined || props.purchaseLedgerId === null || props.purchaseLedgerId === '') {
+ tableData.value = []
+ return
+ }
+ tableLoading.value = true
+ getPurchaseReturnOrderByPurchaseLedgerId({
+ purchaseLedgerId: props.purchaseLedgerId,
+ })
+ .then((res) => {
+ const list = parseProductRowsFromLedgerResponse(res)
+ tableData.value = list
+ })
+ .catch(() => {
+ tableData.value = []
+ })
+ .finally(() => {
+ tableLoading.value = false
+ })
+}
+
+const handleSubmit = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�鏉′骇鍝�");
+ return;
+ }
+
+ emit('completed', selectedRows.value);
+ closeModal()
+}
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+onMounted(() => {
+ fetchData()
+})
+
+</script>
diff --git a/src/views/procurementManagement/purchaseReturnOrder/index.vue b/src/views/procurementManagement/purchaseReturnOrder/index.vue
new file mode 100644
index 0000000..f8866e1
--- /dev/null
+++ b/src/views/procurementManagement/purchaseReturnOrder/index.vue
@@ -0,0 +1,481 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="閫�鏂欏崟鍙凤細">
+ <el-input
+ v-model="searchForm.no"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery"
+ />
+ </el-form-item>
+
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery"> 鎼滅储 </el-button>
+ </el-form-item>
+ </el-form>
+
+ <div>
+ <el-button type="primary" @click="isShowNewModal = true"
+ >鏂板</el-button
+ >
+ </div>
+ </div>
+
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ :page="page"
+ :height="'calc(100vh - 18.5em)'"
+ @selection-change="handleSelectionChange"
+ @pagination="paginationChange"
+ >
+ <template #operation="{ row }">
+ <el-button
+ link
+ type="primary"
+ size="small"
+ style="color: #67c23a"
+ @click="handleDetail(row)"
+ >璇︽儏</el-button
+ >
+ <el-button link size="small" @click="handleDelete(row)"
+ >鍒犻櫎</el-button
+ >
+ </template>
+ </PIMTable>
+ </div>
+ <new
+ v-if="isShowNewModal"
+ v-model:visible="isShowNewModal"
+ @completed="handleQuery"
+ />
+
+ <el-dialog
+ v-model="detailVisible"
+ title="閲囪喘閫�璐ц鎯�"
+ width="1200"
+ destroy-on-close
+ >
+ <div v-loading="detailLoading">
+ <el-descriptions :column="3" border>
+ <el-descriptions-item label="閫�鏂欏崟鍙�">{{
+ detailData.no || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="閫�璐ф柟寮�">{{
+ getReturnTypeLabel(detailData.returnType)
+ }}</el-descriptions-item>
+ <el-descriptions-item label="渚涘簲鍟嗗悕绉�">{{
+ detailData.supplierName || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰闃舵">{{
+ getProjectPhaseLabel(detailData.projectPhase)
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍏宠仈鐨勯噰璐鍗曞彿">{{
+ detailData.purchaseContractNumber || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍒朵綔鏃ユ湡">{{
+ detailData.preparedAt || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍒跺崟浜�">{{
+ detailData.preparedUserName || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="閫�鏂欎汉">{{
+ detailData.returnUserName || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鏁村崟鎶樻墸棰�">{{
+ formatAmount(detailData.totalDiscountAmount)
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鏁村崟鎶樻墸鐜�">{{
+ detailData.totalDiscountRate ?? "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鎴愪氦閲戦">{{
+ formatAmount(detailData.totalAmount)
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓浜�">{{
+ detailData.createUserName || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍒涘缓鏃堕棿">{{
+ detailData.createTime || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鏈�杩戞洿鏂版椂闂�">{{
+ detailData.updateTime || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞" :span="3">{{
+ detailData.remark || "--"
+ }}</el-descriptions-item>
+ </el-descriptions>
+
+ <el-divider content-position="left">浜у搧鍒楄〃</el-divider>
+
+ <el-table
+ :data="detailProducts"
+ border
+ max-height="420"
+ style="width: 100%"
+ >
+ <el-table-column
+ align="center"
+ label="搴忓彿"
+ type="index"
+ width="60"
+ />
+ <el-table-column label="鍏ュ簱鍗曞彿" prop="inboundBatches" width="150" />
+ <el-table-column label="鎵规鍙�" prop="batchNo" width="150" />
+ <el-table-column
+ label="浜у搧澶х被"
+ prop="productCategory"
+ min-width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="瑙勬牸鍨嬪彿"
+ prop="specificationModel"
+ min-width="140"
+ show-overflow-tooltip
+ />
+ <el-table-column label="鍗曚綅" prop="unit" width="80" />
+ <el-table-column label="鏁伴噺" prop="stockInNum" width="80" />
+ <el-table-column label="鍙��璐ф暟閲�"
+ prop="unQuantity"
+ width="100" />
+ <el-table-column label="宸查��璐ф暟閲�"
+ width="100">
+ <template #default="scope">
+ {{ calcAlreadyReturned(scope.row) }}
+ </template>
+ </el-table-column>
+ <!-- <el-table-column label="搴撳瓨棰勮鏁伴噺" prop="warnNum" width="120" /> -->
+ <!-- <el-table-column label="绋庣巼(%)" prop="taxRate" width="90" /> -->
+ <el-table-column
+ label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ width="130"
+ >
+ <template #default="scope">{{
+ formatAmount(scope.row.taxInclusiveUnitPrice)
+ }}</template>
+ </el-table-column>
+ <!-- <el-table-column
+ label="閫�璐ф�讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ width="130"
+ >
+ <template #default="scope">{{
+ formatAmount(scope.row.taxInclusiveTotalPrice)
+ }}</template>
+ </el-table-column>
+ <el-table-column
+ label="涓嶉��璐ф�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ width="140"
+ >
+ <template #default="scope">{{
+ formatAmount(scope.row.taxExclusiveTotalPrice)
+ }}</template>
+ </el-table-column> -->
+ <el-table-column
+ label="鏄惁璐ㄦ"
+ prop="isChecked"
+ width="100"
+ align="center"
+ >
+ <template #default="scope">
+ <el-tag :type="scope.row.isChecked ? 'success' : 'info'">
+ {{ scope.row.isChecked ? "鏄�" : "鍚�" }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <template #footer>
+ <el-button @click="detailVisible = false">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import {
+ ref,
+ reactive,
+ toRefs,
+ onMounted,
+ defineAsyncComponent,
+ getCurrentInstance,
+} from "vue";
+const { proxy } = getCurrentInstance();
+import {
+ findPurchaseReturnOrderListPage,
+ getPurchaseReturnOrderDetail,
+ deletePurchaseReturnOrder,
+} from "@/api/procurementManagement/purchase_return_order.js";
+const New = defineAsyncComponent(() =>
+ import("@/views/procurementManagement/purchaseReturnOrder/New.vue")
+);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const detailVisible = ref(false);
+const detailLoading = ref(false);
+const detailData = ref({});
+const detailProducts = ref([]);
+// 鏄惁鏄剧ず鏂板寮规
+const isShowNewModal = ref(false);
+const returnTypeOptions = [
+ { label: "閫�璐ч��娆�", value: 0 },
+ { label: "鎷掓敹", value: 1 },
+];
+const projectPhaseOptions = [
+ { label: "绔嬮」", value: 0 },
+ { label: "璁捐", value: 1 },
+ { label: "閲囪喘", value: 2 },
+ { label: "鐢熶骇", value: 3 },
+ { label: "鍑鸿揣", value: 4 },
+];
+const tableColumn = ref([
+ {
+ label: "閫�鏂欏崟鍙�",
+ prop: "no",
+ },
+ {
+ label: "閫�璐ф柟寮�",
+ prop: "returnType",
+ formatData: (val) =>
+ returnTypeOptions.find((item) => item.value === val)?.label || "--",
+ },
+ {
+ label: "渚涘簲鍟嗗悕绉�",
+ prop: "supplierName",
+ width: 180,
+ },
+ {
+ label: "椤圭洰闃舵",
+ prop: "projectPhase",
+ width: 100,
+ formatData: (val) =>
+ projectPhaseOptions.find((item) => String(item.value) === String(val))
+ ?.label || "--",
+ },
+ {
+ label: "鍏宠仈鐨勯噰璐鍗曞彿",
+ prop: "purchaseContractNumber",
+ width: 160,
+ },
+ {
+ label: "鍒朵綔鏃ユ湡",
+ prop: "preparedAt",
+ width: 130,
+ },
+ {
+ label: "鍒跺崟浜�",
+ prop: "preparedUserName",
+ width: 110,
+ },
+ {
+ label: "閫�鏂欎汉",
+ prop: "returnUserName",
+ width: 110,
+ },
+
+ {
+ label: "鏁村崟鎶樻墸棰�",
+ prop: "totalDiscountAmount",
+ width: 120,
+ },
+ {
+ label: "鏁村崟鎶樻墸鐜�",
+ prop: "totalDiscountRate",
+ width: 120,
+ },
+ {
+ label: "鎴愪氦閲戦",
+ prop: "totalAmount",
+ width: 120,
+ },
+ {
+ label: "鍒涘缓浜�",
+ prop: "createUserName",
+ width: 110,
+ },
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 170,
+ },
+ {
+ label: "鏈�杩戞洿鏂版椂闂�",
+ prop: "updateTime",
+ width: 170,
+ },
+ {
+ label: "澶囨敞",
+ prop: "remark",
+ width: 180,
+ },
+ {
+ dataType: "action",
+ width: 120,
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ operation: [
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: (row) => {
+ handleDetail(row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ clickFun: (row) => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+]);
+const data = reactive({
+ searchForm: {
+ no: "",
+ },
+});
+const { searchForm } = toRefs(data);
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+// 鍒犻櫎鎿嶄綔
+const handleDelete = (row) => {
+ console.log("鍒犻櫎琛屾暟鎹細", row);
+ proxy?.$modal
+ ?.confirm("纭畾瑕佸垹闄ゅ悧锛熷垹闄ゅ皢鏃犳硶鎭㈠")
+ .then(() => {
+ // 杩欓噷璋冪敤鍒犻櫎鎺ュ彛锛屼紶鍏� row.id
+ deletePurchaseReturnOrder(row.id)
+ .then(() => {
+ proxy?.$modal?.msgSuccess?.("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy?.$modal?.msgError?.("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {
+ // 鍙栨秷鍒犻櫎
+ proxy?.$modal?.msgInfo?.("宸插彇娑堝垹闄�");
+ });
+};
+// 鏌ョ湅璇︽儏
+const handleDetail = (row) => {
+ if (!row?.id) {
+ proxy?.$modal?.msgWarning?.("鏈幏鍙栧埌鍗曟嵁ID");
+ return;
+ }
+ detailVisible.value = true;
+ detailLoading.value = true;
+ getPurchaseReturnOrderDetail(row.id)
+ .then((res) => {
+ const payload = res?.data || {};
+ detailData.value = payload;
+ // 鎷兼帴杩炰釜瀵硅薄鎴愪竴涓璞★紝鏂逛究灞曠ず item 鍜� item.salesLedgerProduct 閲岀殑瀛楁
+
+ detailProducts.value =
+ payload.purchaseReturnOrderProductsDetailVoList.map((item) => ({
+ ...item,
+ ...item.salesLedgerProduct,
+ })) || [];
+ })
+ .catch(() => {
+ proxy?.$modal?.msgError?.("鑾峰彇璇︽儏澶辫触");
+ })
+ .finally(() => {
+ detailLoading.value = false;
+ });
+};
+
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ findPurchaseReturnOrderListPage({ ...searchForm.value, ...page })
+ .then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+};
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter((item) => item.id);
+};
+
+const getReturnTypeLabel = (value) => {
+ return (
+ returnTypeOptions.find((item) => String(item.value) === String(value))
+ ?.label || "--"
+ );
+};
+
+const getProjectPhaseLabel = (value) => {
+ return (
+ projectPhaseOptions.find((item) => String(item.value) === String(value))
+ ?.label || "--"
+ );
+};
+
+const formatAmount = (value) => {
+ if (value === null || value === undefined || value === "") {
+ return "--";
+ }
+ const num = Number(value);
+ if (Number.isNaN(num)) {
+ return value;
+ }
+ return num.toFixed(2);
+};
+
+/** 宸查��璐ф暟閲� = 鍏ュ簱琛屾�绘暟閲� 鈭� 褰撳墠鍙��璐ф暟閲忥紙鍓╀綑锛� */
+const calcAlreadyReturned = (row) => {
+ const total = Number(row?.stockInNum ?? row?.totalQuantity ?? row?.quantity ?? 0);
+ const un = Number(row?.unQuantity ?? 0);
+ if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
+ return Math.max(total - un, 0);
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+<style scoped>
+.table_list {
+ margin-top: unset;
+}
+</style>
+
diff --git a/src/views/procurementManagement/qualityInspection/index.vue b/src/views/procurementManagement/qualityInspection/index.vue
new file mode 100644
index 0000000..aee1d99
--- /dev/null
+++ b/src/views/procurementManagement/qualityInspection/index.vue
@@ -0,0 +1,324 @@
+<template>
+ <div class="app-container">
+ <el-card class="search-card" shadow="never">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="璐ㄦ鍗曞彿锛�" style="width: 300px;">
+ <el-input v-model="searchForm.inspectionNo" placeholder="璇疯緭鍏ヨ川妫�鍗曞彿" clearable />
+ </el-form-item>
+ <el-form-item label="璐ㄦ鐘舵�侊細" style="width: 300px;">
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" clearable>
+ <el-option label="寰呰川妫�" value="pending" />
+ <el-option label="璐ㄦ涓�" value="inspecting" />
+ <el-option label="宸插畬鎴�" value="completed" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <el-card class="table-card" shadow="never">
+ <div class="table-header">
+ <el-button type="primary" @click="openDialog('add')">鏂板璐ㄦ鍗�</el-button>
+ <el-button type="success" @click="handleBatchComplete">鎵归噺瀹屾垚</el-button>
+ <el-button type="danger" @click="handleBatchDelete">鎵归噺鍒犻櫎</el-button>
+ </div>
+
+ <el-table :data="tableData" border v-loading="loading" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="璐ㄦ鍗曞彿" prop="inspectionNo" width="180" />
+ <el-table-column label="鍒拌揣鍗曞彿" prop="arrivalNo" width="180" />
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" />
+ <el-table-column label="璐ㄦ鐘舵��" prop="status" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堟牸鏁伴噺" prop="qualifiedQuantity" width="100" />
+ <el-table-column label="涓嶅悎鏍兼暟閲�" prop="unqualifiedQuantity" width="100" />
+ <el-table-column label="璐ㄦ鏃堕棿" prop="inspectionTime" width="180" />
+ <el-table-column label="鎿嶄綔" width="200" align="center">
+ <template #default="{ row }">
+ <el-button type="primary" link @click="openDialog('edit', row)">缂栬緫</el-button>
+ <el-button type="success" link @click="handleComplete(row)" v-if="row.status !== 'completed'">瀹屾垚</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+
+ <FormDialog v-model="dialogVisible" :title="dialogType === 'add' ? '鏂板璐ㄦ鍗�' : '缂栬緫璐ㄦ鍗�'" :width="'1000px'" :operation-type="dialogType" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="formData" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒拌揣鍗曞彿">
+ <el-select v-model="formData.arrivalNo" placeholder="璇烽�夋嫨鍒拌揣鍗�" style="width: 100%">
+ <el-option label="AR20241201001" value="AR20241201001" />
+ <el-option label="AR20241201002" value="AR20241201002" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="渚涘簲鍟嗗悕绉�">
+ <el-input v-model="formData.supplierName" placeholder="渚涘簲鍟嗗悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="璐ㄦ鍟嗗搧">
+ <div class="product-list" style="width: 100%;">
+ <el-table :data="formData.products" border width="100%">
+ <el-table-column label="鍟嗗搧鍚嶇О" width="150">
+ <template #default="{ row }">
+ <el-input v-model="row.productName" placeholder="璇疯緭鍏ュ晢鍝佸悕绉�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="瑙勬牸鍨嬪彿" width="150">
+ <template #default="{ row }">
+ <el-input v-model="row.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒拌揣鏁伴噺" width="150">
+ <template #default="{ row }">
+ <el-input-number v-model="row.arrivalQuantity" :min="0" placeholder="鏁伴噺" style="width: 100%;"/>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍚堟牸鏁伴噺" width="150">
+ <template #default="{ row }">
+ <el-input-number v-model="row.qualifiedQuantity" :min="0" placeholder="鏁伴噺" style="width: 100%;"/>
+ </template>
+ </el-table-column>
+ <el-table-column label="涓嶅悎鏍兼暟閲�" width="150">
+ <template #default="{ row }">
+ <el-input-number v-model="row.unqualifiedQuantity" :min="0" placeholder="鏁伴噺" style="width: 100%;"/>
+ </template>
+ </el-table-column>
+ <el-table-column label="涓嶅悎鏍煎師鍥�" width="200">
+ <template #default="{ row }">
+ <el-input v-model="row.unqualifiedReason" placeholder="璇疯緭鍏ヤ笉鍚堟牸鍘熷洜" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="100">
+ <template #default="{ $index }">
+ <el-button type="danger" link @click="removeProduct($index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="add-product-btn">
+ <el-button type="primary" @click="addProduct">娣诲姞鍟嗗搧</el-button>
+ </div>
+ </div>
+ </el-form-item>
+
+ <el-form-item label="璐ㄦ鍛�">
+ <el-input v-model="formData.inspector" placeholder="璇疯緭鍏ヨ川妫�鍛樺鍚�" />
+ </el-form-item>
+
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿">
+ <el-date-picker v-model="formCreateTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-form-item label="澶囨敞">
+ <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import { ref, reactive, computed } from 'vue'
+import dayjs from 'dayjs'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+const loading = ref(false)
+const dialogVisible = ref(false)
+const dialogType = ref('add')
+const selectedRows = ref([])
+
+const searchForm = reactive({
+ inspectionNo: '',
+ status: ''
+})
+
+const formData = reactive({
+ arrivalNo: '',
+ supplierName: '',
+ products: [],
+ inspector: '',
+ remark: '',
+ createTime: ''
+})
+const formCreateTimeDate = computed({
+ get: () => (formData.createTime ? String(formData.createTime).split(' ')[0] : ''),
+ set: (value) => {
+ formData.createTime = value ? `${value} ${dayjs().format('HH:mm:ss')}` : ''
+ }
+})
+
+const mockData = [
+ {
+ id: 1,
+ inspectionNo: 'QI20241201001',
+ arrivalNo: 'AR20241201001',
+ supplierName: '渚涘簲鍟咥',
+ status: 'completed',
+ qualifiedQuantity: 240,
+ unqualifiedQuantity: 10,
+ inspectionTime: '2025-12-01 16:30:00',
+ inspector: '闄堝織寮�',
+ remark: '璐ㄦ瀹屾垚'
+ }
+]
+
+const tableData = ref([...mockData])
+
+const getStatusType = (status) => {
+ const statusMap = { pending: 'info', inspecting: 'warning', completed: 'success' }
+ return statusMap[status] || 'info'
+}
+
+const getStatusText = (status) => {
+ const statusMap = { pending: '寰呰川妫�', inspecting: '璐ㄦ涓�', completed: '宸插畬鎴�' }
+ return statusMap[status] || '鏈煡'
+}
+
+const handleSearch = () => {
+ loading.value = true
+ setTimeout(() => { loading.value = false }, 500)
+}
+
+const resetSearch = () => {
+ Object.assign(searchForm, { inspectionNo: '', status: '' })
+}
+
+const openDialog = (type, row = {}) => {
+ dialogType.value = type
+ if (type === 'edit' && row.id) {
+ Object.assign(formData, {
+ arrivalNo: row.arrivalNo,
+ supplierName: row.supplierName,
+ inspector: row.inspector,
+ remark: row.remark,
+ createTime: row.createTime || ''
+ })
+ } else {
+ Object.assign(formData, {
+ arrivalNo: '',
+ supplierName: '',
+ products: [],
+ inspector: '',
+ remark: '',
+ createTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
+ })
+ }
+ dialogVisible.value = true
+}
+
+const handleSubmit = () => {
+ if (!formData.products || formData.products.length === 0) {
+ ElMessage.error('璇疯嚦灏戞坊鍔犱竴鏉¤川妫�鍟嗗搧')
+ return
+ }
+
+ for (let i = 0; i < formData.products.length; i++) {
+ const product = formData.products[i]
+ if (product.qualifiedQuantity === null || product.qualifiedQuantity === undefined) {
+ ElMessage.error(`绗�${i + 1}鏉″晢鍝佺殑鍚堟牸鏁伴噺涓嶈兘涓虹┖`)
+ return
+ }
+ if (product.unqualifiedQuantity === null || product.unqualifiedQuantity === undefined) {
+ ElMessage.error(`绗�${i + 1}鏉″晢鍝佺殑涓嶅悎鏍兼暟閲忎笉鑳戒负绌篳)
+ return
+ }
+ }
+
+ const totalQualified = formData.products.reduce((sum, p) => sum + (p.qualifiedQuantity || 0), 0)
+ const totalUnqualified = formData.products.reduce((sum, p) => sum + (p.unqualifiedQuantity || 0), 0)
+
+ if (dialogType.value === 'add') {
+ const newInspection = {
+ id: Date.now(),
+ inspectionNo: '',
+ arrivalNo: formData.arrivalNo,
+ supplierName: formData.supplierName,
+ status: 'pending',
+ qualifiedQuantity: totalQualified,
+ unqualifiedQuantity: totalUnqualified,
+ inspectionTime: formData.createTime,
+ inspector: formData.inspector,
+ remark: formData.remark
+ }
+ tableData.value.unshift(newInspection)
+ ElMessage.success('鏂板鎴愬姛')
+ }
+ dialogVisible.value = false
+}
+
+const handleComplete = (row) => {
+ row.status = 'completed'
+ ElMessage.success('璐ㄦ瀹屾垚')
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖鏉¤褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ const index = tableData.value.findIndex(item => item.id === row.id)
+ if (index !== -1) {
+ tableData.value.splice(index, 1)
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ }
+ })
+}
+
+const handleBatchComplete = () => {
+ ElMessage.success('鎵归噺瀹屾垚鎴愬姛')
+}
+
+const handleBatchDelete = () => {
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+}
+
+const handleSelectionChange = (rows) => {
+ selectedRows.value = rows
+}
+
+const addProduct = () => {
+ formData.products.push({
+ productName: '',
+ specification: '',
+ arrivalQuantity: 0,
+ qualifiedQuantity: 0,
+ unqualifiedQuantity: 0,
+ unqualifiedReason: ''
+ })
+}
+
+const removeProduct = (index) => {
+ formData.products.splice(index, 1)
+}
+</script>
+
+<style scoped>
+.app-container { padding: 20px; }
+.search-card { margin-bottom: 20px; }
+.table-card { margin-bottom: 20px; }
+.table-header { margin-bottom: 20px; }
+.product-list { border: 1px solid #dcdfe6; border-radius: 4px; padding: 15px; }
+.product-item { margin-bottom: 15px; padding: 10px; background-color: #f5f7fa; border-radius: 4px; }
+.add-product-btn { margin-top: 15px; text-align: center; }
+</style>
diff --git a/src/views/procurementManagement/returnManagement/index.vue b/src/views/procurementManagement/returnManagement/index.vue
new file mode 100644
index 0000000..d44b588
--- /dev/null
+++ b/src/views/procurementManagement/returnManagement/index.vue
@@ -0,0 +1,271 @@
+<template>
+ <div class="app-container">
+ <el-card class="search-card" shadow="never">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="閫�璐у崟鍙凤細" style="width: 300px;">
+ <el-input v-model="searchForm.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable />
+ </el-form-item>
+ <el-form-item label="閫�璐х被鍨嬶細" style="width: 300px;">
+ <el-select v-model="searchForm.returnType" placeholder="璇烽�夋嫨绫诲瀷" clearable>
+ <el-option label="閲囪喘閫�璐�" value="purchase" />
+ <el-option label="璐ㄦ閫�璐�" value="quality" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <el-card class="table-card" shadow="never">
+ <div class="table-header">
+ <el-button type="primary" @click="openDialog('add')">鏂板閫�璐у崟</el-button>
+ <el-button type="danger" @click="handleBatchDelete">鎵归噺鍒犻櫎</el-button>
+ </div>
+
+ <el-table :data="tableData" border v-loading="loading" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="閫�璐у崟鍙�" prop="returnNo" width="180" />
+ <el-table-column label="鍏宠仈鍗曞彿" prop="relatedNo" width="180" />
+ <el-table-column label="閫�璐х被鍨�" prop="returnType" width="100">
+ <template #default="{ row }">
+ <el-tag :type="row.returnType === 'purchase' ? 'danger' : 'warning'">
+ {{ getReturnTypeText(row.returnType) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="渚涘簲鍟嗗悕绉�" prop="supplierName" />
+ <el-table-column label="閫�璐х姸鎬�" prop="status" width="100">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" prop="createTime" width="180" />
+ <el-table-column label="鎿嶄綔" width="200" align="center">
+ <template #default="{ row }">
+ <el-button type="primary" link @click="openDialog('edit', row)">缂栬緫</el-button>
+ <el-button type="success" link @click="handleApprove(row)" v-if="row.status === 'pending'">瀹℃牳</el-button>
+ <el-button type="danger" link @click="handleDelete(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.current"
+ :limit="pagination.size"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <FormDialog v-model="dialogVisible" :title="dialogType === 'add' ? '鏂板閫�璐у崟' : '缂栬緫閫�璐у崟'" :width="'600px'" :operation-type="dialogType" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="formData" label-width="120px">
+ <el-form-item label="閫�璐х被鍨�">
+ <el-select v-model="formData.returnType" placeholder="璇烽�夋嫨閫�璐х被鍨�" style="width: 100%">
+ <el-option label="閲囪喘閫�璐�" value="purchase" />
+ <el-option label="璐ㄦ閫�璐�" value="quality" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍏宠仈鍗曞彿">
+ <el-select v-model="formData.relatedNo" placeholder="璇烽�夋嫨鍏宠仈鍗曞彿" style="width: 100%">
+ <el-option v-for="item in onList" :key="item.arrivalNo" :label="item.arrivalNo" :value="item.arrivalNo" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟嗗悕绉�">
+ <el-input v-model="formData.supplierName" placeholder="璇疯緭鍏ヤ緵搴斿晢鍚嶇О" />
+ </el-form-item>
+ <el-form-item label="閫�璐у師鍥�">
+ <el-select v-model="formData.returnReason" placeholder="璇烽�夋嫨閫�璐у師鍥�" style="width: 100%">
+ <el-option label="璐ㄩ噺闂" value="quality" />
+ <el-option label="瑙勬牸涓嶇" value="specification" />
+ <el-option label="鏁伴噺閿欒" value="quantity" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞">
+ <el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import { ref, reactive,onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import Pagination from '@/components/PIMTable/Pagination.vue'
+import {listPage,add,update,del} from "@/api/procurementManagement/returnManagement.js"
+import {listPageCopy} from "@/api/procurementManagement/arrivalManagement.js"
+
+onMounted(() => {
+ getList()
+ list()
+})
+const onList = ref([])
+const list = () =>{
+ listPageCopy({current:-1}).then(res=>{
+ if(res.code === 200){
+ onList.value = res.data.records
+ }
+ })
+}
+const tableData = ref([])
+const getList = () => {
+ loading.value = true
+ listPage({...searchForm,...pagination}).then(res =>{
+ if(res.code === 200){
+ tableData.value = res.data.records
+ console.log(tableData.value)
+ total.value = res.data.total
+ loading.value = false
+ }
+ })
+}
+
+const loading = ref(false)
+const dialogVisible = ref(false)
+const dialogType = ref('add')
+const selectedRows = ref([])
+
+
+const pagination = reactive({
+ current: 1,
+ size: 10
+})
+
+const total = ref(0)
+
+const searchForm = reactive({
+ returnNo: '',
+ returnType: ''
+})
+
+const formData = reactive({
+ returnType: '',
+ relatedNo: '',
+ supplierName: '',
+ returnReason: '',
+ remark: '',
+ status: ''
+})
+
+const getReturnTypeText = (type) => {
+ const typeMap = { purchase: '閲囪喘閫�璐�', quality: '璐ㄦ閫�璐�' }
+ return typeMap[type] || '鏈煡'
+}
+
+const getStatusType = (status) => {
+ const statusMap = { pending: 'warning', approved: 'success', returned: 'info' }
+ return statusMap[status] || 'info'
+}
+
+const getStatusText = (status) => {
+ const statusMap = { pending: '寰呭鏍�', approved: '宸插鏍�', returned: '宸查��璐�' }
+ return statusMap[status] || '鏈煡'
+}
+
+const handleSearch = () => {
+ loading.value = true
+ getList()
+}
+
+const resetSearch = () => {
+ Object.assign(searchForm, { returnNo: '', returnType: '' })
+}
+
+const openDialog = (type, row = {}) => {
+ dialogType.value = type
+ obj.id = row.id
+ if (type === 'edit' && row.id) {
+ Object.assign(formData, {
+ returnType: row.returnType,
+ relatedNo: row.relatedNo,
+ supplierName: row.supplierName,
+ returnReason: row.returnReason,
+ remark: row.remark,
+ status: row.status
+ })
+ } else {
+ Object.assign(formData, {
+ returnType: '',
+ relatedNo: '',
+ supplierName: '',
+ returnReason: '',
+ remark: '',
+ status: 'pending'
+ })
+ }
+ dialogVisible.value = true
+}
+const obj = reactive({
+ id: ''
+})
+const handleSubmit = () => {
+ if (dialogType.value === 'add') {
+ formData.status = 'pending'
+ add(formData).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鏂板鎴愬姛')
+ getList()
+ }
+ })
+ }else{
+ update({...formData,...obj}).then(res => {
+ if(res.code === 200){
+ ElMessage.success('淇敼鎴愬姛')
+ getList()
+ }
+ })
+ }
+ dialogVisible.value = false
+}
+
+const handleApprove = (row) => {
+ row.status = 'approved'
+ update(row).then(res => {
+ if(res.code === 200){
+ ElMessage.success('瀹℃牳鎴愬姛')
+ getList()
+ }
+ })
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ繖鏉¤褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = [row.id]
+ del(ids).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getList()
+ }
+ })
+ })
+}
+
+const handleBatchDelete = () => {
+ let ids = selectedRows.value.map(item => item.id)
+ del(ids).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+ getList()
+ }
+ })
+}
+
+const handleSelectionChange = (rows) => {
+ selectedRows.value = rows
+}
+</script>
+
+<style scoped>
+.app-container { padding: 20px; }
+.search-card { margin-bottom: 20px; }
+.table-card { margin-bottom: 20px; }
+.table-header { margin-bottom: 20px; }
+</style>
diff --git a/src/views/procurementManagement/thePaymentLedger/index.vue b/src/views/procurementManagement/thePaymentLedger/index.vue
new file mode 100644
index 0000000..4031ea7
--- /dev/null
+++ b/src/views/procurementManagement/thePaymentLedger/index.vue
@@ -0,0 +1,104 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">渚涘簲鍟嗗悕绉�/鍚堝悓鍙凤細</span>
+ <el-input
+ v-model="searchForm.supplierNameOrContractNo"
+ style="width: 240px"
+ placeholder="杈撳叆渚涘簲鍟嗗悕绉�/鍚堝悓鍙锋悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px"
+ >鎼滅储</el-button
+ >
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="total"
+ ></PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { Search } from "@element-plus/icons-vue";
+import dayjs from "dayjs";
+// import { registrationList } from "@/api/procurementManagement/paymentLedger.js";
+const tableColumn = ref([
+ {
+ label: "浠樻鏃ユ湡",
+ prop: "paymentDate",
+ },
+ {
+ label: "渚涘簲鍟嗗悕绉�",
+ prop: "supplierName",
+ },
+ {
+ label: "浠樻閲戦",
+ prop: "currentPaymentAmount",
+ },
+ {
+ label: "鐧昏浜�",
+ prop: "registrant",
+ },
+ {
+ label: "鐧昏鏃ユ湡",
+ prop: "registrationtDate",
+ formatData: cell =>
+ cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+const purchaseLedgerList = ref([]);
+const invoiceNumberList = ref([]);
+const page = reactive({
+ current: 1,
+ size: 10,
+});
+const total = ref(0);
+
+// 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+const operationType = ref("");
+const dialogFormVisible = ref(false);
+const data = reactive({
+ searchForm: {
+ supplierNameOrContractNo: "",
+ },
+});
+const { searchForm, form, rules } = toRefs(data);
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = ({ current, limit }) => {
+ page.current = current;
+ page.size = limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ // registrationList({ ...searchForm.value, ...page }).then((res) => {
+ // tableLoading.value = false;
+ // tableData.value = res.rows;
+ // total.value = res.total;
+ // });
+};
+getList();
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/procurementManagement/transferManagement/index.vue b/src/views/procurementManagement/transferManagement/index.vue
new file mode 100644
index 0000000..3646449
--- /dev/null
+++ b/src/views/procurementManagement/transferManagement/index.vue
@@ -0,0 +1,431 @@
+<template>
+ <div class="app-container">
+ <!-- 鎼滅储杩囨护鍖� -->
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="閲囪喘鍚堝悓鍙�">
+ <el-input v-model="searchForm.purchaseContractNumber" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ <el-form-item label="渚涘簲鍟�">
+ <el-input v-model="searchForm.supplierName" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="search">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 琛ㄦ牸灞曠ず鍖� -->
+ <el-table :data="orderList" border v-loading="loading" height="calc(100vh - 12em)">
+ <!-- 娣诲姞搴忓彿鍒� -->
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column prop="purchaseContractNumber" label="閲囪喘鍚堝悓鍙�" show-overflow-tooltip />
+ <el-table-column prop="supplierName" label="渚涘簲鍟�" show-overflow-tooltip />
+ <el-table-column label="浠樻鐘舵��">
+ <template #default="scope">
+ <el-tag
+ :type="getPaymentStatusType(scope.row.paymentStatus)"
+ size="small"
+ >
+ {{ getPaymentStatusText(scope.row.paymentStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏀惰揣鐘舵��">
+ <template #default="scope">
+ <el-tag
+ :type="getReceiptStatusType(scope.row.receiptStatus)"
+ size="small"
+ >
+ {{ getReceiptStatusText(scope.row.receiptStatus) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="receivedQuantity" label="宸叉敹璐ф暟閲�"/>
+ <el-table-column prop="unreceivedQuantity" label="鏈敹璐ф暟閲�"/>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right" align="center">
+ <template #default="scope">
+ <el-button
+ type="primary"
+ size="small"
+ @click="confirmReceipter(scope.row)"
+ >
+ 纭鏀惰揣
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍦ㄨ〃鏍间笅鏂规坊鍔犲垎椤� -->
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ <!-- 纭鏀惰揣瀵硅瘽妗� -->
+ <FormDialog v-model="receiptDialogVisible" title="纭鏀惰揣" :width="'70%'" @close="receiptDialogVisible = false" @confirm="submitReceipt" @cancel="receiptDialogVisible = false">
+ <el-form :model="receiptForm" label-width="120px" ref="formRef">
+ <el-form-item label="閲囪喘鍚堝悓鍙�">
+ <el-input v-model="receiptForm.purchaseContractNumber" disabled />
+ </el-form-item>
+ <el-form-item label="寮傚父鍘熷洜">
+ <el-input
+ v-model="receiptForm.exceptionReason"
+ type="textarea"
+ placeholder="璇疯緭鍏ュ紓甯稿師鍥狅紙涓嶅悎鏍兼椂濉啓锛�"
+ />
+ </el-form-item>
+ <el-table
+ :data="productList"
+ border
+ v-loading="loadingProducts"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column
+ align="center"
+ label="搴忓彿"
+ type="index"
+ width="60"
+ />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+ <el-table-column label="鍗曚綅" prop="unit" width="70" />
+ <el-table-column label="渚涘簲鍟�" prop="supplierName" width="100" />
+ <el-table-column label="閲囪喘鏁伴噺" prop="quantity" width="100" />
+ <el-table-column label="寰呭叆搴撴暟閲�" prop="quantity0" width="100" />
+ <el-table-column label="鏈鍏ュ簱鏁伴噺" prop="quantityStock" width="150">
+ <template #default="scope">
+ <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="scope.row.quantityStock" />
+ </template>
+ </el-table-column>
+<!-- 鍚堟牸鎴栦笉鍚堟牸-->
+ <el-table-column label="鏄惁鍚堟牸" width="100">
+ <template #default="scope">
+ <el-select v-model="scope.row.isQualified" placeholder="璇烽�夋嫨">
+ <el-option label="鍚堟牸" value="1" />
+ <el-option label="涓嶅悎鏍�" value="2" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="绋庣巼(%)" prop="taxRate" width="120" />
+ <el-table-column
+ label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="formattedNumber"
+ width="150"
+ />
+ <el-table-column
+ label="鍚◣鎬讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150"
+ />
+ <el-table-column
+ label="涓嶅惈绋庢�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ :formatter="formattedNumber"
+ width="150"
+ />
+ </el-table>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import FormDialog from '@/components/Dialog/FormDialog.vue';
+import {ref, onMounted, getCurrentInstance} from 'vue'
+import {
+ getPurchaseOrders,
+ confirmReceipt,
+ addPurchaseException
+} from '@/api/procurementManagement/transferManagement.js'
+import {selectProductRecordListByPuechaserId, addSutockIn, updateStockIn} from "@/api/inventoryManagement/stockIn.js";
+import useUserStore from "@/store/modules/user.js";
+
+
+const userStore = useUserStore()
+const { proxy } = getCurrentInstance()
+
+// 鏁版嵁瀹氫箟
+const orderList = ref([])
+const receiptDialogVisible = ref(false)
+const receiptForm = ref({
+ purchaseContractNumber: '',
+ exceptionReason: '',
+ purchaseLedgerId: '',
+})
+const operationType = ref('')// 鎿嶄綔绫诲瀷: 'add' 鎴� 'edit'
+const productList = ref([]);// 浜у搧鍒楄〃鏁版嵁
+const loadingProducts = ref(false);// 浜у搧鍔犺浇鐘舵��
+const selectedRows = ref([]);
+const loading = ref(false);
+const total = ref(0); // 鎬昏褰曟暟
+// 鎼滅储琛ㄥ崟
+const searchForm = ref({
+ purchaseContractNumber: '',
+ supplierName: '',
+})
+// 鍒嗛〉鏁版嵁
+const page = reactive({
+ current: 1,
+ size: 100, // 姣忛〉鏄剧ず鏁伴噺
+});
+
+// 鍒嗛〉鍙樺寲澶勭悊
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getReceiptOrders(); // 閲嶆柊鑾峰彇鏁版嵁
+};
+// 鑾峰彇璁㈠崟鍒楄〃
+const getReceiptOrders = async () => {
+ loading.value = true;
+ try {
+ const response = await getPurchaseOrders({
+ ...searchForm.value,
+ current: page.current,
+ size: page.size
+ });
+ // 浣跨敤 Promise.all 澶勭悊鎵�鏈夊紓姝ヨ姹�
+ const processedOrders = await Promise.all(response.data.records.map(async (order) => {
+ // 绛夊緟寮傛鑾峰彇浜у搧璁板綍
+ const productRes = await selectProductRecordListByPuechaserId({
+ purchaseContractNumber: order.purchaseContractNumber
+ });
+
+ // 纭繚 productRes.data 瀛樺湪
+ if (productRes && productRes.data && Array.isArray(productRes.data)) {
+ // 璁$畻鎬绘暟閲�
+ order.totalQuantity = productRes.data.reduce((acc, cur) => acc + (cur.quantity || 0), 0);
+ // 璁$畻鏈敹璐ф暟閲�
+ order.unreceivedQuantity = productRes.data.reduce((acc, cur) => acc + (cur.quantity0 || 0), 0);
+ // 璁$畻宸叉敹璐ф暟閲�
+ order.receivedQuantity = order.totalQuantity - order.unreceivedQuantity;
+
+ // 淇鐘舵�佸垽鏂�昏緫锛堜娇鐢� === 杩涜姣旇緝锛�
+ if (order.unreceivedQuantity === 0) {
+ order.paymentStatus = 1;
+ order.receiptStatus = 1;
+ } else if (order.receivedQuantity === 0) {
+ order.paymentStatus = 3;
+ order.receiptStatus = 3;
+ } else {
+ order.paymentStatus = 2;
+ order.receiptStatus = 2;
+ }
+ } else {
+ // 濡傛灉娌℃湁浜у搧璁板綍锛岃缃粯璁ゅ��
+ order.totalQuantity = 0;
+ order.unreceivedQuantity = 0;
+ order.receivedQuantity = 0;
+ order.receiptStatus = 3; // 鏈叆搴�
+ }
+
+ return order;
+ }));
+
+ // 姝g‘璧嬪�肩粰 orderList
+ orderList.value = processedOrders;
+ total.value = response.data.total;
+ } catch (error) {
+ console.error('鑾峰彇璁㈠崟鍒楄〃澶辫触:', error);
+ proxy.$modal.msgError('鑾峰彇璁㈠崟鍒楄〃澶辫触');
+ } finally {
+ loading.value = false;
+ }
+}
+
+// 浠樻鐘舵�佹樉绀哄鐞�
+const getPaymentStatusText = (status) => {
+ const statusMap = { '1': '宸蹭粯娆�', '2': '閮ㄥ垎浠樻', '3': '鏈粯娆�' }
+ return statusMap[status] || '鏈煡'
+}
+
+const getPaymentStatusType = (status) => {
+ const typeMap = { '1': 'success', '2': 'warning', '3': 'danger' }
+ return typeMap[status] || 'info'
+}
+
+// 鏀惰揣鐘舵�佸鐞�
+const getReceiptStatusText = (status) => {
+ const statusMap = { '1': '鏀惰揣瀹屾垚', '2': '閮ㄥ垎鍏ュ簱', '3': '鏈叆搴�' }
+ return statusMap[status] || '鏈煡'
+}
+
+const getReceiptStatusType = (status) => {
+ const typeMap = { '1': 'success', '2': 'warning', '3': 'info' }
+ return typeMap[status] || 'info'
+}
+const exceedsAddLimit = (product) => {
+ const stock = Number(product?.quantityStock ?? 0);
+ const waiting = Number(product?.quantity0 ?? 0);
+ if (!Number.isFinite(stock) || !Number.isFinite(waiting)) {
+ return false;
+ }
+ return stock > waiting;
+};
+const exceedsEditLimit = (product) => {
+ const stock = Number(product?.quantityStock ?? 0);
+ const waiting = Number(product?.quantity0 ?? 0);
+ const original = Number(product?.originalQuantityStock ?? 0);
+ if (!Number.isFinite(stock) || !Number.isFinite(waiting) || !Number.isFinite(original)) {
+ return false;
+ }
+ return stock > waiting + original;
+};
+const updatePro = async () => {
+ const target = selectedRows.value[0];
+ const stock = Number(target?.quantityStock ?? 0);
+ if (!Number.isFinite(stock) || stock <= 0) {
+ proxy.$modal.msgWarning('璇峰~鍐欐湁鏁堢殑鍏ュ簱鏁伴噺');
+ return;
+ }
+ if (exceedsEditLimit(target)) {
+ proxy.$modal.msgError('鏈鍏ュ簱鏁伴噺涓嶈兘瓒呰繃鍘熷叆搴撴暟閲忎笌寰呭叆搴撴暟閲忎箣鍜�');
+ return;
+ }
+ const stockInData = {
+ id: selectedRows.value[0].recordId,
+ quantityStock: Number(selectedRows.value[0].quantityStock),// 浣跨敤鏂版牸寮忓寲鍑芥暟
+ };
+ await updateStockIn(stockInData)
+ proxy.$modal.msgSuccess('淇敼鍏ュ簱鎴愬姛')
+ closeDia()
+ getReceiptOrders() // 鍒锋柊鍒楄〃
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter(item => item.id);
+}
+// 鎵撳紑寮规-纭鏀惰揣
+const confirmReceipter = (row) => {
+ receiptForm.value = {
+ purchaseContractNumber: row.purchaseContractNumber,
+ purchaseLedgerId: row.id,
+ exceptionReason: ''
+ }
+ selectedRows.value = []
+ receiptDialogVisible.value = true
+ fetchProductsByContract()
+}
+
+const fetchProductsByContract = async () =>
+{
+ try {
+ loadingProducts.value = true
+ // 鏍规嵁鍚堝悓鏌ヨ浜у搧璁板綍
+ const productRes = await selectProductRecordListByPuechaserId({
+ purchaseContractNumber: receiptForm.value.purchaseContractNumber
+ });
+ console.log('productRes:', productRes)
+ operationType.value = 'add'
+ if (!productRes.data || productRes.data.length === 0) {
+ proxy.$modal.msgWarning('璇ュ悎鍚屼笅娌℃湁浜у搧璁板綍')
+ productList.value = [];
+ return
+ }
+ // 澶勭悊浜у搧鏁版嵁锛屾坊鍔犳湰娆″叆搴撴暟閲忓瓧娈�
+ productList.value = productRes.data.map(item => ({
+ ...item,
+ quantityStock: 0,
+ originalQuantityStock: Number(item.quantityStock ?? item.inboundQuantity ?? 0),
+ }))
+ selectedRows.value = productList.value
+ } catch (error) {
+ console.error('鏌ヨ浜у搧璁板綍澶辫触:', error)
+ proxy.$modal.msgError('鏌ヨ浜у搧璁板綍澶辫触')
+ productList.value = [];
+ } finally {
+ loadingProducts.value = false
+ }
+}
+
+
+// 鎻愪氦鏀惰揣纭
+const submitReceipt = async () => {
+ if(operationType.value !== 'add'){
+ await updatePro()
+ return
+ }
+ try {
+ await proxy.$refs.formRef.validate()
+ // 楠岃瘉鍏ュ簱鏁伴噺
+ const invalidProducts = selectedRows.value.filter((product) => {
+ const stock = Number(product?.quantityStock ?? 0);
+ if (!Number.isFinite(stock) || stock <= 0) {
+ return true;
+ }
+ return exceedsAddLimit(product);
+ })
+
+ if (invalidProducts.length > 0) {
+ proxy.$modal.msgError('鏈鍏ュ簱鏁伴噺闇�澶т簬0锛屼笖涓嶈兘瓒呰繃寰呭叆搴撴暟閲�')
+ return
+ }
+ loading.value = true
+ // 鍑嗗鎻愪氦鏁版嵁 - 淇敼涓哄悗绔渶瑕佺殑鏍煎紡
+ const stockInData = {
+ // 鍏ュ簱鍗曞熀鏈俊鎭�
+ ...receiptForm.value,
+ nickName: userStore.nickName,
+ details: selectedRows.value.map(product => ({
+ id: product.id,
+ inboundQuantity: Number(product.quantityStock)
+ })),
+ };
+ //濡傛灉浜у搧鍚堟牸
+ if(productList.value.every(product => product.isQualified === '1')){
+ await addSutockIn(stockInData)
+
+ proxy.$modal.msgSuccess('纭鏀惰揣,鍏ュ簱鎴愬姛')
+ }else{
+ stockInData.details.forEach(item => {
+ const ProcurementExceptionRecord = {
+ purchaseContractNumber: receiptForm.value.purchaseContractNumber,
+ purchaseLedgerId: receiptForm.value.purchaseLedgerId,
+ exceptionNum: item.inboundQuantity,
+ exceptionReason: receiptForm.value.exceptionReason
+ }
+ addPurchaseException(ProcurementExceptionRecord).then(response => {
+ proxy.$modal.msgSuccess('浜у搧涓嶅悎鏍硷紝閲囪喘寮傚父璁板綍鎴愬姛')
+ })
+ })
+ }
+ closeDia()
+ getReceiptOrders() // 鍒锋柊鍒楄〃
+
+ } catch (error) {
+ console.error('鎻愪氦澶辫触:', error)
+ if (!error.errors) {
+ proxy.$modal.msgError('鎿嶄綔澶辫触锛岃閲嶈瘯')
+ }
+ } finally {
+ loading.value = false
+ }
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.$refs.formRef.resetFields()
+ receiptDialogVisible.value = false
+}
+// 鎼滅储鍜岄噸缃�
+const search = () => {
+ getReceiptOrders()
+}
+
+const resetSearch = () => {
+ searchForm.value = {
+ purchaseContractNumber: '',
+ supplierName: '',
+ }
+ getReceiptOrders()
+}
+
+onMounted(() => {
+ getReceiptOrders()
+})
+</script>
diff --git a/src/views/productManagement/productIdentifier/index.vue b/src/views/productManagement/productIdentifier/index.vue
new file mode 100644
index 0000000..d638e7a
--- /dev/null
+++ b/src/views/productManagement/productIdentifier/index.vue
@@ -0,0 +1,834 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20"
+ class="search-row">
+ <el-col :span="6">
+ <el-input v-model="searchForm.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @keyup.enter="handleSearch">
+ <template #prefix>
+ <el-icon>
+ <Search />
+ </el-icon>
+ </template>
+ </el-input>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.identifierType"
+ placeholder="璇烽�夋嫨鏍囪瘑绫诲瀷"
+ clearable>
+ <el-option label="浜岀淮鐮�"
+ value="浜岀淮鐮�"></el-option>
+ <el-option label="闃蹭吉鐮�"
+ value="闃蹭吉鐮�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.status"
+ placeholder="璇烽�夋嫨鐘舵��"
+ clearable>
+ <el-option label="宸茬敓鎴�"
+ value="宸茬敓鎴�"></el-option>
+ <el-option label="宸插垎閰�"
+ value="宸插垎閰�"></el-option>
+ <el-option label="宸蹭娇鐢�"
+ value="宸蹭娇鐢�"></el-option>
+ <el-option label="宸蹭綔搴�"
+ value="宸蹭綔搴�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary"
+ @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ <el-button style="float: right;"
+ type="primary"
+ @click="handleAdd">
+ 鏂板鏍囪瘑
+ </el-button>
+ </el-col>
+ </el-row>
+ <!-- 浜у搧鏍囪瘑鍒楄〃 -->
+ <el-table :data="filteredList"
+ style="width: 100%"
+ v-loading="loading"
+ border
+ stripe
+ height="calc(100vh - 22em)">
+ <el-table-column prop="id"
+ label="ID"
+ width="80"
+ align="center" />
+ <el-table-column prop="productName"
+ label="浜у搧鍚嶇О"
+ width="150" />
+ <el-table-column prop="productCode"
+ label="浜у搧缂栫爜"
+ width="120" />
+ <el-table-column prop="batchNo"
+ label="鎵规鍙�"
+ width="120" />
+ <el-table-column prop="identifierType"
+ label="鏍囪瘑绫诲瀷"
+ width="100">
+ <template #default="scope">
+ <el-tag :type="getIdentifierTypeType(scope.row.identifierType)">
+ {{ scope.row.identifierType }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="identifierCode"
+ label="鏍囪瘑鐮�" />
+ <el-table-column prop="status"
+ label="鐘舵��"
+ width="100">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="generateTime"
+ label="鐢熸垚鏃堕棿"
+ width="160" />
+ <el-table-column label="鎿嶄綔"
+ fixed="right"
+ align="center"
+ width="280">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ @click="handleView(scope.row)" style="color: #67C23A">鏌ョ湅</el-button>
+ <el-button link
+ type="primary"
+ @click="handleEdit(scope.row)">缂栬緫</el-button>
+ <el-button link
+ type="success"
+ @click="generateQRCode(scope.row)">鐢熸垚浜岀淮鐮�</el-button>
+ <el-button link
+ type="primary"
+ @click="handleExport(scope.row)">瀵煎嚭</el-button>
+ <el-button link
+ type="primary"
+ @click="handleReassign(scope.row)"
+ v-if="scope.row.status === '宸插垎閰�'">閲嶆柊鍒嗛厤</el-button>
+ <el-button link
+ type="danger"
+ @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉 -->
+ <pagination :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.currentPage"
+ :limit="pagination.pageSize"
+ @pagination="handleCurrentChange" />
+ </el-card>
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="700px">
+ <el-form :model="form"
+ :rules="rules"
+ ref="formRef"
+ label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productName">
+ <el-input v-model="form.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧缂栫爜"
+ prop="productCode">
+ <el-input v-model="form.productCode"
+ placeholder="璇疯緭鍏ヤ骇鍝佺紪鐮�"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎵规鍙�"
+ prop="batchNo">
+ <el-input v-model="form.batchNo"
+ placeholder="璇疯緭鍏ユ壒娆″彿"></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏍囪瘑绫诲瀷"
+ prop="identifierType">
+ <el-select v-model="form.identifierType"
+ placeholder="璇烽�夋嫨鏍囪瘑绫诲瀷"
+ style="width: 100%">
+ <el-option label="浜岀淮鐮�"
+ value="浜岀淮鐮�"></el-option>
+ <el-option label="闃蹭吉鐮�"
+ value="闃蹭吉鐮�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐢熸垚鏁伴噺"
+ prop="quantity">
+ <el-input-number v-model="form.quantity"
+ :min="1"
+ :max="10000"
+ style="width: 100%"></el-input-number>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��"
+ prop="status">
+ <el-select v-model="form.status"
+ placeholder="璇烽�夋嫨鐘舵��"
+ style="width: 100%">
+ <el-option label="宸茬敓鎴�"
+ value="宸茬敓鎴�"></el-option>
+ <el-option label="宸插垎閰�"
+ value="宸插垎閰�"></el-option>
+ <el-option label="宸蹭娇鐢�"
+ value="宸蹭娇鐢�"></el-option>
+ <el-option label="宸蹭綔搴�"
+ value="宸蹭綔搴�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input type="textarea"
+ v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�"
+ rows="3"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="handleSubmit">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 鏍囪瘑鐢熸垚瀵硅瘽妗� -->
+ <el-dialog v-model="generateDialogVisible"
+ title="鏍囪瘑鐢熸垚"
+ width="500px">
+ <el-form label-width="100px">
+ <el-form-item label="浜у搧鍚嶇О">
+ <span>{{ currentProduct.productName }}</span>
+ </el-form-item>
+ <el-form-item label="浜у搧缂栫爜">
+ <span>{{ currentProduct.productCode }}</span>
+ </el-form-item>
+ <el-form-item label="鎵规鍙�">
+ <span>{{ currentProduct.batchNo }}</span>
+ </el-form-item>
+ <el-form-item label="鏍囪瘑绫诲瀷">
+ <span>{{ currentProduct.identifierType }}</span>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿">
+ <el-date-picker v-model="createTimeDate"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"></el-date-picker>
+ </el-form-item>
+ <el-form-item label="鐢熸垚鏁伴噺"
+ prop="generateQuantity">
+ <el-input-number v-model="generateQuantity"
+ :min="1"
+ :max="10000"
+ style="width: 100%"></el-input-number>
+ </el-form-item>
+ <el-form-item label="缂栫爜瑙勫垯"
+ prop="codeRule">
+ <el-select v-model="codeRule"
+ placeholder="璇烽�夋嫨缂栫爜瑙勫垯"
+ style="width: 100%">
+ <el-option label="浜у搧缂栫爜+鎵规鍙�+搴忓彿"
+ value="浜у搧缂栫爜+鎵规鍙�+搴忓彿"></el-option>
+ <el-option label="鏃堕棿鎴�+闅忔満鏁�"
+ value="鏃堕棿鎴�+闅忔満鏁�"></el-option>
+ <el-option label="鑷畾涔夎鍒�"
+ value="鑷畾涔夎鍒�"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鑷畾涔夊墠缂�"
+ prop="customPrefix"
+ v-if="codeRule === '鑷畾涔夎鍒�'">
+ <el-input v-model="customPrefix"
+ placeholder="璇疯緭鍏ヨ嚜瀹氫箟鍓嶇紑"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="generateIdentifiers">鐢� 鎴�</el-button>
+ <el-button @click="generateDialogVisible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 閲嶆柊鍒嗛厤瀵硅瘽妗� -->
+ <el-dialog v-model="reassignDialogVisible"
+ title="閲嶆柊鍒嗛厤鏍囪瘑"
+ width="500px">
+ <el-form label-width="100px">
+ <el-form-item label="浜у搧鍚嶇О">
+ <span>{{ currentProduct.productName }}</span>
+ </el-form-item>
+ <el-form-item label="鏍囪瘑鐮�">
+ <span>{{ currentProduct.identifierCode }}</span>
+ </el-form-item>
+ <el-form-item label="鏂版壒娆″彿"
+ prop="newBatchNo">
+ <el-input v-model="newBatchNo"
+ placeholder="璇疯緭鍏ユ柊鎵规鍙�"></el-input>
+ </el-form-item>
+ <el-form-item label="鍒嗛厤鍘熷洜"
+ prop="reassignReason">
+ <el-input type="textarea"
+ v-model="reassignReason"
+ rows="3"
+ placeholder="璇疯緭鍏ラ噸鏂板垎閰嶅師鍥�"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="saveReassign">纭� 瀹�</el-button>
+ <el-button @click="reassignDialogVisible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <!-- 浜岀淮鐮侀瑙堝璇濇 -->
+ <el-dialog v-model="qrCodeDialogVisible"
+ title="浜岀淮鐮侀瑙�"
+ width="500px"
+ center>
+ <div class="qr-preview-container">
+ <div v-if="qrCodeUrl"
+ class="qr-image-container">
+ <img :src="qrCodeUrl"
+ alt="浜岀淮鐮�"
+ class="qr-image" />
+ <div class="qr-info">
+ <p><strong>浜у搧鍚嶇О锛�</strong>{{ currentQRProduct.productName }}</p>
+ <p><strong>浜у搧缂栫爜锛�</strong>{{ currentQRProduct.productCode }}</p>
+ <p><strong>鎵规鍙凤細</strong>{{ currentQRProduct.batchNo }}</p>
+ <p><strong>鏍囪瘑鐮侊細</strong>{{ currentQRProduct.identifierCode }}</p>
+ <p><strong>鏍囪瘑绫诲瀷锛�</strong>{{ currentQRProduct.identifierType }}</p>
+ </div>
+ </div>
+ <div v-else
+ class="qr-loading">
+ <el-icon class="is-loading">
+ <Loading />
+ </el-icon>
+ <p>姝e湪鐢熸垚浜岀淮鐮�...</p>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="qrCodeDialogVisible = false">鍏抽棴</el-button>
+ <el-button v-if="qrCodeUrl"
+ type="primary"
+ @click="copyQRContent"
+ icon="CopyDocument">
+ 澶嶅埗鍐呭
+ </el-button>
+ <el-button v-if="qrCodeUrl"
+ type="success"
+ @click="downloadQRCode"
+ icon="Download">
+ 涓嬭浇浜岀淮鐮�
+ </el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed } from "vue";
+ import dayjs from "dayjs";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { Plus, Search, Loading, Download } from "@element-plus/icons-vue";
+ import Pagination from "@/components/PIMTable/Pagination.vue";
+ import QRCode from "qrcode";
+
+ // 鍝嶅簲寮忔暟鎹�
+ const loading = ref(false);
+ const searchForm = reactive({
+ productName: "",
+ identifierType: "",
+ status: "",
+ });
+
+ const identifierList = ref([
+ {
+ id: 1,
+ productName: "宸ヤ笟浼犳劅鍣ˋ鍨�",
+ productCode: "SENSOR001",
+ batchNo: "B202312001",
+ identifierType: "浜岀淮鐮�",
+ identifierCode: "QR_SENSOR001_B202312001_001",
+ status: "宸插垎閰�",
+ generateTime: "2023-12-01 10:00:00",
+ remark: "閲嶈浜у搧鏍囪瘑",
+ },
+ {
+ id: 2,
+ productName: "鎺у埗闈㈡澘B鍨�",
+ productCode: "PANEL002",
+ batchNo: "B202312002",
+ identifierType: "闃蹭吉鐮�",
+ identifierCode: "SEC_PANEL002_B202312002_001",
+ status: "宸茬敓鎴�",
+ generateTime: "2023-12-02 14:30:00",
+ remark: "甯歌浜у搧鏍囪瘑",
+ },
+ {
+ id: 3,
+ productName: "鏁版嵁閲囬泦鍣–鍨�",
+ productCode: "COLLECTOR003",
+ batchNo: "B202312003",
+ identifierType: "闃蹭吉鐮�",
+ identifierCode: "SEC_COLLECTOR003_B202312003_001",
+ status: "宸蹭娇鐢�",
+ generateTime: "2023-12-03 09:15:00",
+ remark: "娴嬭瘯浜у搧鏍囪瘑",
+ },
+ ]);
+
+ const pagination = reactive({
+ total: 3,
+ currentPage: 1,
+ pageSize: 10,
+ });
+
+ const dialogVisible = ref(false);
+ const dialogTitle = ref("鏂板鏍囪瘑");
+ const form = reactive({
+ productName: "",
+ productCode: "",
+ batchNo: "",
+ identifierType: "",
+ quantity: 1,
+ status: "宸茬敓鎴�",
+ remark: "",
+ });
+
+ const rules = {
+ productName: [{ required: true, message: "璇疯緭鍏ヤ骇鍝佸悕绉�", trigger: "blur" }],
+ productCode: [{ required: true, message: "璇疯緭鍏ヤ骇鍝佺紪鐮�", trigger: "blur" }],
+ batchNo: [{ required: true, message: "璇疯緭鍏ユ壒娆″彿", trigger: "blur" }],
+ identifierType: [
+ { required: true, message: "璇烽�夋嫨鏍囪瘑绫诲瀷", trigger: "change" },
+ ],
+ quantity: [{ required: true, message: "璇疯緭鍏ョ敓鎴愭暟閲�", trigger: "blur" }],
+ status: [{ required: true, message: "璇烽�夋嫨鐘舵��", trigger: "change" }],
+ };
+
+ const isEdit = ref(false);
+ const editId = ref(null);
+ const generateDialogVisible = ref(false);
+ const reassignDialogVisible = ref(false);
+ const currentProduct = ref({});
+ const generateQuantity = ref(1);
+ const codeRule = ref("");
+ const customPrefix = ref("");
+ const createTime = ref(dayjs().format("YYYY-MM-DD HH:mm:ss"));
+ const createTimeDate = computed({
+ get: () => (createTime.value ? String(createTime.value).split(" ")[0] : ""),
+ set: (value) => {
+ createTime.value = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
+ const newBatchNo = ref("");
+ const reassignReason = ref("");
+ const formRef = ref();
+
+ // 浜岀淮鐮佺浉鍏冲彉閲�
+ const qrCodeDialogVisible = ref(false);
+ const qrCodeUrl = ref("");
+ const currentQRProduct = ref({});
+
+ // 璁$畻灞炴��
+ const filteredList = computed(() => {
+ let list = identifierList.value;
+ if (searchForm.productName) {
+ list = list.filter(item =>
+ item.productName.includes(searchForm.productName)
+ );
+ }
+ if (searchForm.identifierType) {
+ list = list.filter(
+ item => item.identifierType === searchForm.identifierType
+ );
+ }
+ if (searchForm.status) {
+ list = list.filter(item => item.status === searchForm.status);
+ }
+ return list;
+ });
+
+ // 鏂规硶
+ const getIdentifierTypeType = type => {
+ const typeMap = {
+ 浜岀淮鐮�: "success",
+ 闃蹭吉鐮�: "warning",
+ };
+ return typeMap[type] || "info";
+ };
+
+ const getStatusType = status => {
+ const statusMap = {
+ 宸茬敓鎴�: "info",
+ 宸插垎閰�: "primary",
+ 宸蹭娇鐢�: "success",
+ 宸蹭綔搴�: "danger",
+ };
+ return statusMap[status] || "info";
+ };
+
+ const handleSearch = () => {
+ // 鎼滅储閫昏緫宸插湪computed涓鐞�
+ };
+
+ const resetSearch = () => {
+ searchForm.productName = "";
+ searchForm.identifierType = "";
+ searchForm.status = "";
+ };
+
+ const handleAdd = () => {
+ dialogTitle.value = "鏂板鏍囪瘑";
+ isEdit.value = false;
+ form.productName = "";
+ form.productCode = "";
+ form.batchNo = "";
+ form.identifierType = "";
+ form.quantity = 1;
+ form.status = "宸茬敓鎴�";
+ form.remark = "";
+ dialogVisible.value = true;
+ };
+
+ const handleView = row => {
+ // 鏌ョ湅鏍囪瘑璇︽儏
+ ElMessage.info("鏌ョ湅鏍囪瘑璇︽儏鍔熻兘寰呭疄鐜�");
+ };
+
+ const handleEdit = row => {
+ dialogTitle.value = "缂栬緫鏍囪瘑";
+ isEdit.value = true;
+ editId.value = row.id;
+ Object.assign(form, row);
+ dialogVisible.value = true;
+ };
+
+ const handleExport = row => {
+ // 瀵煎嚭鏍囪瘑
+ ElMessage.success(`宸插鍑烘爣璇�: ${row.identifierCode}`);
+ };
+
+ const handleReassign = row => {
+ currentProduct.value = row;
+ newBatchNo.value = "";
+ reassignReason.value = "";
+ reassignDialogVisible.value = true;
+ };
+
+ const handleDelete = row => {
+ ElMessageBox.confirm("纭鍒犻櫎璇ユ爣璇嗗悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ const index = identifierList.value.findIndex(item => item.id === row.id);
+ if (index > -1) {
+ identifierList.value.splice(index, 1);
+ pagination.total--;
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ }
+ });
+ };
+
+ // 鐢熸垚浜岀淮鐮�
+ const generateQRCode = async row => {
+ try {
+ // 妫�鏌ュ繀瑕佸瓧娈�
+ if (!row.productName || !row.productCode || !row.batchNo) {
+ ElMessage.warning("浜у搧淇℃伅涓嶅畬鏁达紝鏃犳硶鐢熸垚浜岀淮鐮�");
+ return;
+ }
+
+ currentQRProduct.value = row;
+ qrCodeUrl.value = "";
+ qrCodeDialogVisible.value = true;
+
+ // 鏋勫缓浜岀淮鐮佸唴瀹�
+ let qrContent = "";
+ if (row.identifierType === "浜岀淮鐮�") {
+ qrContent = `${row.productName}|${row.productCode}|${row.batchNo}|${row.identifierCode}`;
+ } else if (row.identifierType === "闃蹭吉鐮�") {
+ // 闃蹭吉鐮佹牸寮忥細SEC_浜у搧缂栫爜_鎵规鍙穇鏃堕棿鎴砡闅忔満鏁�
+ const timestamp = Date.now();
+ const random = Math.random().toString(36).substr(2, 8);
+ qrContent = `SEC_${row.productCode}_${row.batchNo}_${timestamp}_${random}`;
+ }
+
+ // 鐢熸垚浜岀淮鐮�
+ qrCodeUrl.value = await QRCode.toDataURL(qrContent, {
+ width: 256,
+ margin: 2,
+ color: {
+ dark: "#000000",
+ light: "#FFFFFF",
+ },
+ errorCorrectionLevel: row.identifierType === "闃蹭吉鐮�" ? "H" : "M",
+ });
+
+ ElMessage.success("浜岀淮鐮佺敓鎴愭垚鍔燂紒");
+ } catch (error) {
+ console.error("鐢熸垚浜岀淮鐮佸け璐�:", error);
+ ElMessage.error("鐢熸垚浜岀淮鐮佸け璐ワ細" + error.message);
+ qrCodeDialogVisible.value = false;
+ }
+ };
+
+ // 涓嬭浇浜岀淮鐮�
+ const downloadQRCode = () => {
+ if (!qrCodeUrl.value) {
+ ElMessage.warning("璇峰厛鐢熸垚浜岀淮鐮�");
+ return;
+ }
+
+ const a = document.createElement("a");
+ a.href = qrCodeUrl.value;
+ a.download = `${currentQRProduct.value.productName}_${
+ currentQRProduct.value.identifierType
+ }_${new Date().getTime()}.png`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ ElMessage.success("涓嬭浇鎴愬姛锛�");
+ };
+
+ // 澶嶅埗浜岀淮鐮佸唴瀹�
+ const copyQRContent = async () => {
+ if (!currentQRProduct.value) {
+ ElMessage.warning("娌℃湁鍙鍒剁殑鍐呭");
+ return;
+ }
+
+ try {
+ let content = "";
+ if (currentQRProduct.value.identifierType === "浜岀淮鐮�") {
+ content = `${currentQRProduct.value.productName}|${currentQRProduct.value.productCode}|${currentQRProduct.value.batchNo}|${currentQRProduct.value.identifierCode}`;
+ } else if (currentQRProduct.value.identifierType === "闃蹭吉鐮�") {
+ const timestamp = Date.now();
+ const random = Math.random().toString(36).substr(2, 8);
+ content = `SEC_${currentQRProduct.value.productCode}_${currentQRProduct.value.batchNo}_${timestamp}_${random}`;
+ }
+
+ await navigator.clipboard.writeText(content);
+ ElMessage.success("鍐呭宸插鍒跺埌鍓创鏉�");
+ } catch (error) {
+ // 闄嶇骇鏂规
+ const textArea = document.createElement("textarea");
+ textArea.value = content;
+ document.body.appendChild(textArea);
+ textArea.select();
+ document.execCommand("copy");
+ document.body.removeChild(textArea);
+ ElMessage.success("鍐呭宸插鍒跺埌鍓创鏉�");
+ }
+ };
+
+ const generateIdentifiers = () => {
+ if (!codeRule.value) {
+ ElMessage.warning("璇烽�夋嫨缂栫爜瑙勫垯");
+ return;
+ }
+
+ // 鐢熸垚鏍囪瘑鐨勯�昏緫
+ const newIdentifiers = [];
+ for (let i = 1; i <= generateQuantity.value; i++) {
+ let identifierCode = "";
+ if (codeRule.value === "浜у搧缂栫爜+鎵规鍙�+搴忓彿") {
+ identifierCode = `${currentProduct.value.productCode}_${
+ currentProduct.value.batchNo
+ }_${String(i).padStart(3, "0")}`;
+ } else if (codeRule.value === "鏃堕棿鎴�+闅忔満鏁�") {
+ identifierCode = "";
+ } else if (codeRule.value === "鑷畾涔夎鍒�") {
+ identifierCode = "";
+ }
+
+ newIdentifiers.push({
+ id: Math.max(...identifierList.value.map(item => item.id)) + i,
+ productName: currentProduct.value.productName,
+ productCode: currentProduct.value.productCode,
+ batchNo: currentProduct.value.batchNo,
+ identifierType: currentProduct.value.identifierType,
+ identifierCode: identifierCode,
+ status: "宸茬敓鎴�",
+ generateTime: createTime.value,
+ remark: "鎵归噺鐢熸垚",
+ });
+ }
+
+ identifierList.value.push(...newIdentifiers);
+ pagination.total += newIdentifiers.length;
+ ElMessage.success(`鎴愬姛鐢熸垚 ${newIdentifiers.length} 涓爣璇哷);
+ generateDialogVisible.value = false;
+ };
+
+ const saveReassign = () => {
+ if (!newBatchNo.value) {
+ ElMessage.warning("璇疯緭鍏ユ柊鎵规鍙�");
+ return;
+ }
+
+ const index = identifierList.value.findIndex(
+ item => item.id === currentProduct.value.id
+ );
+ if (index > -1) {
+ identifierList.value[index].batchNo = newBatchNo.value;
+ identifierList.value[index].status = "宸插垎閰�";
+ ElMessage.success("鏍囪瘑閲嶆柊鍒嗛厤鎴愬姛");
+ reassignDialogVisible.value = false;
+ }
+ };
+
+ const handleSubmit = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ if (isEdit.value) {
+ // 缂栬緫
+ const index = identifierList.value.findIndex(
+ item => item.id === editId.value
+ );
+ if (index > -1) {
+ identifierList.value[index] = { ...form, id: editId.value };
+ ElMessage.success("缂栬緫鎴愬姛");
+ }
+ } else {
+ // 鏂板
+ const newId =
+ Math.max(...identifierList.value.map(item => item.id)) + 1;
+
+ // 鏍规嵁鏍囪瘑绫诲瀷鐢熸垚涓嶅悓鐨勬爣璇嗙爜
+ let identifierCode = "";
+ if (form.identifierType === "浜岀淮鐮�") {
+ identifierCode = `QR_${form.productCode}_${form.batchNo}_001`;
+ } else if (form.identifierType === "闃蹭吉鐮�") {
+ identifierCode = `SEC_${form.productCode}_${form.batchNo}_001`;
+ }
+
+ identifierList.value.push({
+ ...form,
+ id: newId,
+ identifierCode: identifierCode,
+ generateTime: new Date().toLocaleString(),
+ });
+ pagination.total++;
+ ElMessage.success("鏂板鎴愬姛");
+ }
+ dialogVisible.value = false;
+ }
+ });
+ };
+
+ const handleCurrentChange = val => {
+ pagination.currentPage = val.page;
+ pagination.pageSize = val.limit;
+ };
+</script>
+
+<style scoped>
+ .search-row {
+ margin-bottom: 20px;
+ }
+
+ .quick-actions-row {
+ margin-bottom: 20px;
+ }
+
+ .quick-actions-row .el-alert {
+ margin-bottom: 0;
+ }
+
+ .quick-actions-row .el-alert p {
+ margin: 5px 0;
+ font-size: 14px;
+ line-height: 1.5;
+ }
+
+ /* 浜岀淮鐮侀瑙堟牱寮� */
+ .qr-preview-container {
+ text-align: center;
+ padding: 20px;
+ }
+
+ .qr-image-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 20px;
+ }
+
+ .qr-image {
+ max-width: 100%;
+ height: auto;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ }
+
+ .qr-info {
+ text-align: left;
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 8px;
+ min-width: 300px;
+ }
+
+ .qr-info p {
+ margin: 8px 0;
+ color: #666;
+ font-size: 14px;
+ }
+
+ .qr-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 15px;
+ padding: 40px 0;
+ }
+
+ .qr-loading .el-icon {
+ font-size: 32px;
+ color: #409eff;
+ }
+
+ .qr-loading p {
+ color: #666;
+ margin: 0;
+ }
+</style>
diff --git a/src/views/productionManagement/operationScheduling/components/formDia.vue b/src/views/productionManagement/operationScheduling/components/formDia.vue
new file mode 100644
index 0000000..06b46ac
--- /dev/null
+++ b/src/views/productionManagement/operationScheduling/components/formDia.vue
@@ -0,0 +1,251 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="宸ュ簭鎺掍骇"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-button type="primary" @click="addRow" style="margin-bottom: 10px;">鏂板</el-button>
+ <span style="font-size: 18px;margin-left: 10px">寰呮帓浜ф暟閲忥細{{pendingNum}}</span>
+<!-- <div style="margin-bottom: 10px; margin-left: 10px;">-->
+<!-- <el-form-item label="棰嗙敤锛�" style="margin-bottom: 0;">-->
+<!-- <el-input v-model="receive" placeholder="璇疯緭鍏ラ鐢�" style="width: 200px;" />-->
+<!-- </el-form-item>-->
+<!-- </div>-->
+ <el-table :data="tableData" border style="width: 100%" :summary-method="summarizeMainTable" show-summary :row-key="row => row.id">
+ <el-table-column label="搴忓彿" width="60" align="center">
+ <template #default="scope">
+ {{ scope.$index + 1 }}
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ュ簭" prop="process" width="150">
+ <template #default="scope">
+ <el-input v-model="scope.row.process" placeholder="璇疯緭鍏ュ伐搴�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="浜х嚎" prop="productionLine" width="150">
+ <template #default="scope">
+ <el-select
+ v-model="scope.row.productionLine"
+ placeholder="閫夋嫨浜х嚎"
+ style="width: 100%;"
+ clearable
+ >
+ <el-option
+ v-for="line in productionLines"
+ :key="line.value"
+ :label="line.label"
+ :value="line.value"
+ />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍗曚綅" prop="unit" width="90">
+ <template #default="scope">
+ <el-input v-model="scope.row.unit" placeholder="璇疯緭鍏ュ崟浣�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙e懗/鍝佸悕/瑙勬牸" prop="type" width="150">
+ <template #default="scope">
+ <el-input v-model="scope.row.type" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掍骇鏁伴噺" width="200" prop="schedulingNum">
+ <template #default="scope">
+ <el-input-number
+ v-model="scope.row.schedulingNum"
+ placeholder="璇疯緭鍏�"
+ :min="0"
+ :step="0.1"
+ :precision="2"
+ clearable
+ style="width: 100%"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="宸ユ椂瀹氶" width="200" prop="workHours">
+ <template #default="scope">
+ <el-input-number
+ v-model="scope.row.workHours"
+ placeholder="璇疯緭鍏�"
+ :min="0"
+ :step="0.1"
+ :precision="2"
+ clearable
+ style="width: 100%"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掍骇鏃ユ湡" prop="schedulingDate" width="200">
+ <template #default="scope">
+ <el-date-picker v-model="scope.row.schedulingDate" type="date" placeholder="閫夋嫨鏃ユ湡" style="width: 100%;" value-format="YYYY-MM-DD" format="YYYY-MM-DD"/>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎺掍骇浜�" prop="schedulingUserId" width="150">
+ <template #default="scope">
+ <el-select
+ v-model="scope.row.schedulingUserId"
+ placeholder="閫夋嫨浜哄憳"
+ style="width: 100%;"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId"
+ />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" prop="remark" width="200">
+ <template #default="scope">
+ <el-input v-model="scope.row.remark" placeholder="璇疯緭鍏ュ娉�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80">
+ <template #default="scope">
+ <el-button type="danger" size="small" @click="removeRow(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, getCurrentInstance} from "vue";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+import {processScheduling} from "@/api/productionManagement/operationScheduling.js";
+const { proxy } = getCurrentInstance()
+const { work_step } = proxy.useDict("work_step")
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const tableData = ref([]);
+const unitFromRow = ref('');
+const idFromRow = ref('');
+const specificationModelFromRow = ref('');
+const pendingNum = ref(0);
+const userList = ref([])
+const receive = ref('')
+const sunqianUserId = ref('')
+// 浜х嚎閫夐」
+const productionLines = ref([
+ { label: '浜х嚎1', value: '浜х嚎1' },
+ { label: '浜х嚎2', value: '浜х嚎2' },
+ { label: '浜х嚎3', value: '浜х嚎3' },
+ { label: '浜х嚎4', value: '浜х嚎4' }
+])
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ pendingNum.value = row?.pendingNum ?? 0;
+ unitFromRow.value = row?.unit ?? '';
+ idFromRow.value = row?.id ?? '';
+ specificationModelFromRow.value = row?.specificationModel ?? '';
+
+ userListNoPageByTenantId().then((res) => {
+ userList.value = res.data;
+ // 鎵惧埌瀛欏�╃殑鐢ㄦ埛ID骞惰缃负榛樿鍊�
+ const sunqianUser = userList.value.find(user => user.nickName === '瀛欏��');
+ if (sunqianUser) {
+ sunqianUserId.value = sunqianUser.userId;
+ }
+ // 鍦ㄧ敤鎴峰垪琛ㄥ姞杞藉畬鎴愬悗鍒涘缓琛屾暟鎹紝骞跺皢浜х嚎鏁版嵁甯﹀叆
+ tableData.value = [createRow(row)];
+ });
+}
+
+const createRow = (row) => ({
+ id: idFromRow.value,
+ process: '鍖呰',
+ schedulingDate: '',
+ schedulingNum: null,
+ schedulingUserId: sunqianUserId.value, // 榛樿璁剧疆涓哄瓩鍊╃殑鐢ㄦ埛ID
+ workHours: null,
+ unit: unitFromRow.value,
+ remark: '',
+ type: specificationModelFromRow.value,
+ productionLine: row?.productionLine ?? '', // 浠庤鏁版嵁涓幏鍙栦骇绾夸俊鎭�
+});
+
+const submitForm = () => {
+ // 1. 妫�鏌ユ瘡涓�琛屾槸鍚﹀~鍐欏畬鏁�
+ for (let i = 0; i < tableData.value.length; i++) {
+ const row = tableData.value[i];
+ if (
+ !row.process ||
+ !row.schedulingDate ||
+ row.schedulingNum === '' || row.schedulingNum === null ||
+ !row.schedulingUserId ||
+ row.workHours === '' || row.workHours === null ||
+ !row.unit ||
+ !row.productionLine
+ ) {
+ proxy.$modal.msgError(`绗�${i + 1}琛屾暟鎹湭濉啓瀹屾暣`);
+ return;
+ }
+ }
+ // 2. 鍚堣鎺掍骇鏁伴噺
+ const totalSchedulingNum = tableData.value.reduce((sum, row) => {
+ return sum + Number(row.schedulingNum || 0);
+ }, 0);
+ if (totalSchedulingNum > Number(pendingNum.value)) {
+ proxy.$modal.msgError('鎺掍骇鏁伴噺鍚堣涓嶈兘瓒呰繃寰呮帓浜ф暟閲�');
+ return;
+ }
+ // 3. 灏� receive 瀛楁娣诲姞鍒版瘡鏉℃暟鎹腑锛屽苟绉婚櫎 loss 瀛楁
+ const submitData = tableData.value.map(row => {
+ const { loss, ...rest } = row;
+ return {
+ ...rest,
+ receive: receive.value
+ };
+ });
+ processScheduling(submitData).then((res) => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+}
+const summarizeMainTable = (param) => {
+ return proxy.summarizeTable(param, ['schedulingNum']);
+};
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ receive.value = '';
+ tableData.value = [];
+ unitFromRow.value = '';
+ idFromRow.value = '';
+ specificationModelFromRow.value = '';
+ pendingNum.value = 0;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+
+const addRow = () => {
+ tableData.value.push(createRow());
+};
+const removeRow = (index) => {
+ tableData.value.splice(index, 1);
+};
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/operationScheduling/index.vue b/src/views/productionManagement/operationScheduling/index.vue
new file mode 100644
index 0000000..a9c06fb
--- /dev/null
+++ b/src/views/productionManagement/operationScheduling/index.vue
@@ -0,0 +1,289 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="瀹㈡埛鍚嶇О:">
+ <el-input v-model="searchForm.customerName" placeholder="璇疯緭鍏�" clearable prefix-icon="Search"
+ style="width: 200px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鍚堝悓鍙�:">
+ <el-input v-model="searchForm.salesContractNo" placeholder="璇疯緭鍏�" clearable prefix-icon="Search"
+ style="width: 200px;"
+ @change="handleQuery" />
+ </el-form-item>
+<!-- <el-form-item label="椤圭洰鍚嶇О:">-->
+<!-- <el-input v-model="searchForm.projectName" placeholder="璇疯緭鍏�" clearable prefix-icon="Search"-->
+<!-- style="width: 200px;"-->
+<!-- @change="handleQuery" />-->
+<!-- </el-form-item>-->
+ <el-form-item label="娲惧伐鏃ユ湡:">
+ <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+ placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" @change="handleQuery" style="width: 140px" clearable>
+ <el-option label="寰呮帓浜�" :value="1"></el-option>
+ <el-option label="宸叉帓浜�" :value="3"></el-option>
+ <el-option label="鎺掍骇涓�" :value="2"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div style="text-align: right" class="mb10">
+ <el-button type="primary" @click="openForm">宸ュ簭鎺掍骇</el-button>
+ <el-button type="danger" @click="handleDelete" plain>鍙栨秷鎺掍骇</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ </div>
+</template>
+
+<script setup>
+import {onMounted, ref} from "vue";
+import FormDia from "@/views/productionManagement/operationScheduling/components/formDia.vue";
+import {ElMessageBox} from "element-plus";
+import dayjs from "dayjs";
+import {listPageProcess, productionDispatchDelete} from "@/api/productionManagement/operationScheduling.js";
+
+const data = reactive({
+ searchForm: {
+ staffName: "",
+ customerName: "",
+ salesContractNo: "",
+ status: 1,
+ entryDate: [dayjs().format("YYYY-MM-DD"), dayjs().format("YYYY-MM-DD")], // 褰曞叆鏃ユ湡锛岄粯璁ゅ綋澶�
+ entryDateStart: dayjs().format("YYYY-MM-DD"),
+ entryDateEnd: dayjs().format("YYYY-MM-DD"),
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 3) {
+ return "宸叉帓浜�";
+ } else if (params == 1) {
+ return "寰呮帓浜�";
+ } else {
+ return '鎺掍骇涓�';
+ }
+ },
+ formatType: (params) => {
+ if (params == 3) {
+ return "success";
+ } else if (params == 1) {
+ return "primary";
+ } else {
+ return 'warning';
+ }
+ },
+ },
+ {
+ label: "娲惧伐鏃ユ湡",
+ prop: "schedulingDate",
+ width: 120,
+ },
+ {
+ label: "娲惧伐浜�",
+ prop: "schedulingUserName",
+ },
+ {
+ label: "鍚堝悓鍙�",
+ prop: "salesContractNo",
+ width: 200,
+ },
+ // {
+ // label: "瀹㈡埛鍚堝悓鍙�",
+ // prop: "customerContractNo",
+ // width: 200,
+ // },
+ {
+ label: "瀹㈡埛鍚嶇О",
+ prop: "customerName",
+ width: 200,
+ },
+ // {
+ // label: "椤圭洰鍚嶇О",
+ // prop: "projectName",
+ // width:300
+ // },
+ {
+ label: "浜у搧澶х被",
+ prop: "productCategory",
+ width: 150,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "specificationModel",
+ width: 150,
+ },
+ {
+ label: "缁戝畾鏈哄櫒",
+ prop: "speculativeTradingName",
+ width: 220,
+ },
+ // {
+ // label: "浜х嚎",
+ // prop: "productionLine",
+ // width: 220,
+ // },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鎺掍骇鎬绘暟",
+ prop: "schedulingNum",
+ },
+ {
+ label: "宸叉帓浜ф暟閲�",
+ prop: "successNum",
+ width: 100,
+ },
+ {
+ label: "寰呮帓浜ф暟閲�",
+ prop: "pendingNum",
+ width: 100,
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const formDia = ref()
+const { proxy } = getCurrentInstance()
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const changeDaterange = (value) => {
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ } else {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ }
+ handleQuery();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined
+ listPageProcess(params).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records.map(item => ({
+ ...item,
+ pendingNum: (Number(item.schedulingNum) || 0) - (Number(item.successNum) || 0)
+ }));
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ if (selectedRows.value.length !== 1) {
+ proxy.$message.error("璇烽�夋嫨涓�鏉℃暟鎹�");
+ return;
+ }
+ if (selectedRows.value[0].pendingNum == 0) {
+ proxy.$message.warning("鏃犻渶鍐嶆帓浜�");
+ return;
+ }
+ nextTick(() => {
+ formDia.value?.openDialog(type, selectedRows.value[0])
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ // 鏂板锛氬垽鏂槸鍚︽湁宸叉帓浜х殑鏁版嵁
+ const hasScheduled = selectedRows.value.some(item => item.status == 3);
+ if (hasScheduled) {
+ proxy.$modal.msgWarning("宸叉帓浜ф暟鎹笉鑳藉彇娑堟帓浜�");
+ return;
+ }
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("鏄惁纭鍙栨秷鎺掍骇锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ tableLoading.value = true;
+ productionDispatchDelete(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍙栨秷鎺掍骇鎴愬姛");
+ getList();
+ })
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/salesLedger/scheduling/exportTwo", {}, "宸ュ簭鎺掍骇.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/productionManagement/processRoute/Edit.vue b/src/views/productionManagement/processRoute/Edit.vue
new file mode 100644
index 0000000..0c0fe0f
--- /dev/null
+++ b/src/views/productionManagement/processRoute/Edit.vue
@@ -0,0 +1,252 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="缂栬緫宸ヨ壓璺嚎"
+ width="400"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="浜у搧鍚嶇О"
+ prop="productModelId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨浜у搧',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-button type="primary" @click="showProductSelectDialog = true">
+ {{ formState.productName && formState.productModelName
+ ? `${formState.productName} - ${formState.productModelName}`
+ : '閫夋嫨浜у搧' }}
+ </el-button>
+ </el-form-item>
+
+ <el-form-item
+ label="BOM"
+ prop="bomId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨BOM',
+ trigger: 'change',
+ }
+ ]"
+ >
+ <el-select
+ v-model="formState.bomId"
+ placeholder="璇烽�夋嫨BOM"
+ clearable
+ :disabled="!formState.productModelId || bomOptions.length === 0"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in bomOptions"
+ :key="item.id"
+ :label="item.bomNo || `BOM-${item.id}`"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+
+ <el-form-item label="澶囨敞" prop="description">
+ <el-input v-model="formState.description" type="textarea" />
+ </el-form-item>
+ </el-form>
+
+ <!-- 浜у搧閫夋嫨寮圭獥 -->
+ <ProductSelectDialog
+ v-model="showProductSelectDialog"
+ @confirm="handleProductSelect"
+ single
+ />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, computed, getCurrentInstance, onMounted, nextTick, watch} from "vue";
+import {update} from "@/api/productionManagement/processRoute.js";
+import {getByModel} from "@/api/productionManagement/productBom.js";
+import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ record: {
+ type: Object,
+ required: true,
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ productModelName: "",
+ bomId: undefined,
+ description: '',
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const showProductSelectDialog = ref(false);
+const bomOptions = ref([]);
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+// 璁剧疆琛ㄥ崟鏁版嵁
+const setFormData = () => {
+ if (props.record) {
+ formState.value = {
+ ...props.record,
+ productId: props.record.productId,
+ productModelId: props.record.productModelId,
+ productName: props.record.productName || "",
+ // 娉ㄦ剰锛歳ecord涓殑瀛楁鏄痬odel锛岄渶瑕佹槧灏勫埌productModelName
+ productModelName: props.record.model || props.record.productModelName || "",
+ bomId: props.record.bomId,
+ description: props.record.description || '',
+ };
+ // 濡傛灉鏈変骇鍝佸瀷鍙稩D锛屽姞杞紹OM鍒楄〃
+ if (props.record.productModelId) {
+ loadBomList(props.record.productModelId);
+ }
+ }
+}
+
+// 鍔犺浇BOM鍒楄〃
+const loadBomList = async (productModelId) => {
+ if (!productModelId) {
+ bomOptions.value = [];
+ return;
+ }
+ try {
+ const res = await getByModel(productModelId);
+ // 澶勭悊杩斿洖鐨凚OM鏁版嵁锛氬彲鑳芥槸鏁扮粍銆佸璞℃垨鍖呭惈data瀛楁
+ let bomList = [];
+ if (Array.isArray(res)) {
+ bomList = res;
+ } else if (res && res.data) {
+ bomList = Array.isArray(res.data) ? res.data : [res.data];
+ } else if (res && typeof res === 'object') {
+ bomList = [res];
+ }
+ bomOptions.value = bomList;
+ } catch (error) {
+ console.error("鍔犺浇BOM鍒楄〃澶辫触锛�", error);
+ bomOptions.value = [];
+ }
+};
+
+// 浜у搧閫夋嫨澶勭悊
+const handleProductSelect = async (products) => {
+ if (products && products.length > 0) {
+ const product = products[0];
+ // 鍏堟煡璇OM鍒楄〃锛堝繀閫夛級
+ try {
+ const res = await getByModel(product.id);
+ // 澶勭悊杩斿洖鐨凚OM鏁版嵁锛氬彲鑳芥槸鏁扮粍銆佸璞℃垨鍖呭惈data瀛楁
+ let bomList = [];
+ if (Array.isArray(res)) {
+ bomList = res;
+ } else if (res && res.data) {
+ bomList = Array.isArray(res.data) ? res.data : [res.data];
+ } else if (res && typeof res === 'object') {
+ bomList = [res];
+ }
+
+ if (bomList.length > 0) {
+ formState.value.productModelId = product.id;
+ formState.value.productName = product.productName;
+ formState.value.productModelName = product.model;
+ // 濡傛灉褰撳墠閫夋嫨鐨凚OM涓嶅湪鏂板垪琛ㄤ腑锛屽垯閲嶇疆BOM閫夋嫨
+ const currentBomExists = bomList.some(bom => bom.id === formState.value.bomId);
+ if (!currentBomExists) {
+ formState.value.bomId = undefined;
+ }
+ bomOptions.value = bomList;
+ showProductSelectDialog.value = false;
+ // 瑙﹀彂琛ㄥ崟楠岃瘉鏇存柊
+ proxy.$refs["formRef"]?.validateField('productModelId');
+ } else {
+ proxy.$modal.msgError("璇ヤ骇鍝佹病鏈塀OM锛岃鍏堝垱寤築OM");
+ }
+ } catch (error) {
+ // 濡傛灉鎺ュ彛杩斿洖404鎴栧叾浠栭敊璇紝璇存槑娌℃湁BOM
+ proxy.$modal.msgError("璇ヤ骇鍝佹病鏈塀OM锛岃鍏堝垱寤築OM");
+ }
+ }
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ // 楠岃瘉鏄惁閫夋嫨浜嗕骇鍝佸拰BOM
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨浜у搧");
+ return;
+ }
+ if (!formState.value.bomId) {
+ proxy.$modal.msgError("璇烽�夋嫨BOM");
+ return;
+ }
+ update(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+
+
+// 鐩戝惉寮圭獥鎵撳紑锛屽垵濮嬪寲琛ㄥ崟鏁版嵁
+watch(() => props.visible, (visible) => {
+ if (visible && props.record) {
+ nextTick(() => {
+ setFormData();
+ });
+ }
+}, { immediate: true });
+
+onMounted(() => {
+ if (props.visible && props.record) {
+ setFormData();
+ }
+});
+</script>
diff --git a/src/views/productionManagement/processRoute/ItemsForm.vue b/src/views/productionManagement/processRoute/ItemsForm.vue
new file mode 100644
index 0000000..ed6e499
--- /dev/null
+++ b/src/views/productionManagement/processRoute/ItemsForm.vue
@@ -0,0 +1,531 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="宸ヨ壓璺嚎椤圭洰"
+ width="800px"
+ @close="closeModal"
+ >
+ <div class="operate-button">
+ <el-button
+ type="primary"
+ @click="isShowProductSelectDialog = true"
+ class="mb5"
+ style="margin-bottom: 10px;"
+ >
+ 閫夋嫨浜у搧
+ </el-button>
+
+ <el-switch
+ v-model="isTable"
+ inline-prompt
+ active-text="琛ㄦ牸"
+ inactive-text="鍒楄〃"
+ @change="handleViewChange"
+ />
+ </div>
+
+ <el-table
+ v-if="isTable"
+ ref="multipleTable"
+ v-loading="tableLoading"
+ border
+ :data="routeItems"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ row-key="id"
+ tooltip-effect="dark"
+ class="lims-table"
+ style="cursor: move;"
+ >
+ <el-table-column align="center" label="搴忓彿" width="60">
+ <template #default="scope">
+ {{ scope.$index + 1 }}
+ </template>
+ </el-table-column>
+
+ <el-table-column
+ v-for="(item, index) in tableColumn"
+ :key="index"
+ :label="item.label"
+ :width="item.width"
+ show-overflow-tooltip
+ >
+ <template #default="scope" v-if="item.dataType === 'action'">
+ <el-button
+ v-for="(op, opIndex) in item.operation"
+ :key="opIndex"
+ :type="op.type"
+ :link="op.link"
+ size="small"
+ @click.stop="op.clickFun(scope.row)"
+ >
+ {{ op.name }}
+ </el-button>
+ </template>
+
+ <template #default="scope" v-else>
+ <template v-if="item.prop === 'processId'">
+ <el-select
+ v-model="scope.row[item.prop]"
+ style="width: 100%;"
+ @mousedown.stop
+ >
+ <el-option
+ v-for="process in processOptions"
+ :key="process.id"
+ :label="process.name"
+ :value="process.id"
+ />
+ </el-select>
+ </template>
+ <template v-else>
+ {{ scope.row[item.prop] || '-' }}
+ </template>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 浣跨敤鏅�歞iv鏇夸唬el-steps -->
+ <div
+ v-else
+ ref="stepsContainer"
+ class="mb5 custom-steps"
+ style="padding: 10px 0; display: flex; flex-wrap: nowrap; gap: 20px; align-items: flex-start;"
+ >
+ <div
+ v-for="(item, index) in routeItems"
+ :key="item.id"
+ class="custom-step draggable-step"
+ :data-id="item.id"
+ style="cursor: move; flex: 0 0 auto; min-width: 220px;"
+ >
+ <div class="step-content">
+ <div class="step-number">{{ index + 1 }}</div>
+ <el-card
+ :header="item.productName"
+ class="step-card"
+ style="cursor: move;"
+ >
+ <div class="step-card-content">
+ <p>{{ item.model }}</p>
+ <p>{{ item.unit }}</p>
+ <el-select
+ v-model="item.processId"
+ style="width: 100%;"
+ @mousedown.stop
+ >
+ <el-option
+ v-for="process in processOptions"
+ :key="process.id"
+ :label="process.name"
+ :value="process.id"
+ />
+ </el-select>
+ </div>
+ <template #footer>
+ <div class="step-card-footer">
+ <el-button type="danger" link size="small" @click.stop="removeItemByID(item.id)">鍒犻櫎</el-button>
+ </div>
+ </template>
+ </el-card>
+ </div>
+ </div>
+ </div>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <ProductSelectDialog
+ v-model="isShowProductSelectDialog"
+ @confirm="handelSelectProducts"
+ />
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance, onMounted, onUnmounted, nextTick } from "vue";
+import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+import { findProcessRouteItemList, addOrUpdateProcessRouteItem } from "@/api/productionManagement/processRouteItem.js";
+import { processList } from "@/api/productionManagement/productionProcess.js";
+import Sortable from 'sortablejs';
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ default: false
+ },
+ record: {
+ type: Object,
+ required: true,
+ default: () => ({})
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+const processOptions = ref([]);
+const tableLoading = ref(false);
+const isShowProductSelectDialog = ref(false);
+const routeItems = ref([]);
+let tableSortable = null;
+let stepsSortable = null;
+const multipleTable = ref(null);
+const stepsContainer = ref(null);
+const isTable = ref(true);
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ }
+});
+
+const tableColumn = ref([
+ { label: "浜у搧鍚嶇О", prop: "productName", width: 180 },
+ { label: "瑙勬牸鍚嶇О", prop: "model", width: 150 },
+ { label: "鍗曚綅", prop: "unit", width: 80 },
+ { label: "宸ュ簭鍚嶇О", prop: "processId", width: 180 },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 100,
+ operation: [
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ link: true,
+ clickFun: (row) => {
+ const idx = routeItems.value.findIndex(item => item.id === row.id);
+ if (idx > -1) {
+ removeItem(idx)
+ }
+ }
+ }
+ ]
+ }
+]);
+
+const removeItem = (index) => {
+ routeItems.value.splice(index, 1);
+ nextTick(() => initSortable());
+};
+
+const removeItemByID = (id) => {
+ const idx = routeItems.value.findIndex(item => item.id === id);
+ if (idx > -1) {
+ routeItems.value.splice(idx, 1);
+ nextTick(() => initSortable());
+ }
+};
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const handelSelectProducts = (products) => {
+ destroySortable();
+
+ const newData = products.map(({ id, ...product }) => ({
+ ...product,
+ productModelId: id,
+ routeId: props.record.id,
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
+ processId: undefined
+ }));
+
+ console.log('閫夋嫨浜у搧鍓嶆暟缁�:', routeItems.value);
+ routeItems.value.push(...newData);
+ routeItems.value = [...routeItems.value];
+ console.log('閫夋嫨浜у搧鍚庢暟缁�:', routeItems.value);
+
+ // 寤惰繜鍒濆鍖栵紝纭繚DOM瀹屽叏娓叉煋
+ nextTick(() => {
+ // 寮哄埗閲嶆柊娓叉煋缁勪欢
+ if (proxy?.$forceUpdate) {
+ proxy.$forceUpdate();
+ }
+
+ const temp = [...routeItems.value];
+ routeItems.value = [];
+ nextTick(() => {
+ routeItems.value = temp;
+ initSortable();
+ });
+ });
+};
+
+const findProcessRouteItems = () => {
+ tableLoading.value = true;
+ findProcessRouteItemList({ routeId: props.record.id })
+ .then(res => {
+ tableLoading.value = false;
+ routeItems.value = res.data.map(item => ({
+ ...item,
+ processId: item.processId === 0 ? undefined : item.processId
+ }));
+ // 寤惰繜鍒濆鍖栵紝纭繚DOM瀹屽叏娓叉煋
+ nextTick(() => {
+ setTimeout(() => initSortable(), 100);
+ });
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+ });
+};
+
+const findProcessList = () => {
+ processList({})
+ .then(res => {
+ processOptions.value = res.data;
+ })
+ .catch(err => {
+ console.error("鑾峰彇宸ュ簭澶辫触锛�", err);
+ });
+};
+
+const { proxy } = getCurrentInstance() || {};
+
+const handleSubmit = () => {
+ const hasEmptyProcess = routeItems.value.some(item => !item.processId);
+ if (hasEmptyProcess) {
+ proxy?.$modal?.msgError("璇蜂负鎵�鏈夐」鐩�夋嫨宸ュ簭");
+ return;
+ }
+
+ addOrUpdateProcessRouteItem({
+ routeId: props.record.id,
+ processRouteItem: routeItems.value.map(({ id, ...item }) => item)
+ })
+ .then(res => {
+ isShow.value = false;
+ emit('completed');
+ proxy?.$modal?.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ .catch(err => {
+ proxy?.$modal?.msgError(`鎻愪氦澶辫触锛�${err.msg || "缃戠粶寮傚父"}`);
+ });
+};
+
+const destroySortable = () => {
+ if (tableSortable) {
+ tableSortable.destroy();
+ tableSortable = null;
+ }
+ if (stepsSortable) {
+ stepsSortable.destroy();
+ stepsSortable = null;
+ }
+};
+
+const initSortable = () => {
+ destroySortable();
+
+ if (isTable.value) {
+ if (!multipleTable.value) return;
+ const tbody = multipleTable.value.$el.querySelector('.el-table__body tbody') ||
+ multipleTable.value.$el.querySelector('.el-table__body-wrapper > table > tbody');
+ if (!tbody) return;
+
+ tableSortable = new Sortable(tbody, {
+ animation: 150,
+ ghostClass: 'sortable-ghost',
+ handle: '.el-table__row',
+ filter: '.el-button, .el-select',
+ onEnd: (evt) => {
+ if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
+
+ // 浣跨敤鏁扮粍 splice 鏂规硶閲嶆柊鎺掑簭锛屼笌琛ㄦ牸妯″紡淇濇寔涓�鑷�
+ const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
+ routeItems.value.splice(evt.newIndex, 0, moveItem);
+ routeItems.value = [...routeItems.value];
+ console.log('鎺掑簭鍚庢暟缁�:', routeItems.value);
+ }
+ });
+ } else {
+ if (!stepsContainer.value) return;
+
+ // 淇敼锛氱洿鎺ヤ娇鐢╯tepsContainer.value浣滀负鎷栨嫿瀹瑰櫒
+ const stepsList = stepsContainer.value;
+ if (!stepsList) {
+ console.warn('鏈壘鍒版楠ゆ潯鎷栨嫿瀹瑰櫒');
+ return;
+ }
+
+ // 淇敼锛氱畝鍖栨嫋鎷介厤缃�
+ stepsSortable = new Sortable(stepsList, {
+ animation: 150,
+ ghostClass: 'sortable-ghost',
+ draggable: '.draggable-step', // 鍙嫋鎷藉厓绱�
+ handle: '.draggable-step, .step-card', // 鎷栨嫿鎵嬫焺
+ filter: '.el-button, .el-select, .el-input', // 杩囨护鎸夐挳/閫夋嫨鍣�
+ forceFallback: true,
+ fallbackClass: 'sortable-fallback',
+ preventOnFilter: true,
+ scroll: true,
+ scrollSensitivity: 30,
+ scrollSpeed: 10,
+ bubbleScroll: true,
+ onEnd: (evt) => {
+ if (evt.oldIndex === evt.newIndex || !routeItems.value[evt.oldIndex]) return;
+
+ // 浣跨敤鏁扮粍 splice 鏂规硶閲嶆柊鎺掑簭
+ const moveItem = routeItems.value.splice(evt.oldIndex, 1)[0];
+ routeItems.value.splice(evt.newIndex, 0, moveItem);
+ routeItems.value = [...routeItems.value];
+ }
+ });
+
+ // 璋冭瘯锛氭墦鍗板鍣ㄥ拰瀹炰緥锛岀‘璁ょ粦瀹氭垚鍔�
+ console.log('姝ラ鏉℃嫋鎷藉鍣�:', stepsList);
+ console.log('Sortable瀹炰緥:', stepsSortable);
+ }
+};
+
+const handleViewChange = () => {
+ destroySortable();
+ // 寤惰繜鍒濆鍖栵紝纭繚瑙嗗浘鍒囨崲鍚嶥OM瀹屽叏娓叉煋
+ nextTick(() => {
+ setTimeout(() => initSortable(), 100);
+ });
+};
+
+onMounted(() => {
+ findProcessRouteItems();
+ findProcessList();
+});
+
+onUnmounted(() => {
+ destroySortable();
+});
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow
+});
+</script>
+
+<style scoped>
+:deep(.sortable-ghost) {
+ opacity: 0.6;
+ background-color: #f5f7fa !important;
+}
+
+:deep(.el-table__row) {
+ transition: background-color 0.2s;
+}
+
+:deep(.el-table__row:hover) {
+ background-color: #f9fafc !important;
+}
+
+:deep(.el-card__footer){
+ padding: 0 !important;
+}
+
+.operate-button {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+/* 淇敼锛氳嚜瀹氫箟姝ラ鏉″鍣ㄦ牱寮� */
+.custom-steps {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ gap: 20px;
+ min-height: 100px;
+}
+
+/* 淇敼锛氳嚜瀹氫箟姝ラ椤规牱寮� */
+.custom-step {
+ cursor: move !important;
+ padding: 8px;
+ position: relative;
+ transition: all 0.2s ease;
+ flex: 0 0 auto;
+ min-width: 220px;
+ touch-action: none;
+}
+
+/* 鎷栨嫿鎮诞鏍峰紡锛屾彁绀哄彲鎷栨嫿 */
+.custom-step:hover {
+ background-color: rgba(64, 158, 255, 0.05);
+ transform: translateY(-2px);
+}
+
+.sortable-ghost {
+ opacity: 0.4;
+ background-color: #f5f7fa !important;
+ border: 2px dashed #409eff;
+ margin: 10px;
+ transform: scale(1.02);
+}
+
+.sortable-fallback {
+ opacity: 0.9;
+ background-color: #f5f7fa;
+ border: 1px solid #409eff;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transform: rotate(2deg);
+ margin: 10px;
+}
+
+.step-card {
+ cursor: move !important;
+ transition: box-shadow 0.2s ease;
+ user-select: none;
+ -webkit-user-select: none;
+ pointer-events: auto;
+ margin: 10px;
+ height: 240px;
+}
+
+.step-card:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.step-content {
+ width: 220px;
+ user-select: none;
+}
+
+.step-card-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.step-card-footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ padding: 10px;
+}
+
+/* 鑷畾涔夊簭鍙锋牱寮忎紭鍖� */
+.step-number {
+ font-weight: bold;
+ text-align: center;
+ width: 36px;
+ height: 36px;
+ line-height: 36px;
+ margin: 0 auto 10px;
+ background: #409eff;
+ color: #fff;
+ border-radius: 50%;
+ font-size: 14px;
+}
+</style>
diff --git a/src/views/productionManagement/processRoute/New.vue b/src/views/productionManagement/processRoute/New.vue
new file mode 100644
index 0000000..b7f4f26
--- /dev/null
+++ b/src/views/productionManagement/processRoute/New.vue
@@ -0,0 +1,187 @@
+<template>
+ <div>
+ <el-dialog v-model="isShow"
+ title="鏂板宸ヨ壓璺嚎"
+ width="400"
+ @close="closeModal">
+ <el-form label-width="140px"
+ :model="formState"
+ label-position="top"
+ ref="formRef">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productModelId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨浜у搧',
+ trigger: 'change',
+ }
+ ]">
+ <el-button type="primary"
+ @click="showProductSelectDialog = true">
+ {{ formState.productName && formState.productModelName
+ ? `${formState.productName} - ${formState.productModelName}`
+ : '閫夋嫨浜у搧' }}
+ </el-button>
+ </el-form-item>
+ <el-form-item label="BOM"
+ prop="bomId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨BOM',
+ trigger: 'change',
+ }
+ ]">
+ <el-select v-model="formState.bomId"
+ placeholder="璇烽�夋嫨BOM"
+ clearable
+ :disabled="!formState.productModelId || bomOptions.length === 0"
+ style="width: 100%">
+ <el-option v-for="item in bomOptions"
+ :key="item.id"
+ :label="item.bomNo || `BOM-${item.id}`"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="description">
+ <el-input v-model="formState.description"
+ type="textarea" />
+ </el-form-item>
+ </el-form>
+ <!-- 浜у搧閫夋嫨寮圭獥 -->
+ <ProductSelectDialog v-model="showProductSelectDialog"
+ @confirm="handleProductSelect"
+ single />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, computed, getCurrentInstance } from "vue";
+ import { add } from "@/api/productionManagement/processRoute.js";
+ import { getByModel } from "@/api/productionManagement/productBom.js";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ });
+
+ const emit = defineEmits(["update:visible", "completed"]);
+
+ // 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+ const formState = ref({
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ productModelName: "",
+ bomId: undefined,
+ description: "",
+ });
+
+ const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+ });
+
+ const showProductSelectDialog = ref(false);
+ const bomOptions = ref([]);
+
+ let { proxy } = getCurrentInstance();
+
+ const closeModal = () => {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ formState.value = {
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ productModelName: "",
+ bomId: undefined,
+ description: "",
+ };
+ bomOptions.value = [];
+ isShow.value = false;
+ };
+
+ // 浜у搧閫夋嫨澶勭悊
+ const handleProductSelect = async products => {
+ if (products && products.length > 0) {
+ const product = products[0];
+ // 鍏堟煡璇OM鍒楄〃锛堝繀閫夛級
+ try {
+ const res = await getByModel(product.id);
+ // 澶勭悊杩斿洖鐨凚OM鏁版嵁锛氬彲鑳芥槸鏁扮粍銆佸璞℃垨鍖呭惈data瀛楁
+ let bomList = [];
+ if (Array.isArray(res)) {
+ bomList = res;
+ } else if (res && res.data) {
+ bomList = Array.isArray(res.data) ? res.data : [res.data];
+ } else if (res && typeof res === "object") {
+ bomList = [res];
+ }
+
+ if (bomList.length > 0) {
+ formState.value.productModelId = product.id;
+ formState.value.productName = product.productName;
+ formState.value.productModelName = product.model;
+ formState.value.bomId = undefined; // 閲嶇疆BOM閫夋嫨
+ bomOptions.value = bomList;
+ showProductSelectDialog.value = false;
+ // 瑙﹀彂琛ㄥ崟楠岃瘉鏇存柊
+ proxy.$refs["formRef"]?.validateField("productModelId");
+ } else {
+ proxy.$modal.msgError("璇ヤ骇鍝佹病鏈塀OM锛岃鍏堝垱寤築OM");
+ }
+ } catch (error) {
+ // 濡傛灉鎺ュ彛杩斿洖404鎴栧叾浠栭敊璇紝璇存槑娌℃湁BOM
+ proxy.$modal.msgError("璇ヤ骇鍝佹病鏈塀OM锛岃鍏堝垱寤築OM");
+ }
+ }
+ };
+
+ const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ // 楠岃瘉鏄惁閫夋嫨浜嗕骇鍝佸拰BOM
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨浜у搧");
+ return;
+ }
+ if (!formState.value.bomId) {
+ proxy.$modal.msgError("璇烽�夋嫨BOM");
+ return;
+ }
+ console.log(formState.value, "formState.value====");
+
+ add(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit("completed");
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ });
+ }
+ });
+ };
+
+ defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+ });
+</script>
diff --git a/src/views/productionManagement/processRoute/index.vue b/src/views/productionManagement/processRoute/index.vue
new file mode 100644
index 0000000..1ef5b9d
--- /dev/null
+++ b/src/views/productionManagement/processRoute/index.vue
@@ -0,0 +1,298 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm"
+ :inline="true">
+ <el-form-item label="瑙勬牸鍚嶇О:">
+ <el-input v-model="searchForm.model"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div style="text-align: right"
+ class="mb10">
+ <el-button type="primary"
+ @click="showNewModal">鏂板宸ヨ壓璺嚎</el-button>
+ <el-button type="danger"
+ @click="handleDelete"
+ :disabled="selectedRows.length === 0"
+ plain>鍒犻櫎宸ヨ壓璺嚎</el-button>
+ </div>
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total" />
+ </div>
+ <new-process v-if="isShowNewModal"
+ v-model:visible="isShowNewModal"
+ @completed="getList" />
+ <edit-process v-if="isShowEditModal"
+ v-model:visible="isShowEditModal"
+ :record="record"
+ @completed="getList" />
+ <route-item-form v-if="isShowItemModal"
+ v-model:visible="isShowItemModal"
+ :record="record"
+ @completed="getList" />
+ <FileList v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ :record-type="'technology_routing'"
+ :record-id="currentProcessRouteId" />
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref } from "vue";
+ import NewProcess from "@/views/productionManagement/processRoute/New.vue";
+ import EditProcess from "@/views/productionManagement/processRoute/Edit.vue";
+ import RouteItemForm from "@/views/productionManagement/processRoute/ItemsForm.vue";
+ import { listPage, del } from "@/api/productionManagement/processRoute.js";
+ const FileList = defineAsyncComponent(() =>
+ import("@/components/Dialog/FileList.vue")
+ );
+
+ import { useRouter } from "vue-router";
+ import { ElMessage, ElMessageBox } from "element-plus";
+
+ const router = useRouter();
+ const data = reactive({
+ searchForm: {
+ model: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+ const tableColumn = ref([
+ {
+ label: "宸ヨ壓璺嚎缂栧彿",
+ prop: "processRouteCode",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ },
+ {
+ label: "瑙勬牸鍚嶇О",
+ prop: "model",
+ },
+ {
+ label: "BOM缂栧彿",
+ prop: "bomNo",
+ },
+ {
+ label: "鎻忚堪",
+ prop: "description",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ showEditModal(row);
+ },
+ },
+ {
+ name: "璺嚎椤圭洰",
+ type: "text",
+ clickFun: row => {
+ showItemModal(row);
+ },
+ },
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ openFileDialog(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const isShowNewModal = ref(false);
+ const isShowEditModal = ref(false);
+ const isShowItemModal = ref(false);
+ const record = ref({});
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+
+ // 闄勪欢鐩稿叧
+ const fileDialogVisible = ref(false);
+ const fileListDialogRef = ref(null);
+ const currentProcessRouteId = ref(null);
+ const filePage = reactive({
+ current: 1,
+ size: 1000,
+ total: 0,
+ });
+
+ const { proxy } = getCurrentInstance();
+
+ // 闄勪欢锛氭煡璇�
+ const fetchProcessRouteFiles = async processRouteId => {
+ const params = {
+ current: filePage.current,
+ size: filePage.size,
+ processRouteId,
+ };
+ const res = await listProcessRouteFiles(params);
+ const records = res?.data?.records || [];
+ filePage.total = res?.data?.total || records.length;
+ const mapped = records.map(item => ({
+ id: item.id,
+ name: item.fileName || item.name,
+ url: item.fileUrl || item.url,
+ raw: item,
+ }));
+ fileListDialogRef.value?.setList(mapped);
+ };
+
+ // 鎵撳紑闄勪欢寮圭獥
+ const openFileDialog = async row => {
+ currentProcessRouteId.value = row.id;
+ fileDialogVisible.value = true;
+ await fetchProcessRouteFiles(row.id);
+ };
+
+ // 鍒锋柊闄勪欢鍒楄〃
+ const refreshFileList = async () => {
+ if (!currentProcessRouteId.value) return;
+ await fetchProcessRouteFiles(currentProcessRouteId.value);
+ };
+
+ // 涓婁紶闄勪欢
+ const handleAttachmentUpload = async filePayload => {
+ if (!currentProcessRouteId.value) return;
+ const payload = {
+ fileName: filePayload?.fileName || filePayload?.name,
+ fileUrl: filePayload?.fileUrl || filePayload?.url,
+ processRouteId: currentProcessRouteId.value,
+ };
+ await addProcessRouteFile(payload);
+ ElMessage.success("鏂囦欢涓婁紶鎴愬姛");
+ await refreshFileList();
+ };
+
+ // 鍒犻櫎闄勪欢
+ const handleAttachmentDelete = async row => {
+ if (!row?.id) return false;
+ try {
+ await ElMessageBox.confirm("纭鍒犻櫎璇ラ檮浠讹紵", "鎻愮ず", {
+ type: "warning",
+ });
+ } catch {
+ return false;
+ }
+ await delProcessRouteFile([row.id]);
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ await refreshFileList();
+ };
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined;
+ listPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records.map(item => ({
+ ...item,
+ }));
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鎵撳紑鏂板寮规
+ const showNewModal = () => {
+ isShowNewModal.value = true;
+ };
+
+ const showEditModal = row => {
+ isShowEditModal.value = true;
+ record.value = row;
+ };
+
+ const showItemModal = row => {
+ router.push({
+ path: "/productionManagement/processRouteItem",
+ query: {
+ id: row.id,
+ processRouteCode: row.processRouteCode || "",
+ productName: row.productName || "",
+ model: row.model || "",
+ bomNo: row.bomNo || "",
+ bomId: row.bomId || "",
+ description: row.description || "",
+ type: "route",
+ },
+ });
+ };
+
+ // 鍒犻櫎
+ function handleDelete() {
+ const ids = selectedRows.value.map(item => item.id);
+ proxy.$modal
+ .confirm("鏄惁纭鍒犻櫎宸插嬀閫夌殑鏁版嵁椤癸紵")
+ .then(function () {
+ return del(ids);
+ })
+ .then(() => {
+ getList();
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ })
+ .catch(() => {});
+ }
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped>
+ .table_list {
+ margin-top: unset;
+ }
+</style>
diff --git a/src/views/productionManagement/processRoute/processRouteItem/index.vue b/src/views/productionManagement/processRoute/processRouteItem/index.vue
new file mode 100644
index 0000000..3c52410
--- /dev/null
+++ b/src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -0,0 +1,1864 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="宸ヨ壓璺嚎椤圭洰" />
+ <!-- 宸ヨ壓璺嚎淇℃伅灞曠ず -->
+ <el-card v-if="routeInfo.processRouteCode"
+ class="route-info-card"
+ shadow="hover">
+ <div class="route-info">
+ <div class="info-item">
+ <div class="info-label-wrapper">
+ <span class="info-label">宸ヨ壓璺嚎缂栧彿</span>
+ </div>
+ <div class="info-value-wrapper">
+ <span class="info-value">{{ routeInfo.processRouteCode }}</span>
+ </div>
+ </div>
+ <div class="info-item">
+ <div class="info-label-wrapper">
+ <span class="info-label">浜у搧鍚嶇О</span>
+ </div>
+ <div class="info-value-wrapper">
+ <span class="info-value">{{ routeInfo.productName || '-' }}</span>
+ </div>
+ </div>
+ <div class="info-item">
+ <div class="info-label-wrapper">
+ <span class="info-label">瑙勬牸鍚嶇О</span>
+ </div>
+ <div class="info-value-wrapper">
+ <span class="info-value">{{ routeInfo.model || '-' }}</span>
+ </div>
+ </div>
+ <div class="info-item">
+ <div class="info-label-wrapper">
+ <span class="info-label">BOM缂栧彿</span>
+ </div>
+ <div class="info-value-wrapper">
+ <span class="info-value">{{ routeInfo.bomNo || '-' }}</span>
+ </div>
+ </div>
+ <div class="info-item"
+ v-if="routeInfo.quantity && routeInfo.quantity !== 0">
+ <div class="info-label-wrapper">
+ <span class="info-label">闇�姹傛暟閲�</span>
+ </div>
+ <div class="info-value-wrapper">
+ <span class="info-value">{{ routeInfo.quantity || '-' }}</span>
+ </div>
+ </div>
+ <div class="info-item">
+ <div class="info-label-wrapper">
+ <span class="info-label">澶囨敞</span>
+ </div>
+ <div class="info-value-wrapper">
+ <span class="info-value">{{ routeInfo.description }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ <!-- 闄勪欢妯″潡 -->
+ <div v-if="pageType === 'order'"
+ class="section-header">
+ <div class="section-title">闄勪欢</div>
+ </div>
+ <el-card v-if="pageType === 'order'"
+ class="attachment-card"
+ shadow="hover"
+ style="margin-top: 10px; margin-bottom: 20px;">
+ <el-table :data="attachmentTableData"
+ border
+ class="attachment-table">
+ <el-table-column label="闄勪欢鍚嶇О"
+ prop="originalFilename"
+ show-overflow-tooltip />
+ <el-table-column fixed="right"
+ label="鎿嶄綔"
+ width="200"
+ align="center">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ size="small"
+ @click="downloadAttachmentFile(scope.row.downloadURL)">
+ 涓嬭浇
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+ <!-- 琛ㄦ牸瑙嗗浘 -->
+ <div v-if="viewMode === 'table'"
+ class="section-header">
+ <div class="section-title">宸ヨ壓璺嚎椤圭洰鍒楄〃</div>
+ <div class="section-actions">
+ <el-button icon="Grid"
+ @click="toggleView"
+ style="margin-right: 10px;">
+ 鍗$墖瑙嗗浘
+ </el-button>
+ <el-button v-if="editable"
+ type="primary"
+ @click="handleAdd">鏂板</el-button>
+ </div>
+ </div>
+ <el-table v-if="viewMode === 'table'"
+ ref="tableRef"
+ v-loading="tableLoading"
+ border
+ :data="tableData"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ row-key="id"
+ tooltip-effect="dark"
+ class="lims-table">
+ <el-table-column align="center"
+ label="搴忓彿"
+ width="60"
+ type="index" />
+ <el-table-column label="宸ュ簭鍚嶇О"
+ prop="technologyOperationId"
+ width="200">
+ <template #default="scope">
+ {{ scope.row.technologyOperationName || scope.row.operationName || '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙傛暟鍒楄〃"
+ min-width="160">
+ <template #default="scope">
+ <el-button type="primary"
+ link
+ size="small"
+ @click="handleViewParams(scope.row)">鍙傛暟鍒楄〃</el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName"
+ min-width="160" />
+ <el-table-column label="瑙勬牸鍚嶇О"
+ prop="model"
+ min-width="140" />
+ <el-table-column label="鍗曚綅"
+ prop="unit"
+ width="100" />
+ <el-table-column label="璁¤垂绫诲瀷"
+ prop="type"
+ width="100">
+ <template #default="scope">
+ {{scope.row.type==0 ? "璁℃椂" : "璁′欢"}}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁璐ㄦ"
+ prop="isQuality"
+ width="100">
+ <template #default="scope">
+ {{scope.row.isQuality ? "鏄�" : "鍚�"}}
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄惁鐢熶骇"
+ prop="isProduction"
+ width="100">
+ <template #default="scope">
+ {{scope.row.isProduction ? "鏄�" : "鍚�"}}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ align="center"
+ fixed="right"
+ width="150">
+ <template #default="scope">
+ <el-button type="primary"
+ link
+ size="small"
+ @click="handleEdit(scope.row)"
+ :disabled="scope.row.isComplete || !editable">缂栬緫</el-button>
+ <el-button type="danger"
+ link
+ size="small"
+ @click="handleDelete(scope.row)"
+ :disabled="scope.row.isComplete || !editable">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍗$墖瑙嗗浘 -->
+ <template v-else>
+ <div class="section-header">
+ <div class="section-title">宸ヨ壓璺嚎椤圭洰鍒楄〃</div>
+ <div class="section-actions">
+ <el-button icon="Menu"
+ @click="toggleView"
+ style="margin-right: 10px;">
+ 琛ㄦ牸瑙嗗浘
+ </el-button>
+ <el-button v-if="editable"
+ type="primary"
+ @click="handleAdd">鏂板</el-button>
+ </div>
+ </div>
+ <div v-loading="tableLoading"
+ class="card-container">
+ <div ref="cardsContainer"
+ class="cards-wrapper">
+ <div v-for="(item, index) in tableData"
+ :key="item.id || index"
+ class="process-card"
+ :data-index="index">
+ <!-- 搴忓彿鍦嗗湀 -->
+ <div class="card-header">
+ <div class="card-number">{{ index + 1 }}</div>
+ <div class="card-process-name">{{ item.technologyOperationName || item.operationName || '-' }}</div>
+ </div>
+ <!-- 浜у搧淇℃伅 -->
+ <div class="card-content">
+ <div v-if="item.productName"
+ class="product-info">
+ <div class="product-name">{{ item.productName }}</div>
+ <div v-if="item.model"
+ class="product-model">
+ {{ item.model }}
+ <!-- <span v-if="item.unit" class="product-unit">{{ item.unit }}</span> -->
+ </div>
+ <el-tag class="product-tag"
+ :type="item.type == 1 ? 'primary' : 'success'"
+ style="margin-left: 8px;">{{ item.type==0?'璁℃椂':'璁′欢' }}</el-tag>
+ <el-tag type="primary"
+ class="product-tag"
+ style="margin-left: 8px;"
+ v-if="item.isQuality">璐ㄦ</el-tag>
+ <el-tag type="primary"
+ class="product-tag"
+ style="margin-left: 8px;"
+ v-if="item.isProduction">鐢熶骇</el-tag>
+ </div>
+ <div v-else
+ class="product-info empty">鏆傛棤浜у搧淇℃伅</div>
+ </div>
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <div class="card-footer">
+ <el-button type="primary"
+ link
+ size="small"
+ @click="handleEdit(item)"
+ :disabled="item.isComplete || !editable">缂栬緫</el-button>
+ <el-button type="info"
+ link
+ size="small"
+ @click="handleViewParams(item)">鍙傛暟鍒楄〃</el-button>
+ <el-button type="danger"
+ link
+ size="small"
+ @click="handleDelete(item)"
+ :disabled="item.isComplete || !editable">鍒犻櫎</el-button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+ <!-- bom妯″潡 -->
+ <div class="section-header"
+ style="margin-top: 20px;">
+ <div class="section-title">BOM 缁撴瀯</div>
+ <div class="section-actions"
+ v-if="pageType === 'order' && editable">
+ <el-button v-if="!bomDataValue.isEdit"
+ type="primary"
+ @click="bomDataValue.isEdit = true">
+ 缂栬緫
+ </el-button>
+ <el-button v-if="bomDataValue.isEdit"
+ @click="cancelEditBom">
+ 鍙栨秷
+ </el-button>
+ <el-button v-if="bomDataValue.isEdit"
+ type="primary"
+ @click="handleSaveBom"
+ :loading="bomDataValue.loading">
+ 淇濆瓨BOM
+ </el-button>
+ </div>
+ </div>
+ <el-table :data="bomTableData"
+ border
+ :preserve-expanded-content="false"
+ :default-expand-all="true"
+ style="width: 100%">
+ <el-table-column type="expand">
+ <template #default>
+ <el-form ref="bomFormRef"
+ :model="bomDataValue">
+ <el-table :data="bomDataValue.dataList"
+ row-key="tempId"
+ default-expand-all
+ :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+ style="width: 100%">
+ <el-table-column prop="productName"
+ label="浜у搧" />
+ <el-table-column prop="model"
+ label="瑙勬牸">
+ <template #default="{ row }">
+ <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
+ :rules="[{ required: true, message: '璇烽�夋嫨瑙勬牸', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-select v-model="row.model"
+ placeholder="璇烽�夋嫨瑙勬牸"
+ clearable
+ :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)"
+ style="width: 100%"
+ @visible-change="(v) => { if (v) openBomDialog(row.tempId) }">
+ <el-option v-if="row.model"
+ :label="row.model"
+ :value="row.model" />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="processName"
+ label="娑堣�楀伐搴�">
+ <template #default="{ row }">
+ <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
+ :rules="bomDataValue.dataList.some(item => (item).tempId === row.tempId) ? [] : [{ required: true, message: '璇烽�夋嫨娑堣�楀伐搴�', trigger: 'change' }]"
+ style="margin: 0">
+ <el-select v-model="row.processId"
+ placeholder="璇烽�夋嫨"
+ filterable
+ clearable
+ style="width: 100%"
+ @change="value => handleBomProcessChange(row, value)"
+ :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)">
+ <el-option v-for="item in bomDataValue.processOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unitQuantity"
+ label="鍗曚綅浜у嚭鎵�闇�鏁伴噺">
+ <template #default="{ row }">
+ <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
+ :rules="[{ required: true, message: '璇疯緭鍏ュ崟浣嶄骇鍑烘墍闇�鏁伴噺', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input-number v-model="row.unitQuantity"
+ :min="0"
+ :precision="2"
+ :step="1"
+ controls-position="right"
+ style="width: 100%"
+ @change="handleUnitQuantityChange"
+ :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column v-if="pageType === 'order'"
+ prop="demandedQuantity"
+ label="闇�姹傛�婚噺">
+ <template #default="{ row }">
+ <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
+ :rules="[{ required: true, message: '璇疯緭鍏ラ渶姹傛�婚噺', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input-number v-model="row.demandedQuantity"
+ :min="0"
+ :precision="2"
+ :step="1"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="true" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unit"
+ label="鍗曚綅">
+ <template #default="{ row }">
+ <el-form-item v-if="pageType === 'order' && bomDataValue.isEdit"
+ :rules="[{ required: true, message: '璇疯緭鍏ュ崟浣�', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input v-model="row.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable
+ :disabled="!bomDataValue.isEdit || bomDataValue.dataList.some(item => (item).tempId === row.tempId)" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ fixed="right"
+ width="200"
+ v-if="pageType === 'order' && bomDataValue.isEdit">
+ <template #default="{ row }">
+ <el-button v-if="bomDataValue.isEdit && !bomDataValue.dataList.some(item => (item).tempId === row.tempId)"
+ type="danger"
+ text
+ @click="removeBomItem(row.tempId)">鍒犻櫎
+ </el-button>
+ <el-button v-if="bomDataValue.isEdit"
+ type="primary"
+ text
+ @click="addBomItem(row.tempId)">娣诲姞
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ </template>
+ </el-table-column>
+ <el-table-column label="BOM缂栧彿"
+ prop="bomNo" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model" />
+ </el-table>
+ <ProductSelectDialog v-if="bomDataValue.showProductDialog"
+ v-model="bomDataValue.showProductDialog"
+ :single="true"
+ @confirm="handleBomProduct" />
+ <!-- 涓婁紶缁勪欢寮圭獥 -->
+ <el-dialog v-model="uploadDialogVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeAttachmentUpload">
+ <AttachmentUpload v-model:file-list="newFileList" />
+ <template #footer>
+ <el-button @click="saveAttachmentUpload"
+ type="primary">淇濆瓨</el-button>
+ <el-button @click="closeAttachmentUpload">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ <!-- 鏂板/缂栬緫寮圭獥 -->
+ <el-dialog v-model="dialogVisible"
+ :title="operationType === 'add' ? '鏂板宸ヨ壓璺嚎椤圭洰' : '缂栬緫宸ヨ壓璺嚎椤圭洰'"
+ width="500px"
+ @close="closeDialog">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120px">
+ <el-form-item label="宸ュ簭"
+ v-if="operationType === 'add' || pageType === 'route'"
+ prop="technologyOperationId">
+ <el-select v-model="form.technologyOperationId"
+ placeholder="璇烽�夋嫨宸ュ簭"
+ clearable
+ @change="processChange"
+ style="width: 100%">
+ <el-option v-for="process in processOptions"
+ :key="process.id"
+ :label="process.name"
+ :value="process.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ュ簭"
+ v-else>
+ <span>{{ getProcessName(form.technologyOperationId) }}</span>
+ </el-form-item>
+ <el-form-item label="浜у搧鍚嶇О"
+ v-if="operationType === 'add' || pageType === 'route'"
+ prop="productModelId">
+ <el-button type="primary"
+ @click="showProductSelectDialog = true">
+ {{ form.productName
+ ? (form.model ? `${form.productName} - ${form.model}` : form.productName)
+ : '閫夋嫨浜у搧' }}
+ </el-button>
+ </el-form-item>
+ <el-form-item label="浜у搧鍚嶇О"
+ v-else>
+ <span>{{ form.productName }}{{ form.model ? ' - ' + form.model : '' }}</span>
+ </el-form-item>
+ <el-form-item label="鍗曚綅"
+ v-if="operationType === 'add' || pageType === 'route'"
+ prop="unit">
+ <el-input v-model="form.unit"
+ :placeholder="form.productModelId ? '鏍规嵁閫夋嫨鐨勪骇鍝佽嚜鍔ㄥ甫鍑�' : '璇峰厛閫夋嫨浜у搧'"
+ clearable
+ :disabled="true" />
+ </el-form-item>
+ <el-form-item label="鍗曚綅"
+ v-else>
+ <span>{{ form.unit }}</span>
+ </el-form-item>
+ <el-form-item label="璁¤垂绫诲瀷"
+ prop="type">
+ <el-radio-group v-model="form.type">
+ <el-radio :label="0">璁℃椂</el-radio>
+ <el-radio :label="1">璁′欢</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ"
+ prop="isQuality">
+ <el-switch v-model="form.isQuality"
+ :active-value="true"
+ :inactive-value="false" />
+ </el-form-item>
+ <el-form-item label="鏄惁鐢熶骇"
+ prop="isProduction">
+ <el-switch v-model="form.isProduction"
+ :active-value="true"
+ :inactive-value="false" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary"
+ @click="handleSubmit"
+ :loading="submitLoading">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ <!-- 浜у搧閫夋嫨瀵硅瘽妗� -->
+ <ProductSelectDialog v-model="showProductSelectDialog"
+ @confirm="handleProductSelect"
+ single />
+ <!-- 鍙傛暟鍒楄〃瀵硅瘽妗� -->
+ <ProcessParamListDialog v-model="showParamListDialog"
+ :title="`${currentProcess ? (currentProcess.processName || currentProcess.technologyOperationName || currentProcess.operationName) : ''} - 鍙傛暟鍒楄〃`"
+ :route-id="routeId"
+ :order-id="orderId"
+ :process="currentProcess"
+ :page-type="pageType"
+ :param-list="paramList"
+ :editable="editable"
+ @getsyncProcessParamItem="getsyncProcessParamItem"
+ @refresh="refreshParamList" />
+ </div>
+</template>
+
+<script setup>
+ import {
+ ref,
+ computed,
+ getCurrentInstance,
+ onMounted,
+ onUnmounted,
+ nextTick,
+ } from "vue";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+ import ProcessParamListDialog from "@/components/ProcessParamListDialog.vue";
+ import {
+ findProcessRouteItemList,
+ addOrUpdateProcessRouteItem,
+ addOrUpdateProcessRouteItem1,
+ sortProcessRouteItem,
+ batchDeleteProcessRouteItem,
+ getProcessParamList,
+ } from "@/api/productionManagement/processRouteItem.js";
+ import {
+ syncProcessParamItem,
+ syncProcessParamItemOrder,
+ } from "@/api/productionManagement/processRouteItem.js";
+ import {
+ findProductProcessRouteItemList,
+ deleteRouteItem,
+ addRouteItem,
+ findProcessParamListOrder,
+ addOrUpdateProductProcessRouteItem,
+ sortRouteItem,
+ } from "@/api/productionManagement/productProcessRoute.js";
+ import { processList } from "@/api/productionManagement/productionProcess.js";
+ import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
+ import {
+ queryList,
+ queryList2,
+ add2,
+ } from "@/api/productionManagement/productStructure.js";
+ import AttachmentUpload from "@/components/AttachmentUpload/file/index.vue";
+ import {
+ attachmentList,
+ deleteAttachment,
+ createAttachment,
+ } from "@/api/basicData/storageAttachment.js";
+
+ import { useRoute } from "vue-router";
+ import { ElMessageBox, ElMessage } from "element-plus";
+ import Sortable from "sortablejs";
+
+ const route = useRoute();
+ const { proxy } = getCurrentInstance() || {};
+
+ const routeId = computed(() => route.query.id);
+ const orderId = computed(() => route.query.orderId);
+ const pageType = computed(() => route.query.type);
+ const editable = computed(() => route.query.editable !== "false");
+ const technologyRoutingId = computed(() => route.query.technologyRoutingId);
+
+ const tableLoading = ref(false);
+ const tableData = ref([]);
+ const dialogVisible = ref(false);
+ const operationType = ref("add"); // add | edit
+ const formRef = ref(null);
+ const bomFormRef = ref(null);
+ const submitLoading = ref(false);
+ const cardsContainer = ref(null);
+ const tableRef = ref(null);
+ const viewMode = ref("card"); // table | card
+ const routeInfo = ref({
+ processRouteCode: "",
+ productName: "",
+ model: "",
+ bomNo: "",
+ description: "",
+ quantity: 0,
+ technologyRoutingId: "",
+ });
+
+ // 闄勪欢鐩稿叧
+ const attachmentTableData = ref([]);
+ const uploadDialogVisible = ref(false);
+ const newFileList = ref([]);
+
+ const getAttachmentList = () => {
+ if (!technologyRoutingId.value) return;
+ attachmentList({
+ recordType: "technology_routing",
+ recordId: technologyRoutingId.value,
+ }).then(res => {
+ attachmentTableData.value = (res && res.data) || [];
+ });
+ };
+
+ const handleUploadAttachment = () => {
+ uploadDialogVisible.value = true;
+ };
+
+ const saveAttachmentUpload = async () => {
+ if (newFileList.value.length > 0) {
+ createAttachment({
+ application: "file",
+ recordType: "technology_routing",
+ recordId: technologyRoutingId.value,
+ storageBlobDTOs: [...newFileList.value, ...attachmentTableData.value],
+ })
+ .then(res => {
+ if (res && res.code === 200) {
+ proxy?.$modal?.msgSuccess("涓婁紶鎴愬姛");
+ newFileList.value = [];
+ getAttachmentList();
+ }
+ })
+ .finally(() => {
+ uploadDialogVisible.value = false;
+ });
+ }
+ };
+
+ const closeAttachmentUpload = () => {
+ newFileList.value = [];
+ uploadDialogVisible.value = false;
+ };
+
+ const handleDeleteAttachment = async row => {
+ deleteAttachment([row.storageAttachmentId]).then(res => {
+ if (res && res.code === 200) {
+ proxy?.$modal?.msgSuccess("鍒犻櫎鎴愬姛");
+ getAttachmentList();
+ }
+ });
+ };
+
+ const downloadAttachmentFile = url => {
+ window.open(url, "_blank");
+ };
+
+ const processOptions = ref([]);
+ const showProductSelectDialog = ref(false);
+ const showParamListDialog = ref(false);
+ const currentProcess = ref(null);
+ const paramList = ref([]);
+ let tableSortable = null;
+ let cardSortable = null;
+
+ // 鍒囨崲瑙嗗浘
+ const toggleView = () => {
+ viewMode.value = viewMode.value === "table" ? "card" : "table";
+ // 鍒囨崲瑙嗗浘鍚庨噸鏂板垵濮嬪寲鎷栨嫿鎺掑簭
+ nextTick(() => {
+ initSortable();
+ });
+ };
+
+ const form = ref({
+ id: undefined,
+ routeId: routeId.value,
+ technologyOperationId: undefined,
+ productModelId: undefined,
+ productName: "",
+ model: "",
+ unit: "",
+ isQuality: false,
+ type: 0,
+ isProduction: false,
+ });
+
+ const rules = {
+ technologyOperationId: [
+ { required: true, message: "璇烽�夋嫨宸ュ簭", trigger: "change" },
+ ],
+ productModelId: [
+ { required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" },
+ ],
+ };
+
+ const getsyncProcessParamItem = () => {
+ ElMessageBox.confirm("鏄惁瑕嗙洊褰撳墠宸ュ簭宸插瓨鍦ㄥ弬鏁帮紵", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ if (pageType.value === "order") {
+ syncProcessParamItemOrder({
+ replaceExisting: true,
+ technologyRoutingOperationId: currentProcess.value.id,
+ }).then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍚屾鎴愬姛");
+ refreshParamList();
+ } else {
+ ElMessage.error(res.msg || "鍚屾澶辫触");
+ }
+ });
+ } else {
+ syncProcessParamItem({
+ replaceExisting: true,
+ technologyRoutingOperationId: currentProcess.value.id,
+ }).then(res => {
+ if (res.code === 200) {
+ ElMessage.success("鍚屾鎴愬姛");
+ refreshParamList();
+ } else {
+ ElMessage.error(res.msg || "鍚屾澶辫触");
+ }
+ });
+ }
+ })
+ .catch(() => {});
+ };
+
+ // 鏍规嵁宸ュ簭ID鑾峰彇宸ュ簭鍚嶇О
+ const getProcessName = technologyOperationId => {
+ if (!technologyOperationId) return "";
+ const process = processOptions.value.find(
+ p => p.id === technologyOperationId
+ );
+ return process ? process.name : "";
+ };
+
+ // 鑾峰彇鍒楄〃
+ const getList = () => {
+ tableLoading.value = true;
+ const listPromise =
+ pageType.value === "order"
+ ? findProductProcessRouteItemList({ orderId: orderId.value })
+ : findProcessRouteItemList({ routeId: routeId.value });
+
+ listPromise
+ .then(res => {
+ tableData.value = res.data || [];
+ tableLoading.value = false;
+ // 鍒楄〃鍔犺浇瀹屾垚鍚庡垵濮嬪寲鎷栨嫿鎺掑簭
+ nextTick(() => {
+ initSortable();
+ });
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+ proxy?.$modal?.msgError("鑾峰彇鍒楄〃澶辫触");
+ });
+ };
+
+ // 鑾峰彇宸ュ簭鍒楄〃
+ const getProcessList = () => {
+ processList({ size: -1, current: -1 })
+ .then(res => {
+ processOptions.value = res.data.records || [];
+ bomDataValue.value.processOptions = processOptions.value;
+ })
+ .catch(err => {
+ console.error("鑾峰彇宸ュ簭澶辫触锛�", err);
+ });
+ };
+
+ // 鑾峰彇宸ヨ壓璺嚎璇︽儏锛堜粠璺敱鍙傛暟鑾峰彇锛�
+ const getRouteInfo = () => {
+ routeInfo.value = {
+ processRouteCode: route.query.processRouteCode || "",
+ productName: route.query.productName || "",
+ model: route.query.model || "",
+ bomNo: route.query.bomNo || "",
+ bomId: route.query.bomId || "",
+ description: route.query.description || "",
+ quantity: route.query.quantity || 0,
+ technologyRoutingId: route.query.technologyRoutingId || "",
+ status: !(route.query.status == 1 || route.query.status === "false"),
+ };
+ bomTableData.value[0].productName = routeInfo.value.productName;
+ bomTableData.value[0].model = routeInfo.value.model;
+ bomTableData.value[0].bomNo = routeInfo.value.bomNo;
+ };
+
+ // 鏂板
+ const handleAdd = () => {
+ operationType.value = "add";
+ resetForm();
+ dialogVisible.value = true;
+ };
+
+ // 缂栬緫
+ const handleEdit = row => {
+ operationType.value = "edit";
+ form.value = {
+ id: row.id,
+ routeId: routeId.value,
+ technologyOperationId: row.technologyOperationId,
+ productModelId: row.productModelId,
+ productName: row.productName || "",
+ model: row.model || "",
+ unit: row.unit || "",
+ isQuality: row.isQuality,
+ type: row.type || 0,
+ isProduction: row.isProduction,
+ };
+ dialogVisible.value = true;
+ };
+
+ // 鍒犻櫎
+ const handleDelete = row => {
+ ElMessageBox.confirm("纭鍒犻櫎璇ュ伐鑹鸿矾绾块」鐩紵", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // 鐢熶骇璁㈠崟涓嬩娇鐢� productProcessRoute 鐨勫垹闄ゆ帴鍙o紙璺敱鍚庢嫾鎺� id锛夛紝鍏跺畠鎯呭喌浣跨敤宸ヨ壓璺嚎椤圭洰鎵归噺鍒犻櫎鎺ュ彛
+ const deletePromise =
+ pageType.value === "order"
+ ? deleteRouteItem(row.id)
+ : batchDeleteProcessRouteItem([row.id]);
+
+ deletePromise
+ .then(() => {
+ proxy?.$modal?.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy?.$modal?.msgError("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {});
+ };
+
+ // 浜у搧閫夋嫨
+ const handleProductSelect = products => {
+ console.log(products, "===products===");
+ if (products && products.length > 0) {
+ const product = products[0];
+ console.log(product, "product");
+ form.value = {
+ ...form.value,
+ productModelId: product.id,
+ productName: product.productName,
+ model: product.model,
+ unit: product.unit || "",
+ };
+ showProductSelectDialog.value = false;
+ // 瑙﹀彂琛ㄥ崟楠岃瘉
+ // formRef.value?.validateField("productModelId");
+ }
+ };
+
+ // 鎻愪氦
+ const handleSubmit = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ submitLoading.value = true;
+
+ if (operationType.value === "add") {
+ // 鏂板锛氫紶鍗曚釜瀵硅薄锛屽寘鍚玠ragSort瀛楁
+ // dragSort = 褰撳墠鍒楄〃闀垮害 + 1锛岃〃绀烘柊澧炶褰曟帓鍦ㄦ渶鍚�
+ const dragSort = tableData.value.length + 1;
+ const isOrderPage = pageType.value === "order";
+
+ const addPromise = isOrderPage
+ ? addRouteItem({
+ productionOrderId: Number(orderId.value),
+ orderRoutingId: Number(routeId.value),
+ technologyOperationId: form.value.technologyOperationId,
+ technologyRoutingId: Number(routeId.value),
+ operationName: getProcessName(form.value.technologyOperationId),
+ productModelId: form.value.productModelId,
+ isQuality: form.value.isQuality,
+ type: form.value.type,
+ isProduction: form.value.isProduction,
+ dragSort,
+ })
+ : addOrUpdateProcessRouteItem({
+ technologyRoutingId: Number(routeId.value),
+ technologyOperationId: form.value.technologyOperationId,
+ productModelId: form.value.productModelId,
+ isQuality: form.value.isQuality,
+ type: form.value.type,
+ isProduction: form.value.isProduction,
+ dragSort,
+ });
+
+ addPromise
+ .then(() => {
+ proxy?.$modal?.msgSuccess("鏂板鎴愬姛");
+ closeDialog();
+ getList();
+ })
+ .catch(() => {
+ proxy?.$modal?.msgError("鏂板澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ } else {
+ // 缂栬緫锛氱敓浜ц鍗曚笅浣跨敤 productProcessRoute/updateRouteItem锛屽叾瀹冩儏鍐典娇鐢ㄥ伐鑹鸿矾绾块」鐩洿鏂版帴鍙�
+ const isOrderPage = pageType.value === "order";
+
+ const updatePromise = isOrderPage
+ ? addOrUpdateProductProcessRouteItem({
+ id: form.value.id,
+ technologyOperationId: form.value.technologyOperationId,
+ operationName: getProcessName(form.value.technologyOperationId),
+ productModelId: form.value.productModelId,
+ isQuality: form.value.isQuality,
+ type: form.value.type,
+ isProduction: form.value.isProduction,
+ })
+ : addOrUpdateProcessRouteItem1({
+ technologyRoutingId: Number(routeId.value),
+ technologyOperationId: form.value.technologyOperationId,
+ productModelId: form.value.productModelId,
+ id: form.value.id,
+ isQuality: form.value.isQuality,
+ type: form.value.type,
+ isProduction: form.value.isProduction,
+ });
+
+ updatePromise
+ .then(() => {
+ proxy?.$modal?.msgSuccess("淇敼鎴愬姛");
+ closeDialog();
+ getList();
+ })
+ .catch(() => {
+ proxy?.$modal?.msgError("淇敼澶辫触");
+ })
+ .finally(() => {
+ submitLoading.value = false;
+ });
+ }
+ }
+ });
+ };
+
+ // 閲嶇疆琛ㄥ崟
+ const resetForm = () => {
+ form.value = {
+ id: undefined,
+ routeId: routeId.value,
+ technologyOperationId: undefined,
+ productModelId: undefined,
+ productName: "",
+ model: "",
+ unit: "",
+ isQuality: false,
+ type: 0,
+ isProduction: false,
+ };
+ formRef.value?.resetFields();
+ };
+
+ // 鍏抽棴寮圭獥
+ const closeDialog = () => {
+ dialogVisible.value = false;
+ resetForm();
+ };
+
+ // 鏌ョ湅鍙傛暟鍒楄〃
+ const handleViewParams = row => {
+ currentProcess.value = row;
+ const param = {
+ productionOrderRoutingOperationId: row.id,
+ productionOrderId: orderId.value,
+ };
+ const param1 = {
+ technologyRoutingOperationId: row.id,
+ productionOrderId: orderId.value,
+ };
+
+ const apiPromise =
+ pageType.value === "order"
+ ? findProcessParamListOrder(param)
+ : getProcessParamList(param1);
+
+ apiPromise
+ .then(res => {
+ paramList.value = res.data || [];
+ showParamListDialog.value = true;
+ })
+ .catch(err => {
+ console.error("鑾峰彇鍙傛暟鍒楄〃澶辫触锛�", err);
+ proxy?.$modal?.msgError("鑾峰彇鍙傛暟鍒楄〃澶辫触");
+ });
+ };
+
+ // 鍒锋柊鍙傛暟鍒楄〃
+ const refreshParamList = () => {
+ if (currentProcess.value) {
+ handleViewParams(currentProcess.value);
+ }
+ };
+
+ // 鍒濆鍖栨嫋鎷芥帓搴�
+ const initSortable = () => {
+ destroySortable();
+ if (!editable.value) return;
+
+ if (viewMode.value === "table") {
+ // 琛ㄦ牸瑙嗗浘鐨勬嫋鎷芥帓搴�
+ if (!tableRef.value) return;
+
+ const tbody =
+ tableRef.value.$el.querySelector(".el-table__body tbody") ||
+ tableRef.value.$el.querySelector(
+ ".el-table__body-wrapper > table > tbody"
+ );
+
+ if (!tbody) return;
+
+ tableSortable = new Sortable(tbody, {
+ animation: 150,
+ ghostClass: "sortable-ghost",
+ handle: ".el-table__row",
+ filter: ".el-button, .el-select",
+ onEnd: evt => {
+ if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex])
+ return;
+
+ // 閲嶆柊鎺掑簭鏁扮粍
+ const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
+ tableData.value.splice(evt.newIndex, 0, moveItem);
+
+ // 璁$畻鏂扮殑搴忓彿锛坉ragSort浠�1寮�濮嬶級
+ const newIndex = evt.newIndex;
+ const dragSort = newIndex + 1;
+
+ // 璋冪敤鎺掑簭鎺ュ彛
+ if (moveItem.id) {
+ const isOrderPage = pageType.value === "order";
+ const sortPromise = isOrderPage
+ ? sortRouteItem({
+ id: moveItem.id,
+ dragSort: dragSort,
+ })
+ : sortProcessRouteItem({
+ id: moveItem.id,
+ dragSort: dragSort,
+ });
+
+ sortPromise
+ .then(() => {
+ // 鏇存柊鎵�鏈夎鐨刣ragSort
+ tableData.value.forEach((item, index) => {
+ if (item.id) {
+ item.dragSort = index + 1;
+ }
+ });
+ proxy?.$modal?.msgSuccess("鎺掑簭鎴愬姛");
+ })
+ .catch(err => {
+ // 鎺掑簭澶辫触锛屾仮澶嶅師鏁扮粍
+ tableData.value.splice(newIndex, 1);
+ tableData.value.splice(evt.oldIndex, 0, moveItem);
+ proxy?.$modal?.msgError("鎺掑簭澶辫触");
+ console.error("鎺掑簭澶辫触锛�", err);
+ });
+ }
+ },
+ });
+ } else {
+ // 鍗$墖瑙嗗浘鐨勬嫋鎷芥帓搴�
+ if (!cardsContainer.value) return;
+
+ cardSortable = new Sortable(cardsContainer.value, {
+ animation: 150,
+ ghostClass: "sortable-ghost",
+ handle: ".process-card",
+ filter: ".el-button",
+ onEnd: evt => {
+ if (evt.oldIndex === evt.newIndex || !tableData.value[evt.oldIndex])
+ return;
+
+ // 閲嶆柊鎺掑簭鏁扮粍
+ const moveItem = tableData.value.splice(evt.oldIndex, 1)[0];
+ tableData.value.splice(evt.newIndex, 0, moveItem);
+
+ // 璁$畻鏂扮殑搴忓彿锛坉ragSort浠�1寮�濮嬶級
+ const newIndex = evt.newIndex;
+ const dragSort = newIndex + 1;
+
+ // 璋冪敤鎺掑簭鎺ュ彛
+ if (moveItem.id) {
+ const isOrderPage = pageType.value === "order";
+ const sortPromise = isOrderPage
+ ? sortRouteItem({
+ id: moveItem.id,
+ dragSort: dragSort,
+ })
+ : sortProcessRouteItem({
+ id: moveItem.id,
+ dragSort: dragSort,
+ });
+
+ sortPromise
+ .then(() => {
+ // 鏇存柊鎵�鏈夎鐨刣ragSort
+ tableData.value.forEach((item, index) => {
+ if (item.id) {
+ item.dragSort = index + 1;
+ }
+ });
+ proxy?.$modal?.msgSuccess("鎺掑簭鎴愬姛");
+ })
+ .catch(err => {
+ // 鎺掑簭澶辫触锛屾仮澶嶅師鏁扮粍
+ tableData.value.splice(newIndex, 1);
+ tableData.value.splice(evt.oldIndex, 0, moveItem);
+ proxy?.$modal?.msgError("鎺掑簭澶辫触");
+ console.error("鎺掑簭澶辫触锛�", err);
+ });
+ }
+ },
+ });
+ }
+ };
+
+ // 閿�姣佹嫋鎷芥帓搴�
+ const destroySortable = () => {
+ if (tableSortable) {
+ tableSortable.destroy();
+ tableSortable = null;
+ }
+ if (cardSortable) {
+ cardSortable.destroy();
+ cardSortable = null;
+ }
+ };
+
+ // BOM鐩稿叧鐘舵�佸拰鏂规硶
+ const bomTableData = ref([
+ {
+ productName: "",
+ model: "",
+ bomNo: "",
+ },
+ ]);
+
+ const bomDataValue = ref({
+ dataList: [],
+ processOptions: [],
+ showProductDialog: false,
+ currentRowName: null,
+ loading: false,
+ isEdit: false,
+ });
+
+ const syncProcessOperationFields = item => {
+ const processId =
+ item.processId ?? item.operationId ?? item.technologyOperationId ?? "";
+ if (!processId) {
+ item.processId = "";
+ return;
+ }
+ const option = bomDataValue.value.processOptions.find(
+ p => p.id === processId
+ );
+ const processName =
+ option?.name || item.processName || item.operationName || "";
+
+ item.processId = processId;
+ if (pageType.value === "order") {
+ item.technologyOperationId = processId;
+ } else {
+ item.operationId = processId;
+ }
+ item.processName = processName;
+ item.operationName = processName;
+ };
+
+ const normalizeTreeData = items => {
+ items.forEach(item => {
+ item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`;
+ syncProcessOperationFields(item);
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ normalizeTreeData(item.children);
+ }
+ });
+ };
+
+ const toQuantityNumber = value => {
+ const numberValue = Number(value);
+ if (!Number.isFinite(numberValue)) {
+ return 0;
+ }
+ return Number(numberValue.toFixed(2));
+ };
+
+ const syncDemandedQuantityTree = (items, parentDemandedQuantity = null) => {
+ items.forEach(item => {
+ if (parentDemandedQuantity !== null) {
+ item.demandedQuantity = toQuantityNumber(
+ parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
+ );
+ }
+
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ syncDemandedQuantityTree(
+ item.children,
+ toQuantityNumber(item.demandedQuantity)
+ );
+ }
+ });
+ };
+
+ const recalculateDemandedQuantities = () => {
+ if (pageType.value !== "order") {
+ return;
+ }
+
+ const rootDemandedQuantity = routeInfo.value.quantity;
+ if (
+ rootDemandedQuantity === undefined ||
+ rootDemandedQuantity === null ||
+ rootDemandedQuantity === ""
+ ) {
+ syncDemandedQuantityTree(bomDataValue.value.dataList);
+ return;
+ }
+
+ syncDemandedQuantityTree(
+ bomDataValue.value.dataList,
+ toQuantityNumber(rootDemandedQuantity)
+ );
+ };
+
+ const processChange = value => {
+ processOptions.value.forEach(item => {
+ if (item.id == value) {
+ form.value.isQuality = item.isQuality;
+ form.value.type = item.type || 0;
+ form.value.isProduction = item.isProduction;
+ }
+ });
+ };
+
+ const findSiblings = (items, tempId) => {
+ if (!items || items.length === 0) return null;
+ // 妫�鏌ュ綋鍓嶅眰绾�
+ if (items.some(item => item.tempId === tempId)) {
+ return items;
+ }
+ // 閫掑綊鏌ユ壘瀛愮骇
+ for (const item of items) {
+ if (item.children && item.children.length > 0) {
+ const result = findSiblings(item.children, tempId);
+ if (result) return result;
+ }
+ }
+ return null;
+ };
+
+ const handleBomProcessChange = (row, value) => {
+ row.processId = value || "";
+ syncProcessOperationFields(row);
+
+ // 妫�鏌ュ悓涓�灞傜骇鏄惁宸茬粡鏈夊叾浠栦笉鍚岀殑宸ュ簭琚�変腑
+ const siblings = findSiblings(bomDataValue.value.dataList, row.tempId);
+ if (siblings && value) {
+ const hasDifferentProcess = siblings.some(sibling => {
+ return (
+ sibling.tempId !== row.tempId &&
+ sibling.processId &&
+ sibling.processId !== value
+ );
+ });
+ if (hasDifferentProcess) {
+ ElMessage.warning("鍚屼竴灞傜骇宸插瓨鍦ㄤ笉鍚岀殑宸ュ簭锛岃鍏堢粺涓�宸ュ簭鍚庡啀杩涜淇敼");
+ }
+ }
+ };
+
+ const openBomDialog = tempId => {
+ bomDataValue.value.currentRowName = tempId;
+ bomDataValue.value.showProductDialog = true;
+ };
+
+ const fetchBomData = async () => {
+ try {
+ const isOrderPage = pageType.value === "order";
+ const { data } = await (isOrderPage ? queryList2 : queryList)(
+ routeInfo.value.bomId
+ );
+ bomDataValue.value.dataList = data || [];
+ normalizeTreeData(bomDataValue.value.dataList);
+ recalculateDemandedQuantities();
+ } catch (err) {
+ console.error("鑾峰彇BOM鏁版嵁澶辫触锛�", err);
+ }
+ };
+
+ const childItem = (item, tempId, productData) => {
+ if (item.tempId === tempId) {
+ item.productName = productData.productName;
+ item.model = productData.model;
+ item.productModelId = productData.id;
+ item.unit = productData.unit || "";
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ for (let child of item.children) {
+ if (childItem(child, tempId, productData)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ const handleBomProduct = row => {
+ if (!Array.isArray(row) || row.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�涓骇鍝�");
+ return;
+ }
+ const productData = row[row.length - 1];
+
+ const isTopLevel = bomDataValue.value.dataList.some(
+ item => item.tempId === bomDataValue.value.currentRowName
+ );
+ if (isTopLevel) {
+ if (
+ productData.productName === bomTableData.value[0].productName &&
+ productData.model === bomTableData.value[0].model
+ ) {
+ const hasOther = bomDataValue.value.dataList.some(
+ item =>
+ item.tempId !== bomDataValue.value.currentRowName &&
+ item.productName === bomTableData.value[0].productName &&
+ item.model === bomTableData.value[0].model
+ );
+ if (hasOther) {
+ ElMessage.warning("鏈�澶栧眰鍜屽綋鍓嶄骇鍝佷竴鏍风殑涓�绾у彧鑳芥湁涓�涓�");
+ return;
+ }
+ }
+ }
+ bomDataValue.value.dataList.forEach(item => {
+ if (item.tempId === bomDataValue.value.currentRowName) {
+ item.productName = productData.productName;
+ item.model = productData.model;
+ item.productModelId = productData.id;
+ item.unit = productData.unit || "";
+ return;
+ }
+ childItem(item, bomDataValue.value.currentRowName, productData);
+ });
+ bomDataValue.value.showProductDialog = false;
+ };
+
+ const removeBomItem = tempId => {
+ const topIndex = bomDataValue.value.dataList.findIndex(
+ item => item.tempId === tempId
+ );
+ if (topIndex !== -1) {
+ bomDataValue.value.dataList.splice(topIndex, 1);
+ return;
+ }
+
+ const delchildItem = (items, tempId) => {
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.tempId === tempId) {
+ items.splice(i, 1);
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ if (delchildItem(item.children, tempId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ bomDataValue.value.dataList.forEach(item => {
+ if (item.children && item.children.length > 0) {
+ delchildItem(item.children, tempId);
+ }
+ });
+ };
+
+ const handleUnitQuantityChange = () => {
+ recalculateDemandedQuantities();
+ };
+
+ const addchildItem = (item, tempId) => {
+ if (item.tempId === tempId) {
+ if (!item.children) {
+ item.children = [];
+ }
+ item.children.push({
+ parentId: item.id || "",
+ parentTempId: item.tempId || "",
+ productName: "",
+ productId: "",
+ model: undefined,
+ productModelId: undefined,
+ processId: "",
+ processName: "",
+ [pageType.value === "order" ? "technologyOperationId" : "operationId"]:
+ "",
+ operationName: "",
+ unitQuantity: 1,
+ demandedQuantity: 0,
+ children: [],
+ unit: "",
+ tempId: new Date().getTime(),
+ });
+ recalculateDemandedQuantities();
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ for (let child of item.children) {
+ if (addchildItem(child, tempId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ const addBomItem = tempId => {
+ bomDataValue.value.dataList.forEach(item => {
+ if (item.tempId === tempId) {
+ if (!item.children) {
+ item.children = [];
+ }
+ item.children.push({
+ parentId: item.id || "",
+ parentTempId: item.tempId || "",
+ productName: "",
+ productId: "",
+ model: undefined,
+ productModelId: undefined,
+ processId: "",
+ processName: "",
+ [pageType.value === "order" ? "technologyOperationId" : "operationId"]:
+ "",
+ operationName: "",
+ unitQuantity: 1,
+ demandedQuantity: 0,
+ unit: "",
+ children: [],
+ tempId: new Date().getTime(),
+ });
+ recalculateDemandedQuantities();
+ return;
+ }
+ addchildItem(item, tempId);
+ });
+ };
+
+ const validateAllBom = () => {
+ let isValid = true;
+ const isOrderPage = pageType.value === "order";
+
+ const validateItem = (item, isTopLevel = false) => {
+ if (!item.model) {
+ ElMessage.error("璇烽�夋嫨瑙勬牸");
+ isValid = false;
+ return;
+ }
+ if (!isTopLevel && !item.processId) {
+ ElMessage.error("璇烽�夋嫨娑堣�楀伐搴�");
+ isValid = false;
+ return;
+ }
+ if (!item.unitQuantity) {
+ ElMessage.error("璇疯緭鍏ュ崟浣嶄骇鍑烘墍闇�鏁伴噺");
+ isValid = false;
+ return;
+ }
+ if (isOrderPage && !item.demandedQuantity) {
+ ElMessage.error("璇疯緭鍏ラ渶姹傛�婚噺");
+ isValid = false;
+ return;
+ }
+
+ if (item.children && item.children.length > 0) {
+ item.children.forEach(child => {
+ validateItem(child, false);
+ });
+ }
+ };
+
+ // 鏍¢獙鍚屼竴灞傜骇鐨勫伐搴忔槸鍚︿竴鑷�
+ const validateProcessConsistency = items => {
+ if (!items || items.length === 0) return;
+
+ // 妫�鏌ュ綋鍓嶅眰绾�
+ const processes = items
+ .filter(item => item.processId)
+ .map(item => item.processId);
+ if (processes.length > 1) {
+ const uniqueProcesses = [...new Set(processes)];
+ if (uniqueProcesses.length > 1) {
+ ElMessage.error("鍚屼竴灞傜骇鐨勫伐搴忓繀椤讳竴鑷�");
+ isValid = false;
+ return;
+ }
+ }
+
+ // 閫掑綊妫�鏌ュ瓙绾�
+ items.forEach(item => {
+ if (item.children && item.children.length > 0) {
+ validateProcessConsistency(item.children);
+ }
+ });
+ };
+
+ bomDataValue.value.dataList.forEach(item => {
+ validateItem(item, true);
+ });
+
+ validateProcessConsistency(bomDataValue.value.dataList);
+
+ return isValid;
+ };
+
+ const buildSubmitTree = items => {
+ return items.map(item => {
+ const current = { ...item };
+ syncProcessOperationFields(current);
+ current.children = Array.isArray(current.children)
+ ? buildSubmitTree(current.children)
+ : [];
+ return current;
+ });
+ };
+
+ const cancelEditBom = () => {
+ bomDataValue.value.isEdit = false;
+ fetchBomData();
+ };
+
+ const handleSaveBom = () => {
+ bomDataValue.value.loading = true;
+ console.log(bomDataValue.value.dataList, "bomDataValue.value.dataList");
+
+ normalizeTreeData(bomDataValue.value.dataList);
+ recalculateDemandedQuantities();
+
+ const valid = validateAllBom();
+ if (valid) {
+ add2({
+ // bomId: Number(routeInfo.value.bomId),
+ productionOrderBomId: Number(routeInfo.value.bomId) || null,
+ children: buildSubmitTree(bomDataValue.value.dataList || []),
+ })
+ .then(() => {
+ ElMessage.success("BOM淇濆瓨鎴愬姛");
+ bomDataValue.value.isEdit = false;
+ refreshCurrentPage();
+ })
+ .catch(() => {
+ ElMessage.error("BOM淇濆瓨澶辫触");
+ })
+ .finally(() => {
+ bomDataValue.value.loading = false;
+ });
+ } else {
+ bomDataValue.value.loading = false;
+ }
+ };
+
+ const refreshCurrentPage = () => {
+ getRouteInfo();
+ getList();
+ getProcessList();
+ fetchBomData();
+ if (pageType.value === "order") {
+ getAttachmentList();
+ }
+ };
+
+ onMounted(() => {
+ refreshCurrentPage();
+ });
+
+ onUnmounted(() => {
+ destroySortable();
+ });
+</script>
+
+<style scoped>
+ .card-container {
+ padding: 20px 0;
+ }
+
+ .cards-wrapper {
+ display: flex;
+ gap: 16px;
+ overflow-x: auto;
+ padding: 10px 0;
+ min-height: 200px;
+ }
+
+ .cards-wrapper::-webkit-scrollbar {
+ height: 8px;
+ }
+
+ .cards-wrapper::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+ }
+
+ .cards-wrapper::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 4px;
+ }
+
+ .cards-wrapper::-webkit-scrollbar-thumb:hover {
+ background: #a8a8a8;
+ }
+
+ .process-card {
+ flex-shrink: 0;
+ width: 220px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ cursor: move;
+ transition: all 0.3s;
+ }
+
+ .process-card:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transform: translateY(-2px);
+ }
+
+ .card-header {
+ text-align: center;
+ margin-bottom: 12px;
+ }
+
+ .card-number {
+ width: 36px;
+ height: 36px;
+ line-height: 36px;
+ border-radius: 50%;
+ background: #409eff;
+ color: #fff;
+ font-weight: bold;
+ font-size: 16px;
+ margin: 0 auto 8px;
+ }
+
+ .card-process-name {
+ font-size: 14px;
+ color: #333;
+ font-weight: 500;
+ word-break: break-all;
+ }
+
+ .card-content {
+ flex: 1;
+ margin-bottom: 12px;
+ min-height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .product-info {
+ font-size: 13px;
+ color: #666;
+ text-align: center;
+ width: 100%;
+ }
+
+ .product-info.empty {
+ color: #999;
+ text-align: center;
+ padding: 20px 0;
+ }
+
+ .product-name {
+ margin-bottom: 6px;
+ word-break: break-all;
+ line-height: 1.5;
+ text-align: center;
+ }
+
+ .product-model {
+ color: #909399;
+ font-size: 12px;
+ word-break: break-all;
+ line-height: 1.5;
+ text-align: center;
+ }
+
+ .product-unit {
+ margin-left: 4px;
+ color: #409eff;
+ }
+
+ .product-tag {
+ margin: 10px 0;
+ }
+
+ .card-footer {
+ display: flex;
+ justify-content: space-around;
+ padding-top: 12px;
+ border-top: 1px solid #f0f0f0;
+ }
+
+ .card-footer .el-button {
+ padding: 0;
+ font-size: 12px;
+ }
+
+ :deep(.sortable-ghost) {
+ opacity: 0.5;
+ background-color: #f5f7fa !important;
+ }
+
+ :deep(.sortable-drag) {
+ opacity: 0.8;
+ }
+
+ /* 琛ㄦ牸瑙嗗浘鏍峰紡 */
+ :deep(.el-table__row) {
+ transition: background-color 0.2s;
+ cursor: move;
+ }
+
+ :deep(.el-table__row:hover) {
+ background-color: #f9fafc !important;
+ }
+
+ /* 鍖哄煙鏍囬鏍峰紡 */
+ .section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ }
+
+ .section-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ padding-left: 12px;
+ position: relative;
+ margin-bottom: 0;
+ }
+
+ .section-title::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 3px;
+ height: 16px;
+ background: #409eff;
+ border-radius: 2px;
+ }
+
+ .section-actions {
+ display: flex;
+ align-items: center;
+ }
+
+ /* 宸ヨ壓璺嚎淇℃伅鍗$墖鏍峰紡 */
+ .route-info-card {
+ margin-bottom: 20px;
+ border: 1px solid #e4e7ed;
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ .route-info {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 16px;
+ padding: 4px;
+ }
+
+ .info-item {
+ display: flex;
+ flex-direction: column;
+ background: #ffffff;
+ border-radius: 6px;
+ padding: 14px 16px;
+ border: 1px solid #f0f2f5;
+ transition: all 0.3s ease;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
+ }
+
+ .info-item:hover {
+ border-color: #409eff;
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
+ transform: translateY(-1px);
+ }
+
+ .info-item.full-width {
+ grid-column: 1 / -1;
+ }
+
+ .info-label-wrapper {
+ margin-bottom: 8px;
+ }
+
+ .info-label {
+ display: inline-block;
+ color: #909399;
+ font-size: 12px;
+ font-weight: 500;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ padding: 2px 0;
+ position: relative;
+ }
+
+ .info-label::after {
+ content: "";
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 20px;
+ height: 2px;
+ background: linear-gradient(90deg, #409eff, transparent);
+ border-radius: 1px;
+ }
+
+ .info-value-wrapper {
+ flex: 1;
+ }
+
+ .info-value {
+ display: block;
+ color: #303133;
+ font-size: 15px;
+ font-weight: 500;
+ line-height: 1.5;
+ word-break: break-all;
+ }
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/processStatistics/index.vue b/src/views/productionManagement/processStatistics/index.vue
new file mode 100644
index 0000000..25fa531
--- /dev/null
+++ b/src/views/productionManagement/processStatistics/index.vue
@@ -0,0 +1,268 @@
+<template>
+ <div class="app-container">
+ <div class="search-bar">
+ <el-form :model="searchForm"
+ inline>
+ <el-form-item label="鏃ユ湡鍖洪棿:">
+ <el-date-picker v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 240px" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ icon="Search"
+ @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="stats-grid"
+ v-loading="loading">
+ <el-row :gutter="16"
+ v-if="statsData.length > 0">
+ <el-col v-for="(item, index) in statsData"
+ :key="index"
+ :xs="24"
+ :sm="12"
+ :md="8"
+ :lg="4.8"
+ :xl="4.8"
+ class="mb-16">
+ <div class="stats-card">
+ <div class="card-header">
+ <span class="process-name">{{ item.name }}</span>
+ <div class="header-stats">
+ <div class="stat-row">
+ <span class="label">璁″垝鏁�</span>
+ <span class="value">{{ item.planned }}</span>
+ </div>
+ <div class="stat-row">
+ <span class="label">鑹搧鏁�</span>
+ <span class="value">{{ item.good }}</span>
+ </div>
+ <div class="stat-row">
+ <span class="label">涓嶈壇鍝佹暟</span>
+ <span class="value">{{ item.bad }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="card-body">
+ <div class="main-stat">
+ <div class="big-number">{{ item.total }}</div>
+ <div class="sub-label">鐢熶骇浠诲姟鏁�</div>
+ </div>
+ </div>
+ <div class="card-footer">
+ <div class="progress-info">
+ <span class="progress-label">杩涘害:</span>
+ <el-progress :percentage="Math.min(item.percentage, 100)"
+ :color="getProgressColor(item.percentage)"
+ :stroke-width="10"
+ :show-text="false"
+ class="flex-1" />
+ <span class="percentage-text">{{ item.percentage }}%</span>
+ </div>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ <el-empty v-else
+ description="鏆傛棤鏁版嵁" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { reactive, ref, onMounted } from "vue";
+ import dayjs from "dayjs";
+ import { getOperationStatistics } from "@/api/productionManagement/workOrder.js";
+
+ const loading = ref(false);
+ const searchForm = reactive({
+ dateRange: [],
+ });
+
+ const statsData = ref([]);
+
+ const getProgressColor = percentage => {
+ if (percentage >= 100) return "#67c23a";
+ if (percentage >= 50) return "#409eff";
+ if (percentage >= 25) return "#e6a23c";
+ return "red";
+ };
+
+ const getList = () => {
+ loading.value = true;
+ const params = {
+ startDate: searchForm.dateRange?.[0] || "",
+ endDate: searchForm.dateRange?.[1] || "",
+ };
+ getOperationStatistics(params)
+ .then(res => {
+ // 鏍规嵁瀹為檯鎺ュ彛杩斿洖鐨勫瓧娈佃繘琛屾槧灏�
+ statsData.value = (res.data || []).map(item => ({
+ name: item.operationName || "-",
+ total: item.productionTaskCount || 0,
+ planned: item.planQuantity || 0,
+ good: item.goodQuantity || 0,
+ bad: item.scrapQty || 0,
+ percentage: Number(item.completionStatus || 0),
+ }));
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+ };
+
+ const handleQuery = () => {
+ getList();
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .app-container {
+ padding: 20px;
+ background-color: #f0f2f5;
+ min-height: calc(100vh - 84px);
+ }
+
+ .search-bar {
+ background: #fff;
+ padding: 15px 20px 0;
+ border-radius: 4px;
+ margin-bottom: 20px;
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
+ }
+
+ .mb-16 {
+ margin-bottom: 16px;
+ }
+
+ // 妯℃嫙 lg="4.8" 鍥犱负 element 涓嶆敮鎸� 24/5
+ @media only screen and (min-width: 1200px) {
+ .el-col-lg-4-8 {
+ width: 20%;
+ max-width: 20%;
+ flex: 0 0 20%;
+ }
+ }
+
+ .stats-card {
+ background: #fff;
+ border-radius: 8px;
+ padding: 16px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ transition: transform 0.3s;
+
+ &:hover {
+ transform: translateY(-2px);
+ }
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 12px;
+
+ .process-name {
+ background-color: #e6f7ff;
+ color: #1890ff;
+ padding: 2px 8px;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .header-stats {
+ text-align: right;
+
+ .stat-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 2px;
+
+ .label {
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .value {
+ font-size: 13px;
+ color: #303133;
+ font-weight: bold;
+ min-width: 24px;
+ }
+ }
+ }
+ }
+
+ .card-body {
+ padding: 10px 0;
+
+ .main-stat {
+ .big-number {
+ font-size: 28px;
+ font-weight: bold;
+ color: #303133;
+ line-height: 1;
+ }
+
+ .sub-label {
+ font-size: 14px;
+ color: #606266;
+ margin-top: 8px;
+ font-weight: 500;
+ }
+ }
+ }
+
+ .card-footer {
+ margin-top: 16px;
+
+ .progress-info {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .progress-label {
+ font-size: 12px;
+ color: #909399;
+ white-space: nowrap;
+ }
+
+ .flex-1 {
+ flex: 1;
+ }
+
+ .percentage-text {
+ font-size: 12px;
+ color: #606266;
+ min-width: 45px;
+ text-align: right;
+ }
+ }
+ }
+ }
+
+ // 淇 el-col 甯冨眬閫傞厤 5 鍒�
+ :deep(.el-row) {
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ @media only screen and (min-width: 1200px) {
+ .el-col-lg-4\.8 {
+ flex: 0 0 20%;
+ max-width: 20%;
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/productStructure/Detail/index.vue b/src/views/productionManagement/productStructure/Detail/index.vue
new file mode 100644
index 0000000..5cb08e8
--- /dev/null
+++ b/src/views/productionManagement/productStructure/Detail/index.vue
@@ -0,0 +1,699 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="浜у搧缁撴瀯璇︽儏">
+ <template #right-button>
+ <el-button v-if="!dataValue.isEdit && !isOrderPage"
+ type="primary"
+ @click="dataValue.isEdit = true">缂栬緫
+ </el-button>
+ <el-button v-if="dataValue.isEdit && !isOrderPage"
+ type="primary"
+ @click="cancelEdit">鍙栨秷
+ </el-button>
+ <el-button v-if="!isOrderPage"
+ type="primary"
+ :loading="dataValue.loading"
+ @click="submit"
+ :disabled="!dataValue.isEdit">纭
+ </el-button>
+ </template>
+ </PageHeader>
+ <el-table :data="tableData"
+ border
+ :preserve-expanded-content="false"
+ :default-expand-all="true"
+ style="width: 100%">
+ <el-table-column type="expand">
+ <template #default="props">
+ <el-form ref="form"
+ :model="dataValue">
+ <el-table :data="dataValue.dataList"
+ row-key="tempId"
+ default-expand-all
+ :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+ style="width: 100%">
+ <el-table-column prop="productName"
+ label="浜у搧" />
+ <el-table-column prop="model"
+ label="瑙勬牸">
+ <template #default="{ row, $index }">
+ <el-form-item v-if="dataValue.isEdit"
+ :rules="[{ required: true, message: '璇烽�夋嫨瑙勬牸', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-select v-model="row.model"
+ placeholder="璇烽�夋嫨瑙勬牸"
+ clearable
+ :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
+ style="width: 100%"
+ @visible-change="(v) => { if (v) openDialog(row.tempId) }">
+ <el-option v-if="row.model"
+ :label="row.model"
+ :value="row.model" />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="processName"
+ label="娑堣�楀伐搴�">
+ <template #default="{ row, $index }">
+ <el-form-item v-if="dataValue.isEdit"
+ :rules="dataValue.dataList.some(item => (item as any).tempId === row.tempId) ? [] : [{ required: true, message: '璇烽�夋嫨娑堣�楀伐搴�', trigger: 'change' }]"
+ style="margin: 0">
+ <el-select v-model="row.processId"
+ placeholder="璇烽�夋嫨"
+ filterable
+ clearable
+ style="width: 100%"
+ @change="value => handleProcessChange(row, value)"
+ :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)">
+ <el-option v-for="item in dataValue.processOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unitQuantity"
+ label="鍗曚綅浜у嚭鎵�闇�鏁伴噺">
+ <template #default="{ row, $index }">
+ <el-form-item v-if="dataValue.isEdit"
+ :rules="[{ required: true, message: '璇疯緭鍏ュ崟浣嶄骇鍑烘墍闇�鏁伴噺', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input-number v-model="row.unitQuantity"
+ :min="0"
+ :precision="2"
+ :step="1"
+ controls-position="right"
+ style="width: 100%"
+ @change="handleUnitQuantityChange"
+ :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column v-if="isOrderPage"
+ prop="demandedQuantity"
+ label="闇�姹傛�婚噺">
+ <template #default="{ row, $index }">
+ <el-form-item v-if="dataValue.isEdit"
+ :rules="[{ required: true, message: '璇疯緭鍏ラ渶姹傛�婚噺', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input-number v-model="row.demandedQuantity"
+ :min="0"
+ :precision="2"
+ :step="1"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="true" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unit"
+ label="鍗曚綅">
+ <template #default="{ row, $index }">
+ <el-form-item v-if="dataValue.isEdit"
+ :rules="[{ required: true, message: '璇疯緭鍏ュ崟浣�', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input v-model="row.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable
+ :disabled="!dataValue.isEdit || dataValue.dataList.some(item => (item as any).tempId === row.tempId)" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ fixed="right"
+ width="200">
+ <template #default="{ row, $index }">
+ <el-button v-if="dataValue.isEdit && !dataValue.dataList.some(item => (item as any).tempId === row.tempId)"
+ type="danger"
+ text
+ @click="removeItem(row.tempId)">鍒犻櫎
+ </el-button>
+ <el-button v-if="dataValue.isEdit"
+ type="primary"
+ text
+ @click="addItem2(row.tempId)">娣诲姞
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ </template>
+ </el-table-column>
+ <el-table-column label="BOM缂栧彿"
+ prop="bomNo" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="model" />
+ </el-table>
+ <product-select-dialog v-if="dataValue.showProductDialog"
+ v-model:model-value="dataValue.showProductDialog"
+ :single="true"
+ @confirm="handleProduct" />
+ </div>
+</template>
+
+<script setup lang="ts">
+ import {
+ computed,
+ defineAsyncComponent,
+ defineComponent,
+ onMounted,
+ reactive,
+ ref,
+ } from "vue";
+ import {
+ queryList,
+ addBomDetail,
+ } from "@/api/productionManagement/productStructure.js";
+ import { listProcessBom } from "@/api/productionManagement/productionOrder.js";
+ import { list } from "@/api/productionManagement/productionProcess";
+ import { ElMessage } from "element-plus";
+ import { useRoute, useRouter } from "vue-router";
+
+ defineComponent({
+ name: "StructureEdit",
+ });
+
+ const ProductSelectDialog = defineAsyncComponent(
+ () => import("@/views/basicData/product/ProductSelectDialog.vue")
+ );
+ const emit = defineEmits(["update:router"]);
+ const form = ref();
+
+ const route = useRoute();
+ const router = useRouter();
+ const routeId = computed({
+ get() {
+ return route.query.id;
+ },
+
+ set(val) {
+ emit("update:router", val);
+ },
+ });
+
+ // 浠庤矾鐢卞弬鏁拌幏鍙栦骇鍝佷俊鎭�
+ const routeBomNo = computed(() => route.query.bomNo || "");
+ const routeProductName = computed(() => route.query.productName || "");
+ const routeProductModelName = computed(
+ () => route.query.productModelName || ""
+ );
+ const routeOrderId = computed(() => route.query.orderId);
+ const pageType = computed(() => route.query.type);
+ const isOrderPage = computed(
+ () => pageType.value === "order" && routeOrderId.value
+ );
+
+ const dataValue = reactive({
+ dataList: [],
+ productOptions: [],
+ processOptions: [],
+ showProductDialog: false,
+ currentRowIndex: null,
+ currentRowName: null,
+ loading: false,
+ isEdit: false,
+ });
+
+ const normalizeListData = (source: any) => {
+ if (Array.isArray(source)) {
+ return source;
+ }
+ if (Array.isArray(source?.records)) {
+ return source.records;
+ }
+ return [];
+ };
+
+ const getProcessOptionById = (id: any) => {
+ if (id === undefined || id === null || id === "") {
+ return null;
+ }
+ return (
+ normalizeListData(dataValue.processOptions).find(
+ option => String(option.id) === String(id)
+ ) || null
+ );
+ };
+
+ const syncProcessOperationFields = (item: any) => {
+ const processId = item.processId ?? item.operationId ?? "";
+ if (!processId) {
+ item.processId = "";
+ item.operationId = "";
+ item.processName = "";
+ item.operationName = "";
+ return;
+ }
+
+ const option = getProcessOptionById(processId);
+ const processName =
+ option?.name || item.processName || item.operationName || "";
+
+ item.processId = processId;
+ item.operationId = processId;
+ item.processName = processName;
+ item.operationName = processName;
+ };
+
+ const normalizeTreeData = (items: any[]) => {
+ items.forEach((item: any) => {
+ item.tempId = item.tempId || item.id || `${Date.now()}_${Math.random()}`;
+ syncProcessOperationFields(item);
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ normalizeTreeData(item.children);
+ }
+ });
+ };
+
+ const toQuantityNumber = (value: any) => {
+ const numberValue = Number(value);
+ if (!Number.isFinite(numberValue)) {
+ return 0;
+ }
+ return Number(numberValue.toFixed(2));
+ };
+
+ const syncDemandedQuantityTree = (
+ items: any[],
+ parentDemandedQuantity: number | null = null
+ ) => {
+ items.forEach((item: any) => {
+ if (parentDemandedQuantity !== null) {
+ item.demandedQuantity = toQuantityNumber(
+ parentDemandedQuantity * toQuantityNumber(item.unitQuantity)
+ );
+ }
+
+ if (Array.isArray(item.children) && item.children.length > 0) {
+ syncDemandedQuantityTree(
+ item.children,
+ toQuantityNumber(item.demandedQuantity)
+ );
+ }
+ });
+ };
+
+ const recalculateDemandedQuantities = () => {
+ if (!isOrderPage.value) {
+ return;
+ }
+
+ syncDemandedQuantityTree(dataValue.dataList);
+ };
+
+ const buildSubmitTree = (items: any[]) => {
+ return items.map((item: any) => {
+ const current = { ...item };
+ syncProcessOperationFields(current);
+ current.children = Array.isArray(current.children)
+ ? buildSubmitTree(current.children)
+ : [];
+ return current;
+ });
+ };
+
+ const findSiblings = (items: any[], tempId: string): any[] | null => {
+ if (!items || items.length === 0) return null;
+ // 妫�鏌ュ綋鍓嶅眰绾�
+ if (items.some(item => item.tempId === tempId)) {
+ return items;
+ }
+ // 閫掑綊鏌ユ壘瀛愮骇
+ for (const item of items) {
+ if (item.children && item.children.length > 0) {
+ const result = findSiblings(item.children, tempId);
+ if (result) return result;
+ }
+ }
+ return null;
+ };
+
+ const handleProcessChange = (row: any, value: any) => {
+ row.processId = value || "";
+ syncProcessOperationFields(row);
+
+ // 妫�鏌ュ悓涓�灞傜骇鏄惁宸茬粡鏈夊叾浠栦笉鍚岀殑宸ュ簭琚�変腑
+ const siblings = findSiblings(dataValue.dataList, row.tempId);
+ if (siblings && value) {
+ const hasDifferentProcess = siblings.some(sibling => {
+ return sibling.tempId !== row.tempId && sibling.processId && sibling.processId !== value;
+ });
+ if (hasDifferentProcess) {
+ ElMessage.warning("鍚屼竴灞傜骇宸插瓨鍦ㄤ笉鍚岀殑宸ュ簭锛岃鍏堢粺涓�宸ュ簭鍚庡啀杩涜淇敼");
+ }
+ }
+ };
+
+ const handleUnitQuantityChange = () => {
+ recalculateDemandedQuantities();
+ };
+
+ const tableData = reactive([
+ {
+ productName: "",
+ model: "",
+ bomNo: "",
+ },
+ ]);
+
+ const openDialog = (tempId: any) => {
+ console.log(tempId, "tempId");
+ dataValue.currentRowName = tempId;
+ dataValue.showProductDialog = true;
+ };
+
+ const fetchData = async () => {
+ if (isOrderPage.value) {
+ // 璁㈠崟鎯呭喌锛氫娇鐢ㄨ鍗曠殑浜у搧缁撴瀯鎺ュ彛
+ const { data } = await listProcessBom({ orderId: routeOrderId.value });
+ dataValue.dataList = (data as any) || [];
+ normalizeTreeData(dataValue.dataList);
+ recalculateDemandedQuantities();
+ } else {
+ // 闈炶鍗曟儏鍐碉細浣跨敤鍘熸潵鐨勬帴鍙�
+ const { data } = await queryList(routeId.value);
+ dataValue.dataList = (data as any) || [];
+ console.log(dataValue);
+ normalizeTreeData(dataValue.dataList);
+ console.log(dataValue.dataList, "dataValue.dataList");
+ }
+ };
+
+ const fetchProcessOptions = async () => {
+ const { data } = await list({});
+ console.log(data, "dataValue.dataList");
+ dataValue.processOptions = normalizeListData(data);
+ };
+
+ const handleProduct = (row: any) => {
+ if (!Array.isArray(row) || row.length === 0) {
+ ElMessage.warning("璇烽�夋嫨涓�涓骇鍝�");
+ return;
+ }
+ // 鍙厑璁镐竴涓細濡傛灉涓婃父杩斿洖浜嗗涓紝榛樿浣跨敤鏈�鍚庝竴娆¢�夋嫨骞惰鐩栧綋鍓嶅��
+ const productData = row[row.length - 1];
+
+ // 鏈�澶栧眰缁勪欢涓紝涓庡綋鍓嶄骇鍝佺浉鍚岀殑浜у搧鍙兘鏈変竴涓�
+ const isTopLevel = dataValue.dataList.some(
+ item => (item as any).tempId === dataValue.currentRowName
+ );
+ if (isTopLevel) {
+ if (
+ productData.productName === tableData[0].productName &&
+ productData.model === tableData[0].model
+ ) {
+ // 鏌ユ壘鏄惁宸茬粡鏈夊叾浠栭《灞傝宸茬粡鏄繖涓骇鍝�
+ const hasOther = dataValue.dataList.some(
+ item =>
+ (item as any).tempId !== dataValue.currentRowName &&
+ (item as any).productName === tableData[0].productName &&
+ (item as any).model === tableData[0].model
+ );
+ if (hasOther) {
+ ElMessage.warning("鏈�澶栧眰鍜屽綋鍓嶄骇鍝佷竴鏍风殑涓�绾у彧鑳芥湁涓�涓�");
+ return;
+ }
+ }
+ }
+ // dataValue.dataList[dataValue.currentRowIndex].productName =
+ // row[0].productName;
+ // dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
+ // dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
+ // dataValue.dataList[dataValue.currentRowIndex].unit = row[0].unit || "";
+ dataValue.dataList.map(item => {
+ if (item.tempId === dataValue.currentRowName) {
+ item.productName = productData.productName;
+ item.model = productData.model;
+ item.productModelId = productData.id;
+ item.unit = productData.unit || "";
+ return;
+ }
+ childItem(item, dataValue.currentRowName, productData);
+ });
+ dataValue.showProductDialog = false;
+ };
+ const childItem = (item: any, tempId: any, productData: any) => {
+ if (item.tempId === tempId) {
+ item.productName = productData.productName;
+ item.model = productData.model;
+ item.productModelId = productData.id;
+ item.unit = productData.unit || "";
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ for (let child of item.children) {
+ if (childItem(child, tempId, productData)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ // 閫掑綊鏍¢獙鎵�鏈夊眰绾х殑琛ㄥ崟鏁版嵁
+ const validateAll = () => {
+ let isValid = true;
+
+ // 鏍¢獙涓�缁勫厔寮熻妭鐐圭殑宸ュ簭鏄惁閮界浉鍚�
+ const checkProcessUniqueness = (items: any[]) => {
+ if (!items || items.length === 0 || !isValid) return;
+
+ // 鑾峰彇绗竴涓潪绌虹殑宸ュ簭ID浣滀负鍙傝��
+ const firstProcessId = items.find(item => item.processId)?.processId;
+
+ // 濡傛灉鏈夊伐搴廔D锛屾鏌ユ墍鏈夐」鏄惁閮戒娇鐢ㄧ浉鍚岀殑宸ュ簭
+ if (firstProcessId) {
+ for (const item of items) {
+ if (item.processId && item.processId !== firstProcessId) {
+ const option1 = getProcessOptionById(firstProcessId);
+ const option2 = getProcessOptionById(item.processId);
+ const processName1 = option1?.name || "鏈煡宸ュ簭";
+ const processName2 = option2?.name || "鏈煡宸ュ簭";
+ ElMessage.error(
+ `褰撳墠灞傜骇涓嬪伐搴忎笉涓�鑷达紝璇蜂娇鐢ㄧ浉鍚岀殑宸ュ簭銆傚瓨鍦ㄣ��${processName1}銆嶅拰銆�${processName2}銆峘
+ );
+ isValid = false;
+ return;
+ }
+ }
+ }
+
+ // 閫掑綊鏍¢獙瀛愮骇鐨勫厔寮熻妭鐐�
+ for (const item of items) {
+ if (item.children && item.children.length > 0) {
+ checkProcessUniqueness(item.children);
+ }
+ }
+ };
+
+ // 鏍¢獙鍑芥暟
+ const validateItem = (item: any, isTopLevel = false) => {
+ if (!isValid) return;
+ // 鏍¢獙褰撳墠椤圭殑蹇呭~瀛楁
+ if (!item.model) {
+ ElMessage.error("璇烽�夋嫨瑙勬牸");
+ isValid = false;
+ return;
+ }
+ if (!isTopLevel && !item.processId) {
+ ElMessage.error("璇烽�夋嫨娑堣�楀伐搴�");
+ isValid = false;
+ return;
+ }
+ if (!item.unitQuantity) {
+ ElMessage.error("璇疯緭鍏ュ崟浣嶄骇鍑烘墍闇�鏁伴噺");
+ isValid = false;
+ return;
+ }
+ if (isOrderPage.value && !item.demandedQuantity) {
+ ElMessage.error("璇疯緭鍏ラ渶姹傛�婚噺");
+ isValid = false;
+ return;
+ }
+ // if (!item.unit) {
+ // ElMessage.error("璇疯緭鍏ュ崟浣�");
+ // isValid = false;
+ // return;
+ // }
+
+ // 閫掑綊鏍¢獙瀛愰」瀛楁
+ if (item.children && item.children.length > 0) {
+ item.children.forEach(child => {
+ validateItem(child, false);
+ });
+ }
+ };
+
+ // 1. 棣栧厛鏍¢獙鍚屼竴鐖剁骇涓嬬殑鍚屽眰娑堣�楀伐搴忔槸鍚﹀敮涓�
+ checkProcessUniqueness(dataValue.dataList);
+ if (!isValid) return false;
+
+ // 2. 鐒跺悗閬嶅巻鏍¢獙鎵�鏈夐《灞傞」鐨勫瓧娈靛繀濉儏鍐�
+ dataValue.dataList.forEach(item => {
+ validateItem(item, true);
+ });
+
+ return isValid;
+ };
+
+ const submit = () => {
+ dataValue.loading = true;
+ normalizeTreeData(dataValue.dataList);
+ recalculateDemandedQuantities();
+
+ // 鍏堣繘琛岃〃鍗曟牎楠�
+ const valid = validateAll();
+ console.log(dataValue.dataList, "dataValue.dataList");
+ if (valid) {
+ addBomDetail({
+ bomId: routeId.value,
+ children: buildSubmitTree(dataValue.dataList || []),
+ })
+ .then(res => {
+ router.go(-1);
+ ElMessage.success("淇濆瓨鎴愬姛");
+ dataValue.loading = false;
+ })
+ .catch(() => {
+ dataValue.loading = false;
+ });
+ } else {
+ dataValue.loading = false;
+ }
+ };
+
+ const removeItem = (tempId: string) => {
+ // 鍏堝皾璇曚粠椤跺眰鍒犻櫎
+ const topIndex = dataValue.dataList.findIndex(item => item.tempId === tempId);
+ if (topIndex !== -1) {
+ dataValue.dataList.splice(topIndex, 1);
+ return;
+ }
+
+ // 閫掑綊鍒犻櫎瀛愰」
+ const delchildItem = (items: any[], tempId: any) => {
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ if (item.tempId === tempId) {
+ items.splice(i, 1);
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ if (delchildItem(item.children, tempId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ dataValue.dataList.forEach(item => {
+ if (item.children && item.children.length > 0) {
+ delchildItem(item.children, tempId);
+ }
+ });
+ };
+ const addItem2 = tempId => {
+ dataValue.dataList.map(item => {
+ if (item.tempId === tempId) {
+ if (!item.children) {
+ item.children = [];
+ }
+ item.children.push({
+ parentId: item.id || "",
+ parentTempId: item.tempId || "",
+ productName: "",
+ productId: "",
+ model: undefined,
+ productModelId: undefined,
+ processId: "",
+ processName: "",
+ operationId: "",
+ operationName: "",
+ unitQuantity: 1,
+ demandedQuantity: 0,
+ unit: "",
+ children: [],
+
+ tempId: new Date().getTime(),
+ });
+ recalculateDemandedQuantities();
+ return;
+ }
+ addchildItem(item, tempId);
+ });
+ };
+ const addchildItem = (item: any, tempId: any) => {
+ if (item.tempId === tempId) {
+ console.log(item, "item");
+ if (!item.children) {
+ item.children = [];
+ }
+ item.children.push({
+ parentId: item.id || "",
+ parentTempId: item.tempId || "",
+ productName: "",
+ productId: "",
+ model: undefined,
+ productModelId: undefined,
+ processId: "",
+ processName: "",
+ operationId: "",
+ operationName: "",
+ unitQuantity: 1,
+ demandedQuantity: 0,
+ children: [],
+ unit: "",
+ tempId: new Date().getTime(),
+ });
+ recalculateDemandedQuantities();
+ return true;
+ }
+ if (item.children && item.children.length > 0) {
+ for (let child of item.children) {
+ if (addchildItem(child, tempId)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ const getPropPath = (row, field) => {
+ // 涓烘瘡涓猺ow鐢熸垚鍞竴鐨勮矾寰�
+ // 浣跨敤row.id鎴栫储寮曚綔涓哄敮涓�鏍囪瘑
+ let path = "dataList";
+
+ // 绠�鍗曞疄鐜帮細浣跨敤row鐨刬d鎴栦竴涓敮涓�鏍囪瘑
+ const uniqueId = row.id || Math.floor(Math.random() * 10000);
+ path += `.${uniqueId}`;
+
+ return path + `.${field}`;
+ };
+
+ const cancelEdit = () => {
+ dataValue.isEdit = false;
+ // dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
+ fetchData();
+ };
+
+ onMounted(async () => {
+ // 浠庤矾鐢卞弬鏁板洖鏄炬暟鎹�
+ tableData[0].productName = routeProductName.value as string;
+ tableData[0].model = routeProductModelName.value as string;
+ tableData[0].bomNo = routeBomNo.value as string;
+
+ // 璁㈠崟鎯呭喌涓嬬鐢ㄧ紪杈�
+ if (isOrderPage.value) {
+ dataValue.isEdit = false;
+ }
+
+ // 鍏堝姞杞藉伐搴忛�夐」锛屽啀鍔犺浇鏁版嵁锛岀‘淇漞l-select鑳藉姝g‘鍥炴樉
+ await fetchProcessOptions();
+ await fetchData();
+ });
+</script>
\ No newline at end of file
diff --git a/src/views/productionManagement/productStructure/StructureEdit.vue b/src/views/productionManagement/productStructure/StructureEdit.vue
new file mode 100644
index 0000000..4d07f5d
--- /dev/null
+++ b/src/views/productionManagement/productStructure/StructureEdit.vue
@@ -0,0 +1,311 @@
+<template>
+ <el-dialog v-model="visible"
+ title="缁撴瀯"
+ width="1200"
+ close-on-click-modal
+ @close="visible = false">
+ <el-button v-if="dataValue.isEdit"
+ type="primary"
+ @click="addItem"
+ style="margin-bottom: 10px">娣诲姞
+ </el-button>
+ <el-button v-if="!dataValue.isEdit"
+ type="primary"
+ @click="dataValue.isEdit = true"
+ style="margin-bottom: 10px">缂栬緫
+ </el-button>
+ <el-button v-if="dataValue.isEdit"
+ type="primary"
+ @click="cancelEdit"
+ style="margin-bottom: 10px">鍙栨秷
+ </el-button>
+
+ <el-table
+ :data="tableData"
+ border
+ :preserve-expanded-content="false"
+ style="width: 100%"
+ >
+ <el-table-column type="expand">
+ <template #default="props">
+ <el-form ref="form"
+ :model="dataValue">
+ <el-table :data="dataValue.dataList"
+ style="width: 100%">
+ <el-table-column prop="productName"
+ label="浜у搧"
+ width="150" />
+ <el-table-column prop="model"
+ label="瑙勬牸"
+ width="150">
+ <template #default="{ row, $index }">
+ <el-form-item v-if="dataValue.isEdit"
+ :prop="`dataList.${$index}.model`"
+ :rules="[{ required: true, message: '璇烽�夋嫨瑙勬牸', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-select v-model="row.model"
+ placeholder="璇烽�夋嫨浜у搧"
+ clearable
+ :disabled="!dataValue.isEdit"
+ style="width: 100%"
+ @visible-change="(v) => { if (v) openDialog($index) }">
+ <el-option v-if="row.model"
+ :label="row.model"
+ :value="row.model" />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="processId"
+ label="娑堣�楀伐搴�"
+ width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`dataList.${$index}.processId`"
+ :rules="[{ required: true, message: '璇烽�夋嫨娑堣�楀伐搴�', trigger: 'change' }]"
+ style="margin: 0">
+ <el-select v-model="row.processId"
+ placeholder="璇烽�夋嫨"
+ filterable
+ clearable
+ style="width: 100%"
+ :disabled="!dataValue.isEdit">
+ <el-option v-for="item in dataValue.processOptions"
+ :key="item.id"
+ :label="item.name"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unitQuantity"
+ label="鍗曚綅浜у嚭鎵�闇�鏁伴噺"
+ width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`dataList.${$index}.unitQuantity`"
+ :rules="[{ required: true, message: '璇疯緭鍏ュ崟浣嶄骇鍑烘墍闇�鏁伴噺', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input-number v-model="row.unitQuantity"
+ :min="0"
+ :precision="2"
+ :step="1"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="!dataValue.isEdit" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="demandedQuantity"
+ label="闇�姹傛�婚噺"
+ width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`dataList.${$index}.demandedQuantity`"
+ :rules="[{ required: true, message: '璇疯緭鍏ラ渶姹傛�婚噺', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input-number v-model="row.demandedQuantity"
+ :min="0"
+ :precision="2"
+ :step="1"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="!dataValue.isEdit" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unit"
+ label="鍗曚綅"
+ width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`dataList.${$index}.unit`"
+ :rules="[{ required: true, message: '璇疯緭鍏ュ崟浣�', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input v-model="row.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable
+ :disabled="!dataValue.isEdit" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="diskQuantity"
+ label="鐩樻暟锛堢洏锛�"
+ width="150">
+ <template #default="{ row, $index }">
+ <el-form-item :prop="`dataList.${$index}.diskQuantity`"
+ :rules="[{ required: true, message: '璇疯緭鍏ョ洏鏁�', trigger: ['blur','change'] }]"
+ style="margin: 0">
+ <el-input-number v-model="row.diskQuantity"
+ :min="0"
+ :precision="0"
+ :step="1"
+ controls-position="right"
+ style="width: 100%"
+ :disabled="!dataValue.isEdit" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔">
+ <template #default="{ row, $index }">
+ <el-button type="danger"
+ text
+ @click="dataValue.dataList.splice($index, 1)">鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+ </template>
+ </el-table-column>
+ <el-table-column label="浜у搧缂栫爜" prop="productCode" />
+ <el-table-column label="浜у搧鍚嶇О" prop="productName" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="model" />
+ <el-table-column label="鍗曚綅" prop="unit" />
+ </el-table>
+
+ <product-select-dialog v-if="dataValue.showProductDialog"
+ v-model:model-value="dataValue.showProductDialog"
+ @confirm="handleProduct" />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ :loading="dataValue.loading"
+ @click="submit"
+ :disabled="!dataValue.isEdit">
+ 纭
+ </el-button>
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup lang="ts">
+ import {
+ computed,
+ defineAsyncComponent,
+ defineComponent,
+ onMounted,
+ reactive,
+ ref,
+ } from "vue";
+ import { queryList, add } from "@/api/productionManagement/productStructure.js";
+ import { list } from "@/api/productionManagement/productionProcess";
+ import { ElMessage } from "element-plus";
+
+ defineComponent({
+ name: "StructureEdit",
+ });
+
+ const ProductSelectDialog = defineAsyncComponent(
+ () => import("@/views/basicData/product/ProductSelectDialog.vue")
+ );
+ const form = ref();
+
+ const props = defineProps({
+ showModel: {
+ type: Boolean,
+ default: false,
+ },
+ record: {
+ type: Object,
+ required: true,
+ },
+ });
+
+ const emits = defineEmits(["update:showModel"]);
+ const visible = computed({
+ get() {
+ return props.showModel;
+ },
+ set(val) {
+ emits("update:showModel", val);
+ },
+ });
+
+ const dataValue = reactive({
+ dataList: [],
+ productOptions: [],
+ processOptions: [],
+ showProductDialog: false,
+ currentRowIndex: null,
+ loading: false,
+ isEdit: false,
+ });
+
+ const tableData = [
+ {
+ productName: props.record.productName,
+ model: props.record.model,
+ unit: props.record.unit,
+ productCode: props.record.productCode,
+ }
+ ]
+
+ const openDialog = index => {
+ dataValue.currentRowIndex = index;
+ dataValue.showProductDialog = true;
+ };
+
+ const fetchData = async () => {
+ const { data } = await queryList(props.record.id);
+ dataValue.dataList = data;
+ };
+
+ const fetchProcessOptions = async () => {
+ const { data } = await list(props.record.id);
+ dataValue.processOptions = data;
+ };
+
+ const handleProduct = row => {
+ if (row?.length > 1) {
+ ElMessage.error("鍙兘閫夋嫨涓�涓骇鍝�");
+ }
+ dataValue.dataList[dataValue.currentRowIndex].productName =
+ row[0].productName;
+ dataValue.dataList[dataValue.currentRowIndex].model = row[0].model;
+ dataValue.dataList[dataValue.currentRowIndex].productModelId = row[0].id;
+ dataValue.showProductDialog = false;
+ };
+
+ const submit = () => {
+ form.value
+ .validate(valid => {
+ dataValue.loading = true;
+ if (valid) {
+ add({
+ parentId: props.record.id,
+ productStructureList: dataValue.dataList || [],
+ }).then(res => {
+ ElMessage.success("淇濆瓨鎴愬姛");
+ visible.value = false;
+ dataValue.loading = false;
+ });
+ }
+ })
+ .finally(() => {
+ dataValue.loading = false;
+ });
+ };
+
+ const addItem = () => {
+ dataValue.dataList.push({
+ productName: "",
+ productId: "",
+ model: undefined,
+ productModelId: undefined,
+ processId: "",
+ unitQuantity: 0,
+ demandedQuantity: 0,
+ unit: "",
+ diskQuantity: 0,
+ });
+ };
+
+ const cancelEdit = () => {
+ dataValue.isEdit = false;
+ dataValue.dataList = dataValue.dataList.filter(item => item.id !== undefined);
+ };
+
+ onMounted(() => {
+ fetchData();
+ fetchProcessOptions();
+ });
+</script>
\ No newline at end of file
diff --git a/src/views/productionManagement/productStructure/index.vue b/src/views/productionManagement/productStructure/index.vue
new file mode 100644
index 0000000..e05ed3c
--- /dev/null
+++ b/src/views/productionManagement/productStructure/index.vue
@@ -0,0 +1,538 @@
+<template>
+ <div class="app-container">
+ <div class="table_list">
+ <div style="text-align: right; margin-bottom: 10px;">
+ <el-button type="primary"
+ @click="handleAdd">鏂板</el-button>
+ <el-button type="info"
+ plain
+ icon="Upload"
+ @click="handleImport"
+ v-hasPermi="['product:bom:import']">瀵煎叆</el-button>
+ <el-button type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ :disabled="selectedRows.length !== 1"
+ v-hasPermi="['product:bom:export']">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleBatchDelete"
+ :disabled="selectedRows.length === 0">鍒犻櫎</el-button>
+ </div>
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination">
+ <template #detail="{ row }">
+ <el-button type="primary"
+ text
+ @click="showDetail(row)">{{ row.bomNo }}
+ </el-button>
+ </template>
+ </PIMTable>
+ </div>
+ <StructureEdit v-if="showEdit"
+ v-model:show-model="showEdit"
+ :record="currentRow" />
+ <!-- 鏂板/缂栬緫寮圭獥 -->
+ <el-dialog v-model="dialogVisible"
+ :title="operationType === 'add' ? '鏂板BOM' : '缂栬緫BOM'"
+ width="600px"
+ @close="closeDialog">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120px">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productModelId">
+ <el-button type="primary"
+ @click="showProductSelectDialog = true">
+ {{ form.productName || '閫夋嫨浜у搧' }}
+ </el-button>
+ </el-form-item>
+ <el-form-item label="鐗堟湰鍙�"
+ prop="version">
+ <el-input v-model="form.version"
+ placeholder="璇疯緭鍏ョ増鏈彿"
+ clearable />
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�"
+ clearable />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button type="primary"
+ @click="handleSubmit">纭畾</el-button>
+ <el-button @click="closeDialog">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+ <!-- 浜у搧閫夋嫨寮圭獥 -->
+ <ProductSelectDialog v-model="showProductSelectDialog"
+ @confirm="handleProductSelect"
+ single />
+ <!-- BOM瀵煎叆瀵硅瘽妗� -->
+ <ImportDialog ref="uploadRef"
+ v-model="upload.open"
+ :title="upload.title"
+ :action="upload.url"
+ :headers="upload.headers"
+ :disabled="upload.isUploading"
+ :on-progress="handleFileUploadProgress"
+ :on-success="handleFileSuccess"
+ :show-download-template="true"
+ @confirm="submitFileForm"
+ @download-template="handleDownloadTemplate"
+ @close="handleImportClose" />
+ </div>
+</template>
+
+<script setup>
+ import {
+ ref,
+ reactive,
+ toRefs,
+ onMounted,
+ getCurrentInstance,
+ defineAsyncComponent,
+ } from "vue";
+ import { getToken } from "@/utils/auth";
+ import {
+ listPage,
+ add,
+ copy,
+ update,
+ batchDelete,
+ exportBom,
+ downloadTemplate,
+ } from "@/api/productionManagement/productBom.js";
+ import { useRouter } from "vue-router";
+ import { ElMessageBox } from "element-plus";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+ import ImportDialog from "@/components/Dialog/ImportDialog.vue";
+
+ const router = useRouter();
+ const { proxy } = getCurrentInstance();
+ const StructureEdit = defineAsyncComponent(() =>
+ import("@/views/productionManagement/productStructure/StructureEdit.vue")
+ );
+
+ const tableColumn = ref([
+ {
+ label: "BOM缂栧彿",
+ prop: "bomNo",
+ dataType: "slot",
+ slot: "detail",
+ minWidth: 140,
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+
+ minWidth: 160,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "productModelName",
+ minWidth: 140,
+ },
+ {
+ label: "鐗堟湰鍙�",
+ prop: "version",
+ width: 100,
+ },
+ {
+ label: "澶囨敞",
+ prop: "remark",
+ minWidth: 160,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 250,
+ operation: [
+ {
+ name: "澶嶅埗",
+ type: "text",
+ clickFun: row => {
+ handleCopy(row);
+ },
+ },
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ handleEdit(row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ link: true,
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ]);
+
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const showEdit = ref(false);
+ const selectedRows = ref([]);
+ const currentRow = ref({});
+ const dialogVisible = ref(false);
+ const operationType = ref("add"); // add | edit
+ const formRef = ref(null);
+ const showProductSelectDialog = ref(false);
+
+ // BOM瀵煎叆鍙傛暟
+ const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙BOM瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙BOM瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/technologyBom/uploadBom",
+ });
+
+ const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ const data = reactive({
+ form: {
+ id: undefined,
+ productName: "",
+ productModelName: "",
+ productModelId: "",
+ remark: "",
+ version: "",
+ },
+ rules: {
+ productModelId: [
+ { required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" },
+ ],
+ version: [{ required: true, message: "璇疯緭鍏ョ増鏈彿", trigger: "blur" }],
+ },
+ });
+
+ const { form, rules } = toRefs(data);
+
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鍒嗛〉
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ // 鏌ヨ鍒楄〃
+ const getList = () => {
+ tableLoading.value = true;
+ listPage({
+ current: page.current,
+ size: page.size,
+ })
+ .then(res => {
+ const records = res?.data?.records || [];
+ tableData.value = records;
+ page.total = res?.data?.total || 0;
+ })
+ .catch(err => {
+ console.error("鑾峰彇鍒楄〃澶辫触锛�", err);
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鏂板
+ const handleAdd = () => {
+ operationType.value = "add";
+ Object.assign(form.value, {
+ id: undefined,
+ productName: "",
+ productModelName: "",
+ productModelId: "",
+ remark: "",
+ version: "",
+ });
+ dialogVisible.value = true;
+ };
+ const handleCopy = row => {
+ // handleAdd(row);
+ ElMessageBox.confirm("纭澶嶅埗璇OM锛�", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ copy({
+ id: row.id,
+ })
+ .then(() => {
+ proxy.$modal.msgSuccess("澶嶅埗鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("澶嶅埗澶辫触");
+ });
+ })
+ .catch(() => {});
+ };
+
+ // 缂栬緫
+ const handleEdit = row => {
+ operationType.value = "edit";
+ Object.assign(form.value, {
+ id: row.id,
+ productName: row.productName || "",
+ productModelName: row.productModelName || "",
+ productModelId: row.productModelId || "",
+ remark: row.remark || "",
+ version: row.version || "",
+ });
+ dialogVisible.value = true;
+ };
+
+ // 鍒犻櫎锛堝崟鏉★級
+ const handleDelete = row => {
+ ElMessageBox.confirm("纭鍒犻櫎璇OM锛�", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchDelete([row.id])
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {});
+ };
+
+ // 鎵归噺鍒犻櫎
+ const handleBatchDelete = () => {
+ if (!selectedRows.value.length) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ batchDelete(ids)
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {});
+ };
+
+ // 浜у搧閫夋嫨
+ const handleProductSelect = products => {
+ if (products && products.length > 0) {
+ const product = products[0];
+ form.value.productModelId = product.id;
+ form.value.productName = product.productName;
+ form.value.productModelName = product.model;
+ }
+ showProductSelectDialog.value = false;
+ };
+
+ // 鎻愪氦琛ㄥ崟
+ const handleSubmit = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ const payload = { ...form.value };
+ if (operationType.value === "add") {
+ add(payload)
+ .then(() => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ closeDialog();
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鏂板澶辫触");
+ });
+ } else {
+ update(payload)
+ .then(() => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeDialog();
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("淇敼澶辫触");
+ });
+ }
+ }
+ });
+ };
+
+ // 鍏抽棴寮圭獥
+ const closeDialog = () => {
+ dialogVisible.value = false;
+ formRef.value?.resetFields();
+ };
+
+ // 瀵煎叆鎸夐挳鎿嶄綔
+ const handleImport = () => {
+ upload.title = "BOM瀵煎叆";
+ upload.open = true;
+ };
+
+ // 鍏抽棴瀵煎叆瀵硅瘽妗嗘椂娓呴櫎鏂囦欢
+ const handleImportClose = () => {
+ proxy.$refs["uploadRef"].clearFiles();
+ };
+
+ // 鏂囦欢涓婁紶涓鐞�
+ const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true;
+ };
+
+ // 鏂囦欢涓婁紶鎴愬姛澶勭悊
+ const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false;
+ upload.isUploading = false;
+ proxy.$refs["uploadRef"].clearFiles();
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess(response.msg || "瀵煎叆鎴愬姛");
+ getList();
+ } else {
+ proxy.$alert(
+ "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
+ response.msg +
+ "</div>",
+ "瀵煎叆缁撴灉",
+ { dangerouslyUseHTMLString: true }
+ );
+ }
+ };
+
+ // 鎻愪氦涓婁紶鏂囦欢
+ const submitFileForm = () => {
+ proxy.$refs["uploadRef"].submit();
+ };
+
+ // 瀵煎嚭鎸夐挳鎿嶄綔
+ const handleExport = () => {
+ if (selectedRows.value.length !== 1) {
+ proxy.$modal.msgWarning("璇烽�夋嫨涓�鏉℃暟鎹繘琛屽鍑�");
+ return;
+ }
+
+ const bomId = selectedRows.value[0].id;
+ const fileName = `BOM_${selectedRows.value[0].bomNo || bomId}.xlsx`;
+
+ exportBom(bomId)
+ .then(res => {
+ // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+ if (!res) {
+ proxy.$modal.msgError("瀵煎嚭澶辫触锛岃繑鍥炴暟鎹负绌�");
+ return;
+ }
+
+ const blob = new Blob([res], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ const downloadElement = document.createElement("a");
+ const href = window.URL.createObjectURL(blob);
+
+ downloadElement.style.display = "none";
+ downloadElement.href = href;
+ downloadElement.download = fileName;
+
+ document.body.appendChild(downloadElement);
+ downloadElement.click();
+
+ document.body.removeChild(downloadElement);
+ window.URL.revokeObjectURL(href);
+
+ proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
+ })
+ .catch(err => {
+ console.error("瀵煎嚭寮傚父锛�", err);
+ proxy.$modal.msgError("绯荤粺寮傚父锛屽鍑哄け璐�");
+ });
+ };
+
+ // 涓嬭浇妯℃澘
+ const handleDownloadTemplate = async () => {
+ const res = await downloadTemplate();
+ // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+ if (!res) {
+ proxy.$modal.msgError("涓嬭浇澶辫触锛岃繑鍥炴暟鎹负绌�");
+ return;
+ }
+
+ const blob = new Blob([res], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ const downloadElement = document.createElement("a");
+ const href = window.URL.createObjectURL(blob);
+
+ downloadElement.href = href;
+ downloadElement.download = "BOM妯℃澘.xlsx";
+
+ document.body.appendChild(downloadElement);
+ downloadElement.click();
+
+ document.body.removeChild(downloadElement);
+ window.URL.revokeObjectURL(href);
+
+ proxy.$modal.msgSuccess("涓嬭浇鎴愬姛");
+ };
+
+ // 鏌ョ湅璇︽儏
+ const showDetail = row => {
+ router.push({
+ path: "/productionManagement/productStructureDetail",
+ query: {
+ id: row.id,
+ bomNo: row.bomNo || "",
+ productName: row.productName || "",
+ productModelName: row.productModelName || "",
+ },
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
diff --git a/src/views/productionManagement/productionCosting/index.vue b/src/views/productionManagement/productionCosting/index.vue
new file mode 100644
index 0000000..3e79b93
--- /dev/null
+++ b/src/views/productionManagement/productionCosting/index.vue
@@ -0,0 +1,394 @@
+<template>
+ <div class="app-container">
+ <div class="table_list">
+ <el-row :gutter="16"
+ class="content-row">
+ <!-- 宸︿晶鍙拌处 + 椤堕儴绛涢�� -->
+ <el-col :xs="24"
+ :sm="24"
+ :md="24"
+ :lg="8"
+ :xl="8"
+ class="left-col">
+ <div class="left-panel">
+ <div class="left-header">
+ <el-form :model="searchForm"
+ inline>
+ <el-form-item prop="dateType">
+ <el-radio-group v-model="searchForm.dateType"
+ size="small"
+ @change="handleDateTypeChange">
+ <el-radio-button label="day">鏃�</el-radio-button>
+ <el-radio-button label="month">鏈�</el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏃ユ湡锛�"
+ prop="dateRange">
+ <el-date-picker v-model="searchForm.dateRange"
+ :type="searchForm.dateType === 'day' ? 'date' : 'daterange'"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ style="width: 200px"
+ @change="handleDateRangeChange" />
+ </el-form-item>
+ </el-form>
+ </div>
+ <PIMTable rowKey="id"
+ :column="leftTableColumn"
+ :tableData="leftTableData"
+ :tableLoading="tableLoading"
+ :page="page"
+ @row-click="handleLeftRowClick"
+ @pagination="pagination"></PIMTable>
+ </div>
+ </el-col>
+ <!-- 鍙充晶鏄庣粏 -->
+ <el-col :xs="24"
+ :sm="24"
+ :md="24"
+ :lg="16"
+ :xl="16"
+ class="right-col">
+ <div class="right-panel">
+ <el-form inline>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleOut">瀵煎嚭</el-button>
+ </el-form-item>
+ </el-form>
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page1"
+ :tableLoading="tableLoading1"
+ style="margin-right: 20px;"
+ @pagination="pagination1"></PIMTable>
+ </div>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import dayjs from "dayjs";
+ import {
+ salesLedgerProductionAccountingListProductionDetails,
+ salesLedgerProductionAccountingList,
+ } from "@/api/productionManagement/productionCosting.js";
+ const { proxy } = getCurrentInstance();
+
+ const tableColumn = ref([
+ {
+ label: "鐢熶骇鏃ユ湡",
+ prop: "schedulingDate",
+ minWidth: 100,
+ },
+ {
+ label: "鐢熶骇浜�",
+ prop: "schedulingUserName",
+ minWidth: 100,
+ },
+ // {
+ // label: "鍚堝悓鍙�",
+ // prop: "salesContractNo",
+ // minWidth: 100,
+ // },
+ // {
+ // label: "瀹㈡埛鍚嶇О",
+ // prop: "customerName",
+ // minWidth: 100,
+ // },
+ {
+ label: "浜у搧澶х被",
+ prop: "productName",
+ minWidth: 100,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "productModelName",
+ minWidth: 100,
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ minWidth: 100,
+ },
+ {
+ label: "宸ュ簭",
+ prop: "process",
+ minWidth: 100,
+ },
+ {
+ label: "宸ユ椂锛坔锛�",
+ prop: "workHour",
+ minWidth: 100,
+ },
+ {
+ label: "鐢熶骇鏁伴噺",
+ prop: "quantity",
+ minWidth: 100,
+ },
+ {
+ label: "宸ユ椂瀹氶",
+ prop: "workHours",
+ minWidth: 100,
+ },
+ {
+ label: "宸ヨ祫",
+ prop: "wages",
+ minWidth: 100,
+ },
+ ]);
+
+ // 宸︿晶姹囨�诲彴璐﹀垪锛堢敓浜т汉銆佷骇閲忋�佸伐璧勩�佸悎鏍肩巼锛�
+ const leftTableColumn = ref([
+ {
+ label: "鐢熶骇浜�",
+ prop: "schedulingUserName",
+ minWidth: 100,
+ },
+ {
+ label: "浜ч噺",
+ prop: "finishedNum",
+ minWidth: 100,
+ },
+ {
+ label: "宸ヨ祫",
+ prop: "wages",
+ minWidth: 100,
+ },
+ {
+ label: "鍚堟牸鐜�",
+ prop: "outputRate",
+ minWidth: 100,
+ formatData: val => {
+ if (val == null || val === "") return "-";
+ return parseFloat(val).toFixed(2) + "%";
+ },
+ },
+ ]);
+
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const tableLoading1 = ref(false);
+ const leftTableData = ref([]);
+ // 鏃� / 鏈� 鍒囨崲锛堥粯璁ゆ寜鏃ワ級
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+
+ const page1 = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+
+ const data = reactive({
+ searchForm: {
+ schedulingUserName: "",
+ salesContractNo: "",
+ dateType: "day",
+ dateRange: dayjs().format("YYYY-MM-DD"),
+ entryDate: dayjs().format("YYYY-MM-DD"),
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const pagination1 = obj => {
+ page1.current = obj.page;
+ page1.size = obj.limit;
+ getList1();
+ };
+
+ const handleDateRangeChange = value => {
+ if (value) {
+ if (searchForm.value.dateType === "day") {
+ searchForm.value.entryDate = value;
+ } else {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ } else {
+ searchForm.value.entryDate = undefined;
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ }
+ reloadData();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+
+ salesLedgerProductionAccountingList(params)
+ .then(res => {
+ const records = res.data.records || [];
+ leftTableData.value = records;
+ page.total = res.data.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const getList1 = () => {
+ tableLoading1.value = true;
+ const params = { ...page1, ...searchForm.value };
+ salesLedgerProductionAccountingListProductionDetails(params)
+ .then(res => {
+ tableData.value = res.data.records || [];
+ page1.total = res.data.total || 0;
+ })
+ .finally(() => {
+ tableLoading1.value = false;
+ });
+ };
+
+ // 鏋勫缓宸︿晶姹囨�诲彴璐︼紙鎸夌敓浜т汉姹囨�讳骇閲忋�佸伐璧勭瓑锛�
+ const buildLeftTableData = records => {
+ const map = {};
+ records.forEach(item => {
+ const key = item.schedulingUserName || "鏈煡";
+ if (!map[key]) {
+ map[key] = {
+ id: key,
+ schedulingUserName: key,
+ finishedNum: 0,
+ wages: 0,
+ qualifiedRate: item.qualifiedRate ?? null,
+ };
+ }
+ map[key].finishedNum += Number(item.finishedNum || 0);
+ map[key].wages += Number(item.wages || 0);
+ if (item.qualifiedRate != null) {
+ map[key].qualifiedRate = item.qualifiedRate;
+ }
+ });
+ leftTableData.value = Object.values(map);
+ };
+
+ // 宸︿晶鏃�/鏈堝垏鎹�
+ const handleDateTypeChange = value => {
+ // 杩欓噷鍙綔涓虹瓫閫夋潯浠剁殑涓�閮ㄥ垎锛岀洿鎺ラ噸鏂版煡璇㈠垪琛�
+ if (value === "day") {
+ searchForm.value.entryDate = dayjs().format("YYYY-MM-DD");
+ searchForm.value.dateRange = searchForm.value.entryDate;
+ } else {
+ searchForm.value.entryDateStart = dayjs()
+ .startOf("month")
+ .format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs().endOf("month").format("YYYY-MM-DD");
+ searchForm.value.dateRange = [
+ searchForm.value.entryDateStart,
+ searchForm.value.entryDateEnd,
+ ];
+ }
+
+ reloadData();
+ };
+
+ const reloadData = () => {
+ page.current = 1;
+ page1.current = 1;
+ getList();
+ tableData.value = [];
+ };
+
+ // 鐐瑰嚮宸︿晶琛岋紝鍒峰彸渚ф槑缁嗭紙鎸夌敓浜т汉杩囨护锛�
+ const handleLeftRowClick = row => {
+ searchForm.value.schedulingUserName = row.schedulingUserName || "";
+ handleQuery();
+ };
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page1.current = 1;
+ getList1();
+ };
+
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(
+ "/salesLedger/productionAccounting/export",
+ {},
+ "鐢熶骇鏍哥畻.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .content-row {
+ width: 100%;
+ }
+
+ .content-row .left-col,
+ .content-row .right-col {
+ margin-bottom: 16px;
+ }
+
+ .left-panel,
+ .right-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ min-width: 0;
+ }
+
+ .left-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .left-title {
+ font-size: 16px;
+ color: #ffffff;
+ }
+
+ .header-filters {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ justify-content: flex-end;
+ gap: 8px;
+ }
+
+ .search_title {
+ color: #ffffff;
+ }
+
+ .ml10 {
+ margin-left: 10px;
+ }
+</style>
diff --git a/src/views/productionManagement/productionDispatching/components/autoDispatchDia.vue b/src/views/productionManagement/productionDispatching/components/autoDispatchDia.vue
new file mode 100644
index 0000000..b4a76f6
--- /dev/null
+++ b/src/views/productionManagement/productionDispatching/components/autoDispatchDia.vue
@@ -0,0 +1,153 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="鑷姩娲惧伐"
+ width="80%"
+ @close="closeDia"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" ref="formRef">
+ <el-divider content-position="left">娲惧伐鍒楄〃</el-divider>
+
+ <el-table
+ :data="dispatchList"
+ border
+ style="width: 100%; margin-top: 20px;"
+ :row-class-name="tableRowClassName"
+ >
+ <el-table-column label="搴忓彿" type="index" width="60" align="center" />
+ <el-table-column label="鍚堝悓鍙�" prop="salesContractNo" width="200" />
+ <el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" width="200" />
+ <!-- <el-table-column label="椤圭洰鍚嶇О" prop="projectName" width="250" /> -->
+ <el-table-column label="浜у搧澶х被" prop="productCategory" width="150" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" width="200" />
+ <el-table-column label="缁戝畾鏈哄櫒" prop="speculativeTradingName" width="120" />
+ <el-table-column label="鎬绘暟閲�" prop="quantity" width="100" align="right" />
+ <el-table-column label="宸叉帓浜�" prop="schedulingNum" width="100" align="right" fixed="right" />
+ <el-table-column label="寰呮帓浜�" prop="pendingQuantity" width="100" align="right" fixed="right" />
+ <el-table-column label="鏈鎺掍骇" width="150" align="center" fixed="right">
+ <template #default="{ row }">
+ <el-input-number
+ v-model="row.schedulingNum"
+ :min="0"
+ :max="row.pendingQuantity"
+ :step="1"
+ :precision="0"
+ size="small"
+ style="width: 120px"
+ @change="(value) => changeCurrentNum(value, row)"
+ />
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭娲惧伐</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, toRefs, computed} from "vue";
+import {productionDispatch, productionDispatchList} from "@/api/productionManagement/productionOrder.js";
+
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+
+const data = reactive({
+ form: {},
+ dispatchList: [], // 娲惧伐鍒楄〃鏁版嵁
+});
+
+const { form, dispatchList } = toRefs(data);
+
+
+// 琛ㄦ牸琛屾牱寮�
+const tableRowClassName = ({ rowIndex }) => {
+ if (rowIndex % 2 === 1) {
+ return 'even-row'
+ }
+ return ''
+}
+
+// 淇敼鏈鎺掍骇鏁伴噺
+const changeCurrentNum = (value, row) => {
+ if (value > row.pendingQuantity) {
+ row.schedulingNum = row.pendingQuantity
+ proxy.$modal.msgWarning('鎺掍骇鏁伴噺涓嶅彲澶т簬寰呮帓浜ф暟閲�')
+ }
+}
+
+// 鎵撳紑寮规
+const openDialog = (type, rows) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+
+ // 澶勭悊浼犲叆鐨勬暟鎹�
+ dispatchList.value = rows.map(row => ({
+ ...row,
+ schedulingNum: 0, // 鍒濆鍖栨湰娆℃帓浜ф暟閲忎负0
+ pendingQuantity: (Number(row.quantity) || 0) - (Number(row.schedulingNum) || 0) // 璁$畻寰呮帓浜ф暟閲�
+ }))
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ // 妫�鏌ユ槸鍚︽湁鎺掍骇鏁版嵁
+ const hasSchedulingData = dispatchList.value.some(item => item.schedulingNum > 0)
+ if (!hasSchedulingData) {
+ proxy.$modal.msgWarning('璇疯嚦灏戜负涓�鏉¤褰曡缃帓浜ф暟閲�')
+ return
+ }
+
+ // 鏋勯�犳彁浜ゆ暟鎹� - 鐩存帴浼犻�掓暟缁勶紝涓嶈繃婊�
+ const submitData = dispatchList.value
+
+ console.log('鎻愪氦鑷姩娲惧伐鏁版嵁:', submitData)
+
+ // 璋冪敤API锛堣繖閲岄渶瑕佹牴鎹疄闄呮帴鍙h皟鏁达級
+ productionDispatchList(submitData).then(res => {
+ proxy.$modal.msgSuccess(res.msg);
+ closeDia();
+ }).catch(err => {
+ proxy.$modal.msgError("娲惧伐澶辫触");
+ console.error('娲惧伐澶辫触:', err);
+ })
+}
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ dispatchList.value = []
+ emit('close')
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+:deep(.even-row) {
+ background-color: #fafafa;
+}
+
+:deep(.el-table .cell) {
+ padding: 8px 12px;
+}
+
+:deep(.el-table th) {
+ background-color: #f5f7fa;
+ color: #606266;
+ font-weight: 600;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/productionDispatching/components/formDia.vue b/src/views/productionManagement/productionDispatching/components/formDia.vue
new file mode 100644
index 0000000..a514d9a
--- /dev/null
+++ b/src/views/productionManagement/productionDispatching/components/formDia.vue
@@ -0,0 +1,192 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="鐢熶骇娲惧伐"
+ width="50%"
+ @close="closeDia"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <!-- <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="椤圭洰鍚嶇О锛�" prop="projectName">
+ <el-input v-model="form.projectName" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧澶х被锛�" prop="productCategory">
+ <el-input v-model="form.productCategory" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row> -->
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="specificationModel">
+ <el-input v-model="form.specificationModel" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁戝畾鏈哄櫒锛�" prop="speculativeTradingName">
+ <el-input v-model="form.speculativeTradingName" placeholder="鑷姩鑾峰彇" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鎬绘暟閲忥細" prop="quantity">
+ <el-input v-model="form.quantity" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寰呮帓浜ф暟閲忥細" prop="pendingQuantity">
+ <el-input v-model="form.pendingQuantity" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鏈鎺掍骇鏁伴噺锛�" prop="schedulingNum">
+ <el-input-number
+ v-model="form.schedulingNum"
+ placeholder="璇疯緭鍏�"
+ :min="0"
+ :step="0.1"
+ :precision="2"
+ clearable
+ @change="changeNum"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧澶х被锛�" prop="productCategory">
+ <el-input v-model="form.productCategory" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="娲惧伐浜猴細" prop="schedulingUserId">
+ <el-select
+ v-model="form.schedulingUserId"
+ placeholder="閫夋嫨浜哄憳"
+ style="width: 100%;"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="娲惧伐鏃ユ湡锛�" prop="schedulingDate">
+ <el-date-picker
+ v-model="form.schedulingDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+// import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+import {productionDispatch} from "@/api/productionManagement/productionOrder.js";
+import useUserStore from "@/store/modules/user.js";
+import dayjs from "dayjs";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const data = reactive({
+ form: {
+ projectName: "",
+ productCategory: "",
+ specificationModel: "", // 瑙勬牸鍨嬪彿
+ quantity: "",
+ schedulingNum: "",
+ schedulingUserId: "",
+ schedulingDate: "",
+ pendingQuantity: "",
+ speculativeTradingName: "", // 缁戝畾鏈哄櫒鍚嶇О
+ },
+ rules: {
+ schedulingNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" },],
+ schedulingUserId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" },],
+ schedulingDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" },],
+ },
+});
+const { form, rules } = toRefs(data);
+const userList = ref([])
+const userStore = useUserStore()
+
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ userListNoPageByTenantId().then((res) => {
+ userList.value = res.data;
+ });
+ form.value = {...row}
+ form.value.schedulingNum = 0
+ form.value.schedulingUserId = userStore.id
+ form.value.schedulingDate = dayjs().format("YYYY-MM-DD");
+}
+
+//
+const changeNum = (value) => {
+ if (value > form.value.pendingQuantity) {
+ form.value.schedulingNum = form.value.pendingQuantity;
+ proxy.$modal.msgWarning('鎺掍骇鏁伴噺涓嶅彲澶т簬寰呮帓浜ф暟閲�')
+ }
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ productionDispatch(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ })
+}
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/productionDispatching/index.vue b/src/views/productionManagement/productionDispatching/index.vue
new file mode 100644
index 0000000..2d39890
--- /dev/null
+++ b/src/views/productionManagement/productionDispatching/index.vue
@@ -0,0 +1,630 @@
+<template>
+ <div class="app-container">
+ <!-- 鐐掓満1-4 灞曠ず锛堟�婚噺 / 姝e湪鐢熶骇閲� / 绌轰綑閲忥級 -->
+ <div class="machines-grid">
+ <div v-for="machine in machines" :key="machine.id" class="machine-card">
+ <div class="machine-title">{{ machine.name }}</div>
+ <div class="machine-metrics">
+ <div class="machine-control">
+ <span>鎬婚噺(kg)锛�</span>
+ <el-input-number v-model="machineData[machine.name].workLoad" :min="0" :step="1" size="small" />
+ </div>
+ <div><span> 棰勮鎶曞叆閲�(kg)锛�</span><span>{{ machineData[machine.name].currentWorkLoad }}</span></div>
+ <div><span>绌轰綑宸ヤ綔閲�(kg)锛�</span><span>{{ machineData[machine.name].vacant }}</span></div>
+ </div>
+ </div>
+ <div class="save-button-container">
+ <div class="loss-rate-container">
+ <span class="loss-rate-label">鎹熻�楃巼(%)锛�</span>
+ <el-select v-model="rate" placeholder="璇烽�夋嫨鎹熻�楃巼" style="width: 120px" size="small">
+ <el-option label="6" :value="6" />
+ <el-option label="7" :value="7" />
+ <el-option label="8" :value="8" />
+ <el-option label="9" :value="9" />
+ <el-option label="10" :value="10" />
+ </el-select>
+ </div>
+ <el-button type="primary" @click="saveMachineTotals" size="small">淇濆瓨璁剧疆</el-button>
+ </div>
+ </div>
+ <div class="search_form">
+ <div>
+ <span class="search_title">瀹㈡埛鍚嶇О锛�</span>
+ <el-input
+ v-model="searchForm.customerName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search"
+ />
+ <span class="search_title ml10">鍚堝悓鍙凤細</span>
+ <el-input
+ v-model="searchForm.salesContractNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search"
+ />
+<!-- <span class="search_title ml10">椤圭洰鍚嶇О锛�</span>-->
+<!-- <el-input-->
+<!-- v-model="searchForm.projectName"-->
+<!-- style="width: 240px"-->
+<!-- placeholder="璇疯緭鍏�"-->
+<!-- @change="handleQuery"-->
+<!-- clearable-->
+<!-- prefix-icon="Search"-->
+<!-- />-->
+ <span class="search_title ml10">褰曞叆鏃ユ湡锛�</span>
+ <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+ placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
+ <el-checkbox
+ style="margin-left: 10px"
+ v-model="searchForm.status"
+ label="涓嶆樉绀哄緟鎺掓暟閲忎负0"
+ @change="handleQuery"
+ />
+ <el-button type="primary" @click="handleQuery" style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">鐢熶骇娲惧伐</el-button>
+ <el-button type="success" @click="openAutoDispatch">鑷姩娲惧伐</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery"></form-dia>
+ <auto-dispatch-dia ref="autoDispatchDia" @close="handleQuery"></auto-dispatch-dia>
+ </div>
+</template>
+
+<script setup>
+import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick, computed, watch} from "vue";
+import FormDia from "@/views/productionManagement/productionDispatching/components/formDia.vue";
+import AutoDispatchDia from "@/views/productionManagement/productionDispatching/components/autoDispatchDia.vue";
+import dayjs from "dayjs";
+import {schedulingListPage, schedulingList, addSpeculatTrading, updateSpeculatTrading, getLossRate, addLossRate, updateLossRate} from "@/api/productionManagement/productionOrder.js";
+import { ElMessageBox } from "element-plus";
+
+const data = reactive({
+ searchForm: {
+ customerName: "",
+ salesContractNo: "",
+ projectName: "",
+ status: true,
+ entryDate: [dayjs().format("YYYY-MM-DD"), dayjs().format("YYYY-MM-DD")], // 褰曞叆鏃ユ湡锛岄粯璁ゅ綋澶�
+ entryDateStart: dayjs().format("YYYY-MM-DD"),
+ entryDateEnd: dayjs().format("YYYY-MM-DD"),
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鍚堝悓鍙�",
+ prop: "salesContractNo",
+ width: 220,
+ },
+ {
+ label: "瀹㈡埛鍚嶇О",
+ prop: "customerName",
+ width: 250,
+ },
+ {
+ label: "浜у搧澶х被",
+ prop: "productCategory",
+ width: 160,
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "specificationModel",
+ width: 120,
+ },
+ {
+ label: "缁戝畾鏈哄櫒",
+ prop: "speculativeTradingName",
+ width: 160,
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ width:90
+ },
+ {
+ label: "褰曞叆鏃ユ湡",
+ prop: "entryDate",
+ width: 120,
+ },
+ {
+ label: "鐘舵��",
+ prop: "status",
+ dataType: "tag",
+ formatType: (params) => {
+ if (params == '鐢熶骇涓�') {
+ return "warning";
+ } else if (params == '鏈紑濮�') {
+ return "danger";
+ } else {
+ return "success";
+ }
+ },
+ },
+ {
+ label: "鐢熶骇杩涘害",
+ prop: "progress",
+ formatData: (cellValue) => {
+ // 濡傛灉鍊间负绌烘垨undefined锛屾樉绀虹┖瀛楃涓�
+ if (cellValue === null || cellValue === undefined || cellValue === '') {
+ return '';
+ }
+ // 鐩存帴鍦ㄦ暟瀛楀悗闈㈡坊鍔犵櫨鍒嗗彿
+ return `${cellValue}%`;
+ }
+ },
+ {
+ label: "鏁伴噺",
+ prop: "quantity",
+ },
+ {
+ label: "鎺掍骇鏁伴噺",
+ prop: "schedulingNum",
+ width: 100,
+ },
+ {
+ label: "寰呮帓鏁伴噺",
+ prop: "pendingQuantity",
+ width: 100,
+ fixed: 'right',
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const formDia = ref()
+const autoDispatchDia = ref()
+const { proxy } = getCurrentInstance()
+
+// 鐐掓満鏁版嵁
+const machineData = reactive({
+ "鐐掓満1": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
+ "鐐掓満2": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
+ "鐐掓満3": { workLoad: 0, currentWorkLoad: 0, vacant: 0 },
+ "鐐掓満4": { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
+})
+
+// 鐐掓満閰嶇疆鏁扮粍
+const machines = [
+ { id: 1, name: '鐐掓満1' },
+ { id: 2, name: '鐐掓満2' },
+ { id: 3, name: '鐐掓満3' },
+ { id: 4, name: '鐐掓満4' }
+]
+
+// 淇濆瓨鐐掓満鎬婚噺璁剧疆
+const saveMachineTotals = () => {
+ // 楠岃瘉鎹熻�楃巼鏄惁宸查�夋嫨
+ if (rate.value === null || rate.value === undefined || isNaN(rate.value)) {
+ proxy.$message.warning('璇烽�夋嫨鎹熻�楃巼');
+ return;
+ }
+
+ // 鏋勯�犱繚瀛樻暟鎹暟缁勶紝浣跨敤machines鏁扮粍寰幆鏋勫缓
+ const saveData = machines.map(machine => {
+ const saveItem = {
+ name: machine.name, // 鐐掓満鍚嶇О
+ workLoad: machineData[machine.name].workLoad, // 鎬婚噺
+ currentWorkLoad: machineData[machine.name].currentWorkLoad, // 棰勮鎶曞叆閲�
+ vacant: machineData[machine.name].vacant // 绌轰綑閲�
+ };
+
+ // 濡傛灉鏄慨鏀规搷浣滐紝闇�瑕佷紶閫抜d瀛楁
+ if (hasQueryData.value) {
+ const queryData = getMachineQueryData(machine.id);
+ if (queryData && queryData.id) {
+ saveItem.id = queryData.id;
+ }
+ }
+
+ return saveItem;
+ });
+
+ // 鏋勯�犳崯鑰楃巼鏁版嵁
+ const rateData = {
+ rate: rate.value
+ };
+
+ // 濡傛灉鏈塈D锛岃鏄庢槸淇敼鎿嶄綔
+ if (rateId.value) {
+ rateData.id = rateId.value;
+ }
+
+ // 鏍规嵁鏄惁鏈夋煡璇㈡暟鎹喅瀹氳皟鐢ㄦ柊澧炴帴鍙h繕鏄慨鏀规帴鍙�
+ const saveApi = hasQueryData.value ? updateSpeculatTrading : addSpeculatTrading;
+ const successMessage = hasQueryData.value ? '鐐掓満璁剧疆淇敼鎴愬姛' : '鐐掓満璁剧疆鏂板鎴愬姛';
+
+ // 鏍规嵁鏄惁鏈塈D鍐冲畾璋冪敤鏂板鎺ュ彛杩樻槸淇敼鎺ュ彛
+ const rateApi = rateId.value ? updateLossRate : addLossRate;
+ const rateSuccessMessage = rateId.value ? '鎹熻�楃巼淇敼鎴愬姛' : '鎹熻�楃巼鏂板鎴愬姛';
+
+ // 骞惰璋冪敤涓や釜鎺ュ彛
+ Promise.all([
+ saveApi(saveData),
+ rateApi(rateData)
+ ]).then(([saveRes, rateRes]) => {
+ proxy.$message.success(successMessage);
+ proxy.$message.success(rateSuccessMessage);
+
+ // 淇濆瓨鎴愬姛鍚庯紝璁剧疆hasQueryData涓簍rue锛屼笅娆′繚瀛樺皢璋冪敤淇敼鎺ュ彛
+ if (!hasQueryData.value) {
+ hasQueryData.value = true;
+ }
+
+ // 濡傛灉杩斿洖浜咺D锛屼繚瀛樿捣鏉�
+ if (rateRes && rateRes.data && rateRes.data.id) {
+ rateId.value = rateRes.data.id;
+ }
+
+ // 淇濆瓨鎴愬姛鍚庨噸鏂拌皟鐢ㄦ煡璇㈤〉闈�
+ getList();
+ }).catch(err => {
+ proxy.$message.error('淇濆瓨澶辫触');
+ console.error('淇濆瓨澶辫触:', err);
+ });
+}
+
+// 鑾峰彇鐐掓満鏌ヨ鏁版嵁
+const machineQueryData = ref([]);
+
+const getMachineQueryData = (machineId) => {
+ return machineQueryData.value.find(item => item.id === machineId);
+};
+
+const getMachineIndex = (item) => {
+ // 鍏煎澶氱瀛楁鍛藉悕锛岃繑鍥� 1-4 涔嬩竴锛屽惁鍒欒繑鍥� 0锛堟湭鐭ワ級
+ const candidates = [item.machineId, item.machineNo, item.machine, item.deviceNo, item.deviceId]
+ for (const v of candidates) {
+ if (v === undefined || v === null) continue
+ const n = Number(String(v).replace(/[^\d]/g, "")) // 鎶藉彇鏁板瓧
+ if ([1,2,3,4].includes(n)) return n
+ }
+ return 0
+}
+
+const computeTodaySummary = () => {
+ const todayStr = dayjs().format("YYYY-MM-DD")
+
+ // 閲嶇疆鎵�鏈夌倰鏈烘暟鎹�
+ machines.forEach(machine => {
+ machineData[machine.name] = { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
+ })
+
+ tableData.value.forEach(item => {
+ // 浠呯粺璁″綋澶�
+ const isToday = dayjs(item.entryDate).format("YYYY-MM-DD") === todayStr
+ if (!isToday) return
+
+ // 浣跨敤姝g‘鐨勫瓧娈靛悕锛歸orkLoad锛堢倰鏈哄伐浣滈噺锛�, currentWorkLoad锛堢倰鏈烘鍦ㄥ伐浣滈噺锛�
+ const workLoad = Number(item.workLoad) || 0
+ const currentWorkLoad = Number(item.currentWorkLoad) || 0
+ const machineName = item.speculativeTradingName || '鐐掓満1'
+
+ if (machineData[machineName]) {
+ machineData[machineName].workLoad += workLoad
+ machineData[machineName].currentWorkLoad += currentWorkLoad
+ machineData[machineName].vacant = machineData[machineName].workLoad - machineData[machineName].currentWorkLoad
+ }
+ })
+}
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+// 鏄惁鏈夋煡璇㈡暟鎹�
+const hasQueryData = ref(false)
+// 鎹熻�楃巼
+const rate = ref(6)
+// 鎹熻�楃巼ID
+const rateId = ref(null)
+
+// 鑾峰彇鐐掓満姝e湪宸ヤ綔閲忔暟鎹�
+const getMachineProductionData = () => {
+ schedulingList().then((res) => {
+ // 澶勭悊鐐掓満姝e湪宸ヤ綔閲忔暟鎹�
+ if (res.data && Array.isArray(res.data)) {
+ // 璁剧疆鏄惁鏈夋煡璇㈡暟鎹�
+ hasQueryData.value = res.data.length > 0
+
+ // 淇濆瓨鏌ヨ鏁版嵁鍒癿achineQueryData
+ machineQueryData.value = res.data;
+
+ // 閲嶇疆鎵�鏈夌倰鏈烘暟鎹�
+ machines.forEach(machine => {
+ machineData[machine.name] = { workLoad: 0, currentWorkLoad: 0, vacant: 0 }
+ });
+
+ // 閬嶅巻鏁版嵁锛屾牴鎹煡璇㈣繑鍥炵殑鏁版嵁缁撴瀯澶勭悊
+ res.data.forEach(item => {
+ // 鏍规嵁name瀛楁纭畾鐐掓満
+ const machineName = item.name || '鐐掓満1';
+
+ if (machineData[machineName]) {
+ // 濡傛灉鏌ヨ鏁版嵁涓湁workLoad锛屽垯鍒濆鍖栫倰鏈烘�婚噺
+ if (item.workLoad !== null && item.workLoad !== undefined) {
+ machineData[machineName].workLoad = Number(item.workLoad) || 0;
+ }
+
+ // 濡傛灉鏌ヨ鏁版嵁涓湁currentWorkLoad锛屽垯璁剧疆姝e湪宸ヤ綔閲�
+ if (item.currentWorkLoad !== null && item.currentWorkLoad !== undefined) {
+ machineData[machineName].currentWorkLoad = Number(item.currentWorkLoad) || 0;
+ }
+
+ // 璁$畻绌轰綑宸ヤ綔閲�
+ machineData[machineName].vacant = machineData[machineName].workLoad - machineData[machineName].currentWorkLoad;
+ }
+ });
+ }
+ }).catch(err => {
+ console.error('鑾峰彇鐐掓満姝e湪宸ヤ綔閲忔暟鎹け璐�:', err);
+ });
+};
+
+const changeDaterange = (value) => {
+ if (value) {
+ searchForm.value.entryDateStart = value[0];
+ searchForm.value.entryDateEnd = value[1];
+ } else {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ }
+ handleQuery();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ // 鏋勯�犱竴涓柊鐨勫璞★紝涓嶅寘鍚玡ntryDate瀛楁
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined
+ schedulingListPage(params).then((res) => {
+ tableLoading.value = false;
+ // 澶勭悊姣忔潯鏁版嵁锛屽鍔爌endingQuantity瀛楁
+ tableData.value = res.data.records.map(item => ({
+ ...item,
+ pendingQuantity: (Number(item.quantity) || 0) - (Number(item.schedulingNum) || 0)
+ }));
+ page.total = res.data.total;
+ computeTodaySummary()
+
+ // 鍚屾椂鑾峰彇鐐掓満姝e湪宸ヤ綔閲忔暟鎹�
+ getMachineProductionData();
+ // 鑾峰彇鎹熻�楃巼鏁版嵁
+ getLossRateData();
+ }).catch(() => {
+ tableLoading.value = false;
+ })
+};
+
+// 鑾峰彇鎹熻�楃巼鏁版嵁
+const getLossRateData = () => {
+ getLossRate().then((res) => {
+ const data = res.data || res;
+ if (data && data.rate !== undefined && data.rate !== null) {
+ rate.value = Number(data.rate); // 纭繚杞崲涓烘暟瀛�
+ rateId.value = data.id || null;
+ } else {
+ rate.value = 6;
+ rateId.value = null;
+ }
+ }).catch(err => {
+ console.error('鑾峰彇鎹熻�楃巼鏁版嵁澶辫触:', err);
+ rate.value = 6;
+ rateId.value = null;
+ });
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type) => {
+ if (selectedRows.value.length !== 1) {
+ proxy.$message.error("璇烽�夋嫨涓�鏉℃暟鎹�");
+ return;
+ }
+ if (selectedRows.value[0].pendingQuantity == 0) {
+ proxy.$message.warning("鏃犻渶鍐嶆淳宸�");
+ return;
+ }
+ nextTick(() => {
+ formDia.value?.openDialog(type, selectedRows.value[0])
+ })
+};
+
+// 鎵撳紑鑷姩娲惧伐寮规
+const openAutoDispatch = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$message.error("璇烽�夋嫨鑷冲皯涓�鏉℃暟鎹�");
+ return;
+ }
+
+ // 杩囨护鎺夊緟鎺掍骇鏁伴噺涓�0鐨勬暟鎹�
+ const validRows = selectedRows.value.filter(row => row.pendingQuantity > 0);
+
+ if (validRows.length === 0) {
+ proxy.$message.warning("閫変腑鐨勬暟鎹棤闇�娲惧伐");
+ return;
+ }
+
+ nextTick(() => {
+ autoDispatchDia.value?.openDialog('auto', validRows)
+ })
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/salesLedger/scheduling/exportOne", {}, "鐢熶骇娲惧伐.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getList();
+ getLossRateData();
+});
+</script>
+
+<style scoped>
+.summary-bar{
+ display: flex;
+ gap: 16px;
+ margin: 10px 0 16px 0;
+}
+.summary-item{
+ background: #f5f7fa;
+ border: 1px solid #ebeef5;
+ border-radius: 6px;
+ padding: 10px 16px;
+ min-width: 160px;
+}
+.summary-label{
+ color: #909399;
+ font-size: 12px;
+ margin-bottom: 6px;
+}
+.summary-value{
+ color: #303133;
+ font-size: 20px;
+ font-weight: 600;
+}
+.summary-control{
+ display: flex;
+ align-items: center;
+ height: 28px;
+}
+.machines-grid{
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: 16px;
+ margin-bottom: 20px;
+ padding: 16px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ border: 1px solid #e9ecef;
+}
+.machine-card{
+ border: 1px solid #dee2e6;
+ border-radius: 8px;
+ padding: 16px;
+ background: #fff;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
+ transition: all 0.3s ease;
+}
+.machine-card:hover{
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
+}
+.machine-title{
+ font-weight: 600;
+ font-size: 16px;
+ margin-bottom: 12px;
+ color: #2c3e50;
+ text-align: center;
+ padding-bottom: 8px;
+ border-bottom: 2px solid #3498db;
+}
+.machine-metrics{
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ color: #495057;
+}
+.machine-control{
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ padding: 8px 0;
+ border-bottom: 1px solid #f1f3f4;
+}
+.machine-control span{
+ font-size: 14px;
+ white-space: nowrap;
+ color: #6c757d;
+ font-weight: 500;
+}
+.machine-metrics > div:not(.machine-control) {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 0;
+ font-size: 14px;
+}
+.machine-metrics > div:not(.machine-control) span:first-child {
+ color: #6c757d;
+}
+.machine-metrics > div:not(.machine-control) span:last-child {
+ font-weight: 600;
+ color: #2c3e50;
+}
+.save-button-container{
+ grid-column: 1 / -1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 16px;
+ margin-top: 16px;
+ padding-top: 16px;
+ border-top: 1px solid #e9ecef;
+}
+.loss-rate-container{
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.loss-rate-label{
+ font-size: 14px;
+ color: #6c757d;
+ font-weight: 500;
+ white-space: nowrap;
+}
+</style>
+
+
+
+
+
+
+
diff --git a/src/views/productionManagement/productionOrder/New.vue b/src/views/productionManagement/productionOrder/New.vue
new file mode 100644
index 0000000..ea74186
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/New.vue
@@ -0,0 +1,204 @@
+<template>
+ <div>
+ <el-dialog v-model="isShow"
+ title="鏂板鐢熶骇璁㈠崟"
+ width="800"
+ @close="closeModal">
+ <el-form label-width="140px"
+ :model="formState"
+ label-position="top"
+ ref="formRef">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productModelId"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨浜у搧',
+ trigger: 'change',
+ }
+ ]">
+ <el-button type="primary"
+ @click="showProductSelectDialog = true">
+ {{ formState.productName ? formState.productName : '閫夋嫨浜у搧' }}
+ </el-button>
+ </el-form-item>
+ <el-form-item label="瑙勬牸"
+ prop="productModelName">
+ <el-input v-model="formState.productModelName"
+ disabled />
+ </el-form-item>
+ <el-form-item label="鍗曚綅"
+ prop="unit">
+ <el-input v-model="formState.unit"
+ disabled />
+ </el-form-item>
+ <el-form-item label="宸ヨ壓璺嚎">
+ <el-select v-model="formState.technologyRoutingId"
+ placeholder="璇烽�夋嫨宸ヨ壓璺嚎"
+ style="width: 100%;"
+ :loading="bindRouteLoading">
+ <el-option v-for="item in routeOptions"
+ :key="item.id"
+ :label="`${item.processRouteCode || ''}`"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="闇�姹傛暟閲�"
+ prop="quantity">
+ <el-input-number v-model="formState.quantity"
+ :step="1"
+ :min="0"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="璁″垝瀹屾垚鏃堕棿"
+ prop="planCompleteTime">
+ <el-date-picker v-model="formState.planCompleteTime"
+ type="date"
+ placeholder="閫夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ </el-form>
+ <!-- 浜у搧閫夋嫨寮圭獥 -->
+ <ProductSelectDialog v-model="showProductSelectDialog"
+ @confirm="handleProductSelect"
+ single />
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, computed, getCurrentInstance } from "vue";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+ import {
+ addProductOrder,
+ listProcessRoute,
+ } from "@/api/productionManagement/productionOrder.js";
+ import { listPage } from "@/api/productionManagement/processRoute.js";
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ type: {
+ type: String,
+ required: true,
+ default: "qualified",
+ },
+ });
+
+ const emit = defineEmits(["update:visible", "completed"]);
+
+ // 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+ const formState = ref({
+ productId: undefined,
+ productModelId: undefined,
+ technologyRoutingId: undefined,
+ productName: "",
+ productModelName: "",
+ unit: "",
+ quantity: 0,
+ planCompleteTime: "",
+ });
+
+ const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+ });
+
+ const showProductSelectDialog = ref(false);
+
+ let { proxy } = getCurrentInstance();
+
+ const closeModal = () => {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ formState.value = {
+ productId: undefined,
+ productModelId: undefined,
+ technologyRoutingId: undefined,
+ productName: "",
+ productModelName: "",
+ unit: "",
+ quantity: "",
+ planCompleteTime: "",
+ };
+ isShow.value = false;
+ };
+
+ // 浜у搧閫夋嫨澶勭悊
+ const handleProductSelect = async products => {
+ if (products && products.length > 0) {
+ const product = products[0];
+ formState.value.productId = product.productId;
+ formState.value.productName = product.productName;
+ formState.value.productModelName = product.model;
+ formState.value.productModelId = product.id;
+ formState.value.unit = product.unit;
+ showProductSelectDialog.value = false;
+ fetchRouteOptions(product.id);
+ // 瑙﹀彂琛ㄥ崟楠岃瘉鏇存柊
+ proxy.$refs["formRef"]?.validateField("productModelId");
+ }
+ };
+
+ const routeOptions = ref([]);
+ const bindRouteLoading = ref(false);
+ const fetchRouteOptions = productModelId => {
+ formState.value.technologyRoutingId = undefined;
+ routeOptions.value = [];
+ bindRouteLoading.value = true;
+ listPage({ productModelId: productModelId })
+ .then(res => {
+ routeOptions.value = res.data.records || [];
+ })
+ .finally(() => {
+ bindRouteLoading.value = false;
+ });
+ };
+
+ const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ // 楠岃瘉鏄惁閫夋嫨浜嗕骇鍝佸拰瑙勬牸
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨浜у搧");
+ return;
+ }
+ if (!formState.value.productModelId) {
+ proxy.$modal.msgError("璇烽�夋嫨瑙勬牸");
+ return;
+ }
+ if (formState.value.quantity <= 0) {
+ proxy.$modal.msgError("闇�姹傛暟閲忓繀椤诲ぇ浜�0");
+ return;
+ }
+
+ addProductOrder(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit("completed");
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ });
+ }
+ });
+ };
+
+ defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+ });
+</script>
diff --git a/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
new file mode 100644
index 0000000..370815e
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue
@@ -0,0 +1,284 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogVisible"
+ title="棰嗘枡璇︽儏"
+ width="1400px"
+ @close="handleClose">
+ <el-table v-loading="materialDetailLoading"
+ :data="materialDetailTableData"
+ border
+ row-key="id">
+ <el-table-column label="宸ュ簭鍚嶇О"
+ prop="operationName"
+ min-width="180" />
+ <el-table-column label="鍘熸枡鍚嶇О"
+ prop="productName"
+ min-width="160" />
+ <el-table-column label="鍘熸枡鍨嬪彿"
+ prop="model"
+ min-width="180" />
+ <el-table-column label="鎵瑰彿"
+ prop="batchNo"
+ min-width="150" />
+ <el-table-column label="闇�姹傛暟閲�"
+ prop="demandedQuantity"
+ min-width="110" />
+ <el-table-column label="璁¢噺鍗曚綅"
+ prop="unit"
+ width="100" />
+ <el-table-column label="棰嗙敤鏁伴噺"
+ prop="pickQuantity"
+ min-width="110" />
+ <el-table-column label="琛ユ枡鏁伴噺"
+ min-width="120">
+ <template #default="{ row }">
+ <el-button type="primary"
+ link
+ @click="handleViewSupplementRecord(row)">
+ {{ row.feedingQty ?? 0 }}
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�鏂欐暟閲�"
+ min-width="110">
+ <template #default="{ row }">
+ {{ row.returnQty ?? 0 }}
+ </template>
+ </el-table-column>
+ <el-table-column label="瀹為檯鏁伴噺"
+ min-width="140">
+ <template #default="{ row }">
+ <el-input-number v-model="row.actualQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ placeholder="杈撳叆瀹為檯鏁伴噺"
+ style="width: 100%;"
+ :disabled="row.returned || orderRow?.end"
+ @change="val => handleActualQtyChange(row, val)" />
+ </template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button v-if="!orderRow?.end"
+ type="warning"
+ :loading="materialReturnConfirming"
+ :disabled="!canOpenReturnSummary"
+ @click="openReturnSummaryDialog">
+ 閫�鏂欑‘璁�
+ </el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <el-dialog v-model="supplementRecordDialogVisible"
+ title="琛ユ枡璁板綍"
+ width="800px">
+ <el-table v-loading="supplementRecordLoading"
+ :data="supplementRecordTableData"
+ border
+ row-key="id">
+ <el-table-column label="琛ユ枡鏁伴噺"
+ prop="pickQuantity"
+ min-width="120" />
+ <el-table-column label="琛ユ枡浜�"
+ prop="supplementUserName"
+ min-width="120" />
+ <el-table-column label="琛ユ枡鏃ユ湡"
+ prop="supplementTime"
+ min-width="160" />
+ <el-table-column label="琛ユ枡鍘熷洜"
+ prop="feedingReason"
+ min-width="200" />
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="supplementRecordDialogVisible = false">鍏抽棴</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <el-dialog v-model="returnSummaryDialogVisible"
+ title="閫�鏂欐眹鎬荤‘璁�"
+ width="900px">
+ <el-table :data="returnSummaryList"
+ border
+ row-key="summaryKey">
+ <el-table-column label="鍘熸枡鍚嶇О"
+ prop="materialName"
+ min-width="180" />
+ <el-table-column label="鍘熸枡鍨嬪彿"
+ prop="materialModel"
+ min-width="180" />
+ <el-table-column label="璁¢噺鍗曚綅"
+ prop="unit"
+ min-width="100" />
+ <el-table-column label="閫�鏂欐眹鎬绘暟閲�"
+ prop="returnQtyTotal"
+ min-width="140" />
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="materialReturnConfirming"
+ @click="handleReturnConfirm">纭鎻愪氦</el-button>
+ <el-button @click="returnSummaryDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from "vue";
+ import { ElMessage } from "element-plus";
+ import {
+ listMaterialPickingDetail,
+ listMaterialSupplementRecord,
+ updateMaterialPickingLedger,
+ } from "@/api/productionManagement/productionOrder.js";
+
+ const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ orderRow: { type: Object, default: null },
+ });
+ const emit = defineEmits(["update:modelValue", "confirmed"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const materialDetailLoading = ref(false);
+ const materialDetailTableData = ref([]);
+ const materialReturnConfirming = ref(false);
+ const supplementRecordDialogVisible = ref(false);
+ const supplementRecordLoading = ref(false);
+ const supplementRecordTableData = ref([]);
+ const returnSummaryDialogVisible = ref(false);
+ const returnSummaryList = ref([]);
+ const calcReturnQty = item =>
+ Number(item.pickQuantity || 0) +
+ Number(item.feedingQty || 0) -
+ Number(item.actualQty || 0);
+ const canOpenReturnSummary = computed(() =>
+ materialDetailTableData.value.some(
+ item => item.returned !== true && calcReturnQty(item) > 0
+ )
+ );
+
+ const loadDetailList = async () => {
+ if (!props.orderRow?.id) return;
+ materialDetailLoading.value = true;
+ materialDetailTableData.value = [];
+ try {
+ const res = await listMaterialPickingDetail(props.orderRow.id);
+ materialDetailTableData.value = (res.data || []).map(item => ({
+ ...item,
+ actualQty:
+ item.actualQty ??
+ Number(item.pickQuantity || 0) + Number(item.feedingQty || 0),
+ returnQty: item.returnQty ?? 0,
+ }));
+ } finally {
+ materialDetailLoading.value = false;
+ }
+ };
+
+ watch(
+ () => dialogVisible.value,
+ visible => {
+ if (visible) {
+ loadDetailList();
+ }
+ }
+ );
+
+ const handleClose = () => {
+ materialDetailTableData.value = [];
+ };
+
+ const handleActualQtyChange = (row, val) => {
+ row.returnQty = calcReturnQty(row);
+ };
+
+ const handleViewSupplementRecord = async row => {
+ if (!row?.id) return;
+ supplementRecordDialogVisible.value = true;
+ supplementRecordLoading.value = true;
+ supplementRecordTableData.value = [];
+ try {
+ const res = await listMaterialSupplementRecord({
+ pickId: row.id,
+ productionOrderId: props.orderRow.id,
+ });
+ supplementRecordTableData.value = res.data || [];
+ } finally {
+ supplementRecordLoading.value = false;
+ }
+ };
+
+ const buildReturnSummary = () => {
+ const map = new Map();
+ materialDetailTableData.value.forEach(item => {
+ const returnQty = calcReturnQty(item);
+ if (returnQty <= 0) return;
+ const key = `${item.productModelId || ""}_${item.productName || ""}_${
+ item.model || ""
+ }_${item.unit || ""}`;
+ const old = map.get(key) || {
+ summaryKey: key,
+ materialName: item.productName || "",
+ materialModel: item.model || "",
+ unit: item.unit || "",
+ returnQtyTotal: 0,
+ };
+ old.returnQtyTotal += returnQty;
+ map.set(key, old);
+ });
+ return Array.from(map.values());
+ };
+
+ const openReturnSummaryDialog = async () => {
+ if (!canOpenReturnSummary.value) {
+ ElMessage.warning("閫�鏂欐暟閲�=棰嗙敤鏁伴噺+琛ユ枡鏁伴噺-瀹為檯鏁伴噺锛屼笖闇�澶т簬0");
+ return;
+ }
+ returnSummaryList.value = buildReturnSummary();
+ returnSummaryDialogVisible.value = true;
+ };
+
+ const handleReturnConfirm = async () => {
+ if (!props.orderRow?.id) return;
+ materialReturnConfirming.value = true;
+ try {
+ await updateMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: materialDetailTableData.value.map(item => ({
+ id: item.id,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.productModelId,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQuantity,
+ batchNo: item.batchNo,
+ feedingQty: item.feedingQty,
+ returnQty: item.returnQty,
+ actualQty: item.actualQty,
+ feedingReason: item.feedingReason,
+ returned: true,
+ })),
+ });
+ returnSummaryDialogVisible.value = false;
+ dialogVisible.value = false;
+ emit("confirmed");
+ } finally {
+ materialReturnConfirming.value = false;
+ }
+ };
+</script>
+
+<style scoped lang="scss"></style>
diff --git a/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
new file mode 100644
index 0000000..09e7421
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue
@@ -0,0 +1,427 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogVisible"
+ title="棰嗘枡鍙拌处"
+ width="1200px"
+ @close="handleClose">
+ <div class="material-toolbar">
+ <el-button type="primary"
+ @click="handleAddMaterialRow">鏂板</el-button>
+ </div>
+ <el-table v-loading="materialTableLoading"
+ :data="materialTableData"
+ border
+ row-key="tempId">
+ <el-table-column label="宸ュ簭鍚嶇О"
+ min-width="140">
+ <template #default="{ row }">
+ <span v-if="row.bom === true">{{ row.operationName || "-" }}</span>
+ <el-select v-else
+ v-model="row.operationName"
+ placeholder="璇烽�夋嫨宸ュ簭"
+ clearable
+ filterable
+ style="width: 100%;"
+ @change="val => handleProcessNameChange(row, val)">
+ <el-option v-for="item in processOptions"
+ :key="item.technologyOperationId"
+ :label="item.name"
+ :value="item.name" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍘熸枡鍚嶇О"
+ min-width="140">
+ <template #default="{ row }">
+ <span v-if="row.bom === true">{{ row.materialName || "-" }}</span>
+ <el-button v-else
+ type="primary"
+ link
+ @click="openMaterialProductSelect(row)">
+ {{ row.materialName || "閫夋嫨鍘熸枡" }}
+ </el-button>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍘熸枡鍨嬪彿"
+ min-width="140">
+ <template #default="{ row }">
+ {{ row.materialModel || "-" }}
+ </template>
+ </el-table-column>
+ <!-- 鎵瑰彿澶氶�� -->
+ <el-table-column min-width="200"
+ label="鎵瑰彿">
+ <template #default="{ row }">
+ <el-select v-model="row.batchNo"
+ multiple
+ collapse-tags
+ collapse-tags-indicator
+ placeholder="璇烽�夋嫨鎵瑰彿"
+ style="width: 100%;">
+ <el-option v-for="item in row.batchNoList"
+ :key="item"
+ :label="item"
+ :value="item" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="闇�姹傛暟閲�"
+ min-width="120">
+ <template #default="{ row }">
+ <span v-if="row.bom === true">{{ row.demandedQuantity ?? "-" }}</span>
+ <el-input-number v-else
+ v-model="row.demandedQuantity"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ style="width: 100%;"
+ @change="val => handleRequiredQtyChange(row, val)" />
+ </template>
+ </el-table-column>
+ <el-table-column label="璁¢噺鍗曚綅"
+ width="100">
+ <template #default="{ row }">
+ {{ row.unit || "-" }}
+ </template>
+ </el-table-column>
+ <el-table-column label="棰嗙敤鏁伴噺"
+ min-width="120">
+ <template #default="{ row }">
+ <el-input-number v-model="row.pickQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ style="width: 100%;" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ width="90"
+ fixed="right">
+ <template #default="{ $index, row }">
+ <el-button v-if="row.bom !== true"
+ type="danger"
+ link
+ @click="handleDeleteMaterialRow($index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="materialSaving"
+ :disabled="isSaveDisabled"
+ @click="handleMaterialSave">淇濆瓨</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <ProductSelectDialog v-model="materialProductDialogVisible"
+ @confirm="handleMaterialProductConfirm"
+ single />
+ <!-- request-url="/stockInventory/rawMaterials" -->
+ </div>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from "vue";
+ import { ElMessage } from "element-plus";
+ import ProductSelectDialog from "@/views/basicData/product/ProductSelectDialog.vue";
+ import {
+ findProductProcessRouteItemList,
+ listMain,
+ } from "@/api/productionManagement/productProcessRoute.js";
+ import {
+ listMaterialPickingDetail,
+ listMaterialPickingBom,
+ listMaterialPickingLedger,
+ saveMaterialPickingLedger,
+ updateMaterialPickingLedger,
+ } from "@/api/productionManagement/productionOrder.js";
+ import { queryList2 } from "@/api/productionManagement/productStructure.js";
+
+ const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ orderRow: { type: Object, default: null },
+ });
+ const emit = defineEmits(["update:modelValue", "saved"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const materialProductDialogVisible = ref(false);
+ const materialTableLoading = ref(false);
+ const materialSaving = ref(false);
+ const materialTableData = ref([]);
+
+ const isSaveDisabled = computed(() => {
+ if (materialTableData.value.length === 0) return true;
+ return !materialTableData.value.some(row => {
+ // 妫�鏌ユ槸鍚︽湁浠讳綍鐢ㄦ埛杈撳叆鍐呭
+ const hasBatch = Array.isArray(row.batchNo) && row.batchNo.length > 0;
+ const hasPickQty =
+ row.pickQty !== null && row.pickQty !== undefined && row.pickQty !== 0;
+
+ if (row.bom) {
+ // 瀵逛簬鏉ヨ嚜BOM鐨勮锛岃緭鍏ユ鍙湁鈥滄壒鍙封�濆拰鈥滈鐢ㄦ暟閲忊��
+ return hasBatch || hasPickQty;
+ } else {
+ // 瀵逛簬鏂板琛岋紝杈撳叆妗嗗寘鎷�滃伐搴忊�濄�佲�滃師鏂欌�濄�佲�滈渶姹傛暟閲忊�濄�佲�滄壒鍙封�濆拰鈥滈鐢ㄦ暟閲忊��
+ const hasOperation = !!row.operationName;
+ const hasMaterial = !!row.materialName;
+ const hasDemanded =
+ row.demandedQuantity !== null &&
+ row.demandedQuantity !== undefined &&
+ row.demandedQuantity !== 0;
+ return (
+ hasBatch || hasPickQty || hasOperation || hasMaterial || hasDemanded
+ );
+ }
+ });
+ });
+
+ const processOptions = ref([]);
+ const currentMaterialSelectRowIndex = ref(-1);
+ let materialTempId = 0;
+
+ const createMaterialRow = (row = {}) => ({
+ tempId: row.id || `temp_${++materialTempId}`,
+ id: row.id,
+ processId: row.processId || row.technologyOperationId,
+ technologyOperationId: row.technologyOperationId || row.processId,
+ operationName: row.operationName || "",
+ bom: row.bom === true,
+ materialModelId: row.materialModelId || row.productModelId,
+ materialName: row.materialName || row.productName || "",
+ materialModel: row.materialModel || row.model || "",
+ demandedQuantity: Number(row.requiredQty ?? row.demandedQuantity ?? 0),
+ unit: row.unit || "",
+ pickQty: Number(row.pickQty ?? row.pickQuantity ?? 0),
+ batchNo: row.batchNo
+ ? typeof row.batchNo === "string"
+ ? row.batchNo.split(",")
+ : row.batchNo
+ : [],
+ batchNoList: row.batchNoList || [],
+ });
+
+ const getProcessOptions = async () => {
+ if (!props.orderRow?.id) return;
+ const res = await findProductProcessRouteItemList({
+ orderId: props.orderRow.id,
+ });
+ const routeList = Array.isArray(res?.data)
+ ? res.data
+ : res?.data?.records || [];
+ const processMap = new Map();
+ routeList.forEach(item => {
+ const processId = item.technologyOperationId;
+ const operationName = item.operationName;
+ if (!processId || !operationName) return;
+ const key = `${processId}_${operationName}`;
+ if (!processMap.has(key)) {
+ processMap.set(key, {
+ id: processId,
+ name: operationName,
+ });
+ }
+ });
+ processOptions.value = Array.from(processMap.values());
+ };
+ const isDetail = ref(true);
+
+ const loadMaterialData = async () => {
+ if (!props.orderRow?.id) return;
+ materialTableLoading.value = true;
+ materialTableData.value = [];
+ await getProcessOptions();
+ try {
+ const detailRes = await listMaterialPickingDetail(props.orderRow.id);
+ const detailList = Array.isArray(detailRes?.data)
+ ? detailRes.data
+ : detailRes?.data?.records || [];
+ if (detailList.length > 0) {
+ isDetail.value = true;
+ materialTableData.value = detailList.map(item => createMaterialRow(item));
+ return;
+ } else {
+ isDetail.value = false;
+ const bomRes = await listMaterialPickingBom(props.orderRow.id);
+ const bomList = Array.isArray(bomRes?.data)
+ ? bomRes.data
+ : bomRes?.data?.records || [];
+ materialTableData.value = bomList.map(item => createMaterialRow(item));
+ return;
+ }
+ } finally {
+ materialTableLoading.value = false;
+ }
+ };
+
+ watch(
+ () => dialogVisible.value,
+ visible => {
+ if (visible) {
+ loadMaterialData();
+ }
+ }
+ );
+
+ const handleClose = () => {
+ materialTableData.value = [];
+ currentMaterialSelectRowIndex.value = -1;
+ };
+
+ const handleAddMaterialRow = () => {
+ materialTableData.value.push(createMaterialRow());
+ };
+
+ const handleDeleteMaterialRow = index => {
+ materialTableData.value.splice(index, 1);
+ };
+
+ const handleProcessNameChange = (row, operationName) => {
+ const process = processOptions.value.find(
+ item => item.name === operationName
+ );
+ row.technologyOperationId = process?.technologyOperationId;
+ };
+
+ const handleRequiredQtyChange = (row, val) => {
+ const required = Number(val ?? 0);
+ row.demandedQuantity = required;
+ if (!row.pickQty || Number(row.pickQty) === 0) {
+ row.pickQty = required;
+ }
+ };
+
+ const openMaterialProductSelect = row => {
+ currentMaterialSelectRowIndex.value = materialTableData.value.findIndex(
+ item => item.tempId === row.tempId
+ );
+ materialProductDialogVisible.value = true;
+ };
+
+ const handleMaterialProductConfirm = products => {
+ console.log(products, "products");
+
+ if (!products || products.length === 0) return;
+ const index = currentMaterialSelectRowIndex.value;
+ if (index < 0 || !materialTableData.value[index]) return;
+ const product = products[0];
+ const row = materialTableData.value[index];
+ row.materialModelId =
+ product.materialModelId || product.modelId || product.id;
+ row.materialName =
+ product.materialName || product.productName || product.name || "";
+ row.materialModel = product.materialModel || product.model || "";
+ row.unit = product.unit || product.measureUnit || "";
+ row.batchNoList = product.batchNoList;
+ currentMaterialSelectRowIndex.value = -1;
+ materialProductDialogVisible.value = false;
+ };
+
+ const validateMaterialRows = () => {
+ if (materialTableData.value.length === 0) {
+ return { valid: false, message: "璇峰厛鏂板棰嗘枡鏁版嵁" };
+ }
+ const invalidNewRow = materialTableData.value.find(
+ item => item.bom !== true && (!item.operationName || !item.materialName)
+ );
+ if (invalidNewRow) {
+ return { valid: false, message: "鏂板琛岀殑宸ュ簭鍚嶇О鍜屽師鏂欏悕绉颁负蹇呭~椤�" };
+ }
+ const invalidRow = materialTableData.value.find(
+ item =>
+ !item.operationName ||
+ !item.materialName ||
+ (Number(item.pickQty) > 0 &&
+ (!item.batchNo || item.batchNo.length === 0)) ||
+ (item.batchNo && item.batchNo.length > 0 && Number(item.pickQty) <= 0) ||
+ item.demandedQuantity === null ||
+ item.demandedQuantity === undefined ||
+ item.pickQty === null ||
+ item.pickQty === undefined
+ );
+ if (invalidRow) {
+ if (
+ invalidRow.batchNo &&
+ invalidRow.batchNo.length > 0 &&
+ Number(invalidRow.pickQty) <= 0
+ ) {
+ return { valid: false, message: "閫夋嫨浜嗘壒鍙锋椂锛岄鐢ㄦ暟閲忓繀椤诲ぇ浜庨浂" };
+ }
+ return { valid: false, message: "璇峰畬鍠勫伐搴忋�佸師鏂欍�佹壒鍙峰拰鏁伴噺鍚庡啀淇濆瓨" };
+ }
+ return { valid: true, message: "" };
+ };
+
+ const handleMaterialSave = async () => {
+ if (!props.orderRow?.id) return;
+ const validateResult = validateMaterialRows();
+ if (!validateResult.valid) {
+ ElMessage.warning(validateResult.message);
+ return;
+ }
+ materialSaving.value = true;
+ try {
+ if (isDetail.value) {
+ await updateMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: materialTableData.value.map(item => ({
+ id: item.id,
+ // processId: item.operationName,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.materialModelId,
+ // materialName: item.materialName,
+ // materialModel: item.materialModel,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQty,
+ batchNo: Array.isArray(item.batchNo)
+ ? item.batchNo.join(",")
+ : item.batchNo,
+ })),
+ });
+ } else {
+ await saveMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: materialTableData.value.map(item => ({
+ id: item.id,
+ // processId: item.operationName,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.materialModelId,
+ // materialName: item.materialName,
+ // materialModel: item.materialModel,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQty,
+ batchNo: Array.isArray(item.batchNo)
+ ? item.batchNo.join(",")
+ : item.batchNo,
+ })),
+ });
+ }
+
+ ElMessage({ message: "棰嗘枡鎴愬姛", type: "success" });
+ emit("saved");
+ dialogVisible.value = false;
+ } finally {
+ materialSaving.value = false;
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ .material-toolbar {
+ margin-bottom: 12px;
+ text-align: right;
+ }
+</style>
diff --git a/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue b/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue
new file mode 100644
index 0000000..4f052ed
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue
@@ -0,0 +1,165 @@
+<template>
+ <el-dialog v-model="dialogVisible"
+ title="琛ユ枡"
+ width="1200px"
+ @close="handleClose">
+ <el-table v-loading="loading"
+ :data="tableData"
+ border
+ row-key="id">
+ <el-table-column label="宸ュ簭鍚嶇О"
+ prop="operationName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍚嶇О"
+ prop="productName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍨嬪彿"
+ prop="model"
+ min-width="140" />
+ <el-table-column label="璁¢噺鍗曚綅"
+ prop="unit"
+ width="100" />
+ <el-table-column label="闇�姹傛暟閲�"
+ prop="demandedQuantity"
+ width="100" />
+ <el-table-column label="棰嗙敤鏁伴噺"
+ prop="pickQuantity"
+ width="100" />
+ <el-table-column label="宸茶ˉ鏁伴噺"
+ prop="feedingQty"
+ width="100" />
+ <el-table-column label="琛ユ枡鏁伴噺"
+ min-width="150">
+ <template #default="{ row }">
+ <el-input-number v-model="row.newSupplementQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ placeholder="杈撳叆琛ユ枡鏁伴噺"
+ style="width: 100%;" />
+ </template>
+ </el-table-column>
+ <el-table-column label="琛ユ枡鍘熷洜"
+ min-width="200">
+ <template #default="{ row }">
+ <el-input v-model="row.newSupplementReason"
+ placeholder="杈撳叆琛ユ枡鍘熷洜"
+ maxlength="200"
+ show-word-limit />
+ </template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="submitting"
+ @click="handleSubmit">纭� 瀹�</el-button>
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ </span>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+ import { computed, ref, watch } from "vue";
+ import { ElMessage } from "element-plus";
+ import {
+ listMaterialPickingDetail,
+ updateMaterialPickingLedger,
+ } from "@/api/productionManagement/productionOrder.js";
+
+ const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ orderRow: { type: Object, default: null },
+ });
+ const emit = defineEmits(["update:modelValue", "saved"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const loading = ref(false);
+ const submitting = ref(false);
+ const tableData = ref([]);
+
+ const loadData = async () => {
+ if (!props.orderRow?.id) return;
+ loading.value = true;
+ try {
+ const res = await listMaterialPickingDetail(props.orderRow.id);
+ tableData.value = (res.data || []).map(item => ({
+ ...item,
+ newSupplementQty: 0,
+ newSupplementReason: "",
+ }));
+ } catch (e) {
+ console.error("鑾峰彇鐗╂枡鏄庣粏澶辫触锛�", e);
+ ElMessage.error("鑾峰彇鐗╂枡鏄庣粏澶辫触");
+ } finally {
+ loading.value = false;
+ }
+ };
+
+ watch(
+ () => dialogVisible.value,
+ visible => {
+ if (visible) {
+ loadData();
+ }
+ }
+ );
+
+ const handleClose = () => {
+ tableData.value = [];
+ };
+
+ const handleSubmit = async () => {
+ const supplementList = tableData.value.filter(
+ item => item.newSupplementQty > 0
+ );
+ if (supplementList.length === 0) {
+ ElMessage.warning("璇疯嚦灏戣緭鍏ヤ竴鏉¤ˉ鏂欐暟閲�");
+ return;
+ }
+
+ const invalidRow = supplementList.find(item => !item.newSupplementReason);
+ if (invalidRow) {
+ ElMessage.warning("璇疯緭鍏ヨˉ鏂欏師鍥�");
+ return;
+ }
+
+ submitting.value = true;
+ try {
+ await updateMaterialPickingLedger({
+ productionOrderId: props.orderRow.id,
+ productionOrderPickDto: tableData.value.map(item => ({
+ id: item.id,
+ technologyOperationId: item.technologyOperationId,
+ operationName: item.operationName,
+ bom: item.bom === true,
+ productModelId: item.productModelId,
+ demandedQuantity: item.demandedQuantity,
+ unit: item.unit,
+ pickQuantity: item.pickQuantity,
+ batchNo: item.batchNo,
+ feedingQuantity: item.newSupplementQty || 0,
+ feedingReason: item.newSupplementReason || "",
+ pickType: 2,
+ })),
+ });
+ ElMessage.success("琛ユ枡鎴愬姛");
+ dialogVisible.value = false;
+ emit("saved");
+ } catch (e) {
+ console.error("琛ユ枡澶辫触锛�", e);
+ ElMessage.error("琛ユ枡澶辫触");
+ } finally {
+ submitting.value = false;
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+</style>
diff --git a/src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue b/src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue
new file mode 100644
index 0000000..d1aa267
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue
@@ -0,0 +1,225 @@
+<template>
+ <div class="print-container"
+ id="print-requisition">
+ <div class="print-content">
+ <div class="bill-title">鐢熶骇棰嗘枡鍗�</div>
+ <div class="info-grid">
+ <div class="info-row">
+ <div class="info-item">
+ <span class="label">鍒涘缓鏃ユ湡锛�</span>
+ <span class="value">{{ formatDate(orderRow?.createTime) }}</span>
+ </div>
+ <div class="info-item">
+ <span class="label">棰嗘枡鍗曞彿锛�</span>
+ <span class="value">{{ orderRow?.npsNo }}</span>
+ </div>
+ <div class="info-item">
+ <span class="label">鐢宠浜猴細</span>
+ <span class="value">{{ userName }}</span>
+ </div>
+ </div>
+ <div class="info-row">
+ <div class="info-item"
+ style="width: 50%;">
+ <span class="label">浜у搧鍚嶇О/鍨嬪彿锛�</span>
+ <span class="value">{{ orderRow?.productName }} / {{ orderRow?.model }}</span>
+ </div>
+ <div class="info-item"
+ style="width: 25%;">
+ <span class="label">鐢熶骇鏁伴噺锛�</span>
+ <span class="value">{{ orderRow?.quantity }}</span>
+ </div>
+ <div class="info-item"
+ style="width: 25%;">
+ <span class="label">闇�姹傛棩鏈燂細</span>
+ <span class="value">{{ formatDate(orderRow?.planCompleteTime) }}</span>
+ </div>
+ </div>
+ </div>
+ <table class="material-table">
+ <thead>
+ <tr>
+ <th width="50">搴忓彿</th>
+ <th>宸ュ簭鍚嶇О</th>
+ <th>瑙勬牸/鍚嶇О</th>
+ <th>鎵瑰彿</th>
+ <th width="80">闇�姹傛暟閲�</th>
+ <th width="80">棰嗘枡鏁伴噺</th>
+ <th width="60">鍗曚綅</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(item, index) in materialList"
+ :key="index">
+ <td align="center">{{ index + 1 }}</td>
+ <td>{{ item.operationName || '-' }}</td>
+ <td>{{ item.materialName || item.productName }} {{ item.materialModel || item.model }}</td>
+ <td>{{ item.batchNo || '-' }}</td>
+ <td align="right">{{ item.demandedQuantity }}</td>
+ <td align="right">{{ item.pickQuantity || item.pickQty || 0 }}</td>
+ <td align="center">{{ item.unit }}</td>
+ </tr>
+ </tbody>
+ </table>
+ <div class="print-footer">
+ <div class="footer-item">棰嗘枡锛歘_______________</div>
+ <div class="footer-item">鍙戞枡锛歘_______________</div>
+ <div class="footer-item">瀹℃牳锛歘_______________</div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import dayjs from "dayjs";
+ import useUserStore from "@/store/modules/user";
+ import { computed } from "vue";
+
+ const props = defineProps({
+ orderRow: {
+ type: Object,
+ default: () => ({}),
+ },
+ materialList: {
+ type: Array,
+ default: () => [],
+ },
+ });
+
+ const userStore = useUserStore();
+ const userName = computed(() => userStore.nickName || userStore.name || "-");
+
+ const formatDate = date => {
+ return date ? dayjs(date).format("YYYY骞碝M鏈圖D鏃�") : "-";
+ };
+</script>
+
+<style lang="scss">
+ /* 灞忓箷鏄剧ず鏍峰紡 */
+ .print-requisition-wrapper {
+ display: none;
+ }
+
+ /* 鎵撳嵃涓撶敤鏍峰紡 */
+ @media print {
+ @page {
+ size: landscape;
+ margin: 10mm;
+ }
+
+ /* 鍩虹鎵撳嵃璁剧疆 */
+ html,
+ body {
+ visibility: hidden;
+ height: auto !important;
+ overflow: visible !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ width: 100%;
+ }
+
+ /* 鏄惧紡鏄剧ず鎵撳嵃瀹瑰櫒鍙婂叾鎵�鏈夊瓙鍏冪礌 */
+ .print-requisition-wrapper,
+ .print-requisition-wrapper * {
+ visibility: visible !important;
+ }
+
+ /* 纭繚鎵撳嵃瀹瑰櫒鍗犳嵁鏁翠釜椤甸潰骞剁Щ闄ょ粷瀵瑰畾浣嶅共鎵� */
+ .print-requisition-wrapper {
+ display: block !important;
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: auto;
+ background: white;
+ margin: 0 !important;
+ padding: 0 !important;
+ z-index: 99999;
+ }
+
+ .print-container {
+ width: 100% !important;
+ padding: 0 10mm; /* 浣跨敤瀵圭О鐨勫乏鍙冲唴杈硅窛纭繚灞呬腑 */
+ box-sizing: border-box;
+ height: auto;
+ overflow: visible;
+ color: #000;
+ font-family: "SimSun", "STSong", serif;
+ page-break-inside: avoid;
+ display: block;
+ .print-content {
+ width: 100%;
+ text-align: center;
+ }
+ .bill-title {
+ font-size: 20px;
+ font-weight: bold;
+ text-align: center;
+ margin-bottom: 20px;
+ letter-spacing: 5px;
+ text-decoration: underline;
+ }
+
+ .info-grid {
+ margin-bottom: 10px;
+ font-size: 14px;
+
+ .info-row {
+ display: flex;
+ flex-wrap: wrap;
+ margin-bottom: 8px;
+
+ .info-item {
+ width: 33.33%;
+ display: flex;
+ align-items: flex-end;
+
+ .label {
+ font-weight: bold;
+ white-space: nowrap;
+ }
+ .value {
+ border-bottom: 1px solid #000;
+ padding: 0 5px;
+ flex: 1;
+ min-height: 20px;
+ }
+ }
+ }
+ }
+
+ .material-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 2px solid #000;
+ font-size: 13px;
+
+ th,
+ td {
+ border: 1px solid #000 !important;
+ padding: 6px 4px;
+ height: 25px;
+ -webkit-print-color-adjust: exact;
+ print-color-adjust: exact;
+ }
+
+ th {
+ background-color: #f2f2f2 !important;
+ font-weight: bold;
+ }
+ }
+
+ .print-footer {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 30px;
+ padding: 0 10px;
+
+ .footer-item {
+ font-size: 14px;
+ }
+ }
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/productionOrder/index.vue b/src/views/productionManagement/productionOrder/index.vue
new file mode 100644
index 0000000..9c7682b
--- /dev/null
+++ b/src/views/productionManagement/productionOrder/index.vue
@@ -0,0 +1,1025 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm"
+ :inline="true">
+ <el-form-item label="鐢熶骇璁㈠崟鍙�:">
+ <el-input v-model="searchForm.npsNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 160px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="浜у搧鍚嶇О:">
+ <el-input v-model="searchForm.productName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 160px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="瑙勬牸:">
+ <el-input v-model="searchForm.model"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 160px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐘舵��:">
+ <el-select v-model="searchForm.status"
+ placeholder="璇烽�夋嫨"
+ style="width: 160px;"
+ @change="handleQuery">
+ <el-option label="寰呭紑濮�"
+ value="1" />
+ <el-option label="杩涜涓�"
+ value="2" />
+ <el-option label="宸插畬鎴�"
+ value="3" />
+ <el-option label="宸插彇娑�"
+ value="4" />
+ <el-option label="宸茬粨鏉�"
+ value="5" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
+ <el-button type="info"
+ @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <div class="action-buttons">
+ <!-- <el-button type="primary"
+ @click="isShowNewModal = true">鏂板</el-button> -->
+ <el-button type="danger"
+ @click="handleDelete">閫�鍥�</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ :row-class-name="tableRowClassName"
+ :isSelection="true"
+ :selectable="row => !row.endOrder"
+ @selection-change="handleSelectionChange"
+ @pagination="pagination">
+ <template #completionStatus="{ row }">
+ <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
+ :color="progressColor(toProgressPercentage(row?.completionStatus))"
+ :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
+ </template>
+ <template #processRouteStatus="{ row }">
+ <div v-if="row.processRouteStatus && row.processRouteStatus.length"
+ class="process-progress-container">
+ <div v-for="(item, index) in row.processRouteStatus"
+ :key="index"
+ class="process-step">
+ <div class="step-content">
+ <div class="step-circle"
+ :class="{ 'is-completed': item.percentage >= 100 }">
+ <span class="step-percentage"
+ :style="{ color: item.percentage >= 70 ? item.percentage >= 100 ? '#67c23a' : '#f56c6c' : '#000' }">{{ item.percentage }}%</span>
+ </div>
+ <div class="step-name">{{ item.name }}</div>
+ </div>
+ <div v-if="index < row.processRouteStatus.length - 1"
+ class="step-line"></div>
+ </div>
+ </div>
+ <span v-else>-</span>
+ </template>
+ </PIMTable>
+ </div>
+ <el-dialog v-model="bindRouteDialogVisible"
+ title="缁戝畾宸ヨ壓璺嚎"
+ width="500px">
+ <el-form label-width="90px">
+ <el-form-item label="宸ヨ壓璺嚎">
+ <el-select v-model="bindForm.routeId"
+ placeholder="璇烽�夋嫨宸ヨ壓璺嚎"
+ style="width: 100%;"
+ :loading="bindRouteLoading">
+ <el-option v-for="item in routeOptions"
+ :key="item.id"
+ :label="`${item.processRouteCode || ''}`"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="bindRouteSaving"
+ @click="handleBindRouteConfirm">纭� 璁�</el-button>
+ <el-button @click="bindRouteDialogVisible = false">鍙� 娑�</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鏉ユ簮鏁版嵁寮圭獥 -->
+ <el-dialog v-model="sourceDataDialogVisible"
+ title="鏉ユ簮鏁版嵁"
+ width="1200px">
+ <div v-if="sourceRowData"
+ class="applyno-summary1">
+ <div class="summary-item">
+ <span class="summary-label">浜у搧鍚嶇О锛�</span>
+ <span class="summary-value">
+ <el-tag type="primary">{{ sourceRowData.productName || '-' }}</el-tag>
+ </span>
+ </div>
+ <div class="summary-item">
+ <span class="summary-label">瑙勬牸锛�</span>
+ <span class="summary-value">{{ sourceRowData.model || '-' }}</span>
+ </div>
+ <div class="summary-item">
+ <span class="summary-label">璁㈠崟闇�姹傛暟閲忥細</span>
+ <span class="summary-value">{{ sourceRowData.quantity || 0 }}</span>
+ </div>
+ </div>
+ <div class="source-table-container">
+ <div class="source-data-cards-container">
+ <div v-for="(item, index) in sourceTableData"
+ :key="index"
+ class="source-data-card">
+ <div class="card-body">
+ <div class="info-grid">
+ <div class="info-item">
+ <div class="info-label">璁″垝鍙�</div>
+ <div class="info-value">{{ item.mpsNo || '-' }}</div>
+ </div>
+ <div class="info-item">
+ <div class="info-label">鏁版嵁鏉ユ簮</div>
+ <div class="info-value">
+ <el-tag :type="item.source === '閿�鍞�' ? 'primary' : 'warning'">
+ {{ item.source || '鏈煡' }}
+ </el-tag>
+ </div>
+ </div>
+ <div class="info-item">
+ <div class="info-label">鍚堝悓鍙�</div>
+ <div class="info-value">{{ item.salesContractNo || '-' }}</div>
+ </div>
+ <div class="info-item">
+ <div class="info-label">瀹㈡埛鍚嶇О</div>
+ <div class="info-value">{{ item.customerName || '-' }}</div>
+ </div>
+ <div class="info-item">
+ <div class="info-label">椤圭洰鍚嶇О</div>
+ <div class="info-value">{{ item.projectName || '-' }}</div>
+ </div>
+ <div class="info-item">
+ <div class="info-label">璁″垝闇�姹傛暟閲�</div>
+ <div class="info-value">{{ item.qtyRequired || 0 }} {{ item.unit || '' }}</div>
+ </div>
+ <div class="info-item">
+ <div class="info-label">鍗曚綅</div>
+ <div class="info-value">{{ item.unit || '-' }}</div>
+ </div>
+ <div class="info-item">
+ <div class="info-label">闇�姹傛棩鏈�</div>
+ <div class="info-value">{{ item.requiredDate ? dayjs(item.requiredDate).format('YYYY-MM-DD') : '-' }}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-dialog>
+ <MaterialLedgerDialog v-model="materialDialogVisible"
+ :order-row="currentMaterialOrder"
+ @saved="getList" />
+ <MaterialDetailDialog v-model="materialDetailDialogVisible"
+ :order-row="currentMaterialDetailOrder"
+ @confirmed="getList" />
+ <MaterialSupplementDialog v-model="materialSupplementDialogVisible"
+ :order-row="currentMaterialSupplementOrder"
+ @saved="getList" />
+ <new-product-order v-if="isShowNewModal"
+ v-model:visible="isShowNewModal"
+ @completed="handleQuery" />
+ <!-- 鎵撳嵃棰嗘枡鍗曠粍浠� -->
+ <div class="print-requisition-wrapper">
+ <PrintMaterialRequisition ref="printRef"
+ :order-row="printOrderRow"
+ :material-list="printMaterialList" />
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import {
+ computed,
+ defineAsyncComponent,
+ getCurrentInstance,
+ onMounted,
+ reactive,
+ ref,
+ toRefs,
+ } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import dayjs from "dayjs";
+ import { useRouter } from "vue-router";
+ import {
+ productOrderListPage,
+ listProcessRoute,
+ bindingRoute,
+ listProcessBom,
+ delProductOrder,
+ getProductOrderSource,
+ updateProductOrder,
+ } from "@/api/productionManagement/productionOrder.js";
+ import { productWorkOrderPage } from "@/api/productionManagement/workOrder.js";
+ import { listMain as getOrderProcessRouteMain } from "@/api/productionManagement/productProcessRoute.js";
+ import MaterialLedgerDialog from "@/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue";
+ import MaterialDetailDialog from "@/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue";
+ import MaterialSupplementDialog from "@/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue";
+ import PrintMaterialRequisition from "@/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import { listPage } from "@/api/productionManagement/processRoute.js";
+ import {
+ listMaterialPickingDetail,
+ listMaterialPickingBom,
+ } from "@/api/productionManagement/productionOrder.js";
+ const NewProductOrder = defineAsyncComponent(() =>
+ import("@/views/productionManagement/productionOrder/New.vue")
+ );
+
+ const { proxy } = getCurrentInstance();
+
+ const router = useRouter();
+ const isShowNewModal = ref(false);
+ const sourceDataDialogVisible = ref(false);
+ const sourceTableData = ref([]);
+ const sourceRowData = ref(null);
+ const sourcePage = reactive({
+ total: 0,
+ });
+
+ const processColumnWidth = computed(() => {
+ if (!tableData.value || tableData.value.length === 0) return "200px";
+ const maxProcesses = Math.max(
+ ...tableData.value.map(row => row.processRouteStatus?.length || 0)
+ );
+ if (maxProcesses === 0) return "100px";
+ // 姣忎釜宸ュ簭鍦嗗湀 36px + 绾挎潯 30px = 66px锛岄澶栧姞 60px 杈硅窛鍜屾枃瀛楃┖闂�
+ return `${maxProcesses * 66 + 60}px`;
+ });
+
+ const tableColumn = computed(() => [
+ {
+ label: "鐢熶骇璁㈠崟鍙�",
+ prop: "npsNo",
+ width: "150px",
+ },
+ // 1.寰呭紑濮嬨��2.杩涜涓��3.宸插畬鎴愩��4.宸插彇娑堛��5.宸茬粨鏉�
+ {
+ label: "鐘舵��",
+ prop: "status",
+ width: "150px",
+ dataType: "tag",
+ formatData: val =>
+ val === 1
+ ? "寰呭紑濮�"
+ : val === 2
+ ? "杩涜涓�"
+ : val === 3
+ ? "宸插畬鎴�"
+ : val === 5
+ ? "宸茬粨鏉�"
+ : "宸插彇娑�",
+ formatType: val =>
+ val === 1
+ ? "primary"
+ : val === 2
+ ? "warning"
+ : val === 3
+ ? "success"
+ : val === 5
+ ? "danger"
+ : "info",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: "120px",
+ },
+ {
+ label: "瑙勬牸",
+ prop: "model",
+ width: "120px",
+ },
+ {
+ label: "宸ヨ壓璺嚎缂栧彿",
+ prop: "processRouteCode",
+ width: "200px",
+ },
+ {
+ label: "闇�姹傛暟閲�",
+ prop: "quantity",
+ },
+ {
+ label: "瀹屾垚鏁伴噺",
+ prop: "completeQuantity",
+ },
+ {
+ label: "宸ュ簭鐢熶骇杩涘害",
+ prop: "processRouteStatus",
+ dataType: "slot",
+ slot: "processRouteStatus",
+ width: processColumnWidth.value,
+ },
+ {
+ dataType: "slot",
+ label: "瀹屾垚杩涘害",
+ prop: "completionStatus",
+ slot: "completionStatus",
+ width: 180,
+ },
+ {
+ label: "寮�濮嬫棩鏈�",
+ prop: "startTime",
+ formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
+ width: 120,
+ },
+ {
+ label: "缁撴潫鏃ユ湡",
+ prop: "endTime",
+ formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
+ width: 120,
+ },
+ {
+ label: "璁″垝瀹屾垚鏃堕棿",
+ prop: "planCompleteTime",
+ formatData: val => (val ? dayjs(val).format("YYYY-MM-DD") : ""),
+ width: 120,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ operation: [
+ {
+ name: "宸ヨ壓璺嚎",
+ type: "text",
+ showHide: row => row.processRouteCode,
+ clickFun: row => {
+ showRouteItemModal(row);
+ },
+ },
+ {
+ name: "缁戝畾宸ヨ壓璺嚎",
+ type: "text",
+ showHide: row => !row.processRouteCode && !row.endOrder,
+ clickFun: row => {
+ openBindRouteDialog(row, "add");
+ },
+ },
+ {
+ name: "鏇存崲宸ヨ壓璺嚎",
+ type: "text",
+ showHide: row => row.processRouteCode && !row.endOrder,
+ clickFun: row => {
+ openBindRouteDialog(row, "change");
+ },
+ },
+ {
+ name: "鏉ユ簮",
+ type: "text",
+ clickFun: row => {
+ showSourceData(row);
+ },
+ },
+ {
+ name: "棰嗘枡",
+ type: "text",
+ color: "#5EC7AB",
+ showHide: row => !row.endOrder && !row.returned,
+ clickFun: row => {
+ openMaterialDialog(row);
+ },
+ },
+ {
+ name: "琛ユ枡",
+ type: "text",
+ color: "#5EC7AB",
+ showHide: row => !row.endOrder && !row.returned,
+ clickFun: row => {
+ openMaterialSupplementDialog(row);
+ },
+ },
+ {
+ name: "棰嗘枡璇︽儏",
+ type: "text",
+ color: "#5EC7AB",
+ clickFun: row => {
+ openMaterialDetailDialog(row);
+ },
+ },
+ {
+ name: "鎵撳嵃棰嗘枡鍗�",
+ type: "text",
+ color: "#5EC7AB",
+ showHide: row => !row.endOrder,
+ clickFun: row => {
+ handlePrint(row);
+ },
+ },
+ {
+ name: "鐢熶骇杩芥函",
+ type: "text",
+ color: "#409eff",
+ clickFun: row => {
+ router.push({
+ path: "/productionManagement/productionTraceability",
+ query: {
+ npsNo: row.npsNo,
+ productName: row.productName,
+ model: row.model,
+ },
+ });
+ },
+ },
+ {
+ name: "缁撴潫璁㈠崟",
+ type: "text",
+ color: "red",
+ showHide: row => !row.endOrder,
+ clickFun: row => {
+ handleEndOrder(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const selectedRows = ref([]);
+
+ const data = reactive({
+ searchForm: {
+ npsNo: "",
+ customerName: "",
+ salesContractNo: "",
+ projectName: "",
+ productName: "",
+ model: "",
+ status: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
+
+ // 30/50/80/100 鍒嗘棰滆壊锛氱孩/姗�/钃�/缁�
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
+
+ // 娣诲姞琛ㄨ绫诲悕鏂规硶
+ const tableRowClassName = ({ row }) => {
+ if (!row.planCompleteTime) return "";
+ if (row.isFh) return "";
+
+ const diff = row.deliveryDaysDiff;
+ if (diff === 15) {
+ return "yellow";
+ } else if (diff === 10) {
+ return "pink";
+ } else if (diff === 2) {
+ return "purple";
+ } else if (diff < 2) {
+ return "red";
+ }
+ };
+
+ // 缁戝畾宸ヨ壓璺嚎寮规
+ const bindRouteDialogVisible = ref(false);
+ const bindRouteLoading = ref(false);
+ const bindRouteSaving = ref(false);
+ const routeOptions = ref([]);
+ const bindForm = reactive({
+ orderId: null,
+ routeId: null,
+ });
+ const materialDialogVisible = ref(false);
+ const currentMaterialOrder = ref(null);
+ const materialDetailDialogVisible = ref(false);
+ const currentMaterialDetailOrder = ref(null);
+ const materialSupplementDialogVisible = ref(false);
+ const currentMaterialSupplementOrder = ref(null);
+
+ // 鎵撳嵃鐩稿叧
+ const printOrderRow = ref(null);
+ const printMaterialList = ref([]);
+ const handlePrint = async row => {
+ printOrderRow.value = row;
+ proxy.$modal.loading("姝e湪鑾峰彇棰嗘枡鏁版嵁...");
+ try {
+ printMaterialList.value = [];
+ const detailRes = await listMaterialPickingDetail(row.id);
+ const detailList = Array.isArray(detailRes?.data)
+ ? detailRes.data
+ : detailRes?.data?.records || [];
+
+ if (detailList.length > 0) {
+ printMaterialList.value = detailList;
+ }
+
+ if (printMaterialList.value.length === 0) {
+ proxy.$modal.msgWarning("鏆傛棤棰嗘枡鏁版嵁");
+ return;
+ }
+
+ // 绛夊緟 DOM 鏇存柊鍚庢墽琛屾墦鍗�
+ proxy.$nextTick(() => {
+ setTimeout(() => {
+ window.print();
+ }, 800);
+ });
+ } catch (e) {
+ console.error("鑾峰彇棰嗘枡鏁版嵁澶辫触锛�", e);
+ proxy.$modal.msgError("鑾峰彇棰嗘枡鏁版嵁澶辫触");
+ } finally {
+ proxy.$modal.closeLoading();
+ }
+ };
+
+ const openBindRouteDialog = async (row, type) => {
+ bindForm.orderId = row.id;
+ bindForm.routeId = type === "add" ? null : row.technologyRoutingId;
+ bindRouteDialogVisible.value = true;
+ routeOptions.value = [];
+ if (!row.productModelId) {
+ proxy.$modal.msgWarning("褰撳墠璁㈠崟缂哄皯浜у搧鍨嬪彿锛屾棤娉曟煡璇㈠伐鑹鸿矾绾�");
+ bindRouteDialogVisible.value = false;
+ return;
+ }
+ bindRouteLoading.value = true;
+ try {
+ const res = await listPage({ productModelId: row.productModelId });
+ routeOptions.value = res.data.records || [];
+ } catch (e) {
+ console.error("鑾峰彇宸ヨ壓璺嚎鍒楄〃澶辫触锛�", e);
+ proxy.$modal.msgError("鑾峰彇宸ヨ壓璺嚎鍒楄〃澶辫触");
+ } finally {
+ bindRouteLoading.value = false;
+ }
+ };
+
+ const handleBindRouteConfirm = async () => {
+ if (!bindForm.routeId) {
+ proxy.$modal.msgWarning("璇烽�夋嫨宸ヨ壓璺嚎");
+ return;
+ }
+ bindRouteSaving.value = true;
+ try {
+ await bindingRoute({
+ id: bindForm.orderId,
+ technologyRoutingId: bindForm.routeId,
+ });
+ proxy.$modal.msgSuccess("缁戝畾鎴愬姛");
+ bindRouteDialogVisible.value = false;
+ getList();
+ } catch (e) {
+ console.error("缁戝畾宸ヨ壓璺嚎澶辫触锛�", e);
+ proxy.$modal.msgError("缁戝畾宸ヨ壓璺嚎澶辫触");
+ } finally {
+ bindRouteSaving.value = false;
+ }
+ };
+
+ const openMaterialDialog = row => {
+ currentMaterialOrder.value = row;
+ materialDialogVisible.value = true;
+ };
+
+ const openMaterialDetailDialog = async row => {
+ currentMaterialDetailOrder.value = row;
+ materialDetailDialogVisible.value = true;
+ };
+
+ const openMaterialSupplementDialog = row => {
+ currentMaterialSupplementOrder.value = row;
+ materialSupplementDialogVisible.value = true;
+ };
+
+ const handleReset = () => {
+ searchForm.value = {
+ ...searchForm.value,
+ npsNo: "",
+ customerName: "",
+ salesContractNo: "",
+ projectName: "",
+ productName: "",
+ model: "",
+ status: "",
+ };
+ handleQuery();
+ };
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const changeDaterange = value => {
+ if (value) {
+ searchForm.value.entryDateStart = value[0];
+ searchForm.value.entryDateEnd = value[1];
+ } else {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ }
+ handleQuery();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ // 鏋勯�犱竴涓柊鐨勫璞★紝涓嶅寘鍚玡ntryDate瀛楁
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined;
+ productOrderListPage(params)
+ .then(async res => {
+ const records = res.data.records || [];
+ // 涓烘瘡涓鍗曟煡璇㈠搴旂殑宸ュ簭杩涘害鏁版嵁
+ const processPromises = records.map(async item => {
+ if (item.npsNo) {
+ try {
+ const workOrderRes = await productWorkOrderPage({
+ npsNo: item.npsNo,
+ size: 100,
+ });
+ const workOrders = workOrderRes.data.records || [];
+ // 鎸夌収宸ュ簭椤哄簭鎺掑簭锛堝鏋滄湁椤哄簭瀛楁锛屽亣璁句负 orderNum 鎴栨寜杩斿洖椤哄簭锛�
+ // 杞崲涓� processRouteStatus 鏍煎紡
+ const processRouteStatus = workOrders.map(wo => ({
+ name: wo.operationName || "鏈煡宸ュ簭",
+ percentage: wo.completionStatus > 100 ? 100 : wo.completionStatus,
+ }));
+ return { ...item, processRouteStatus };
+ } catch (error) {
+ console.error(`鑾峰彇宸ュ崟 ${item.npsNo} 杩涘害澶辫触:`, error);
+ return { ...item, processRouteStatus: [] };
+ }
+ }
+ return { ...item, processRouteStatus: [] };
+ });
+
+ tableData.value = await Promise.all(processPromises);
+ page.total = res.data.total;
+ tableLoading.value = false;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const showRouteItemModal = async row => {
+ const orderId = row.id;
+ try {
+ const res = await getOrderProcessRouteMain(orderId);
+ const data = res.data || {};
+ if (!data || !data.id) {
+ proxy.$modal.msgWarning("鏈壘鍒板叧鑱旂殑宸ヨ壓璺嚎");
+ return;
+ }
+ router.push({
+ path: "/productionManagement/processRouteItem",
+ query: {
+ id: data.id,
+ bomId: data.orderBomId,
+ processRouteCode: data.processRouteCode || "",
+ productName: row.productName || "",
+ model: row.model || "",
+ bomNo: row.bomNo || "",
+ description: data.description || "",
+ quantity: row.quantity || 0,
+ technologyRoutingId: data.technologyRoutingId,
+ orderId,
+ type: "order",
+ editable: !row.endOrder,
+ },
+ });
+ } catch (e) {
+ console.error("鑾峰彇宸ヨ壓璺嚎涓讳俊鎭け璐ワ細", e);
+ proxy.$modal.msgError("鑾峰彇宸ヨ壓璺嚎淇℃伅澶辫触");
+ }
+ };
+
+ const showProductStructure = row => {
+ router.push({
+ path: "/productionManagement/productStructureDetail",
+ query: {
+ id: row.id,
+ bomNo: row.bomNo || "",
+ productName: row.productName || "",
+ productModelName: row.model || "",
+ orderId: row.id,
+ type: "order",
+ },
+ });
+ };
+
+ // 鏌ョ湅鏉ユ簮鐢熶骇璁″垝鏁版嵁
+ const showSourceData = row => {
+ // 瀛樺偍鐐瑰嚮鏉ユ簮鎸夐挳鏃朵紶閫掔殑row鍙傛暟
+ sourceRowData.value = row;
+ // 璋冪敤API鑾峰彇鏉ユ簮鏁版嵁
+ getProductOrderSource(row.id)
+ .then(res => {
+ if (res.code === 200) {
+ // 鐩存帴瀛樺偍杩斿洖鐨勬墎骞冲寲鏁版嵁
+ sourceTableData.value = res.data || [];
+ sourcePage.total = sourceTableData.value.length;
+ // 鎵撳紑寮圭獥
+ sourceDataDialogVisible.value = true;
+ } else {
+ proxy.$modal.msgError(res.msg || "鑾峰彇鏉ユ簮鏁版嵁澶辫触");
+ }
+ })
+ .catch(err => {
+ proxy.$modal.msgError("鑾峰彇鏉ユ簮鏁版嵁澶辫触");
+ console.error(err);
+ });
+ };
+
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("鏄惁閫�鍥炶鐢熶骇璁㈠崟锛�", "閫�鍥�", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ console.log(ids, "ids");
+ delProductOrder(ids).then(res => {
+ proxy.$modal.msgSuccess("閫�鍥炴垚鍔�");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(
+ "/productionOrder/export",
+ { ...searchForm.value },
+ "鐢熶骇璁㈠崟鏁版嵁.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 缁撴潫璁㈠崟
+ const handleEndOrder = row => {
+ ElMessageBox.confirm(`鏄惁纭缁撴潫璁㈠崟锛�${row.npsNo}锛焋, "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ const params = {
+ id: row.id,
+ endOrder: true,
+ };
+ updateProductOrder(params).then(() => {
+ proxy.$modal.msgSuccess("缁撴潫璁㈠崟鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {});
+ };
+
+ const handleConfirmRoute = () => {};
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .search_form {
+ align-items: start;
+ }
+
+ .action-buttons {
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 8px;
+ }
+
+ :deep(.yellow) {
+ background-color: #faf0de;
+ }
+
+ :deep(.pink) {
+ background-color: #fae1de;
+ }
+
+ :deep(.red) {
+ background-color: #f80202;
+ }
+
+ :deep(.purple) {
+ background-color: #f4defa;
+ }
+ .table_list {
+ margin-top: unset;
+ }
+
+ .process-progress-container {
+ display: inline-flex;
+ align-items: center;
+ padding: 10px 0;
+ white-space: nowrap;
+
+ .process-step {
+ display: flex;
+ align-items: center;
+ position: relative;
+
+ .step-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ z-index: 1;
+
+ .step-circle {
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ border: 2px solid #409eff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #fff;
+ margin-bottom: 4px;
+
+ .step-percentage {
+ font-size: 11px;
+ font-weight: bold;
+ }
+
+ &.is-completed {
+ border-color: #67c23a;
+ .step-percentage {
+ color: #67c23a;
+ }
+ }
+ }
+
+ .step-name {
+ font-size: 12px;
+ color: #606266;
+ white-space: nowrap;
+ }
+ }
+
+ .step-line {
+ width: 30px;
+ height: 1px;
+ background-color: #dcdfe6;
+ margin: 0 -2px;
+ margin-top: -20px; // 鍚戜笂鍋忕Щ浠ュ榻愬渾蹇�
+ }
+ }
+ }
+</style>
+<style lang="scss">
+ .status-cell {
+ font-weight: 600;
+ color: #409eff;
+ font-family: "Courier New", monospace;
+ text-shadow: 0 1px 2px rgba(64, 158, 255, 0.2);
+ }
+
+ .source-table-container {
+ margin-top: 20px;
+ }
+
+ .source-data-cards-container {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ max-height: 500px;
+ overflow-y: auto;
+ padding: 10px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ padding-bottom: 20px;
+
+ .source-data-card {
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+
+ .card-body {
+ padding: 20px;
+
+ .info-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+
+ .info-item {
+ display: flex;
+ flex-direction: column;
+
+ .info-label {
+ font-size: 12px;
+ color: #909399;
+ margin-bottom: 4px;
+ font-weight: 500;
+ }
+
+ .info-value {
+ font-size: 14px;
+ color: #303133;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ .applyno-summary1 {
+ padding: 16px 20px;
+ background: #f5f7fa;
+ border-bottom: 1px solid #e4e7ed;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+
+ .summary-item {
+ display: flex;
+ align-items: center;
+ margin-right: 20px;
+
+ .summary-label {
+ font-size: 13px;
+ color: #909399;
+ margin-right: 8px;
+ font-weight: 500;
+ }
+
+ .summary-value {
+ font-size: 14px;
+ color: #303133;
+ font-weight: 500;
+ }
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/productionProcess/Edit.vue b/src/views/productionManagement/productionProcess/Edit.vue
new file mode 100644
index 0000000..28077b6
--- /dev/null
+++ b/src/views/productionManagement/productionProcess/Edit.vue
@@ -0,0 +1,168 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="缂栬緫宸ュ簭"
+ width="400"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="宸ュ簭鍚嶇О锛�"
+ prop="name"
+ :rules="[
+ {
+ required: true,
+ message: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ {
+ max: 100,
+ message: '鏈�澶�100涓瓧绗�',
+ }
+ ]">
+ <el-input v-model="formState.name" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭缂栧彿" prop="no">
+ <el-input v-model="formState.no" />
+ </el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
+ <el-input v-model="formState.salaryQuota" type="number" :step="0.001" />
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ" prop="isQuality">
+ <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鍏ュ簱" prop="inbound">
+ <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鎶ュ伐" prop="reportWork">
+ <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formState.remark" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance, watch } from "vue";
+import {update} from "@/api/productionManagement/productionProcess.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+
+ record: {
+ type: Object,
+ required: true,
+ }
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ id: props.record.id,
+ name: props.record.name,
+ type: props.record.type,
+ no: props.record.no,
+ remark: props.record.remark,
+ salaryQuota: props.record.salaryQuota,
+ isQuality: props.record.isQuality,
+ inbound: props.record.inbound,
+ reportWork: props.record.reportWork,
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+// 鐩戝惉 record 鍙樺寲锛屾洿鏂拌〃鍗曟暟鎹�
+watch(() => props.record, (newRecord) => {
+ if (newRecord && isShow.value) {
+ formState.value = {
+ id: newRecord.id,
+ name: newRecord.name || '',
+ no: newRecord.no || '',
+ type: newRecord.type,
+ remark: newRecord.remark || '',
+ salaryQuota: newRecord.salaryQuota || '',
+ isQuality: props.record.isQuality,
+ inbound: newRecord.inbound,
+ reportWork: newRecord.reportWork,
+ };
+ }
+}, { immediate: true, deep: true });
+
+// 鐩戝惉寮圭獥鎵撳紑锛岄噸鏂板垵濮嬪寲琛ㄥ崟鏁版嵁
+watch(() => props.visible, (visible) => {
+ if (visible && props.record) {
+ formState.value = {
+ id: props.record.id,
+ name: props.record.name || '',
+ no: props.record.no || '',
+ type: props.record.type,
+ remark: props.record.remark || '',
+ salaryQuota: props.record.salaryQuota || '',
+ isQuality: props.record.isQuality,
+ inbound: props.record.inbound,
+ reportWork: props.record.reportWork,
+ };
+ }
+});
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ update(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
diff --git a/src/views/productionManagement/productionProcess/New.vue b/src/views/productionManagement/productionProcess/New.vue
new file mode 100644
index 0000000..0b3fd47
--- /dev/null
+++ b/src/views/productionManagement/productionProcess/New.vue
@@ -0,0 +1,129 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鏂板宸ュ簭"
+ width="400"
+ @close="closeModal"
+ >
+ <el-form label-width="140px" :model="formState" label-position="top" ref="formRef">
+ <el-form-item
+ label="宸ュ簭鍚嶇О锛�"
+ prop="name"
+ :rules="[
+ {
+ required: true,
+ message: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ {
+ max: 100,
+ message: '鏈�澶�100涓瓧绗�',
+ }
+ ]">
+ <el-input v-model="formState.name" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭缂栧彿" prop="no">
+ <el-input v-model="formState.no" />
+ </el-form-item>
+ <el-form-item
+ label="宸ュ簭绫诲瀷"
+ prop="type"
+ :rules="[
+ {
+ required: true,
+ message: '璇烽�夋嫨宸ュ簭绫诲瀷',
+ }
+ ]"
+ >
+ <el-select v-model="formState.type" placeholder="璇烽�夋嫨宸ュ簭绫诲瀷">
+ <el-option label="璁℃椂" :value="0" />
+ <el-option label="璁′欢" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ヨ祫瀹氶" prop="salaryQuota">
+ <el-input v-model="formState.salaryQuota" type="number" :step="0.001">
+ <template #append>鍏�</template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ" prop="isQuality">
+ <el-switch v-model="formState.isQuality" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鍏ュ簱" prop="inbound">
+ <el-switch v-model="formState.inbound" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="鏄惁鎶ュ伐" prop="reportWork">
+ <el-switch v-model="formState.reportWork" :active-value="true" inactive-value="false"/>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="formState.remark" type="textarea" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSubmit">纭</el-button>
+ <el-button @click="closeModal">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, getCurrentInstance } from "vue";
+import {add} from "@/api/productionManagement/productionProcess.js";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+// 鍝嶅簲寮忔暟鎹紙鏇夸唬閫夐」寮忕殑 data锛�
+const formState = ref({
+ name: '',
+ type: undefined,
+ remark: '',
+ salaryQuota: '',
+ isQuality: false,
+ inbound: false,
+ reportWork: false,
+});
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+let { proxy } = getCurrentInstance()
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const handleSubmit = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ add(formState.value).then(res => {
+ // 鍏抽棴妯℃�佹
+ isShow.value = false;
+ // 鍛婄煡鐖剁粍浠跺凡瀹屾垚
+ emit('completed');
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ })
+ }
+ })
+};
+
+defineExpose({
+ closeModal,
+ handleSubmit,
+ isShow,
+});
+</script>
diff --git a/src/views/productionManagement/productionProcess/index.vue b/src/views/productionManagement/productionProcess/index.vue
new file mode 100644
index 0000000..ee49657
--- /dev/null
+++ b/src/views/productionManagement/productionProcess/index.vue
@@ -0,0 +1,1111 @@
+<template>
+ <div class="app-container">
+ <div class="process-config-container">
+ <!-- 宸︿晶宸ュ簭鍒楄〃 -->
+ <div class="process-list-section">
+ <div class="section-header">
+ <h3 class="section-title">宸ュ簭鍒楄〃</h3>
+ <el-button type="primary"
+ size="small"
+ @click="handleAddProcess">
+ <el-icon>
+ <Plus />
+ </el-icon>鏂板宸ュ簭
+ </el-button>
+ </div>
+ <div class="process-card-list"
+ v-loading="processLoading">
+ <div v-for="process in processValueList"
+ :key="process.id"
+ class="process-card"
+ :class="{ active: selectedProcess?.id === process.id }"
+ @click="selectProcess(process)">
+ <div class="card-header">
+ <div class="process-name">{{ process.name }} <span class="process-code">{{ process.no }}</span></div>
+ <div class="card-actions">
+ <el-button link
+ type="primary"
+ @click.stop="handleEditProcess(process)">
+ <el-icon>
+ <Edit />
+ </el-icon>
+ 缂栬緫
+ </el-button>
+ <el-button link
+ type="danger"
+ @click.stop="handleDeleteProcess(process)">
+ <el-icon>
+ <Delete />
+ </el-icon>
+ 鍒犻櫎
+ </el-button>
+ </div>
+ </div>
+ <div class="card-body">
+ <!-- <div class="process-name">{{ process.name }}</div> -->
+ <div class="process-desc">{{ process.remark || '鏆傛棤鎻忚堪' }}</div>
+ <div class="process-device">鍏宠仈璁惧: {{ (deviceOptions.find(item => item.id === Number(process.deviceLedgerId))?.deviceName) || '鏈叧鑱�' }}</div>
+ </div>
+ <div class="card-footer">
+ <div class="status-tag">
+ <el-tag size="small"
+ :type="process.isQuality ? 'warning' : 'info'">
+ {{ process.isQuality ? '璐ㄦ' : '闈炶川妫�' }}
+ </el-tag>
+ <el-tag size="small"
+ style="margin-left: 8px"
+ :type="process.isProduction ? 'warning' : 'info'">
+ {{ process.isProduction ? '鐢熶骇' : '涓嶇敓浜�' }}
+ </el-tag>
+ <el-tag v-if="process.type !== null && process.type !== undefined"
+ size="small"
+ :type="process.type == 1 ? 'primary' : 'success'"
+ style="margin-left: 8px">
+ {{ process.type == 0 ? '璁℃椂' : '璁′欢' }}
+ </el-tag>
+ </div>
+ <span class="param-count">宸ヨ祫瀹氶: 楼{{ process.salaryQuota || 0 }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- 鍙充晶鍙傛暟鍒楄〃 -->
+ <div class="param-list-section">
+ <div class="section-header">
+ <h3 class="section-title">
+ {{ selectedProcess ? selectedProcess.name + ' - 鍙傛暟閰嶇疆' : '璇烽�夋嫨宸ュ簭' }}
+ </h3>
+ <el-button type="primary"
+ size="small"
+ :disabled="!selectedProcess"
+ @click="openParamDialog">
+ <el-icon>
+ <Plus />
+ </el-icon>閫夋嫨鍙傛暟
+ </el-button>
+ </div>
+ <div class="param-table-wrapper">
+ <PIMTable v-if="selectedProcess"
+ rowKey="id"
+ :column="paramColumn"
+ :tableData="paramList"
+ :page="paramPage2"
+ height="calc(100vh - 280px)"
+ :isSelection="false"
+ @pagination="handleParamPagination" />
+ <div v-else
+ class="empty-tip">
+ <el-empty description="璇蜂粠宸︿晶閫夋嫨涓�涓伐搴�" />
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- 宸ュ簭鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog v-model="processDialogVisible"
+ :title="isProcessEdit ? '缂栬緫宸ュ簭' : '鏂板宸ュ簭'"
+ width="500px">
+ <el-form :model="processForm"
+ :rules="processRules"
+ ref="processFormRef"
+ label-width="100px">
+ <el-form-item label="宸ュ簭缂栫爜"
+ prop="no">
+ <el-input v-model="processForm.no"
+ placeholder="璇疯緭鍏ュ伐搴忕紪鐮�" />
+ </el-form-item>
+ <el-form-item label="宸ュ簭鍚嶇О"
+ prop="name">
+ <el-input v-model="processForm.name"
+ placeholder="璇疯緭鍏ュ伐搴忓悕绉�" />
+ </el-form-item>
+ <el-form-item label="宸ヨ祫瀹氶"
+ prop="salaryQuota">
+ <el-input v-model="processForm.salaryQuota"
+ type="number"
+ :step="0.001" />
+ </el-form-item>
+ <el-form-item label="鏄惁璐ㄦ"
+ prop="isQuality">
+ <el-switch v-model="processForm.isQuality" />
+ </el-form-item>
+ <el-form-item label="鏄惁鐢熶骇"
+ prop="isProduction">
+ <el-switch v-model="processForm.isProduction" />
+ </el-form-item>
+ <el-form-item label="璁¤垂绫诲瀷"
+ prop="type">
+ <el-radio-group v-model="processForm.type">
+ <el-radio :label="0">璁℃椂</el-radio>
+ <el-radio :label="1">璁′欢</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鍏宠仈璁惧"
+ prop="deviceLedgerId">
+ <el-select v-model="processForm.deviceLedgerId"
+ placeholder="璇烽�夋嫨璁惧"
+ clearable
+ filterable
+ style="width: 100%">
+ <el-option v-for="item in deviceOptions"
+ :key="item.id"
+ :label="item.deviceName"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ュ簭鎻忚堪"
+ prop="remark">
+ <el-input v-model="processForm.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ伐搴忔弿杩�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleProcessSubmit">纭畾</el-button>
+ <el-button @click="processDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 閫夋嫨鍙傛暟瀵硅瘽妗� -->
+ <el-dialog v-model="paramDialogVisible"
+ title="閫夋嫨鍙傛暟"
+ width="1000px">
+ <div class="param-select-container">
+ <!-- 宸︿晶鍙傛暟鍒楄〃 -->
+ <div class="param-list-area">
+ <div class="area-title">鍙�夊弬鏁�</div>
+ <div class="search-box">
+ <el-input v-model="paramSearchKeyword"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉版悳绱�"
+ clearable
+ size="small"
+ @input="handleSelectParam">
+ <template #prefix>
+ <el-icon>
+ <Search />
+ </el-icon>
+ </template>
+ </el-input>
+ </div>
+ <el-table :data="filteredParamList"
+ height="300"
+ border
+ highlight-current-row
+ @current-change="handleParamSelect">
+ <el-table-column prop="paramName"
+ label="鍙傛暟鍚嶇О" />
+ <el-table-column prop="paramType"
+ label="鍙傛暟绫诲瀷">
+ <template #default="scope">
+ <el-tag size="small"
+ :type="getParamTypeTag(scope.row.paramType)">
+ {{ getParamTypeText(scope.row.paramType) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ <!-- 鍒嗛〉鎺т欢 -->
+ <div class="pagination-container"
+ style="margin-top: 10px;">
+ <el-pagination v-model:current-page="paramPage.current"
+ v-model:page-size="paramPage.size"
+ :page-sizes="[10, 20, 50, 100]"
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="paramPage.total"
+ @size-change="handleParamSizeChange"
+ @current-change="handleParamCurrentChange"
+ size="small" />
+ </div>
+ </div>
+ <!-- 鍙充晶鍙傛暟璇︽儏 -->
+ <div class="param-detail-area">
+ <div class="area-title">鍙傛暟璇︽儏</div>
+ <el-form v-if="selectedParam"
+ :model="selectedParam"
+ label-width="100px"
+ class="param-detail-form">
+ <el-form-item label="鍙傛暟鍚嶇О">
+ <span class="detail-text">{{ selectedParam.paramName }}</span>
+ </el-form-item>
+ <el-form-item label="鍙傛暟绫诲瀷">
+ <el-tag size="small"
+ :type="getParamTypeTag(selectedParam.paramType)">
+ {{ getParamTypeText(selectedParam.paramType) }}
+ </el-tag>
+ </el-form-item>
+ <el-form-item label="鍙傛暟鏍煎紡">
+ <span class="detail-text">{{ selectedParam.paramFormat || '-' }}</span>
+ </el-form-item>
+ <el-form-item label="鍗曚綅">
+ <span class="detail-text">{{ selectedParam.unit || '-' }}</span>
+ </el-form-item>
+ <el-form-item label="鏍囧噯鍊�">
+ <el-input v-model="selectedParam.standardValue"
+ @input="val => onStandardValueInput(val, selectedParam)"
+ placeholder="璇疯緭鍏ラ粯璁ゅ��" />
+ </el-form-item>
+ </el-form>
+ <el-empty v-else
+ description="璇蜂粠宸︿晶閫夋嫨鍙傛暟" />
+ </div>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :disabled="!selectedParam"
+ @click="handleParamSubmit">纭畾</el-button>
+ <el-button @click="paramDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 缂栬緫鍙傛暟瀵硅瘽妗� -->
+ <el-dialog v-model="editParamDialogVisible"
+ title="缂栬緫鍙傛暟"
+ width="600px">
+ <el-form :model="editParamForm"
+ :rules="editParamRules"
+ ref="editParamFormRef"
+ label-width="120px">
+ <el-form-item label="鍙傛暟鍚嶇О">
+ <span class="detail-text">{{ editParamForm.paramName }}</span>
+ </el-form-item>
+ <el-form-item label="鏍囧噯鍊�"
+ prop="standardValue">
+ <el-input v-model="editParamForm.standardValue"
+ @input="val => onStandardValueInput(val, editParamForm)"
+ placeholder="璇疯緭鍏ユ爣鍑嗗��" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleEditParamSubmit">纭畾</el-button>
+ <el-button @click="editParamDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, computed, onMounted } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import { Plus, Edit, Delete, Search } from "@element-plus/icons-vue";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import { listType } from "@/api/system/dict/type";
+ import {
+ add,
+ update,
+ del,
+ list as getProcessListApi,
+ processList,
+ getProcessParamList,
+ addProcessParam,
+ editProcessParam,
+ deleteProcessParam,
+ } from "@/api/productionManagement/productionProcess.js";
+ import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+ import { getBaseParamList } from "@/api/basicData/parameterMaintenance.js";
+
+ // 宸ュ簭鍒楄〃鏁版嵁
+ const processValueList = ref([]);
+ const selectedProcess = ref(null);
+ const processLoading = ref(false);
+ const deviceOptions = ref([]);
+
+ // 宸ュ簭宸查�夊弬鏁拌〃鏍煎垎椤碉紙鎺ュ彛涓�娆¤繑鍥炲叏閲忥級
+ const paramPage2 = ref({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const paramListRaw = ref([]);
+ const paramList = computed(() => {
+ const all = paramListRaw.value;
+ const { current, size } = paramPage2.value;
+ const start = (current - 1) * size;
+ return all.slice(start, start + size);
+ });
+ const paramLoading = ref(false);
+
+ // 鏁版嵁瀛楀吀
+ const dictTypes = ref([]);
+
+ // 宸ュ簭瀵硅瘽妗�
+ const processDialogVisible = ref(false);
+ const isProcessEdit = ref(false);
+ const processFormRef = ref(null);
+ const processForm = reactive({
+ id: null,
+ no: "",
+ name: "",
+ salaryQuota: null,
+ isQuality: false,
+ isProduction: false,
+ remark: "",
+ deviceLedgerId: null,
+ type: 0,
+ });
+ const processRules = {
+ no: [{ required: true, message: "璇疯緭鍏ュ伐搴忕紪鐮�", trigger: "blur" }],
+ name: [{ required: true, message: "璇疯緭鍏ュ伐搴忓悕绉�", trigger: "blur" }],
+ salaryQuota: [
+ {
+ required: false,
+ message: "璇疯緭鍏ュ伐璧勫畾棰�",
+ trigger: "blur",
+ validator: (rule, value, callback) => {
+ if (isNaN(value) || value < 0) {
+ callback(new Error("宸ヨ祫瀹氶蹇呴』鏄潪璐熸暟瀛�"));
+ } else {
+ callback();
+ }
+ },
+ },
+ ],
+ deviceLedgerId: [
+ { required: false, message: "璇烽�夋嫨璁惧", trigger: "change" },
+ ],
+ type: [{ required: false, message: "璇烽�夋嫨璁¤垂绫诲瀷", trigger: "change" }],
+ };
+
+ // 鍙傛暟瀵硅瘽妗�
+ const paramDialogVisible = ref(false);
+ const availableParamList = ref([]);
+ const filteredParamList = ref([]);
+ const selectedParam = ref(null);
+ const paramSearchKeyword = ref("");
+
+ // 鍙�夊弬鏁板垎椤�
+ const paramPage = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ // 缂栬緫鍙傛暟瀵硅瘽妗�
+ const editParamDialogVisible = ref(false);
+ const editParamFormRef = ref(null);
+ const editParamForm = reactive({
+ id: null,
+ technologyOperationId: null,
+ technologyParamId: null,
+ paramName: "",
+ standardValue: null,
+ paramType: null,
+ });
+
+ const onStandardValueInput = (val, target) => {
+ const data = target.value || target;
+ const type = data.paramType;
+ if (type === 1) {
+ // 鏁板�兼牸寮忥細涓嶈兘杈撳叆涓枃鎴栬嫳鏂囧瓧绗�
+ data.standardValue = val.replace(/[a-zA-Z\u4e00-\u9fa5]/g, "");
+ }
+ };
+
+ const editParamRules = {
+ standardValue: [
+ {
+ required: true,
+ message: "璇疯緭鍏ユ爣鍑嗗��",
+ trigger: "blur",
+ validator: (rule, value, callback) => {
+ if (value === null || value === undefined || value === "") {
+ callback(new Error("璇疯緭鍏ユ爣鍑嗗��"));
+ } else {
+ const type = editParamForm.paramType;
+ if (type === 1 && value) {
+ if (/[a-zA-Z\u4e00-\u9fa5]/.test(value)) {
+ return callback(new Error("鏁板�兼牸寮忎笉鑳藉寘鍚腑鑻辨枃瀛楃"));
+ }
+ }
+ callback();
+ }
+ },
+ },
+ ],
+ };
+
+ // 鍙傛暟琛ㄦ牸鍒楅厤缃�
+ const paramColumn = ref([
+ {
+ label: "鍙傛暟鍚嶇О",
+ prop: "paramName",
+ },
+ {
+ label: "鍙傛暟绫诲瀷",
+ prop: "paramType",
+ dataType: "tag",
+ formatType: params => {
+ const typeMap = {
+ 1: "primary",
+ 2: "info",
+ 3: "warning",
+ 4: "success",
+ };
+ return typeMap[params] || "default";
+ },
+ formatData: val => {
+ const labelMap = {
+ 1: "鏁板�兼牸寮�",
+ 2: "鏂囨湰鏍煎紡",
+ 3: "涓嬫媺閫夐」",
+ 4: "鏃堕棿鏍煎紡",
+ };
+ return labelMap[val] || val;
+ },
+ },
+ {
+ label: "鍙栧�兼牸寮�",
+ prop: "paramFormat",
+ },
+ {
+ label: "鏍囧噯鍊�",
+ prop: "standardValue",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鎿嶄綔",
+ dataType: "action",
+ width: "150",
+ operation: [
+ {
+ name: "缂栬緫",
+ clickFun: row => handleEditParam(row),
+ },
+ {
+ name: "鍒犻櫎",
+ clickFun: row => handleDeleteParam(row),
+ },
+ ],
+ },
+ ]);
+
+ // 鑾峰彇宸ュ簭鍒楄〃
+ const getProcessList = () => {
+ processLoading.value = true;
+ getProcessListApi({ size: -1, current: -1 })
+ .then(res => {
+ processValueList.value = res.data.records || [];
+ console.log(
+ processValueList.value,
+ "reprocessValueList.value==========s"
+ );
+ })
+ .catch(() => {
+ ElMessage.error("鑾峰彇宸ュ簭鍒楄〃澶辫触");
+ })
+ .finally(() => {
+ processLoading.value = false;
+ });
+ };
+
+ const loadDeviceName = async () => {
+ try {
+ const { data } = await getDeviceLedger();
+ deviceOptions.value = data || [];
+ } catch (error) {
+ console.error("鍔犺浇璁惧鍒楄〃澶辫触", error);
+ }
+ };
+
+ // 鑾峰彇鍙傛暟鍒楄〃
+ const getParamList = processId => {
+ paramLoading.value = true;
+ getProcessParamList({ technologyOperationId: processId })
+ .then(res => {
+ const list = res.data || [];
+ paramListRaw.value = Array.isArray(list) ? list : [];
+ paramPage2.value.total = paramListRaw.value.length;
+ const maxPage = Math.max(
+ 1,
+ Math.ceil(paramPage2.value.total / paramPage2.value.size) || 1
+ );
+ if (paramPage2.value.current > maxPage) {
+ paramPage2.value.current = maxPage;
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鑾峰彇鍙傛暟鍒楄〃澶辫触");
+ })
+ .finally(() => {
+ paramLoading.value = false;
+ });
+ };
+
+ // 閫夋嫨宸ュ簭
+ const selectProcess = process => {
+ selectedProcess.value = process;
+ paramPage2.value.current = 1;
+ getParamList(process.id);
+ };
+
+ // 宸ュ簭鎿嶄綔
+ const handleAddProcess = () => {
+ isProcessEdit.value = false;
+ processForm.id = null;
+ processForm.no = "";
+ processForm.name = "";
+ processForm.salaryQuota = null;
+ processForm.isQuality = false;
+ processForm.isProduction = false;
+ processForm.remark = "";
+ processForm.deviceLedgerId = null;
+ processForm.type = 0;
+ processDialogVisible.value = true;
+ };
+
+ const handleEditProcess = async process => {
+ isProcessEdit.value = true;
+ processForm.id = process.id;
+ processForm.no = process.no;
+ processForm.name = process.name;
+ processForm.salaryQuota = process.salaryQuota;
+ processForm.isQuality = !!process.isQuality;
+ processForm.isProduction = !!process.isProduction;
+ processForm.remark = process.remark || "";
+ // 濡傛灉璁惧 ID 涓� 0 鎴栬�呭湪璁惧鍒楄〃涓壘涓嶅埌锛屽垯鍥炴樉涓虹┖锛坣ull锛�
+ const deviceId = Number(process.deviceLedgerId);
+ const hasDevice = deviceOptions.value.some(item => item.id === deviceId);
+ processForm.deviceLedgerId = deviceId && hasDevice ? deviceId : null;
+ processForm.type = process.type;
+ processDialogVisible.value = true;
+ };
+
+ const handleDeleteProcess = process => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ宸ュ簭鍚楋紵", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ del([process.id])
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getProcessList();
+ if (selectedProcess.value?.id === process.id) {
+ selectedProcess.value = null;
+ paramListRaw.value = [];
+ paramPage2.value.total = 0;
+ }
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+ };
+
+ const handleProcessSubmit = () => {
+ processFormRef.value.validate(valid => {
+ if (valid) {
+ if (processForm.id) {
+ update(processForm)
+ .then(() => {
+ ElMessage.success("缂栬緫鎴愬姛");
+ processDialogVisible.value = false;
+ getProcessList();
+ })
+ .catch(() => {
+ ElMessage.error("缂栬緫澶辫触");
+ });
+ } else {
+ add(processForm)
+ .then(() => {
+ ElMessage.success("鏂板鎴愬姛");
+ processDialogVisible.value = false;
+ getProcessList();
+ })
+ .catch(() => {
+ ElMessage.error("鏂板澶辫触");
+ });
+ }
+ }
+ });
+ };
+ const openParamDialog = () => {
+ paramSearchKeyword.value = "";
+ if (!selectedProcess.value) {
+ ElMessage.warning("璇峰厛閫夋嫨涓�涓伐搴�");
+ return;
+ }
+ // 鑾峰彇鍙�夊弬鏁板垪琛�
+ getBaseParamList({
+ paramName: paramSearchKeyword.value,
+ current: paramPage.current,
+ size: paramPage.size,
+ }).then(res => {
+ if (res.code === 200) {
+ filteredParamList.value = res.data?.records || [];
+ paramPage.total = res.data?.total || 0;
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ });
+ console.log(filteredParamList.value, "鍙�夊弬鏁板垪琛�");
+ selectedParam.value = null;
+ paramDialogVisible.value = true;
+ };
+
+ // 鍙傛暟鎿嶄綔
+ const handleSelectParam = () => {
+ if (!selectedProcess.value) {
+ ElMessage.warning("璇峰厛閫夋嫨涓�涓伐搴�");
+ return;
+ }
+ // 鑾峰彇鍙�夊弬鏁板垪琛�
+ getBaseParamList({
+ paramName: paramSearchKeyword.value,
+ current: paramPage.current,
+ size: paramPage.size,
+ }).then(res => {
+ if (res.code === 200) {
+ filteredParamList.value = res.data?.records || [];
+ paramPage.total = res.data?.total || 0;
+ } else {
+ ElMessage.error(res.msg || "鏌ヨ澶辫触");
+ }
+ });
+ console.log(filteredParamList.value, "鍙�夊弬鏁板垪琛�");
+ selectedParam.value = null;
+ paramDialogVisible.value = true;
+ };
+
+ const handleParamSelect = row => {
+ selectedParam.value = row;
+ };
+
+ const handleParamSearch = () => {
+ // 閲嶇疆鍒嗛〉
+ paramPage.current = 1;
+ // 閲嶆柊鍔犺浇鏁版嵁
+ handleSelectParam();
+ };
+
+ // 澶勭悊鍒嗛〉澶у皬鍙樺寲
+ const handleParamSizeChange = size => {
+ paramPage.size = size;
+ handleSelectParam();
+ };
+
+ // 澶勭悊褰撳墠椤电爜鍙樺寲
+ const handleParamCurrentChange = current => {
+ paramPage.current = current;
+ handleSelectParam();
+ };
+ const getParamTypeText = type => {
+ const typeMap = {
+ 1: "鏁板�兼牸寮�",
+ 2: "鏂囨湰鏍煎紡",
+ 3: "涓嬫媺閫夐」",
+ 4: "鏃堕棿鏍煎紡",
+ };
+ return typeMap[type] || "鏈煡鍙傛暟绫诲瀷";
+ };
+ const getParamTypeTag = type => {
+ const typeMap = {
+ 1: "primary",
+ 2: "info",
+ 3: "warning",
+ 4: "success",
+ };
+ return typeMap[type] || "default";
+ };
+
+ const handleDeleteParam = row => {
+ ElMessageBox.confirm("纭畾瑕佸垹闄よ鍙傛暟鍚楋紵", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ deleteProcessParam(row.id)
+ .then(() => {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getParamList(selectedProcess.value.id);
+ })
+ .catch(() => {
+ ElMessage.error("鍒犻櫎澶辫触");
+ });
+ });
+ };
+
+ const handleEditParam = row => {
+ editParamForm.id = row.id;
+ editParamForm.technologyOperationId = row.technologyOperationId;
+ editParamForm.technologyParamId = row.technologyParamId;
+ editParamForm.paramName = row.paramName;
+ editParamForm.standardValue = row.standardValue;
+ editParamForm.paramType = row.paramType;
+ editParamDialogVisible.value = true;
+ };
+
+ const handleEditParamSubmit = () => {
+ editParamFormRef.value.validate(valid => {
+ if (valid) {
+ editProcessParam(editParamForm)
+ .then(() => {
+ ElMessage.success("缂栬緫鎴愬姛");
+ editParamDialogVisible.value = false;
+ getParamList(selectedProcess.value.id);
+ })
+ .catch(() => {
+ ElMessage.error("缂栬緫澶辫触");
+ });
+ }
+ });
+ };
+
+ const handleParamSubmit = () => {
+ if (!selectedParam.value) {
+ ElMessage.warning("璇峰厛閫夋嫨涓�涓弬鏁�");
+ return;
+ }
+ addProcessParam({
+ technologyOperationId: selectedProcess.value.id,
+ technologyParamId: selectedParam.value.id,
+ standardValue: selectedParam.value.standardValue,
+ })
+ .then(() => {
+ ElMessage.success("娣诲姞鎴愬姛");
+ paramDialogVisible.value = false;
+ getParamList(selectedProcess.value.id);
+ })
+ .catch(() => {
+ ElMessage.error("娣诲姞澶辫触");
+ });
+ };
+
+ const handleParamPagination = obj => {
+ paramPage2.value.current = obj.page;
+ paramPage2.value.size = obj.limit;
+ };
+
+ // 鑾峰彇鏁版嵁瀛楀吀
+ const getDictTypes = () => {
+ listType({ pageNum: 1, pageSize: 1000 }).then(res => {
+ dictTypes.value = res.rows || [];
+ });
+ };
+
+ onMounted(() => {
+ loadDeviceName();
+ getProcessList();
+ getDictTypes();
+ });
+</script>
+
+<style scoped lang="scss">
+ .app-container {
+ padding: 20px;
+ background-color: #f0f2f5;
+ min-height: calc(100vh - 84px);
+ }
+
+ .process-config-container {
+ display: flex;
+ gap: 20px;
+ height: calc(100vh - 124px);
+ }
+
+ // 宸︿晶宸ュ簭鍒楄〃
+ .process-list-section {
+ width: 370px;
+ min-width: 370px;
+ flex-shrink: 0;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ display: flex;
+ flex-direction: column;
+ }
+
+ .section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ border-bottom: 1px solid #ebeef5;
+
+ .section-title {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+ }
+
+ .process-card-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 16px;
+ }
+
+ .process-card {
+ background: #fff;
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 12px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+
+ &:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ transform: translateY(-2px);
+ }
+
+ &.active {
+ border-color: #409eff;
+ background: #f5f7fa;
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
+ }
+
+ .card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8px;
+
+ .process-code {
+ font-size: 12px;
+ // color: #909399;
+ color: #cb9b18;
+ font-family: "Courier New", monospace;
+ }
+
+ .card-actions {
+ display: flex;
+ gap: 4px;
+
+ .el-button {
+ padding: 4px;
+ }
+ }
+ }
+
+ .card-body {
+ margin-bottom: 12px;
+
+ .process-name {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 4px;
+ }
+
+ .process-desc {
+ font-size: 12px;
+ color: #909399;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ margin-bottom: 4px;
+ }
+
+ .process-device {
+ font-size: 12px;
+ color: #606266;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+
+ .card-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .param-count {
+ font-size: 12px;
+ color: #606266;
+ }
+ }
+ }
+
+ // 鍙充晶鍙傛暟鍒楄〃
+ .param-list-section {
+ flex: 1;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ }
+
+ .param-table-wrapper {
+ flex: 1;
+ padding: 0 20px 20px;
+ overflow: auto;
+ min-width: 100%;
+ }
+
+ /* 琛ㄦ牸妯悜婊氬姩 */
+ .param-table-wrapper :deep(.el-table) {
+ min-width: 100%;
+ }
+
+ .param-table-wrapper :deep(.el-table__body-wrapper) {
+ overflow-x: auto;
+ }
+
+ .pagination-container {
+ margin-top: 10px;
+ overflow-x: auto;
+ padding-bottom: 8px;
+ }
+
+ .pagination-container .el-pagination {
+ white-space: nowrap;
+ }
+
+ /* 鍝嶅簲寮忚皟鏁� */
+ @media screen and (max-width: 768px) {
+ .pagination-container {
+ font-size: 12px;
+ }
+
+ .pagination-container .el-pagination__sizes {
+ margin-right: 8px;
+ }
+
+ .pagination-container .el-pagination__jump {
+ margin-left: 8px;
+ }
+
+ .pagination-container .el-pagination__page-size {
+ font-size: 12px;
+ }
+ }
+
+ .empty-tip {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ // 琛ㄦ牸鏍峰紡
+ :deep(.el-table) {
+ border: none;
+ border-radius: 6px;
+ overflow: hidden;
+
+ .el-table__header-wrapper {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+
+ th {
+ background: transparent;
+ font-weight: 600;
+ // color: #ffffff;
+ border-bottom: none;
+ padding: 16px 0;
+ }
+ }
+
+ .el-table__body-wrapper {
+ tr {
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: linear-gradient(
+ 90deg,
+ rgba(102, 126, 234, 0.05) 0%,
+ rgba(118, 75, 162, 0.05) 100%
+ );
+ }
+
+ td {
+ border-bottom: 1px solid #f0f0f0;
+ padding: 14px 0;
+ color: #303133;
+ }
+ }
+ }
+ }
+
+ // 缂栫爜鍗曞厓鏍兼牱寮�
+ :deep(.code-cell) {
+ color: #e6a23c;
+ font-family: "Courier New", monospace;
+ font-weight: 500;
+ }
+
+ // 鏁板�煎崟鍏冩牸鏍峰紡
+ :deep(.quantity-cell) {
+ font-weight: 600;
+ color: #409eff;
+ font-family: "Courier New", monospace;
+ }
+
+ // 閫夋嫨鍙傛暟瀵硅瘽妗嗘牱寮�
+ .param-select-container {
+ display: flex;
+ gap: 20px;
+ height: 450px;
+
+ .param-list-area {
+ // flex: 1;
+ width: 380px;
+ display: flex;
+ flex-direction: column;
+
+ .area-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 12px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #ebeef5;
+ }
+
+ .search-box {
+ margin-bottom: 12px;
+
+ .el-input {
+ width: 100%;
+ }
+ }
+ }
+
+ .param-detail-area {
+ // width: 380px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: #f5f7fa;
+ border-radius: 8px;
+ padding: 16px;
+
+ .area-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #303133;
+ margin-bottom: 16px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #ebeef5;
+ }
+
+ .param-detail-form {
+ .el-form-item {
+ margin-bottom: 12px;
+
+ .el-form-item__label {
+ color: #606266;
+ font-weight: 500;
+ }
+ }
+
+ .detail-text {
+ color: #303133;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/productionReporting/Input.vue b/src/views/productionManagement/productionReporting/Input.vue
new file mode 100644
index 0000000..3ba68f7
--- /dev/null
+++ b/src/views/productionManagement/productionReporting/Input.vue
@@ -0,0 +1,115 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="isShow"
+ title="鎶曞叆"
+ @close="closeModal"
+ >
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="data"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ ></PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="closeModal">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, computed, onMounted} from "vue";
+import { productionProductInputListPage } from "@/api/productionManagement/productionProductInput";
+
+const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ productionProductMainId: {
+ type: Number,
+ required: true,
+ },
+});
+
+const emit = defineEmits(['update:visible', 'completed']);
+
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0
+});
+
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ fetchData();
+};
+
+const tableLoading = ref(false);
+
+const tableColumn = [
+ {
+ label: '鎶ュ伐鍗曞彿',
+ prop: 'productNo',
+ },
+ {
+ label: '鎶曞叆浜у搧鍚嶇О',
+ prop: 'productName',
+ },
+ {
+ label: '鎶曞叆浜у搧鍨嬪彿',
+ prop: 'model',
+ },
+ {
+ label: '鎶曞叆鏁伴噺',
+ prop: 'quantity',
+ },
+ {
+ label: '鍗曚綅',
+ prop: 'unit',
+ },
+]
+
+const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit('update:visible', val);
+ },
+});
+
+const data = ref([])
+
+const closeModal = () => {
+ isShow.value = false;
+};
+
+const fetchData = () => {
+ tableLoading.value = true;
+ const params = { productMainId: props.productionProductMainId, ...page };
+
+ productionProductInputListPage(params).then(res => {
+ tableLoading.value = false;
+ data.value = res.data.records;
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+
+defineExpose({
+ closeModal,
+ isShow,
+});
+
+onMounted(() => {
+ fetchData()
+})
+</script>
diff --git a/src/views/productionManagement/productionReporting/Output.vue b/src/views/productionManagement/productionReporting/Output.vue
new file mode 100644
index 0000000..4eeac43
--- /dev/null
+++ b/src/views/productionManagement/productionReporting/Output.vue
@@ -0,0 +1,106 @@
+<template>
+ <div>
+ <el-dialog v-model="isShow"
+ title="浜у嚭"
+ @close="closeModal">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="data"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination"></PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="closeModal">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, computed, onMounted } from "vue";
+ import { productionProductOutputListPage } from "@/api/productionManagement/productionProductOutput.js";
+
+ const props = defineProps({
+ visible: {
+ type: Boolean,
+ required: true,
+ },
+ productionProductMainId: {
+ type: Number,
+ required: true,
+ },
+ });
+
+ const emit = defineEmits(["update:visible", "completed"]);
+
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ fetchData();
+ };
+
+ const tableLoading = ref(false);
+
+ const tableColumn = [
+ {
+ label: "鎶ュ伐鍗曞彿",
+ prop: "productNo",
+ },
+ {
+ label: "浜у搧鍨嬪彿",
+ prop: "model",
+ },
+ {
+ label: "浜у嚭鏁伴噺",
+ prop: "quantity",
+ },
+ ];
+
+ const isShow = computed({
+ get() {
+ return props.visible;
+ },
+ set(val) {
+ emit("update:visible", val);
+ },
+ });
+
+ const data = ref([]);
+
+ const closeModal = () => {
+ isShow.value = false;
+ };
+
+ const fetchData = () => {
+ tableLoading.value = true;
+ const params = { productMainId: props.productionProductMainId, ...page };
+
+ productionProductOutputListPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ data.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+
+ defineExpose({
+ closeModal,
+ isShow,
+ });
+
+ onMounted(() => {
+ fetchData();
+ });
+</script>
diff --git a/src/views/productionManagement/productionReporting/components/formDia.vue b/src/views/productionManagement/productionReporting/components/formDia.vue
new file mode 100644
index 0000000..15958e6
--- /dev/null
+++ b/src/views/productionManagement/productionReporting/components/formDia.vue
@@ -0,0 +1,185 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="鐢熶骇鎶ュ伐"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鎺掍骇鏁伴噺锛�" prop="schedulingNum">
+ <el-input v-model="form.schedulingNum" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="寰呯敓浜ф暟閲忥細" prop="pendingNum">
+ <el-input v-model="form.pendingNum" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鏈鐢熶骇鏁伴噺锛�" prop="finishedNum">
+ <el-input-number
+ v-model="form.finishedNum"
+ placeholder="璇疯緭鍏�"
+ :min="0"
+ :step="0.1"
+ :precision="2"
+ clearable
+ style="width: 100%"
+ @change="changeNum"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍗曚环(鍏�)锛�" prop="unitPrice">
+ <el-input v-model="form.unitPrice" placeholder="璇疯緭鍏�" clearable @input="calculateTotalPrice"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鎬讳环(鍏�)锛�" prop="totalPrice">
+ <el-input v-model="form.totalPrice" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鐢熶骇浜猴細" prop="schedulingUserId">
+ <el-select
+ v-model="form.schedulingUserId"
+ placeholder="閫夋嫨浜哄憳"
+ style="width: 100%;"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ >
+ <el-option
+ v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢熶骇鏃ユ湡锛�" prop="schedulingDate">
+ <el-date-picker
+ v-model="form.schedulingDate"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+// import {getStaffJoinInfo, staffJoinAdd, staffJoinUpdate} from "@/api/personnelManagement/onboarding.js";
+import {userListNoPageByTenantId} from "@/api/system/user.js";
+import {productionReport, productionReportUpdate} from "@/api/productionManagement/productionReporting.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const userList = ref([])
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const data = reactive({
+ form: {
+ successNum: "",
+ schedulingNum: "",
+ finishedNum: "",
+ schedulingUserId: "",
+ schedulingDate: "",
+ unitPrice: "",
+ totalPrice: "",
+ },
+ rules: {
+ schedulingNum: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" },],
+ },
+});
+const { form, rules } = toRefs(data);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ userListNoPageByTenantId().then((res) => {
+ userList.value = res.data;
+ });
+ form.value = {...row}
+}
+
+const changeNum = (value) => {
+ if (value > form.value.schedulingNum) {
+ form.value.finishedNum = form.value.schedulingNum;
+ proxy.$modal.msgWarning('鏈鐢熶骇鏁伴噺涓嶅彲澶т簬鎺掍骇鏁伴噺')
+ }
+ form.value.pendingNum = form.value.schedulingNum - form.value.finishedNum;
+ calculateTotalPrice();
+}
+
+// 璁$畻鎬讳环
+const calculateTotalPrice = () => {
+ const quantity = Number(form.value.finishedNum ?? 0);
+ const unitPrice = Number(form.value.unitPrice ?? 0);
+
+ if (quantity > 0 && unitPrice > 0) {
+ form.value.totalPrice = (quantity * unitPrice).toFixed(2);
+ } else {
+ form.value.totalPrice = '0.00';
+ }
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ form.value.staffState = 1
+ if (operationType.value === "add") {
+ productionReport(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ } else {
+ productionReportUpdate(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ }
+ })
+}
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/productionManagement/productionReporting/index.vue b/src/views/productionManagement/productionReporting/index.vue
new file mode 100644
index 0000000..fa4e163
--- /dev/null
+++ b/src/views/productionManagement/productionReporting/index.vue
@@ -0,0 +1,476 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm"
+ :inline="true">
+ <el-form-item label="鎶ュ伐浜哄憳鍚嶇О:">
+ <el-input v-model="searchForm.nickName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="宸ュ崟鍙�:">
+ <el-input v-model="searchForm.workOrderNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div style="text-align: right"
+ class="mb10">
+ <!-- <el-button type="primary"
+ @click="openForm('add')">鐢熶骇鎶ュ伐</el-button> -->
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ </div>
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ :expandRowKeys="expandedRowKeys"
+ @expand-change="expandChange"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total">
+ <template #expand="{ row }">
+ <el-table :data="expandData"
+ border
+ show-summary
+ :summary-method="summarizeMainTable"
+ v-loading="childrenLoading">
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鏈鐢熶骇鏁伴噺"
+ prop="finishedNum"
+ align="center"
+ width="400">
+ <template #default="scope">
+ <el-input-number :step="0.01"
+ :min="0"
+ style="width: 100%"
+ v-model="scope.row.finishedNum"
+ :disabled="!scope.row.editType"
+ :precision="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="changeNum(scope.row)" />
+ </template>
+ </el-table-column>
+ <!-- <el-table-column label="寰呯敓浜ф暟閲�" prop="pendingNum" width="240" align="center"></el-table-column>-->
+ <el-table-column label="鐢熶骇浜�"
+ prop="schedulingUserId"
+ width="400">
+ <template #default="scope">
+ <el-select v-model="scope.row.schedulingUserId"
+ placeholder="閫夋嫨浜哄憳"
+ :disabled="!scope.row.editType"
+ style="width: 100%;">
+ <el-option v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐢熶骇鏃ユ湡"
+ prop="schedulingDate"
+ width="400">
+ <template #default="scope">
+ <el-date-picker v-model="scope.row.schedulingDate"
+ type="date"
+ :disabled="!scope.row.editType"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ size="small"
+ @click="changeEditType(scope.row)"
+ v-if="!scope.row.editType"
+ :disabled="scope.row.parentStatus === 3">缂栬緫</el-button>
+ <el-button link
+ type="primary"
+ size="small"
+ @click="saveReceiptPayment(scope.row)"
+ v-if="scope.row.editType">淇濆瓨</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </template>
+ </PIMTable>
+ </div>
+ <form-dia ref="formDia"
+ @close="handleQuery"></form-dia>
+ <input-modal v-if="isShowInput"
+ v-model:visible="isShowInput"
+ :production-product-main-id="isShowingId" />
+ <!-- 鍙傛暟璇︽儏寮圭獥 -->
+ <el-dialog v-model="paramDetailVisible"
+ title="鍙傛暟璇︽儏"
+ width="600px">
+ <div v-if="currentParams && currentParams.length > 0"
+ class="param-detail-list">
+ <el-descriptions :column="1"
+ border>
+ <el-descriptions-item v-for="param in currentParams"
+ :key="param.id"
+ :label="param.paramName">
+ {{ param.inputValue }}
+ <span v-if="param.unit && param.unit !== '/'"
+ class="unit-text">({{ param.unit }})</span>
+ </el-descriptions-item>
+ </el-descriptions>
+ </div>
+ <el-empty v-else
+ description="鏆傛棤鍙傛暟鏁版嵁" />
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="paramDetailVisible = false">鍏抽棴</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
+ import FormDia from "@/views/productionManagement/productionReporting/components/formDia.vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ productionReportUpdate,
+ workListPageById,
+ productionReportDelete,
+ } from "@/api/productionManagement/productionReporting.js";
+ import { productionProductMainListPage } from "@/api/productionManagement/productionProductMain.js";
+ import { userListNoPageByTenantId } from "@/api/system/user.js";
+ import InputModal from "@/views/productionManagement/productionReporting/Input.vue";
+
+ const data = reactive({
+ searchForm: {
+ nickName: "",
+ workOrderNo: "",
+ workOrderStatus: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+ const expandedRowKeys = ref([]);
+ const expandData = ref([]);
+ const userList = ref([]);
+ const tableColumn = ref([
+ {
+ label: "鎶ュ伐鍗曞彿",
+ prop: "productNo",
+ width: 120,
+ },
+ {
+ label: "鎶ュ伐浜哄憳",
+ prop: "nickName",
+ width: 120,
+ },
+ {
+ label: "宸ユ椂锛坔锛�",
+ width: 100,
+ prop: "workHour",
+ },
+ {
+ label: "宸ュ簭",
+ prop: "process",
+ width: 120,
+ },
+ {
+ label: "宸ュ崟缂栧彿",
+ prop: "workOrderNo",
+ width: 120,
+ },
+ {
+ label: "閿�鍞悎鍚屽彿",
+ prop: "salesContractNo",
+ width: 120,
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: 120,
+ },
+ {
+ label: "浜у搧瑙勬牸鍨嬪彿",
+ prop: "productModelName",
+ width: 120,
+ },
+ {
+ label: "浜у嚭鏁伴噺",
+ prop: "quantity",
+ width: 120,
+ },
+ {
+ label: "鎶ュ簾鏁伴噺",
+ prop: "scrapQty",
+ width: 120,
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ width: 120,
+ },
+
+ {
+ label: "鍒涘缓鏃堕棿",
+ prop: "createTime",
+ width: 120,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 250,
+ operation: [
+ {
+ name: "鏌ョ湅鎶曞叆",
+ type: "text",
+ clickFun: row => {
+ showInput(row);
+ },
+ },
+ {
+ name: "鍙傛暟璇︽儏",
+ type: "text",
+ clickFun: row => {
+ showParamDetail(row);
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ clickFun: row => {
+ deleteReport(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const paramDetailVisible = ref(false);
+ const currentParams = ref([]);
+
+ const showParamDetail = row => {
+ currentParams.value = row.productionOperationParamList || [];
+ paramDetailVisible.value = true;
+ };
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const childrenLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const formDia = ref();
+ const { proxy } = getCurrentInstance();
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const changeDaterange = value => {
+ if (value) {
+ searchForm.value.entryDateStart = value[0];
+ searchForm.value.entryDateEnd = value[1];
+ } else {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ }
+ handleQuery();
+ };
+ const deleteReport = row => {
+ ElMessageBox.confirm("纭畾鍒犻櫎璇ユ姤宸ュ悧锛�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ productionReportDelete({ id: row.id }).then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ } else {
+ ElMessageBox.alert(res.msg || "鍒犻櫎澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ }
+ });
+ });
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined;
+ expandedRowKeys.value = [];
+ productionProductMainListPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records.map(item => ({
+ ...item,
+ pendingFinishNum:
+ (Number(item.schedulingNum) || 0) - (Number(item.finishedNum) || 0),
+ }));
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ // 灞曞紑琛�
+ const expandChange = (row, expandedRows) => {
+ userListNoPageByTenantId().then(res => {
+ userList.value = res.data;
+ });
+ if (expandedRows.length > 0) {
+ nextTick(() => {
+ expandedRowKeys.value = [];
+ try {
+ childrenLoading.value = true;
+ workListPageById({ id: row.id }).then(res => {
+ childrenLoading.value = false;
+ const index = tableData.value.findIndex(item => item.id === row.id);
+ if (index > -1) {
+ expandData.value = res.data.map(item => ({
+ ...item,
+ pendingNum:
+ (Number(item.schedulingNum) || 0) -
+ (Number(item.finishedNum) || 0),
+ parentStatus: row.status, // 鏂板鐖惰〃鐘舵��
+ }));
+ }
+ expandedRowKeys.value.push(row.id);
+ });
+ } catch (error) {
+ childrenLoading.value = false;
+ console.log(error);
+ }
+ });
+ } else {
+ expandedRowKeys.value = [];
+ }
+ };
+ const changeNum = row => {
+ // 鎵惧埌鐖惰〃鏍兼暟鎹�
+ const parentRow = tableData.value.find(
+ item => item.id === expandedRowKeys.value[0]
+ );
+ // 璁$畻鎵�鏈夊瓙琛ㄦ牸 finishedNum 鐨勬�诲拰
+ const totalFinishedNum = expandData.value.reduce(
+ (sum, item) => sum + (Number(item.finishedNum) || 0),
+ 0
+ );
+ // 鐖惰〃鏍肩殑鎺掍骇鏁伴噺
+ const schedulingNum = parentRow ? Number(parentRow.schedulingNum) : 0;
+
+ if (totalFinishedNum > schedulingNum) {
+ // 鍥為��鏈杈撳叆
+ row.finishedNum =
+ schedulingNum - (totalFinishedNum - Number(row.finishedNum));
+ proxy.$modal.msgWarning("鎵�鏈夋湰娆$敓浜ф暟閲忎箣鍜屼笉鍙ぇ浜庢帓浜ф暟閲�");
+ }
+ row.pendingNum = row.schedulingNum - row.finishedNum;
+ };
+ // 缂栬緫淇敼鐘舵��
+ const changeEditType = row => {
+ row.editType = !row.editType;
+ };
+ // 淇濆瓨璁板綍
+ const saveReceiptPayment = row => {
+ productionReportUpdate(row).then(res => {
+ row.editType = !row.editType;
+ getList();
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+ const summarizeMainTable = param => {
+ return proxy.summarizeTable(param, ["finishedNum"]);
+ };
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ if (selectedRows.value.length !== 1) {
+ proxy.$message.error("璇烽�夋嫨涓�鏉℃暟鎹�");
+ return;
+ }
+ if (selectedRows.value[0].pendingFinishNum == 0) {
+ proxy.$message.warning("鏃犻渶鍐嶆姤宸�");
+ return;
+ }
+ nextTick(() => {
+ const rowInfo = type === "add" ? selectedRows.value[0] : row;
+ formDia.value?.openDialog(type, rowInfo);
+ });
+ };
+
+ // 鎵撳紑鎶曞叆妯℃�佹
+ const isShowInput = ref(false);
+ const isShowingId = ref(0);
+ const showInput = row => {
+ isShowInput.value = true;
+ isShowingId.value = row.id;
+ };
+
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/productionProductMain/export", {}, "鐢熶骇鎶ュ伐.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped>
+ .unit-text {
+ margin-left: 5px;
+ color: #909399;
+ font-size: 12px;
+ }
+ .param-detail-list {
+ padding: 10px;
+ }
+ .table_list {
+ margin-top: unset;
+ }
+</style>
diff --git a/src/views/productionManagement/safetyMonitoring/index.vue b/src/views/productionManagement/safetyMonitoring/index.vue
new file mode 100644
index 0000000..12922e7
--- /dev/null
+++ b/src/views/productionManagement/safetyMonitoring/index.vue
@@ -0,0 +1,873 @@
+<template>
+ <div class="safety-monitoring">
+ <el-row :gutter="20">
+ <!-- 宸︿晶锛氬疄鏃剁洃鎺у尯鍩� -->
+ <el-col :span="16">
+ <el-card class="monitoring-card">
+ <div slot="header" class="card-header">
+ <span>瀹炴椂姘斾綋娴撳害鐩戞帶</span>
+ <el-tag :type="systemStatus === 'normal' ? 'success' : 'danger'">
+ {{ systemStatus === 'normal' ? '绯荤粺姝e父' : '绯荤粺鍛婅' }}
+ </el-tag>
+ </div>
+
+ <!-- 鍌ㄧ綈鍖虹洃鎺� -->
+ <div class="monitoring-section">
+ <h3>鍌ㄧ綈鍖虹洃鎺�</h3>
+ <div class="sensor-grid">
+ <div class="sensor-item" v-for="sensor in tankSensors" :key="sensor.id">
+ <div class="sensor-header">
+ <span>{{ sensor.name }}</span>
+ <el-tag :type="sensor.status === 'normal' ? 'success' : 'danger'" size="small">
+ {{ sensor.status === 'normal' ? '姝e父' : '瓒呮爣' }}
+ </el-tag>
+ </div>
+ <div class="sensor-data">
+ <div class="data-item">
+ <span>鐢茬兎: {{ sensor.methane.toFixed(2) }}%</span>
+ <el-progress
+ :percentage="Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)"
+ :color="getProgressColor(Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100), 80)"
+ :format="formatProgress"
+ :stroke-width="8"
+ />
+ </div>
+ <div class="data-item">
+ <span>纭寲姘�: {{ sensor.h2s.toFixed(2) }}ppm</span>
+ <el-progress
+ :percentage="Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)"
+ :color="getProgressColor(Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100), 80)"
+ :format="formatProgress"
+ :stroke-width="8"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 浜曞彛鍘嬬缉鏈虹洃鎺� -->
+ <div class="monitoring-section">
+ <h3>浜曞彛鍘嬬缉鏈虹洃鎺�</h3>
+ <div class="sensor-grid">
+ <div class="sensor-item" v-for="sensor in compressorSensors" :key="sensor.id">
+ <div class="sensor-header">
+ <span>{{ sensor.name }}</span>
+ <el-tag :type="sensor.status === 'normal' ? 'success' : 'danger'" size="small">
+ {{ sensor.status === 'normal' ? '姝e父' : '瓒呮爣' }}
+ </el-tag>
+ </div>
+ <div class="sensor-data">
+ <div class="data-item">
+ <span>鐢茬兎: {{ sensor.methane.toFixed(2) }}%</span>
+ <el-progress
+ :percentage="Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)"
+ :color="getProgressColor(sensor.methane, 2.5)"
+ :format="formatProgress"
+ :stroke-width="8"
+ />
+ </div>
+ <div class="data-item">
+ <span>纭寲姘�: {{ sensor.h2s.toFixed(2) }}ppm</span>
+ <el-progress
+ :percentage="Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)"
+ :color="getProgressColor(sensor.h2s, 10)"
+ :format="formatProgress"
+ :stroke-width="8"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 瀹炴椂鏇茬嚎鍥� -->
+ <div class="chart-section">
+ <h3>瀹炴椂娴撳害鏇茬嚎</h3>
+ <div class="chart-container">
+ <div ref="chart" class="chart"></div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+
+ <!-- 鍙充晶锛氭帶鍒堕潰鏉� -->
+ <el-col :span="8">
+ <el-card class="control-card">
+ <div slot="header" class="card-header">
+ <span>搴旀�ユ帶鍒堕潰鏉�</span>
+ </div>
+
+ <!-- 鍠锋穻鐘舵�� -->
+ <div class="control-section">
+ <h4>鍠锋穻绯荤粺鐘舵��</h4>
+ <div class="status-grid">
+ <div class="status-item" v-for="sprinkler in sprinklerSystems" :key="sprinkler.id">
+ <div class="status-indicator" :class="sprinkler.status">
+ <i class="el-icon-circle-check" v-if="sprinkler.status === 'active'"></i>
+ <i class="el-icon-circle-close" v-else></i>
+ </div>
+ <span>{{ sprinkler.name }}</span>
+ <el-tag :type="sprinkler.status === 'active' ? 'success' : 'info'" size="small">
+ {{ sprinkler.status === 'active' ? '杩愯涓�' : '寰呮満' }}
+ </el-tag>
+ </div>
+ </div>
+ </div>
+
+ <!-- 搴旀�ヨ褰曟寜閽� -->
+ <h4>搴旀�ョ鐞�</h4>
+
+ <div class="control-section1">
+ <el-button type="primary" @click="showEmergencyRecords" style="margin-bottom: 10px;">
+ 搴旀�ヨ褰�
+ </el-button>
+ <el-button type="warning" @click="triggerEmergency" :disabled="!hasEmergency">
+ 瑙﹀彂搴旀�ュ搷搴�
+ </el-button>
+ </div>
+
+ <!-- 绯荤粺鏃ュ織 -->
+ <div class="control-section">
+ <h4>绯荤粺鏃ュ織</h4>
+ <div class="log-container">
+ <div class="log-item" v-for="log in systemLogs" :key="log.id">
+ <span class="log-time">{{ log.time }}</span>
+ <span class="log-content">{{ log.content }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <!-- 娉勬紡棰勮寮圭獥 -->
+ <el-dialog
+ title="鈿狅笍 娉勬紡棰勮"
+ :visible.sync="leakWarningVisible"
+ width="500px"
+ :close-on-click-modal="false"
+ :close-on-press-escape="false"
+ class="leak-warning-dialog"
+ >
+ <div class="warning-content">
+ <div class="warning-icon">
+ <i class="el-icon-warning"></i>
+ </div>
+ <div class="warning-text">
+ <h3>妫�娴嬪埌姘斾綋娴撳害瓒呮爣锛�</h3>
+ <p>浣嶇疆锛歿{ currentWarning.location }}</p>
+ <p>瓒呮爣姘斾綋锛歿{ currentWarning.gas }}</p>
+ <p>褰撳墠娴撳害锛歿{ currentWarning.value }}</p>
+ </div>
+ </div>
+ <div slot="footer" class="dialog-footer">
+ <el-button type="danger" @click="acknowledgeWarning">纭鍛婅</el-button>
+ <el-button type="primary" @click="viewDetails">鏌ョ湅璇︽儏</el-button>
+ </div>
+ </el-dialog>
+
+ <!-- 搴旀�ヨ褰曞脊绐� -->
+ <el-dialog
+ title="搴旀�ヨ褰�"
+ :visible.sync="emergencyRecordsVisible"
+ width="800px"
+ >
+ <el-table :data="emergencyRecords" style="width: 100%">
+ <el-table-column prop="time" label="鏃堕棿" width="180"></el-table-column>
+ <el-table-column prop="location" label="浣嶇疆" width="150"></el-table-column>
+ <el-table-column prop="type" label="绫诲瀷" width="120"></el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template slot-scope="scope">
+ <el-tag :type="scope.row.status === 'resolved' ? 'success' : 'warning'">
+ {{ scope.row.status === 'resolved' ? '宸茶В鍐�' : '澶勭悊涓�' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="description" label="鎻忚堪"></el-table-column>
+ <el-table-column label="鎿嶄綔" width="120">
+ <template slot-scope="scope">
+ <el-button type="text" @click="viewBlockchainDetails(scope.row)">
+ 鍖哄潡閾捐鎯�
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+
+ <!-- 鍖哄潡閾惧瓨璇佽鎯呭脊绐� -->
+ <el-dialog
+ title="鍖哄潡閾惧瓨璇佽鎯�"
+ :visible.sync="blockchainDetailsVisible"
+ width="900px"
+ >
+ <div class="blockchain-details">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="浜嬩欢ID">{{ currentEvent.id }}</el-descriptions-item>
+ <el-descriptions-item label="鏃堕棿鎴�">{{ currentEvent.timestamp }}</el-descriptions-item>
+ <el-descriptions-item label="浣嶇疆">{{ currentEvent.location }}</el-descriptions-item>
+ <el-descriptions-item label="浜嬩欢绫诲瀷">{{ currentEvent.type }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="sensor-data-section">
+ <h4>浼犳劅鍣ㄦ暟鎹�</h4>
+ <el-table :data="currentEvent.sensorData" style="width: 100%">
+ <el-table-column prop="sensor" label="浼犳劅鍣�"></el-table-column>
+ <el-table-column prop="methane" label="鐢茬兎娴撳害"></el-table-column>
+ <el-table-column prop="h2s" label="纭寲姘㈡祿搴�"></el-table-column>
+ <el-table-column prop="timestamp" label="璁板綍鏃堕棿"></el-table-column>
+ </el-table>
+ </div>
+
+ <div class="action-log-section">
+ <h4>澶勭疆鍔ㄤ綔璁板綍</h4>
+ <el-timeline>
+ <el-timeline-item
+ v-for="action in currentEvent.actions"
+ :key="action.id"
+ :timestamp="action.timestamp"
+ :type="action.type === 'emergency' ? 'danger' : 'primary'"
+ >
+ {{ action.description }}
+ </el-timeline-item>
+ </el-timeline>
+ </div>
+
+ <div class="blockchain-info">
+ <h4>鍖哄潡閾句俊鎭�</h4>
+ <el-descriptions :column="1" border>
+ <el-descriptions-item label="鍖哄潡鍝堝笇">{{ currentEvent.blockHash }}</el-descriptions-item>
+ <el-descriptions-item label="浜ゆ槗鍝堝笇">{{ currentEvent.txHash }}</el-descriptions-item>
+ <el-descriptions-item label="纭鏁�">{{ currentEvent.confirmations }}</el-descriptions-item>
+ </el-descriptions>
+ </div>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+
+export default {
+ name: 'SafetyMonitoring',
+ data() {
+ return {
+ systemStatus: 'normal',
+ leakWarningVisible: false,
+ emergencyRecordsVisible: false,
+ blockchainDetailsVisible: false,
+ currentWarning: {},
+ currentEvent: {},
+ hasEmergency: false,
+
+ // 鍌ㄧ綈鍖轰紶鎰熷櫒鏁版嵁
+ tankSensors: [
+ { id: 1, name: '鍌ㄧ綈T-001', methane: 1.20, h2s: 2.10, status: 'normal' },
+ { id: 2, name: '鍌ㄧ綈T-002', methane: 0.80, h2s: 1.50, status: 'normal' },
+ { id: 3, name: '鍌ㄧ綈T-003', methane: 3.20, h2s: 8.50, status: 'warning' },
+ { id: 4, name: '鍌ㄧ綈T-004', methane: 0.60, h2s: 0.80, status: 'normal' }
+ ],
+
+ // 浜曞彛鍘嬬缉鏈轰紶鎰熷櫒鏁版嵁
+ compressorSensors: [
+ { id: 5, name: '鍘嬬缉鏈篊-001', methane: 2.10, h2s: 3.20, status: 'normal' },
+ { id: 6, name: '鍘嬬缉鏈篊-002', methane: 4.80, h2s: 12.50, status: 'warning' },
+ { id: 7, name: '鍘嬬缉鏈篊-003', methane: 1.80, h2s: 2.80, status: 'normal' }
+ ],
+
+ // 鍠锋穻绯荤粺鐘舵��
+ sprinklerSystems: [
+ { id: 1, name: '鍌ㄧ綈鍖哄柗娣�', status: 'active' },
+ { id: 2, name: '鍘嬬缉鏈哄尯鍠锋穻', status: 'standby' },
+ { id: 3, name: '绱ф�ュ柗娣�', status: 'standby' }
+ ],
+
+ // 绯荤粺鏃ュ織
+ systemLogs: [
+ { id: 1, time: '14:30:25', content: '绯荤粺鍚姩瀹屾垚锛屾墍鏈変紶鎰熷櫒姝e父' },
+ { id: 2, time: '14:35:12', content: '鍌ㄧ綈T-003鐢茬兎娴撳害瓒呮爣锛岃Е鍙戦璀�' },
+ { id: 3, time: '14:35:15', content: '鍚姩鍌ㄧ綈鍖哄柗娣嬬郴缁�' },
+ { id: 4, time: '14:35:20', content: '鍙戦�佺揣鎬ョ枏鏁e箍鎾�' }
+ ],
+
+ // 搴旀�ヨ褰�
+ emergencyRecords: [
+ {
+ id: 'EM001',
+ time: '2025-01-15 14:35:12',
+ location: '鍌ㄧ綈T-003',
+ type: '鐢茬兎瓒呮爣',
+ status: 'resolved',
+ description: '鍌ㄧ綈T-003鐢茬兎娴撳害杈惧埌3.2%锛岃秴杩囧畨鍏ㄩ槇鍊�2.5%'
+ },
+ {
+ id: 'EM002',
+ time: '2025-01-15 14:35:15',
+ location: '鍘嬬缉鏈篊-002',
+ type: '纭寲姘㈣秴鏍�',
+ status: 'processing',
+ description: '鍘嬬缉鏈篊-002纭寲姘㈡祿搴﹁揪鍒�12.5ppm锛岃秴杩囧畨鍏ㄩ槇鍊�10ppm'
+ }
+ ],
+
+ // 鍥捐〃瀹炰緥
+ chart: null,
+
+ // 瀹氭椂鍣�
+ timer: null
+ }
+ },
+
+ mounted() {
+ this.initChart()
+ this.startDataRefresh()
+ this.checkEmergencyStatus()
+ },
+
+ beforeDestroy() {
+ if (this.timer) {
+ clearInterval(this.timer)
+ }
+ if (this.chart) {
+ this.chart.dispose()
+ }
+ },
+
+ methods: {
+ // 缁熶竴杩涘害鏉℃牸寮忓寲涓轰袱浣嶅皬鏁帮紝閬垮厤娴偣璇樊鏄剧ず
+ formatProgress(percentage) {
+ if (percentage == null || isNaN(percentage)) return '0.00%'
+ const val = Math.round(Number(percentage) * 100) / 100
+ return `${val.toFixed(2)}%`
+ },
+ // 鍒濆鍖栧浘琛�
+ initChart() {
+ this.chart = echarts.init(this.$refs.chart)
+ this.updateChart()
+ },
+
+ // 鏇存柊鍥捐〃鏁版嵁
+ updateChart() {
+ const option = {
+ title: {
+ text: '瀹炴椂姘斾綋娴撳害鐩戞帶',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross'
+ }
+ },
+ legend: {
+ data: ['鍌ㄧ綈鍖虹敳鐑�', '鍌ㄧ綈鍖虹~鍖栨阿', '鍘嬬缉鏈虹敳鐑�', '鍘嬬缉鏈虹~鍖栨阿'],
+ top: 30
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ top: '15%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ data: this.generateTimeData()
+ },
+ yAxis: [
+ {
+ type: 'value',
+ name: '鐢茬兎娴撳害(%)',
+ position: 'left'
+ },
+ {
+ type: 'value',
+ name: '纭寲姘㈡祿搴�(ppm)',
+ position: 'right'
+ }
+ ],
+ series: [
+ {
+ name: '鍌ㄧ綈鍖虹敳鐑�',
+ type: 'line',
+ data: this.generateRandomData(20, 0.5, 3.5),
+ smooth: true,
+ yAxisIndex: 0
+ },
+ {
+ name: '鍌ㄧ綈鍖虹~鍖栨阿',
+ type: 'line',
+ data: this.generateRandomData(20, 0.5, 12),
+ smooth: true,
+ yAxisIndex: 1
+ },
+ {
+ name: '鍘嬬缉鏈虹敳鐑�',
+ type: 'line',
+ data: this.generateRandomData(20, 1.0, 5.0),
+ smooth: true,
+ yAxisIndex: 0
+ },
+ {
+ name: '鍘嬬缉鏈虹~鍖栨阿',
+ type: 'line',
+ data: this.generateRandomData(20, 1.0, 15),
+ smooth: true,
+ yAxisIndex: 1
+ }
+ ]
+ }
+
+ this.chart.setOption(option)
+ },
+
+ // 鐢熸垚鏃堕棿鏁版嵁
+ generateTimeData() {
+ const times = []
+ const now = new Date()
+ for (let i = 19; i >= 0; i--) {
+ const time = new Date(now.getTime() - i * 5 * 60 * 1000)
+ times.push(time.toLocaleTimeString('zh-CN', { hour12: false }))
+ }
+ return times
+ },
+
+ // 鐢熸垚闅忔満鏁版嵁
+ generateRandomData(count, min, max) {
+ const data = []
+ for (let i = 0; i < count; i++) {
+ data.push(+(Math.random() * (max - min) + min).toFixed(2))
+ }
+ return data
+ },
+
+ // 寮�濮嬫暟鎹埛鏂�
+ startDataRefresh() {
+ this.timer = setInterval(() => {
+ this.refreshSensorData()
+ this.updateChart()
+ this.checkEmergencyStatus()
+ }, 5000) // 姣�5绉掑埛鏂颁竴娆�
+ },
+
+ // 鍒锋柊浼犳劅鍣ㄦ暟鎹�
+ refreshSensorData() {
+ // 鏇存柊鍌ㄧ綈鍖轰紶鎰熷櫒鏁版嵁
+ this.tankSensors.forEach(sensor => {
+ sensor.methane = +(Math.random() * 4).toFixed(2)
+ sensor.h2s = +(Math.random() * 15).toFixed(2)
+ sensor.status = this.getSensorStatus(sensor.methane, sensor.h2s)
+ })
+
+ // 鏇存柊鍘嬬缉鏈轰紶鎰熷櫒鏁版嵁
+ this.compressorSensors.forEach(sensor => {
+ sensor.methane = +(Math.random() * 6).toFixed(2)
+ sensor.h2s = +(Math.random() * 20).toFixed(2)
+ sensor.status = this.getSensorStatus(sensor.methane, sensor.h2s)
+ })
+
+ // 妫�鏌ユ槸鍚﹂渶瑕佽Е鍙戦璀�
+ this.checkLeakWarning()
+ },
+
+ // 鑾峰彇浼犳劅鍣ㄧ姸鎬�
+ getSensorStatus(methane, h2s) {
+ const methanePct = Math.min(Math.round(methane * 40 * 100) / 100, 100)
+ const h2sPct = Math.min(Math.round((h2s / 20) * 100 * 100) / 100, 100)
+ if (methanePct >= 80 || h2sPct >= 80) {
+ return 'warning'
+ }
+ return 'normal'
+ },
+
+ // 妫�鏌ユ硠婕忛璀�
+ checkLeakWarning() {
+ const allSensors = [...this.tankSensors, ...this.compressorSensors]
+ const warningSensor = allSensors.find(sensor => this.getSensorStatus(sensor.methane, sensor.h2s) === 'warning')
+
+ if (warningSensor && !this.leakWarningVisible) {
+ this.triggerLeakWarning(warningSensor)
+ }
+ },
+
+ // 瑙﹀彂娉勬紡棰勮
+ triggerLeakWarning(sensor) {
+ const methanePct = Math.min(Math.round(sensor.methane * 40 * 100) / 100, 100)
+ const h2sPct = Math.min(Math.round((sensor.h2s / 20) * 100 * 100) / 100, 100)
+ const isMethaneMajor = methanePct >= h2sPct
+ const overGas = isMethaneMajor ? '鐢茬兎' : '纭寲姘�'
+ const percent = (isMethaneMajor ? methanePct : h2sPct).toFixed(2)
+ this.currentWarning = {
+ location: sensor.name,
+ gas: overGas,
+ value: `${percent}%`
+ }
+
+ this.leakWarningVisible = true
+ this.hasEmergency = true
+
+ // 鑷姩瑙﹀彂搴旀�ュ搷搴�
+ this.autoEmergencyResponse(sensor)
+
+ // 娣诲姞绯荤粺鏃ュ織
+ this.addSystemLog(`妫�娴嬪埌${sensor.name}姘斾綋娴撳害瓒呮爣锛岃Е鍙戞硠婕忛璀)
+ },
+
+ // 鑷姩搴旀�ュ搷搴�
+ autoEmergencyResponse(sensor) {
+ // 鍚姩鍠锋穻绯荤粺
+ if (sensor.name.includes('鍌ㄧ綈')) {
+ this.sprinklerSystems[0].status = 'active'
+ } else if (sensor.name.includes('鍘嬬缉鏈�')) {
+ this.sprinklerSystems[1].status = 'active'
+ }
+
+ // 娣诲姞绯荤粺鏃ュ織
+ this.addSystemLog(`鍚姩${sensor.name}鍖哄煙鍠锋穻绯荤粺`)
+ this.addSystemLog(`鍙戦�佺揣鎬ョ枏鏁e箍鎾璥)
+
+ // 鍒涘缓搴旀�ヨ褰�
+ this.createEmergencyRecord(sensor)
+ },
+
+ // 娣诲姞绯荤粺鏃ュ織
+ addSystemLog(content) {
+ const now = new Date()
+ const time = now.toLocaleTimeString('zh-CN', { hour12: false })
+
+ this.systemLogs.unshift({
+ id: Date.now(),
+ time: time,
+ content: content
+ })
+
+ // 淇濇寔鏈�澶�20鏉℃棩蹇�
+ if (this.systemLogs.length > 20) {
+ this.systemLogs = this.systemLogs.slice(0, 20)
+ }
+ },
+
+ // 鍒涘缓搴旀�ヨ褰�
+ createEmergencyRecord(sensor) {
+ const now = new Date()
+ const record = {
+ id: `EM${Date.now()}`,
+ time: now.toLocaleString('zh-CN'),
+ location: sensor.name,
+ type: sensor.methane > 2.5 ? '鐢茬兎瓒呮爣' : '纭寲姘㈣秴鏍�',
+ status: 'processing',
+ description: `${sensor.name}妫�娴嬪埌${sensor.methane > 2.5 ? '鐢茬兎' : '纭寲姘�'}娴撳害瓒呮爣`
+ }
+
+ this.emergencyRecords.unshift(record)
+ },
+
+ // 鑾峰彇杩涘害鏉¢鑹�
+ getProgressColor(value, threshold) {
+ if (value > threshold) {
+ return '#F56C6C'
+ } else if (value > threshold * 0.8) {
+ return '#E6A23C'
+ }
+ return '#67C23A'
+ },
+
+ // 妫�鏌ュ簲鎬ョ姸鎬�
+ checkEmergencyStatus() {
+ const allSensors = [...this.tankSensors, ...this.compressorSensors]
+ const has = allSensors.some(sensor => this.getSensorStatus(sensor.methane, sensor.h2s) === 'warning')
+ this.hasEmergency = has
+ this.systemStatus = has ? 'warning' : 'normal'
+ },
+
+ // 纭鍛婅
+ acknowledgeWarning() {
+ this.leakWarningVisible = false
+ this.addSystemLog('娉勬紡棰勮宸茬‘璁�')
+ },
+
+ // 鏌ョ湅璇︽儏
+ viewDetails() {
+ this.leakWarningVisible = false
+ // 杩欓噷鍙互璺宠浆鍒拌缁嗛〉闈㈡垨鏄剧ず鏇村淇℃伅
+ },
+
+ // 鏄剧ず搴旀�ヨ褰�
+ showEmergencyRecords() {
+ this.emergencyRecordsVisible = true
+ },
+
+ // 鏌ョ湅鍖哄潡閾捐鎯�
+ viewBlockchainDetails(record) {
+ this.currentEvent = {
+ id: record.id,
+ timestamp: record.time,
+ location: record.location,
+ type: record.type,
+ sensorData: [
+ {
+ sensor: '鐢茬兎浼犳劅鍣�',
+ methane: '3.2%',
+ h2s: '8.5ppm',
+ timestamp: record.time
+ },
+ {
+ sensor: '纭寲姘紶鎰熷櫒',
+ methane: '2.8%',
+ h2s: '12.5ppm',
+ timestamp: record.time
+ }
+ ],
+ actions: [
+ {
+ id: 1,
+ timestamp: record.time,
+ type: 'emergency',
+ description: '妫�娴嬪埌姘斾綋娴撳害瓒呮爣锛岃Е鍙戦璀�'
+ },
+ {
+ id: 2,
+ timestamp: new Date(new Date(record.time).getTime() + 3000).toLocaleString('zh-CN'),
+ type: 'action',
+ description: '鍚姩鍠锋穻绯荤粺闄嶆俯'
+ },
+ {
+ id: 3,
+ timestamp: new Date(new Date(record.time).getTime() + 5000).toLocaleString('zh-CN'),
+ type: 'action',
+ description: '鍙戦�佺揣鎬ョ枏鏁e箍鎾�'
+ }
+ ],
+ blockHash: '0x1234567890abcdef...',
+ txHash: '0xabcdef1234567890...',
+ confirmations: 12
+ }
+
+ this.emergencyRecordsVisible = false
+ this.blockchainDetailsVisible = true
+ },
+
+ // 瑙﹀彂搴旀�ュ搷搴�
+ triggerEmergency() {
+ this.$message.success('搴旀�ュ搷搴斿凡瑙﹀彂')
+ this.addSystemLog('鎵嬪姩瑙﹀彂搴旀�ュ搷搴�')
+ }
+ }
+}
+</script>
+
+<style scoped>
+.safety-monitoring {
+ padding: 20px;
+ background-color: #f5f7fa;
+ min-height: calc(100vh - 84px);
+}
+
+.monitoring-card, .control-card {
+ margin-bottom: 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.monitoring-section {
+ margin-bottom: 30px;
+}
+
+.monitoring-section h3 {
+ color: #303133;
+ margin-bottom: 15px;
+ padding-bottom: 8px;
+ border-bottom: 2px solid #409EFF;
+}
+
+.sensor-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ gap: 15px;
+}
+
+.sensor-item {
+ background: #fff;
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ padding: 15px;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.sensor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ font-weight: bold;
+}
+
+.sensor-data .data-item {
+ margin-bottom: 12px;
+}
+
+.sensor-data .data-item span {
+ display: block;
+ margin-bottom: 5px;
+ font-size: 14px;
+ color: #606266;
+}
+
+.chart-section {
+ margin-top: 30px;
+}
+
+.chart-section h3 {
+ color: #303133;
+ margin-bottom: 15px;
+ padding-bottom: 8px;
+ border-bottom: 2px solid #409EFF;
+}
+
+.chart-container {
+ background: #fff;
+ border-radius: 8px;
+ padding: 20px;
+}
+
+.chart {
+ width: 100%;
+ height: 400px;
+}
+
+.control-section {
+ margin-bottom: 25px;
+}
+.control-section1 {
+ display: flex;
+}
+
+.control-section h4 {
+ color: #303133;
+ margin-bottom: 15px;
+ font-size: 16px;
+}
+
+.status-grid {
+ display: grid;
+ gap: 10px;
+}
+
+.status-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px;
+ background: #f8f9fa;
+ border-radius: 6px;
+}
+
+.status-indicator {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.status-indicator.active {
+ color: #67C23A;
+}
+
+.status-indicator.standby {
+ color: #909399;
+}
+
+.log-container {
+ max-height: 200px;
+ overflow-y: auto;
+ background: #f8f9fa;
+ border-radius: 6px;
+ padding: 10px;
+}
+
+.log-item {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 8px;
+ font-size: 12px;
+}
+
+.log-time {
+ color: #909399;
+ min-width: 60px;
+}
+
+.log-content {
+ color: #606266;
+}
+
+/* 娉勬紡棰勮寮圭獥鏍峰紡 */
+.leak-warning-dialog {
+ background: #fff5f5;
+}
+
+.warning-content {
+ text-align: center;
+ padding: 20px 0;
+}
+
+.warning-icon {
+ font-size: 60px;
+ color: #F56C6C;
+ margin-bottom: 20px;
+}
+
+.warning-text h3 {
+ color: #F56C6C;
+ margin-bottom: 15px;
+}
+
+.warning-text p {
+ margin: 8px 0;
+ color: #606266;
+}
+
+/* 鍖哄潡閾捐鎯呮牱寮� */
+.blockchain-details {
+ padding: 20px 0;
+}
+
+.sensor-data-section, .action-log-section, .blockchain-info {
+ margin-top: 25px;
+}
+
+.sensor-data-section h4, .action-log-section h4, .blockchain-info h4 {
+ color: #303133;
+ margin-bottom: 15px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid #e4e7ed;
+}
+
+/* 鍝嶅簲寮忚璁� */
+@media (max-width: 1200px) {
+ .sensor-grid {
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ }
+}
+
+@media (max-width: 768px) {
+ .safety-monitoring {
+ padding: 10px;
+ }
+
+ .sensor-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .chart {
+ height: 300px;
+ }
+}
+</style>
diff --git a/src/views/productionManagement/workOrder/components/filesDia.vue b/src/views/productionManagement/workOrder/components/filesDia.vue
new file mode 100644
index 0000000..1336861
--- /dev/null
+++ b/src/views/productionManagement/workOrder/components/filesDia.vue
@@ -0,0 +1,202 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogVisible" title="宸ュ崟闄勪欢" width="50%" @close="closeDia">
+ <div style="margin-bottom: 10px; text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :before-upload="beforeUpload"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ accept="image/*"
+ style="display: inline; margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶鍥剧墖</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :total="page.total"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="paginationSearch"
+ height="500"
+ />
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { ElMessageBox } from "element-plus";
+import { getToken } from "@/utils/auth.js";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import filePreview from "@/components/filePreview/index.vue";
+import {
+ productWorkOrderFileAdd,
+ productWorkOrderFileDel,
+ productWorkOrderFileListPage,
+} from "@/api/productionManagement/productWorkOrderFile.js";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(["close"]);
+
+const dialogVisible = ref(false);
+const currentWorkOrderId = ref("");
+const selectedRows = ref([]);
+const filePreviewRef = ref();
+
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ width: 120,
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: row => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: row => {
+ filePreviewRef.value?.open(row.url);
+ },
+ },
+ ],
+ },
+]);
+
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload");
+
+const beforeUpload = file => {
+ const isImage = file?.type?.startsWith("image/");
+ if (!isImage) {
+ proxy.$modal.msgError("鍙兘涓婁紶鍥剧墖鏂囦欢");
+ }
+ return isImage;
+};
+
+const openDialog = row => {
+ dialogVisible.value = true;
+ currentWorkOrderId.value = row.id;
+ page.current = 1;
+ getList();
+};
+
+const closeDia = () => {
+ dialogVisible.value = false;
+ emit("close");
+};
+
+const paginationSearch = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ productWorkOrderFileListPage({
+ workOrderId: currentWorkOrderId.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then(res => {
+ tableData.value = res.data.records || [];
+ page.total = res.data.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+};
+
+function handleUploadSuccess(res) {
+ if (res.code == 200) {
+ const fileRow = {
+ name: res.data.originalName,
+ url: res.data.tempPath,
+ workOrderId: currentWorkOrderId.value,
+ };
+ productWorkOrderFileAdd(fileRow).then(() => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList();
+ });
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+
+const handleDelete = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ productWorkOrderFileDel(ids).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped></style>
+
diff --git a/src/views/productionManagement/workOrder/index.vue b/src/views/productionManagement/workOrder/index.vue
new file mode 100644
index 0000000..8c76b2e
--- /dev/null
+++ b/src/views/productionManagement/workOrder/index.vue
@@ -0,0 +1,874 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div class="search-row">
+ <div class="search-item">
+ <span class="search_title">宸ュ崟缂栧彿锛�</span>
+ <el-input v-model="searchForm.workOrderNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
+ </div>
+ <div class="search-item">
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
+ </div>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination">
+ <template #completionStatus="{ row }">
+ <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
+ :color="progressColor(toProgressPercentage(row?.completionStatus))"
+ :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
+ </template>
+ </PIMTable>
+ </div>
+ <el-dialog v-model="editDialogVisible"
+ title="缂栬緫鏃堕棿"
+ width="500px">
+ <el-form :model="editrow"
+ label-width="120px">
+ <el-form-item label="璁″垝寮�濮嬫椂闂�">
+ <el-date-picker v-model="editrow.planStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
+ </el-form-item>
+ <el-form-item label="璁″垝缁撴潫鏃堕棿">
+ <el-date-picker v-model="editrow.planEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
+ </el-form-item>
+ <el-form-item label="瀹為檯寮�濮嬫椂闂�">
+ <el-date-picker v-model="editrow.actualStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
+ </el-form-item>
+ <el-form-item label="瀹為檯缁撴潫鏃堕棿">
+ <el-date-picker v-model="editrow.actualEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleUpdate">纭畾</el-button>
+ <el-button @click="editDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <el-dialog v-model="transferCardVisible"
+ title="娴佽浆鍗�"
+ width="1000px">
+ <div class="transfer-card-title">宸ュ崟娴佽浆鍗�</div>
+ <div class="transfer-card-container">
+ <div class="transfer-card-info">
+ <div class="info-group">
+ <div class="info-item">
+ <span class="info-label">宸ュ崟缂栧彿</span>
+ <span class="info-value">{{ transferCardRowData.workOrderNo }}</span>
+ </div>
+ <!-- <div class="info-item">
+ <span class="info-label">浜у搧缂栧彿</span>
+ <span class="info-value">{{ transferCardRowData.productNo }}</span>
+ </div> -->
+ <div class="info-item">
+ <span class="info-label">浜у搧鍚嶇О</span>
+ <span class="info-value">{{ transferCardRowData.productName }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">瑙勬牸鍨嬪彿</span>
+ <span class="info-value">{{ transferCardRowData.model }}</span>
+ </div>
+ <!-- <div class="info-item">
+ <span class="info-label">宸ュ崟鐘舵��</span>
+ <span class="info-value">{{
+ transferCardRowData.status === 1 ? '寰呯‘璁�' :
+ transferCardRowData.status === 2 ? '寰呯敓浜�' :
+ transferCardRowData.status === 3 ? '鐢熶骇涓�' :
+ transferCardRowData.status === 4 ? '宸茬敓浜�' :
+ transferCardRowData.status
+ }}</span>
+ </div> -->
+ <div class="info-item">
+ <span class="info-label">璁″垝寮�濮嬫椂闂�</span>
+ <span class="info-value">{{ transferCardRowData.planStartTime }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">璁″垝缁撴潫鏃堕棿</span>
+ <span class="info-value">{{ transferCardRowData.planEndTime }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">澶囨敞</span>
+ <span class="info-value">{{ transferCardRowData.remark }}</span>
+ </div>
+ </div>
+ <div class="info-group">
+ <div class="info-item">
+ <span class="info-label">闇�姹傛暟閲�</span>
+ <span class="info-value">{{ transferCardRowData.planQuantity }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">瀹屾垚鏁伴噺</span>
+ <span class="info-value">{{ transferCardRowData.completeQuantity }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鑹搧鏁伴噺</span>
+ <span class="info-value">0</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">涓嶈壇鍝佹暟</span>
+ <span class="info-value">0</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">瀹為檯寮�濮嬫椂闂�</span>
+ <span class="info-value">{{ transferCardRowData.actualStartTime }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">瀹為檯缁撴潫鏃堕棿</span>
+ <span class="info-value">{{ transferCardRowData.actualEndTime }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="transfer-card-qr">
+ <div class="qr-container">
+ <img :src="transferCardQrUrl"
+ alt="娴佽浆鍗′簩缁寸爜"
+ style="width: 200px; height: 200px;" />
+ <!-- <div class="qr-tip"
+ style="margin-top: 10px; text-align: center;">娴佽浆鍗′簩缁寸爜</div> -->
+ </div>
+ </div>
+ </div>
+ <div class="print-button-container"
+ style=" text-align: center;
+ margin-bottom: 40px;">
+ <el-button type="primary"
+ style="margin-top: 20px;"
+ @click="printTransferCard">鎵撳嵃娴佽浆鍗�</el-button>
+ </div>
+ </el-dialog>
+ <el-dialog v-model="reportDialogVisible"
+ title="鎶ュ伐"
+ width="500px">
+ <el-form ref="reportFormRef"
+ :model="reportForm"
+ :rules="reportFormRules"
+ label-width="120px">
+ <el-form-item label="寰呯敓浜ф暟閲�">
+ <el-input v-model="reportForm.planQuantity"
+ readonly
+ style="width: 300px" />
+ </el-form-item>
+ <el-form-item label="鏈鐢熶骇鏁伴噺"
+ prop="quantity">
+ <el-input v-model.number="reportForm.quantity"
+ type="number"
+ min="1"
+ step="1"
+ style="width: 300px"
+ placeholder="璇疯緭鍏ユ湰娆$敓浜ф暟閲�"
+ @input="handleQuantityInput" />
+ </el-form-item>
+ <el-form-item label="鎶ュ簾鏁伴噺"
+ prop="scrapQty">
+ <el-input v-model.number="reportForm.scrapQty"
+ type="number"
+ min="0"
+ step="1"
+ style="width: 300px"
+ placeholder="璇疯緭鍏ユ姤搴熸暟閲�"
+ @input="handleScrapQtyInput" />
+ </el-form-item>
+ <el-form-item label="鐝粍淇℃伅">
+ <el-select v-model="reportForm.userId"
+ style="width: 300px"
+ placeholder="璇烽�夋嫨鐝粍淇℃伅"
+ clearable
+ filterable
+ @change="handleUserChange">
+ <el-option v-for="user in userOptions"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleReport">纭畾</el-button>
+ <el-button @click="reportDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <FilesDia ref="workOrderFilesRef" />
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref, nextTick } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import dayjs from "dayjs";
+ import {
+ productWorkOrderPage,
+ updateProductWorkOrder,
+ addProductMain,
+ downProductWorkOrder,
+ } from "@/api/productionManagement/workOrder.js";
+ import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+ import QRCode from "qrcode";
+ import { getCurrentInstance, reactive, toRefs } from "vue";
+ import FilesDia from "./components/filesDia.vue";
+ const { proxy } = getCurrentInstance();
+
+ const tableColumn = ref([
+ {
+ label: "宸ュ崟绫诲瀷",
+ prop: "workOrderType",
+ width: "80",
+ },
+ {
+ label: "宸ュ崟缂栧彿",
+ prop: "workOrderNo",
+ width: "140",
+ },
+ {
+ label: "鐢熶骇璁㈠崟鍙�",
+ prop: "productOrderNpsNo",
+ width: "140",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: "140",
+ },
+ {
+ label: "瑙勬牸",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "宸ュ簭鍚嶇О",
+ prop: "processName",
+ },
+ {
+ label: "闇�姹傛暟閲�",
+ prop: "planQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚鏁伴噺",
+ prop: "completeQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚杩涘害",
+ prop: "completionStatus",
+ dataType: "slot",
+ slot: "completionStatus",
+ width: "140",
+ },
+ {
+ label: "璁″垝寮�濮嬫椂闂�",
+ prop: "planStartTime",
+ width: "140",
+ },
+ {
+ label: "璁″垝缁撴潫鏃堕棿",
+ prop: "planEndTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯寮�濮嬫椂闂�",
+ prop: "actualStartTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯缁撴潫鏃堕棿",
+ prop: "actualEndTime",
+ width: "140",
+ },
+ {
+ label: "鎿嶄綔",
+ width: "200",
+ align: "center",
+ dataType: "action",
+ fixed: "right",
+ operation: [
+ {
+ name: "缂栬緫",
+ clickFun: row => {
+ handleEdit(row);
+ },
+ },
+ {
+ name: "娴佽浆鍗�",
+ clickFun: row => {
+ downloadAndPrintWorkOrder(row);
+ },
+ },
+ {
+ name: "闄勪欢",
+ clickFun: row => {
+ openWorkOrderFiles(row);
+ },
+ },
+ {
+ name: "鎶ュ伐",
+ clickFun: row => {
+ showReportDialog(row);
+ },
+ disabled: row => row.planQuantity <= 0,
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const qrCodeUrl = ref("");
+ const qrRowData = ref(null);
+ const editDialogVisible = ref(false);
+ const transferCardVisible = ref(false);
+ const transferCardData = ref([]);
+ const transferCardQrUrl = ref("");
+ const transferCardRowData = ref(null);
+ const reportDialogVisible = ref(false);
+ const workOrderFilesRef = ref(null);
+ const reportFormRef = ref(null);
+ const userOptions = ref([]);
+ const reportForm = reactive({
+ planQuantity: 0,
+ quantity: null,
+ scrapQty: null,
+ userName: "",
+ workOrderId: "",
+ reportWork: "",
+ productProcessRouteItemId: "",
+ userId: "",
+ productMainId: null,
+ });
+
+ // 鏈鐢熶骇鏁伴噺楠岃瘉瑙勫垯
+ const validateQuantity = (rule, value, callback) => {
+ if (value === null || value === undefined || value === "") {
+ callback(new Error("璇疯緭鍏ユ湰娆$敓浜ф暟閲�"));
+ return;
+ }
+ const num = Number(value);
+ // 鏁存暟涓斿ぇ浜庣瓑浜�1
+ if (isNaN(num) || !Number.isInteger(num) || num < 1) {
+ callback(new Error("鏈鐢熶骇鏁伴噺蹇呴』澶т簬绛変簬1"));
+ return;
+ }
+ callback();
+ };
+
+ // 鎶ュ簾鏁伴噺楠岃瘉瑙勫垯
+ const validateScrapQty = (rule, value, callback) => {
+ if (value === null || value === undefined || value === "") {
+ callback();
+ return;
+ }
+ const num = Number(value);
+ // 鏁存暟涓斿ぇ浜庣瓑浜�0
+ if (isNaN(num) || !Number.isInteger(num) || num < 0) {
+ callback(new Error("鎶ュ簾鏁伴噺蹇呴』澶т簬绛変簬0"));
+ return;
+ }
+ callback();
+ };
+
+ // 楠岃瘉瑙勫垯
+ const reportFormRules = {
+ quantity: [{ required: true, validator: validateQuantity, trigger: "blur" }],
+ scrapQty: [{ validator: validateScrapQty, trigger: "blur" }],
+ };
+
+ // 澶勭悊鏈鐢熶骇鏁伴噺杈撳叆锛岄檺鍒跺繀椤诲ぇ浜庣瓑浜�1
+ const handleQuantityInput = value => {
+ if (value === "" || value === null || value === undefined) {
+ reportForm.quantity = null;
+ return;
+ }
+ const num = Number(value);
+ if (isNaN(num)) {
+ return;
+ }
+ // 濡傛灉灏忎簬1锛屾竻闄�
+ if (num < 1) {
+ reportForm.quantity = null;
+ return;
+ }
+ // 濡傛灉鏄皬鏁板彇鏁存暟閮ㄥ垎
+ if (!Number.isInteger(num)) {
+ const intValue = Math.floor(num);
+ // 濡傛灉鍙栨暣鍚庡皬浜�1锛屾竻闄�
+ if (intValue < 1) {
+ reportForm.quantity = null;
+ return;
+ }
+ reportForm.quantity = intValue;
+ return;
+ }
+ reportForm.quantity = num;
+ };
+
+ // 澶勭悊鎶ュ簾鏁伴噺
+ const handleScrapQtyInput = value => {
+ if (value === "" || value === null || value === undefined) {
+ reportForm.scrapQty = null;
+ return;
+ }
+ const num = Number(value);
+ // 濡傛灉鏄疦aN锛屼繚鎸佸師鍊�
+ if (isNaN(num)) {
+ return;
+ }
+ // 濡傛灉鏄礋鏁帮紝娓呴櫎杈撳叆
+ if (num < 0) {
+ reportForm.scrapQty = null;
+ return;
+ }
+ // 濡傛灉鏄皬鏁帮紝鍙栨暣鏁伴儴鍒�
+ if (!Number.isInteger(num)) {
+ reportForm.scrapQty = Math.floor(num);
+ return;
+ }
+ // 鏈夋晥鐨勯潪璐熸暣鏁帮紙鍖呮嫭0锛�
+ reportForm.scrapQty = num;
+ };
+ const currentReportRowData = ref(null);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+
+ const data = reactive({
+ searchForm: {
+ workOrderNo: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
+ let editrow = ref(null);
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ productWorkOrderPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 涓嬭浇骞舵墦鍗板伐鍗曟祦杞崱锛堟枃浠舵祦锛�
+ const downloadAndPrintWorkOrder = async row => {
+ if (!row || !row.id) {
+ proxy.$modal.msgError("缂哄皯宸ュ崟ID锛屾棤娉曚笅杞芥祦杞崱");
+ return;
+ }
+ const fileName = row.workOrderNo
+ ? `宸ュ崟娴佽浆鍗${row.workOrderNo}.xlsx`
+ : "宸ュ崟娴佽浆鍗�.xlsx";
+ try {
+ // 璋冪敤鎺ュ彛锛屼互 responseType: 'blob' 鑾峰彇鏂囦欢娴�
+ const blob = await downProductWorkOrder(row.id);
+
+ if (!blob) {
+ proxy.$modal.msgError("鏈幏鍙栧埌娴佽浆鍗℃枃浠�");
+ return;
+ }
+
+ // 鍒涘缓 Blob URL
+ const fileBlob =
+ blob instanceof Blob
+ ? blob
+ : new Blob([blob], { type: blob.type || "application/octet-stream" });
+ const url = window.URL.createObjectURL(fileBlob);
+
+ // 鍒涘缓闅愯棌 iframe锛岀敤浜庤Е鍙戞祻瑙堝櫒鎵撳嵃
+ const iframe = document.createElement("iframe");
+ iframe.style.position = "fixed";
+ iframe.style.right = "0";
+ iframe.style.bottom = "0";
+ iframe.style.width = "0";
+ iframe.style.height = "0";
+ iframe.style.border = "0";
+ iframe.src = url;
+ document.body.appendChild(iframe);
+
+ iframe.onload = () => {
+ try {
+ iframe.contentWindow?.focus();
+ iframe.contentWindow?.print();
+ } catch (e) {
+ console.error("鑷姩璋冪敤鎵撳嵃澶辫触", e);
+ // 閫�鑰屾眰鍏舵锛屾墦寮�鏂扮獥鍙g敱鐢ㄦ埛鎵嬪姩鎵撳嵃
+ window.open(url);
+ }
+ };
+ } catch (e) {
+ console.error("涓嬭浇宸ュ崟娴佽浆鍗″け璐�", e);
+ proxy.$modal.msgError("涓嬭浇宸ュ崟娴佽浆鍗″け璐�");
+ }
+ };
+
+ const showTransferCard = async row => {
+ transferCardRowData.value = row;
+ const qrContent = String(row.id);
+
+ transferCardQrUrl.value = await QRCode.toDataURL(qrContent);
+ transferCardVisible.value = true;
+ };
+
+ const printTransferCard = () => {
+ window.print();
+ };
+
+ const handleEdit = row => {
+ editrow.value = JSON.parse(JSON.stringify(row));
+ editDialogVisible.value = true;
+ };
+
+ const handleUpdate = () => {
+ updateProductWorkOrder(editrow.value)
+ .then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ editDialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ ElMessageBox.alert("淇敼澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ });
+ };
+
+ const showReportDialog = row => {
+ currentReportRowData.value = row;
+ reportForm.planQuantity = row.planQuantity - row.completeQuantity;
+ reportForm.quantity =
+ row.quantity !== undefined && row.quantity !== null ? row.quantity : null;
+ reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
+ reportForm.workOrderId = row.id;
+ reportForm.reportWork = row.reportWork;
+ reportForm.productMainId = row.productMainId;
+ reportForm.scrapQty =
+ row.scrapQty !== undefined && row.scrapQty !== null ? row.scrapQty : null;
+ nextTick(() => {
+ reportFormRef.value?.clearValidate();
+ });
+ // 鑾峰彇褰撳墠鐧诲綍鐢ㄦ埛淇℃伅锛岃缃负榛樿閫変腑
+ getUserProfile()
+ .then(res => {
+ if (res.code === 200) {
+ reportForm.userId = res.data.userId;
+ reportForm.userName = res.data.nickName;
+ }
+ })
+ .catch(err => {
+ console.error("鑾峰彇鐢ㄦ埛淇℃伅澶辫触", err);
+ });
+
+ reportDialogVisible.value = true;
+ };
+
+ const openWorkOrderFiles = row => {
+ workOrderFilesRef.value?.openDialog(row);
+ };
+
+ const handleReport = () => {
+ reportFormRef.value?.validate(valid => {
+ if (!valid) {
+ return false;
+ }
+
+ if (reportForm.planQuantity <= 0) {
+ ElMessageBox.alert("寰呯敓浜ф暟閲忎负0锛屾棤娉曟姤宸�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ // 楠岃瘉鏈鐢熶骇鏁伴噺
+ if (
+ reportForm.quantity === null ||
+ reportForm.quantity === undefined ||
+ reportForm.quantity === ""
+ ) {
+ ElMessageBox.alert("璇疯緭鍏ユ湰娆$敓浜ф暟閲�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ const quantity = Number(reportForm.quantity);
+ const scrapQty =
+ reportForm.scrapQty === null ||
+ reportForm.scrapQty === undefined ||
+ reportForm.scrapQty === ""
+ ? 0
+ : Number(reportForm.scrapQty);
+
+ // 鏈鐢熶骇鏁伴噺
+ if (isNaN(quantity) || !Number.isInteger(quantity) || quantity < 1) {
+ ElMessageBox.alert("鏈鐢熶骇鏁伴噺蹇呴』澶т簬绛変簬1", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ // 鎶ュ簾鏁伴噺蹇呴』鏄暣鏁颁笖澶т簬绛変簬0
+ if (isNaN(scrapQty) || !Number.isInteger(scrapQty) || scrapQty < 0) {
+ ElMessageBox.alert("鎶ュ簾鏁伴噺蹇呴』澶т簬绛変簬0", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ if (quantity > reportForm.planQuantity) {
+ ElMessageBox.alert("鏈鐢熶骇鏁伴噺涓嶈兘瓒呰繃寰呯敓浜ф暟閲�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ const submitData = {
+ ...reportForm,
+ quantity: quantity,
+ scrapQty: scrapQty,
+ };
+
+ // console.log(submitData);
+ addProductMain(submitData).then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鎶ュ伐鎴愬姛");
+ reportDialogVisible.value = false;
+ getList();
+ } else {
+ ElMessageBox.alert(res.msg || "鎶ュ伐澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ }
+ });
+ });
+ };
+
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ const getUserList = () => {
+ userListNoPageByTenantId()
+ .then(res => {
+ if (res.code === 200) {
+ userOptions.value = res.data || [];
+ }
+ })
+ .catch(err => {
+ console.error("鑾峰彇鐢ㄦ埛鍒楄〃澶辫触", err);
+ });
+ };
+
+ // 鐢ㄦ埛閫夋嫨鍙樺寲鏃舵洿鏂� userName
+ const handleUserChange = userId => {
+ if (userId) {
+ const selectedUser = userOptions.value.find(user => user.userId === userId);
+ if (selectedUser) {
+ reportForm.userName = selectedUser.nickName;
+ }
+ } else {
+ reportForm.userName = "";
+ }
+ };
+
+ onMounted(() => {
+ getList();
+ getUserList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .search_form {
+ margin-bottom: 20px;
+ .search-row {
+ display: flex;
+ gap: 20px;
+ align-items: center;
+ .search-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ }
+ }
+
+ .transfer-card-title {
+ font-size: 24px;
+ font-weight: bold;
+ text-align: center;
+ margin-bottom: 20px;
+ }
+
+ .transfer-card-container {
+ display: flex;
+ gap: 20px;
+ height: 350px;
+ .transfer-card-info {
+ flex: 1;
+ overflow: auto;
+ .info-group {
+ width: 50%;
+ float: left;
+ }
+ .info-item {
+ display: flex;
+ margin-bottom: 15px;
+ .info-label {
+ width: 120px;
+ font-weight: bold;
+ margin-right: 20px;
+ }
+ .info-value {
+ flex: 1;
+ }
+ }
+ }
+ .transfer-card-qr {
+ width: 240px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ }
+ }
+</style>
+
+<style lang="scss">
+ @media print {
+ @page {
+ size: landscape;
+ }
+ body * {
+ visibility: hidden;
+ }
+ .el-dialog__wrapper,
+ .el-dialog,
+ .el-dialog__body,
+ .transfer-card-title,
+ .transfer-card-container,
+ .transfer-card-container *,
+ .info-item,
+ .info-label,
+ .info-value {
+ visibility: visible;
+ }
+ .print-button-container {
+ visibility: hidden;
+ }
+ .el-dialog__wrapper {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ margin: 0;
+ }
+ .el-dialog {
+ width: 100% !important;
+ max-width: 800px;
+ margin: 0 auto !important;
+ }
+ .el-dialog__header,
+ .el-dialog__footer {
+ display: none;
+ }
+ .el-dialog__body {
+ padding: 20px;
+ }
+ .transfer-card-container {
+ height: auto;
+ display: flex;
+ gap: 20px;
+ }
+ .transfer-card-info {
+ flex: 1;
+ .info-group {
+ width: 100%;
+ float: none;
+ margin-bottom: 20px;
+ }
+ .info-item {
+ display: flex;
+ margin-bottom: 10px;
+ .info-label {
+ width: 100px;
+ font-weight: bold;
+ margin-right: 15px;
+ white-space: nowrap;
+ }
+ .info-value {
+ flex: 1;
+ word-break: break-word;
+ }
+ }
+ }
+ .transfer-card-qr {
+ width: 160px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ }
+ .qr-container img {
+ width: 140px !important;
+ height: 140px !important;
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/workOrderEdit/index.vue b/src/views/productionManagement/workOrderEdit/index.vue
new file mode 100644
index 0000000..37cbb4e
--- /dev/null
+++ b/src/views/productionManagement/workOrderEdit/index.vue
@@ -0,0 +1,530 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div class="search-row">
+ <div class="search-item">
+ <span class="search_title">宸ュ崟缂栧彿锛�</span>
+ <el-input v-model="searchForm.workOrderNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
+ </div>
+ <div class="search-item">
+ <span class="search_title">鐢熶骇璁㈠崟鍙凤細</span>
+ <el-input v-model="searchForm.npsNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
+ </div>
+ <div class="search-item">
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
+ </div>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination">
+ <template #completionStatus="{ row }">
+ <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
+ :color="progressColor(toProgressPercentage(row?.completionStatus))"
+ :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
+ </template>
+ </PIMTable>
+ </div>
+ <el-dialog v-model="editDialogVisible"
+ title="缂栬緫璁″垝鏃堕棿"
+ width="500px">
+ <el-form :model="editrow"
+ label-width="120px">
+ <el-form-item label="璁″垝寮�濮嬫椂闂�">
+ <el-date-picker v-model="editrow.planStartTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
+ </el-form-item>
+ <el-form-item label="璁″垝缁撴潫鏃堕棿">
+ <el-date-picker v-model="editrow.planEndTime"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ value-format="YYYY-MM-DD"
+ style="width: 300px" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleUpdate">纭畾</el-button>
+ <el-button @click="editDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鎸囧畾鎶ュ伐浜哄脊绐� -->
+ <el-dialog v-model="assignReporterDialogVisible"
+ title="鎸囧畾鎶ュ伐浜�"
+ width="800px">
+ <div class="assign-reporter-content">
+ <div class="search-box">
+ <el-input
+ v-model="employeeSearchKeyword"
+ placeholder="鎼滅储浜哄憳濮撳悕"
+ clearable
+ @input="handleEmployeeSearch"
+ style="width: 350px">
+ <template #prefix>
+ <i class="el-icon-search"></i>
+ </template>
+ </el-input>
+ </div>
+ <div class="selected-tags-box"
+ v-if="selectedEmployeeIds.length > 0">
+ <div class="tags-label">宸查�夋嫨锛�</div>
+ <div class="tags-list">
+ <el-tag v-for="id in selectedEmployeeIds"
+ :key="id"
+ closable
+ @close="removeEmployeeTag(id)"
+ class="employee-tag">
+ {{ getEmployeeNameById(id) }}
+ </el-tag>
+ </div>
+ </div>
+ <div class="employee-list-container"
+ v-loading="employeeTableLoading">
+ <el-checkbox-group v-model="selectedEmployeeIds">
+ <div class="employee-grid">
+ <div v-for="item in filteredEmployeeList"
+ :key="item.userId"
+ class="employee-item">
+ <el-checkbox :label="item.userId"
+ border>
+ <div class="employee-info">
+ <span class="name">{{ item.nickName }}</span>
+ <span class="dept">{{ item.dept?.deptName }}</span>
+ </div>
+ </el-checkbox>
+ </div>
+ </div>
+ </el-checkbox-group>
+ <div v-if="filteredEmployeeList.length === 0"
+ class="empty-text">
+ {{ employeeSearchKeyword ? '鏃犲尮閰嶄汉鍛�' : '鏆傛棤浜哄憳鏁版嵁' }}
+ </div>
+ </div>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleSaveReporters">纭畾</el-button>
+ <el-button @click="assignReporterDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { getCurrentInstance, onMounted, reactive, ref, toRefs, computed } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ productWorkOrderPage,
+ updateProductWorkOrder,
+ assignProductWorkOrder,
+ } from "@/api/productionManagement/workOrder.js";
+ import { listUser } from "@/api/system/user.js";
+
+ const { proxy } = getCurrentInstance();
+
+ const tableColumn = ref([
+ {
+ label: "宸ュ崟绫诲瀷",
+ prop: "workOrderType",
+ width: "80",
+ },
+ {
+ label: "宸ュ崟缂栧彿",
+ prop: "workOrderNo",
+ width: "140",
+ },
+ {
+ label: "鐢熶骇璁㈠崟鍙�",
+ prop: "npsNo",
+ width: "140",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: "140",
+ },
+ {
+ label: "瑙勬牸",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "宸ュ簭鍚嶇О",
+ prop: "operationName",
+ width: "100",
+ },
+ {
+ label: "闇�姹傛暟閲�",
+ prop: "planQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚鏁伴噺",
+ prop: "completeQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚杩涘害",
+ prop: "completionStatus",
+ dataType: "slot",
+ slot: "completionStatus",
+ width: "140",
+ },
+ {
+ label: "璁″垝寮�濮嬫椂闂�",
+ prop: "planStartTime",
+ width: "140",
+ },
+ {
+ label: "璁″垝缁撴潫鏃堕棿",
+ prop: "planEndTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯寮�濮嬫椂闂�",
+ prop: "actualStartTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯缁撴潫鏃堕棿",
+ prop: "actualEndTime",
+ width: "140",
+ },
+ {
+ label: "鎸囧畾鎶ュ伐浜�",
+ prop: "userNames",
+ width: "180",
+ },
+ {
+ label: "鎿嶄綔",
+ width: "200",
+ align: "center",
+ dataType: "action",
+ fixed: "right",
+ operation: [
+ {
+ name: "璁″垝鏃堕棿",
+ clickFun: row => {
+ handleEdit(row);
+ },
+ },
+ {
+ name: "鎸囧畾鎶ュ伐浜�",
+ clickFun: row => {
+ handleAssignReporter(row);
+ },
+ },
+ ],
+ },
+ ]);
+
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const editDialogVisible = ref(false);
+ const editrow = ref(null);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+
+ // 鎸囧畾鎶ュ伐浜虹浉鍏�
+ const assignReporterDialogVisible = ref(false);
+ const employeeTableLoading = ref(false);
+ const employeeTableData = ref([]);
+ const employeePage = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const employeeSearchForm = reactive({
+ staffName: "",
+ });
+ const employeeSearchKeyword = ref("");
+ const selectedEmployeeIds = ref([]);
+ const currentWorkOrder = ref(null);
+
+ const filteredEmployeeList = computed(() => {
+ const keyword = employeeSearchKeyword.value.trim().toLowerCase();
+ if (!keyword) {
+ return employeeTableData.value;
+ }
+ return employeeTableData.value.filter(item => {
+ const name = (item.nickName || "").toLowerCase();
+ const dept = (item.dept?.deptName || "").toLowerCase();
+ return name.includes(keyword) || dept.includes(keyword);
+ });
+ });
+
+ const handleEmployeeSearch = () => {
+ };
+
+ const data = reactive({
+ searchForm: {
+ workOrderNo: "",
+ npsNo: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
+
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
+
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ productWorkOrderPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const handleEdit = row => {
+ editrow.value = JSON.parse(JSON.stringify(row));
+ editDialogVisible.value = true;
+ };
+
+ const handleUpdate = () => {
+ updateProductWorkOrder(editrow.value)
+ .then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ editDialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ ElMessageBox.alert("淇敼澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ });
+ };
+
+ const handleAssignReporter = row => {
+ currentWorkOrder.value = row;
+ assignReporterDialogVisible.value = true;
+ // 鍥炴樉宸插嬀閫夌殑浜哄憳
+ if (row.userIds) {
+ try {
+ selectedEmployeeIds.value = JSON.parse(row.userIds);
+ } catch (e) {
+ selectedEmployeeIds.value = [];
+ }
+ } else {
+ selectedEmployeeIds.value = [];
+ }
+ employeeSearchForm.staffName = "";
+ getEmployeeList();
+ };
+
+ const getEmployeeList = () => {
+ employeeTableLoading.value = true;
+ const params = {
+ pageNum: 1,
+ pageSize: 100,
+ };
+ listUser(params)
+ .then(res => {
+ employeeTableLoading.value = false;
+ employeeTableData.value = res.rows;
+ employeePage.total = res.total;
+ })
+ .catch(() => {
+ employeeTableLoading.value = false;
+ });
+ };
+
+ const getEmployeeNameById = id => {
+ const employee = employeeTableData.value.find(item => item.userId === id);
+ return employee ? employee.nickName : id;
+ };
+
+ const removeEmployeeTag = id => {
+ selectedEmployeeIds.value = selectedEmployeeIds.value.filter(
+ item => item !== id
+ );
+ };
+
+ const handleSaveReporters = () => {
+ if (selectedEmployeeIds.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鎶ュ伐浜�");
+ return;
+ }
+
+ const updateData = {
+ id: currentWorkOrder.value.id,
+ userIds: JSON.stringify(selectedEmployeeIds.value),
+ };
+ console.log(updateData, "updateData");
+
+ assignProductWorkOrder(updateData)
+ .then(() => {
+ proxy.$modal.msgSuccess("鎸囧畾鎴愬姛");
+ assignReporterDialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鎸囧畾澶辫触");
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .search-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .search-item {
+ display: flex;
+ align-items: center;
+ }
+
+ .search_title {
+ margin-right: 8px;
+ font-size: 14px;
+ color: #606266;
+ }
+
+ .assign-reporter-content {
+ .search-box {
+ margin-bottom: 16px;
+ display: flex;
+ justify-content: center;
+ }
+
+ .selected-tags-box {
+ margin-bottom: 16px;
+ padding: 12px;
+ background-color: #f5f7fa;
+ border-radius: 4px;
+ display: flex;
+ align-items: flex-start;
+
+ .tags-label {
+ font-size: 14px;
+ color: #606266;
+ margin-right: 8px;
+ white-space: nowrap;
+ margin-top: 4px;
+ }
+
+ .tags-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+
+ .employee-tag {
+ margin-bottom: 4px;
+ }
+ }
+ }
+
+ .employee-list-container {
+ max-height: 400px;
+ overflow-y: auto;
+ padding: 10px;
+ border: 1px solid #f0f0f0;
+ border-radius: 4px;
+
+ .employee-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
+ gap: 12px;
+ }
+
+ .employee-item {
+ :deep(.el-checkbox) {
+ width: 100%;
+ margin-right: 0;
+ height: auto;
+ padding: 8px;
+
+ .el-checkbox__label {
+ width: 100%;
+ }
+ }
+
+ .employee-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+
+ .name {
+ font-weight: bold;
+ font-size: 14px;
+ color: #303133;
+ }
+
+ .dept {
+ font-size: 12px;
+ color: #909399;
+ }
+ }
+ }
+
+ .empty-text {
+ text-align: center;
+ color: #909399;
+ padding: 20px;
+ }
+ }
+ }
+</style>
diff --git a/src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue b/src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue
new file mode 100644
index 0000000..45944b5
--- /dev/null
+++ b/src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue
@@ -0,0 +1,320 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogVisible"
+ title="鐗╂枡"
+ width="1200px"
+ @close="handleCloseMaterialDialog">
+ <el-table v-loading="materialTableLoading"
+ :data="materialTableData"
+ border
+ row-key="id">
+ <el-table-column label="宸ュ簭鍚嶇О"
+ prop="processName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍚嶇О"
+ prop="materialName"
+ min-width="140" />
+ <el-table-column label="鍘熸枡鍨嬪彿"
+ prop="materialModel"
+ min-width="140" />
+ <el-table-column label="璁¢噺鍗曚綅"
+ prop="unit"
+ min-width="100" />
+ <el-table-column label="绾胯竟浠撴暟閲�"
+ prop="pickQty"
+ min-width="100" />
+ <el-table-column label="琛ユ枡鏁伴噺"
+ prop="supplementQty"
+ min-width="100" />
+ <el-table-column label="瀹為檯鏁伴噺"
+ min-width="140">
+ <template #default="{ row }">
+ <el-input-number v-model="row.actualQty"
+ :min="0"
+ :precision="3"
+ :step="1"
+ controls-position="right"
+ style="width: 100%;" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ align="center"
+ fixed="right"
+ width="180">
+ <template #default="{ row }">
+ <el-button type="primary"
+ link
+ @click="openSupplementDialog(row)">琛ユ枡</el-button>
+ <el-button type="info"
+ link
+ @click="openSupplementRecordDialog(row)">琛ユ枡璁板綍</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="pickSubmitting"
+ @click="handleSubmitPick">棰嗙敤</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <FormDialog v-model="supplementDialogVisible"
+ title="琛ユ枡"
+ width="500px"
+ @confirm="handleSubmitSupplement">
+ <el-form ref="supplementFormRef"
+ :model="supplementForm"
+ :rules="supplementRules"
+ label-width="100px">
+ <el-form-item label="琛ユ枡鏁伴噺"
+ prop="supplementQty">
+ <el-input-number v-model="supplementForm.supplementQty"
+ :min="0.001"
+ :precision="3"
+ :step="1"
+ style="width: 100%;" />
+ </el-form-item>
+ <el-form-item label="琛ユ枡鍘熷洜"
+ prop="supplementReason">
+ <el-input v-model="supplementForm.supplementReason"
+ type="textarea"
+ :rows="3"
+ maxlength="200"
+ show-word-limit
+ placeholder="璇疯緭鍏ヨˉ鏂欏師鍥�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ :loading="supplementSubmitting"
+ @click="handleSubmitSupplement">纭畾</el-button>
+ <el-button @click="supplementDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </FormDialog>
+ <el-dialog v-model="supplementRecordDialogVisible"
+ title="琛ユ枡璁板綍"
+ width="900px">
+ <el-table v-loading="supplementRecordLoading"
+ :data="supplementRecordTableData"
+ border
+ row-key="id">
+ <el-table-column label="琛ユ枡鏁伴噺"
+ prop="supplementQty"
+ min-width="100" />
+ <el-table-column label="琛ユ枡鍘熷洜"
+ prop="supplementReason"
+ min-width="200" />
+ <el-table-column label="琛ユ枡浜�"
+ prop="supplementUserName"
+ min-width="120" />
+ <el-table-column label="琛ユ枡鏃ユ湡"
+ prop="supplementTime"
+ min-width="160" />
+ </el-table>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="supplementRecordDialogVisible = false">鍏抽棴</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { computed, nextTick, reactive, ref, watch } from "vue";
+ import { ElMessage } from "element-plus";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import {
+ listWorkOrderMaterialLedger,
+ addWorkOrderMaterialSupplement,
+ listWorkOrderMaterialSupplementRecord,
+ pickWorkOrderMaterial,
+ } from "@/api/productionManagement/workOrder.js";
+
+ const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false,
+ },
+ rowData: {
+ type: Object,
+ default: () => null,
+ },
+ });
+
+ const emit = defineEmits(["update:modelValue", "refresh"]);
+
+ const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: val => emit("update:modelValue", val),
+ });
+
+ const materialTableLoading = ref(false);
+ const materialTableData = ref([]);
+ const currentMaterialRow = ref(null);
+ const currentMaterialOrderRow = ref(null);
+ const pickSubmitting = ref(false);
+
+ const supplementDialogVisible = ref(false);
+ const supplementSubmitting = ref(false);
+ const supplementFormRef = ref(null);
+ const supplementForm = reactive({
+ supplementQty: null,
+ supplementReason: "",
+ });
+
+ const supplementRecordDialogVisible = ref(false);
+ const supplementRecordLoading = ref(false);
+ const supplementRecordTableData = ref([]);
+
+ const supplementRules = {
+ supplementQty: [
+ { required: true, message: "璇疯緭鍏ヨˉ鏂欐暟閲�", trigger: "blur" },
+ ],
+ supplementReason: [
+ { required: true, message: "璇疯緭鍏ヨˉ鏂欏師鍥�", trigger: "blur" },
+ ],
+ };
+ const loadMaterialTable = async row => {
+ if (!row?.id) return;
+ currentMaterialOrderRow.value = row;
+ materialTableLoading.value = true;
+ materialTableData.value = [];
+ try {
+ const res = await listWorkOrderMaterialLedger({
+ workOrderId: row.id,
+ processId: row.processId,
+ productProcessRouteItemId: row.productProcessRouteItemId,
+ });
+ materialTableData.value = res.data || [];
+ } catch (e) {
+ console.error("鑾峰彇鐗╂枡鍙拌处澶辫触", e);
+ ElMessage.error("鑾峰彇鐗╂枡鍙拌处澶辫触");
+ } finally {
+ materialTableLoading.value = false;
+ }
+ };
+
+ watch(
+ () => props.modelValue,
+ visible => {
+ if (visible && props.rowData) {
+ loadMaterialTable(props.rowData);
+ }
+ }
+ );
+
+ const handleCloseMaterialDialog = () => {
+ materialTableData.value = [];
+ currentMaterialRow.value = null;
+ currentMaterialOrderRow.value = null;
+ };
+
+ const openSupplementDialog = row => {
+ currentMaterialRow.value = row;
+ supplementForm.supplementQty = null;
+ supplementForm.supplementReason = "";
+ supplementDialogVisible.value = true;
+ nextTick(() => {
+ supplementFormRef.value?.clearValidate();
+ });
+ };
+
+ const handleSubmitSupplement = () => {
+ supplementFormRef.value?.validate(async valid => {
+ if (!valid || !currentMaterialRow.value?.id) {
+ ElMessage.warning("缂哄皯鐗╂枡鏄庣粏ID");
+ return;
+ }
+ supplementSubmitting.value = true;
+ try {
+ await addWorkOrderMaterialSupplement({
+ materialLedgerId: currentMaterialRow.value.id,
+ supplementQty: Number(supplementForm.supplementQty),
+ supplementReason: supplementForm.supplementReason,
+ workOrderId: currentMaterialOrderRow.value?.id,
+ });
+ supplementDialogVisible.value = false;
+ await loadMaterialTable(currentMaterialOrderRow.value);
+ ElMessage.success("琛ユ枡鎴愬姛");
+ emit("refresh");
+ } catch (e) {
+ console.error("琛ユ枡澶辫触", e);
+ ElMessage.error("琛ユ枡澶辫触");
+ } finally {
+ supplementSubmitting.value = false;
+ }
+ });
+ };
+
+ const openSupplementRecordDialog = async row => {
+ supplementRecordDialogVisible.value = true;
+ supplementRecordLoading.value = true;
+ supplementRecordTableData.value = [];
+ try {
+ const res = await listWorkOrderMaterialSupplementRecord({
+ materialLedgerId: row.id,
+ });
+ supplementRecordTableData.value = res.data || [];
+ } catch (e) {
+ console.error("鑾峰彇琛ユ枡璁板綍澶辫触", e);
+ ElMessage.error("鑾峰彇琛ユ枡璁板綍澶辫触");
+ } finally {
+ supplementRecordLoading.value = false;
+ }
+ };
+
+ const validatePickRows = () => {
+ if (materialTableData.value.length === 0) {
+ return { valid: false, message: "鏆傛棤鍙鐢ㄧ墿鏂�" };
+ }
+ const invalidRow = materialTableData.value.find(
+ item =>
+ item.actualQty === null ||
+ item.actualQty === undefined ||
+ item.actualQty === ""
+ );
+ if (invalidRow) {
+ return { valid: false, message: "璇峰~鍐欏疄闄呮暟閲忓悗鍐嶉鐢�" };
+ }
+ const exceedRow = materialTableData.value.find(item => {
+ const maxQty = Number(item.pickQty || 0) + Number(item.supplementQty || 0);
+ return Number(item.actualQty || 0) > maxQty;
+ });
+ if (exceedRow) {
+ return { valid: false, message: "瀹為檯鏁伴噺涓嶈兘澶т簬棰嗙敤鏁伴噺+琛ユ枡鏁伴噺" };
+ }
+ return { valid: true, message: "" };
+ };
+
+ const handleSubmitPick = async () => {
+ if (!currentMaterialOrderRow.value?.id) return;
+ const validateResult = validatePickRows();
+ if (!validateResult.valid) {
+ ElMessage.warning(validateResult.message);
+ return;
+ }
+ pickSubmitting.value = true;
+ try {
+ await pickWorkOrderMaterial({
+ workOrderId: currentMaterialOrderRow.value.id,
+ items: materialTableData.value.map(item => ({
+ materialLedgerId: item.id,
+ actualQty: Number(item.actualQty || 0),
+ })),
+ });
+ ElMessage.success("棰嗙敤鎴愬姛");
+ await loadMaterialTable(currentMaterialOrderRow.value);
+ emit("refresh");
+ } catch (e) {
+ console.error("棰嗙敤澶辫触", e);
+ ElMessage.error("棰嗙敤澶辫触");
+ } finally {
+ pickSubmitting.value = false;
+ }
+ };
+</script>
diff --git a/src/views/productionManagement/workOrderManagement/components/filesDia.vue b/src/views/productionManagement/workOrderManagement/components/filesDia.vue
new file mode 100644
index 0000000..84d8bd4
--- /dev/null
+++ b/src/views/productionManagement/workOrderManagement/components/filesDia.vue
@@ -0,0 +1,201 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogVisible" title="宸ュ崟闄勪欢" width="50%" @close="closeDia">
+ <div style="margin-bottom: 10px; text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ :before-upload="beforeUpload"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ accept="image/*"
+ style="display: inline; margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶鍥剧墖</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :total="page.total"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="paginationSearch"
+ height="500"
+ />
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, getCurrentInstance } from "vue";
+import { ElMessageBox } from "element-plus";
+import { getToken } from "@/utils/auth.js";
+import PIMTable from "@/components/PIMTable/PIMTable.vue";
+import filePreview from "@/components/filePreview/index.vue";
+import {
+ productWorkOrderFileAdd,
+ productWorkOrderFileDel,
+ productWorkOrderFileListPage,
+} from "@/api/productionManagement/productWorkOrderFile.js";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(["close"]);
+
+const dialogVisible = ref(false);
+const currentWorkOrderId = ref("");
+const selectedRows = ref([]);
+const filePreviewRef = ref();
+
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ width: 120,
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: row => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: row => {
+ filePreviewRef.value?.open(row.url);
+ },
+ },
+ ],
+ },
+]);
+
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload");
+
+const beforeUpload = file => {
+ const isImage = file?.type?.startsWith("image/");
+ if (!isImage) {
+ proxy.$modal.msgError("鍙兘涓婁紶鍥剧墖鏂囦欢");
+ }
+ return isImage;
+};
+
+const openDialog = row => {
+ dialogVisible.value = true;
+ currentWorkOrderId.value = row.id;
+ page.current = 1;
+ getList();
+};
+
+const closeDia = () => {
+ dialogVisible.value = false;
+ emit("close");
+};
+
+const paginationSearch = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ productWorkOrderFileListPage({
+ workOrderId: currentWorkOrderId.value,
+ current: page.current,
+ size: page.size,
+ })
+ .then(res => {
+ tableData.value = res.data.records || [];
+ page.total = res.data.total || 0;
+ })
+ .finally(() => {
+ tableLoading.value = false;
+ });
+};
+
+const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+};
+
+function handleUploadSuccess(res) {
+ if (res.code == 200) {
+ const fileRow = {
+ name: res.data.originalName,
+ url: res.data.tempPath,
+ workOrderId: currentWorkOrderId.value,
+ };
+ productWorkOrderFileAdd(fileRow).then(() => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList();
+ });
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+
+const handleDelete = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ const ids = selectedRows.value.map(item => item.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ productWorkOrderFileDel(ids).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped></style>
\ No newline at end of file
diff --git a/src/views/productionManagement/workOrderManagement/index.vue b/src/views/productionManagement/workOrderManagement/index.vue
new file mode 100644
index 0000000..8bc6dc2
--- /dev/null
+++ b/src/views/productionManagement/workOrderManagement/index.vue
@@ -0,0 +1,1040 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div class="search-row">
+ <div class="search-item">
+ <span class="search_title">宸ュ崟缂栧彿锛�</span>
+ <el-input v-model="searchForm.workOrderNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
+ </div>
+ <div class="search-item">
+ <span class="search_title">鐢熶骇璁㈠崟鍙凤細</span>
+ <el-input v-model="searchForm.npsNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏�"
+ @change="handleQuery"
+ clearable
+ prefix-icon="Search" />
+ </div>
+ <div class="search-item">
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
+ </div>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ @pagination="pagination">
+ <template #completionStatus="{ row }">
+ <el-progress :percentage="toProgressPercentage(row?.completionStatus)"
+ :color="progressColor(toProgressPercentage(row?.completionStatus))"
+ :status="toProgressPercentage(row?.completionStatus) >= 100 ? 'success' : ''" />
+ </template>
+ </PIMTable>
+ </div>
+ <!-- 娴佽浆鍗″脊绐� -->
+ <el-dialog v-model="transferCardVisible"
+ title="娴佽浆鍗�"
+ width="1000px">
+ <div class="transfer-card-title">宸ュ崟娴佽浆鍗�</div>
+ <div class="transfer-card-container">
+ <div class="transfer-card-info">
+ <div class="info-group">
+ <div class="info-item">
+ <span class="info-label">宸ュ崟缂栧彿</span>
+ <span class="info-value">{{ transferCardRowData.workOrderNo }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">浜у搧鍚嶇О</span>
+ <span class="info-value">{{ transferCardRowData.productName }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">瑙勬牸鍨嬪彿</span>
+ <span class="info-value">{{ transferCardRowData.model }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">璁″垝寮�濮嬫椂闂�</span>
+ <span class="info-value">{{ transferCardRowData.planStartTime }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">璁″垝缁撴潫鏃堕棿</span>
+ <span class="info-value">{{ transferCardRowData.planEndTime }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">澶囨敞</span>
+ <span class="info-value">{{ transferCardRowData.remark }}</span>
+ </div>
+ </div>
+ <div class="info-group">
+ <div class="info-item">
+ <span class="info-label">闇�姹傛暟閲�</span>
+ <span class="info-value">{{ transferCardRowData.planQuantity }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-value">{{ transferCardRowData.completeQuantity }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">鑹搧鏁伴噺</span>
+ <span class="info-value">0</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">涓嶈壇鍝佹暟</span>
+ <span class="info-value">0</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">瀹為檯寮�濮嬫椂闂�</span>
+ <span class="info-value">{{ transferCardRowData.actualStartTime }}</span>
+ </div>
+ <div class="info-item">
+ <span class="info-label">瀹為檯缁撴潫鏃堕棿</span>
+ <span class="info-value">{{ transferCardRowData.actualEndTime }}</span>
+ </div>
+ </div>
+ </div>
+ <div class="transfer-card-qr">
+ <div class="qr-container">
+ <img :src="transferCardQrUrl"
+ alt="娴佽浆鍗′簩缁寸爜"
+ style="width: 200px; height: 200px;" />
+ </div>
+ </div>
+ </div>
+ <div class="print-button-container"
+ style=" text-align: center;
+ margin-bottom: 40px;">
+ <el-button type="primary"
+ style="margin-top: 20px;"
+ @click="printTransferCard">鎵撳嵃娴佽浆鍗�</el-button>
+ </div>
+ </el-dialog>
+ <!-- 鎶ュ伐寮圭獥 -->
+ <el-dialog v-model="reportDialogVisible"
+ title="鎶ュ伐"
+ width="500px">
+ <el-form ref="reportFormRef"
+ :model="reportForm"
+ :rules="reportFormRules"
+ label-width="120px">
+ <el-form-item label="寰呯敓浜ф暟閲�">
+ <el-input v-model="reportForm.planQuantity"
+ readonly
+ style="width: 300px" />
+ </el-form-item>
+ <el-form-item label="鐢熶骇鍚堟牸鏁伴噺"
+ prop="quantity">
+ <el-input v-model.number="reportForm.quantity"
+ type="number"
+ min="0"
+ step="1"
+ style="width: 300px"
+ placeholder="璇疯緭鍏ョ敓浜у悎鏍兼暟閲�"
+ @input="handleQuantityInput" />
+ </el-form-item>
+ <el-form-item label="鎶ュ簾鏁伴噺"
+ prop="scrapQty">
+ <el-input v-model.number="reportForm.scrapQty"
+ type="number"
+ min="0"
+ step="1"
+ style="width: 300px"
+ placeholder="璇疯緭鍏ユ姤搴熸暟閲�"
+ @input="handleScrapQtyInput" />
+ </el-form-item>
+ <el-form-item label="鐝粍淇℃伅">
+ <el-select v-model="reportForm.userId"
+ style="width: 300px"
+ placeholder="璇烽�夋嫨鐝粍淇℃伅"
+ clearable
+ filterable
+ @change="handleUserChange">
+ <el-option v-for="user in userOptions"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId" />
+ </el-select>
+ </el-form-item>
+ <!-- 宸ユ椂 -->
+ <el-form-item label="宸ユ椂"
+ v-if="currentReportRowData?.type == 0"
+ prop="workHour">
+ <el-input v-model.number="reportForm.workHour"
+ type="number"
+ min="0"
+ style="width: 280px"
+ placeholder="璇疯緭鍏ュ伐鏃�" /><span style="margin-left:10px"
+ class="param-unit">h</span>
+ </el-form-item>
+ <div v-if="params.length > 0"
+ class="param-grid"
+ v-loading="paramLoading">
+ <el-form-item v-for="param in params"
+ :key="param.id"
+ :label="param.paramName"
+ :label-width="120"
+ class="param-item">
+ <template v-if="param.paramType == '1'">
+ <div class="param-input-group">
+ <el-input-number v-model="reportForm.paramGroups[param.id]"
+ controls-position="right"
+ :key="param.id"
+ style="width: 250px"
+ class="param-input" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else-if="param.paramType == '2'">
+ <div class="param-input-group">
+ <el-input v-model="reportForm.paramGroups[param.id]"
+ :key="param.id"
+ style="width: 250px"
+ class="param-input" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else-if="param.paramType == '3'">
+ <div class="param-input-group">
+ <el-select v-model="reportForm.paramGroups[param.id]"
+ placeholder="璇烽�夋嫨"
+ :key="param.id"
+ class="param-select"
+ style="width: 250px">
+ <el-option v-for="option in dictOptions[param.paramFormat] || []"
+ :key="option.dictLabel"
+ :label="option.dictLabel"
+ :value="option.dictLabel" />
+ </el-select>
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else-if="param.paramType == '4'">
+ <div class="param-input-group">
+ <el-date-picker :value-format="param.paramFormat.replace('yyyy', 'YYYY').replace('dd', 'DD')"
+ :format="param.paramFormat.replace('yyyy', 'YYYY').replace('dd', 'DD')"
+ :key="param.id"
+ :type="param.paramFormat=='yyyy-MM-dd'?'date':'datetime'"
+ v-model="reportForm.paramGroups[param.id]"
+ class="param-input"
+ style="width: 250px" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ <template v-else>
+ <div class="param-input-group">
+ <el-input v-model="reportForm.paramGroups[param.id]"
+ :key="param.id"
+ class="param-input" />
+ <span v-if="param.unit && param.unit != '/'"
+ class="param-unit">{{ param.unit }}</span>
+ </div>
+ </template>
+ </el-form-item>
+ </div>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleReport">纭畾</el-button>
+ <el-button @click="reportDialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <MaterialDialog v-model="materialDialogVisible"
+ :row-data="currentMaterialOrderRow"
+ @refresh="getList" />
+ <FileList v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ :editable="!currentWorkOrderRow?.endOrder"
+ :record-type="'production_operation_task'"
+ :record-id="currentWorkOrderId" />
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref, nextTick } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import dayjs from "dayjs";
+ import {
+ productWorkOrderPage,
+ addProductMain,
+ downProductWorkOrder,
+ } from "@/api/productionManagement/workOrder.js";
+ import { listMaterialPickingDetail } from "@/api/productionManagement/productionOrder.js";
+ import { findProcessParamListOrder } from "@/api/productionManagement/productProcessRoute.js";
+ import { getUserProfile, userListNoPageByTenantId } from "@/api/system/user.js";
+ import { getDicts } from "@/api/system/dict/data";
+ import QRCode from "qrcode";
+ import { getCurrentInstance, reactive, toRefs } from "vue";
+ import MaterialDialog from "./components/MaterialDialog.vue";
+ const FileList = defineAsyncComponent(() =>
+ import("@/components/Dialog/FileList.vue")
+ );
+
+ import useUserStore from "@/store/modules/user";
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
+
+ const tableColumn = ref([
+ {
+ label: "宸ュ崟绫诲瀷",
+ prop: "workOrderType",
+ width: "80",
+ },
+ {
+ label: "宸ュ崟缂栧彿",
+ prop: "workOrderNo",
+ width: "140",
+ },
+ {
+ label: "鐢熶骇璁㈠崟鍙�",
+ prop: "npsNo",
+ width: "140",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: "140",
+ },
+ {
+ label: "瑙勬牸",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "宸ュ簭鍚嶇О",
+ prop: "operationName",
+ width: "100",
+ },
+ {
+ label: "闇�姹傛暟閲�",
+ prop: "planQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚鏁伴噺",
+ prop: "completeQuantity",
+ width: "140",
+ },
+ {
+ label: "瀹屾垚杩涘害",
+ prop: "completionStatus",
+ dataType: "slot",
+ slot: "completionStatus",
+ width: "140",
+ },
+ {
+ label: "璁″垝寮�濮嬫椂闂�",
+ prop: "planStartTime",
+ width: "140",
+ },
+ {
+ label: "璁″垝缁撴潫鏃堕棿",
+ prop: "planEndTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯寮�濮嬫椂闂�",
+ prop: "actualStartTime",
+ width: "140",
+ },
+ {
+ label: "瀹為檯缁撴潫鏃堕棿",
+ prop: "actualEndTime",
+ width: "140",
+ },
+ {
+ label: "鎿嶄綔",
+ width: "260",
+ align: "center",
+ dataType: "action",
+ fixed: "right",
+ operation: [
+ {
+ name: "娴佽浆鍗�",
+ clickFun: row => {
+ downloadAndPrintWorkOrder(row);
+ },
+ },
+ {
+ name: "闄勪欢",
+ clickFun: row => {
+ openWorkOrderFiles(row);
+ },
+ },
+ // {
+ // name: "鐗╂枡",
+ // clickFun: row => {
+ // openMaterialDialog(row);
+ // },
+ // },
+ {
+ name: "鎶ュ伐",
+ clickFun: row => {
+ showReportDialog(row);
+ },
+ showHide: row => !row.endOrder,
+ disabled: row => {
+ if (row.planQuantity <= 0) return true;
+ if (!row.userIds) return false;
+ try {
+ const userIds =
+ typeof row.userIds === "string"
+ ? JSON.parse(row.userIds)
+ : row.userIds;
+ if (Array.isArray(userIds)) {
+ return !userIds.some(id => String(id) === String(userStore.id));
+ }
+ return true;
+ } catch (e) {
+ return true;
+ }
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const transferCardVisible = ref(false);
+ const transferCardData = ref([]);
+ const transferCardQrUrl = ref("");
+ const transferCardRowData = ref(null);
+ const reportDialogVisible = ref(false);
+ const fileDialogVisible = ref(false);
+ const currentWorkOrderId = ref(null);
+ const reportFormRef = ref(null);
+ const userOptions = ref([]);
+ const reportForm = reactive({
+ planQuantity: 0,
+ quantity: null,
+ scrapQty: null,
+ userName: "",
+ workOrderId: "",
+ reportWork: "",
+ productProcessRouteItemId: "",
+ userId: "",
+ productMainId: null,
+ productionOrderRoutingOperationId: "",
+ productionOrderId: "",
+ workHour: 0,
+ paramGroups: {},
+ });
+
+ const params = ref({});
+ const dictOptions = ref({});
+ const paramLoading = ref(false);
+
+ // 鐢熶骇鍚堟牸鏁伴噺楠岃瘉瑙勫垯
+ const validateQuantity = (rule, value, callback) => {
+ if (value === null || value === undefined || value === "") {
+ callback(new Error("璇疯緭鍏ョ敓浜у悎鏍兼暟閲�"));
+ return;
+ }
+ const num = Number(value);
+ // 鏁存暟涓斿ぇ浜庣瓑浜�1
+ if (isNaN(num) || !Number.isInteger(num) || num < 0) {
+ callback(new Error("鐢熶骇鍚堟牸鏁伴噺蹇呴』澶т簬绛変簬0"));
+ return;
+ }
+ callback();
+ };
+
+ // 鎶ュ簾鏁伴噺楠岃瘉瑙勫垯
+ const validateScrapQty = (rule, value, callback) => {
+ if (value === null || value === undefined || value === "") {
+ callback();
+ return;
+ }
+ const num = Number(value);
+ // 鏁存暟涓斿ぇ浜庣瓑浜�0
+ if (isNaN(num) || !Number.isInteger(num) || num < 0) {
+ callback(new Error("鎶ュ簾鏁伴噺蹇呴』澶т簬绛変簬0"));
+ return;
+ }
+ callback();
+ };
+
+ // 楠岃瘉瑙勫垯
+ const reportFormRules = {
+ quantity: [{ required: true, validator: validateQuantity, trigger: "blur" }],
+ scrapQty: [{ validator: validateScrapQty, trigger: "blur" }],
+ };
+
+ // 澶勭悊鐢熶骇鍚堟牸鏁伴噺杈撳叆锛岄檺鍒跺繀椤诲ぇ浜庣瓑浜�0
+ const handleQuantityInput = value => {
+ if (value === "" || value === null || value === undefined) {
+ reportForm.quantity = null;
+ return;
+ }
+ const num = Number(value);
+ if (isNaN(num)) {
+ return;
+ }
+ // 濡傛灉灏忎簬1锛屾竻闄�
+ if (num < 0) {
+ reportForm.quantity = null;
+ return;
+ }
+ // 濡傛灉鏄皬鏁板彇鏁存暟閮ㄥ垎
+ if (!Number.isInteger(num)) {
+ const intValue = Math.floor(num);
+ // 濡傛灉鍙栨暣鍚庡皬浜�1锛屾竻闄�
+ if (intValue < 0) {
+ reportForm.quantity = null;
+ return;
+ }
+ reportForm.quantity = intValue;
+ return;
+ }
+ reportForm.quantity = num;
+ };
+
+ // 澶勭悊鎶ュ簾鏁伴噺
+ const handleScrapQtyInput = value => {
+ if (value === "" || value === null || value === undefined) {
+ reportForm.scrapQty = null;
+ return;
+ }
+ const num = Number(value);
+ // 濡傛灉鏄疦aN锛屼繚鎸佸師鍊�
+ if (isNaN(num)) {
+ return;
+ }
+ // 濡傛灉鏄礋鏁帮紝娓呴櫎杈撳叆
+ if (num < 0) {
+ reportForm.scrapQty = null;
+ return;
+ }
+ // 濡傛灉鏄皬鏁帮紝鍙栨暣鏁伴儴鍒�
+ if (!Number.isInteger(num)) {
+ reportForm.scrapQty = Math.floor(num);
+ return;
+ }
+ // 鏈夋晥鐨勯潪璐熸暣鏁帮紙鍖呮嫭0锛�
+ reportForm.scrapQty = num;
+ };
+
+ const currentReportRowData = ref(null);
+ const materialDialogVisible = ref(false);
+ const currentMaterialOrderRow = ref(null);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+
+ const data = reactive({
+ searchForm: {
+ workOrderNo: "",
+ npsNo: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+ const toProgressPercentage = val => {
+ const n = Number(val);
+ if (!Number.isFinite(n)) return 0;
+ if (n <= 0) return 0;
+ if (n >= 100) return 100;
+ return Math.round(n);
+ };
+ const progressColor = percentage => {
+ const p = toProgressPercentage(percentage);
+ if (p < 30) return "#f56c6c";
+ if (p < 50) return "#e6a23c";
+ if (p < 80) return "#409eff";
+ return "#67c23a";
+ };
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ productWorkOrderPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 涓嬭浇骞舵墦鍗板伐鍗曟祦杞崱锛堟枃浠舵祦锛�
+ const downloadAndPrintWorkOrder = async row => {
+ if (!row || !row.id) {
+ proxy.$modal.msgError("缂哄皯宸ュ崟ID锛屾棤娉曚笅杞芥祦杞崱");
+ return;
+ }
+ const fileName = row.workOrderNo
+ ? `宸ュ崟娴佽浆鍗${row.workOrderNo}.xlsx`
+ : "宸ュ崟娴佽浆鍗�.xlsx";
+ try {
+ // 璋冪敤鎺ュ彛锛屼互 responseType: 'blob' 鑾峰彇鏂囦欢娴�
+ const blob = await downProductWorkOrder(row.id);
+
+ if (!blob) {
+ proxy.$modal.msgError("鏈幏鍙栧埌娴佽浆鍗℃枃浠�");
+ return;
+ }
+
+ // 鍒涘缓 Blob URL
+ const fileBlob =
+ blob instanceof Blob
+ ? blob
+ : new Blob([blob], { type: blob.type || "application/octet-stream" });
+ const url = window.URL.createObjectURL(fileBlob);
+
+ // 鍒涘缓闅愯棌 iframe锛岀敤浜庤Е鍙戞祻瑙堝櫒鎵撳嵃
+ const iframe = document.createElement("iframe");
+ iframe.style.position = "fixed";
+ iframe.style.right = "0";
+ iframe.style.bottom = "0";
+ iframe.style.width = "0";
+ iframe.style.height = "0";
+ iframe.style.border = "0";
+ iframe.src = url;
+ document.body.appendChild(iframe);
+
+ iframe.onload = () => {
+ try {
+ iframe.contentWindow?.focus();
+ iframe.contentWindow?.print();
+ } catch (e) {
+ console.error("鑷姩璋冪敤鎵撳嵃澶辫触", e);
+ // 閫�鑰屾眰鍏舵锛屾墦寮�鏂扮獥鍙g敱鐢ㄦ埛鎵嬪姩鎵撳嵃
+ window.open(url);
+ }
+ };
+ } catch (e) {
+ console.error("涓嬭浇宸ュ崟娴佽浆鍗″け璐�", e);
+ proxy.$modal.msgError("涓嬭浇宸ュ崟娴佽浆鍗″け璐�");
+ }
+ };
+
+ const showTransferCard = async row => {
+ transferCardRowData.value = row;
+ const qrContent = String(row.id);
+
+ transferCardQrUrl.value = await QRCode.toDataURL(qrContent);
+ transferCardVisible.value = true;
+ };
+
+ const printTransferCard = () => {
+ window.print();
+ };
+ const currentWorkOrderRow = ref(null);
+
+ const openWorkOrderFiles = row => {
+ currentWorkOrderId.value = row.id;
+ currentWorkOrderRow.value = row;
+ fileDialogVisible.value = true;
+ };
+
+ const showReportDialog = async row => {
+ if (row.productionOrderId) {
+ try {
+ const res = await listMaterialPickingDetail(row.productionOrderId);
+ const records = Array.isArray(res.data)
+ ? res.data
+ : res.data?.records || [];
+ if (res.code === 200 && records.length === 0) {
+ proxy.$modal.msgError("鏈鏂欐棤娉曟姤宸�");
+ return;
+ }
+ } catch (error) {
+ console.error("鏌ヨ棰嗘枡璇︽儏澶辫触:", error);
+ }
+ }
+ currentReportRowData.value = row;
+ const planQuantity = Number(row.planQuantity || 0);
+ const completeQuantity = Number(row.completeQuantity || 0);
+ const remainingQuantity = Math.max(0, planQuantity - completeQuantity);
+ reportForm.planQuantity = remainingQuantity;
+ reportForm.quantity =
+ row.quantity !== undefined && row.quantity !== null ? row.quantity : null;
+ reportForm.productProcessRouteItemId = row.productProcessRouteItemId;
+ reportForm.workOrderId = row.id;
+ reportForm.reportWork = row.reportWork;
+ reportForm.productMainId = row.productMainId;
+ reportForm.scrapQty =
+ row.scrapQty !== undefined && row.scrapQty !== null ? row.scrapQty : null;
+ reportForm.productionOrderRoutingOperationId =
+ row.productionOrderRoutingOperationId;
+ reportForm.productionOrderId = row.productionOrderId;
+ if (row.type == 0) {
+ reportForm.workHour = row.workHour || 0;
+ } else {
+ reportForm.workHour = 0;
+ }
+ nextTick(() => {
+ reportFormRef.value?.clearValidate();
+ if (row.productionOrderRoutingOperationId && row.productionOrderId) {
+ loadParams(row.productionOrderRoutingOperationId, row.productionOrderId);
+ }
+ });
+ getUserProfile()
+ .then(res => {
+ if (res.code === 200) {
+ reportForm.userId = res.data.userId;
+ reportForm.userName = res.data.nickName;
+ }
+ })
+ .catch(err => {
+ console.error("鑾峰彇鐢ㄦ埛淇℃伅澶辫触", err);
+ });
+
+ reportDialogVisible.value = true;
+ };
+
+ const openMaterialDialog = row => {
+ currentMaterialOrderRow.value = row;
+ materialDialogVisible.value = true;
+ };
+
+ const handleReport = () => {
+ reportFormRef.value?.validate(valid => {
+ if (!valid) {
+ return false;
+ }
+
+ if (reportForm.planQuantity <= 0) {
+ ElMessageBox.alert("寰呯敓浜ф暟閲忎负0锛屾棤娉曟姤宸�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ // 楠岃瘉鐢熶骇鍚堟牸鏁伴噺
+ if (
+ reportForm.quantity === null ||
+ reportForm.quantity === undefined ||
+ reportForm.quantity === ""
+ ) {
+ ElMessageBox.alert("璇疯緭鍏ョ敓浜у悎鏍兼暟閲�", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ const quantity = Number(reportForm.quantity);
+
+ if (isNaN(quantity) || quantity < 0) {
+ ElMessageBox.alert("鐢熶骇鍚堟牸鏁伴噺蹇呴』澶т簬绛変簬0", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ // if (quantity > reportForm.planQuantity) {
+ // ElMessageBox.alert("鏈鐢熶骇鏁伴噺涓嶈兘瓒呰繃寰呯敓浜ф暟閲�", "鎻愮ず", {
+ // confirmButtonText: "纭畾",
+ // });
+ // return;
+ // }
+
+ // 楠岃瘉鎶ュ簾鏁伴噺
+ const scrapQty = Number(reportForm.scrapQty);
+ if (!isNaN(scrapQty) && scrapQty < 0) {
+ ElMessageBox.alert("鎶ュ簾鏁伴噺涓嶈兘灏忎簬0", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ return;
+ }
+
+ // if (!isNaN(scrapQty) && scrapQty > quantity) {
+ // ElMessageBox.alert("鎶ュ簾鏁伴噺涓嶈兘澶т簬鏈鐢熶骇鏁伴噺", "鎻愮ず", {
+ // confirmButtonText: "纭畾",
+ // });
+ // return;
+ // }
+
+ const productionOperationParamList = params.value.map(param => ({
+ ...param,
+ inputValue: reportForm.paramGroups[param.id] ?? "",
+ }));
+
+ const submitParams = {
+ quantity: quantity,
+ scrapQty: isNaN(scrapQty) ? 0 : scrapQty,
+ userId: reportForm.userId,
+ userName: reportForm.userName,
+ productionOperationTaskId: reportForm.workOrderId,
+ productProcessRouteItemId: reportForm.productProcessRouteItemId,
+ reportWork: reportForm.reportWork,
+ productMainId: reportForm.productMainId,
+ productionOrderRoutingOperationId:
+ reportForm.productionOrderRoutingOperationId,
+ productionOrderId: reportForm.productionOrderId,
+ workHour: reportForm.workHour,
+ productionOperationParamList: productionOperationParamList,
+ };
+
+ addProductMain(submitParams)
+ .then(res => {
+ proxy.$modal.msgSuccess("鎶ュ伐鎴愬姛");
+ reportDialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ ElMessageBox.alert("鎶ュ伐澶辫触", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ });
+ });
+ });
+ };
+
+ const handleUserChange = val => {
+ const user = userOptions.value.find(item => item.userId === val);
+ reportForm.userName = user ? user.nickName : "";
+ };
+
+ const getDictOptions = async dictType => {
+ if (!dictType) return [];
+ if (dictOptions.value[dictType]) return dictOptions.value[dictType];
+ try {
+ const res = await getDicts(dictType);
+ if (res.code === 200) {
+ dictOptions.value[dictType] = res.data;
+ return res.data;
+ }
+ return [];
+ } catch (error) {
+ console.error("鑾峰彇瀛楀吀鏁版嵁澶辫触:", error);
+ return [];
+ }
+ };
+
+ const loadParams = (productionOrderRoutingOperationId, productionOrderId) => {
+ paramLoading.value = true;
+ findProcessParamListOrder({
+ productionOrderRoutingOperationId,
+ productionOrderId,
+ })
+ .then(res => {
+ if (res.code === 200) {
+ const paramList = res.data || [];
+ params.value = paramList;
+ reportForm.paramGroups = {};
+ paramList.forEach(param => {
+ if (!reportForm.paramGroups[param.id]) {
+ reportForm.paramGroups[param.id] = "";
+ }
+ if (param.paramType == "3" && param.paramFormat) {
+ getDictOptions(param.paramFormat);
+ }
+ });
+ }
+ })
+ .catch(err => {
+ console.error("鑾峰彇宸ュ簭鍙傛暟澶辫触:", err);
+ })
+ .finally(() => {
+ paramLoading.value = false;
+ });
+ };
+
+ onMounted(() => {
+ userStore.getInfo();
+ getList();
+ // 鑾峰彇鐢ㄦ埛鍒楄〃
+ userListNoPageByTenantId().then(res => {
+ if (res.code === 200) {
+ userOptions.value = res.data;
+ }
+ });
+ });
+</script>
+
+<style scoped lang="scss">
+ .search-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+ .search-item {
+ display: flex;
+ align-items: center;
+ }
+ .search_title {
+ margin-right: 8px;
+ font-size: 14px;
+ color: #606266;
+ }
+ .transfer-card-title {
+ text-align: center;
+ font-size: 24px;
+ font-weight: bold;
+ margin-bottom: 20px;
+ color: #303133;
+ }
+ .transfer-card-container {
+ display: flex;
+ justify-content: space-between;
+ padding: 20px;
+ }
+ .transfer-card-info {
+ flex: 1;
+ margin-right: 20px;
+ }
+ .info-group {
+ margin-bottom: 20px;
+ }
+ .info-item {
+ display: flex;
+ margin-bottom: 10px;
+ }
+ .info-label {
+ width: 100px;
+ font-weight: bold;
+ color: #606266;
+ }
+ .info-value {
+ flex: 1;
+ color: #303133;
+ }
+ .transfer-card-qr {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ .qr-container {
+ text-align: center;
+ }
+ .print-button-container {
+ text-align: center;
+ margin-top: 20px;
+ }
+ .param-grid {
+ margin-top: 10px;
+ border-top: 1px solid #ebe9f3;
+ padding-top: 10px;
+ }
+ .param-item {
+ margin-bottom: 12px;
+ }
+ .param-input-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .param-input {
+ flex: 1;
+ }
+ .param-select {
+ flex: 1;
+ }
+ .param-unit {
+ color: #909399;
+ font-size: 12px;
+ min-width: 30px;
+ }
+</style>
+
+<style lang="scss">
+ @media print {
+ @page {
+ size: landscape;
+ }
+ body * {
+ visibility: hidden;
+ }
+ .el-dialog__wrapper,
+ .el-dialog,
+ .el-dialog__body,
+ .transfer-card-title,
+ .transfer-card-container,
+ .transfer-card-container *,
+ .info-item,
+ .info-label,
+ .info-value {
+ visibility: visible;
+ }
+ .print-button-container {
+ visibility: hidden;
+ }
+ .el-dialog__wrapper {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ margin: 0;
+ }
+ .el-dialog {
+ width: 100% !important;
+ max-width: 800px;
+ margin: 0 auto !important;
+ }
+ .el-dialog__header,
+ .el-dialog__footer {
+ display: none;
+ }
+ .el-dialog__body {
+ padding: 20px;
+ }
+ .transfer-card-container {
+ height: auto;
+ display: flex;
+ gap: 20px;
+ }
+ .transfer-card-info {
+ flex: 1;
+ .info-group {
+ width: 100%;
+ float: none;
+ margin-bottom: 20px;
+ }
+ .info-item {
+ display: flex;
+ margin-bottom: 10px;
+ .info-label {
+ width: 100px;
+ font-weight: bold;
+ margin-right: 15px;
+ white-space: nowrap;
+ }
+ .info-value {
+ flex: 1;
+ word-break: break-word;
+ }
+ }
+ }
+ .transfer-card-qr {
+ width: 160px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: flex-start;
+ }
+ .qr-container img {
+ width: 140px !important;
+ height: 140px !important;
+ }
+ }
+</style>
diff --git a/src/views/productionPlan/productionPlan/components/PIMTable.vue b/src/views/productionPlan/productionPlan/components/PIMTable.vue
new file mode 100644
index 0000000..5f5eea5
--- /dev/null
+++ b/src/views/productionPlan/productionPlan/components/PIMTable.vue
@@ -0,0 +1,471 @@
+<template>
+ <el-table ref="multipleTable"
+ v-loading="tableLoading"
+ :border="border"
+ :data="tableData"
+ :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
+ :height="height"
+ :highlight-current-row="highlightCurrentRow"
+ :row-class-name="rowClassName"
+ :row-style="rowStyle"
+ :row-key="rowKey"
+ :style="tableStyle"
+ tooltip-effect="dark"
+ :tooltip-options="{ appendTo: 'body' }"
+ :expand-row-keys="expandRowKeys"
+ :show-summary="isShowSummary"
+ :summary-method="summaryMethod"
+ @row-click="rowClick"
+ @current-change="currentChange"
+ @selection-change="handleSelectionChange"
+ @expand-change="expandChange"
+ @select-all="handleSelectAll"
+ class="lims-table">
+ <el-table-column align="center"
+ type="selection"
+ width="55"
+ v-if="isSelection"
+ :selectable="selectable" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column v-for="(item, index) in column"
+ :key="index"
+ :column-key="item.columnKey"
+ :filter-method="item.filterHandler"
+ :filter-multiple="item.filterMultiple"
+ :filtered-value="item.filteredValue"
+ :filters="item.filters"
+ :fixed="item.fixed"
+ :label="item.label"
+ :prop="item.prop"
+ :show-overflow-tooltip="item.dataType !== 'action' && item.dataType !== 'slot'"
+ :align="item.align"
+ :sortable="!!item.sortable"
+ :type="item.type"
+ :width="item.width"
+ :class-name="item.className || ''">
+ <template #header="scope">
+ <div class="pim-table-header-cell">
+ <div class="pim-table-header-title">
+ {{ item.label }}
+ </div>
+ <div v-if="item.headerSlot"
+ class="pim-table-header-extra">
+ <slot :name="item.headerSlot"
+ :column="scope.column" />
+ </div>
+ </div>
+ </template>
+ <template v-if="item.hasOwnProperty('colunmTemplate')"
+ #[item.colunmTemplate]="scope">
+ <slot v-if="item.theadSlot"
+ :name="item.theadSlot"
+ :index="scope.$index"
+ :row="scope.row" />
+ </template>
+ <template #default="scope">
+ <!-- 鎻掓Ы -->
+ <div v-if="item.dataType == 'slot'"
+ :class="item.className || ''">
+ <slot v-if="item.slot"
+ :index="scope.$index"
+ :name="item.slot"
+ :row="scope.row" />
+ </div>
+ <!-- 杩涘害鏉� -->
+ <div v-else-if="item.dataType == 'progress'"
+ :class="item.className || ''">
+ <el-progress :percentage="Number(scope.row[item.prop])" />
+ </div>
+ <!-- 鍥剧墖 -->
+ <div v-else-if="item.dataType == 'image'"
+ :class="item.className || ''">
+ <img :src="javaApi + '/img/' + scope.row[item.prop]"
+ alt=""
+ style="width: 40px; height: 40px; margin-top: 10px" />
+ </div>
+ <!-- tag -->
+ <div v-else-if="item.dataType == 'tag'"
+ :class="item.className || ''">
+ <el-tag v-if="
+ typeof dataTypeFn(scope.row[item.prop], item.formatData) ===
+ 'string'
+ "
+ :title="formatters(scope.row[item.prop], item.formatData)"
+ :type="formatType(scope.row[item.prop], item.formatType)">
+ {{ formatters(scope.row[item.prop], item.formatData) }}
+ </el-tag>
+ <el-tag v-for="(tag, index) in dataTypeFn(
+ scope.row[item.prop],
+ item.formatData
+ )"
+ v-else-if="
+ typeof dataTypeFn(scope.row[item.prop], item.formatData) ===
+ 'object'
+ "
+ :key="index"
+ :title="formatters(scope.row[item.prop], item.formatData)"
+ :type="formatType(tag, item.formatType)">
+ {{ item.tagGroup ? tag[item.tagGroup.label] ?? tag : tag }}
+ </el-tag>
+ <el-tag v-else
+ :title="formatters(scope.row[item.prop], item.formatData)"
+ :type="formatType(scope.row[item.prop], item.formatType)">
+ {{ formatters(scope.row[item.prop], item.formatData) }}
+ </el-tag>
+ </div>
+ <!-- 鎸夐挳 -->
+ <div v-else-if="item.dataType == 'action'"
+ :class="item.className || ''"
+ @click.stop>
+ <template v-for="(o, key) in item.operation"
+ :key="key">
+ <el-button v-show="o.type != 'upload'"
+ v-if="o.showHide ? o.showHide(scope.row) : true"
+ :disabled="o.disabled ? o.disabled(scope.row) : false"
+ :plain="o.plain"
+ type="primary"
+ :style="{
+ color:
+ o.name === '鍒犻櫎' || o.name === 'delete'
+ ? '#f56c6c'
+ : o.color,
+ }"
+ link
+ @click.stop="o.clickFun(scope.row)"
+ :key="key">
+ {{ o.name }}
+ </el-button>
+ <el-upload :action="
+ javaApi +
+ o.url +
+ '?id=' +
+ (o.uploadIdFun ? o.uploadIdFun(scope.row) : scope.row.id)
+ "
+ ref="uploadRef"
+ :multiple="o.multiple ? o.multiple : false"
+ :limit="1"
+ :disabled="o.disabled ? o.disabled(scope.row) : false"
+ :accept="
+ o.accept
+ ? o.accept
+ : '.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar'
+ "
+ v-if="o.type == 'upload'"
+ style="display: inline-block; width: 50px"
+ v-show="o.showHide ? o.showHide(scope.row) : true"
+ :headers="uploadHeader"
+ :before-upload="(file) => beforeUpload(file, scope.$index)"
+ :on-change="
+ (file, fileList) => handleChange(file, fileList, scope.$index)
+ "
+ :on-error="
+ (error, file, fileList) =>
+ onError(error, file, fileList, scope.$index)
+ "
+ :on-success="
+ (response, file, fileList) =>
+ handleSuccessUp(response, file, fileList, scope.$index)
+ "
+ :on-exceed="onExceed"
+ :show-file-list="false">
+ <el-button link
+ type="primary"
+ :disabled="o.disabled ? o.disabled(scope.row) : false">{{ o.name }}</el-button>
+ </el-upload>
+ </template>
+ </div>
+ <!-- 鍙偣鍑荤殑鏂囧瓧 -->
+ <div v-else-if="item.dataType == 'link'"
+ :class="item.className || ''"
+ class="cell link"
+ style="width: 100%"
+ @click="goLink(scope.row, item.linkMethod)">
+ <span v-if="!item.formatData">{{ 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-else>{{
+ formatters(scope.row[item.prop], item.formatData)
+ }}</span>
+ </div>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-if="isShowPagination"
+ :total="page.total"
+ :layout="page.layout"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationSearch" />
+</template>
+
+<script setup>
+ import pagination from "../../../../components/PIMTable/Pagination.vue";
+ import { ref, inject, getCurrentInstance } from "vue";
+ import { ElMessage } from "element-plus";
+
+ // 鑾峰彇鍏ㄥ眬鐨� uploadHeader
+ const { proxy } = getCurrentInstance();
+ const uploadHeader = proxy.uploadHeader;
+ const javaApi = proxy.javaApi;
+
+ const emit = defineEmits([
+ "pagination",
+ "expand-change",
+ "selection-change",
+ "row-click",
+ ]);
+
+ // Filters
+ const typeFn = (val, row) => {
+ return typeof val === "function" ? val(row) : val;
+ };
+
+ const formatters = (val, format) => {
+ return typeof format === "function" ? format(val) : val;
+ };
+
+ // Props锛堜娇鐢� defineProps 鐨勯潪 TS 褰㈠紡锛�
+ const props = defineProps({
+ tableLoading: {
+ type: Boolean,
+ default: false,
+ },
+ height: {
+ type: [Number, String],
+ default: "calc(100vh - 22em)",
+ },
+ expandRowKeys: {
+ type: Array,
+ default: () => [],
+ },
+ summaryMethod: {
+ type: Function,
+ default: () => {},
+ },
+ rowClick: {
+ type: Function,
+ default: () => {},
+ },
+ currentChange: {
+ type: Function,
+ default: () => {},
+ },
+ border: {
+ type: Boolean,
+ default: true,
+ },
+ isSelection: {
+ type: Boolean,
+ default: false,
+ },
+ selectable: {
+ type: Function,
+ default: () => true,
+ },
+ isShowPagination: {
+ type: Boolean,
+ default: true,
+ },
+ isShowSummary: {
+ type: Boolean,
+ default: false,
+ },
+ highlightCurrentRow: {
+ type: Boolean,
+ default: false,
+ },
+ headerCellStyle: {
+ type: Object,
+ default: () => ({}),
+ },
+ column: {
+ type: Array,
+ default: () => [],
+ },
+ rowClassName: {
+ type: Function,
+ default: () => "",
+ },
+ rowStyle: {
+ type: [Object, Function],
+ default: () => ({}),
+ },
+ tableData: {
+ type: Array,
+ default: () => [],
+ },
+ rowKey: {
+ type: String,
+ default: "id",
+ },
+ page: {
+ type: Object,
+ default: () => ({
+ total: 0,
+ current: 0,
+ size: 10,
+ layout: "total, sizes, prev, pager, next, jumper",
+ }),
+ },
+ total: {
+ type: Number,
+ default: 0,
+ },
+ tableStyle: {
+ type: [String, Object],
+ default: () => ({ width: "100%" }),
+ },
+ });
+
+ // Data
+ const multipleTable = ref(null);
+ const uploadRefs = ref([]);
+ const currentFiles = ref({});
+ const uploadKeys = ref({});
+
+ const indexMethod = index => {
+ return (props.page.current - 1) * props.page.size + index + 1;
+ };
+
+ // 鐐瑰嚮 link 浜嬩欢
+ const goLink = (row, linkMethod) => {
+ if (!linkMethod) {
+ return ElMessage.warning("璇烽厤缃� link 浜嬩欢");
+ }
+ const parentMethod = getParentMethod(linkMethod);
+ if (typeof parentMethod === "function") {
+ parentMethod(row);
+ } else {
+ console.warn(`鐖剁粍浠朵腑鏈壘鍒版柟娉�: ${linkMethod}`);
+ }
+ };
+
+ // 鑾峰彇鐖剁粍浠舵柟娉曪紙绀轰緥瀹炵幇锛�
+ const getParentMethod = methodName => {
+ const parentMethods = inject("parentMethods", {});
+ return parentMethods[methodName];
+ };
+
+ const dataTypeFn = (val, format) => {
+ if (typeof format === "function") {
+ return format(val);
+ } else return val;
+ };
+
+ const formatType = (val, format) => {
+ if (typeof format === "function") {
+ return format(val);
+ } else return "";
+ };
+
+ // 鏂囦欢鍙樺寲澶勭悊
+ const handleChange = (file, fileList, index) => {
+ if (fileList.length > 1) {
+ const earliestFile = fileList[0];
+ uploadRefs.value[index]?.handleRemove(earliestFile);
+ }
+ currentFiles.value[index] = file;
+ };
+
+ // 鏂囦欢涓婁紶鍓嶆牎楠�
+ const beforeUpload = (rawFile, index) => {
+ currentFiles.value[index] = {};
+ if (rawfile.size > 1024 * 1024 * 10 * 10) {
+ ElMessage.error("涓婁紶鏂囦欢涓嶈秴杩�10M");
+ return false;
+ }
+ return true;
+ };
+
+ // 涓婁紶鎴愬姛
+ const handleSuccessUp = (response, file, fileList, index) => {
+ if (response.code == 200) {
+ if (uploadRefs[index]) {
+ uploadRefs[index].clearFiles();
+ }
+ currentFiles[index] = file;
+ ElMessage.success("涓婁紶鎴愬姛");
+ resetUploadComponent(index);
+ } else {
+ ElMessage.error(response.message);
+ }
+ };
+
+ const resetUploadComponent = index => {
+ uploadKeys[index] = Date.now();
+ };
+
+ // 涓婁紶澶辫触
+ const onError = (error, file, fileList, index) => {
+ ElMessage.error("鏂囦欢涓婁紶澶辫触锛岃閲嶈瘯");
+ if (uploadRefs.value[index]) {
+ uploadRefs.value[index].clearFiles();
+ }
+ };
+
+ // 鏂囦欢鏁伴噺瓒呴檺鎻愮ず
+ const onExceed = () => {
+ ElMessage.warning("瓒呭嚭鏂囦欢涓暟");
+ };
+
+ const paginationSearch = ({ page, limit }) => {
+ emit("pagination", { page: page, limit: limit });
+ };
+
+ const rowClick = row => {
+ emit("row-click", row);
+ };
+
+ const expandChange = (row, expandedRows) => {
+ emit("expand-change", row, expandedRows);
+ };
+
+ const handleSelectionChange = newSelection => {
+ emit("selection-change", newSelection);
+ };
+
+ // 澶勭悊鍏ㄩ�夋搷浣�
+ const handleSelectAll = selection => {
+ if (selection.length) {
+ console.log(selection, "selection");
+ // 鍏ㄩ�夋椂锛屽彧閫夋嫨鍙�夋嫨鐨勮
+ const selectableRows = props.tableData.filter(row => props.selectable(row));
+ // 娓呯┖褰撳墠閫夋嫨
+ multipleTable.value.clearSelection();
+ // 鍙�夋嫨鍙�夋嫨鐨勮
+ selectableRows.forEach(row => {
+ multipleTable.value.toggleRowSelection(row, true);
+ });
+ } else {
+ // 鍙栨秷鍏ㄩ�夋椂锛屽彧鍙栨秷鍙�夋嫨鐨勮鐨勯�夋嫨
+ props.tableData.forEach(row => {
+ if (props.selectable(row)) {
+ multipleTable.value.toggleRowSelection(row, false);
+ }
+ });
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ .cell {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-right: 0 !important;
+ padding-left: 0 !important;
+ }
+
+ .pim-table-header-extra :deep(.el-input),
+ .pim-table-header-extra :deep(.el-select) {
+ width: 100%;
+ }
+</style>
diff --git a/src/views/productionPlan/productionPlan/index.vue b/src/views/productionPlan/productionPlan/index.vue
new file mode 100644
index 0000000..e5bc1f8
--- /dev/null
+++ b/src/views/productionPlan/productionPlan/index.vue
@@ -0,0 +1,1371 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm"
+ ref="queryRef"
+ :inline="true">
+ <!-- 绠�鍖栫増鎼滅储鏉′欢 -->
+ <el-form-item label="涓荤敓浜ц鍒掑彿:"
+ prop="mpsNo">
+ <el-input v-model="searchForm.mpsNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 160px;"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="閿�鍞悎鍚屽彿:"
+ prop="salesContractNo">
+ <el-input v-model="searchForm.salesContractNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 160px;"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="闇�姹傛棩鏈熻寖鍥�:"
+ prop="dateRange">
+ <el-date-picker v-model="searchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 240px;"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="涓嬪彂鐘舵��:"
+ prop="status">
+ <el-select v-model="searchForm.status"
+ placeholder="璇烽�夋嫨鐘舵��"
+ clearable
+ filterable
+ style="width: 100px">
+ <el-option label="寰呬笅鍙�"
+ value="0" />
+ <el-option label="閮ㄥ垎涓嬪彂"
+ value="1" />
+ <el-option label="宸蹭笅鍙�"
+ value="2" />
+ </el-select>
+ </el-form-item>
+ <!-- 灞曞紑鐗堟悳绱㈡潯浠� -->
+ <el-form-item label="浜у搧鍚嶇О:"
+ prop="productName">
+ <el-input v-model="searchForm.productName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 160px;"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿:"
+ prop="model">
+ <el-input v-model="searchForm.model"
+ placeholder="璇疯緭鍏�"
+ clearable
+ style="width: 160px;"
+ @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery">鎼滅储</el-button>
+ <el-button type="info"
+ @click="handleReset">閲嶇疆</el-button>
+ <el-button type="primary"
+ @click="handleAdd">鏂板</el-button>
+ <el-button type="warning"
+ @click="handleMerge">鍚堝苟涓嬪彂</el-button>
+ <el-button type="warning"
+ @click="handleImport">瀵煎叆</el-button>
+ <el-button type="warning"
+ @click="handleExport">瀵煎嚭</el-button>
+ </el-form-item>
+ </el-form>
+ <div>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ height="calc(100vh - 350px)"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ :selectable="isSelectable"
+ @selection-change="handleSelectionChange"
+ @pagination="pagination">
+ <template #qtyRequired="{ row }">
+ {{ row.qtyRequired || '-' }}<span style="color:rgba(12, 46, 40, 0.76)"> {{ row.unit || '鏂�' }}</span>
+ </template>
+ <template #salesContractNo="{ row }">
+ <el-button type="primary"
+ text
+ link
+ @click="showDetail(row)">{{ row.salesContractNo }}
+ </el-button>
+ </template>
+ </PIMTable>
+ </div>
+ <!-- 鍚堝苟涓嬪彂寮圭獥 -->
+ <el-dialog v-model="isShowNewModal"
+ destroy-on-close
+ title="鍚堝苟涓嬪彂"
+ width="600px">
+ <el-form :model="mergeForm"
+ label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="10">
+ <el-form-item label="浜у搧鍚嶇О">
+ <el-tag class="info-display">{{ mergeForm.productName || '-' }}</el-tag>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col>
+ <el-form-item label="瑙勬牸鍨嬪彿">
+ <div class="info-display">{{ mergeForm.model || '-' }}</div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="璁″垝瀹屾垚鏃堕棿">
+ <el-date-picker v-model="mergeForm.planCompleteTime"
+ type="date"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="鐢熶骇鏁伴噺">
+ <el-input-number v-model="mergeForm.totalAssignedQuantity"
+ :min="0"
+ :max="sumAssignedQuantity"
+ @change="onBlur"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿">
+ <el-date-picker v-model="mergeCreateTimeDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ style="width: 100%" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleMergeSubmit">纭畾涓嬪彂</el-button>
+ <el-button @click="isShowNewModal = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 瀵煎叆寮圭獥 -->
+ <ImportDialog ref="importDialogRef"
+ v-model="importDialogVisible"
+ title="瀵煎叆鐢熶骇璁″垝"
+ :action="importAction"
+ :headers="importHeaders"
+ :auto-upload="false"
+ :on-success="handleImportSuccess"
+ :on-error="handleImportError"
+ @confirm="handleImportConfirm"
+ @download-template="handleDownloadTemplate"
+ @close="handleImportClose" />
+ <!-- 鏂板/缂栬緫寮圭獥 -->
+ <el-dialog v-model="dialogVisible"
+ destroy-on-close
+ :title="operationType === 'add' ? '鏂板鐢熶骇璁″垝' : '缂栬緫鐢熶骇璁″垝'"
+ width="600px">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120px">
+ <el-form-item label="涓荤敓浜ц鍒掑彿"
+ prop="mpsNo">
+ <el-input v-model="form.mpsNo"
+ disabled
+ placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productId">
+ <el-tree-select v-model="form.productId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :data="productOptions"
+ :render-after-expand="false"
+ filterable
+ @change="handleProductChange"
+ style="width: 100%" />
+ </el-form-item>
+ <el-form-item label="瑙勬牸鍨嬪彿"
+ prop="productModelId">
+ <el-select v-model="form.productModelId"
+ @change="handleChangeSpecification"
+ filterable
+ style="width: 100%"
+ placeholder="璇烽�夋嫨">
+ <el-option v-for="item in specificationOptions"
+ :key="item.id"
+ :label="item.model"
+ :value="item.id" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎵�闇�鏁伴噺"
+ prop="qtyRequired">
+ <el-input-number v-model="form.qtyRequired"
+ :min="0"
+ style="width: 100%"
+ placeholder="璇疯緭鍏ユ暟閲�" />
+ </el-form-item>
+ <el-form-item label="鍗曚綅"
+ prop="unit">
+ <el-input v-model="form.unit"
+ placeholder="璇疯緭鍏ュ崟浣�" />
+ </el-form-item>
+ <el-form-item label="闇�姹傛棩鏈�"
+ prop="requiredDate">
+ <el-date-picker v-model="form.requiredDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ placeholder="璇烽�夋嫨闇�姹傛棩鏈�" />
+ </el-form-item>
+ <el-form-item label="鎵胯鏃ユ湡"
+ prop="promisedDeliveryDate">
+ <el-date-picker v-model="form.promisedDeliveryDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ placeholder="璇烽�夋嫨鎵胯鏃ユ湡" />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿"
+ prop="createTime">
+ <el-date-picker v-model="formCreateTimeDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ placeholder="璇烽�夋嫨鍒涘缓鏃堕棿" />
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleSubmit">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import {
+ reactive,
+ ref,
+ onMounted,
+ toRefs,
+ getCurrentInstance,
+ computed,
+ } from "vue";
+ import { useRouter } from "vue-router";
+ import dayjs from "dayjs";
+ import { ElMessage } from "element-plus";
+ import { ArrowUp, ArrowDown } from "@element-plus/icons-vue";
+ import { getToken } from "@/utils/auth";
+ import { useDict } from "@/utils/dict";
+ import {
+ productionPlanListPage,
+ productionPlanAdd,
+ productionPlanUpdate,
+ productionPlanDelete,
+ productionPlanCombine,
+ exportProductionPlan,
+ } from "@/api/productionPlan/productionPlan.js";
+ import { productTreeList, modelListPage } from "@/api/basicData/product.js";
+ import PIMTable from "./components/PIMTable.vue";
+ import ImportDialog from "@/components/Dialog/ImportDialog.vue";
+
+ const { proxy } = getCurrentInstance();
+ const router = useRouter();
+
+ const loadProdData = () => {
+ console.log("Mock loadProdData called");
+ return Promise.resolve({ code: 200, msg: "鍚屾鎴愬姛" });
+ };
+
+ // const productionPlanCombine = payload => {
+ // console.log("Mock productionPlanCombine called with:", payload);
+ // return Promise.resolve({ code: 200, msg: "鍚堝苟涓嬪彂鎴愬姛" });
+ // };
+
+ const tableColumn = ref([
+ {
+ label: "涓荤敓浜ц鍒掑彿",
+ prop: "mpsNo",
+ width: "150px",
+ },
+ {
+ label: "鏉ユ簮",
+ prop: "source",
+ width: "150px",
+ dataType: "tag",
+ formatType: params => {
+ return params == "閿�鍞�" ? "primary" : "info";
+ },
+ formatData: params => {
+ return params == "閿�鍞�" ? "閿�鍞�" : "鍐呴儴";
+ },
+ },
+ {
+ label: "閿�鍞悎鍚屽彿",
+ prop: "salesContractNo",
+ width: "200px",
+ dataType: "slot",
+ slot: "salesContractNo",
+ },
+
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ width: "200px",
+ dataType: "tag",
+ formatType: params => {
+ return "primary";
+ },
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ width: "150px",
+ className: "spec-cell",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ width: "100px",
+ },
+ {
+ label: "鎵�闇�鏁伴噺",
+ prop: "qtyRequired",
+ width: "150px",
+ dataType: "slot",
+ slot: "qtyRequired",
+ className: "volume-cell",
+ },
+ {
+ label: "涓嬪彂鐘舵��",
+ prop: "status",
+ width: "120px",
+ className: "status-cell",
+ dataType: "tag",
+ formatType: params => {
+ const typeMap = {
+ 0: "warning",
+ 1: "primary",
+ 2: "info",
+ };
+ return typeMap[params] || "info";
+ },
+ formatData: cell => {
+ const statusMap = {
+ 0: "寰呬笅鍙�",
+ 1: "閮ㄥ垎涓嬪彂",
+ 2: "宸蹭笅鍙�",
+ };
+ return statusMap[cell] || "";
+ },
+ },
+ {
+ label: "宸蹭笅鍙戞暟閲�",
+ prop: "quantityIssued",
+ width: "120px",
+ className: "spec-cell",
+ // formatData: (cell, row) => (cell ? `${cell}${row.unit || "鏂�"}` : 0),
+ },
+ {
+ label: "闇�姹傛棩鏈�",
+ prop: "requiredDate",
+ width: "160px",
+ className: "date-cell",
+ formatData: cell => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
+ },
+ {
+ label: "鎵胯鏃ユ湡",
+ prop: "promisedDeliveryDate",
+ width: "160px",
+ className: "date-cell",
+ formatData: cell => (cell ? dayjs(cell).format("YYYY-MM-DD") : ""),
+ },
+
+ {
+ label: "瀹㈡埛鍚嶇О",
+ prop: "customerName",
+ width: "150px",
+ },
+ {
+ label: "椤圭洰鍚嶇О",
+ prop: "projectName",
+ width: "150px",
+ },
+ {
+ label: "澶囨敞",
+ width: "150px",
+ prop: "remark",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 250,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "primary",
+ link: true,
+ showHide: row => {
+ return row.status == 0 && row.source != "閿�鍞�";
+ },
+ clickFun: row => {
+ handleEdit(row);
+ },
+ },
+ {
+ name: "涓嬪彂",
+ type: "text",
+ showHide: row => {
+ return row.status != 2;
+ },
+ clickFun: row => {
+ mergeForm.productName = row.productName || "";
+ mergeForm.model = row.model || "";
+ mergeForm.totalAssignedQuantity =
+ Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0);
+ mergeForm.planCompleteTime = row.requiredDate || "";
+ mergeForm.productId = row.productId || "";
+ mergeForm.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ mergeForm.ids = [row.id];
+ sumAssignedQuantity.value =
+ Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0);
+ isShowNewModal.value = true;
+ },
+ },
+ {
+ name: "鍒犻櫎",
+ type: "danger",
+ link: true,
+ showHide: row => {
+ return row.status == 0;
+ },
+ clickFun: row => {
+ handleDelete(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const selectedRows = ref([]);
+
+ // 浜у搧绫诲埆姹囨�荤粺璁℃暟鎹�
+ const categorySummary = ref([]);
+ // 浜у搧绫诲埆姹囨�诲脊绐楁帶鍒�
+ const showCategorySummaryDialog = ref(false);
+
+ // 鍚堝苟涓嬪彂寮圭獥鎺у埗
+ const isShowNewModal = ref(false);
+ // 鍚堝苟涓嬪彂琛ㄥ崟鏁版嵁
+ const mergeForm = reactive({
+ productName: "",
+ model: "",
+ totalAssignedQuantity: 0,
+ planCompleteTime: "",
+ productId: "",
+ createTime: "",
+ });
+ const mergeCreateTimeDate = computed({
+ get: () => (mergeForm.createTime ? String(mergeForm.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ mergeForm.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
+
+ // 瀵煎叆鐩稿叧
+ const importDialogRef = ref(null);
+ const importDialogVisible = ref(false);
+ const importAction =
+ import.meta.env.VITE_APP_BASE_API + "/productionPlan/import";
+ const importHeaders = ref({
+ Authorization: `Bearer ${getToken()}`,
+ });
+
+ // 鏂板/缂栬緫鐩稿叧
+ const dialogVisible = ref(false);
+ const operationType = ref("add"); // add | edit
+ const productOptions = ref([]);
+ const specificationOptions = ref([]);
+ const queryRef = ref(null);
+ const formRef = ref(null);
+ const form = reactive({
+ id: undefined,
+ mpsNo: "",
+ productId: undefined,
+ productModelId: undefined,
+ productName: "",
+ model: "",
+ qtyRequired: 0,
+ unit: "鏂�",
+ requiredDate: "",
+ promisedDeliveryDate: "",
+ remark: "",
+ createTime: "",
+ });
+ const formCreateTimeDate = computed({
+ get: () => (form.createTime ? String(form.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
+ const rules = reactive({
+ productId: [{ required: true, message: "璇烽�夋嫨浜у搧", trigger: "change" }],
+ productModelId: [
+ { required: true, message: "璇烽�夋嫨瑙勬牸鍨嬪彿", trigger: "change" },
+ ],
+ qtyRequired: [{ required: true, message: "璇疯緭鍏ユ暟閲�", trigger: "blur" }],
+ requiredDate: [
+ { required: true, message: "璇烽�夋嫨闇�姹傛棩鏈�", trigger: "change" },
+ ],
+ });
+
+ // 澶勭悊杩借釜杩涘害鎸夐挳鐐瑰嚮
+ const handleTrackProgress = row => {
+ // 璺宠浆鍒拌拷韪繘搴﹂〉闈�
+ router.push({
+ path: "/productionPlan/trackProgress",
+ query: {
+ id: row.id,
+ productName: row.productName,
+ model: row.model,
+ },
+ });
+ };
+ const onBlur = value => {
+ // 闄愬埗鍥涗綅灏忔暟
+ mergeForm.totalAssignedQuantity = Number(value.toFixed(4));
+ };
+
+ const fetchProductOptions = () => {
+ return productTreeList().then(res => {
+ productOptions.value = convertIdToValue(res || []);
+ });
+ };
+
+ const convertIdToValue = data => {
+ return data.map(item => {
+ const newItem = {
+ value: item.id,
+ label: item.label,
+ };
+ if (item.children && item.children.length > 0) {
+ newItem.children = convertIdToValue(item.children);
+ }
+ return newItem;
+ });
+ };
+
+ const handleProductChange = value => {
+ form.productModelId = undefined;
+ form.model = undefined;
+ // 鏌ユ壘閫変腑鐨勪骇鍝佸悕绉�
+ const findProductName = (options, val) => {
+ for (const option of options) {
+ if (option.value === val) {
+ return option.label;
+ }
+ if (option.children) {
+ const found = findProductName(option.children, val);
+ if (found) {
+ return found;
+ }
+ }
+ }
+ return "";
+ };
+ form.productName = findProductName(productOptions.value, value);
+ fetchSpecificationOptions(value);
+ };
+
+ const fetchSpecificationOptions = productId => {
+ specificationOptions.value = [];
+ if (productId) {
+ modelListPage({ id: productId, size: 1000, current: 1 }).then(res => {
+ specificationOptions.value = res.records || [];
+ });
+ }
+ };
+
+ const handleChangeSpecification = value => {
+ form.model = undefined;
+ form.unit = "";
+ const selectedModel = specificationOptions.value.find(
+ item => item.id === value
+ );
+ if (selectedModel) {
+ form.model = selectedModel.model;
+ form.unit = selectedModel.unit || "鏂�";
+ }
+ };
+
+ const data = reactive({
+ searchForm: {
+ mpsNo: "",
+ salesContractNo: "",
+ productName: "",
+ model: "",
+ status: "",
+ dateRange: [],
+ },
+ searchFormExpanded: false,
+ });
+ const { searchForm, searchFormExpanded } = toRefs(data);
+
+ // 鍒囨崲鎼滅储琛ㄥ崟灞曞紑/鏀惰捣鐘舵��
+ const toggleSearchForm = () => {
+ data.searchFormExpanded = !data.searchFormExpanded;
+ };
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+
+ /** 閲嶇疆鎸夐挳鎿嶄綔 */
+ const handleReset = () => {
+ if (proxy.resetForm) {
+ proxy.resetForm("queryRef");
+ }
+ Object.assign(searchForm.value, {
+ mpsNo: "",
+ salesContractNo: "",
+ productName: "",
+ model: "",
+ status: "",
+ dateRange: [],
+ });
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ // 璁$畻浜у搧绫诲埆姹囨�荤粺璁�
+ const calculateCategorySummary = () => {
+ const summary = {};
+
+ // 閬嶅巻琛ㄦ牸鏁版嵁锛屾寜浜у搧绫诲埆姹囨��
+ tableData.value.forEach(row => {
+ const category = row.productName || "鏈煡浜у搧";
+ if (!summary[category]) {
+ summary[category] = {
+ materialCode: category,
+ totalAssignedQuantity: 0,
+ };
+ }
+ summary[category].totalAssignedQuantity += Number(
+ (Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0)).toFixed(
+ 4
+ )
+ );
+ });
+
+ // 杞崲涓烘暟缁勬牸寮�
+ categorySummary.value = Object.values(summary);
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ // 鏋勯�犳悳绱㈠弬鏁�
+ const params = { ...searchForm.value, ...page };
+ params.requiredDateStart =
+ params.dateRange && params.dateRange.length > 0 ? params.dateRange[0] : "";
+ params.requiredDateEnd =
+ params.dateRange && params.dateRange.length > 1 ? params.dateRange[1] : "";
+ delete params.dateRange;
+ productionPlanListPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ // 璁$畻浜у搧绫诲埆姹囨�荤粺璁�
+ calculateCategorySummary();
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 閫変腑鐨勮鏍煎瀷鍙稩D
+ const selectedProductModelId = ref("");
+
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ // 濡傛灉鏈夐�変腑鐨勮锛岃褰曠涓�涓�変腑琛岀殑瑙勬牸鍨嬪彿ID
+ if (selection.length > 0) {
+ selectedProductModelId.value = selection[0].productModelId;
+ } else {
+ // 濡傛灉娌℃湁閫変腑鐨勮锛屾竻绌鸿鏍煎瀷鍙稩D
+ selectedProductModelId.value = "";
+ }
+ };
+
+ // 鍒ゆ柇琛屾槸鍚﹀彲閫夋嫨
+ const isSelectable = row => {
+ // 濡傛灉鏄凡涓嬪彂鐘舵�侊紝绂佹鍕鹃��
+ if (row.status == 2) {
+ return false;
+ }
+ // 璁$畻鍓╀綑鏁伴噺
+ const remainingQty = (row.qtyRequired || 0) - (row.quantityIssued || 0);
+ // 濡傛灉鍓╀綑鏁伴噺灏忎簬绛変簬0锛岀姝㈤�夋嫨
+ if (remainingQty <= 0) {
+ return false;
+ }
+ // 濡傛灉娌℃湁閫変腑鐨勮锛屾墍鏈夎閮藉彲閫夋嫨
+ if (!selectedProductModelId.value) {
+ return true;
+ }
+ // 濡傛灉鏈夐�変腑鐨勮锛屽彧鏈夎鏍煎瀷鍙稩D鐩稿悓鐨勮鎵嶅彲閫夋嫨
+ return row.productModelId === selectedProductModelId.value;
+ };
+ // 鎷夊彇鏁版嵁鎸夐挳鎿嶄綔
+ const loadProdDataLoading = ref(false);
+ const sumAssignedQuantity = ref(0);
+
+ // 澶勭悊鍚堝苟涓嬪彂鎸夐挳鐐瑰嚮
+ const handleMerge = () => {
+ if (selectedRows.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸悎骞朵笅鍙戠殑鐢熶骇璁″垝");
+ return;
+ }
+ console.log(selectedRows.value);
+ const firstRow = selectedRows.value[0];
+ const productName = firstRow.productName || "";
+
+ // 璁$畻鎬诲埗閫犳暟閲� (榛樿qtyRequired鐨勫拰)
+ const totalAssignedQuantity = selectedRows.value.reduce((sum, row) => {
+ return sum + Number(row.qtyRequired || 0) - Number(row.quantityIssued || 0);
+ }, 0);
+ sumAssignedQuantity.value = totalAssignedQuantity;
+ console.log(totalAssignedQuantity);
+ // 璁剧疆琛ㄥ崟鏁版嵁
+ mergeForm.productName = productName;
+ mergeForm.model = firstRow.model || "";
+ mergeForm.totalAssignedQuantity = totalAssignedQuantity;
+ mergeForm.planCompleteTime = firstRow.requiredDate || "";
+ mergeForm.productId = firstRow.productId || "";
+ mergeForm.ids = selectedRows.value.map(row => row.id);
+
+ // 鎵撳紑寮圭獥
+ isShowNewModal.value = true;
+ };
+ const showDetail = row => {
+ router.push({
+ path: "/salesManagement/salesLedger",
+ query: {
+ salesContractNo: row.salesContractNo,
+ },
+ });
+ };
+
+ // 澶勭悊鍚堝苟涓嬪彂鎻愪氦
+ const handleMergeSubmit = () => {
+ if (mergeForm.totalAssignedQuantity === 0) {
+ ElMessage.warning("璇疯緭鍏ョ敓浜ф暟閲�");
+ return;
+ }
+ console.log(sumAssignedQuantity.value, "sumAssignedQuantity");
+
+ // 楠岃瘉totalAssignedQuantity涓嶈兘澶т簬鎬绘柟鏁�
+ if (mergeForm.totalAssignedQuantity > sumAssignedQuantity.value) {
+ ElMessage.error("鐢熶骇鏁伴噺涓嶈兘澶т簬褰撳墠璁$畻鐨勬�诲��");
+ return;
+ }
+
+ console.log(mergeForm, "mergeForm");
+ const payload = {
+ ...mergeForm,
+ };
+ productionPlanCombine(payload)
+ .then(res => {
+ if (res.code === 200) {
+ ElMessage.success("涓嬪彂鎴愬姛");
+ getList();
+ isShowNewModal.value = false;
+ // 鍙互閫夋嫨鍒锋柊鍒楄〃鎴栧叾浠栨搷浣�
+ getList();
+ } else {
+ ElMessage.error(res.message || "涓嬪彂澶辫触");
+ }
+ })
+ .catch(err => {
+ console.error("鍚堝苟涓嬪彂寮傚父锛�", err);
+ ElMessage.error("绯荤粺寮傚父锛屽悎骞朵笅鍙戝け璐�");
+ });
+ // 鍙互閫夋嫨鍒锋柊鍒楄〃鎴栧叾浠栨搷浣�
+ };
+
+ // 瀵煎叆
+ const handleImport = () => {
+ importDialogVisible.value = true;
+ };
+
+ // 瀵煎嚭
+ const handleExport = () => {
+ const fileName = `鐢熶骇璁″垝.xlsx`;
+ exportProductionPlan()
+ .then(res => {
+ // 杩斿洖鐨勬暟鎹槸鍚︿负绌�
+ if (!res) {
+ proxy.$modal.msgError("瀵煎嚭澶辫触锛岃繑鍥炴暟鎹负绌�");
+ return;
+ }
+
+ const blob = new Blob([res], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ const downloadElement = document.createElement("a");
+ const href = window.URL.createObjectURL(blob);
+
+ downloadElement.style.display = "none";
+ downloadElement.href = href;
+ downloadElement.download = fileName;
+
+ document.body.appendChild(downloadElement);
+ downloadElement.click();
+
+ document.body.removeChild(downloadElement);
+ window.URL.revokeObjectURL(href);
+
+ proxy.$modal.msgSuccess("瀵煎嚭鎴愬姛");
+ })
+ .catch(err => {
+ console.error("瀵煎嚭寮傚父锛�", err);
+ proxy.$modal.msgError("绯荤粺寮傚父锛屽鍑哄け璐�");
+ });
+ };
+
+ // 瀵煎叆鎴愬姛
+ const handleImportSuccess = response => {
+ if (response.code === 200) {
+ ElMessage.success("瀵煎叆鎴愬姛");
+ importDialogVisible.value = false;
+ getList();
+ } else {
+ ElMessage.error(response.msg || "瀵煎叆澶辫触");
+ }
+ };
+
+ // 瀵煎叆澶辫触
+ const handleImportError = error => {
+ ElMessage.error("瀵煎叆澶辫触锛岃妫�鏌ユ枃浠舵牸寮忔槸鍚︽纭�");
+ };
+
+ // 纭瀵煎叆
+ const handleImportConfirm = () => {
+ if (importDialogRef.value) {
+ importDialogRef.value.submit();
+ }
+ };
+
+ // 涓嬭浇妯℃澘
+ const handleDownloadTemplate = () => {
+ proxy.download(
+ "/productionPlan/downloadTemplate",
+ {},
+ "鐢熶骇璁″垝瀵煎叆妯℃澘.xlsx"
+ );
+ };
+
+ // 鍏抽棴瀵煎叆寮圭獥
+ const handleImportClose = () => {
+ importDialogVisible.value = false;
+ };
+
+ // 鏂板
+ const handleAdd = () => {
+ operationType.value = "add";
+ Object.assign(form, {
+ id: undefined,
+ mpsNo: "",
+ productName: "",
+ productId: undefined,
+ productModelId: undefined,
+ model: "",
+ qtyRequired: 0,
+ unit: "鏂�",
+ requiredDate: "",
+ promisedDeliveryDate: "",
+ remark: "",
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+ });
+ dialogVisible.value = true;
+ fetchProductOptions();
+ };
+
+ // 缂栬緫
+ const handleEdit = row => {
+ operationType.value = "edit";
+ Object.assign(form, {
+ id: row.id,
+ mpsNo: row.mpsNo || "",
+ productName: row.productName || "",
+ productId: row.productId || undefined,
+ productModelId: row.productModelId || undefined,
+ model: row.model || "",
+ qtyRequired: row.qtyRequired || 0,
+ unit: row.unit || "鏂�",
+ requiredDate: row.requiredDate || "",
+ promisedDeliveryDate: row.promisedDeliveryDate || "",
+ remark: row.remark || "",
+ createTime: row.createTime || "",
+ });
+ dialogVisible.value = true;
+ fetchProductOptions();
+ fetchSpecificationOptions(row.productId);
+ };
+
+ // 鍒犻櫎
+ const handleDelete = row => {
+ proxy.$modal
+ .confirm("纭鍒犻櫎璇ョ敓浜ц鍒掞紵", "鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ productionPlanDelete([row.id])
+ .then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ });
+ })
+ .catch(() => {});
+ };
+
+ // 鎻愪氦琛ㄥ崟
+ const handleSubmit = () => {
+ formRef.value.validate(valid => {
+ if (valid) {
+ if (form.qtyRequired === 0) {
+ proxy.$modal.msgError("鏁伴噺涓嶈兘涓�0");
+ return;
+ }
+ const payload = { ...form };
+ if (operationType.value === "add") {
+ payload.id = null;
+ productionPlanAdd(payload)
+ .then(() => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鏂板澶辫触");
+ });
+ }
+ if (operationType.value === "edit") {
+ productionPlanUpdate(payload)
+ .then(() => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ })
+ .catch(() => {
+ proxy.$modal.msgError("淇敼澶辫触");
+ });
+ }
+ }
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .app-container {
+ padding: 24px;
+ background-color: #f0f2f5;
+ min-height: calc(100vh - 48px);
+ }
+
+ .search_form {
+ // margin-bottom: 24px;
+ padding: 20px;
+ background-color: #ffffff;
+ border-radius: 6px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ transition: all 0.3s ease;
+
+ &:hover {
+ box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.08);
+ }
+ }
+
+ .search-header {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ // margin-bottom: 5px;
+ // padding-bottom: 5px;
+ position: relative;
+ bottom: 35px;
+ // border-bottom: 1px solid #ebeef5;
+ }
+
+ .search-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .search-header .el-button {
+ color: #606266;
+ transition: all 0.3s ease;
+ }
+
+ .search-header .el-button:hover {
+ color: #409eff;
+ }
+
+ .search-header .el-icon {
+ margin-right: 4px;
+ }
+
+ .table_list {
+ // margin-bottom: 24px;
+ background-color: #ffffff;
+ border-radius: 6px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ overflow: hidden;
+ height: calc(100vh - 250px);
+ margin-top: 0px !important;
+ }
+
+ :deep(.el-table) {
+ border: none;
+ border-radius: 6px;
+ overflow: hidden;
+ box-shadow: 0 4px 16px rgba(102, 126, 234, 0.1);
+
+ .el-table__header-wrapper {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+
+ th {
+ background: transparent;
+ font-weight: 600;
+ color: #ffffff;
+ border-bottom: none;
+ padding: 16px 0;
+ letter-spacing: 0.5px;
+ }
+ }
+
+ .el-table__body-wrapper {
+ tr {
+ transition: all 0.3s ease;
+
+ &:hover {
+ background: linear-gradient(
+ 90deg,
+ rgba(102, 126, 234, 0.05) 0%,
+ rgba(118, 75, 162, 0.05) 100%
+ );
+ transform: scale(1.002);
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1);
+ }
+
+ td {
+ border-bottom: 1px solid #f0f0f0;
+ padding: 14px 0;
+ color: #303133;
+ }
+ }
+
+ tr.current-row {
+ background: linear-gradient(
+ 90deg,
+ rgba(102, 126, 234, 0.08) 0%,
+ rgba(118, 75, 162, 0.08) 100%
+ );
+ }
+
+ // 鏁板�煎瓧娈垫牱寮�
+ .volume-cell,
+ .dimension-cell {
+ font-weight: 600;
+ color: #409eff;
+ font-family: "Courier New", monospace;
+ text-shadow: 0 1px 2px rgba(64, 158, 255, 0.2);
+ }
+
+ // 瑙勬牸瀛楁鏍峰紡
+ .spec-cell {
+ color: #67c23a;
+ font-weight: 500;
+
+ padding: 4px 8px;
+ border-radius: 4px;
+ }
+
+ // 缂栫爜瀛楁鏍峰紡
+ .code-cell {
+ color: #e6a23c;
+ font-family: "Courier New", monospace;
+ font-weight: 500;
+ padding: 4px 8px;
+ border-radius: 4px;
+ }
+
+ // 鏃ユ湡瀛楁鏍峰紡
+ .date-cell {
+ color: #909399;
+ font-style: italic;
+ }
+
+ // 鐘舵�佹爣绛炬牱寮�
+ .status-tag {
+ &.pending {
+ background: linear-gradient(135deg, #ffeaa7 0%, #fdcb6e 100%);
+ color: #d63031;
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-weight: 500;
+ box-shadow: 0 2px 4px rgba(253, 203, 110, 0.3);
+ }
+
+ &.processing {
+ background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%);
+ color: #ffffff;
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-weight: 500;
+ box-shadow: 0 2px 4px rgba(9, 132, 227, 0.3);
+ }
+
+ &.completed {
+ background: linear-gradient(135deg, #55efc4 0%, #00b894 100%);
+ color: #ffffff;
+ padding: 4px 12px;
+ border-radius: 12px;
+ font-weight: 500;
+ box-shadow: 0 2px 4px rgba(0, 184, 148, 0.3);
+ }
+ }
+ }
+
+ .el-table__empty-block {
+ padding: 60px 0;
+ background-color: #fafafa;
+ }
+ }
+
+ // 鎿嶄綔鎸夐挳鏍峰紡
+ :deep(.el-table .cell .el-button--text) {
+ padding: 6px 10px;
+ border-radius: 4px;
+ transition: all 0.3s ease;
+ font-weight: 500;
+
+ &:hover {
+ background-color: rgba(64, 158, 255, 0.1);
+ transform: translateY(-1px);
+ box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
+ }
+
+ &:nth-of-type(1) {
+ color: #409eff;
+ background: linear-gradient(
+ 135deg,
+ rgba(64, 158, 255, 0.1) 0%,
+ rgba(64, 158, 255, 0.05) 100%
+ );
+ }
+
+ &:nth-of-type(2) {
+ color: #67c23a;
+ background: linear-gradient(
+ 135deg,
+ rgba(103, 194, 58, 0.1) 0%,
+ rgba(103, 194, 58, 0.05) 100%
+ );
+ }
+ }
+
+ // 淇℃伅灞曠ず鏍峰紡
+ .info-display {
+ border-radius: 6px;
+ color: #303133;
+ font-size: 14px;
+ min-height: 32px;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ }
+
+ .pagination-container {
+ display: flex;
+ justify-content: flex-end;
+ padding: 16px 20px;
+ background-color: #ffffff;
+ border-top: 1px solid #ebeef5;
+ border-radius: 0 0 12px 12px;
+ }
+
+ :deep(.el-button) {
+ transition: all 0.3s ease;
+
+ &:hover {
+ transform: translateY(-1px);
+ }
+ }
+
+ :deep(.el-dialog) {
+ border-radius: 6px;
+ overflow: hidden;
+
+ .el-dialog__header {
+ background-color: #fafafa;
+ border-bottom: 1px solid #ebeef5;
+ padding: 20px 24px;
+
+ .el-dialog__title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+ }
+
+ .el-dialog__body {
+ padding: 24px;
+ }
+
+ .el-dialog__footer {
+ padding: 16px 24px;
+ border-top: 1px solid #ebeef5;
+ background-color: #fafafa;
+ }
+ }
+
+ :deep(.el-form) {
+ .el-form-item {
+ margin-bottom: 20px;
+
+ .el-form-item__label {
+ font-weight: 500;
+ color: #303133;
+ }
+
+ .el-input,
+ .el-select,
+ .el-date-picker,
+ .el-input-number {
+ width: 100%;
+
+ // .el-input__inner {
+ // border-radius: 6px;
+ // border: 1px solid #dcdfe6;
+ // transition: all 0.3s ease;
+
+ // &:focus {
+ // border-color: #409eff;
+ // box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+ // }
+ // }
+ }
+ }
+ }
+
+ :deep(.el-tag) {
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-size: 12px;
+ }
+
+ @media (max-width: 768px) {
+ .app-container {
+ padding: 16px;
+ }
+
+ .search_form {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+
+ .el-form {
+ width: 100%;
+
+ .el-form-item {
+ width: 100%;
+ }
+ }
+
+ > div {
+ width: 100%;
+ display: flex;
+ gap: 12px;
+
+ .el-button {
+ flex: 1;
+ }
+ }
+ }
+
+ :deep(.el-table) {
+ th,
+ td {
+ padding: 10px 0;
+ font-size: 12px;
+ }
+ }
+
+ :deep(.el-dialog) {
+ width: 90% !important;
+ margin: 20px auto !important;
+ }
+ }
+ .consumption-value {
+ font-weight: bold;
+ color: #409eff;
+ }
+
+ .consumption-unit {
+ font-size: 12px;
+ color: #909399;
+ margin-left: 4px;
+ }
+ // .search_form {
+ // :deep(.el-form-item) {
+ // margin-bottom: 0px !important;
+ // }
+ // }
+ :deep(.el-table .el-table__body-wrapper tr td) {
+ background-color: #fff;
+ }
+</style>
diff --git a/src/views/projectManagement/Management/components/formDia.vue b/src/views/projectManagement/Management/components/formDia.vue
new file mode 100644
index 0000000..0db3efe
--- /dev/null
+++ b/src/views/projectManagement/Management/components/formDia.vue
@@ -0,0 +1,1506 @@
+<template>
+ <el-dialog
+ v-model="dialogVisible"
+ :title="dialogTitle"
+ width="95%"
+ top="5vh"
+ destroy-on-close
+ @close="closeDialog"
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="120px"
+ :disabled="isView"
+ >
+ <div class="section">
+ <div class="section-header" @click="toggleSection('base')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鍩虹璧勬枡</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.base" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.base" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鍗曟嵁缂栧彿" prop="billNo">
+ <el-input v-model="form.billNo" placeholder="绯荤粺鐢熸垚" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鍚嶇О" prop="projectName">
+ <el-input v-model="form.projectName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+ <el-input v-model="form.customerName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="绔嬮」鏃ユ湡" prop="setupDate">
+ <el-date-picker
+ v-model="form.setupDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰鏉ユ簮" prop="projectSource">
+ <el-input v-model="form.projectSource" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="绔嬮」浜�" prop="creatorName">
+ <el-input v-model="form.creatorName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="棰勮宸ユ湡(澶�)" prop="estimatedDays">
+ <el-input-number v-model="form.estimatedDays" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝寮�濮嬫棩鏈�" prop="planStartDate">
+ <el-date-picker
+ v-model="form.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="璁″垝瀹屾垚鏃ユ湡" prop="planEndDate">
+ <el-date-picker
+ v-model="form.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="椤圭洰绫诲瀷" prop="projectManagementPlanId">
+ <el-select v-model="form.projectManagementPlanId" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="opt in projectTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="椤圭洰閲戦" prop="projectAmount">
+ <el-input-number v-model="form.projectAmount" :min="0" controls-position="right" style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="瀹℃牳鐘舵��" prop="auditStatus">
+ <el-select v-model="form.auditStatus" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="d in project_management" :key="d.value" :label="d.label" :value="d.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ </el-row>
+ <el-row :gutter="10" >
+ <el-col :span="24">
+ <el-upload
+ v-model:file-list="fileList"
+ :action="upload.url"
+ :headers="upload.headers"
+ multiple
+ :disabled="isView"
+ :before-upload="beforeUpload"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="files"
+ :on-remove="handleRemove"
+ >
+ <el-button type="primary" :disabled="isView">涓婁紶鏂囦欢</el-button>
+ </el-upload>
+ <div v-if="existingAttachments.length > 0" class="attachment-list">
+ <div
+ v-for="(att, idx) in existingAttachments"
+ :key="att.id || att.url || idx"
+ class="attachment-item"
+ >
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="100" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('product')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>浜у搧淇℃伅</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" @click="openProductForm('add')">娣诲姞</el-button>
+ <el-button v-if="!isView" plain type="danger" @click="deleteProduct">鍒犻櫎</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('product')">
+ <ArrowDown v-if="sectionCollapsed.product" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.product" class="section-body">
+ <el-table
+ :data="productData"
+ border
+ show-summary
+ :summary-method="summarizeProductTable"
+ @selection-change="productSelected"
+ >
+ <el-table-column v-if="!isView" align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+ <el-table-column label="鍗曚綅" prop="unit" />
+ <el-table-column label="鏁伴噺" prop="quantity" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column v-if="!isView" fixed="right" label="鎿嶄綔" min-width="60" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="openProductForm('edit', scope.row, scope.$index)">缂栬緫</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('team')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰鍥㈤槦</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addTeamRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('team')">
+ <ArrowDown v-if="sectionCollapsed.team" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.team" class="section-body">
+ <PIMTable
+ :column="teamColumns"
+ :tableData="form.teamList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="220"
+ >
+ <template #memberId="{ row }">
+ <el-select v-model="row.memberId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #roleId="{ row }">
+ <el-select v-model="row.roleId" placeholder="璇烽�夋嫨" clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
+ </el-select>
+ </template>
+ <template #enterDate="{ row }">
+ <el-date-picker
+ v-model="row.enterDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #leaveDate="{ row }">
+ <el-date-picker
+ v-model="row.leaveDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #phone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamRemark="{ row }">
+ <el-input v-model="row.remark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #teamAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeTeamRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <!-- <div class="section">
+ <div class="section-header" @click="toggleSection('phase')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>椤圭洰闃舵</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addPhaseRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('phase')">
+ <ArrowDown v-if="sectionCollapsed.phase" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.phase" class="section-body">
+ <PIMTable
+ :column="phaseColumns"
+ :tableData="form.phaseList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="240"
+ >
+ <template #phaseName="{ row }">
+ <el-input v-model="row.phaseName" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseDesc="{ row }">
+ <el-input v-model="row.description" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #ownerId="{ row }">
+ <el-select v-model="row.ownerId" placeholder="璇烽�夋嫨" filterable clearable style="width: 100%" :disabled="isView">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </template>
+ <template #planDays="{ row }">
+ <el-input-number v-model="row.planDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #planStart="{ row }">
+ <el-date-picker
+ v-model="row.planStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #planEnd="{ row }">
+ <el-date-picker
+ v-model="row.planEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #progress="{ row }">
+ <el-input-number v-model="row.progress" :min="0" :max="100" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #actualStart="{ row }">
+ <el-date-picker
+ v-model="row.actualStartDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #actualEnd="{ row }">
+ <el-date-picker
+ v-model="row.actualEndDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ :disabled="isView"
+ />
+ </template>
+ <template #overdueDays="{ row }">
+ <el-input-number v-model="row.overdueDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
+ </template>
+ <template #completion="{ row }">
+ <el-input v-model="row.completionRemark" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #phaseAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removePhaseRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div> -->
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('address')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鏀惰揣鍦板潃</span>
+ </div>
+ <div class="section-actions" @click.stop>
+ <el-button v-if="!isView" type="primary" :icon="Plus" @click="addAddressRow">鏂板琛�</el-button>
+ <el-icon class="toggle-icon" @click="toggleSection('address')">
+ <ArrowDown v-if="sectionCollapsed.address" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ </div>
+ <div v-show="!sectionCollapsed.address" class="section-body">
+ <PIMTable
+ :column="addressColumns"
+ :tableData="form.addressList"
+ :tableLoading="false"
+ :isSelection="false"
+ :isShowPagination="false"
+ height="200"
+ >
+ <template #receiver="{ row }">
+ <el-input v-model="row.receiver" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverPhone="{ row }">
+ <el-input v-model="row.phone" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #receiverAddress="{ row }">
+ <el-input v-model="row.address" placeholder="璇疯緭鍏�" clearable :disabled="isView" />
+ </template>
+ <template #addressAction="{ row, index }">
+ <el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeAddressRow(index)">鍒犻櫎</el-button>
+ <span v-else>鈥�</span>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+
+ <div class="section">
+ <div class="section-header" @click="toggleSection('contact')">
+ <div class="section-title">
+ <span class="section-bar" />
+ <span>鑱旂郴淇℃伅</span>
+ </div>
+ <el-icon class="toggle-icon">
+ <ArrowDown v-if="sectionCollapsed.contact" />
+ <ArrowUp v-else />
+ </el-icon>
+ </div>
+ <div v-show="!sectionCollapsed.contact" class="section-body">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="鑱旂郴浜哄鍚�" prop="contactName">
+ <el-input v-model="form.contactName" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎬у埆" prop="contactGender">
+ <el-select v-model="form.contactGender" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option label="鐢�" value="1" />
+ <el-option label="濂�" value="2" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鐢熸棩" prop="contactBirthday">
+ <el-date-picker
+ v-model="form.contactBirthday"
+ type="date"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="閭" prop="contactEmail">
+ <el-input v-model="form.contactEmail" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="閮ㄩ棬" prop="contactDept">
+ <el-input v-model="form.contactDept" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鑱屽姟" prop="contactJob">
+ <el-input v-model="form.contactJob" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="鎵嬫満鍙风爜" prop="contactMobile">
+ <el-input v-model="form.contactMobile" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="寰俊鍙风爜" prop="contactWechat">
+ <el-input v-model="form.contactWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-form-item label="QQ" prop="contactQq">
+ <el-input v-model="form.contactQq" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="浼佷笟寰俊" prop="contactWorkWechat">
+ <el-input v-model="form.contactWorkWechat" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍦板潃" prop="contactAddress">
+ <el-input v-model="form.contactAddress" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="contactRemark">
+ <el-input v-model="form.contactRemark" type="textarea" :rows="2" placeholder="璇疯緭鍏�" maxlength="200" show-word-limit />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </div>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button v-if="!isView" type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDialog">{{ isView ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <FormDialog
+ v-model="productFormVisible"
+ :title="productOperationType === 'add' ? '鏂板浜у搧' : '缂栬緫浜у搧'"
+ :width="'40%'"
+ :operation-type="productOperationType"
+ @close="closeProductDia"
+ @confirm="submitProduct"
+ @cancel="closeProductDia"
+ >
+ <el-form ref="productFormRef" :model="productForm" label-width="140px" label-position="top" :rules="productRules">
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="浜у搧澶х被锛�" prop="productCategoryId">
+ <el-tree-select
+ v-model="productForm.productCategoryId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ check-strictly
+ :data="productCategoryOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ @change="getModels"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="productModelId">
+ <el-select v-model="productForm.productModelId" placeholder="璇烽�夋嫨" clearable filterable @change="getProductModel">
+ <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input v-model="productForm.unit" placeholder="璇疯緭鍏�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)锛�" prop="taxRate">
+ <el-select v-model="productForm.taxRate" placeholder="璇烽�夋嫨" clearable @change="calculateFromTaxRate">
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鍗曚环(鍏�)锛�" prop="taxInclusiveUnitPrice">
+ <el-input-number
+ v-model="productForm.taxInclusiveUnitPrice"
+ :step="0.01"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromUnitPrice"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺锛�" prop="quantity">
+ <el-input-number
+ v-model="productForm.quantity"
+ :step="0.1"
+ :min="0"
+ :precision="2"
+ style="width: 100%"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromQuantity"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鎬讳环(鍏�)锛�" prop="taxInclusiveTotalPrice">
+ <el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromTotalPrice" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓嶅惈绋庢�讳环(鍏�)锛�" prop="taxExclusiveTotalPrice">
+ <el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="璇疯緭鍏�" clearable @change="calculateFromExclusiveTotalPrice" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷锛�" prop="invoiceType">
+ <el-select v-model="productForm.invoiceType" placeholder="璇烽�夋嫨" clearable>
+ <el-option label="澧炴櫘绁�" value="澧炴櫘绁�" />
+ <el-option label="澧炰笓绁�" value="澧炰笓绁�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup name="ProjectManagementFormDia">
+import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue'
+import { ArrowDown, ArrowUp, Delete, Plus, Document } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+import { listPlan } from '@/api/projectManagement/projectType'
+import { findRoleListPage } from '@/api/projectManagement/role'
+import { userListAll } from '@/api/publicApi'
+import { addProject, getProject, updateProject } from '@/api/projectManagement/project'
+import { modelList, productTreeList } from '@/api/basicData/product'
+import { delProduct as delSalesProduct } from '@/api/salesManagement/salesLedger'
+
+const emit = defineEmits(['completed'])
+const { proxy } = getCurrentInstance()
+const { bill_status, project_management, plan_status, tax_rate } = proxy.useDict('bill_status', 'project_management', 'plan_status', 'tax_rate')
+
+const dialogVisible = ref(false)
+const operationType = ref('add')
+const formRef = ref()
+const fileList = ref([])
+const existingAttachments = ref([])
+const upload = reactive({
+ url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
+ headers: { Authorization: 'Bearer ' + getToken() }
+})
+
+const projectTypeOptions = ref([])
+const roleOptions = ref([])
+const userOptions = ref([])
+const productData = ref([])
+const productSelectedRows = ref([])
+const productCategoryOptions = ref([])
+const modelOptions = ref([])
+const productFormVisible = ref(false)
+const productOperationType = ref('add')
+const productFormRef = ref()
+const productIndex = ref(0)
+const isCalculating = ref(false)
+
+const productFormData = reactive({
+ productForm: {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ },
+ productRules: {
+ productCategoryId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ productModelId: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ unit: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ quantity: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxInclusiveUnitPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxRate: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }],
+ taxInclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ taxExclusiveTotalPrice: [{ required: true, message: '璇疯緭鍏�', trigger: 'blur' }],
+ invoiceType: [{ required: true, message: '璇烽�夋嫨', trigger: 'change' }]
+ }
+})
+
+const { productForm, productRules } = toRefs(productFormData)
+
+const data = reactive({
+ form: {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ },
+ rules: {
+ projectName: [{ required: true, message: '璇疯緭鍏ラ」鐩悕绉�', trigger: 'blur' }]
+ }
+})
+
+const { form, rules } = toRefs(data)
+
+const sectionCollapsed = reactive({
+ base: false,
+ product: false,
+ team: false,
+ phase: false,
+ address: false,
+ contact: false
+})
+
+const isView = computed(() => operationType.value === 'view')
+const dialogTitle = computed(() => {
+ if (operationType.value === 'add') return '鏂板椤圭洰'
+ if (operationType.value === 'edit') return '缂栬緫椤圭洰'
+ return '椤圭洰璇︽儏'
+})
+
+const teamColumns = [
+ { label: '濮撳悕', prop: 'memberId', align: 'center', width: 180, dataType: 'slot', slot: 'memberId' },
+ { label: '椤圭洰缁勮鑹�', prop: 'roleId', align: 'center', width: 160, dataType: 'slot', slot: 'roleId' },
+ { label: '杩涘叆鏃ユ湡', prop: 'enterDate', align: 'center', width: 160, dataType: 'slot', slot: 'enterDate' },
+ { label: '绂诲紑鏃ユ湡', prop: 'leaveDate', align: 'center', width: 160, dataType: 'slot', slot: 'leaveDate' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'phone' },
+ { label: '澶囨敞', prop: 'remark', align: 'center', dataType: 'slot', slot: 'teamRemark' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'teamAction', fixed: 'right' }
+]
+
+const phaseColumns = [
+ { label: '闃舵鍚嶇О', prop: 'phaseName', align: 'center', width: 160, dataType: 'slot', slot: 'phaseName' },
+ { label: '鎻忚堪', prop: 'description', align: 'center', width: 200, dataType: 'slot', slot: 'phaseDesc' },
+ { label: '璐熻矗浜�', prop: 'ownerId', align: 'center', width: 160, dataType: 'slot', slot: 'ownerId' },
+ { label: '棰勮宸ユ湡(澶�)', prop: 'planDays', align: 'center', width: 140, dataType: 'slot', slot: 'planDays' },
+ { label: '璁″垝寮�濮嬫棩鏈�', prop: 'planStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'planStart' },
+ { label: '璁″垝缁撴潫鏃ユ湡', prop: 'planEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'planEnd' },
+ { label: '杩涘害(%)', prop: 'progress', align: 'center', width: 120, dataType: 'slot', slot: 'progress' },
+ { label: '瀹為檯寮�濮嬫棩鏈�', prop: 'actualStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualStart' },
+ { label: '瀹為檯缁撴潫鏃ユ湡', prop: 'actualEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualEnd' },
+ { label: '閫炬湡澶╂暟', prop: 'overdueDays', align: 'center', width: 120, dataType: 'slot', slot: 'overdueDays' },
+ { label: '瀹屾垚鎯呭喌', prop: 'completionRemark', align: 'center', width: 200, dataType: 'slot', slot: 'completion' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'phaseAction', fixed: 'right' }
+]
+
+const addressColumns = [
+ { label: '鏀惰揣浜�', prop: 'receiver', align: 'center', width: 180, dataType: 'slot', slot: 'receiver' },
+ { label: '鑱旂郴鏂瑰紡', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'receiverPhone' },
+ { label: '鏀惰揣鍦板潃', prop: 'address', align: 'center', dataType: 'slot', slot: 'receiverAddress' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'addressAction', fixed: 'right' }
+]
+
+function toggleSection(key) {
+ sectionCollapsed[key] = !sectionCollapsed[key]
+}
+
+function resetFormData() {
+ Object.assign(form.value, {
+ id: undefined,
+ clientId: undefined,
+ parentProjectId: undefined,
+ projectManagementPlanId: undefined,
+ managerId: undefined,
+ salesmanId: undefined,
+ salesmanName: '',
+ actualStartDate: '',
+ actualEndDate: '',
+ departmentId: undefined,
+ departmentName: '',
+ orderDate: '',
+ billNo: '',
+ projectName: '',
+ customerName: '',
+ parentProjectName: '',
+ setupDate: '',
+ projectSource: '',
+ creatorName: '',
+ billStatus: '',
+ projectStage: '',
+ estimatedDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ projectManagementPlanId: undefined,
+ projectAmount: 0,
+ auditStatus: '',
+ remark: '',
+ attachmentIds: [],
+ teamList: [],
+ phaseList: [],
+ addressList: [],
+ contactName: '',
+ contactGender: '',
+ contactBirthday: '',
+ contactEmail: '',
+ contactDept: '',
+ contactJob: '',
+ contactMobile: '',
+ contactWechat: '',
+ contactQq: '',
+ contactWorkWechat: '',
+ contactAddress: '',
+ contactRemark: ''
+ })
+ fileList.value = []
+ productData.value = []
+}
+
+function formattedNumber(row, column, cellValue) {
+ const val = Number(cellValue ?? 0)
+ return Number.isFinite(val) ? val.toFixed(2) : '0.00'
+}
+
+function summarizeProductTable(param) {
+ return proxy.summarizeTable(param, ['taxInclusiveTotalPrice', 'taxExclusiveTotalPrice'])
+}
+
+function productSelected(selection) {
+ productSelectedRows.value = selection
+}
+
+function convertIdToValue(data) {
+ return (Array.isArray(data) ? data : []).map(item => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+function findNodeById(nodes, productId) {
+ for (let i = 0; i < (nodes || []).length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId)
+ if (foundNode) return foundNode
+ }
+ }
+ return null
+}
+
+function findNodeIdByLabel(nodes, label) {
+ if (!label) return null
+ for (let i = 0; i < (nodes || []).length; i++) {
+ const node = nodes[i]
+ if (node.label === label) return node.value
+ if (node.children && node.children.length > 0) {
+ const found = findNodeIdByLabel(node.children, label)
+ if (found !== null && found !== undefined) return found
+ }
+ }
+ return null
+}
+
+function getProductOptions() {
+ return productTreeList().then(res => {
+ const list = res?.data || res
+ productCategoryOptions.value = convertIdToValue(list)
+ return productCategoryOptions.value
+ })
+}
+
+function getModels(value) {
+ const categoryLabel = findNodeById(productCategoryOptions.value, value)
+ productForm.value.productCategory = categoryLabel || ''
+ modelList({ id: value }).then(res => {
+ modelOptions.value = res?.data || res || []
+ })
+}
+
+function getProductModel(value) {
+ const index = (modelOptions.value || []).findIndex(item => item.id === value)
+ if (index !== -1) {
+ productForm.value.specificationModel = modelOptions.value[index].model
+ productForm.value.unit = modelOptions.value[index].unit
+ } else {
+ productForm.value.specificationModel = ''
+ productForm.value.unit = ''
+ }
+}
+
+async function openProductForm(type, row, index) {
+ productOperationType.value = type
+ productIndex.value = index || 0
+ productForm.value = {}
+ proxy.resetForm('productFormRef')
+
+ if (!productCategoryOptions.value || productCategoryOptions.value.length === 0) {
+ await getProductOptions()
+ }
+
+ if (type === 'edit' && row) {
+ productForm.value = { ...row }
+ try {
+ const categoryId = findNodeIdByLabel(productCategoryOptions.value, productForm.value.productCategory)
+ if (categoryId) {
+ productForm.value.productCategoryId = categoryId
+ const models = await modelList({ id: categoryId })
+ modelOptions.value = models?.data || models || []
+ const currentModel = (modelOptions.value || []).find(m => m.model === productForm.value.specificationModel)
+ if (currentModel) {
+ productForm.value.productModelId = currentModel.id
+ }
+ }
+ } catch {}
+ } else {
+ productForm.value = {
+ productCategoryId: undefined,
+ productCategory: '',
+ productModelId: undefined,
+ specificationModel: '',
+ unit: '',
+ quantity: '',
+ taxInclusiveUnitPrice: '',
+ taxRate: '',
+ taxInclusiveTotalPrice: '',
+ taxExclusiveTotalPrice: '',
+ invoiceType: ''
+ }
+ }
+
+ productFormVisible.value = true
+}
+
+function closeProductDia() {
+ proxy.resetForm('productFormRef')
+ productFormVisible.value = false
+}
+
+function submitProduct() {
+ productFormRef.value?.validate?.(valid => {
+ if (!valid) return
+ const payload = { ...productForm.value }
+ if (productOperationType.value === 'add') {
+ productData.value.push(payload)
+ } else {
+ productData.value[productIndex.value] = payload
+ }
+ closeProductDia()
+ })
+}
+
+function deleteProduct() {
+ if (!productSelectedRows.value || productSelectedRows.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const selectedIds = productSelectedRows.value.map(r => r?.id).filter(Boolean)
+ if (operationType.value !== 'add' && selectedIds.length > 0) {
+ delSalesProduct(selectedIds)
+ .then(() => {
+ proxy.$modal?.msgSuccess?.('鍒犻櫎鎴愬姛')
+ productData.value = productData.value.filter(row => !selectedIds.includes(row?.id))
+ productSelectedRows.value = []
+ })
+ .catch(() => {})
+ return
+ }
+
+ productData.value = productData.value.filter(row => !productSelectedRows.value.includes(row))
+ productSelectedRows.value = []
+}
+
+function calculateFromTotalPrice() {
+ if (isCalculating.value) return
+ const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ if (!totalPrice || !quantity || quantity <= 0) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(totalPrice, productForm.value.taxRate)
+ }
+ isCalculating.value = false
+}
+
+function calculateFromExclusiveTotalPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const exclusiveTotalPrice = parseFloat(productForm.value.taxExclusiveTotalPrice)
+ const quantity = parseFloat(productForm.value.quantity)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) return
+ isCalculating.value = true
+ const taxRateDecimal = taxRate / 100
+ const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal)
+ productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2)
+ productForm.value.taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2)
+ isCalculating.value = false
+}
+
+function calculateFromQuantity() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromUnitPrice() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const quantity = parseFloat(productForm.value.quantity)
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
+ if (!quantity || quantity <= 0 || !unitPrice) return
+ isCalculating.value = true
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ )
+ }
+ isCalculating.value = false
+}
+
+function calculateFromTaxRate() {
+ if (!productForm.value.taxRate) {
+ proxy.$modal?.msgWarning?.('璇峰厛閫夋嫨绋庣巼')
+ return
+ }
+ if (isCalculating.value) return
+ const inclusiveTotalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
+ const taxRate = parseFloat(productForm.value.taxRate)
+ if (!inclusiveTotalPrice || !taxRate) return
+ isCalculating.value = true
+ productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate)
+ isCalculating.value = false
+}
+
+async function loadProjectTypeOptions() {
+ try {
+ const res = await listPlan({ current: 1, size: 999 })
+ const records = res?.data?.records || res?.records || res?.rows || []
+ projectTypeOptions.value = records.map(item => ({ label: item.name, value: item.id }))
+ } catch {
+ projectTypeOptions.value = []
+ }
+}
+
+async function loadRoleOptions() {
+ try {
+ const res = await findRoleListPage({ pageNum: 1, pageSize: 999 })
+ const records = res?.data?.records || res?.rows || res?.records || []
+ roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id }))
+ } catch {
+ roleOptions.value = []
+ }
+}
+
+async function loadUserOptions() {
+ try {
+ const res = await userListAll()
+ const list = res?.data || res?.rows || res || []
+ userOptions.value = (Array.isArray(list) ? list : []).map(u => ({
+ label: u.nickName || u.userName || u.username || u.name,
+ value: u.userId || u.id
+ }))
+ } catch {
+ userOptions.value = []
+ }
+}
+
+function addTeamRow() {
+ form.value.teamList.push({
+ memberId: undefined,
+ roleId: undefined,
+ enterDate: '',
+ leaveDate: '',
+ phone: '',
+ remark: ''
+ })
+}
+
+function removeTeamRow(index) {
+ if (index > -1) form.value.teamList.splice(index, 1)
+}
+
+function addPhaseRow() {
+ form.value.phaseList.push({
+ phaseName: '',
+ description: '',
+ ownerId: undefined,
+ planDays: 0,
+ planStartDate: '',
+ planEndDate: '',
+ progress: 0,
+ actualStartDate: '',
+ actualEndDate: '',
+ overdueDays: 0,
+ completionRemark: ''
+ })
+}
+
+function removePhaseRow(index) {
+ if (index > -1) form.value.phaseList.splice(index, 1)
+}
+
+function addAddressRow() {
+ form.value.addressList.push({
+ receiver: '',
+ phone: '',
+ address: ''
+ })
+}
+
+function removeAddressRow(index) {
+ if (index > -1) form.value.addressList.splice(index, 1)
+}
+
+function beforeUpload() {
+ if (isView.value) return false
+ proxy.$modal?.loading?.('姝e湪涓婁紶鏂囦欢锛岃绋嶅��...')
+ return true
+}
+
+function handleUploadError() {
+ proxy.$modal?.closeLoading?.()
+ ElMessage.error('涓婁紶鏂囦欢澶辫触')
+}
+
+function handleUploadSuccess(res, file) {
+ console.log(res, file)
+ proxy.$modal?.closeLoading?.()
+ if (res?.code !== 200) {
+ ElMessage.error(res?.msg || '涓婁紶澶辫触')
+ return
+ }
+ const attachmentId = res?.data?.[0]?.id ?? ""
+ if (!attachmentId) return
+ form.value.attachmentIds.push(attachmentId)
+ console.log(form.value.attachmentIds)
+ ElMessage.success('涓婁紶鎴愬姛')
+}
+
+function handleRemove(file) {
+ const attachmentId = file?.attachmentId
+ if (!attachmentId) return
+ form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
+}
+
+async function openDialog(payload = {}) {
+ operationType.value = payload.operationType || 'add'
+ resetFormData()
+ await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), getProductOptions()])
+ if (payload.row?.id) {
+ try {
+ const res = await getProject(payload.row.id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ const info = detail?.info || {}
+ const shippingAddress = detail?.shippingAddress || {}
+ const contractInfo = detail?.contractInfo || {}
+
+ const normalizeId = v => {
+ if (v === undefined || v === null || v === '') return undefined
+ const n = Number(v)
+ return Number.isNaN(n) ? v : n
+ }
+
+ const normalizeDictValue = v => {
+ if (v === undefined || v === null || v === '') return ''
+ return String(v)
+ }
+
+ const computeEstimatedDays = (start, end) => {
+ if (!start || !end) return 0
+ const startTime = new Date(`${start}T00:00:00`).getTime()
+ const endTime = new Date(`${end}T00:00:00`).getTime()
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) return 0
+ if (endTime < startTime) return 0
+ return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
+ }
+
+ Object.assign(form.value, {
+ id: info.id,
+ billNo: info.no ?? '',
+ projectManagementPlanId: info.projectManagementPlanId ?? '',
+ estimatedDays: Number(info.estimatedDays) || computeEstimatedDays(info.planStartTime, info.planEndTime) || 0,
+ projectName: info.title ?? '',
+ customerName: info.clientName ?? '',
+ parentProjectName: info.projectManagementInfoParentName ?? '',
+ setupDate: info.establishTime ?? '',
+ projectSource: info.source ?? '',
+ creatorName: info.managerName ?? '',
+ billStatus: normalizeDictValue(info.status),
+ projectStage: normalizeDictValue(info.stage ?? info.projectStage),
+ planStartDate: info.planStartTime ?? '',
+ planEndDate: info.planEndTime ?? '',
+ projectAmount: info.orderAmount ?? 0,
+ auditStatus: normalizeDictValue(info.reviewStatus),
+ remark: info.remark ?? '',
+ attachmentIds: Array.isArray(info.attachmentIds) ? info.attachmentIds : [],
+ teamList: Array.isArray(info.teamList) ? info.teamList.map(t => ({
+ memberId: normalizeId(t.userId),
+ roleId: normalizeId(t.userRoleId),
+ enterDate: t.joinTime,
+ leaveDate: t.departTime,
+ phone: t.contact,
+ remark: t.remark
+ })) : [],
+ addressList: shippingAddress?.address
+ ? [{
+ receiver: shippingAddress.consignee,
+ phone: shippingAddress.contract,
+ address: shippingAddress.address
+ }]
+ : [],
+ contactName: contractInfo.name ?? '',
+ contactGender: contractInfo.sex === '鐢�' ? '1' : contractInfo.sex === '濂�' ? '2' : '',
+ contactBirthday: contractInfo.birthday ?? '',
+ contactDept: contractInfo.department ?? '',
+ contactJob: contractInfo.job ?? '',
+ contactMobile: contractInfo.phoneNumber ?? '',
+ contactEmail: contractInfo.email ?? '',
+ contactQq: contractInfo.qq ?? '',
+ contactWechat: contractInfo.wx ?? '',
+ contactWorkWechat: contractInfo.lineaFissa ?? '',
+ contactAddress: contractInfo.origineEtnica ?? '',
+ contactRemark: contractInfo.rappresentanteLegale ?? ''
+ })
+
+ existingAttachments.value = Array.isArray(info.attachmentList)
+ ? info.attachmentList.map(a => ({
+ id: a.id ?? a.fileId,
+ name: a.fileName ?? a.name,
+ url: a.url ?? a.fileUrl ?? a.path
+ }))
+ : []
+
+ const rawPhaseList =
+ detail?.phaseList ||
+ detail?.projectPhaseList ||
+ detail?.projectStageList ||
+ info?.phaseList ||
+ info?.projectPhaseList ||
+ []
+ form.value.phaseList = Array.isArray(rawPhaseList)
+ ? rawPhaseList.map(p => ({
+ phaseName: p.phaseName ?? p.name ?? p.title ?? '',
+ description: p.description ?? p.workContent ?? p.desc ?? '',
+ ownerId: normalizeId(p.ownerId ?? p.leaderId ?? p.userId),
+ planDays: Number(p.planDays ?? p.estimatedDuration ?? p.estimatedDays) || 0,
+ planStartDate: p.planStartDate ?? p.planStartTime ?? p.startDate ?? '',
+ planEndDate: p.planEndDate ?? p.planEndTime ?? p.endDate ?? '',
+ progress: Number(p.progress ?? p.schedule) || 0,
+ actualStartDate: p.actualStartDate ?? p.actualStartTime ?? '',
+ actualEndDate: p.actualEndDate ?? p.actualEndTime ?? '',
+ overdueDays: Number(p.overdueDays ?? p.overDays) || 0,
+ completionRemark: p.completionRemark ?? p.remark ?? ''
+ }))
+ : []
+
+ productData.value = detail?.salesLedgerProductList || detail?.productData || []
+ } catch {}
+ }
+ if (form.value.teamList.length === 0 && !isView.value) addTeamRow()
+ if (form.value.phaseList.length === 0 && !isView.value) addPhaseRow()
+ dialogVisible.value = true
+}
+
+function downloadAttachment(att) {
+ if (att) {
+ try {
+ proxy.$download.byUrl(att.url, att.originalFilename);
+ return
+ } catch (e) {}
+ }
+ ElMessage.warning('闄勪欢鏆傛棤涓嬭浇鍦板潃')
+}
+function closeDialog() {
+ dialogVisible.value = false
+}
+
+async function submitForm() {
+ if (isView.value) {
+ closeDialog()
+ return
+ }
+ await formRef.value?.validate?.()
+ if (!productData.value || productData.value.length === 0) {
+ proxy.$modal?.msgWarning?.('璇锋坊鍔犱骇鍝佷俊鎭�')
+ return
+ }
+ const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label
+ const teamList = (form.value.teamList || []).map(t => ({
+ userId: t.memberId,
+ userName: findLabel(userOptions.value, t.memberId),
+ userRoleId: t.roleId,
+ userRoleName: findLabel(roleOptions.value, t.roleId),
+ joinTime: t.enterDate,
+ departTime: t.leaveDate,
+ contact: t.phone,
+ remark: t.remark
+ }))
+
+ const shippingRow = (form.value.addressList || [])[0] || {}
+ const shippingAddress = {
+ id: undefined,
+ consignee: shippingRow.receiver,
+ contract: shippingRow.phone,
+ address: shippingRow.address
+ }
+
+ const contractInfo = {
+ id: undefined,
+ name: form.value.contactName,
+ sex: form.value.contactGender === '1' ? '鐢�' : form.value.contactGender === '2' ? '濂�' : '',
+ birthday: form.value.contactBirthday,
+ department: form.value.contactDept,
+ job: form.value.contactJob,
+ phoneNumber: form.value.contactMobile,
+ email: form.value.contactEmail,
+ qq: form.value.contactQq,
+ lineaFissa: form.value.contactWorkWechat,
+ wx: form.value.contactWechat,
+ origineEtnica: form.value.contactAddress,
+ rappresentanteLegale: form.value.contactRemark
+ }
+ const info = {
+ id: form.value.id ?? null,
+ no: form.value.billNo,
+ title: form.value.projectName,
+ clientId: form.value.clientId ?? null,
+ clientName: form.value.customerName,
+ projectManagementInfoParentId: form.value.parentProjectId ?? null,
+ projectManagementPlanId: form.value.projectManagementPlanId ?? null,
+ establishTime: form.value.setupDate,
+ source: form.value.projectSource,
+ managerId: form.value.managerId ?? null,
+ managerName: form.value.creatorName,
+ salesmanId: form.value.salesmanId ?? null,
+ salesmanName: form.value.salesmanName ?? '',
+ planStartTime: form.value.planStartDate,
+ planEndTime: form.value.planEndDate,
+ actualStartTime: form.value.actualStartDate,
+ actualEndTime: form.value.actualEndDate,
+ status: form.value.billStatus === '' || form.value.billStatus === undefined || form.value.billStatus === null ? null : Number(form.value.billStatus),
+ departmentId: form.value.departmentId ?? null,
+ departmentName: form.value.departmentName ?? '',
+ orderDate: form.value.orderDate,
+ orderAmount: form.value.projectAmount,
+ reviewStatus: form.value.auditStatus === '' || form.value.auditStatus === undefined || form.value.auditStatus === null ? null : Number(form.value.auditStatus),
+ stage: form.value.projectStage === '' || form.value.projectStage === undefined || form.value.projectStage === null ? null : Number(form.value.projectStage),
+ remark: form.value.remark,
+ attachmentIds: Array.isArray(form.value.attachmentIds) ? form.value.attachmentIds : [],
+ teamList
+ }
+
+ const payload = {
+ info,
+ shippingAddress,
+ contractInfo,
+ salesLedgerProductList: productData.value
+ }
+
+ const req = operationType.value === 'edit' ? updateProject : addProject
+ const res = await req(payload)
+ if (res?.code === 200) {
+ ElMessage.success('淇濆瓨鎴愬姛')
+ closeDialog()
+ emit('completed')
+ return
+ }
+ ElMessage.error(res?.msg || '淇濆瓨澶辫触')
+}
+
+defineExpose({ openDialog })
+</script>
+
+<style scoped lang="scss">
+.section {
+ border: 1px solid #ebeef5;
+ border-radius: 8px;
+ margin-bottom: 14px;
+ background: #fff;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ cursor: pointer;
+}
+
+.section-title {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ color: #303133;
+}
+
+.section-bar {
+ width: 3px;
+ height: 14px;
+ background: #002FA7;
+ border-radius: 2px;
+}
+
+.section-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.toggle-icon {
+ color: #909399;
+}
+
+.section-body {
+ padding: 0 14px 14px;
+}
+
+.dialog-footer {
+ display: flex;
+ justify-content: center;
+ gap: 12px;
+}
+.attachment-upload{
+
+}
+</style>
diff --git a/src/views/projectManagement/Management/index.vue b/src/views/projectManagement/Management/index.vue
new file mode 100644
index 0000000..0438aef
--- /dev/null
+++ b/src/views/projectManagement/Management/index.vue
@@ -0,0 +1,428 @@
+<template>
+ <div class="app-container">
+ <SearchPanel
+ v-model="queryParams"
+ :schema="searchSchema"
+ @search="handleQuery"
+ @reset="resetQuery"
+ >
+ <template #billStatus="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨鍗曟嵁鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in bill_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ <template #auditStatus="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨璁″垝鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in project_management" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ <template #projectStage="{ item }">
+ <el-select v-model="queryParams[item.prop]" placeholder="璇烽�夋嫨瀹℃牳鐘舵��" clearable style="width: 100%">
+ <el-option v-for="dict in plan_status" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </template>
+ </SearchPanel>
+
+ <div class="table-container">
+ <div class="table-actions">
+ <el-button style="background-color: #002FA7; color: #fff" @click="handleAdd">鏂板</el-button>
+ <!-- <el-dropdown split-button type="default" @command="handleGenerateBill" style="margin-left: 10px;">
+ 鐢熸垚鍗曟嵁
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="1">鐢熸垚鍗曟嵁1</el-dropdown-item>
+ <el-dropdown-item command="2">鐢熸垚鍗曟嵁2</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown> -->
+ <el-button :loading="submitLoading" @click="handleSubmit">鎻愪氦</el-button>
+ <el-button :loading="auditLoading" @click="handleAudit">瀹℃牳</el-button>
+ <!-- <el-button :loading="reverseAuditLoading" @click="handleReverseAudit">鍙嶅鏍�</el-button> -->
+ <el-button :loading="deleteLoading" @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+
+ <PIMTable
+ :column="columns"
+ :tableData="tableData"
+ :page="pagination"
+ :tableLoading="loading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="handlePagination"
+ >
+ <template #auditStatus="{ row }">
+ <dict-tag :options="project_management" :value="row.auditStatus" />
+ </template>
+ <template #projectStage="{ row }">
+ <dict-tag :options="plan_status" :value="row.projectStage" />
+ </template>
+ <template #action="{ row }">
+ <el-button link type="primary" @click="handleEdit(row)">缂栬緫</el-button>
+ <el-button link type="primary" :loading="progressBtnLoadingId===row.id" @click="handleProgressReport(row)">杩涘害姹囨姤</el-button>
+ <el-button link type="primary" @click="handleDetail(row)">璇︽儏</el-button>
+ </template>
+ </PIMTable>
+ </div>
+
+ <FormDia ref="formDiaRef" @completed="getList" />
+ <ProgressReportDialog
+ v-model="progressReportVisible"
+ :project-id="progressProjectId"
+ :project-info="progressProjectInfo"
+ :plan-nodes="progressPlanNodes"
+ :default-plan-node-id="progressDefaultPlanNodeId"
+ @submitted="handleProgressSubmitted"
+ />
+ </div>
+</template>
+
+<script setup name="ProjectManagement">
+import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
+import { useRouter } from 'vue-router'
+import SearchPanel from '@/components/SearchPanel/index.vue'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import FormDia from './components/formDia.vue'
+import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue'
+import {
+ listProject,
+ delProject,
+ submitProject,
+ auditProject,
+ reverseAuditProject,
+ getProject,
+ saveStage
+} from '@/api/projectManagement/project'
+import { listPlan } from '@/api/projectManagement/projectType'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import useUserStore from '@/store/modules/user'
+
+const { proxy } = getCurrentInstance()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+const router = useRouter()
+const userStore = useUserStore()
+
+const loading = ref(false)
+const ids = ref([])
+const tableData = ref([])
+const formDiaRef = ref()
+const progressReportVisible = ref(false)
+const progressProjectId = ref(undefined)
+const progressProjectInfo = ref({})
+const progressPlanNodes = ref([])
+const progressDefaultPlanNodeId = ref(undefined)
+const progressBtnLoadingId = ref(null)
+const submitLoading = ref(false)
+const auditLoading = ref(false)
+const reverseAuditLoading = ref(false)
+const deleteLoading = ref(false)
+
+const data = reactive({
+ queryParams: {
+ projectNameOrCode: undefined,
+ customerName: undefined,
+ billStatus: undefined,
+ projectStage: undefined,
+ auditStatus: undefined,
+ salesperson: undefined,
+ pageNum: 1,
+ pageSize: 10
+ },
+ pagination: {
+ current: 1,
+ size: 10,
+ total: 0,
+ layout: 'total, sizes, prev, pager, next, jumper'
+ }
+})
+
+const { queryParams, pagination } = toRefs(data)
+
+const searchSchema = [
+ { prop: 'projectNameOrCode', label: '椤圭洰鍚嶇О/缂栧彿', type: 'input', placeholder: '璇疯緭鍏ラ」鐩悕绉�/缂栧彿' },
+ { prop: 'customerName', label: '瀹㈡埛鍚嶇О', type: 'input', placeholder: '璇疯緭鍏ュ鎴峰悕绉�' },
+ { prop: 'billStatus', label: '鍗曟嵁鐘舵��', slot: 'billStatus' },
+ { prop: 'projectStage', label: '璁″垝鐘舵��', slot: 'projectStage' },
+ { prop: 'auditStatus', label: '瀹℃牳鐘舵��', slot: 'auditStatus' },
+ { prop: 'salesperson', label: '涓氬姟浜哄憳', type: 'input', placeholder: '璇疯緭鍏ヤ笟鍔′汉鍛�' }
+]
+
+const columns = [
+ { label: '鍗曟嵁缂栧彿', prop: 'billNo', align: 'center', width: '150' },
+ { label: '椤圭洰鍚嶇О', prop: 'projectName', align: 'center' },
+ { label: '瀹℃牳鐘舵��', prop: 'auditStatus', align: 'center', dataType: 'slot', slot: 'auditStatus' },
+ { label: '瀹㈡埛鍚嶇О', prop: 'customerName', align: 'center' },
+ { label: '绔嬮」鏃ユ湡', prop: 'setupDate', align: 'center', width: '120' },
+ { label: '椤圭洰鏉ユ簮', prop: 'projectSource', align: 'center' },
+ { label: '椤圭洰鍒嗙被', prop: 'projectClassification', align: 'center' },
+ { label: '鎿嶄綔', prop: 'action', align: 'center', width: '250', dataType: 'slot', slot: 'action', fixed: 'right' }
+]
+
+function getList() {
+ loading.value = true
+ const params = {
+ noOrName: queryParams.value.projectNameOrCode,
+ clientName: queryParams.value.customerName,
+ salesmanName: queryParams.value.salesperson,
+ reviewStatus: queryParams.value.auditStatus,
+ stage: queryParams.value.projectStage,
+ current: queryParams.value.pageNum,
+ size: queryParams.value.pageSize
+ }
+ listProject(params)
+ .then(response => {
+ const records = response?.data?.records || response?.rows || response?.records || []
+ const billFilter = queryParams.value.billStatus
+ const filtered = billFilter === undefined || billFilter === null || billFilter === ''
+ ? records
+ : records.filter(r => String(r.billStatus ?? r.status) === String(billFilter))
+ tableData.value = filtered.map(r => ({
+ id: r.id,
+ billNo: r.no ?? r.billNo,
+ projectName: r.title ?? r.projectName,
+ billStatus: r.billStatus ?? r.status,
+ auditStatus: r.reviewStatus ?? r.auditStatus,
+ projectStage: r.stage ?? r.projectStage,
+ customerName: r.clientName ?? r.customerName,
+ parentProject: r.parentTitle ?? r.parentName ?? r.parentProject,
+ setupDate: r.establishTime ?? r.setupDate,
+ projectType: r.planName ?? r.projectType,
+ projectSource: r.source ?? r.projectSource,
+ projectClassification: r.departmentName ?? r.projectClassification,
+ raw: r
+ }))
+ pagination.value.total = response?.total || response?.data?.total || 0
+ })
+ .finally(() => {
+ loading.value = false
+ })
+}
+
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ pagination.value.current = 1
+ getList()
+}
+
+function resetQuery() {
+ queryParams.value = {
+ projectNameOrCode: undefined,
+ customerName: undefined,
+ billStatus: undefined,
+ projectStage: undefined,
+ auditStatus: undefined,
+ salesperson: undefined,
+ pageNum: 1,
+ pageSize: 10
+ }
+ handleQuery()
+}
+
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.id)
+}
+
+function handlePagination({ page, limit }) {
+ queryParams.value.pageNum = page
+ queryParams.value.pageSize = limit
+ pagination.value.current = page
+ pagination.value.size = limit
+ getList()
+}
+
+function handleAdd() {
+ formDiaRef.value?.openDialog({ operationType: 'add' })
+}
+
+async function handleDelete() {
+ const delIds = ids.value
+ if (delIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁椤�')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭鍒犻櫎鎵�閫夋暟鎹」?', '璀﹀憡', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ deleteLoading.value = true
+ await delProject(delIds)
+ getList()
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ } catch {} finally {
+ deleteLoading.value = false
+ }
+}
+
+async function handleSubmit() {
+ const submitIds = ids.value
+ if (submitIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佹彁浜ょ殑鏁版嵁椤�')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭鎻愪氦鎵�閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ submitLoading.value = true
+ await Promise.all(submitIds.map(id => submitProject({ id })))
+ getList()
+ ElMessage.success('鎻愪氦鎴愬姛')
+ } catch {} finally {
+ submitLoading.value = false
+ }
+}
+
+async function handleAudit() {
+ const auditIds = ids.value
+ if (auditIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸鏍哥殑鏁版嵁椤�')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭瀹℃牳鎵�閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ auditLoading.value = true
+ await Promise.all(auditIds.map(id => auditProject({ id })))
+ getList()
+ ElMessage.success('瀹℃牳鎴愬姛')
+ } catch {} finally {
+ auditLoading.value = false
+ }
+}
+
+async function handleReverseAudit() {
+ const reverseAuditIds = ids.value
+ if (reverseAuditIds.length === 0) {
+ ElMessage.warning('璇烽�夋嫨瑕佸弽瀹℃牳鐨勬暟鎹」')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('鏄惁纭鍙嶅鏍告墍閫夋暟鎹」?', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ reverseAuditLoading.value = true
+ await Promise.all(reverseAuditIds.map(id => reverseAuditProject({ id })))
+ getList()
+ ElMessage.success('鍙嶅鏍告垚鍔�')
+ } catch {} finally {
+ reverseAuditLoading.value = false
+ }
+}
+
+function handleGenerateBill(command) {
+ ElMessage.info(`鐢熸垚鍗曟嵁: ${command}`)
+}
+
+function computeDefaultPlanNodeId(stageVal, nodes) {
+ const list = Array.isArray(nodes) ? nodes : []
+ if (list.length === 0) return undefined
+ const direct = list.find(n => String(n.id) === String(stageVal))
+ if (direct?.id) return direct.id
+ const sorted = [...list].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ const idx = Number(stageVal)
+ if (Number.isFinite(idx)) {
+ const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0]
+ if (byIndex?.id) return byIndex.id
+ }
+ return sorted[0]?.id
+}
+
+async function handleProgressReport(row) {
+ if (!row?.id) return
+ try {
+ progressBtnLoadingId.value = row.id
+ const res = await getProject(row.id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ const info = detail?.info || {}
+ progressProjectId.value = info.id
+ progressProjectInfo.value = info
+
+ const planId = info.projectManagementPlanId
+ if (planId) {
+ const planRes = await listPlan({ current: 1, size: 999 })
+ const records = planRes?.data?.records || planRes?.records || planRes?.rows || []
+ const plan = (records || []).find(p => String(p.id) === String(planId)) || {}
+ progressPlanNodes.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : []
+ } else {
+ progressPlanNodes.value = []
+ }
+
+ progressDefaultPlanNodeId.value = computeDefaultPlanNodeId(info.stage, progressPlanNodes.value)
+ progressReportVisible.value = true
+ } catch (e) {
+ ElMessage.error('鑾峰彇椤圭洰璇︽儏澶辫触')
+ } finally {
+ progressBtnLoadingId.value = null
+ }
+}
+
+async function handleProgressSubmitted(payload) {
+ try {
+ const nodes = Array.isArray(progressPlanNodes.value) ? progressPlanNodes.value : []
+ const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
+ const description = payload.remark
+ ? `${payload.reportDate || ''} ${payload.remark}`.trim()
+ : `${payload.reportDate || ''} 杩涘害姹囨姤`.trim()
+ const req = {
+ id: null,
+ projectManagementPlanNodeId: payload.planNodeId,
+ projectManagementInfoId: progressProjectId.value,
+ description,
+ actualLeaderId: userStore.id || progressProjectInfo.value?.managerId,
+ actualLeaderName: userStore.nickName || progressProjectInfo.value?.managerName,
+ estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
+ planStartTime: payload.planStartTime || progressProjectInfo.value?.planStartTime,
+ planEndTime: payload.planEndTime || progressProjectInfo.value?.planEndTime,
+ actualStartTime: payload.actualStartTime || null,
+ actualEndTime: payload.actualEndTime || null,
+ progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0,
+ storageBlobDTOs: Array.isArray(payload.storageBlobDTOs) ? payload.storageBlobDTOs : []
+ }
+ const res = await saveStage(req)
+ if (res?.code === 200) {
+ ElMessage.success('鎻愪氦鎴愬姛')
+ getList()
+ return
+ }
+ ElMessage.error(res?.msg || '鎻愪氦澶辫触')
+ } catch (e) {
+ ElMessage.error('鎻愪氦澶辫触')
+ }
+}
+
+function handleDetail(row) {
+ if (!row?.id) return
+ router.push(`/projectManagement/Management/detail/${row.id}`)
+}
+
+function handleEdit(row) {
+ formDiaRef.value?.openDialog({ operationType: 'edit', row })
+}
+
+onMounted(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+.app-container {
+ padding: 20px;
+}
+.table-container {
+ background-color: #fff;
+ padding: 20px;
+ border-radius: 4px;
+}
+.table-actions {
+ text-align: right;
+ margin-bottom: 10px;
+}
+</style>
diff --git a/src/views/projectManagement/Management/projectDetail.vue b/src/views/projectManagement/Management/projectDetail.vue
new file mode 100644
index 0000000..be670cb
--- /dev/null
+++ b/src/views/projectManagement/Management/projectDetail.vue
@@ -0,0 +1,532 @@
+<template>
+ <div class="app-container">
+ <el-card class="header-card" shadow="never">
+ <div class="header-row">
+ <div class="header-title">椤圭洰璇︽儏</div>
+ <div class="header-actions">
+ <el-button style="color: white;background: #002FA7;" @click="openProgressReport">杩涘害姹囨姤</el-button>
+ <!-- <el-button type="danger" @click="openDiscussProgress">娲借皥杩涘害</el-button> -->
+ <el-button @click="goBack">杩斿洖</el-button>
+ </div>
+ </div>
+ <el-steps v-if="steps.length > 0" :active="activeStep" align-center finish-status="success">
+ <el-step v-for="(s, idx) in steps" :key="idx" :title="s" />
+ </el-steps>
+ </el-card>
+
+ <el-card class="content-card" shadow="never" v-loading="loading">
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="鍩虹璧勬枡" name="base">
+ <el-descriptions :column="4" border>
+ <el-descriptions-item label="椤圭洰ID">{{ info.id ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曟嵁缂栧彿">{{ info.no || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鍚嶇О">{{ info.title || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ info.clientName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛ID">{{ info.clientId ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鐖堕」鐩�">{{ parentProjectLabel }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰绫诲瀷">{{ projectTypeLabel }}</el-descriptions-item>
+ <el-descriptions-item label="绔嬮」鏃ユ湡">{{ info.establishTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鏉ユ簮">{{ info.source || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="绔嬮」浜�">{{ info.managerName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="涓氬姟鍛�">{{ info.salesmanName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閮ㄩ棬">{{ info.departmentName || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曟嵁鐘舵��">
+ <dict-tag :options="bill_status" :value="String(info.status ?? '')" />
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳鐘舵��">
+ <dict-tag :options="project_management" :value="String(info.reviewStatus ?? '')" />
+ </el-descriptions-item>
+ <el-descriptions-item label="璁″垝鐘舵��">
+ <dict-tag :options="plan_status" :value="String(info.stage ?? '')" />
+ </el-descriptions-item>
+ <el-descriptions-item label="棰勮宸ユ湡(澶�)">{{ estimatedDays }}</el-descriptions-item>
+ <el-descriptions-item label="璁″垝寮�濮嬫棩鏈�">{{ info.planStartTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="璁″垝瀹屾垚鏃ユ湡">{{ info.planEndTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹為檯寮�濮嬫棩鏈�">{{ info.actualStartTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="瀹為檯瀹屾垚鏃ユ湡">{{ info.actualEndTime || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="璁㈠崟鏃ユ湡">{{ info.orderDate || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰閲戦">{{ info.orderAmount ?? '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞" :span="4">{{ info.remark || '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="attachment-block" v-if="attachments.length > 0">
+ <div class="attachment-title">闄勪欢</div>
+ <div class="attachment-list">
+ <div v-for="(att, idx) in attachments" :key="att.id || att.url || idx" class="attachment-item">
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </div>
+
+ <el-divider content-position="left">浜у搧淇℃伅</el-divider>
+ <el-table :data="productRows" border show-summary :summary-method="summarizeProductTable">
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" show-overflow-tooltip />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" show-overflow-tooltip />
+ <el-table-column label="鍗曚綅" prop="unit" width="90" />
+ <el-table-column label="鏁伴噺" prop="quantity" width="90" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" width="90" />
+ <el-table-column label="鍚◣鍗曚环(鍏�)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍚◣鎬讳环(鍏�)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="涓嶅惈绋庢�讳环(鍏�)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
+ <el-table-column label="鍙戠エ绫诲瀷" prop="invoiceType" width="110" />
+ </el-table>
+
+ <el-divider content-position="left">椤圭洰鍥㈤槦</el-divider>
+ <el-table :data="teamRows" border>
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="濮撳悕" prop="userName" show-overflow-tooltip />
+ <el-table-column label="椤圭洰缁勮鑹�" prop="userRoleName" show-overflow-tooltip />
+ <el-table-column label="杩涘叆鏃ユ湡" prop="joinTime" width="140" />
+ <el-table-column label="绂诲紑鏃ユ湡" prop="departTime" width="140" />
+ <el-table-column label="鑱旂郴鏂瑰紡" prop="contact" show-overflow-tooltip />
+ <el-table-column label="澶囨敞" prop="remark" show-overflow-tooltip />
+ </el-table>
+
+ <el-divider content-position="left">鏀惰揣鍦板潃</el-divider>
+ <el-descriptions :column="3" border>
+ <el-descriptions-item label="鏀惰揣浜�">{{ shippingAddress.consignee || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鑱旂郴鏂瑰紡">{{ shippingAddress.contract || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍦板潃">{{ shippingAddress.address || '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <el-divider content-position="left">鑱旂郴淇℃伅</el-divider>
+ <el-descriptions :column="4" border>
+ <el-descriptions-item label="鑱旂郴浜�">{{ contractInfo.name || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鎬у埆">{{ contractInfo.sex || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鐢熸棩">{{ contractInfo.birthday || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閮ㄩ棬">{{ contractInfo.department || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鑱屽姟">{{ contractInfo.job || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鎵嬫満鍙�">{{ contractInfo.phoneNumber || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="閭">{{ contractInfo.email || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="QQ">{{ contractInfo.qq || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="鍥哄畾鐢佃瘽">{{ contractInfo.lineaFissa || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="寰俊">{{ contractInfo.wx || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="绫嶈疮">{{ contractInfo.origineEtnica || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="娉曚汉浠h〃">{{ contractInfo.rappresentanteLegale || '-' }}</el-descriptions-item>
+ </el-descriptions>
+ </el-tab-pane>
+
+ <el-tab-pane label="椤圭洰绫诲瀷" name="plan">
+ <el-descriptions :column="4" border>
+ <el-descriptions-item label="绫诲瀷鍚嶇О">{{ projectPlan.name || '-' }}</el-descriptions-item>
+ <el-descriptions-item label="澶囨敞" :span="3">{{ projectPlan.description || '-' }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div class="attachment-block" v-if="planAttachments.length > 0">
+ <div class="attachment-title">绫诲瀷闄勪欢</div>
+ <div class="attachment-list">
+ <div v-for="(att, idx) in planAttachments" :key="att.id || att.url || idx" class="attachment-item">
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.name || att.fileName || att.url || '闄勪欢' }}</span>
+ <el-button link type="primary" size="small" @click="downloadAttachment(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </div>
+
+ <el-table :data="planNodeRows" border style="margin-top: 14px;">
+ <el-table-column align="center" label="姝ラ" type="index" width="80" />
+ <el-table-column label="闃舵鍚嶇О" prop="name" min-width="160" show-overflow-tooltip />
+ <el-table-column label="璐熻矗浜�" prop="leaderName" width="140" show-overflow-tooltip />
+ <el-table-column label="棰勮宸ユ湡(澶�)" prop="estimatedDuration" width="140" />
+ <el-table-column label="宸ユ椂鍗曚环" prop="hourlyRate" width="120" />
+ <el-table-column label="浣滀笟鍐呭" prop="workContent" min-width="180" show-overflow-tooltip />
+ </el-table>
+ </el-tab-pane>
+
+ <el-tab-pane label="椤圭洰闃舵" name="stage">
+ <el-table :data="stageNodeRows" border style="margin-top: 14px;">
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column label="闃舵" prop="stageName" min-width="160" show-overflow-tooltip />
+ <el-table-column label="鎻忚堪" prop="description" min-width="220" show-overflow-tooltip />
+ <el-table-column label="瀹為檯璐熻矗浜�" prop="actualLeaderName" width="140" show-overflow-tooltip />
+ <el-table-column label="杩涘害(%)" prop="progress" width="110" />
+ <el-table-column label="璁″垝寮�濮�" prop="planStartTime" width="120" />
+ <el-table-column label="璁″垝缁撴潫" prop="planEndTime" width="120" />
+ <el-table-column label="瀹為檯寮�濮�" prop="actualStartTime" width="120" />
+ <el-table-column label="瀹為檯缁撴潫" prop="actualEndTime" width="120" />
+ <el-table-column label="棰勮宸ユ湡(澶�)" prop="estimatedDuration" width="130" />
+ <el-table-column label="闄勪欢" width="90" align="center">
+ <template #default="{ row }">
+ <span>{{ row.attachmentCount }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column v-if="false" label="鎿嶄綔" width="100" align="center" fixed="right" >
+ <template #default="{ row }">
+ <el-button link type="danger" size="small" @click="handleDeleteStage(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </div>
+
+ <ProgressReportDialog
+ v-model="progressReportVisible"
+ :project-id="info.id"
+ :project-info="info"
+ :plan-nodes="planNodeRows"
+ :default-plan-node-id="defaultPlanNodeId"
+ @submitted="handleProgressSubmitted"
+ />
+ <DiscussProgressDialog
+ v-model="discussProgressVisible"
+ :project-id="info.id"
+ :plan-nodes="planNodeRows"
+ :default-plan-node-id="defaultPlanNodeId"
+ @submitted="handleDiscussSubmitted"
+ />
+</template>
+
+<script setup name="ProjectManagementDetail">
+import { computed, getCurrentInstance, onMounted, ref } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { Document } from '@element-plus/icons-vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getProject, saveStage, listStage, deleteStage } from '@/api/projectManagement/project'
+import { listPlan } from '@/api/projectManagement/projectType'
+import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue'
+import DiscussProgressDialog from '@/components/ProjectManagement/DiscussProgressDialog.vue'
+import useUserStore from '@/store/modules/user'
+
+const { proxy } = getCurrentInstance()
+const route = useRoute()
+const router = useRouter()
+const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
+
+const loading = ref(false)
+const activeTab = ref('base')
+const progressReportVisible = ref(false)
+const discussProgressVisible = ref(false)
+const userStore = useUserStore()
+
+const info = ref({})
+const shippingAddress = ref({})
+const contractInfo = ref({})
+const productRows = ref([])
+const teamRows = ref([])
+const attachments = ref([])
+const projectTypeMap = ref(new Map())
+const projectPlan = ref({})
+const planNodeRows = ref([])
+const planAttachments = ref([])
+const stageNodeRows = ref([])
+
+const estimatedDays = computed(() => {
+ const raw = info.value?.estimatedDays
+ const n = Number(raw)
+ if (Number.isFinite(n) && n > 0) return n
+ const start = info.value?.planStartTime
+ const end = info.value?.planEndTime
+ if (!start || !end) return 0
+ const startTime = new Date(`${start}T00:00:00`).getTime()
+ const endTime = new Date(`${end}T00:00:00`).getTime()
+ if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || endTime < startTime) return 0
+ return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
+})
+
+const projectTypeLabel = computed(() => {
+ const id = info.value?.projectManagementPlanId
+ if (id === undefined || id === null || id === '') return '-'
+ const p = projectTypeMap.value.get(Number(id))
+ return p?.name || String(id)
+})
+
+const parentProjectLabel = computed(() => {
+ return (
+ info.value?.parentTitle ||
+ info.value?.projectManagementInfoParentName ||
+ info.value?.projectManagementInfoParentId ||
+ '-'
+ )
+})
+
+const planStageEnum = computed(() => {
+ const list = Array.isArray(plan_status) ? plan_status : []
+ return list.map(i => ({ value: String(i.value), label: i.label }))
+})
+
+const steps = computed(() => {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ const labels = sorted.map(i => i.name || i.workContent).filter(Boolean)
+ return labels.length > 0 ? labels : planStageEnum.value.map(i => i.label)
+})
+
+const activeStep = computed(() => {
+ const statusOrStage = info.value?.stage ?? info.value?.status
+ const enumList = planStageEnum.value
+ // 浼樺厛浣跨敤 planStageEnum 鐨� value 鍖归厤
+ const found = enumList.find(i => i.value === String(statusOrStage))
+ const label = found?.label
+ // 鍦ㄩ」鐩被鍨嬭妭鐐逛腑鏌ユ壘瀵瑰簲 label 鐨勪笅鏍�
+ const nodeLabels = steps.value
+ const idxByLabel = label ? nodeLabels.findIndex(l => String(l) === String(label)) : -1
+ if (idxByLabel >= 0) return idxByLabel + 1
+ // 鍥為��锛氬鏋� statusOrStage 鏄暟瀛楃储寮�
+ const n = Number(statusOrStage)
+ if (Number.isFinite(n) && n > 0) return Math.min(n, nodeLabels.length)
+ return 0
+})
+
+function goBack() {
+ router.back()
+}
+
+function openProgressReport() {
+ progressReportVisible.value = true
+}
+
+function openDiscussProgress() {
+ discussProgressVisible.value = true
+}
+
+const defaultPlanNodeId = computed(() => {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ if (nodes.length === 0) return undefined
+ const stageVal = info.value?.stage
+ const direct = nodes.find(n => String(n.id) === String(stageVal))
+ if (direct?.id) return direct.id
+ const sorted = [...nodes].sort((a, b) => Number(a.sort ?? 0) - Number(b.sort ?? 0))
+ const idx = Number(stageVal)
+ if (Number.isFinite(idx)) {
+ const byIndex = sorted[idx - 1] || sorted[idx] || sorted[0]
+ if (byIndex?.id) return byIndex.id
+ }
+ return sorted[0]?.id
+})
+
+async function handleProgressSubmitted(payload) {
+ try {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
+ const description = payload.remark
+ ? `${payload.reportDate || ''} ${payload.remark}`.trim()
+ : `${payload.reportDate || ''} 杩涘害姹囨姤`.trim()
+ const req = {
+ id: null,
+ projectManagementPlanNodeId: payload.planNodeId,
+ projectManagementInfoId: info.value?.id,
+ description,
+ actualLeaderId: userStore.id || info.value?.managerId,
+ actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName,
+ estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
+ planStartTime: payload.planStartTime || info.value?.planStartTime,
+ planEndTime: payload.planEndTime || info.value?.planEndTime,
+ actualStartTime: payload.actualStartTime || null,
+ actualEndTime: payload.actualEndTime || null,
+ progress: Number(payload.totalProgress ?? payload.completionProgress ?? 0) || 0,
+ storageBlobDTOs: Array.isArray(payload.storageBlobDTOs) ? payload.storageBlobDTOs : []
+ }
+ const res = await saveStage(req)
+ if (res?.code === 200) {
+ ElMessage.success('鎻愪氦鎴愬姛')
+ await Promise.all([loadDetail(), loadStageList()])
+ return
+ }
+ ElMessage.error(res?.msg || '鎻愪氦澶辫触')
+ } catch (e) {
+ ElMessage.error('鎻愪氦澶辫触')
+ }
+}
+
+async function handleDiscussSubmitted(payload) {
+ try {
+ const nodes = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const node = nodes.find(n => String(n.id) === String(payload.planNodeId)) || {}
+ const req = {
+ id: null,
+ projectManagementPlanNodeId: payload.planNodeId,
+ projectManagementInfoId: info.value?.id,
+ description: payload.remark,
+ actualLeaderId: userStore.id || info.value?.managerId,
+ actualLeaderName: userStore.nickName || info.value?.managerName || info.value?.managerName,
+ estimatedDuration: Number(node.estimatedDuration ?? 0) || 0,
+ planStartTime: info.value?.planStartTime,
+ planEndTime: info.value?.planEndTime,
+ actualStartTime: info.value?.actualStartTime || null,
+ actualEndTime: info.value?.actualEndTime || null,
+ progress: Number(info.value?.progress ?? 0) || 0,
+ attachmentIds: Array.isArray(payload.attachmentIds) ? payload.attachmentIds : []
+ }
+ const res = await saveStage(req)
+ if (res?.code === 200) {
+ ElMessage.success('鎻愪氦鎴愬姛')
+ await Promise.all([loadDetail(), loadStageList()])
+ return
+ }
+ ElMessage.error(res?.msg || '鎻愪氦澶辫触')
+ } catch (e) {
+ ElMessage.error('鎻愪氦澶辫触')
+ }
+}
+
+function downloadAttachment(row) {
+ if (row?.url) {
+ try {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+ return
+ } catch (e) {}
+ }
+ ElMessage.warning('闄勪欢鏆傛棤涓嬭浇鍦板潃')
+}
+
+async function loadProjectTypeMap() {
+ try {
+ const res = await listPlan({ current: 1, size: 999 })
+ const records = res?.data?.records || res?.records || res?.rows || []
+ projectTypeMap.value = new Map((records || []).map(r => [Number(r.id), r]))
+ } catch {
+ projectTypeMap.value = new Map()
+ }
+}
+
+function getPlanNodeName(planNodeId) {
+ const list = Array.isArray(planNodeRows.value) ? planNodeRows.value : []
+ const node = list.find(n => String(n.id) === String(planNodeId))
+ return node?.name || node?.workContent || String(planNodeId ?? '')
+}
+
+async function loadStageList() {
+ const projectId = info.value?.id
+ if (!projectId) {
+ stageNodeRows.value = []
+ return
+ }
+ try {
+ const res = await listStage(projectId)
+ const data = res?.data?.data ?? res?.data ?? res
+ const list = data?.records || data?.rows || data?.list || data || []
+ const records = Array.isArray(list) ? list : []
+ stageNodeRows.value = records.map(r => {
+ const attachmentList = Array.isArray(r.attachmentList) ? r.attachmentList : []
+ const attachmentIds = Array.isArray(r.attachmentIds) ? r.attachmentIds : []
+ return {
+ ...r,
+ stageName: getPlanNodeName(r.projectManagementPlanNodeId),
+ attachmentCount: attachmentList.length || attachmentIds.length || 0
+ }
+ })
+ } catch {
+ stageNodeRows.value = []
+ }
+}
+
+async function handleDeleteStage(row) {
+ const stageId = row?.id
+ if (!stageId) return
+ try {
+ await ElMessageBox.confirm('鏄惁纭鍒犻櫎璇ラ」鐩樁娈碉紵', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ const res = await deleteStage(stageId)
+ if (res?.code === 200) {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ await loadStageList()
+ return
+ }
+ ElMessage.error(res?.msg || '鍒犻櫎澶辫触')
+ } catch {}
+}
+
+function syncProjectPlan() {
+ const id = info.value?.projectManagementPlanId
+ if (id === undefined || id === null || id === '') {
+ projectPlan.value = {}
+ planNodeRows.value = []
+ planAttachments.value = []
+ return
+ }
+ const plan = projectTypeMap.value.get(Number(id)) || {}
+ projectPlan.value = plan || {}
+ planNodeRows.value = Array.isArray(plan?.planNodeList) ? plan.planNodeList : []
+ planAttachments.value = Array.isArray(plan?.attachmentList) ? plan.attachmentList : []
+}
+
+async function loadDetail() {
+ const id = route.params?.id
+ if (!id) return
+ loading.value = true
+ try {
+ const res = await getProject(id)
+ const detail = res?.data?.data ?? res?.data ?? res
+ info.value = detail?.info || {}
+ shippingAddress.value = detail?.shippingAddress || {}
+ contractInfo.value = detail?.contractInfo || {}
+ productRows.value = Array.isArray(detail?.salesLedgerProductList) ? detail.salesLedgerProductList : []
+ teamRows.value = Array.isArray(detail?.info?.teamList) ? detail.info.teamList : []
+ attachments.value = Array.isArray(detail?.info?.attachmentList) ? detail.info.attachmentList : []
+ syncProjectPlan()
+ await loadStageList()
+ } finally {
+ loading.value = false
+ }
+}
+
+onMounted(async () => {
+ await loadProjectTypeMap()
+ await loadDetail()
+})
+</script>
+
+<style scoped lang="scss">
+.section-bar {
+ width: 3px;
+ height: 14px;
+ background: #002FA7;
+ border-radius: 2px;
+}
+.header-card {
+ margin-bottom: 14px;
+}
+
+.header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+
+.header-title {
+ font-weight: 600;
+ font-size: 16px;
+}
+
+.content-card {
+ border-radius: 8px;
+}
+
+.attachment-block {
+ margin-top: 14px;
+}
+
+.attachment-title {
+ font-weight: 600;
+ margin-bottom: 8px;
+}
+
+.attachment-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.attachment-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.attachment-name {
+ max-width: 520px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+</style>
+
diff --git a/src/views/projectManagement/projectType/components/ProjectTypeDialog.vue b/src/views/projectManagement/projectType/components/ProjectTypeDialog.vue
new file mode 100644
index 0000000..bc1e196
--- /dev/null
+++ b/src/views/projectManagement/projectType/components/ProjectTypeDialog.vue
@@ -0,0 +1,431 @@
+<template>
+ <el-dialog
+ :title="title"
+ v-model="visible"
+ width="1000px"
+ append-to-body
+ @close="handleClose"
+ >
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="0">
+ <!-- 椤堕儴鍩虹淇℃伅 -->
+ <div class="base-info-row">
+ <div class="info-item">
+ <span class="item-label required">鍚嶇О</span>
+ <el-input
+ v-model="form.name"
+ placeholder="璇疯緭鍏ュ悕绉�"
+ maxlength="10"
+ show-word-limit
+ style="width: 220px"
+ />
+ </div>
+ <div class="info-item">
+ <span class="item-label">澶囨敞</span>
+ <el-input
+ v-model="form.description"
+ placeholder="璇疯緭鍏ュ娉�"
+ maxlength="20"
+ show-word-limit
+ style="width: 220px"
+ />
+ </div>
+ </div>
+ <div class="base-info-row">
+ <div class="info-item">
+ <span class="item-label">闄勪欢</span>
+ <FileUpload v-if="isEdit" v-model:file-list="uploadFileList" />
+ <span v-else class="text-gray-400 text-sm">璇峰厛淇濆瓨鍚庡啀涓婁紶闄勪欢</span>
+ </div>
+ </div>
+
+
+ <!-- 姝ラ閰嶇疆琛ㄦ牸 -->
+ <p class="top-tip">璇锋寜鐓ч『搴忛厤缃」鐩樁娈碉紝鎷栨嫿<b>姝ラ</b>鎺掑簭鍗冲彲</p>
+ <div class="step-table-container">
+ <el-table
+ :data="form.savePlanNodeList"
+ border
+ style="width: 100%"
+ row-key="id"
+ class="drag-table"
+ >
+ <el-table-column label="姝ラ" width="80" align="center" class-name="drag-handle">
+ <template #default="scope">
+ <div class="step-index" style="cursor: move;">
+ {{ scope.$index + 1 }}
+ </div>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="闃舵鍚嶇О" min-width="150">
+ <template #header>
+ <span class="required-star">*</span> 闃舵鍚嶇О
+ </template>
+ <template #default="scope">
+ <el-form-item
+ :prop="'savePlanNodeList.' + scope.$index + '.name'"
+ :rules="[{ required: true, message: '璇疯緭鍏ラ樁娈靛悕绉�', trigger: 'blur' }]"
+ >
+ <el-input v-model="scope.row.name" placeholder="璇疯緭鍏�" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="璐熻矗浜�" width="180">
+ <template #header>
+ <span class="required-star">*</span> 璐熻矗浜�
+ </template>
+ <template #default="scope">
+ <el-form-item
+ :prop="'savePlanNodeList.' + scope.$index + '.leaderId'"
+ :rules="[{ required: true, message: '璇烽�夋嫨璐熻矗浜�', trigger: 'change' }]"
+ >
+ <el-select
+ v-model="scope.row.leaderId"
+ placeholder="娴嬭瘯"
+ @change="(val) => handleLeaderChange(val, scope.row)"
+ >
+ <el-option
+ v-for="item in userOptions"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="棰勮宸ユ湡 (澶�)" width="150">
+ <template #header>
+ 棰勮宸ユ湡 (澶�)
+ <el-tooltip content="瀹屾垚璇ラ樁娈甸璁¢渶瑕佺殑澶╂暟" placement="top">
+ <el-icon class="info-icon"><QuestionFilled /></el-icon>
+ </el-tooltip>
+ </template>
+ <template #default="scope">
+ <el-input-number
+ v-model="scope.row.estimatedDuration"
+ :min="0"
+ controls-position="right"
+ style="width: 100%"
+ />
+ </template>
+ </el-table-column>
+
+ <el-table-column label="宸ユ椂鍗曚环" width="120">
+ <template #default="scope">
+ <el-input v-model="scope.row.hourlyRate" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+
+ <el-table-column label="浣滀笟鍐呭" min-width="150">
+ <template #default="scope">
+ <el-input v-model="scope.row.workContent" placeholder="璇疯緭鍏�" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" min-width="150">
+ <template #default="scope">
+ <el-button type="danger" size="mini" @click="removeStep(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="add-row-btn" @click="addStep">
+ <el-icon><Plus /></el-icon> 鏂板涓�琛�
+ </div>
+ </div>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">鎻愪氦</el-button>
+ <el-button @click="visible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref, watch, onMounted, nextTick } from 'vue';
+import { Plus, QuestionFilled } from '@element-plus/icons-vue';
+import { userListNoPageByTenantId } from '@/api/system/user';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { getToken } from '@/utils/auth';
+import Sortable from 'sortablejs';
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+
+const props = defineProps({
+ modelValue: Boolean,
+ title: String,
+ data: Object
+});
+
+const emit = defineEmits(['update:modelValue', 'submit']);
+
+const visible = ref(false);
+const formRef = ref(null);
+const userOptions = ref([]);
+const isEdit = ref(false);
+let sortable = null;
+
+const form = ref({
+ id: undefined,
+ name: '',
+ description: '',
+ savePlanNodeList: []
+});
+const uploadFileList = ref([]);
+
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ュ悕绉�', trigger: 'blur' }]
+};
+
+// 鐩戝惉寮圭獥鏄剧ず/闅愯棌
+watch(() => props.modelValue, (val) => {
+ visible.value = val;
+ if (val) {
+ if (props.data) {
+ // 缂栬緫妯″紡 - 鍥炴樉鏁版嵁
+ isEdit.value = true;
+ form.value = {
+ id: props.data.id,
+ name: props.data.name,
+ description: props.data.description,
+ savePlanNodeList: []
+ };
+ uploadFileList.value = props.data.storageBlobDTOs || [];
+
+ // 鍥炴樉姝ラ鑺傜偣
+ if (props.data.planNodeList && props.data.planNodeList.length > 0) {
+ form.value.savePlanNodeList = props.data.planNodeList.map(node => ({
+ id: node.id,
+ projectManagementPlanId: node.projectManagementPlanId,
+ sort: node.sort,
+ name: node.name,
+ leaderId: node.leaderId,
+ leaderName: node.leaderName,
+ estimatedDuration: node.estimatedDuration,
+ hourlyRate: node.hourlyRate,
+ workContent: node.workContent
+ }));
+ } else {
+ form.value.savePlanNodeList = [createDefaultNode()];
+ }
+ } else {
+ // 鏂板妯″紡
+ isEdit.value = false;
+ resetForm();
+ }
+ // 鍒濆鍖栨嫋鎷�
+ nextTick(() => {
+ initSortable();
+ });
+ }
+});
+
+watch(visible, (val) => {
+ emit('update:modelValue', val);
+});
+
+/** 鍒濆鍖栨嫋鎷� */
+function initSortable() {
+ const el = document.querySelector('.drag-table .el-table__body-wrapper tbody');
+ if (!el) return;
+
+ if (sortable) {
+ sortable.destroy();
+ }
+
+ sortable = Sortable.create(el, {
+ handle: '.drag-handle',
+ animation: 150,
+ onEnd: ({ newIndex, oldIndex }) => {
+ const targetRow = form.value.savePlanNodeList.splice(oldIndex, 1)[0];
+ form.value.savePlanNodeList.splice(newIndex, 0, targetRow);
+ }
+ });
+}
+
+/** 鍒涘缓榛樿鑺傜偣瀵硅薄 */
+function createDefaultNode() {
+ return {
+ name: '',
+ leaderId: null,
+ leaderName: null,
+ estimatedDuration: null,
+ hourlyRate: null,
+ workContent: null
+ };
+}
+
+/** 閲嶇疆琛ㄥ崟 */
+function resetForm() {
+ form.value = {
+ id: undefined,
+ name: '',
+ description: '',
+ savePlanNodeList: [createDefaultNode()]
+ };
+ uploadFileList.value = [];
+ if (formRef.value) {
+ formRef.value.resetFields();
+ }
+}
+
+/** 鑾峰彇鐢ㄦ埛鍒楄〃 */
+async function getUserList() {
+ try {
+ const res = await userListNoPageByTenantId();
+ if (res.code === 200) {
+ userOptions.value = res.data || [];
+ }
+ } catch (error) {
+ console.error('鑾峰彇鐢ㄦ埛鍒楄〃澶辫触:', error);
+ }
+}
+
+/** 澶勭悊璐熻矗浜哄彉鍖� */
+function handleLeaderChange(val, row) {
+ const user = userOptions.value.find(u => u.userId === val);
+ if (user) {
+ row.leaderName = user.nickName;
+ }
+}
+
+/** 娣诲姞姝ラ */
+function addStep() {
+ form.value.savePlanNodeList.push(createDefaultNode());
+}
+
+/** 绉婚櫎姝ラ */
+function removeStep(index) {
+ if (form.value.savePlanNodeList.length <= 1) {
+ ElMessage.warning('鑷冲皯淇濈暀涓�涓楠�');
+ return;
+ }
+
+ ElMessageBox.confirm('鏄惁纭鍒犻櫎璇ユ楠わ紵', '绯荤粺鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ form.value.savePlanNodeList.splice(index, 1);
+ }).catch(() => {});
+}
+
+/** 绉诲姩姝ラ */
+function moveStep(index, direction) {
+ const targetIndex = index + direction;
+ if (targetIndex < 0 || targetIndex >= form.value.savePlanNodeList.length) return;
+
+ const list = form.value.savePlanNodeList;
+ const temp = list[index];
+ list[index] = list[targetIndex];
+ list[targetIndex] = temp;
+}
+
+/** 鎻愪氦琛ㄥ崟 */
+async function submitForm() {
+ if (!formRef.value) return;
+
+ try {
+ const valid = await formRef.value.validate();
+ if (valid) {
+ // 鎻愪氦鍓嶈嚜鍔ㄥ~鍏� sort 瀛楁锛屾寜褰撳墠鏁扮粍椤哄簭鎺掑簭
+ form.value.savePlanNodeList.forEach((node, index) => {
+ node.sort = index;
+ });
+ form.value.storageBlobDTOs = uploadFileList.value;
+ emit('submit', form.value);
+ }
+ } catch (error) {
+ console.error('琛ㄥ崟楠岃瘉澶辫触:', error);
+ }
+}
+
+/** 鍏抽棴寮圭獥 */
+function handleClose() {
+ resetForm();
+}
+
+onMounted(() => {
+ getUserList();
+});
+</script>
+
+<style scoped lang="scss">
+.base-info-row {
+ display: flex;
+ gap: 40px;
+ margin-bottom: 25px;
+ padding: 0 10px;
+
+ .info-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+
+ .item-label {
+ font-size: 14px;
+ color: #606266;
+ white-space: nowrap;
+
+ &.required::before {
+ content: '*';
+ color: #f56c6c;
+ margin-right: 4px;
+ }
+ }
+ }
+}
+
+.step-table-container {
+ padding: 0 10px;
+
+ :deep(.el-form-item) {
+ margin-bottom: 0;
+ }
+
+ .required-star {
+ color: #f56c6c;
+ margin-right: 4px;
+ }
+
+ .info-icon {
+ font-size: 14px;
+ color: #909399;
+ margin-left: 4px;
+ cursor: pointer;
+ }
+
+ .add-row-btn {
+ margin-top: 15px;
+ height: 40px;
+ border: 1px dashed #dcdfe6;
+ border-radius: 4px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: #409eff;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.3s;
+
+ &:hover {
+ border-color: #409eff;
+ background-color: #f0f7ff;
+ }
+ }
+}
+
+.dialog-footer {
+ text-align: center;
+}
+.top-tip {
+
+ font-size: 14px;
+ color: #606266;
+ margin:0 0 10px 10px;
+}
+</style>
diff --git a/src/views/projectManagement/projectType/index.vue b/src/views/projectManagement/projectType/index.vue
new file mode 100644
index 0000000..65cc916
--- /dev/null
+++ b/src/views/projectManagement/projectType/index.vue
@@ -0,0 +1,509 @@
+<template>
+ <div class="app-container">
+ <div class="header-section">
+ <div class="title-container">
+ <span class="blue-bar"></span>
+ <span class="title-text">椤圭洰绫诲瀷</span>
+ </div>
+ <el-button type="primary" class="add-btn" @click="handleAdd">鏂板</el-button>
+ </div>
+
+ <div class="content-section" v-loading="loading">
+ <div class="card-list-scroll">
+ <div v-for="item in projectTypeList" :key="item.id" class="project-type-card">
+ <div class="card-header">
+ <div class="info-group">
+ <span class="label">绫诲瀷鍚嶇О:</span>
+ <span class="value">{{ item.name }}</span>
+ </div>
+ <div class="info-group">
+ <span class="label">澶囨敞:</span>
+ <span class="value">{{ item.description || '--' }}</span>
+ </div>
+ <div class="info-group">
+ <span class="label">闄勪欢:</span>
+ <div
+ class="attachment-info"
+ v-if="(item.attachmentList?.length || 0) > 0"
+ @click="handleExpand(item)"
+ >
+ {{ item.attachmentList[0]?.fileName || item.attachmentList[0]?.name }}
+ <span v-if="item.attachmentList.length > 1" class="file-count">
+ +{{ item.attachmentList.length - 1 }}
+ </span>
+ <span class="expand-link">{{ item.expanded ? '鏀惰捣' : '灞曞紑' }}</span>
+ </div>
+ <span class="value" v-else>--</span>
+ </div>
+ <div class="actions">
+ <el-button link type="primary" @click="handleUpdate(item)">缂栬緫</el-button>
+ <el-button link type="primary" @click="handleCopy(item)">澶嶅埗</el-button>
+ <el-button link type="danger" @click="handleDelete(item)">鍒犻櫎</el-button>
+ </div>
+ </div>
+
+ <el-collapse-transition>
+ <div v-show="item.expanded" class="expanded-content">
+ <div class="attachment-list">
+ <div
+ v-for="att in (item.attachmentList || [])"
+ :key="att.id || att.url || att.fileUrl || att.fileName || att.name"
+ class="attachment-item"
+ >
+ <el-icon><Document /></el-icon>
+ <span class="attachment-name">{{ att.fileName || att.name || '--' }}</span>
+ <el-button link type="primary" size="small" @click="handleDownload(att)">涓嬭浇</el-button>
+ </div>
+ </div>
+ </div>
+ </el-collapse-transition>
+
+ <div class="card-body">
+ <div class="workflow-container">
+ <div v-for="(step, index) in item.steps" :key="index" class="workflow-step">
+ <div class="step-main">
+ <div class="step-circle">{{ index + 1 }}</div>
+ <div v-if="index < item.steps.length - 1" class="step-line"></div>
+ </div>
+ <div class="step-label">{{ step.label }}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="pagination-container">
+ <el-pagination
+ v-model:current-page="queryParams.current"
+ v-model:page-size="queryParams.size"
+ :page-sizes="[10, 20, 30, 50]"
+ layout="total, sizes, prev, pager, next, jumper"
+ :total="total"
+ @size-change="getList"
+ @current-change="getList"
+ />
+ </div>
+ </div>
+
+ <!-- 娣诲姞鎴栦慨鏀归」鐩被鍨嬪璇濇 -->
+ <ProjectTypeDialog
+ v-model="open"
+ :title="title"
+ :data="editData"
+ @submit="handleDialogSubmit"
+ />
+ </div>
+</template>
+
+<script setup name="ProjectType">
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
+import { Document, Download, ArrowDown } from '@element-plus/icons-vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { listPlan, savePlan, deletePlan } from '@/api/projectManagement/projectType';
+import ProjectTypeDialog from './components/ProjectTypeDialog.vue';
+
+const { proxy } = getCurrentInstance();
+
+// 閬僵灞�
+const loading = ref(false);
+// 鎬绘潯鏁�
+const total = ref(0);
+// 椤圭洰绫诲瀷琛ㄦ牸鏁版嵁
+const projectTypeList = ref([]);
+// 寮瑰嚭灞傛爣棰�
+const title = ref("");
+// 鏄惁鏄剧ず寮瑰嚭灞�
+const open = ref(false);
+// 缂栬緫鏁版嵁
+const editData = ref(null);
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+ current: 1,
+ size: 10,
+});
+
+/** 鏌ヨ椤圭洰绫诲瀷鍒楄〃 */
+async function getList() {
+ loading.value = true;
+ try {
+ const res = await listPlan(queryParams);
+ if (res.code === 200) {
+ projectTypeList.value = res.data.records.map(item => ({
+ ...item,
+ expanded: false,
+ attachmentList: Array.isArray(item.attachmentList) ? item.attachmentList : [],
+ // 鍚庣杩斿洖鐨勮妭鐐瑰垪琛ㄥ彲鑳芥槸 planNodeList 鎴� savePlanNodeList
+ steps: (item.planNodeList || item.savePlanNodeList || []).map(node => ({
+ label: node.name
+ }))
+ }));
+ total.value = res.data.total;
+ } else {
+ ElMessage.error(res.msg || "鑾峰彇鍒楄〃澶辫触");
+ }
+ } catch (error) {
+ console.error("鑾峰彇鍒楄〃澶辫触:", error);
+ // 濡傛灉鎺ュ彛涓嶉�氾紝鏆傛椂淇濈暀妯℃嫙鏁版嵁渚涙紨绀�
+ if (projectTypeList.value.length === 0) {
+ projectTypeList.value = [
+ {
+ id: 1,
+ name: 'A椤圭洰',
+ description: '',
+ attachmentList: [{ id: 1, fileName: 'precaution...' }],
+ steps: [{ label: '绔嬮」' }, { label: '璁捐' }, { label: '閲囪喘' }, { label: '鐢熶骇' }, { label: '鍑鸿揣' }],
+ expanded: false
+ }
+ ];
+ total.value = 1;
+ }
+ } finally {
+ loading.value = false;
+ }
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ editData.value = null;
+ open.value = true;
+ title.value = "娣诲姞椤圭洰绫诲瀷";
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ editData.value = row;
+ open.value = true;
+ title.value = "淇敼椤圭洰绫诲瀷";
+}
+
+/** 寮圭獥鎻愪氦澶勭悊 */
+async function handleDialogSubmit(formData) {
+ try {
+ const res = await savePlan(formData);
+ if (res.code === 200) {
+ ElMessage.success(formData.id !== undefined ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+ open.value = false;
+ getList();
+ } else {
+ ElMessage.error(res.msg || "淇濆瓨澶辫触");
+ }
+ } catch (error) {
+ console.error("淇濆瓨澶辫触:", error);
+ }
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ ElMessageBox.confirm('鏄惁纭鍒犻櫎椤圭洰绫诲瀷缂栧彿涓�"' + row.id + '"鐨勬暟鎹」锛�', "绯荤粺鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning"
+ }).then(async function() {
+ try {
+ const res = await deletePlan(row.id);
+ if (res.code === 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ getList();
+ } else {
+ ElMessage.error(res.msg || "鍒犻櫎澶辫触");
+ }
+ } catch (error) {
+ console.error("鍒犻櫎澶辫触:", error);
+ }
+ }).catch(() => {});
+}
+
+/** 澶嶅埗鎸夐挳鎿嶄綔 */
+async function handleCopy(row) {
+ const copyData = {
+ name: row.name + " - 鍓湰",
+ description: row.description,
+ attachmentIds: Array.isArray(row.attachmentIds)
+ ? row.attachmentIds
+ : (row.attachmentList || []).map(x => x.id).filter(Boolean),
+ savePlanNodeList: (row.planNodeList || row.savePlanNodeList || []).map(node => ({
+ name: node.name,
+ leaderId: node.leaderId,
+ leaderName: node.leaderName,
+ estimatedDuration: node.estimatedDuration,
+ hourlyRate: node.hourlyRate,
+ workContent: node.workContent
+ }))
+ };
+ try {
+ const res = await savePlan(copyData);
+ if (res.code === 200) {
+ ElMessage.success("澶嶅埗鎴愬姛");
+ getList();
+ } else {
+ ElMessage.error(res.msg || "澶嶅埗澶辫触");
+ }
+ } catch (error) {
+ console.error("澶嶅埗澶辫触:", error);
+ }
+}
+
+/** 灞曞紑/鏀惰捣闄勪欢 */
+function handleExpand(item) {
+ item.expanded = !item.expanded;
+}
+
+/** 涓嬭浇闄勪欢 */
+function handleDownload(row) {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.app-container {
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.header-section {
+ flex-shrink: 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: #fff;
+ padding: 15px 20px;
+ border-radius: 8px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+
+ .title-container {
+ display: flex;
+ align-items: center;
+
+ .blue-bar {
+ width: 4px;
+ height: 18px;
+ background-color: #409eff;
+ margin-right: 10px;
+ border-radius: 2px;
+ }
+
+ .title-text {
+ font-size: 16px;
+ font-weight: bold;
+ color: #333;
+ }
+ }
+
+ .add-btn {
+ padding: 8px 20px;
+ }
+}
+
+.content-section{
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+}
+
+.card-list-scroll {
+ flex: 1;
+ overflow-y: auto;
+ padding: 20px;
+}
+
+.project-type-card {
+ background-color: #fff;
+ border-radius: 8px;
+ padding: 20px;
+ margin-bottom: 20px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
+ border: 1px solid #ebeef5;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+
+ .card-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 25px;
+ position: relative;
+
+ .info-group {
+ margin-right: 40px;
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+
+ .label {
+ color: #666;
+ margin-right: 8px;
+ }
+
+ .value {
+ color: #333;
+ }
+
+ .attachment-info {
+ display: flex;
+ align-items: center;
+ background-color: #f0f4ff;
+ padding: 4px 10px;
+ border-radius: 4px;
+ color: #409eff;
+ cursor: pointer;
+
+ .file-icon {
+ margin-right: 5px;
+ }
+
+ .file-name {
+ margin-right: 5px;
+ max-width: 100px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .file-count {
+ margin-right: 8px;
+ font-size: 12px;
+ color: #909399;
+ }
+
+ .download-icon {
+ font-size: 12px;
+ margin-right: 10px;
+ }
+
+ .expand-link {
+ color: #409eff;
+ margin-right: 5px;
+ }
+
+ .arrow-icon {
+ color: #409eff;
+ font-size: 12px;
+ }
+ }
+ }
+
+ .actions {
+ margin-left: auto;
+ }
+}
+
+.expanded-content {
+ padding: 0 20px 20px;
+ border-bottom: 1px dashed #ebeef5;
+ margin-bottom: 20px;
+
+ .attachment-list {
+ background-color: #f8f9fa;
+ padding: 15px;
+ border-radius: 4px;
+
+ .attachment-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 14px;
+ color: #606266;
+
+ .el-icon {
+ font-size: 16px;
+ color: #409eff;
+ }
+
+ .attachment-name {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ }
+ }
+}
+
+.card-body {
+ .workflow-container {
+ display: flex;
+ align-items: flex-start;
+ padding: 10px 0;
+ overflow-x: auto;
+
+ .workflow-step {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ min-width: 120px;
+ flex-shrink: 0;
+
+ .step-main {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ position: relative;
+
+ .step-circle {
+ width: 24px;
+ height: 24px;
+ background-color: #409eff;
+ color: #fff;
+ border-radius: 50%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: 12px;
+ font-weight: bold;
+ z-index: 2;
+ margin: 0 auto;
+ }
+
+ .step-line {
+ position: absolute;
+ height: 2px;
+ background-color: #d9e6ff;
+ left: calc(50% + 12px);
+ right: calc(-50% + 12px);
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 1;
+ }
+ }
+
+ .step-label {
+ margin-top: 10px;
+ font-size: 13px;
+ color: #333;
+ text-align: center;
+ }
+ }
+ }
+ }
+}
+
+.pagination-container {
+ flex-shrink: 0;
+ display: flex;
+ justify-content: flex-end;
+ padding: 10px 20px;
+ background-color: #fff;
+ border-top: 1px solid #ebeef5;
+ margin-top: 0;
+}
+
+.step-config-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+</style>
diff --git a/src/views/projectManagement/roles/index.vue b/src/views/projectManagement/roles/index.vue
new file mode 100644
index 0000000..1e161f8
--- /dev/null
+++ b/src/views/projectManagement/roles/index.vue
@@ -0,0 +1,296 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px">
+ <el-form-item label="瑙掕壊鍚嶇О" prop="roleName">
+ <el-input
+ v-model="queryParams.name"
+ placeholder="璇疯緭鍏ヨ鑹插悕绉�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="瑙掕壊鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option label="鍚敤" value="0" />
+ <el-option label="绂佺敤" value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:role:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:role:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:role:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <!-- 琛ㄦ牸鏁版嵁 -->
+ <div class="table_list">
+ <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="瑙掕壊缂栧彿" prop="no" />
+ <el-table-column label="瑙掕壊鍚嶇О" prop="name" :show-overflow-tooltip="true" />
+ <el-table-column label="鐘舵��" align="center" width="100">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ :active-value="0"
+ :inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" align="center" width="120">
+ <template #default="scope">
+ <el-tooltip content="淇敼" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+ </div>
+ <!-- 娣诲姞鎴栦慨鏀硅鑹查厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="roleRef" :model="form" :rules="rules" label-width="100px">
+ <el-form-item label="瑙掕壊缂栧彿" prop="no">
+ <el-input
+ v-model="form.no"
+ style="max-width: 600px"
+ :placeholder="form.isDefaultNo ? '浣跨敤绯荤粺缂栧彿' : '璇疯緭鍏ヨ鑹茬紪鍙�'"
+ :disabled="form.isDefaultNo"
+ >
+ <template #append>
+ <el-checkbox v-model="form.isDefaultNo" size="large" />
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item label="瑙掕壊鍚嶇О" prop="name">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ヨ鑹插悕绉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Role">
+import {onMounted} from "vue";
+import {createRole, deleteRoles, findRoleListPage, updateRole} from "@/api/projectManagement/role.js";
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const roleList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const editingId = ref(undefined)
+const form = reactive({
+ isDefaultNo: true,
+ no: "",
+ name: "",
+});
+
+const rules = computed(() => ({
+ no: [{ required: !form.isDefaultNo, message: "瑙掕壊缂栧彿涓嶈兘涓虹┖", trigger: "blur" }],
+ name: [{ required: true, message: "瑙掕壊鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+}));
+
+const data = reactive({
+ queryParams: {
+ current: 1,
+ size: 10,
+ name: undefined,
+ status: undefined
+ },
+})
+
+const { queryParams } = toRefs(data)
+
+/** 鏌ヨ瑙掕壊鍒楄〃 */
+function getList() {
+ loading.value = true
+ findRoleListPage(queryParams.value).then(res => {
+ roleList.value = res.data.records
+ total.value = res.data.total
+ loading.value = false
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.current = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ queryParams.value.name = undefined
+ queryParams.value.status = undefined
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ let roleIds = []
+ if (row.id) {
+ roleIds = [row.id]
+ } else {
+ roleIds = ids.value
+ }
+ proxy.$modal.confirm('纭畾瑕佸垹闄よ鏁版嵁鍚�?').then(function () {
+ return deleteRoles(roleIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.id)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 瑙掕壊鐘舵�佷慨鏀� */
+function handleStatusChange(row) {
+ let text = row.status === 0 ? "鍚敤" : "鍋滅敤"
+ proxy.$modal.confirm('纭瑕�"' + text + '""' + row.name + '"瑙掕壊鍚�?').then(function () {
+ return updateRole(row)
+ }).then(() => {
+ proxy.$modal.msgSuccess(text + "鎴愬姛")
+ }).catch(function () {
+ row.status = row.status === 0 ? 1 : 0
+ })
+}
+
+/** 閲嶇疆鏂板鐨勮〃鍗曚互鍙婂叾浠栨暟鎹� */
+function reset() {
+ // 閲嶇疆琛ㄥ崟鏁版嵁
+ Object.assign(form, {
+ isDefaultNo: true,
+ no: "",
+ name: "",
+ })
+ editingId.value = undefined
+}
+
+/** 娣诲姞瑙掕壊 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞瑙掕壊"
+}
+
+/** 淇敼瑙掕壊 */
+function handleUpdate(row) {
+ reset()
+ editingId.value = row.id
+ form.no = row.no
+ form.name = row.name
+ title.value = "淇敼瑙掕壊"
+ open.value = true
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["roleRef"].validate(valid => {
+ if (valid) {
+ if (editingId.value) {
+ const data = {
+ id: editingId.value,
+ ...form
+ }
+ updateRole(data).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ }).finally(() => {
+ proxy.$refs["roleRef"].resetFields()
+ open.value = false
+ getList()
+ })
+ } else {
+ createRole(form).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ }).finally(() => {
+ proxy.$refs["roleRef"].resetFields()
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/qualityManagement/afterSalesTraceability/index.vue b/src/views/qualityManagement/afterSalesTraceability/index.vue
new file mode 100644
index 0000000..8cd4856
--- /dev/null
+++ b/src/views/qualityManagement/afterSalesTraceability/index.vue
@@ -0,0 +1,595 @@
+<template>
+ <div class="app-container">
+ <!-- 鏌ヨ鏉′欢 -->
+ <el-form
+ :model="queryParams"
+ ref="queryForm"
+ :inline="true"
+ v-show="showSearch"
+ label-width="90px"
+ >
+ <el-form-item label="浜у搧鍨嬪彿" prop="productModel">
+ <el-input
+ v-model="queryParams.productModel"
+ placeholder="璇疯緭鍏ヤ骇鍝佸瀷鍙�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+ <el-input
+ v-model="queryParams.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ @keyup.enter.native="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍙嶉鏃堕棿" prop="feedbackRange">
+ <el-date-picker
+ v-model="queryParams.feedbackRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ />
+ </el-form-item>
+ <el-form-item label="澶勭悊鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="璇烽�夋嫨澶勭悊鐘舵��"
+ clearable
+ >
+ <el-option
+ v-for="item in statusOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">
+ 鎼滅储
+ </el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <!-- 鎿嶄綔鍖� -->
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="3">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ >
+ 鏂板鍞悗璐ㄩ噺璁板綍
+ </el-button>
+ </el-col>
+ <el-col :span="3">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ >
+ 淇敼
+ </el-button>
+ </el-col>
+ <el-col :span="3">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ >
+ 鍒犻櫎
+ </el-button>
+ </el-col>
+ <el-col :span="3">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ >
+ 瀵煎嚭
+ </el-button>
+ </el-col>
+ <right-toolbar
+ v-model:showSearch="showSearch"
+ @queryTable="getList"
+ />
+ </el-row>
+
+ <!-- 鏁版嵁琛� -->
+ <el-table
+ v-loading="loading"
+ :data="afterSalesList"
+ @selection-change="handleSelectionChange"
+ >
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="搴忓彿" type="index" width="55" align="center" />
+ <el-table-column label="閿�鍞悎鍚屽彿" prop="contractNo" width="160" />
+ <el-table-column label="浜у搧缂栧彿" prop="productCode" width="140" />
+ <el-table-column label="浜у搧鍨嬪彿" prop="productModel" width="140" />
+ <el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" width="160" />
+ <el-table-column label="鑱旂郴鏂瑰紡" prop="contact" width="140" />
+ <el-table-column label="鍙嶉鏃堕棿" prop="feedbackTime" width="160">
+ <template #default="scope">
+ <span>{{ scope.row.feedbackTime }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="闂鎻忚堪" prop="problemDesc" show-overflow-tooltip />
+ <el-table-column label="缁翠慨鎯呭喌" prop="repairInfo" show-overflow-tooltip />
+ <el-table-column label="澶勭悊缁撴灉" prop="result" show-overflow-tooltip />
+ <el-table-column label="澶勭悊鐘舵��" prop="status" width="120" align="center">
+ <template #default="scope">
+ <dict-tag
+ :options="statusOptions"
+ :value="scope.row.status"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width" width="160" fixed="right">
+ <template #default="scope">
+ <el-button size="small" type="text" icon="Edit" @click="handleUpdate(scope.row)">
+ 淇敼
+ </el-button>
+ <el-button size="small" type="text" icon="Delete" @click="handleDelete(scope.row)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 鏂板/淇敼寮圭獥 -->
+ <el-dialog
+ :title="title"
+ v-model="open"
+ width="900px"
+ append-to-body
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="110px"
+ >
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="閿�鍞悎鍚屽彿" prop="contractNo">
+ <el-input
+ v-model="form.contractNo"
+ placeholder="璇烽�夋嫨鍏宠仈閿�鍞悎鍚屽彿"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜у搧缂栧彿" prop="productCode">
+ <el-input
+ v-model="form.productCode"
+ placeholder="璇烽�夋嫨鍏宠仈浜у搧缂栧彿"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍨嬪彿" prop="productModel">
+ <el-input
+ v-model="form.productModel"
+ placeholder="璇疯緭鍏ヤ骇鍝佸瀷鍙�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+ <el-input
+ v-model="form.customerName"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鏂瑰紡" prop="contact">
+ <el-input
+ v-model="form.contact"
+ placeholder="璇疯緭鍏ュ鎴疯仈绯绘柟寮�"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍙嶉鏃堕棿" prop="feedbackTime">
+ <el-date-picker
+ v-model="form.feedbackTime"
+ type="datetime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm"
+ placeholder="璇烽�夋嫨鍙嶉鏃堕棿"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="瀹㈡埛鍙嶉闂" prop="problemDesc">
+ <el-input
+ v-model="form.problemDesc"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯缁嗚褰曞鎴峰弽棣堥棶棰樸�佺幇璞℃弿杩扮瓑淇℃伅"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="缁翠慨鎯呭喌" prop="repairInfo">
+ <el-input
+ v-model="form.repairInfo"
+ type="textarea"
+ :rows="2"
+ placeholder="璁板綍缁翠慨杩囩▼銆佷娇鐢ㄥ浠躲�佽繑淇鏁扮瓑"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊缁撴灉" prop="result">
+ <el-input
+ v-model="form.result"
+ type="textarea"
+ :rows="2"
+ placeholder="璁板綍鏈�缁堝鐞嗙粨鏋滐紝濡傛洿鎹€�侀��璐с�佸崌绾х瓑"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊鐘舵��" prop="status">
+ <el-select
+ v-model="form.status"
+ placeholder="璇烽�夋嫨澶勭悊鐘舵��"
+ >
+ <el-option
+ v-for="item in statusOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="form.remark"
+ type="textarea"
+ :rows="2"
+ placeholder="鍙褰曞悗缁窡韪剰瑙併�佸鐩樼粨璁虹瓑"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="AfterSalesTraceability">
+import { ref, reactive, onMounted } from "vue";
+import { ElMessageBox } from "element-plus";
+
+const { proxy } = getCurrentInstance();
+
+// 鐘舵�佸瓧鍏�
+const statusOptions = ref([
+ { label: "寰呭鐞�", value: "0" },
+ { label: "澶勭悊涓�", value: "1" },
+ { label: "宸插畬鎴�", value: "2" },
+ { label: "宸插叧闂�", value: "3" },
+]);
+
+// 妯℃嫙鍞悗璐ㄩ噺鏁版嵁
+const afterSalesList = ref([
+ {
+ id: 1,
+ contractNo: "SC-2024-001",
+ productCode: "P-10001",
+ productModel: "XG-500A",
+ customerName: "鍗庡崡鐢靛瓙绉戞妧鏈夐檺鍏徃",
+ contact: "寮犲伐 / 13800000001",
+ feedbackTime: "2024-12-01 10:23:00",
+ problemDesc: "浣跨敤涓変釜鏈堝悗鍑虹幇闂存瓏鎬ф帀鐢碉紝褰卞搷浜х嚎绋冲畾杩愯銆�",
+ repairInfo: "瀹夋帓宸ョ▼甯堜笂闂ㄦ淇紝鏇存崲鐢垫簮妯″潡骞跺姞鍥烘帴绾跨瀛愩��",
+ result: "鏇存崲鐢垫簮鎬绘垚锛屾仮澶嶆甯镐娇鐢紝寤鸿瀹㈡埛澧炲姞UPS淇濇姢銆�",
+ status: "2",
+ remark: "鍒楀叆閲嶇偣璺熻釜瀹㈡埛锛屽悗缁瀵熶竴涓搴︺��",
+ },
+ {
+ id: 2,
+ contractNo: "SC-2024-015",
+ productCode: "P-10045",
+ productModel: "XG-500B",
+ customerName: "鍗庝笢绮惧瘑鍒堕�犳湁闄愬叕鍙�",
+ contact: "鏉庡伐 / 13800000002",
+ feedbackTime: "2024-12-05 15:40:00",
+ problemDesc: "閮ㄥ垎鎵规鍑虹幇澶栧3鍒姳锛屽鎴锋姇璇夊瑙傝川閲忎笉杈炬爣銆�",
+ repairInfo: "涓庣敓浜х幇鍦烘牳鏌ワ紝纭鏉ユ枡鎼繍鍙婂寘瑁呯幆鑺傚瓨鍦ㄧ纰伴闄┿��",
+ result: "瀵归棶棰樻壒娆¢噸鏂拌繑宸ワ紝琛ュ彂鑹搧锛屽苟浼樺寲鍖呰闃叉姢鏂规銆�",
+ status: "1",
+ remark: "闇�璺熻釜鍚庣画鎵规鎶曡瘔鐜囧彉鍖栥��",
+ },
+ {
+ id: 3,
+ contractNo: "SC-2024-032",
+ productCode: "P-10110",
+ productModel: "XG-600C",
+ customerName: "瑗垮崡鏂拌兘婧愮鎶�鑲′唤",
+ contact: "鐜嬪伐 / 13800000003",
+ feedbackTime: "2024-11-28 09:15:00",
+ problemDesc: "鐜板満璋冭瘯鏃跺彂鐜版帴鍙d笉鍏煎锛岄渶瑕侀�傞厤瀹㈡埛鏃х増绯荤粺銆�",
+ repairInfo: "杩滅▼鎶�鏈敮鎸�+鐜板満宸ョ▼甯堣仈鍚堟帓鏌ワ紝鎻愪緵杩囨浮閫傞厤鏂规銆�",
+ result: "閫氳繃鏇存崲鎺ユ彃浠跺苟鍗囩骇鍥轰欢鐗堟湰瑙e喅銆�",
+ status: "0",
+ remark: "寤鸿涓嬫鍚堝悓鍓嶇疆娌熼�氭帴鍙h鏍笺��",
+ },
+]);
+
+const open = ref(false);
+const loading = ref(false);
+const showSearch = ref(true);
+const ids = ref([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(3);
+const title = ref("鏂板鍞悗璐ㄩ噺璁板綍");
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ productModel: null,
+ customerName: null,
+ feedbackRange: [],
+ status: null,
+ },
+ rules: {
+ contractNo: [
+ { required: true, message: "閿�鍞悎鍚屽彿涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ productCode: [
+ { required: true, message: "浜у搧缂栧彿涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ productModel: [
+ { required: true, message: "浜у搧鍨嬪彿涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ customerName: [
+ { required: true, message: "瀹㈡埛鍚嶇О涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ contact: [
+ { required: true, message: "鑱旂郴鏂瑰紡涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ feedbackTime: [
+ { required: true, message: "鍙嶉鏃堕棿涓嶈兘涓虹┖", trigger: "change" },
+ ],
+ problemDesc: [
+ { required: true, message: "瀹㈡埛鍙嶉闂涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ status: [
+ { required: true, message: "澶勭悊鐘舵�佷笉鑳戒负绌�", trigger: "change" },
+ ],
+ },
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+// 鏌ヨ鍒楄〃锛堜粎鍓嶇绛涢�夛紝涓嶈皟鎺ュ彛锛�
+function getList() {
+ loading.value = true;
+ const list = afterSalesList.value.filter((item) => {
+ if (
+ queryParams.value.productModel &&
+ !item.productModel
+ ?.toLowerCase()
+ .includes(queryParams.value.productModel.toLowerCase())
+ ) {
+ return false;
+ }
+ if (
+ queryParams.value.customerName &&
+ !item.customerName
+ ?.toLowerCase()
+ .includes(queryParams.value.customerName.toLowerCase())
+ ) {
+ return false;
+ }
+ if (queryParams.value.status && item.status !== queryParams.value.status) {
+ return false;
+ }
+ if (
+ Array.isArray(queryParams.value.feedbackRange) &&
+ queryParams.value.feedbackRange.length === 2
+ ) {
+ const [start, end] = queryParams.value.feedbackRange;
+ const dateStr = item.feedbackTime?.slice(0, 10);
+ if (dateStr < start || dateStr > end) {
+ return false;
+ }
+ }
+ return true;
+ });
+ total.value = list.length;
+ // 姝ゅ鏈仛鍒嗛〉锛屼粎妯℃嫙鍏ㄩ噺灞曠ず
+ afterSalesList.value = list;
+ loading.value = false;
+}
+
+// 鍙栨秷
+function cancel() {
+ open.value = false;
+ reset();
+}
+
+// 琛ㄥ崟閲嶇疆
+function reset() {
+ form.value = {
+ id: null,
+ contractNo: null,
+ productCode: null,
+ productModel: null,
+ customerName: null,
+ contact: null,
+ feedbackTime: null,
+ problemDesc: null,
+ repairInfo: null,
+ result: null,
+ status: "0",
+ remark: null,
+ };
+ proxy.resetForm("formRef");
+}
+
+// 鎼滅储
+function handleQuery() {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+
+// 閲嶇疆
+function resetQuery() {
+ proxy.resetForm("queryForm");
+ queryParams.value.feedbackRange = [];
+ handleQuery();
+}
+
+// 澶氶��
+function handleSelectionChange(selection) {
+ ids.value = selection.map((item) => item.id);
+ single.value = selection.length !== 1;
+ multiple.value = !selection.length;
+}
+
+// 鏂板
+function handleAdd() {
+ reset();
+ open.value = true;
+ title.value = "鏂板鍞悗璐ㄩ噺璁板綍";
+}
+
+// 淇敼
+function handleUpdate(row) {
+ reset();
+ const current = row || afterSalesList.value.find((item) => item.id === ids.value[0]);
+ if (current) {
+ form.value = { ...current };
+ open.value = true;
+ title.value = "淇敼鍞悗璐ㄩ噺璁板綍";
+ }
+}
+
+// 鎻愪氦
+function submitForm() {
+ proxy.$refs["formRef"].validate((valid) => {
+ if (valid) {
+ if (form.value.id != null) {
+ // 淇敼锛氭浛鎹㈡湰鍦版ā鎷熸暟鎹�
+ const index = afterSalesList.value.findIndex(
+ (item) => item.id === form.value.id
+ );
+ if (index !== -1) {
+ afterSalesList.value.splice(index, 1, { ...form.value });
+ }
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ } else {
+ // 鏂板锛氭彃鍏ユ湰鍦版ā鎷熸暟鎹�
+ const newId =
+ afterSalesList.value.length > 0
+ ? Math.max(...afterSalesList.value.map((i) => i.id)) + 1
+ : 1;
+ afterSalesList.value.push({ ...form.value, id: newId });
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ }
+ open.value = false;
+ getList();
+ }
+ });
+}
+
+// 鍒犻櫎
+function handleDelete(row) {
+ const deleteIds = row?.id ? [row.id] : ids.value;
+ if (!deleteIds || deleteIds.length === 0) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨瑕佸垹闄ょ殑璁板綍");
+ return;
+ }
+ ElMessageBox.confirm(
+ '鏄惁纭鍒犻櫎閫変腑鐨勫敭鍚庤川閲忚褰曪紵',
+ "璀﹀憡",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(() => {
+ // 鍓嶇鍒犻櫎鏈湴妯℃嫙鏁版嵁
+ afterSalesList.value = afterSalesList.value.filter(
+ (item) => !deleteIds.includes(item.id)
+ );
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+}
+
+// 瀵煎嚭
+function handleExport() {
+ proxy.$modal.msgSuccess("瀵煎嚭鍔熻兘涓烘紨绀哄姛鑳斤紝褰撳墠鏈鎺ュ疄闄呭鍑烘帴鍙�");
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+.mb8 {
+ margin-bottom: 8px;
+}
+.dialog-footer {
+ text-align: right;
+}
+</style>
+
diff --git a/src/views/qualityManagement/finalInspection/components/filesDia.vue b/src/views/qualityManagement/finalInspection/components/filesDia.vue
new file mode 100644
index 0000000..4517844
--- /dev/null
+++ b/src/views/qualityManagement/finalInspection/components/filesDia.vue
@@ -0,0 +1,182 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ style="display: inline;margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="paginationSearch"
+ height="500"
+ >
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {Search} from "@element-plus/icons-vue";
+import {
+ qualityInspectParamDel,
+ qualityInspectParamInfo,
+ qualityInspectParamUpdate
+} from "@/api/qualityManagement/qualityInspectParam.js";
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import {
+ qualityInspectFileAdd,
+ qualityInspectFileDel,
+ qualityInspectFileListPage
+} from "@/api/qualityManagement/qualityInspectFile.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row) => {
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ qualityInspectFileListPage({inspectId: currentId.value, ...page}).then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const fileRow = {}
+ fileRow.name = res.data.originalName
+ fileRow.url = res.data.tempPath
+ uploadFile(fileRow)
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+function uploadFile(file) {
+ file.inspectId = currentId.value;
+ qualityInspectFileAdd(file).then(res => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ qualityInspectFileDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/finalInspection/components/inspectionFormDia.vue b/src/views/qualityManagement/finalInspection/components/inspectionFormDia.vue
new file mode 100644
index 0000000..411856c
--- /dev/null
+++ b/src/views/qualityManagement/finalInspection/components/inspectionFormDia.vue
@@ -0,0 +1,139 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="濉啓妫�楠岃褰�"
+ width="70%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ height="600"
+ >
+ <template #slot="{ row }">
+ <el-input v-model="row.testValue" clearable/>
+ </template>
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {Search} from "@element-plus/icons-vue";
+import {
+ qualityInspectParamDel,
+ qualityInspectParamInfo,
+ qualityInspectParamUpdate
+} from "@/api/qualityManagement/qualityInspectParam.js";
+import {ElMessageBox} from "element-plus";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const currentId = ref('')
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "鎸囨爣",
+ prop: "parameterItem",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鏍囧噯鍊�",
+ prop: "standardValue",
+ },
+ {
+ label: "鍐呮帶鍊�",
+ prop: "controlValue",
+ },
+ {
+ label: "妫�楠屽��",
+ prop: "testValue",
+ dataType: 'slot',
+ slot: 'slot',
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === 'edit') {
+ currentId.value = row.id;
+ getList()
+ }
+}
+const getList = () => {
+ qualityInspectParamInfo(currentId.value).then(res => {
+ tableData.value = res.data;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ qualityInspectParamUpdate(tableData.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ qualityInspectParamDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/finalInspection/index.vue b/src/views/qualityManagement/finalInspection/index.vue
new file mode 100644
index 0000000..4a59fa4
--- /dev/null
+++ b/src/views/qualityManagement/finalInspection/index.vue
@@ -0,0 +1,522 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <el-form ref="searchFormRef"
+ :model="searchForm"
+ class="demo-form-inline">
+ <el-row :gutter="20">
+ <el-col :span="4">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productName">
+ <el-input v-model="searchForm.productName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉版悳绱�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="6">
+ <el-form-item label="妫�娴嬫棩鏈�"
+ prop="entryDate">
+ <el-date-picker v-model="searchForm.entryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="changeDaterange" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="閿�鍞崟鍙�"
+ prop="salesContractNo">
+ <el-input v-model="searchForm.salesContractNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ラ攢鍞崟鍙锋悳绱�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鐢熶骇宸ュ崟鍙�"
+ prop="workOrderNo">
+ <el-input v-model="searchForm.workOrderNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ョ敓浜у伐鍗曞彿鎼滅储"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <!-- 鎸夐挳 -->
+ <el-col :span="4">
+ <el-form-item>
+ <el-button type="primary"
+ @click="getList">
+ 鎼滅储
+ </el-button>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <div class="actions">
+ <el-button type="primary"
+ @click="openForm('add')">鏂板</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <InspectionFormDia ref="inspectionFormDia"
+ @close="handleQuery"></InspectionFormDia>
+ <FormDia ref="formDia"
+ @close="handleQuery"></FormDia>
+ <files-dia ref="filesDia"
+ @close="handleQuery"></files-dia>
+ <el-dialog v-model="dialogFormVisible"
+ title="缂栬緫妫�楠屽憳"
+ width="30%"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-form-item label="妫�楠屽憳锛�"
+ prop="checkName">
+ <el-select v-model="form.checkName"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.nickName"
+ :label="item.nickName"
+ :value="item.nickName" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+ } from "vue";
+ import InspectionFormDia from "@/views/qualityManagement/finalInspection/components/inspectionFormDia.vue";
+ import FormDia from "@/views/qualityManagement/finalInspection/components/formDia.vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ downloadQualityInspect,
+ qualityInspectDel,
+ qualityInspectListPage,
+ qualityInspectUpdate,
+ submitQualityInspect,
+ } from "@/api/qualityManagement/rawMaterialInspection.js";
+ import FilesDia from "@/views/qualityManagement/finalInspection/components/filesDia.vue";
+ import dayjs from "dayjs";
+ import { userListNoPage } from "@/api/system/user.js";
+ import useUserStore from "@/store/modules/user";
+
+ const data = reactive({
+ searchForm: {
+ productName: "",
+ salesContractNo: "",
+ workOrderNo: "",
+ entryDate: undefined, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ rules: {
+ checkName: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const { searchForm } = toRefs(data);
+ const tableColumn = ref([
+ {
+ label: "妫�娴嬫棩鏈�",
+ prop: "checkTime",
+ width: 120,
+ },
+ {
+ label: "閿�鍞崟鍙�",
+ prop: "salesContractNo",
+ width: 120,
+ },
+ {
+ label: "鐢熶骇宸ュ崟鍙�",
+ prop: "workOrderNo",
+ width: 120,
+ },
+ {
+ label: "妫�楠屽憳",
+ prop: "checkName",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鎬绘暟閲�",
+ prop: "quantity",
+ width: 100,
+ },
+ {
+ label: "鍚堟牸鏁伴噺",
+ prop: "qualifiedQuantity",
+ width: 100,
+ },
+ {
+ label: "涓嶅悎鏍兼暟閲�",
+ prop: "unqualifiedQuantity",
+ width: 100,
+ },
+ {
+ label: "鍚堟牸鐜�",
+ prop: "passRate",
+ width: 100,
+ dataType: "tag",
+ formatType: params => {
+ if (!params) return "";
+ const rate = parseFloat(params);
+ if (rate < 90) {
+ return "danger";
+ } else if (rate === 100) {
+ return "success";
+ } else {
+ return "warning";
+ }
+ },
+ },
+ {
+ label: "妫�娴嬪崟浣�",
+ prop: "checkCompany",
+ width: 120,
+ },
+ {
+ label: "妫�娴嬬粨鏋�",
+ prop: "checkResult",
+ dataType: "tag",
+ formatType: params => {
+ if (params == "涓嶅悎鏍�") {
+ return "danger";
+ } else if (params == "鍚堟牸") {
+ return "success";
+ } else {
+ return "danger";
+ }
+ },
+ },
+ {
+ label: "鎻愪氦鐘舵��",
+ prop: "inspectState",
+ formatData: params => {
+ if (params) {
+ return "宸叉彁浜�";
+ } else {
+ return "鏈彁浜�";
+ }
+ },
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ disabled: row => {
+ // 宸叉彁浜ゅ垯绂佺敤
+ if (row.inspectState == 1) return true;
+ // 濡傛灉妫�楠屽憳鏈夊�硷紝鍙湁褰撳墠鐧诲綍鐢ㄦ埛鑳界紪杈�
+ if (row.checkName) {
+ return row.checkName !== userStore.nickName;
+ }
+ return false;
+ },
+ },
+ {
+ name: "鏌ョ湅",
+ type: "text",
+ clickFun: row => {
+ openForm("view", row);
+ },
+ },
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ openFilesFormDia(row);
+ },
+ },
+ {
+ name: "鎻愪氦",
+ type: "text",
+ clickFun: row => {
+ submit(row.id);
+ },
+ disabled: row => {
+ // 宸叉彁浜ゅ垯绂佺敤
+ if (row.inspectState == 1) return true;
+ // 濡傛灉妫�楠屽憳鏈夊�硷紝鍙湁褰撳墠鐧诲綍鐢ㄦ埛鑳芥彁浜�
+ if (row.checkName) {
+ return row.checkName !== userStore.nickName;
+ }
+ return false;
+ },
+ },
+ {
+ name: "鍒嗛厤妫�楠屽憳",
+ type: "text",
+ clickFun: row => {
+ if (!row.checkName) {
+ open(row);
+ } else {
+ proxy.$modal.msgError("妫�楠屽憳宸插瓨鍦�");
+ }
+ },
+ disabled: row => {
+ return row.inspectState == 1 || row.checkName;
+ },
+ },
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: row => {
+ downLoadFile(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const currentRow = ref(null);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const formDia = ref();
+ const filesDia = ref();
+ const inspectionFormDia = ref();
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
+ const userList = ref([]);
+ const form = ref({
+ checkName: "",
+ });
+ const dialogFormVisible = ref(false);
+
+ const changeDaterange = value => {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ getList();
+ };
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined;
+ qualityInspectListPage({ ...params, inspectType: 2 })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records.map(item => {
+ const quantity = parseFloat(item.quantity);
+ const qualifiedQuantity = parseFloat(item.qualifiedQuantity);
+ let passRate = null;
+ if (!isNaN(quantity) && !isNaN(qualifiedQuantity) && quantity > 0) {
+ passRate = ((qualifiedQuantity / quantity) * 100).toFixed(2) + "%";
+ }
+ return {
+ ...item,
+ passRate: passRate,
+ };
+ });
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎵撳紑鏂板妫�楠屽脊妗�
+ const openInspectionForm = (type, row) => {
+ nextTick(() => {
+ inspectionFormDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎵撳紑闄勪欢寮规
+ const openFilesFormDia = (type, row) => {
+ nextTick(() => {
+ filesDia.value?.openDialog(type, row);
+ });
+ };
+
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ qualityInspectDel(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(
+ "/quality/qualityInspect/export",
+ { inspectType: 2 },
+ "鍑哄巶妫�楠�.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鎻愪环
+ const submit = async id => {
+ const res = await submitQualityInspect({ id: id });
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ getList();
+ }
+ };
+
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ };
+
+ const submitForm = () => {
+ if (currentRow.value) {
+ const data = {
+ ...form.value,
+ id: currentRow.value.id,
+ };
+ qualityInspectUpdate(data).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ };
+
+ const open = async row => {
+ let userLists = await userListNoPage();
+ userList.value = userLists.data;
+ currentRow.value = row;
+ dialogFormVisible.value = true;
+ };
+
+ const downLoadFile = row => {
+ downloadQualityInspect({ id: row.id }).then(blobData => {
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ });
+ const downloadUrl = window.URL.createObjectURL(blob);
+
+ const link = document.createElement("a");
+ link.href = downloadUrl;
+ link.download = "鍘熸潗鏂欐楠屾姤鍛�.docx";
+ document.body.appendChild(link);
+ link.click();
+
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(downloadUrl);
+ });
+ };
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .actions {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 10px;
+ }
+</style>
diff --git a/src/views/qualityManagement/metricBinding/index.vue b/src/views/qualityManagement/metricBinding/index.vue
new file mode 100644
index 0000000..1ac268a
--- /dev/null
+++ b/src/views/qualityManagement/metricBinding/index.vue
@@ -0,0 +1,536 @@
+<template>
+ <div class="app-container metric-binding">
+ <el-row :gutter="16" class="metric-binding-row">
+ <!-- 宸︿晶锛氭娴嬫爣鍑嗗垪琛� -->
+ <el-col :xs="24" :sm="24" :md="12" :lg="14" :xl="14" class="left-col">
+ <div class="panel left-panel">
+ <PIMTable
+ rowKey="id"
+ :column="standardColumns"
+ :tableData="standardTableData"
+ :page="page"
+ :isSelection="false"
+ :rowClassName="rowClassNameCenter"
+ :tableLoading="tableLoading"
+ :rowClick="handleTableRowClick"
+ @pagination="handlePagination"
+ :total="page.total"
+ >
+ <template #standardNoCell="{ row }">
+ <span class="clickable-link" @click="handleStandardRowClick(row)">
+ {{ row.standardNo }}
+ </span>
+ </template>
+
+ <!-- 琛ㄥご鎼滅储 -->
+ <template #standardNoHeader>
+ <el-input
+ v-model="searchForm.standardNo"
+ placeholder="鏍囧噯缂栧彿"
+ clearable
+ size="small"
+ @change="handleQuery"
+ @clear="handleQuery"
+ />
+ </template>
+ <template #standardNameHeader>
+ <el-input
+ v-model="searchForm.standardName"
+ placeholder="鏍囧噯鍚嶇О"
+ clearable
+ size="small"
+ @change="handleQuery"
+ @clear="handleQuery"
+ />
+ </template>
+ <template #inspectTypeHeader>
+ <el-select
+ v-model="searchForm.inspectType"
+ placeholder="绫诲埆"
+ clearable
+ size="small"
+ style="width: 120px"
+ @change="handleQuery"
+ @clear="handleQuery"
+ >
+ <el-option label="鍘熸潗鏂欐楠�" value="0" />
+ <el-option label="杩囩▼妫�楠�" value="1" />
+ <el-option label="鍑哄巶妫�楠�" value="2" />
+ </el-select>
+ </template>
+ <template #stateHeader>
+ <el-select
+ v-model="searchForm.state"
+ placeholder="鐘舵��"
+ clearable
+ size="small"
+ style="width: 110px"
+ @change="handleQuery"
+ @clear="handleQuery"
+ >
+ <el-option label="鑽夌" value="0" />
+ <el-option label="閫氳繃" value="1" />
+ <el-option label="鎾ら攢" value="2" />
+ </el-select>
+ </template>
+ </PIMTable>
+ </div>
+ </el-col>
+
+ <!-- 鍙充晶锛氱粦瀹氬垪琛� -->
+ <el-col :xs="24" :sm="24" :md="12" :lg="10" :xl="10" class="right-col">
+ <div class="panel right-panel">
+ <div class="right-header">
+ <div class="title">缁戝畾鍏崇郴</div>
+ <div class="desc" v-if="currentStandard">
+ 褰撳墠妫�娴嬫爣鍑嗙紪鍙凤細<span class="link-text">{{ currentStandard.standardNo }}</span>
+ </div>
+ <div class="desc" v-else>璇烽�夋嫨宸︿晶妫�娴嬫爣鍑�</div>
+ </div>
+
+ <div class="right-toolbar">
+ <el-button type="primary" :disabled="!currentStandard" @click="openBindingDialog">娣诲姞缁戝畾</el-button>
+ <el-button type="danger" plain :disabled="!currentStandard" @click="handleBatchUnbind">鍒犻櫎</el-button>
+ </div>
+
+ <el-table
+ v-loading="bindingLoading"
+ :data="bindingTableData"
+ border
+ :row-class-name="() => 'row-center'"
+ class="center-table"
+ style="width: 100%"
+ height="calc(100vh - 220px)"
+ @selection-change="handleBindingSelectionChange"
+ >
+ <el-table-column type="selection" width="48" align="center" />
+ <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" min-width="140" />
+ <el-table-column label="鎿嶄綔" width="120" fixed="right" align="center">
+ <template #default="{ row }">
+ <el-button link type="danger" size="small" @click="handleUnbind(row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 娣诲姞缁戝畾寮规 -->
+ <el-dialog
+ v-model="bindingDialogVisible"
+ title="娣诲姞缁戝畾"
+ width="520px"
+ @close="closeBindingDialog"
+ >
+ <el-form label-width="100px">
+ <el-form-item label="浜у搧">
+ <el-tree-select
+ v-model="selectedProductIds"
+ multiple
+ collapse-tags
+ collapse-tags-tooltip
+ placeholder="璇烽�夋嫨浜у搧锛堝彲澶氶�夛級"
+ clearable
+ check-strictly
+ :data="productOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="closeBindingDialog">鍙栨秷</el-button>
+ <el-button type="primary" @click="submitBinding">纭畾</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { Search } from '@element-plus/icons-vue'
+import { ref, reactive, toRefs, onMounted, getCurrentInstance } from 'vue'
+import { ElMessageBox } from 'element-plus'
+import PIMTable from '@/components/PIMTable/PIMTable.vue'
+import { productTreeList } from '@/api/basicData/product.js'
+import {
+ qualityTestStandardListPage
+} from '@/api/qualityManagement/metricMaintenance.js'
+import { productProcessListPage } from '@/api/basicData/productProcess.js'
+import {
+ qualityTestStandardBindingList,
+ qualityTestStandardBindingAdd,
+ qualityTestStandardBindingDel
+} from '@/api/qualityManagement/qualityTestStandardBinding.js'
+
+const { proxy } = getCurrentInstance()
+
+// 宸︿晶鏍囧噯鍒楄〃锛氭暣琛屽唴瀹瑰眳涓紙閰嶅悎鏍峰紡锛�
+const rowClassNameCenter = () => 'row-center'
+
+const data = reactive({
+ searchForm: {
+ standardNo: '',
+ standardName: '',
+ state: '',
+ inspectType: ''
+ }
+})
+const { searchForm } = toRefs(data)
+
+// 宸︿晶
+const standardTableData = ref([])
+const tableLoading = ref(false)
+const page = reactive({ current: 1, size: 10, total: 0 })
+
+// 宸ュ簭涓嬫媺锛堢敤浜庡垪琛ㄥ洖鏄撅級
+const processOptions = ref([])
+
+const getProcessList = async () => {
+ try {
+ const res = await productProcessListPage({ current: 1, size: 1000 })
+ if (res?.code === 200) {
+ const records = res?.data?.records || []
+ processOptions.value = records.map((item) => ({
+ label: item.processName || item.name || item.label,
+ value: item.id || item.processId || item.value
+ }))
+ }
+ } catch (error) {
+ console.error('鑾峰彇宸ュ簭鍒楄〃澶辫触:', error)
+ }
+}
+
+const standardColumns = ref([
+ { label: '鏍囧噯缂栧彿', prop: 'standardNo', dataType: 'slot', slot: 'standardNoCell', minWidth: 160, align: 'center', headerSlot: 'standardNoHeader' },
+ { label: '鏍囧噯鍚嶇О', prop: 'standardName', minWidth: 180, align: 'center', headerSlot: 'standardNameHeader' },
+ {
+ label: '绫诲埆',
+ prop: 'inspectType',
+ headerSlot: 'inspectTypeHeader',
+ align: 'center',
+ dataType: 'tag',
+ formatData: (val) => {
+ const map = { 0: '鍘熸潗鏂欐楠�', 1: '杩囩▼妫�楠�', 2: '鍑哄巶妫�楠�' }
+ return map[val] || val
+ }
+ },
+ {
+ label: '宸ュ簭',
+ prop: 'processId',
+ align: 'center',
+ dataType: 'tag',
+ formatData: (val) => {
+ const target = processOptions.value.find(
+ (item) => String(item.value) === String(val)
+ )
+ return target?.label || val
+ }
+ },
+ {
+ label: '澶囨敞',
+ prop: 'remark',
+ minWidth: 160,
+ align: 'center'
+ }
+ // {
+ // label: '鐘舵��',
+ // prop: 'state',
+ // headerSlot: 'stateHeader',
+ // dataType: 'tag',
+ // formatData: (val) => {
+ // const map = { 0: '鑽夌', 1: '閫氳繃', 2: '鎾ら攢' }
+ // return map[val] || val
+ // },
+ // formatType: (val) => {
+ // if (val == 1) return 'success'
+ // if (val == 2) return 'warning'
+ // return 'info'
+ // }
+ // }
+])
+
+const currentStandard = ref(null)
+
+// 鍙充晶缁戝畾
+const bindingTableData = ref([])
+const bindingLoading = ref(false)
+const bindingSelectedRows = ref([])
+const bindingDialogVisible = ref(false)
+
+// 浜у搧鏍戯紙鐢ㄤ簬缁戝畾閫夋嫨锛�
+const productOptions = ref([])
+const selectedProductIds = ref([])
+
+const getProductOptions = async () => {
+ // 閬垮厤閲嶅璇锋眰
+ if (productOptions.value?.length) return
+ const res = await productTreeList()
+ productOptions.value = convertIdToValue(Array.isArray(res) ? res : [])
+}
+
+function convertIdToValue(data) {
+ return (data || []).map((item) => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+const handleQuery = () => {
+ page.current = 1
+ getStandardList()
+}
+
+const handlePagination = (obj) => {
+ page.current = obj.page
+ page.size = obj.limit
+ getStandardList()
+}
+
+const getStandardList = () => {
+ tableLoading.value = true
+ qualityTestStandardListPage({
+ ...searchForm.value,
+ current: page.current,
+ size: page.size,
+ state: 1
+ })
+ .then((res) => {
+ const records = res?.data?.records || []
+ standardTableData.value = records
+ page.total = res?.data?.total || records.length
+ })
+ .finally(() => {
+ tableLoading.value = false
+ })
+}
+
+// 琛ㄦ牸琛岀偣鍑伙紝鍔犺浇鍙充晶缁戝畾鍒楄〃
+const handleTableRowClick = (row) => {
+ currentStandard.value = row
+ loadBindingList()
+}
+
+// 宸︿晶琛岀偣鍑伙紝鍔犺浇鍙充晶缁戝畾鍒楄〃锛堜繚鐣欑敤浜庢爣鍑嗙紪鍙峰垪鐨勭偣鍑伙級
+const handleStandardRowClick = (row) => {
+ currentStandard.value = row
+ loadBindingList()
+}
+
+const loadBindingList = () => {
+ if (!currentStandard.value?.id) {
+ bindingTableData.value = []
+ return
+ }
+ bindingLoading.value = true
+ qualityTestStandardBindingList({ testStandardId: currentStandard.value.id })
+ .then((res) => {
+ const base = res?.data || []
+ // 灏嗗綋鍓嶆爣鍑嗙殑宸ュ簭鍜屽娉ㄥ甫鍒扮粦瀹氬垪琛ㄤ腑灞曠ず
+ bindingTableData.value = base.map((item) => ({
+ ...item,
+ processId: currentStandard.value?.processId,
+ remark: currentStandard.value?.remark
+ }))
+ })
+ .finally(() => {
+ bindingLoading.value = false
+ })
+}
+
+const handleBindingSelectionChange = (selection) => {
+ bindingSelectedRows.value = selection
+}
+
+const openBindingDialog = () => {
+ if (!currentStandard.value?.id) return
+ selectedProductIds.value = []
+ getProductOptions()
+ bindingDialogVisible.value = true
+}
+
+const closeBindingDialog = () => {
+ bindingDialogVisible.value = false
+}
+
+const submitBinding = async () => {
+ const testStandardId = currentStandard.value?.id
+ if (!testStandardId) return
+ const ids = (selectedProductIds.value || []).filter(Boolean)
+ if (!ids.length) {
+ proxy.$message.warning('璇烽�夋嫨浜у搧')
+ return
+ }
+ const payload = ids.map((pid) => ({
+ productId: pid,
+ testStandardId
+ }))
+ await qualityTestStandardBindingAdd(payload)
+ proxy.$message.success('娣诲姞鎴愬姛')
+ bindingDialogVisible.value = false
+ loadBindingList()
+}
+
+const handleUnbind = async (row) => {
+ const id = row?.id ?? row?.qualityTestStandardBindingId
+ if (id == null || id === '') return
+ try {
+ await ElMessageBox.confirm('纭鍒犻櫎璇ョ粦瀹氾紵', '鎻愮ず', { type: 'warning' })
+ } catch {
+ return
+ }
+ try {
+ await qualityTestStandardBindingDel([id])
+ proxy.$message.success('鍒犻櫎鎴愬姛')
+ loadBindingList()
+ } catch (err) {
+ console.error('鍒犻櫎缁戝畾澶辫触:', err)
+ proxy.$message?.error(err?.message || '鍒犻櫎澶辫触')
+ }
+}
+
+const handleBatchUnbind = async () => {
+ if (!bindingSelectedRows.value.length) {
+ proxy.$message.warning('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const ids = bindingSelectedRows.value
+ .map((i) => i?.id ?? i?.qualityTestStandardBindingId)
+ .filter((id) => id != null && id !== '')
+ if (!ids.length) {
+ proxy.$message.warning('閫変腑鏁版嵁缂哄皯鏈夋晥 id')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�', '鍒犻櫎鎻愮ず', { type: 'warning' })
+ } catch {
+ return
+ }
+ try {
+ await qualityTestStandardBindingDel(ids)
+ proxy.$message.success('鍒犻櫎鎴愬姛')
+ loadBindingList()
+ } catch (err) {
+ console.error('鎵归噺鍒犻櫎缁戝畾澶辫触:', err)
+ proxy.$message?.error(err?.message || '鍒犻櫎澶辫触')
+ }
+}
+
+onMounted(() => {
+ getStandardList()
+ getProcessList()
+})
+</script>
+
+<style scoped>
+.metric-binding-row {
+ width: 100%;
+}
+
+.metric-binding-row .left-col,
+.metric-binding-row .right-col {
+ margin-bottom: 16px;
+}
+
+.metric-binding-row .panel {
+ background: #ffffff;
+ padding: 16px;
+ box-sizing: border-box;
+ height: 100%;
+ min-height: 400px;
+}
+
+.left-panel,
+.right-panel {
+ height: 100%;
+}
+
+.toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.toolbar-right {
+ flex-shrink: 0;
+}
+
+.right-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.right-header .title {
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.right-header .desc {
+ font-size: 13px;
+ color: #666;
+}
+
+.right-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.link-text {
+ color: #409eff;
+ cursor: default;
+}
+
+.clickable-link {
+ color: #409eff;
+ cursor: pointer;
+}
+
+.clickable-link:hover {
+ text-decoration: underline;
+}
+
+:deep(.row-center td) {
+ text-align: center !important;
+}
+
+/* el-table 琛ㄥご/鍐呭缁熶竴灞呬腑锛坮ow-class-name 涓嶄綔鐢ㄤ簬琛ㄥご锛� */
+:deep(.center-table .el-table__header-wrapper th .cell) {
+ text-align: center !important;
+}
+:deep(.center-table .el-table__body-wrapper td .cell) {
+ text-align: center !important;
+}
+
+/* PIMTable 琛ㄥご灞呬腑 */
+:deep(.lims-table .pim-table-header-cell) {
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+:deep(.lims-table .pim-table-header-title) {
+ text-align: center;
+ width: 100%;
+}
+
+:deep(.lims-table .pim-table-header-extra) {
+ width: 100%;
+ margin-top: 4px;
+}
+</style>
diff --git a/src/views/qualityManagement/metricMaintenance/ParamFormDialog.vue b/src/views/qualityManagement/metricMaintenance/ParamFormDialog.vue
new file mode 100644
index 0000000..4c958a0
--- /dev/null
+++ b/src/views/qualityManagement/metricMaintenance/ParamFormDialog.vue
@@ -0,0 +1,78 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ :title="computedTitle"
+ :operation-type="operationType"
+ width="520px"
+ @close="emit('close')"
+ @cancel="handleCancel"
+ @confirm="handleConfirm"
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px"
+ >
+ <el-form-item label="鍙傛暟椤�" prop="parameterItem">
+ <el-input v-model="form.parameterItem" placeholder="璇疯緭鍏ュ弬鏁伴」" />
+ </el-form-item>
+ <el-form-item label="鍗曚綅" prop="unit">
+ <el-input v-model="form.unit" placeholder="璇疯緭鍏ュ崟浣�" />
+ </el-form-item>
+ <el-form-item label="鏍囧噯鍊�" prop="standardValue">
+ <el-input v-model="form.standardValue" placeholder="璇疯緭鍏ユ爣鍑嗗��" />
+ </el-form-item>
+ <el-form-item label="鍐呮帶鍊�" prop="controlValue">
+ <el-input v-model="form.controlValue" placeholder="璇疯緭鍏ュ唴鎺у��" />
+ </el-form-item>
+ <el-form-item label="榛樿鍊�" prop="defaultValue">
+ <el-input v-model="form.defaultValue" placeholder="璇疯緭鍏ラ粯璁ゅ��" />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+
+const props = defineProps({
+ modelValue: { type: Boolean, default: false },
+ operationType: { type: String, default: 'add' }, // add | edit
+ form: { type: Object, required: true }
+})
+
+const emit = defineEmits(['update:modelValue', 'close', 'cancel', 'confirm'])
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const formRef = ref(null)
+
+const rules = {
+ parameterItem: [{ required: true, message: '璇疯緭鍏ュ弬鏁伴」', trigger: 'blur' }],
+ unit: [{ required: true, message: '璇疯緭鍏ュ崟浣�', trigger: 'blur' }]
+}
+
+const computedTitle = computed(() => (props.operationType === 'edit' ? '缂栬緫鏍囧噯鍙傛暟' : '鏂板鏍囧噯鍙傛暟'))
+
+const handleConfirm = () => {
+ formRef.value?.validate?.((valid) => {
+ if (valid) emit('confirm')
+ })
+}
+
+const handleCancel = () => {
+ emit('cancel')
+ dialogVisible.value = false
+}
+
+const resetFields = () => {
+ formRef.value?.resetFields?.()
+}
+
+defineExpose({ resetFields })
+</script>
diff --git a/src/views/qualityManagement/metricMaintenance/StandardFormDialog.vue b/src/views/qualityManagement/metricMaintenance/StandardFormDialog.vue
new file mode 100644
index 0000000..38d535a
--- /dev/null
+++ b/src/views/qualityManagement/metricMaintenance/StandardFormDialog.vue
@@ -0,0 +1,129 @@
+<template>
+ <FormDialog
+ v-model="dialogVisible"
+ :title="computedTitle"
+ :operation-type="operationType"
+ :width="width"
+ @close="emit('close')"
+ @cancel="handleCancel"
+ @confirm="handleConfirm"
+ >
+ <el-form
+ ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px"
+ >
+ <el-form-item label="鏍囧噯缂栧彿" prop="standardNo">
+ <el-input v-model="form.standardNo" placeholder="璇疯緭鍏ユ爣鍑嗙紪鍙�" />
+ </el-form-item>
+ <el-form-item label="鏍囧噯鍚嶇О" prop="standardName">
+ <el-input v-model="form.standardName" placeholder="璇疯緭鍏ユ爣鍑嗗悕绉�" />
+ </el-form-item>
+ <el-form-item label="绫诲埆" prop="inspectType">
+ <el-select v-model="form.inspectType" placeholder="璇烽�夋嫨绫诲埆" style="width: 100%">
+ <el-option label="鍘熸潗鏂欐楠�" value="0" />
+ <el-option label="杩囩▼妫�楠�" value="1" />
+ <el-option label="鍑哄巶妫�楠�" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="宸ュ簭" prop="processId">
+ <el-select v-model="form.processId" placeholder="璇烽�夋嫨宸ュ簭" style="width: 100%">
+ <el-option
+ v-for="item in processOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="state">
+ <el-select v-model="form.state" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%">
+ <el-option label="鑽夌" value="0" />
+ <el-option label="閫氳繃" value="1" />
+ <el-option label="鎾ら攢" value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�"
+ />
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Boolean,
+ default: false
+ },
+ operationType: {
+ type: String,
+ default: 'add'
+ },
+ form: {
+ type: Object,
+ required: true
+ },
+ rules: {
+ type: Object,
+ default: () => ({})
+ },
+ processOptions: {
+ type: Array,
+ default: () => []
+ },
+ width: {
+ type: String,
+ default: '500px'
+ }
+})
+
+const emit = defineEmits(['update:modelValue', 'close', 'cancel', 'confirm'])
+
+const dialogVisible = computed({
+ get: () => props.modelValue,
+ set: (val) => emit('update:modelValue', val)
+})
+
+const formRef = ref(null)
+
+const computedTitle = computed(() => {
+ if (props.operationType === 'edit') return '缂栬緫妫�娴嬫爣鍑�'
+ if (props.operationType === 'copy') return '澶嶅埗妫�娴嬫爣鍑�'
+ return '鏂板妫�娴嬫爣鍑�'
+})
+
+const handleConfirm = () => {
+ if (!formRef.value) {
+ emit('confirm')
+ return
+ }
+ formRef.value.validate((valid) => {
+ if (valid) {
+ emit('confirm')
+ }
+ })
+}
+
+const handleCancel = () => {
+ emit('cancel')
+ dialogVisible.value = false
+}
+
+const resetFields = () => {
+ formRef.value?.resetFields?.()
+}
+
+defineExpose({
+ resetFields
+})
+</script>
diff --git a/src/views/qualityManagement/metricMaintenance/index.vue b/src/views/qualityManagement/metricMaintenance/index.vue
new file mode 100644
index 0000000..deb3e5f
--- /dev/null
+++ b/src/views/qualityManagement/metricMaintenance/index.vue
@@ -0,0 +1,839 @@
+<template>
+ <div class="app-container metric-maintenance">
+ <el-row :gutter="16" class="metric-maintenance-row">
+ <!-- 宸︿晶锛氭娴嬫爣鍑嗗垪琛� -->
+ <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" class="left-col">
+ <div class="left-panel">
+ <div class="toolbar">
+ <div class="toolbar-left"></div>
+ <div class="toolbar-right">
+ <el-button type="primary" @click="openStandardDialog('add')">鏂板</el-button>
+ <el-button type="success" plain @click="handleBatchAudit(1)">鎵瑰噯</el-button>
+ <el-button type="warning" plain @click="handleBatchAudit(2)">鎾ら攢</el-button>
+ <el-button type="danger" plain @click="handleBatchDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="standardColumns"
+ :tableData="standardTableData"
+ :page="page"
+ :isSelection="true"
+ :tableLoading="tableLoading"
+ :rowClassName="rowClassNameCenter"
+ :rowClick="handleTableRowClick"
+ @selection-change="handleSelectionChange"
+ @pagination="handlePagination"
+ :total="page.total"
+ >
+ <template #standardNoCell="{ row }">
+ <span class="clickable-link" @click="handleStandardRowClick(row)">
+ {{ row.standardNo }}
+ </span>
+ </template>
+
+ <!-- 琛ㄥご鎼滅储鎻掓Ы -->
+ <template #standardNoHeader>
+ <el-input
+ v-model="searchForm.standardNo"
+ placeholder="鏍囧噯缂栧彿"
+ clearable
+ size="small"
+ @change="handleQuery"
+ @clear="handleQuery"
+ />
+ </template>
+ <template #standardNameHeader>
+ <el-input
+ v-model="searchForm.standardName"
+ placeholder="鏍囧噯鍚嶇О"
+ clearable
+ size="small"
+ @change="handleQuery"
+ @clear="handleQuery"
+ />
+ </template>
+ <template #inspectTypeHeader>
+ <el-select
+ v-model="searchForm.inspectType"
+ placeholder="绫诲埆"
+ clearable
+ size="small"
+ style="width: 120px"
+ @change="handleQuery"
+ @clear="handleQuery"
+ >
+ <el-option label="鍘熸潗鏂欐楠�" value="0" />
+ <el-option label="杩囩▼妫�楠�" value="1" />
+ <el-option label="鍑哄巶妫�楠�" value="2" />
+ </el-select>
+ </template>
+ <template #stateHeader>
+ <el-select
+ v-model="searchForm.state"
+ placeholder="鐘舵��"
+ clearable
+ size="small"
+ style="width: 110px"
+ @change="handleQuery"
+ @clear="handleQuery"
+ >
+ <el-option label="鑽夌" value="0" />
+ <el-option label="閫氳繃" value="1" />
+ <el-option label="鎾ら攢" value="2" />
+ </el-select>
+ </template>
+ </PIMTable>
+ </div>
+ </el-col>
+
+ <!-- 鍙充晶锛氭爣鍑嗗弬鏁板垪琛� -->
+ <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="12" class="right-col">
+ <div class="right-panel">
+ <div class="right-header">
+ <div class="title">鏍囧噯鍙傛暟</div>
+ <div class="desc" v-if="currentStandard">
+ 鎮ㄥ綋鍓嶉�夋嫨鐨勬娴嬫爣鍑嗙紪鍙锋槸锛�
+ <span class="link-text">{{ currentStandard.standardNo }}</span>
+ </div>
+ <div class="desc" v-else>璇峰厛鍦ㄥ乏渚ч�夋嫨涓�涓娴嬫爣鍑�</div>
+ </div>
+
+ <div class="right-toolbar">
+ <el-button type="primary" :disabled="!currentStandard || isStandardReadonly" @click="openParamDialog('add')">
+ 鏂板
+ </el-button>
+ <el-button type="danger" plain :disabled="!currentStandard || isStandardReadonly" @click="handleParamBatchDelete">
+ 鍒犻櫎
+ </el-button>
+ </div>
+
+ <el-table
+ v-loading="detailLoading"
+ :data="detailTableData"
+ border
+ :row-class-name="() => 'row-center'"
+ class="center-table"
+ style="width: 100%"
+ height="calc(100vh - 220px)"
+ @selection-change="handleParamSelectionChange"
+ >
+ <el-table-column type="selection" width="48" align="center" />
+ <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+ <el-table-column prop="parameterItem" label="鍙傛暟椤�" min-width="120" />
+ <el-table-column prop="unit" label="鍗曚綅" width="80" />
+ <el-table-column prop="standardValue" label="鏍囧噯鍊�" min-width="120" />
+ <el-table-column prop="controlValue" label="鍐呮帶鍊�" min-width="120" />
+ <el-table-column prop="defaultValue" label="榛樿鍊�" min-width="120" />
+ <el-table-column label="鎿嶄綔" width="140" fixed="right" align="center">
+ <template #default="{ row }">
+ <el-button link type="primary" :disabled="isStandardReadonly" @click="openParamDialog('edit', row)">
+ 缂栬緫
+ </el-button>
+ <el-button link type="danger" :disabled="isStandardReadonly" @click="handleParamDelete(row)">
+ 鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 鏂板 / 缂栬緫妫�娴嬫爣鍑� -->
+ <StandardFormDialog
+ ref="standardFormDialogRef"
+ v-model="standardDialogVisible"
+ :operation-type="standardOperationType"
+ :form="standardForm"
+ :rules="standardRules"
+ :process-options="processOptions"
+ @confirm="submitStandardForm"
+ @close="closeStandardDialog"
+ @cancel="closeStandardDialog"
+ />
+
+ <ParamFormDialog
+ ref="paramFormDialogRef"
+ v-model="paramDialogVisible"
+ :operation-type="paramOperationType"
+ :form="paramForm"
+ @confirm="submitParamForm"
+ @close="closeParamDialog"
+ @cancel="closeParamDialog"
+ />
+ </div>
+</template>
+
+<script setup>
+import { Search } from '@element-plus/icons-vue'
+import { ref, reactive, toRefs, onMounted, getCurrentInstance, computed } from 'vue'
+import { ElMessageBox } from 'element-plus'
+import {
+ qualityTestStandardListPage,
+ qualityTestStandardAdd,
+ qualityTestStandardUpdate,
+ qualityTestStandardDel,
+ qualityTestStandardCopyParam,
+ qualityTestStandardAudit,
+ qualityTestStandardParamList,
+ qualityTestStandardParamAdd,
+ qualityTestStandardParamUpdate,
+ qualityTestStandardParamDel
+} from '@/api/qualityManagement/metricMaintenance.js'
+import { productProcessListPage } from '@/api/basicData/productProcess.js'
+import StandardFormDialog from './StandardFormDialog.vue'
+import ParamFormDialog from './ParamFormDialog.vue'
+
+const { proxy } = getCurrentInstance()
+
+// 宸︿晶鏍囧噯鍒楄〃锛氭暣琛屽唴瀹瑰眳涓紙閰嶅悎鏍峰紡锛�
+const rowClassNameCenter = () => 'row-center'
+
+// 鏍囧噯鐘舵�佷负鈥滈�氳繃(1)鈥濇椂锛屽彸渚у弬鏁扮姝㈠鍒犳敼
+const isStandardReadonly = computed(() => {
+ const state = currentStandard.value?.state
+ return state === 1 || state === '1'
+})
+
+// 鎼滅储鏉′欢
+const data = reactive({
+ searchForm: {
+ standardNo: '',
+ standardName: '',
+ remark: '',
+ state: '',
+ inspectType: '',
+ processId: ''
+ },
+ standardForm: {
+ id: undefined,
+ standardNo: '',
+ standardName: '',
+ remark: '',
+ state: '0',
+ inspectType: '',
+ processId: ''
+ },
+ standardRules: {
+ standardNo: [{ required: true, message: '璇疯緭鍏ユ爣鍑嗙紪鍙�', trigger: 'blur' }],
+ standardName: [{ required: true, message: '璇疯緭鍏ユ爣鍑嗗悕绉�', trigger: 'blur' }],
+ inspectType: [{ required: true, message: '璇烽�夋嫨妫�娴嬬被鍨�', trigger: 'change' }],
+ processId: [{ required: false, message: '璇烽�夋嫨宸ュ簭', trigger: 'change' }]
+ }
+})
+
+const { searchForm, standardForm, standardRules } = toRefs(data)
+
+// 宸︿晶琛ㄦ牸
+const standardTableData = ref([])
+const selectedRows = ref([])
+const tableLoading = ref(false)
+const page = reactive({
+ current: 1,
+ size: 10,
+ total: 0
+})
+
+// 宸ュ簭涓嬫媺
+const processOptions = ref([])
+
+// 鑾峰彇宸ュ簭鍒楄〃
+const getProcessList = async () => {
+ try {
+ const res = await productProcessListPage({ current: 1, size: 1000 })
+ if (res?.code === 200) {
+ const records = res?.data?.records || []
+ processOptions.value = records.map(item => ({
+ label: item.processName || item.name || item.label,
+ value: item.id || item.processId || item.value
+ }))
+ }
+ } catch (error) {
+ console.error('鑾峰彇宸ュ簭鍒楄〃澶辫触:', error)
+ }
+}
+
+// 褰撳墠閫変腑鐨勬爣鍑� & 鍙充晶璇︽儏
+const currentStandard = ref(null)
+const detailTableData = ref([])
+const detailLoading = ref(false)
+const paramSelectedRows = ref([])
+const paramDialogVisible = ref(false)
+const paramOperationType = ref('add') // add | edit
+const paramFormDialogRef = ref(null)
+const paramForm = reactive({
+ id: undefined,
+ parameterItem: '',
+ unit: '',
+ standardValue: '',
+ controlValue: '',
+ defaultValue: ''
+})
+
+// 寮圭獥
+const standardDialogVisible = ref(false)
+const standardOperationType = ref('add') // add | edit | copy
+const standardFormDialogRef = ref(null)
+
+// 鍒楀畾涔�
+const standardColumns = ref([
+ {
+ label: '鏍囧噯缂栧彿',
+ prop: 'standardNo',
+ dataType: 'slot',
+ slot: 'standardNoCell',
+ minWidth: 160,
+ align: 'center',
+ headerSlot: 'standardNoHeader'
+ },
+ {
+ label: '鏍囧噯鍚嶇О',
+ prop: 'standardName',
+ minWidth: 180,
+ align: 'center',
+ headerSlot: 'standardNameHeader'
+ },
+ {
+ label: '绫诲埆',
+ prop: 'inspectType',
+ headerSlot: 'inspectTypeHeader',
+ align: 'center',
+ dataType: 'tag',
+ formatData: (val) => {
+ const map = {
+ 0: '鍘熸潗鏂欐楠�',
+ 1: '杩囩▼妫�楠�',
+ 2: '鍑哄巶妫�楠�'
+ }
+ return map[val] || val
+ }
+ },
+ {
+ label: '宸ュ簭',
+ prop: 'processId',
+ align: 'center',
+ dataType: 'tag',
+ formatData: (val) => {
+ const target = processOptions.value.find(
+ (item) => String(item.value) === String(val)
+ )
+ return target?.label || val
+ }
+ },
+ {
+ label: '鐘舵��',
+ prop: 'state',
+ headerSlot: 'stateHeader',
+ align: 'center',
+ dataType: 'tag',
+ formatData: (val) => {
+ const map = {
+ 0: '鑽夌',
+ 1: '閫氳繃',
+ 2: '鎾ら攢'
+ }
+ return map[val] || val
+ },
+ formatType: (val) => {
+ if (val === '1' || val === 1) return 'success'
+ if (val === '2' || val === 2) return 'warning'
+ return 'info'
+ }
+ },
+ {
+ label: '澶囨敞',
+ prop: 'remark',
+ minWidth: 160,
+ align: 'center'
+ },
+ {
+ dataType: 'action',
+ label: '鎿嶄綔',
+ align: 'center',
+ fixed: 'right',
+ width: 220,
+ operation: [
+ {
+ name: '缂栬緫',
+ type: 'text',
+ clickFun: (row) => {
+ openStandardDialog('edit', row)
+ }
+ },
+ {
+ name: '澶嶅埗',
+ type: 'text',
+ clickFun: async (row) => {
+ if (!row?.id) return
+ try {
+ await ElMessageBox.confirm('纭澶嶅埗璇ユ爣鍑嗗弬鏁帮紵', '鎻愮ず', { type: 'warning' })
+ } catch {
+ return
+ }
+ await qualityTestStandardCopyParam(row.id)
+ proxy.$message.success('澶嶅埗鎴愬姛')
+ getStandardList()
+ if (currentStandard.value?.id === row.id) {
+ loadDetail(row.id)
+ }
+ }
+ },
+ {
+ name: '鍒犻櫎',
+ type: 'text',
+ clickFun: (row) => {
+ handleDelete(row)
+ }
+ }
+ ]
+ }
+])
+
+// 鏌ヨ鍒楄〃
+const getStandardList = () => {
+ tableLoading.value = true
+ const params = {
+ ...searchForm.value,
+ current: page.current,
+ size: page.size
+ }
+ qualityTestStandardListPage(params)
+ .then((res) => {
+ const records = res?.data?.records || []
+ standardTableData.value = records
+ page.total = res?.data?.total || records.length
+ })
+ .finally(() => {
+ tableLoading.value = false
+ })
+}
+
+const handleQuery = () => {
+ page.current = 1
+ getStandardList()
+}
+
+const resetQuery = () => {
+ searchForm.value.standardNo = ''
+ searchForm.value.standardName = ''
+ searchForm.value.remark = ''
+ searchForm.value.state = ''
+ searchForm.value.inspectType = ''
+ searchForm.value.processId = ''
+ handleQuery()
+}
+
+const handlePagination = (obj) => {
+ page.current = obj.page
+ page.size = obj.limit
+ getStandardList()
+}
+
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection
+
+ if (!selection.length) {
+ currentStandard.value = null
+ detailTableData.value = []
+ return
+ }
+
+ const nextStandard = selection[selection.length - 1]
+ if (currentStandard.value?.id === nextStandard.id) return
+
+ currentStandard.value = nextStandard
+ loadDetail(nextStandard.id)
+}
+
+// 鎵归噺瀹℃牳锛氱姸鎬� 1=鎵瑰噯锛�2=鎾ら攢
+const handleBatchAudit = async (state) => {
+ if (!selectedRows.value.length) {
+ proxy.$message.warning('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const text = state === 1 ? '鎵瑰噯' : '鎾ら攢'
+ const payload = selectedRows.value
+ .filter(i => i?.id)
+ .map((item) => ({ id: item.id, state }))
+
+ if (!payload.length) {
+ proxy.$message.warning('璇烽�夋嫨鏈夋晥鏁版嵁')
+ return
+ }
+
+ try {
+ await ElMessageBox.confirm(`纭${text}閫変腑鐨勬爣鍑嗭紵`, '鎻愮ず', { type: 'warning' })
+ } catch {
+ return
+ }
+ await qualityTestStandardAudit(payload)
+ proxy.$message.success(`${text}鎴愬姛`)
+ getStandardList()
+}
+
+// 琛ㄦ牸琛岀偣鍑伙紝鍔犺浇鍙充晶鍙傛暟
+const handleTableRowClick = (row) => {
+ currentStandard.value = row
+ loadDetail(row.id)
+}
+
+// 宸︿晶琛岀偣鍑伙紝鍔犺浇鍙充晶鍙傛暟锛堜繚鐣欑敤浜庢爣鍑嗙紪鍙峰垪鐨勭偣鍑伙級
+const handleStandardRowClick = (row) => {
+ currentStandard.value = row
+ loadDetail(row.id)
+}
+
+const loadDetail = (standardId) => {
+ if (!standardId) {
+ detailTableData.value = []
+ return
+ }
+ detailLoading.value = true
+ qualityTestStandardParamList({ testStandardId: standardId }).then((res) => {
+ detailTableData.value = res?.data || []
+ })
+ .finally(() => {
+ detailLoading.value = false
+ })
+}
+
+const handleParamSelectionChange = (selection) => {
+ paramSelectedRows.value = selection
+}
+
+const openParamDialog = (type, row) => {
+ if (!currentStandard.value?.id) return
+ if (isStandardReadonly.value) {
+ proxy.$message.warning('璇ユ爣鍑嗗凡閫氳繃锛屽弬鏁颁笉鍙紪杈�')
+ return
+ }
+ paramOperationType.value = type
+ if (type === 'add') {
+ Object.assign(paramForm, {
+ id: undefined,
+ parameterItem: '',
+ unit: '',
+ standardValue: '',
+ controlValue: '',
+ defaultValue: ''
+ })
+ } else if (type === 'edit' && row) {
+ Object.assign(paramForm, row)
+ }
+ paramDialogVisible.value = true
+}
+
+const closeParamDialog = () => {
+ paramDialogVisible.value = false
+ paramFormDialogRef.value?.resetFields?.()
+}
+
+const submitParamForm = async () => {
+ const testStandardId = currentStandard.value?.id
+ if (!testStandardId) return
+ if (isStandardReadonly.value) {
+ proxy.$message.warning('璇ユ爣鍑嗗凡閫氳繃锛屽弬鏁颁笉鍙紪杈�')
+ return
+ }
+ const payload = { ...paramForm, testStandardId }
+ if (paramOperationType.value === 'edit') {
+ await qualityTestStandardParamUpdate(payload)
+ proxy.$message.success('鎻愪氦鎴愬姛')
+ } else {
+ await qualityTestStandardParamAdd(payload)
+ proxy.$message.success('鎻愪氦鎴愬姛')
+ }
+ closeParamDialog()
+ loadDetail(testStandardId)
+}
+
+const handleParamDelete = async (row) => {
+ if (!row?.id) return
+ if (isStandardReadonly.value) {
+ proxy.$message.warning('璇ユ爣鍑嗗凡閫氳繃锛屽弬鏁颁笉鍙紪杈�')
+ return
+ }
+ try {
+ await ElMessageBox.confirm('纭鍒犻櫎璇ュ弬鏁帮紵', '鎻愮ず', { type: 'warning' })
+ } catch {
+ return
+ }
+ await qualityTestStandardParamDel([row.id])
+ proxy.$message.success('鍒犻櫎鎴愬姛')
+ loadDetail(currentStandard.value?.id)
+}
+
+const handleParamBatchDelete = async () => {
+ if (isStandardReadonly.value) {
+ proxy.$message.warning('璇ユ爣鍑嗗凡閫氳繃锛屽弬鏁颁笉鍙紪杈�')
+ return
+ }
+ if (!paramSelectedRows.value.length) {
+ proxy.$message.warning('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const ids = paramSelectedRows.value.map((i) => i.id)
+ try {
+ await ElMessageBox.confirm('閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�', '鍒犻櫎鎻愮ず', { type: 'warning' })
+ } catch {
+ return
+ }
+ await qualityTestStandardParamDel(ids)
+ proxy.$message.success('鍒犻櫎鎴愬姛')
+ loadDetail(currentStandard.value?.id)
+}
+
+// 鏂板 / 缂栬緫 / 澶嶅埗
+const openStandardDialog = (type, row) => {
+ standardOperationType.value = type
+ if (type === 'add') {
+ Object.assign(standardForm.value, {
+ id: undefined,
+ standardNo: '',
+ standardName: '',
+ remark: '',
+ state: '0',
+ inspectType: '',
+ processId: ''
+ })
+ } else if (type === 'edit' && row) {
+ Object.assign(standardForm.value, {
+ ...row,
+ // 纭繚 inspectType 鍜� state 杞崲涓哄瓧绗︿覆锛屼互鍖归厤 el-select 鐨� value 绫诲瀷
+ inspectType: row.inspectType !== null && row.inspectType !== undefined ? String(row.inspectType) : '',
+ state: row.state !== null && row.state !== undefined ? String(row.state) : '0',
+ // 纭繚 processId 杞崲涓哄瓧绗︿覆鎴栨暟瀛楋紙鏍规嵁瀹為檯闇�瑕侊級
+ processId: row.processId !== null && row.processId !== undefined ? row.processId : ''
+ })
+ } else if (type === 'copy' && row) {
+ const { id, ...rest } = row
+ Object.assign(standardForm.value, {
+ ...rest,
+ id: undefined,
+ standardNo: '',
+ state: '0',
+ // 纭繚 inspectType 杞崲涓哄瓧绗︿覆
+ inspectType: rest.inspectType !== null && rest.inspectType !== undefined ? String(rest.inspectType) : ''
+ })
+ }
+ standardDialogVisible.value = true
+}
+
+const closeStandardDialog = () => {
+ standardDialogVisible.value = false
+ standardFormDialogRef.value?.resetFields?.()
+}
+
+const submitStandardForm = () => {
+ const payload = { ...standardForm.value }
+ const isEdit = standardOperationType.value === 'edit'
+ if (isEdit) {
+ qualityTestStandardUpdate(payload).then(() => {
+ proxy.$message.success('鎻愪氦鎴愬姛')
+ standardDialogVisible.value = false
+ getStandardList()
+ })
+ } else {
+ qualityTestStandardAdd(payload).then(() => {
+ proxy.$message.success('鎻愪氦鎴愬姛')
+ standardDialogVisible.value = false
+ getStandardList()
+ })
+ }
+}
+
+// 鍒犻櫎锛堝崟鏉★級
+const handleDelete = (row) => {
+ const ids = [row.id]
+ ElMessageBox.confirm('閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�', '鍒犻櫎鎻愮ず', {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ .then(() => {
+ tableLoading.value = true
+ qualityTestStandardDel(ids)
+ .then(() => {
+ proxy.$message.success('鍒犻櫎鎴愬姛')
+ getStandardList()
+ if (currentStandard.value && currentStandard.value.id === row.id) {
+ currentStandard.value = null
+ detailTableData.value = []
+ }
+ })
+ .finally(() => {
+ tableLoading.value = false
+ })
+ })
+ .catch(() => {
+ proxy.$modal?.msg('宸插彇娑�')
+ })
+}
+
+// 鎵归噺鍒犻櫎
+const handleBatchDelete = () => {
+ if (!selectedRows.value.length) {
+ proxy.$message.warning('璇烽�夋嫨鏁版嵁')
+ return
+ }
+ const ids = selectedRows.value.map((item) => item.id)
+ ElMessageBox.confirm('閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�', '鍒犻櫎鎻愮ず', {
+ confirmButtonText: '纭',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ })
+ .then(() => {
+ tableLoading.value = true
+ qualityTestStandardDel(ids)
+ .then(() => {
+ proxy.$message.success('鍒犻櫎鎴愬姛')
+ getStandardList()
+ if (currentStandard.value && ids.includes(currentStandard.value.id)) {
+ currentStandard.value = null
+ detailTableData.value = []
+ }
+ })
+ .finally(() => {
+ tableLoading.value = false
+ })
+ })
+ .catch(() => {
+ proxy.$modal?.msg('宸插彇娑�')
+ })
+}
+
+onMounted(() => {
+ getProcessList()
+ getStandardList()
+})
+</script>
+
+<style scoped>
+.metric-maintenance-row {
+ width: 100%;
+}
+
+.metric-maintenance-row .left-col,
+.metric-maintenance-row .right-col {
+ margin-bottom: 16px;
+}
+
+.left-panel,
+.right-panel {
+ min-width: 0;
+ background: #ffffff;
+ padding: 16px;
+ box-sizing: border-box;
+ overflow: hidden;
+ height: 100%;
+ min-height: 400px;
+}
+
+@media (max-width: 768px) {
+ .left-panel,
+ .right-panel {
+ padding: 12px;
+ }
+}
+
+.toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.toolbar-left {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 4px;
+}
+
+.toolbar-right {
+ flex-shrink: 0;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.search-label {
+ margin: 0 4px 0 12px;
+}
+
+.search-label:first-of-type {
+ margin-left: 0;
+}
+
+.right-header {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.right-header .title {
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.right-header .desc {
+ font-size: 13px;
+ color: #666;
+}
+
+.right-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ margin-bottom: 10px;
+}
+
+.link-text {
+ color: #409eff;
+ cursor: default;
+}
+
+.clickable-link {
+ color: #409eff;
+ cursor: pointer;
+}
+
+.clickable-link:hover {
+ text-decoration: underline;
+}
+
+:deep(.row-center td) {
+ text-align: center !important;
+}
+
+/* el-table 琛ㄥご/鍐呭缁熶竴灞呬腑锛坮ow-class-name 涓嶄綔鐢ㄤ簬琛ㄥご锛� */
+:deep(.center-table .el-table__header-wrapper th .cell) {
+ text-align: center !important;
+}
+:deep(.center-table .el-table__body-wrapper td .cell) {
+ text-align: center !important;
+}
+
+/* PIMTable 琛ㄥご灞呬腑 */
+:deep(.lims-table .pim-table-header-cell) {
+ text-align: center;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+:deep(.lims-table .pim-table-header-title) {
+ text-align: center;
+ width: 100%;
+}
+
+:deep(.lims-table .pim-table-header-extra) {
+ width: 100%;
+ margin-top: 4px;
+}
+</style>
diff --git a/src/views/qualityManagement/metricMaintenance/index0.vue b/src/views/qualityManagement/metricMaintenance/index0.vue
new file mode 100644
index 0000000..016a4c1
--- /dev/null
+++ b/src/views/qualityManagement/metricMaintenance/index0.vue
@@ -0,0 +1,415 @@
+<template>
+ <div class="app-container product-view">
+ <div class="left">
+ <div>
+ <el-input
+ v-model="search"
+ style="width: 210px"
+ placeholder="杈撳叆鍏抽敭瀛楄繘琛屾悳绱�"
+ @change="searchFilter"
+ @clear="searchFilter"
+ clearable
+ prefix-icon="Search"
+ />
+ </div>
+ <div ref="containerRef">
+ <el-tree
+ ref="tree"
+ v-loading="treeLoad"
+ :data="list"
+ @node-click="handleNodeClick"
+ :expand-on-click-node="false"
+ default-expand-all
+ :default-expanded-keys="expandedKeys"
+ :draggable="true"
+ :filter-node-method="filterNode"
+ :props="{ children: 'children', label: 'label' }"
+ highlight-current
+ node-key="id"
+ style="
+ height: calc(100vh - 190px);
+ overflow-y: scroll;
+ scrollbar-width: none;
+ "
+ >
+ <template #default="{ node, data }">
+ <div class="custom-tree-node">
+ <span class="tree-node-content">
+ <el-icon class="orange-icon">
+ <component :is="data.children && data.children.length > 0
+ ? node.expanded ? 'FolderOpened' : 'Folder' : 'Tickets'" />
+ </el-icon>
+ {{ data.label }}
+ </span>
+ </div>
+ </template>
+ </el-tree>
+ </div>
+ </div>
+ <div class="right">
+ <div style="margin-bottom: 10px">
+ <el-button type="primary" @click="openModelDia('add')">
+ 鏂板妫�娴嬫寚鏍�
+ </el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button
+ type="danger"
+ @click="handleDelete"
+ style="margin-left: 10px"
+ plain
+ >
+ 鍒犻櫎
+ </el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <el-dialog
+ v-model="modelDia"
+ title="妫�娴嬫寚鏍�"
+ width="400px"
+ @close="closeModelDia"
+ >
+ <el-form
+ :model="modelForm"
+ label-width="140px"
+ label-position="top"
+ :rules="modelRules"
+ ref="modelFormRef"
+ >
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鎸囨爣锛�" prop="parameterItem">
+ <el-input
+ v-model="modelForm.parameterItem"
+ placeholder="璇疯緭鍏ユ寚鏍�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input
+ v-model="modelForm.unit"
+ placeholder="璇疯緭鍏ュ崟浣�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鏍囧噯鍊硷細" prop="standardValue">
+ <el-input
+ v-model="modelForm.standardValue"
+ placeholder="璇疯緭鍏ユ爣鍑嗗��"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鍐呮帶鍊硷細" prop="controlValue">
+ <el-input
+ v-model="modelForm.controlValue"
+ placeholder="璇疯緭鍏ュ唴鎺у��"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitModelForm">纭</el-button>
+ <el-button @click="closeModelDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {addOrEditProductModel, delProductModel, modelListPage, productTreeList} from "@/api/basicData/product.js";
+import ImportExcel from "@/views/basicData/product/ImportExcel/index.vue";
+import {ElMessageBox} from "element-plus";
+import {
+ qualityTestStandardAdd, qualityTestStandardDel,
+ qualityTestStandardListPage,
+ qualityTestStandardUpdate
+} from "@/api/qualityManagement/metricMaintenance.js";
+const { proxy } = getCurrentInstance();
+// 鏍�
+const search = ref("");
+const treeLoad = ref(false);
+const list = ref([]);
+const expandedKeys = ref([]);
+const currentId = ref("");
+const currentParentId = ref("");
+// 鎸囨爣琛ㄦ牸
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 10,
+});
+const tableColumn = ref([
+ {
+ label: "鎸囨爣",
+ prop: "parameterItem",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鏍囧噯鍊�",
+ prop: "standardValue",
+ },
+ {
+ label: "鍐呮帶鍊�",
+ prop: "controlValue",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: (row) => {
+ openModelDia("edit", row);
+ },
+ },
+ ],
+ },
+]);
+const selectedRows = ref([]);
+// 鎸囨爣寮规
+const modelDia = ref(false);
+const modelOperationType = ref("");
+const data = reactive({
+ modelForm: {
+ parameterItem: "",
+ unit: "",
+ standardValue: "",
+ controlValue: "",
+ },
+ modelRules: {
+ parameterItem: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ standardValue: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ controlValue: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ },
+});
+const { modelForm, modelRules } = toRefs(data);
+
+// 鏌ヨ浜у搧鏍�
+const getProductTreeList = () => {
+ treeLoad.value = true;
+ productTreeList().then((res) => {
+ list.value = res;
+ list.value.forEach((a) => {
+ expandedKeys.value.push(a.label);
+ });
+ treeLoad.value = false;
+ }).catch((err) => {
+ treeLoad.value = false;
+ });
+};
+// 杩囨护浜у搧鏍�
+const searchFilter = () => {
+ proxy.$refs.tree.filter(search.value);
+};
+// 閫夋嫨浜у搧
+const handleNodeClick = (val, node, el) => {
+ // 鍙湁鍙跺瓙鑺傜偣鎵嶆墽琛屼互涓嬮�昏緫
+ currentId.value = val.id;
+ currentParentId.value = val.parentId;
+ getModelList();
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+// 鏌ヨ鎸囨爣鏁版嵁
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getModelList();
+};
+const getModelList = () => {
+ tableLoading.value = true;
+ qualityTestStandardListPage({
+ productId: currentId.value,
+ current: page.current,
+ size: page.size,
+ }).then((res) => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ tableLoading.value = false;
+ });
+};
+// 璋冪敤tree杩囨护鏂规硶 涓枃鑻辫繃婊�
+const filterNode = (value, data, node) => {
+ if (!value) {
+ //濡傛灉鏁版嵁涓虹┖锛屽垯杩斿洖true,鏄剧ず鎵�鏈夌殑鏁版嵁椤�
+ return true;
+ }
+ // 鏌ヨ鍒楄〃鏄惁鏈夊尮閰嶆暟鎹紝灏嗗�煎皬鍐欙紝鍖归厤鑻辨枃鏁版嵁
+ let val = value.toLowerCase();
+ return chooseNode(val, data, node); // 璋冪敤杩囨护浜屽眰鏂规硶
+};
+// 杩囨护鐖惰妭鐐� / 瀛愯妭鐐� (濡傛灉杈撳叆鐨勫弬鏁版槸鐖惰妭鐐逛笖鑳藉尮閰嶏紝鍒欒繑鍥炶鑺傜偣浠ュ強鍏朵笅鐨勬墍鏈夊瓙鑺傜偣锛涘鏋滃弬鏁版槸瀛愯妭鐐癸紝鍒欒繑鍥炶鑺傜偣鐨勭埗鑺傜偣銆俷ame鏄腑鏂囧瓧绗︼紝enName鏄嫳鏂囧瓧绗�.
+const chooseNode = (value, data, node) => {
+ if (data.label.indexOf(value) !== -1) {
+ return true;
+ }
+ const level = node.level;
+ // 濡傛灉浼犲叆鐨勮妭鐐规湰韬氨鏄竴绾ц妭鐐瑰氨涓嶇敤鏍¢獙浜�
+ if (level === 1) {
+ return false;
+ }
+ // 鍏堝彇褰撳墠鑺傜偣鐨勭埗鑺傜偣
+ let parentData = node.parent;
+ // 閬嶅巻褰撳墠鑺傜偣鐨勭埗鑺傜偣
+ let index = 0;
+ while (index < level - 1) {
+ // 濡傛灉鍖归厤鍒扮洿鎺ヨ繑鍥烇紝姝ゅname鍊兼槸涓枃瀛楃锛宔nName鏄嫳鏂囧瓧绗︺�傚垽鏂尮閰嶄腑鑻辨枃杩囨护
+ if (parentData.data.label.indexOf(value) !== -1) {
+ return true;
+ }
+ // 鍚﹀垯鐨勮瘽鍐嶅線涓婁竴灞傚仛鍖归厤
+ parentData = parentData.parent;
+ index++;
+ }
+ // 娌″尮閰嶅埌杩斿洖false
+ return false;
+};
+// 鎵撳紑鎸囨爣寮规
+const openModelDia = (type, data) => {
+ modelOperationType.value = type;
+ modelDia.value = true;
+ modelForm.value.model = "";
+ modelForm.value.model = "";
+ modelForm.value.id = "";
+ if (type === "edit") {
+ modelForm.value = { ...data };
+ }
+};
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ proxy.download("/quality/qualityTestStandard/export", {}, "妫�娴嬫寚鏍�.xlsx");
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 鍒犻櫎鎸囨爣
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ tableLoading.value = true;
+ qualityTestStandardDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getModelList();
+ }).finally(() => {
+ tableLoading.value = false;
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 鎻愪氦瑙勬牸鍨嬪彿淇敼
+const submitModelForm = () => {
+ proxy.$refs.modelFormRef.validate((valid) => {
+ if (valid) {
+ modelForm.value.productId = Number(currentId.value);
+ if(modelOperationType.value === 'add') {
+ qualityTestStandardAdd(modelForm.value).then((res) => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeModelDia();
+ getModelList();
+ });
+ } else {
+ qualityTestStandardUpdate(modelForm.value).then((res) => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeModelDia();
+ getModelList();
+ });
+ }
+ }
+ });
+};
+// 鍏抽棴鍨嬪彿寮规
+const closeModelDia = () => {
+ proxy.$refs.modelFormRef.resetFields();
+ modelDia.value = false;
+};
+getProductTreeList();
+</script>
+
+<style scoped>
+.product-view {
+ display: flex;
+}
+.left {
+ width: 380px;
+ padding: 16px;
+ background: #ffffff;
+}
+.right {
+ width: calc(100% - 380px);
+ padding: 16px;
+ margin-left: 20px;
+ background: #ffffff;
+}
+.custom-tree-node {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ padding-right: 8px;
+}
+.tree-node-content {
+ display: flex;
+ align-items: center; /* 鍨傜洿灞呬腑 */
+ height: 100%;
+}
+.orange-icon {
+ color: orange;
+ font-size: 18px;
+ margin-right: 8px; /* 鍥炬爣涓庢枃瀛椾箣闂村姞鐐归棿璺� */
+}
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/nearExpiryReturn/index.vue b/src/views/qualityManagement/nearExpiryReturn/index.vue
new file mode 100644
index 0000000..26dc747
--- /dev/null
+++ b/src/views/qualityManagement/nearExpiryReturn/index.vue
@@ -0,0 +1,445 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams"
+ ref="queryForm"
+ :inline="true"
+ v-show="showSearch"
+ label-width="68px">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productName">
+ <el-input v-model="queryParams.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�"
+ clearable
+ @keyup.enter.native="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鎵规鍙�"
+ prop="batchNumber">
+ <el-input v-model="queryParams.batchNumber"
+ placeholder="璇疯緭鍏ユ壒娆″彿"
+ clearable
+ @keyup.enter.native="handleQuery" />
+ </el-form-item>
+ <el-form-item label="閫�鍥炴棩鏈�"
+ prop="returnDate">
+ <el-date-picker clearable
+ v-model="queryParams.returnDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨閫�鍥炴棩鏈�">
+ </el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ icon="Search"
+ @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh"
+ @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row :gutter="10"
+ class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd">鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate">淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete">鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning"
+ plain
+ icon="Download"
+ @click="handleExport">瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch"
+ @queryTable="getList"></right-toolbar>
+ </el-row>
+ <el-table v-loading="loading"
+ :data="nearExpiryReturnList"
+ @selection-change="handleSelectionChange">
+ <el-table-column type="selection"
+ width="55"
+ align="center" />
+ <el-table-column label="搴忓彿"
+ type="index"
+ width="50"
+ align="center" />
+ <el-table-column label="浜у搧鍚嶇О"
+ prop="productName" />
+ <el-table-column label="瑙勬牸鍨嬪彿"
+ prop="productSpec" />
+ <el-table-column label="鎵规鍙�"
+ prop="batchNumber" />
+ <el-table-column label="鐢熶骇鏃ユ湡"
+ prop="productionDate"
+ align="center">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.productionDate, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒版湡鏃ユ湡"
+ prop="expiryDate"
+ align="center">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.expiryDate, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="閫�鍥炴暟閲�"
+ prop="returnQuantity" />
+ <el-table-column label="閫�鍥炲師鍥�"
+ prop="returnReason" />
+ <el-table-column label="閫�鍥炴棩鏈�"
+ prop="returnDate"
+ align="center">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.returnDate, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="澶勭悊鐘舵��"
+ prop="status"
+ align="center">
+ <template #default="scope">
+ <dict-tag :options="statusOptions"
+ :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔"
+ align="center"
+ class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button size="mini"
+ type="text"
+ icon="Edit"
+ @click="handleUpdate(scope.row)">淇敼</el-button>
+ <el-button size="mini"
+ type="text"
+ icon="Delete"
+ @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total>0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList" />
+ <!-- 娣诲姞鎴栦慨鏀逛复鏈熼��鍥炲彴璐﹀璇濇 -->
+ <el-dialog :title="title"
+ v-model="open"
+ width="800px"
+ append-to-body>
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="100px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍚嶇О"
+ prop="productName">
+ <el-input v-model="form.productName"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿"
+ prop="productSpec">
+ <el-input v-model="form.productSpec"
+ placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎵规鍙�"
+ prop="batchNumber">
+ <el-input v-model="form.batchNumber"
+ placeholder="璇疯緭鍏ユ壒娆″彿" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閫�鍥炴暟閲�"
+ prop="returnQuantity">
+ <el-input-number v-model="form.returnQuantity"
+ controls-position="right"
+ :min="1" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢熶骇鏃ユ湡"
+ prop="productionDate">
+ <el-date-picker clearable
+ v-model="form.productionDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨鐢熶骇鏃ユ湡">
+ </el-date-picker>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍒版湡鏃ユ湡"
+ prop="expiryDate">
+ <el-date-picker clearable
+ v-model="form.expiryDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨鍒版湡鏃ユ湡">
+ </el-date-picker>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="閫�鍥炴棩鏈�"
+ prop="returnDate">
+ <el-date-picker clearable
+ v-model="form.returnDate"
+ type="date"
+ value-format="YYYY-MM-DD"
+ placeholder="璇烽�夋嫨閫�鍥炴棩鏈�">
+ </el-date-picker>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊鐘舵��"
+ prop="status">
+ <el-select v-model="form.status"
+ placeholder="璇烽�夋嫨澶勭悊鐘舵��">
+ <el-option v-for="dict in statusOptions"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="閫�鍥炲師鍥�"
+ prop="returnReason">
+ <el-input v-model="form.returnReason"
+ type="textarea"
+ placeholder="璇疯緭鍏ラ��鍥炲師鍥�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="NearExpiryReturn">
+ import { ref, reactive, onMounted } from "vue";
+ import { ElMessageBox } from "element-plus";
+ // API鎺ュ彛宸茬Щ闄わ紝涓嶅啀璋冪敤鍚庣鎺ュ彛
+
+ const { proxy } = getCurrentInstance();
+ const { parseTime } = proxy;
+
+ const nearExpiryReturnList = ref([]);
+ const open = ref(false);
+ const loading = ref(true);
+ const showSearch = ref(true);
+ const ids = ref([]);
+ const single = ref(true);
+ const multiple = ref(true);
+ const total = ref(0);
+ const title = ref("");
+
+ // 鐘舵�佸瓧鍏�
+ const statusOptions = ref([
+ { label: "寰呭鐞�", value: "0" },
+ { label: "澶勭悊涓�", value: "1" },
+ { label: "宸插畬鎴�", value: "2" },
+ ]);
+
+ const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ productName: null,
+ batchNumber: null,
+ returnDate: null,
+ },
+ rules: {
+ productName: [
+ { required: true, message: "浜у搧鍚嶇О涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ productSpec: [
+ { required: true, message: "瑙勬牸鍨嬪彿涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ batchNumber: [
+ { required: true, message: "鎵规鍙蜂笉鑳戒负绌�", trigger: "blur" },
+ ],
+ returnQuantity: [
+ { required: true, message: "閫�鍥炴暟閲忎笉鑳戒负绌�", trigger: "blur" },
+ ],
+ productionDate: [
+ { required: true, message: "鐢熶骇鏃ユ湡涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ expiryDate: [
+ { required: true, message: "鍒版湡鏃ユ湡涓嶈兘涓虹┖", trigger: "blur" },
+ ],
+ returnDate: [
+ { required: true, message: "閫�鍥炴棩鏈熶笉鑳戒负绌�", trigger: "blur" },
+ ],
+ returnReason: [
+ { required: true, message: "閫�鍥炲師鍥犱笉鑳戒负绌�", trigger: "blur" },
+ ],
+ status: [
+ { required: true, message: "澶勭悊鐘舵�佷笉鑳戒负绌�", trigger: "change" },
+ ],
+ },
+ });
+
+ const { queryParams, form, rules } = toRefs(data);
+
+ /** 鏌ヨ涓存湡閫�鍥炲彴璐﹀垪琛� */
+ function getList() {
+ loading.value = true;
+ // 涓嶈皟鐢ㄦ帴鍙o紝杩斿洖绌烘暟鎹�
+ nearExpiryReturnList.value = [];
+ total.value = 0;
+ loading.value = false;
+ }
+
+ // 鍙栨秷鎸夐挳
+ function cancel() {
+ open.value = false;
+ reset();
+ }
+
+ // 琛ㄥ崟閲嶇疆
+ function reset() {
+ form.value = {
+ id: null,
+ productName: null,
+ productSpec: null,
+ batchNumber: null,
+ productionDate: null,
+ expiryDate: null,
+ returnQuantity: null,
+ returnReason: null,
+ returnDate: null,
+ status: null,
+ remark: null,
+ };
+ proxy.resetForm("formRef");
+ }
+
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ function handleQuery() {
+ queryParams.value.pageNum = 1;
+ getList();
+ }
+
+ /** 閲嶇疆鎸夐挳鎿嶄綔 */
+ function resetQuery() {
+ proxy.resetForm("queryForm");
+ handleQuery();
+ }
+
+ // 澶氶�夋閫変腑鏁版嵁
+ function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.id);
+ single.value = selection.length !== 1;
+ multiple.value = !selection.length;
+ }
+
+ /** 鏂板鎸夐挳鎿嶄綔 */
+ function handleAdd() {
+ reset();
+ open.value = true;
+ title.value = "娣诲姞涓存湡閫�鍥炲彴璐�";
+ }
+
+ /** 淇敼鎸夐挳鎿嶄綔 */
+ function handleUpdate(row) {
+ reset();
+ // 涓嶈皟鐢ㄦ帴鍙o紝鐩存帴浣跨敤浼犲叆鐨勬暟鎹�
+ if (row) {
+ form.value = { ...row };
+ open.value = true;
+ title.value = "淇敼涓存湡閫�鍥炲彴璐�";
+ }
+ }
+
+ /** 鎻愪氦鎸夐挳 */
+ function submitForm() {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ // 涓嶈皟鐢ㄦ帴鍙o紝鍙樉绀烘垚鍔熸彁绀�
+ if (form.value.id != null) {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ } else {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ }
+ open.value = false;
+ getList();
+ }
+ });
+ }
+
+ /** 鍒犻櫎鎸夐挳鎿嶄綔 */
+ function handleDelete(row) {
+ const deleteIds = row.id || ids.value;
+ ElMessageBox.confirm(
+ '鏄惁纭鍒犻櫎涓存湡閫�鍥炲彴璐︾紪鍙蜂负"' + deleteIds + '"鐨勬暟鎹」锛�',
+ "璀﹀憡",
+ {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }
+ )
+ .then(function () {
+ // 涓嶈皟鐢ㄦ帴鍙o紝鍙樉绀烘垚鍔熸彁绀�
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ })
+ .catch(() => {});
+ }
+
+ /** 瀵煎嚭鎸夐挳鎿嶄綔 */
+ function handleExport() {
+ // 涓嶈皟鐢ㄦ帴鍙o紝鍙樉绀烘彁绀�
+ proxy.$modal.msgSuccess("瀵煎嚭鍔熻兘鏆傛湭瀹炵幇");
+ }
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
diff --git a/src/views/qualityManagement/nonconformingManagement/components/formDia.vue b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
new file mode 100644
index 0000000..e747d04
--- /dev/null
+++ b/src/views/qualityManagement/nonconformingManagement/components/formDia.vue
@@ -0,0 +1,296 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板涓嶅悎鏍肩鐞�' : '缂栬緫涓嶅悎鏍肩鐞�'"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绫诲埆锛�" prop="inspectType">
+ <el-select v-model="form.inspectType">
+ <el-option label="鍘熸潗鏂欐楠�" :value="0" />
+ <el-option label="杩囩▼妫�楠�" :value="1" />
+ <el-option label="鍑哄巶妫�楠�" :value="2" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍚嶇О锛�" prop="productId">
+ <el-tree-select
+ v-model="form.productId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ check-strictly
+ @change="getModels"
+ :data="productOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="model">
+ <el-select v-model="form.productModelId" placeholder="璇烽�夋嫨" clearable :disabled="operationType === 'edit'"
+ filterable readonly @change="handleChangeModel">
+ <el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input v-model="form.unit" placeholder="璇疯緭鍏�" clearable/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺锛�" prop="quantity">
+ <el-input-number :step="0.01" :min="0" style="width: 100%" v-model="form.quantity" placeholder="璇疯緭鍏�" clearable :precision="2"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="妫�楠屽憳锛�" prop="checkName">
+ <el-select v-model="form.checkName" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="妫�娴嬫棩鏈燂細" prop="checkTime">
+ <el-date-picker
+ v-model="form.checkTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="涓嶅悎鏍肩幇璞★細" prop="defectivePhenomena">
+ <el-input v-model="form.defectivePhenomena" placeholder="璇疯緭鍏�" clearable/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊缁撴灉锛�" prop="dealResult">
+ <el-select v-model="form.dealResult" placeholder="璇烽�夋嫨" clearable>
+ <el-option :label="item.label" :value="item.value" v-for="item in rejection_handling" :key="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="澶勭悊浜猴細" prop="dealName">
+ <el-select v-model="form.dealName" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊鏃ユ湡锛�" prop="dealTime">
+ <el-date-picker
+ v-model="form.dealTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, toRefs} from "vue";
+import {modelList, productTreeList} from "@/api/basicData/product.js";
+import {
+ getQualityUnqualifiedInfo,
+ qualityUnqualifiedAdd,
+ qualityUnqualifiedUpdate
+} from "@/api/qualityManagement/nonconformingManagement.js";
+import {userListNoPage} from "@/api/system/user.js";
+import useUserStore from "@/store/modules/user";
+const { proxy } = getCurrentInstance()
+const userStore = useUserStore()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const { rejection_handling } = proxy.useDict("rejection_handling")
+const data = reactive({
+ form: {
+ checkTime: "",
+ process: "",
+ checkName: "",
+ productName: "",
+ productId: "",
+ model: "",
+ unit: "",
+ quantity: undefined,
+ checkCompany: "",
+ checkResult: "",
+ inspectType: '',
+ defectivePhenomena: '',
+ dealResult: '',
+ dealName: '',
+ dealTime: '',
+ productModelId: undefined,
+ },
+ rules: {
+ checkTime: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" },],
+ process: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkName: [{ required: true, message: "璇烽�夋嫨妫�楠屽憳", trigger: "change" }],
+ productId: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ model: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ unit: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkCompany: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkResult: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ dealName: [{ required: true, message: "璇烽�夋嫨澶勭悊浜�", trigger: "change" }],
+ },
+});
+const { form, rules } = toRefs(data);
+const productOptions = ref([]);
+const modelOptions = ref([]);
+const userList = ref([]); // 妫�楠屽憳/澶勭悊浜轰笅鎷夊垪琛�
+
+// 鎵撳紑寮规
+const openDialog = async (type, row) => {
+ operationType.value = type;
+ try {
+ const userRes = await userListNoPage();
+ userList.value = userRes.data || [];
+ } catch (e) {
+ console.error("鍔犺浇鐢ㄦ埛鍒楄〃澶辫触", e);
+ userList.value = [];
+ }
+ dialogFormVisible.value = true;
+ if (operationType.value === 'add') {
+ form.value = {
+ checkName: userStore.nickName || '',
+ dealName: '',
+ dealTime: '',
+ dealResult: '',
+ defectivePhenomena: '',
+ inspectType: '',
+ checkTime: '',
+ productId: '',
+ model: '',
+ unit: '',
+ quantity: undefined,
+ productName: '',
+ productModelId: undefined,
+ };
+ } else {
+ form.value = {};
+ }
+ getProductOptions();
+ if (operationType.value === 'edit') {
+ getQualityUnqualifiedInfo(row.id).then(res => {
+ const { inspectState, ...rest } = (res.data || {})
+ form.value = { ...rest }
+ })
+ }
+}
+const getProductOptions = () => {
+ productTreeList().then((res) => {
+ productOptions.value = convertIdToValue(res);
+ });
+};
+const getModels = (value) => {
+ form.value.productName = findNodeById(productOptions.value, value);
+ modelList({ id: value }).then((res) => {
+ modelOptions.value = res;
+ })
+};
+const handleChangeModel = (value) => {
+ const selectedModel = modelOptions.value.find(item => item.id === value);
+ if (selectedModel) {
+ form.value.model = selectedModel.model;
+ }
+};
+const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥炶鑺傜偣
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId);
+ if (foundNode) {
+ return foundNode; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝杩斿洖璇ヨ妭鐐�
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+};
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item;
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+
+ return newItem;
+ });
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ // 鐘舵�佸瓧娈典笉鍦ㄨ〃鍗曞~鍐欙紝涔熶笉浼犵粰鍚庣
+ const { inspectState, ...payload } = (form.value || {})
+ if (operationType.value === "add") {
+ qualityUnqualifiedAdd(payload).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ } else {
+ qualityUnqualifiedUpdate(payload).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
diff --git a/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
new file mode 100644
index 0000000..8f4492a
--- /dev/null
+++ b/src/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue
@@ -0,0 +1,269 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板涓嶅悎鏍煎鐞�' : '澶勭悊涓嶅悎鏍�'"
+ width="70%"
+ @close="closeDia"
+ >
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绫诲埆锛�" prop="inspectType">
+ <el-select v-model="form.inspectType" disabled>
+ <el-option label="鍘熸潗鏂欐楠�" :value="0" />
+ <el-option label="杩囩▼妫�楠�" :value="1" />
+ <el-option label="鍑哄巶妫�楠�" :value="2" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍚嶇О锛�" prop="productId">
+ <el-tree-select
+ v-model="form.productId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ disabled
+ check-strictly
+ @change="getModels"
+ :data="productOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="model">
+ <el-input v-model="form.model" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input v-model="form.unit" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺锛�" prop="quantity">
+ <el-input v-model="form.quantity" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="妫�楠屽憳锛�" prop="checkName">
+ <el-input v-model="form.checkName" placeholder="璇疯緭鍏�" clearable disabled/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="妫�娴嬫棩鏈燂細" prop="checkTime">
+ <el-date-picker
+ v-model="form.checkTime"
+ type="date"
+ disabled
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="涓嶅悎鏍肩幇璞★細" prop="defectivePhenomena">
+ <el-input v-model="form.defectivePhenomena" placeholder="璇疯緭鍏�" clearable/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊缁撴灉锛�" prop="dealResult">
+ <el-select v-model="form.dealResult" placeholder="璇烽�夋嫨" clearable>
+ <el-option :label="item.label" :value="item.value" v-for="item in filteredRejectionHandling" :key="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="澶勭悊浜猴細" prop="dealName">
+ <el-select v-model="form.dealName" placeholder="璇烽�夋嫨" clearable style="width: 100%">
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName"/>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶勭悊鏃ユ湡锛�" prop="dealTime">
+ <el-date-picker
+ v-model="form.dealTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref, reactive, toRefs, computed} from "vue";
+import {productTreeList} from "@/api/basicData/product.js";
+import {
+ getQualityUnqualifiedInfo,
+ qualityUnqualifiedDeal
+} from "@/api/qualityManagement/nonconformingManagement.js";
+import {userListNoPage} from "@/api/system/user.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const { rejection_handling } = proxy.useDict("rejection_handling")
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const data = reactive({
+ form: {
+ checkTime: "",
+ process: "",
+ checkName: "",
+ productName: "",
+ productId: "",
+ model: "",
+ unit: "",
+ quantity: "",
+ checkCompany: "",
+ checkResult: "",
+ inspectType: '',
+ defectivePhenomena: '',
+ dealResult: '',
+ dealName: '',
+ dealTime: '',
+ method: undefined
+ },
+ rules: {
+ checkTime: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" },],
+ process: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkName: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ productId: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ model: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ unit: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ quantity: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkCompany: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkResult: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ defectivePhenomena: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ dealResult: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ dealName: [{ required: true, message: "璇烽�夋嫨澶勭悊浜�", trigger: "change" }],
+ dealTime: [{ required: true, message: "璇疯緭鍏�", trigger: "change" }],
+ },
+});
+const { form, rules } = toRefs(data);
+const productOptions = ref([]);
+const userList = ref([]); // 澶勭悊浜轰笅鎷夊垪琛�
+
+const filteredRejectionHandling = computed(() => {
+ const data = rejection_handling.value;
+ if (form.value.method) {
+ return data.filter(item => item && item.label && item.label !== '杩斿伐' && item.label !== '杩斾慨')
+ }
+ return data
+})
+
+
+// 鎵撳紑寮规
+const openDialog = async (type, row) => {
+ operationType.value = type;
+ // 澶勭悊浜轰笅鎷夊垪琛�
+ try {
+ const userRes = await userListNoPage();
+ userList.value = userRes.data || [];
+ } catch (e) {
+ console.error("鍔犺浇鐢ㄦ埛鍒楄〃澶辫触", e);
+ userList.value = [];
+ }
+ dialogFormVisible.value = true;
+ form.value = {};
+ getProductOptions();
+ if (operationType.value === 'edit') {
+ getQualityUnqualifiedInfo(row.id).then(res => {
+ const { inspectState, ...rest } = (res.data || {})
+ // 鏈夋暟鎹氨鏄剧ず榛樿鍊硷紝娌℃湁灏变笉鏄剧ず
+ form.value = { ...rest }
+ })
+ }
+}
+const getProductOptions = () => {
+ productTreeList().then((res) => {
+ productOptions.value = convertIdToValue(res);
+ });
+};
+const getModels = (value) => {
+ form.value.productName = findNodeById(productOptions.value, value);
+};
+const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥炶鑺傜偣
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId);
+ if (foundNode) {
+ return foundNode; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝杩斿洖璇ヨ妭鐐�
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+};
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item;
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+
+ return newItem;
+ });
+}
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ // 鐘舵�佸瓧娈典笉鍦ㄨ〃鍗曞~鍐欙紝涔熶笉浼犵粰鍚庣锛涘鐞嗙粺涓�璧� /deal 鎺ュ彛
+ const { inspectState, ...payload } = (form.value || {})
+ qualityUnqualifiedDeal(payload).then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+ }
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close')
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/nonconformingManagement/index.vue b/src/views/qualityManagement/nonconformingManagement/index.vue
new file mode 100644
index 0000000..fc9d5d2
--- /dev/null
+++ b/src/views/qualityManagement/nonconformingManagement/index.vue
@@ -0,0 +1,306 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" inline style="margin-bottom: 0;">
+ <el-form-item label="绫诲瀷锛�">
+ <el-select v-model="searchForm.inspectType" clearable style="width: 200px" @change="handleQuery">
+ <el-option label="鍘熸潗鏂欐楠�" :value="0" />
+ <el-option label="杩囩▼妫�楠�" :value="1" />
+ <el-option label="鍑哄巶妫�楠�" :value="2" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵�侊細">
+ <el-select v-model="searchForm.inspectState" clearable style="width: 200px" @change="handleQuery">
+ <el-option label="寰呭鐞�" :value="0" />
+ <el-option label="宸插鐞�" :value="1" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浜у搧鍚嶇О锛�">
+ <el-input
+ v-model="searchForm.productName"
+ style="width: 200px"
+ placeholder="璇疯緭鍏ヤ骇鍝佸悕绉版悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search"
+ />
+ </el-form-item>
+ <el-form-item label="妫�娴嬫棩鏈燂細">
+ <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+ style="width: 300px"
+ placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="mb20" style="text-align: right;">
+ <el-button type="primary" @click="openForm('add')">鏂板</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ ></PIMTable>
+ </div>
+ <FormDia ref="formDia" @close="handleQuery"></FormDia>
+ <InspectionFormDia ref="inspectionFormDia" @close="handleQuery"></InspectionFormDia>
+ </div>
+</template>
+
+<script setup>
+import { Search } from "@element-plus/icons-vue";
+import {onMounted, ref, reactive, toRefs, nextTick, getCurrentInstance} from "vue";
+import FormDia from "@/views/qualityManagement/nonconformingManagement/components/formDia.vue";
+import {ElMessageBox} from "element-plus";
+import {qualityUnqualifiedDel, qualityUnqualifiedListPage} from "@/api/qualityManagement/nonconformingManagement.js";
+import InspectionFormDia from "@/views/qualityManagement/nonconformingManagement/components/inspectionFormDia.vue";
+import dayjs from "dayjs";
+
+const data = reactive({
+ searchForm: {
+ inspectType: "",
+ inspectState: "",
+ productName: "",
+ entryDate: undefined, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+});
+const { searchForm } = toRefs(data);
+const tableColumn = ref([
+ {
+ label: "鐘舵��",
+ prop: "inspectState",
+ dataType: "tag",
+ formatData: (params) => {
+ if (params == 0) {
+ return "寰呭鐞�";
+ } else if (params == 1) {
+ return "宸插鐞�";
+ } else {
+ return null;
+ }
+ },
+ formatType: (params) => {
+ if (params == '涓嶅悎鏍�') {
+ return "danger";
+ } else if (params == '鍚堟牸') {
+ return "success";
+ } else {
+ return 'danger';
+ }
+ },
+ },
+ {
+ label: "妫�娴嬫棩鏈�",
+ prop: "checkTime",
+ width: 120
+ },
+ {
+ label: "绫诲埆",
+ prop: "inspectType",
+ dataType: "tag",
+ width: 120,
+ formatData: (params) => {
+ if (params == 0) {
+ return "鍘熸潗鏂欐楠�";
+ } else if (params == 1) {
+ return "杩囩▼妫�楠�";
+ } else {
+ return '鍑哄巶妫�楠�';
+ }
+ },
+ formatType: (params) => {
+ if (params == '涓嶅悎鏍�') {
+ return "info";
+ } else if (params == '鍚堟牸') {
+ return "success";
+ } else {
+ return 'primary';
+ }
+ },
+ },
+ {
+ label: "妫�楠屽憳",
+ prop: "checkName",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鏁伴噺",
+ prop: "quantity",
+ width: 100
+ },
+ {
+ label: "涓嶅悎鏍肩幇璞�",
+ prop: "defectivePhenomena",
+ width: 120
+ },
+ {
+ label: "澶勭悊缁撴灉",
+ prop: "dealResult",
+ width: 120
+ },
+ {
+ label: "澶勭悊浜�",
+ prop: "dealName",
+ width: 120
+ },
+ {
+ label: "澶勭悊鏃ユ湡",
+ prop: "dealTime",
+ width: 120
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 100,
+ operation: [
+ {
+ name: "澶勭悊",
+ type: "text",
+ clickFun: (row) => {
+ openInspectionForm("edit", row);
+ },
+ disabled: (row) => row.inspectState === 1,
+ },
+ ],
+ },
+]);
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0
+});
+const formDia = ref()
+const inspectionFormDia = ref()
+const { proxy } = getCurrentInstance()
+
+const changeDaterange = (value) => {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ getList();
+};
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined
+ qualityUnqualifiedListPage(params).then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records
+ page.total = res.data.total;
+ }).catch(err => {
+ tableLoading.value = false;
+ })
+};
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = (type, row) => {
+ if (type !== 'add' && row?.inspectState === 1) {
+ proxy.$modal.msgWarning("宸插鐞嗙殑鏁版嵁涓嶈兘鍐嶇紪杈�");
+ return;
+ }
+ nextTick(() => {
+ formDia.value?.openDialog(type, row)
+ })
+};
+// 鎵撳紑澶勭悊寮规
+const openInspectionForm = (type, row) => {
+ if (row?.inspectState === 1) {
+ proxy.$modal.msgWarning("宸插鐞嗙殑鏁版嵁涓嶈兘鍐嶅鐞�");
+ return;
+ }
+ nextTick(() => {
+ inspectionFormDia.value?.openDialog(type, row)
+ })
+};
+
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ qualityUnqualifiedDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/quality/qualityUnqualified/export", {}, "涓嶅悎鏍肩鐞�.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped></style>
diff --git a/src/views/qualityManagement/processInspection/components/filesDia.vue b/src/views/qualityManagement/processInspection/components/filesDia.vue
new file mode 100644
index 0000000..fe63f4b
--- /dev/null
+++ b/src/views/qualityManagement/processInspection/components/filesDia.vue
@@ -0,0 +1,190 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ style="display: inline;margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="paginationSearch"
+ height="500"
+ >
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import filePreview from '@/components/filePreview/index.vue'
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import {
+ qualityInspectFileAdd,
+ qualityInspectFileDel,
+ qualityInspectFileListPage
+} from "@/api/qualityManagement/qualityInspectFile.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const filePreviewRef = ref()
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: (row) => {
+ lookFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row) => {
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ qualityInspectFileListPage({inspectId: currentId.value, ...page}).then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const fileRow = {}
+ fileRow.name = res.data.originalName
+ fileRow.url = res.data.tempPath
+ uploadFile(fileRow)
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+function uploadFile(file) {
+ file.inspectId = currentId.value;
+ qualityInspectFileAdd(file).then(res => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 棰勮闄勪欢
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ qualityInspectFileDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/processInspection/components/inspectionFormDia.vue b/src/views/qualityManagement/processInspection/components/inspectionFormDia.vue
new file mode 100644
index 0000000..411856c
--- /dev/null
+++ b/src/views/qualityManagement/processInspection/components/inspectionFormDia.vue
@@ -0,0 +1,139 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="濉啓妫�楠岃褰�"
+ width="70%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ height="600"
+ >
+ <template #slot="{ row }">
+ <el-input v-model="row.testValue" clearable/>
+ </template>
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {Search} from "@element-plus/icons-vue";
+import {
+ qualityInspectParamDel,
+ qualityInspectParamInfo,
+ qualityInspectParamUpdate
+} from "@/api/qualityManagement/qualityInspectParam.js";
+import {ElMessageBox} from "element-plus";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const currentId = ref('')
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "鎸囨爣",
+ prop: "parameterItem",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鏍囧噯鍊�",
+ prop: "standardValue",
+ },
+ {
+ label: "鍐呮帶鍊�",
+ prop: "controlValue",
+ },
+ {
+ label: "妫�楠屽��",
+ prop: "testValue",
+ dataType: 'slot',
+ slot: 'slot',
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === 'edit') {
+ currentId.value = row.id;
+ getList()
+ }
+}
+const getList = () => {
+ qualityInspectParamInfo(currentId.value).then(res => {
+ tableData.value = res.data;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ qualityInspectParamUpdate(tableData.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ qualityInspectParamDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/processInspection/index.vue b/src/views/qualityManagement/processInspection/index.vue
new file mode 100644
index 0000000..58d1a3a
--- /dev/null
+++ b/src/views/qualityManagement/processInspection/index.vue
@@ -0,0 +1,484 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">宸ュ簭锛�</span>
+ <el-input v-model="searchForm.process"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ伐搴忔悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span style="margin-left: 10px"
+ class="search_title">妫�娴嬫棩鏈燂細</span>
+ <el-date-picker v-model="searchForm.entryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="changeDaterange" />
+ <span style="margin-left: 10px"
+ class="search_title">鐢熶骇宸ュ崟鍙凤細</span>
+ <el-input v-model="searchForm.workOrderNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ョ敓浜у伐鍗曞彿鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <InspectionFormDia ref="inspectionFormDia"
+ @close="handleQuery"></InspectionFormDia>
+ <FormDia ref="formDia"
+ @close="handleQuery"></FormDia>
+ <files-dia ref="filesDia"
+ @close="handleQuery"></files-dia>
+ <el-dialog v-model="dialogFormVisible"
+ title="缂栬緫妫�楠屽憳"
+ width="30%"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-form-item label="妫�楠屽憳锛�"
+ prop="checkName">
+ <el-select v-model="form.checkName"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.nickName"
+ :label="item.nickName"
+ :value="item.nickName" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+ } from "vue";
+ import InspectionFormDia from "@/views/qualityManagement/processInspection/components/inspectionFormDia.vue";
+ import FormDia from "@/views/qualityManagement/processInspection/components/formDia.vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ downloadQualityInspect,
+ qualityInspectDel,
+ qualityInspectListPage,
+ qualityInspectUpdate,
+ submitQualityInspect,
+ } from "@/api/qualityManagement/rawMaterialInspection.js";
+ import FilesDia from "@/views/qualityManagement/processInspection/components/filesDia.vue";
+ import dayjs from "dayjs";
+ import { userListNoPage } from "@/api/system/user.js";
+ import useUserStore from "@/store/modules/user";
+
+ const data = reactive({
+ searchForm: {
+ process: "",
+ entryDate: undefined, // 褰曞叆鏃ユ湡
+ workOrderNo: "",
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ rules: {
+ checkName: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const { searchForm } = toRefs(data);
+ const tableColumn = ref([
+ {
+ label: "妫�娴嬫棩鏈�",
+ prop: "checkTime",
+ width: 120,
+ },
+ {
+ label: "鐢熶骇宸ュ崟鍙�",
+ prop: "workOrderNo",
+ width: 120,
+ },
+ {
+ label: "宸ュ簭",
+ prop: "process",
+ width: 230,
+ },
+ {
+ label: "妫�楠屽憳",
+ prop: "checkName",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鎬绘暟閲�",
+ prop: "quantity",
+ width: 100,
+ },
+ {
+ label: "鍚堟牸鏁伴噺",
+ prop: "qualifiedQuantity",
+ width: 100,
+ },
+ {
+ label: "涓嶅悎鏍兼暟閲�",
+ prop: "unqualifiedQuantity",
+ width: 100,
+ },
+ {
+ label: "鍚堟牸鐜�",
+ prop: "passRate",
+ width: 100,
+ dataType: "tag",
+ formatType: params => {
+ if (!params) return "";
+ const rate = parseFloat(params);
+ if (rate < 90) {
+ return "danger";
+ } else if (rate === 100) {
+ return "success";
+ } else {
+ return "warning";
+ }
+ },
+ },
+ {
+ label: "妫�娴嬪崟浣�",
+ prop: "checkCompany",
+ width: 120,
+ },
+ {
+ label: "妫�娴嬬粨鏋�",
+ prop: "checkResult",
+ dataType: "tag",
+ formatType: params => {
+ if (params == "涓嶅悎鏍�") {
+ return "danger";
+ } else if (params == "鍚堟牸") {
+ return "success";
+ } else {
+ return "danger";
+ }
+ },
+ },
+ {
+ label: "鎻愪氦鐘舵��",
+ prop: "inspectState",
+ formatData: params => {
+ if (params) {
+ return "宸叉彁浜�";
+ } else {
+ return "鏈彁浜�";
+ }
+ },
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ disabled: row => {
+ // 宸叉彁浜ゅ垯绂佺敤
+ if (row.inspectState == 1) return true;
+ // 濡傛灉妫�楠屽憳鏈夊�硷紝鍙湁褰撳墠鐧诲綍鐢ㄦ埛鑳界紪杈�
+ if (row.checkName) {
+ return row.checkName !== userStore.nickName;
+ }
+ return false;
+ },
+ },
+ {
+ name: "鏌ョ湅",
+ type: "text",
+ clickFun: row => {
+ openForm("view", row);
+ },
+ },
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ openFilesFormDia(row);
+ },
+ },
+ {
+ name: "鎻愪氦",
+ type: "text",
+ clickFun: row => {
+ submit(row.id);
+ },
+ disabled: row => {
+ // 宸叉彁浜ゅ垯绂佺敤
+ if (row.inspectState == 1) return true;
+ // 濡傛灉妫�楠屽憳鏈夊�硷紝鍙湁褰撳墠鐧诲綍鐢ㄦ埛鑳芥彁浜�
+ if (row.checkName) {
+ return row.checkName !== userStore.nickName;
+ }
+ return false;
+ },
+ },
+ {
+ name: "鍒嗛厤妫�楠屽憳",
+ type: "text",
+ clickFun: row => {
+ if (!row.checkName) {
+ open(row);
+ } else {
+ proxy.$modal.msgError("妫�楠屽憳宸插瓨鍦�");
+ }
+ },
+ disabled: row => {
+ return row.inspectState == 1 || row.checkName;
+ },
+ },
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: row => {
+ downLoadFile(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const userList = ref([]);
+ const currentRow = ref(null);
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const dialogFormVisible = ref(false);
+ const form = ref({
+ checkName: "",
+ });
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const formDia = ref();
+ const filesDia = ref();
+ const inspectionFormDia = ref();
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
+ const changeDaterange = value => {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ getList();
+ };
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined;
+ qualityInspectListPage({ ...params, inspectType: 1 })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records.map(item => {
+ const quantity = parseFloat(item.quantity);
+ const qualifiedQuantity = parseFloat(item.qualifiedQuantity);
+ let passRate = null;
+ if (!isNaN(quantity) && !isNaN(qualifiedQuantity) && quantity > 0) {
+ passRate = ((qualifiedQuantity / quantity) * 100).toFixed(2) + "%";
+ }
+ return {
+ ...item,
+ passRate: passRate,
+ };
+ });
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎵撳紑鏂板妫�楠屽脊妗�
+ const openInspectionForm = (type, row) => {
+ nextTick(() => {
+ inspectionFormDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎵撳紑闄勪欢寮规
+ const openFilesFormDia = (type, row) => {
+ nextTick(() => {
+ filesDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎻愪环
+ const submit = async id => {
+ const res = await submitQualityInspect({ id: id });
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ getList();
+ }
+ };
+ const open = async row => {
+ let userLists = await userListNoPage();
+ userList.value = userLists.data;
+ currentRow.value = row;
+ dialogFormVisible.value = true;
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ };
+ const submitForm = () => {
+ if (currentRow.value) {
+ const data = {
+ ...form.value,
+ id: currentRow.value.id,
+ };
+ qualityInspectUpdate(data).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ };
+
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ qualityInspectDel(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ const downLoadFile = row => {
+ downloadQualityInspect({ id: row.id }).then(blobData => {
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ });
+ const downloadUrl = window.URL.createObjectURL(blob);
+
+ const link = document.createElement("a");
+ link.href = downloadUrl;
+ link.download = "杩囩▼妫�楠屾姤鍛�.docx";
+ document.body.appendChild(link);
+ link.click();
+
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(downloadUrl);
+ });
+ };
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(
+ "/quality/qualityInspect/export",
+ { inspectType: 1 },
+ "杩囩▼妫�楠�.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped></style>
diff --git a/src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue b/src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue
new file mode 100644
index 0000000..e4c9700
--- /dev/null
+++ b/src/views/qualityManagement/rawMaterialInspection/components/filesDia.vue
@@ -0,0 +1,191 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="涓婁紶闄勪欢"
+ width="50%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-upload
+ v-model:file-list="fileList"
+ class="upload-demo"
+ :action="uploadUrl"
+ :on-success="handleUploadSuccess"
+ :on-error="handleUploadError"
+ name="file"
+ :show-file-list="false"
+ :headers="headers"
+ style="display: inline;margin-right: 10px"
+ >
+ <el-button type="primary">涓婁紶闄勪欢</el-button>
+ </el-upload>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ @pagination="paginationSearch"
+ height="500"
+ >
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import filePreview from '@/components/filePreview/index.vue'
+import {ElMessageBox} from "element-plus";
+import {getToken} from "@/utils/auth.js";
+import {
+ qualityInspectFileAdd,
+ qualityInspectFileDel,
+ qualityInspectFileListPage
+} from "@/api/qualityManagement/qualityInspectFile.js";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const currentId = ref('')
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "鏂囦欢鍚嶇О",
+ prop: "name",
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ operation: [
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: (row) => {
+ downLoadFile(row);
+ },
+ },
+ {
+ name: "棰勮",
+ type: "text",
+ clickFun: (row) => {
+ lookFile(row);
+ },
+ }
+ ],
+ },
+]);
+const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+});
+const tableData = ref([]);
+const fileList = ref([]);
+const tableLoading = ref(false);
+const filePreviewRef = ref()
+const headers = ref({
+ Authorization: "Bearer " + getToken(),
+});
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/file/upload"); // 涓婁紶鐨勫浘鐗囨湇鍔″櫒鍦板潃
+
+// 鎵撳紑寮规
+const openDialog = (row) => {
+ dialogFormVisible.value = true;
+ currentId.value = row.id;
+ getList()
+}
+const paginationSearch = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ qualityInspectFileListPage({inspectId: currentId.value, ...page}).then(res => {
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 涓婁紶鎴愬姛澶勭悊
+function handleUploadSuccess(res, file) {
+ // 濡傛灉涓婁紶鎴愬姛
+ if (res.code == 200) {
+ const fileRow = {}
+ fileRow.name = res.data.originalName
+ fileRow.url = res.data.tempPath
+ uploadFile(fileRow)
+ } else {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ }
+}
+function uploadFile(file) {
+ file.inspectId = currentId.value;
+ qualityInspectFileAdd(file).then(res => {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ getList()
+ })
+}
+// 涓婁紶澶辫触澶勭悊
+function handleUploadError() {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+}
+// 涓嬭浇闄勪欢
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+}
+// 棰勮闄勪欢
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ qualityInspectFileDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ }).catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/rawMaterialInspection/components/inspectionFormDia.vue b/src/views/qualityManagement/rawMaterialInspection/components/inspectionFormDia.vue
new file mode 100644
index 0000000..411856c
--- /dev/null
+++ b/src/views/qualityManagement/rawMaterialInspection/components/inspectionFormDia.vue
@@ -0,0 +1,139 @@
+<template>
+ <div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ title="濉啓妫�楠岃褰�"
+ width="70%"
+ @close="closeDia"
+ >
+ <div style="margin-bottom: 10px;text-align: right">
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :tableLoading="tableLoading"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ height="600"
+ >
+ <template #slot="{ row }">
+ <el-input v-model="row.testValue" clearable/>
+ </template>
+ </PIMTable>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import {ref} from "vue";
+import {Search} from "@element-plus/icons-vue";
+import {
+ qualityInspectParamDel,
+ qualityInspectParamInfo,
+ qualityInspectParamUpdate
+} from "@/api/qualityManagement/qualityInspectParam.js";
+import {ElMessageBox} from "element-plus";
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['close'])
+
+const dialogFormVisible = ref(false);
+const operationType = ref('')
+const currentId = ref('')
+const selectedRows = ref([]);
+const tableColumn = ref([
+ {
+ label: "鎸囨爣",
+ prop: "parameterItem",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鏍囧噯鍊�",
+ prop: "standardValue",
+ },
+ {
+ label: "鍐呮帶鍊�",
+ prop: "controlValue",
+ },
+ {
+ label: "妫�楠屽��",
+ prop: "testValue",
+ dataType: 'slot',
+ slot: 'slot',
+ },
+]);
+const tableData = ref([]);
+const tableLoading = ref(false);
+
+// 鎵撳紑寮规
+const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ if (operationType.value === 'edit') {
+ currentId.value = row.id;
+ getList()
+ }
+}
+const getList = () => {
+ qualityInspectParamInfo(currentId.value).then(res => {
+ tableData.value = res.data;
+ })
+}
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitForm = () => {
+ qualityInspectParamUpdate(tableData.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ })
+}
+// 鍏抽棴寮规
+const closeDia = () => {
+ dialogFormVisible.value = false;
+ emit('close')
+};
+// 鍒犻櫎
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map((item) => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ qualityInspectParamDel(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+defineExpose({
+ openDialog,
+});
+</script>
+
+<style scoped>
+
+</style>
\ No newline at end of file
diff --git a/src/views/qualityManagement/rawMaterialInspection/index.vue b/src/views/qualityManagement/rawMaterialInspection/index.vue
new file mode 100644
index 0000000..6d2acd5
--- /dev/null
+++ b/src/views/qualityManagement/rawMaterialInspection/index.vue
@@ -0,0 +1,490 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">渚涘簲鍟嗭細</span>
+ <el-input v-model="searchForm.supplier"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヤ緵搴斿晢鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span style="margin-left: 10px"
+ class="search_title">妫�娴嬫棩鏈燂細</span>
+ <el-date-picker v-model="searchForm.entryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="changeDaterange" />
+ <span style="margin-left: 10px"
+ class="search_title">閲囪喘璁㈠崟鍙凤細</span>
+ <el-input v-model="searchForm.purchaseContractNo"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ラ噰璐鍗曞彿鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <InspectionFormDia ref="inspectionFormDia"
+ @close="handleQuery"></InspectionFormDia>
+ <FormDia ref="formDia"
+ @close="handleQuery"></FormDia>
+ <files-dia ref="filesDia"
+ @close="handleQuery"></files-dia>
+ <el-dialog v-model="dialogFormVisible"
+ title="缂栬緫妫�楠屽憳"
+ width="30%"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-form-item label="妫�楠屽憳锛�"
+ prop="checkName">
+ <el-select v-model="form.checkName"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.nickName"
+ :label="item.nickName"
+ :value="item.nickName" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ nextTick,
+ } from "vue";
+ import InspectionFormDia from "@/views/qualityManagement/rawMaterialInspection/components/inspectionFormDia.vue";
+ import FormDia from "@/views/qualityManagement/rawMaterialInspection/components/formDia.vue";
+ import { ElMessageBox } from "element-plus";
+ import {
+ downloadQualityInspect,
+ qualityInspectDel,
+ qualityInspectListPage,
+ qualityInspectUpdate,
+ submitQualityInspect,
+ } from "@/api/qualityManagement/rawMaterialInspection.js";
+ import FilesDia from "@/views/qualityManagement/rawMaterialInspection/components/filesDia.vue";
+ import dayjs from "dayjs";
+ import { userListNoPage } from "@/api/system/user.js";
+ import useUserStore from "@/store/modules/user";
+
+ const data = reactive({
+ searchForm: {
+ supplier: "",
+ entryDate: undefined, // 褰曞叆鏃ユ湡
+ purchaseContractNo: "",
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ rules: {
+ checkName: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const { searchForm, rules } = toRefs(data);
+ const tableColumn = ref([
+ {
+ label: "妫�娴嬫棩鏈�",
+ prop: "checkTime",
+ width: 120,
+ },
+ {
+ label: "閲囪喘璁㈠崟鍙�",
+ prop: "purchaseContractNo",
+ width: 120,
+ },
+ {
+ label: "渚涘簲鍟�",
+ prop: "supplier",
+ width: 230,
+ },
+ {
+ label: "妫�楠屽憳",
+ prop: "checkName",
+ },
+ {
+ label: "浜у搧鍚嶇О",
+ prop: "productName",
+ },
+ {
+ label: "瑙勬牸鍨嬪彿",
+ prop: "model",
+ },
+ {
+ label: "鍗曚綅",
+ prop: "unit",
+ },
+ {
+ label: "鎬绘暟閲�",
+ prop: "quantity",
+ width: 100,
+ },
+ {
+ label: "鍚堟牸鏁伴噺",
+ prop: "qualifiedQuantity",
+ width: 100,
+ },
+ {
+ label: "涓嶅悎鏍兼暟閲�",
+ prop: "unqualifiedQuantity",
+ width: 100,
+ },
+ {
+ label: "鍚堟牸鐜�",
+ prop: "passRate",
+ width: 100,
+ dataType: "tag",
+ formatType: params => {
+ if (!params) return "";
+ const rate = parseFloat(params);
+ if (rate < 90) {
+ return "danger";
+ } else if (rate === 100) {
+ return "success";
+ } else {
+ return "warning";
+ }
+ },
+ },
+ {
+ label: "妫�娴嬪崟浣�",
+ prop: "checkCompany",
+ width: 120,
+ },
+ {
+ label: "妫�娴嬪崟浣�",
+ prop: "checkCompany",
+ width: 120,
+ },
+ {
+ label: "妫�娴嬬粨鏋�",
+ prop: "checkResult",
+ dataType: "tag",
+ formatType: params => {
+ if (params === "涓嶅悎鏍�") {
+ return "danger";
+ } else if (params === "鍚堟牸") {
+ return "success";
+ } else {
+ return "danger";
+ }
+ },
+ },
+ {
+ label: "鎻愪氦鐘舵��",
+ prop: "inspectState",
+ formatData: params => {
+ if (params) {
+ return "宸叉彁浜�";
+ } else {
+ return "鏈彁浜�";
+ }
+ },
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 280,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ disabled: row => {
+ // 宸叉彁浜ゅ垯绂佺敤
+ if (row.inspectState == 1) return true;
+ // 濡傛灉妫�楠屽憳鏈夊�硷紝鍙湁褰撳墠鐧诲綍鐢ㄦ埛鑳界紪杈�
+ if (row.checkName) {
+ return row.checkName !== userStore.nickName;
+ }
+ return false;
+ },
+ },
+ {
+ name: "鏌ョ湅",
+ type: "text",
+ clickFun: row => {
+ openForm("view", row);
+ },
+ },
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ openFilesFormDia(row);
+ },
+ },
+ {
+ name: "鎻愪氦",
+ type: "text",
+ clickFun: row => {
+ submit(row.id);
+ },
+ disabled: row => {
+ // 宸叉彁浜ゅ垯绂佺敤
+ if (row.inspectState == 1) return true;
+ // 濡傛灉妫�楠屽憳鏈夊�硷紝鍙湁褰撳墠鐧诲綍鐢ㄦ埛鑳芥彁浜�
+ if (row.checkName) {
+ return row.checkName !== userStore.nickName;
+ }
+ return false;
+ },
+ },
+ {
+ name: "鍒嗛厤妫�楠屽憳",
+ type: "text",
+ clickFun: row => {
+ if (!row.checkName) {
+ open(row);
+ } else {
+ proxy.$modal.msgError("妫�楠屽憳宸插瓨鍦�");
+ }
+ },
+ disabled: row => {
+ return row.inspectState == 1 || row.checkName;
+ },
+ },
+ {
+ name: "涓嬭浇",
+ type: "text",
+ clickFun: row => {
+ downLoadFile(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const userList = ref([]);
+ const dialogFormVisible = ref(false);
+ const form = ref({
+ checkName: "",
+ });
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const currentRow = ref(null);
+ const formDia = ref();
+ const filesDia = ref();
+ const inspectionFormDia = ref();
+ const { proxy } = getCurrentInstance();
+ const userStore = useUserStore();
+ const changeDaterange = value => {
+ searchForm.value.entryDateStart = undefined;
+ searchForm.value.entryDateEnd = undefined;
+ if (value) {
+ searchForm.value.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.value.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ }
+ getList();
+ };
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const params = { ...searchForm.value, ...page };
+ params.entryDate = undefined;
+ qualityInspectListPage({ ...params, inspectType: 0 })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records.map(item => {
+ const quantity = parseFloat(item.quantity);
+ const qualifiedQuantity = parseFloat(item.qualifiedQuantity);
+ let passRate = null;
+ if (!isNaN(quantity) && !isNaN(qualifiedQuantity) && quantity > 0) {
+ passRate = ((qualifiedQuantity / quantity) * 100).toFixed(2) + "%";
+ }
+ return {
+ ...item,
+ passRate: passRate,
+ };
+ });
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鎵撳紑寮规
+ const openForm = (type, row) => {
+ nextTick(() => {
+ formDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎵撳紑闄勪欢寮规
+ const openFilesFormDia = (type, row) => {
+ nextTick(() => {
+ filesDia.value?.openDialog(type, row);
+ });
+ };
+
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ qualityInspectDel(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ // 瀵煎嚭
+ const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download(
+ "/quality/qualityInspect/export",
+ { inspectType: 0 },
+ "鍘熸潗鏂欐楠�.xlsx"
+ );
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ // 鎻愪环
+ const submit = async id => {
+ const res = await submitQualityInspect({ id: id });
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ getList();
+ }
+ };
+
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ };
+
+ const submitForm = () => {
+ if (currentRow.value) {
+ const data = {
+ ...form.value,
+ id: currentRow.value.id,
+ };
+ qualityInspectUpdate(data).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ };
+
+ const open = async row => {
+ let userLists = await userListNoPage();
+ userList.value = userLists.data;
+ currentRow.value = row;
+ dialogFormVisible.value = true;
+ };
+
+ const downLoadFile = row => {
+ downloadQualityInspect({ id: row.id }).then(blobData => {
+ const blob = new Blob([blobData], {
+ type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+ });
+ const downloadUrl = window.URL.createObjectURL(blob);
+
+ const link = document.createElement("a");
+ link.href = downloadUrl;
+ link.download = "鍘熸潗鏂欐楠屾姤鍛�.docx";
+ document.body.appendChild(link);
+ link.click();
+
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(downloadUrl);
+ });
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped></style>
diff --git a/src/views/qualityManagement/visualization/qualityDashboard.vue b/src/views/qualityManagement/visualization/qualityDashboard.vue
new file mode 100644
index 0000000..57462b7
--- /dev/null
+++ b/src/views/qualityManagement/visualization/qualityDashboard.vue
@@ -0,0 +1,307 @@
+<template>
+ <div class="quality-dashboard">
+ <el-row :gutter="16">
+ <el-col :xs="24" :sm="12">
+ <el-card shadow="hover" class="panel">
+ <template #header>
+ <div class="panel-title">
+ 妫�娴嬫牱鍝佸姩鎬佺姸鎬�
+ <div class="actions">
+ <el-switch v-model="voiceEnabled" active-text="璇煶棰勮" inactive-text="闈欓煶" />
+ </div>
+ </div>
+ </template>
+ <div class="status-list">
+ <div v-for="item in sampleStatus" :key="item.id" class="status-item" :class="item.status">
+ <div class="left">
+ <span class="dot" :class="item.status"></span>
+ <span class="name">{{ item.name }}</span>
+ </div>
+ <div class="right">
+ <el-tag :type="statusTagType(item.status)" size="small">{{ statusLabel(item.status) }}</el-tag>
+ <span class="time">{{ item.time }}</span>
+ </div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :xs="24" :sm="12">
+ <el-card shadow="hover" class="panel">
+ <template #header>
+ <div class="panel-title">浠诲姟鎺掕锛圱op 10锛�</div>
+ </template>
+ <EChart :xAxis="tasksXAxis" :yAxis="[{ type: 'value' }]" :series="tasksSeries" :grid="{ left: 40, right: 20, top: 20, bottom: 40 }" :tooltip="{ trigger: 'axis' }" :barColors="['#3b82f6']" :chartStyle="{ height: '320px', width: '100%' }" />
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="16" style="margin-top: 16px;">
+ <el-col :xs="24" :sm="14">
+ <el-card shadow="hover" class="panel">
+ <template #header>
+ <div class="panel-title">鍘嗗彶瓒嬪娍</div>
+ </template>
+ <EChart :xAxis="[{ type: 'category', data: trendXAxis }]" :yAxis="[{ type: 'value', name: '鏁伴噺' }]" :series="trendSeries" :tooltip="{ trigger: 'axis' }" :legend="{ top: 0 }" :lineColors="['#10b981', '#f59e0b']" :chartStyle="{ height: '340px', width: '100%' }" />
+ </el-card>
+ </el-col>
+ <el-col :xs="24" :sm="10">
+ <el-card shadow="hover" class="panel">
+ <template #header>
+ <div class="panel-title">鍚堟牸鐜囧垎鏋�</div>
+ </template>
+ <EChart :series="passRateSeries" :legend="{ show: false }" :chartStyle="{ height: '340px', width: '100%' }" />
+ <div class="passrate-text">
+ 褰撳墠鍚堟牸鐜囷細<b>{{ (passRate * 100).toFixed(1) }}%</b>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="16" style="margin-top: 16px;">
+ <el-col :xs="24">
+ <el-card shadow="hover" class="panel">
+ <template #header>
+ <div class="panel-title">SPC 鎺у埗鍥撅紙Xbar锛�</div>
+ </template>
+ <EChart :xAxis="[{ type: 'category', data: spcXAxis }]" :yAxis="[{ type: 'value', name: '娴嬮噺鍊�' }]" :series="spcSeries" :legend="{ top: 0 }" :tooltip="{ trigger: 'axis' }" :lineColors="['#2563eb', '#ef4444', '#f97316', '#22c55e']" :chartStyle="{ height: '380px', width: '100%' }" />
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+</template>
+
+<script setup>
+import { onMounted, onBeforeUnmount, reactive, ref } from 'vue'
+import EChart from '@/components/Echarts/echarts.vue'
+
+const voiceEnabled = ref(true)
+let dataTimer = null
+
+// 1) 鏍峰搧鍔ㄦ�佺姸鎬侊紙婊氬姩鏇存柊锛�
+const sampleStatus = ref([])
+const statusPool = ['processing', 'warning', 'error', 'success']
+function statusLabel(s) {
+ return s === 'processing' ? '妫�娴嬩腑' : s === 'warning' ? '棰勮' : s === 'error' ? '涓嶅悎鏍�' : '鍚堟牸'
+}
+function statusTagType(s) {
+ return s === 'processing' ? 'info' : s === 'warning' ? 'warning' : s === 'error' ? 'danger' : 'success'
+}
+function randomSample() {
+ const id = Math.random().toString(36).slice(2, 8)
+ const status = statusPool[Math.floor(Math.random() * statusPool.length)]
+ const name = `鏍峰搧-${Math.floor(Math.random() * 900 + 100)}`
+ const time = new Date().toLocaleTimeString('zh-CN', { hour12: false })
+ return { id, name, status, time }
+}
+
+// 2) 浠诲姟鎺掕锛堟煴鐘跺浘锛�
+const tasksXAxis = reactive([{ type: 'category', data: [] }])
+const tasksSeries = ref([
+ {
+ type: 'bar',
+ data: [],
+ label: { show: true, position: 'inside', align: 'center', verticalAlign: 'middle', color: '#fff' },
+ encode: undefined,
+ },
+])
+
+// 3) 鍘嗗彶瓒嬪娍锛堟姌绾匡級
+const trendXAxis = ref([])
+const trendSeries = ref([
+ { name: '鏉ユ牱鏁�', type: 'line', smooth: true, data: [] },
+ { name: '瀹屾垚鏁�', type: 'line', smooth: true, data: [] },
+])
+
+// 4) 鍚堟牸鐜囧垎鏋愶紙浠〃鐩橈級
+const passRate = ref(0.92)
+const passRateSeries = ref([
+ {
+ type: 'gauge',
+ progress: { show: true, width: 12 },
+ axisLine: { lineStyle: { width: 12 } },
+ pointer: { show: true },
+ detail: { valueAnimation: true, formatter: (v) => `${(v * 100).toFixed(1)}%` },
+ data: [{ value: passRate.value }],
+ },
+])
+
+// 5) SPC 鎺у埗鍥�
+const spcXAxis = ref([])
+const spcData = ref([]) // 瀹為檯娴嬮噺鍊�
+const CL = ref(50)
+const UCL = ref(55)
+const LCL = ref(45)
+const spcSeries = ref([
+ {
+ name: '娴嬮噺鍧囧��',
+ type: 'line',
+ smooth: false,
+ symbol: 'circle',
+ data: [],
+ markLine: {
+ symbol: 'none',
+ lineStyle: { type: 'dashed', color: '#999' },
+ data: [
+ { yAxis: () => UCL.value, name: 'UCL' },
+ { yAxis: () => CL.value, name: 'CL' },
+ { yAxis: () => LCL.value, name: 'LCL' },
+ ],
+ label: { formatter: ({ name }) => name },
+ },
+ },
+ { name: 'UCL', type: 'line', data: [], symbol: 'none', lineStyle: { type: 'dashed', color: '#ef4444' } },
+ { name: 'CL', type: 'line', data: [], symbol: 'none', lineStyle: { type: 'dashed', color: '#f97316' } },
+ { name: 'LCL', type: 'line', data: [], symbol: 'none', lineStyle: { type: 'dashed', color: '#22c55e' } },
+])
+
+// 璇煶鎾姤
+function speak(text) {
+ if (!voiceEnabled.value) return
+ if (!('speechSynthesis' in window)) return
+ const utter = new SpeechSynthesisUtterance(text)
+ utter.lang = 'zh-CN'
+ try {
+ window.speechSynthesis.cancel()
+ window.speechSynthesis.speak(utter)
+ } catch (e) {
+ // ignore
+ }
+}
+
+function refreshFakeData() {
+ // 鏍峰搧鐘舵�佹粴鍔�
+ const next = randomSample()
+ sampleStatus.value = [next, ...sampleStatus.value].slice(0, 8)
+
+ // 浠诲姟鎺掕
+ const tasks = Array.from({ length: 10 }).map((_, i) => ({ name: `浠诲姟-${i + 1}`, count: Math.floor(Math.random() * 100 + 20) }))
+ tasks.sort((a, b) => a.count - b.count)
+ tasksXAxis.data = tasks.map(t => t.name)
+ tasksSeries.value[0].data = tasks.map(t => t.count)
+
+ // 鍘嗗彶瓒嬪娍锛堣拷鍔犵偣锛�
+ const nowLabel = new Date().toLocaleTimeString('zh-CN', { minute: '2-digit', second: '2-digit' })
+ if (trendXAxis.value.length > 15) {
+ trendXAxis.value.shift()
+ trendSeries.value[0].data.shift()
+ trendSeries.value[1].data.shift()
+ }
+ trendXAxis.value.push(nowLabel)
+ const incoming = Math.floor(Math.random() * 30 + 20)
+ const finished = Math.max(0, incoming - Math.floor(Math.random() * 10))
+ trendSeries.value[0].data.push(incoming)
+ trendSeries.value[1].data.push(finished)
+
+ // 鍚堟牸鐜囷紙杞诲井娉㈠姩锛�
+ const delta = (Math.random() - 0.5) * 0.02
+ passRate.value = Math.min(0.99, Math.max(0.6, passRate.value + delta))
+ passRateSeries.value[0].data[0].value = passRate.value
+
+ // SPC 鏁版嵁锛堢獥鍙gЩ鍔級
+ const nextVal = CL.value + (Math.random() - 0.5) * 8 // 娉㈠姩
+ if (spcXAxis.value.length > 30) {
+ spcXAxis.value.shift()
+ spcData.value.shift()
+ }
+ spcXAxis.value.push(`${spcXAxis.value.length + 1}`)
+ spcData.value.push(parseFloat(nextVal.toFixed(2)))
+ spcSeries.value[0].data = [...spcData.value]
+ spcSeries.value[1].data = new Array(spcData.value.length).fill(UCL.value)
+ spcSeries.value[2].data = new Array(spcData.value.length).fill(CL.value)
+ spcSeries.value[3].data = new Array(spcData.value.length).fill(LCL.value)
+
+ // 瑙﹀彂鎾姤锛氬悎鏍肩巼杩囦綆鎴� SPC 瓒呴檺
+ if (passRate.value < 0.8) {
+ speak(`棰勮锛屽綋鍓嶅悎鏍肩巼涓� ${(passRate.value * 100).toFixed(0)}%锛屼綆浜� 80% 闃堝�糮)
+ }
+ const last = spcData.value[spcData.value.length - 1]
+ if (last > UCL.value) {
+ speak(`棰勮锛屾渶鏂版祴閲忓�� ${last.toFixed(2)} 瓒呰繃涓婇檺`)
+ }
+ if (last < LCL.value) {
+ speak(`棰勮锛屾渶鏂版祴閲忓�� ${last.toFixed(2)} 浣庝簬涓嬮檺`)
+ }
+}
+
+onMounted(() => {
+ // 鍒濆鍖栧嚑鏉″亣鏁版嵁
+ sampleStatus.value = Array.from({ length: 5 }).map(() => randomSample())
+ for (let i = 0; i < 10; i++) {
+ trendXAxis.value.push(`T-${i}`)
+ trendSeries.value[0].data.push(Math.floor(Math.random() * 30 + 20))
+ trendSeries.value[1].data.push(Math.floor(Math.random() * 25 + 15))
+ }
+ for (let i = 0; i < 20; i++) {
+ spcXAxis.value.push(`${i + 1}`)
+ const v = CL.value + (Math.random() - 0.5) * 6
+ spcData.value.push(parseFloat(v.toFixed(2)))
+ }
+ spcSeries.value[0].data = [...spcData.value]
+ spcSeries.value[1].data = new Array(spcData.value.length).fill(UCL.value)
+ spcSeries.value[2].data = new Array(spcData.value.length).fill(CL.value)
+ spcSeries.value[3].data = new Array(spcData.value.length).fill(LCL.value)
+
+ dataTimer = setInterval(refreshFakeData, 10000)
+})
+
+onBeforeUnmount(() => {
+ if (dataTimer) clearInterval(dataTimer)
+ try { window.speechSynthesis && window.speechSynthesis.cancel() } catch (e) {}
+})
+</script>
+
+<style scoped>
+.quality-dashboard {
+ padding: 8px;
+}
+.panel-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-weight: 600;
+}
+.status-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ max-height: 320px;
+ overflow: auto;
+}
+.status-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 10px;
+ border-radius: 6px;
+ background: var(--el-fill-color-light);
+}
+.status-item .left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+.status-item .right {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+.status-item .name { font-weight: 500; }
+.status-item .time { color: var(--el-text-color-secondary); font-size: 12px; }
+.dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ display: inline-block;
+}
+.dot.processing { background: #60a5fa; }
+.dot.warning { background: #f59e0b; }
+.dot.error { background: #ef4444; }
+.dot.success { background: #10b981; }
+.passrate-text {
+ text-align: center;
+ margin-top: 8px;
+}
+</style>
+
+
diff --git a/src/views/redirect/index.vue b/src/views/redirect/index.vue
new file mode 100644
index 0000000..d932b25
--- /dev/null
+++ b/src/views/redirect/index.vue
@@ -0,0 +1,14 @@
+<template>
+ <div></div>
+</template>
+
+<script setup>
+import { useRoute, useRouter } from 'vue-router'
+
+const route = useRoute()
+const router = useRouter()
+const { params, query } = route
+const { path } = params
+
+router.replace({ path: '/' + path, query })
+</script>
\ No newline at end of file
diff --git a/src/views/register.vue b/src/views/register.vue
new file mode 100644
index 0000000..58d9ba5
--- /dev/null
+++ b/src/views/register.vue
@@ -0,0 +1,220 @@
+<template>
+ <div class="register">
+ <el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
+ <h3 class="title">{{ title }}</h3>
+ <el-form-item prop="username">
+ <el-input
+ v-model="registerForm.username"
+ type="text"
+ size="large"
+ auto-complete="off"
+ placeholder="璐﹀彿"
+ >
+ <template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="password">
+ <el-input
+ v-model="registerForm.password"
+ type="password"
+ size="large"
+ auto-complete="off"
+ placeholder="瀵嗙爜"
+ @keyup.enter="handleRegister"
+ >
+ <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="confirmPassword">
+ <el-input
+ v-model="registerForm.confirmPassword"
+ type="password"
+ size="large"
+ auto-complete="off"
+ placeholder="纭瀵嗙爜"
+ @keyup.enter="handleRegister"
+ >
+ <template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
+ </el-input>
+ </el-form-item>
+ <el-form-item prop="code" v-if="captchaEnabled">
+ <el-input
+ size="large"
+ v-model="registerForm.code"
+ auto-complete="off"
+ placeholder="楠岃瘉鐮�"
+ style="width: 63%"
+ @keyup.enter="handleRegister"
+ >
+ <template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
+ </el-input>
+ <div class="register-code">
+ <img :src="codeUrl" @click="getCode" class="register-code-img"/>
+ </div>
+ </el-form-item>
+ <el-form-item style="width:100%;">
+ <el-button
+ :loading="loading"
+ size="large"
+ type="primary"
+ style="width:100%;"
+ @click.prevent="handleRegister"
+ >
+ <span v-if="!loading">娉� 鍐�</span>
+ <span v-else>娉� 鍐� 涓�...</span>
+ </el-button>
+ <div style="float: right;">
+ <router-link class="link-type" :to="'/login'">浣跨敤宸叉湁璐︽埛鐧诲綍</router-link>
+ </div>
+ </el-form-item>
+ </el-form>
+ <!-- 搴曢儴 -->
+ <div class="el-register-footer">
+ <span>Copyright 漏 2018-2025 ruoyi.vip All Rights Reserved.</span>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ElMessageBox } from "element-plus"
+import { getCodeImg, register } from "@/api/login"
+
+const title = import.meta.env.VITE_APP_TITLE
+const router = useRouter()
+const { proxy } = getCurrentInstance()
+
+const registerForm = ref({
+ username: "",
+ password: "",
+ confirmPassword: "",
+ code: "",
+ uuid: ""
+})
+
+const equalToPassword = (rule, value, callback) => {
+ if (registerForm.value.password !== value) {
+ callback(new Error("涓ゆ杈撳叆鐨勫瘑鐮佷笉涓�鑷�"))
+ } else {
+ callback()
+ }
+}
+
+const registerRules = {
+ username: [
+ { required: true, trigger: "blur", message: "璇疯緭鍏ユ偍鐨勮处鍙�" },
+ { min: 2, max: 20, message: "鐢ㄦ埛璐﹀彿闀垮害蹇呴』浠嬩簬 2 鍜� 20 涔嬮棿", trigger: "blur" }
+ ],
+ password: [
+ { required: true, trigger: "blur", message: "璇疯緭鍏ユ偍鐨勫瘑鐮�" },
+ { min: 5, max: 20, message: "鐢ㄦ埛瀵嗙爜闀垮害蹇呴』浠嬩簬 5 鍜� 20 涔嬮棿", trigger: "blur" },
+ { pattern: /^[^<>"'|\\]+$/, message: "涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > \" ' \\\ |", trigger: "blur" }
+ ],
+ confirmPassword: [
+ { required: true, trigger: "blur", message: "璇峰啀娆¤緭鍏ユ偍鐨勫瘑鐮�" },
+ { required: true, validator: equalToPassword, trigger: "blur" }
+ ],
+ code: [{ required: true, trigger: "change", message: "璇疯緭鍏ラ獙璇佺爜" }]
+}
+
+const codeUrl = ref("")
+const loading = ref(false)
+const captchaEnabled = ref(true)
+
+function handleRegister() {
+ proxy.$refs.registerRef.validate(valid => {
+ if (valid) {
+ loading.value = true
+ register(registerForm.value).then(res => {
+ const username = registerForm.value.username
+ ElMessageBox.alert("<font color='red'>鎭枩浣狅紝鎮ㄧ殑璐﹀彿 " + username + " 娉ㄥ唽鎴愬姛锛�</font>", "绯荤粺鎻愮ず", {
+ dangerouslyUseHTMLString: true,
+ type: "success",
+ }).then(() => {
+ router.push("/login")
+ }).catch(() => {})
+ }).catch(() => {
+ loading.value = false
+ if (captchaEnabled) {
+ getCode()
+ }
+ })
+ }
+ })
+}
+
+function getCode() {
+ getCodeImg().then(res => {
+ captchaEnabled.value = res.captchaEnabled === undefined ? true : res.captchaEnabled
+ if (captchaEnabled.value) {
+ codeUrl.value = "data:image/gif;base64," + res.img
+ registerForm.value.uuid = res.uuid
+ }
+ })
+}
+
+getCode()
+</script>
+
+<style lang='scss' scoped>
+.register {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100%;
+ background-image: url("../assets/images/login-background.png");
+ background-size: cover;
+}
+.title {
+ margin: 0px auto 30px auto;
+ text-align: center;
+ color: #707070;
+}
+
+.register-form {
+ border-radius: 6px;
+ background: #ffffff;
+ width: 400px;
+ padding: 25px 25px 5px 25px;
+ .el-input {
+ height: 40px;
+ input {
+ height: 40px;
+ }
+ }
+ .input-icon {
+ height: 39px;
+ width: 14px;
+ margin-left: 0px;
+ }
+}
+.register-tip {
+ font-size: 13px;
+ text-align: center;
+ color: #bfbfbf;
+}
+.register-code {
+ width: 33%;
+ height: 40px;
+ float: right;
+ img {
+ cursor: pointer;
+ vertical-align: middle;
+ }
+}
+.el-register-footer {
+ height: 40px;
+ line-height: 40px;
+ position: fixed;
+ bottom: 0;
+ width: 100%;
+ text-align: center;
+ color: #fff;
+ font-family: Arial;
+ font-size: 12px;
+ letter-spacing: 1px;
+}
+.register-code-img {
+ height: 40px;
+ padding-left: 12px;
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/CarouselCards.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/CarouselCards.vue
new file mode 100644
index 0000000..0498824
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/CarouselCards.vue
@@ -0,0 +1,306 @@
+<template>
+ <div class="carousel-cards">
+ <button
+ v-if="canScrollLeft"
+ class="nav-button nav-button-left"
+ @click="scrollLeftFn"
+ >
+ <img src="@/assets/BI/jiantou.png" alt="宸︾澶�" />
+ </button>
+ <div
+ class="cards-container"
+ :style="{ '--visible-count': visibleCount }"
+ ref="cardsContainerRef"
+ >
+ <div
+ v-for="(item, index) in items"
+ :key="index"
+ class="card-item"
+ >
+ <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div>
+ <div class="card-title">
+ <div class="card-label">{{ item.label }}</div>
+ <div class="card-value">
+ <span class="value-number">{{ item.value }}</span>
+ <span class="value-unit">{{ item.unit }}</span>
+ </div>
+ <div v-if="item.rate ?? item.ratio ?? item.percent" class="card-rate">
+ <span class="rate-value">{{ item.rate ?? item.ratio ?? item.percent }}%</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <button
+ v-if="canScrollRight"
+ class="nav-button nav-button-right"
+ @click="scrollRightFn"
+ >
+ <img src="@/assets/BI/jiantou.png" alt="鍙崇澶�" />
+ </button>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
+
+const props = defineProps({
+ items: {
+ type: Array,
+ default: () => [],
+ validator: (value) => {
+ return value.every(item =>
+ item && typeof item.label !== 'undefined' &&
+ typeof item.value !== 'undefined' &&
+ typeof item.unit !== 'undefined'
+ )
+ }
+ },
+ visibleCount: {
+ type: Number,
+ default: 3
+ }
+})
+
+const cardsContainerRef = ref(null)
+const currentScrollLeft = ref(0)
+const maxScrollLeft = ref(0)
+
+// 妫�鏌ユ槸鍚﹀彲浠ュ悜宸︽粴鍔�
+const canScrollLeft = computed(() => {
+ return currentScrollLeft.value > 0
+})
+
+// 妫�鏌ユ槸鍚﹀彲浠ュ悜鍙虫粴鍔�
+const canScrollRight = computed(() => {
+ return currentScrollLeft.value < maxScrollLeft.value
+})
+
+// 鏇存柊婊氬姩鐘舵��
+const updateScrollState = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ currentScrollLeft.value = container.scrollLeft
+ maxScrollLeft.value = container.scrollWidth - container.clientWidth
+}
+
+// 鍚戝乏婊氬姩
+const scrollLeftFn = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ const scrollItems = Array.from(container.querySelectorAll('.card-item'))
+ if (scrollItems.length === 0) return
+
+ const itemWidth = scrollItems[0]?.offsetWidth || 0
+ const gap = 12
+ const scrollDistance = itemWidth + gap
+
+ container.scrollBy({
+ left: -scrollDistance,
+ behavior: 'smooth'
+ })
+
+ // 寤惰繜鏇存柊鐘舵�侊紝绛夊緟婊氬姩鍔ㄧ敾瀹屾垚
+ setTimeout(() => {
+ updateScrollState()
+ }, 300)
+}
+
+// 鍚戝彸婊氬姩
+const scrollRightFn = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ const scrollItems = Array.from(container.querySelectorAll('.card-item'))
+ if (scrollItems.length === 0) return
+
+ const itemWidth = scrollItems[0]?.offsetWidth || 0
+ const gap = 12
+ const scrollDistance = itemWidth + gap
+
+ container.scrollBy({
+ left: scrollDistance,
+ behavior: 'smooth'
+ })
+
+ // 寤惰繜鏇存柊鐘舵�侊紝绛夊緟婊氬姩鍔ㄧ敾瀹屾垚
+ setTimeout(() => {
+ updateScrollState()
+ }, 300)
+}
+
+// 鐩戝惉 items 鍙樺寲锛屾洿鏂版粴鍔ㄧ姸鎬�
+watch(() => props.items, () => {
+ nextTick(() => {
+ updateScrollState()
+ })
+}, { deep: true })
+
+onMounted(() => {
+ nextTick(() => {
+ updateScrollState()
+ // 鐩戝惉婊氬姩浜嬩欢
+ const container = cardsContainerRef.value
+ if (container) {
+ container.addEventListener('scroll', updateScrollState)
+ }
+ })
+})
+
+onBeforeUnmount(() => {
+ // 娓呯悊婊氬姩浜嬩欢鐩戝惉鍣�
+ const container = cardsContainerRef.value
+ if (container) {
+ container.removeEventListener('scroll', updateScrollState)
+ }
+})
+</script>
+
+<style scoped>
+.carousel-cards {
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.cards-container {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
+ padding-bottom: 4px;
+ scroll-behavior: smooth;
+}
+
+.cards-container::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+.nav-button {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 32px;
+ height: 32px;
+ background: rgba(26, 88, 176, 0.6);
+ border: 1px solid rgba(26, 88, 176, 0.8);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 10;
+ transition: all 0.3s ease;
+ padding: 0;
+}
+
+.nav-button:hover {
+ background: rgba(26, 88, 176, 0.8);
+ transform: translateY(-50%) scale(1.1);
+}
+
+.nav-button-left {
+ left: -16px;
+}
+
+.nav-button-left img {
+ width: 16px;
+ height: 16px;
+ transform: rotate(180deg);
+}
+
+.nav-button-right {
+ right: -16px;
+}
+
+.nav-button-right img {
+ width: 16px;
+ height: 16px;
+}
+
+.card-item {
+ flex: 0 0 calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
+ min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
+ display: flex;
+ align-items: center;
+ background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
+ border-radius: 8px 8px 8px 8px;
+ padding: 12px 16px;
+ transition: all 0.3s ease;
+}
+
+.card-item:hover {
+ transform: translateY(-2px);
+}
+
+.card-icon {
+ width: 80px;
+ height: 60px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ flex-shrink: 0;
+ margin-right: 12px;
+}
+
+.card-title {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ flex: 1;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 14px;
+ color: #FFFFFF;
+ margin-bottom: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+}
+
+.card-value {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+}
+
+.card-rate {
+ margin-top: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 400;
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.85);
+}
+
+.rate-label {
+ opacity: 0.85;
+}
+
+.rate-value {
+ font-weight: 500;
+}
+
+.value-number {
+ font-weight: 400;
+ font-size: 14px;
+ color: #FFFFFF;
+ line-height: 1;
+}
+
+.value-unit {
+ font-size: 14px;
+ color: #FFFFFF;
+ font-weight: 400;
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/DateTypeSwitch.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/DateTypeSwitch.vue
new file mode 100644
index 0000000..0c57b25
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/DateTypeSwitch.vue
@@ -0,0 +1,94 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="date-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍛�</el-radio-button>
+ <el-radio-button :label="2">鏈�</el-radio-button>
+ <el-radio-button :label="3">瀛e害</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍛�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+// 鐩戝惉澶栭儴鍊煎彉鍖�
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+// 澶勭悊鍊煎彉鍖�
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.date-type-switch {
+ display: inline-flex;
+}
+
+/* 鏈�変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+/* 绗竴涓寜閽乏渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+/* 鏈�鍚庝竴涓寜閽彸渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+/* 鎸夐挳涔嬮棿鐨勫垎闅旂嚎 */
+.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* 閫変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+/* 鎮仠鏁堟灉 */
+.date-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+/* 閫変腑鐘舵�佹偓鍋� */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue
new file mode 100644
index 0000000..313f1df
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue
@@ -0,0 +1,33 @@
+<template>
+ <div class="panel-header">
+ <span class="panel-title">{{ title }}</span>
+ </div>
+</template>
+
+<script setup>
+defineProps({
+ title: {
+ type: String,
+ required: true,
+ default: ''
+ }
+})
+</script>
+
+<style scoped>
+.panel-header {
+ background-image: url("@/assets/BI/kehuhetongback@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.panel-title {
+ width: 100%;
+ font-weight: 500;
+ font-size: 16px;
+ color: #D9ECFF;
+ padding-left: 46px;
+ line-height: 36px;
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/ProductTypeSwitch.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/ProductTypeSwitch.vue
new file mode 100644
index 0000000..87cde44
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/ProductTypeSwitch.vue
@@ -0,0 +1,85 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="product-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍘熸潗鏂�</el-radio-button>
+ <el-radio-button :label="3">鍗婃垚鍝�</el-radio-button>
+ <el-radio-button :label="2">鎴愬搧</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍘熸潗鏂�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.product-type-switch {
+ display: inline-flex;
+}
+
+.product-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+.product-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue
new file mode 100644
index 0000000..f4e49b6
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/center-bottom.vue
@@ -0,0 +1,188 @@
+<template>
+ <div>
+ <PanelHeader title="鍑哄叆搴撹秼鍔�" />
+ <div class="main-panel panel-item-customers">
+ <div class="filters-row">
+
+ <ProductTypeSwitch v-model="productType" @change="handleFilterChange" />
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="lineLegend"
+ :series="lineSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import * as echarts from 'echarts'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from './PanelHeader.vue'
+import ProductTypeSwitch from './ProductTypeSwitch.vue'
+import { productInOutAnalysis } from '@/api/viewIndex.js'
+
+const productType = ref(1) // 1=鍘熸潗鏂� 2=鍗婃垚鍝� 3=鎴愬搧
+
+const chartStyle = { width: '100%', height: '130%' }
+
+const grid = {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ top: '16%',
+ containLabel: true,
+}
+
+const lineLegend = {
+ show: true,
+ top: '2%',
+ left: 'center',
+ itemGap: 24,
+ itemWidth: 12,
+ itemHeight: 12,
+ textStyle: { color: '#B8C8E0', fontSize: 14 },
+ data: [
+ { name: '鍑哄簱', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
+ { name: '鍏ュ簱', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
+ ],
+}
+
+const xAxis1 = ref([
+ {
+ type: 'category',
+ data: [],
+ axisTick: { show: false },
+ axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
+ axisLabel: { color: '#B8C8E0', fontSize: 12 },
+ splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
+ },
+])
+
+const yAxis1 = [
+ {
+ type: 'value',
+ name: '鍗曚綅: 浠�',
+ nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0', fontSize: 12 },
+ splitLine: { lineStyle: { color: '#B8C8E0' } },
+ },
+]
+
+const lineSeries = ref([
+ {
+ name: '鍑哄簱',
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
+ itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
+ { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
+ ]),
+ },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+ {
+ name: '鍏ュ簱',
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
+ itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
+ { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
+ ]),
+ },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+])
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'line' },
+ borderWidth: 1,
+ textStyle: { fontSize: 12 },
+ formatter(params) {
+ let result = params[0].axisValue + '<br/>'
+ params.forEach((item) => {
+ result += `${item.marker} ${item.seriesName}: ${item.value} 浠�<br/>`
+ })
+ return result
+ },
+}
+
+const fetchData = () => {
+ productInOutAnalysis({ type: productType.value })
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const list = res.data
+ xAxis1.value[0].data = list.map((d) => d.date)
+ lineSeries.value[0].data = list.map((d) => Number(d.outCount) || 0)
+ lineSeries.value[1].data = list.map((d) => Number(d.inCount) || 0)
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇浜у搧鍑哄叆搴撳垎鏋愬け璐�:', err)
+ })
+}
+
+const handleFilterChange = () => {
+ fetchData()
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.filters-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 428px;
+}
+</style>
+
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue
new file mode 100644
index 0000000..6e39eb1
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/center-center.vue
@@ -0,0 +1,136 @@
+<template>
+ <div>
+ <!-- 璁惧缁熻 -->
+ <div class="equipment-stats">
+ <div class="equipment-header">
+ <img
+ src="@/assets/BI/shujutongjiicon@2x.png"
+ alt="鍥炬爣"
+ class="equipment-icon"
+ />
+ <span class="equipment-title">浜у搧鍛ㄨ浆澶╂暟</span>
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="barSeries1"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import { productTurnoverDays } from '@/api/viewIndex.js'
+
+const chartStyle = { width: '100%', height: '100%' }
+const grid = { left: '3%', right: '4%', bottom: '3%', top: '4%', containLabel: true }
+const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['鍛ㄨ浆澶╂暟'] }
+const barSeries1 = ref([
+ {
+ name: '鍛ㄨ浆澶╂暟',
+ type: 'bar',
+ barGap: 0,
+ barWidth: 30,
+ emphasis: { focus: 'series' },
+ 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: [],
+ },
+])
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ formatter(params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ result += `<div>${item.marker} ${item.seriesName}: ${item.value} 澶�</div>`
+ })
+ return result
+ },
+}
+const xAxis1 = ref([{ type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] }])
+const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }]
+
+const fetchData = () => {
+ productTurnoverDays()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const list = res.data
+ xAxis1.value[0].data = list.map((d) => d.name)
+ barSeries1.value[0].data = list.map((d) => Number(d.value) || 0)
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇浜у搧鍛ㄨ浆澶╂暟澶辫触:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.equipment-stats {
+ border: 1px solid #1a58b0;
+ padding: 0 18px 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.equipment-header {
+ font-weight: 500;
+ font-size: 21px;
+ display: flex;
+ border-bottom: 1px solid;
+ border-image: linear-gradient(
+ 270deg,
+ rgba(0, 126, 255, 0) 0%,
+ rgba(0, 126, 255, 0.4549) 35%,
+ #007eff 78%,
+ #007eff 100%
+ )
+ 1;
+ padding-bottom: 2px;
+}
+
+.equipment-title {
+ font-weight: 500;
+ font-size: 18px;
+ background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ line-height: 50px;
+}
+
+.equipment-icon {
+ width: 50px;
+ height: 50px;
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue
new file mode 100644
index 0000000..055fb66
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/center-top.vue
@@ -0,0 +1,144 @@
+<template>
+ <div>
+ <!-- 椤堕儴缁熻鍗$墖 -->
+ <div class="stats-cards">
+ <div
+ v-for="item in statItems"
+ :key="item.name"
+ class="stat-card"
+ >
+ <img src="@/assets/BI/icon@2x.png" alt="鍥炬爣" class="card-icon" />
+ <div class="card-content">
+ <span class="card-label">{{ item.name }}</span>
+ <span class="card-value">{{ item.value }}</span>
+ <div class="card-compare" :class="compareClass(Number(item.rate))">
+ <span>鍚屾瘮</span>
+ <span class="compare-value">{{ formatPercent(item.rate) }}</span>
+ <span class="compare-icon">{{ Number(item.rate) >= 0 ? '鈫�' : '鈫�' }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js'
+
+const statItems = ref([])
+
+const formatPercent = (val) => {
+ const num = Number(val) || 0
+ return `${num.toFixed(2)}%`
+}
+
+const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
+
+const fetchData = () => {
+ salesPurchaseStorageProductCount()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ statItems.value = res.data.map((item) => ({
+ name: item.name,
+ value: item.value,
+ rate: item.rate,
+ }))
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇閿�鍞�/閲囪喘/鍌ㄥ瓨浜у搧鏁板け璐�:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.stats-cards {
+ display: flex;
+ gap: 30px;
+}
+
+.stat-card {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ background-image: url('@/assets/BI/border@2x.png');
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 142px;
+}
+
+.card-icon {
+ width: 100px;
+ height: 100px;
+ margin: 20px 20px 0 10px;
+}
+
+.card-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.card-value {
+ font-weight: 500;
+ font-size: 40px;
+ background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 19px;
+ color: rgba(208, 231, 255, 0.7);
+}
+
+.card-compare {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 15px;
+ color: #d0e7ff;
+}
+
+.card-compare > span:first-child {
+ font-size: 13px;
+ opacity: 0.8;
+}
+
+.compare-value {
+ font-weight: 600;
+}
+
+.compare-icon {
+ font-size: 14px;
+ position: relative;
+ top: -1px; /* 杞诲井涓婄Щ锛岃绠ご涓庢枃瀛楀瀭鐩村眳涓榻� */
+}
+
+.compare-up .compare-value,
+.compare-up .compare-icon {
+ color: #00c853;
+}
+
+.compare-down .compare-value,
+.compare-down .compare-icon {
+ color: #ff5252;
+}
+
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
new file mode 100644
index 0000000..65a72fe
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/left-bottom.vue
@@ -0,0 +1,262 @@
+<template>
+ <div>
+ <PanelHeader title="閲囪喘鍝佸垎甯�" />
+ <div class="main-panel panel-item-customers">
+ <CarouselCards :items="cardItems" :visible-count="3" />
+ <div class="pie-chart-wrapper" ref="pieWrapperRef">
+ <div class="pie-background" ref="pieBackgroundRef"></div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :legend="landLegend"
+ :series="landSeries"
+ :tooltip="landTooltip"
+ :color="landColors"
+ :options="pieOptions"
+ style="height: 320px"
+ class="land-chart"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from './PanelHeader.vue'
+import CarouselCards from './CarouselCards.vue'
+import { rawMaterialPurchaseAmountRatio } from '@/api/viewIndex.js'
+import { useChartBackground } from '@/hooks/useChartBackground.js'
+
+const pieWrapperRef = ref(null)
+const pieBackgroundRef = ref(null)
+
+/**
+ * @introduction 鎶婃暟缁勪腑key鍊肩浉鍚岀殑閭d竴椤规彁鍙栧嚭鏉ワ紝缁勬垚涓�涓璞�
+ * @param {鍙傛暟绫诲瀷} array 浼犲叆鐨勬暟缁� [{a:"1",b:"2"},{a:"2",b:"3"}]
+ * @param {鍙傛暟绫诲瀷} key 灞炴�у悕 a
+ * @return {杩斿洖绫诲瀷璇存槑}
+ */
+function array2obj(array, key) {
+ const resObj = {}
+ for (let i = 0; i < array.length; i++) {
+ resObj[array[i][key]] = array[i]
+ }
+ return resObj
+}
+
+// 鏁版嵁鍒楄〃锛堟潵鑷帴鍙o級
+const dataList = ref([])
+
+// 鍗$墖鏁版嵁
+const cardItems = ref([])
+
+// 棰滆壊鍒楄〃
+const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
+
+const landObjData = computed(() => array2obj(dataList.value, 'name'))
+
+// 鍥句緥閰嶇疆锛堝彸渚х珫鎺掞級
+const landLegend = computed(() => {
+ const data = dataList.value.map((d, idx) => ({
+ name: d.name,
+ icon: 'circle',
+ textStyle: {
+ fontSize: 18,
+ color: landColors[idx % landColors.length],
+ },
+ }))
+
+ return {
+ orient: 'vertical',
+ top: 'center',
+ left: '52%',
+ itemGap: 30,
+ data: data,
+ formatter: function (name) {
+ const item = landObjData.value[name]
+ if (!item) return name
+ return `{title|${name}}{value|${item.value}}{unit|鍏儅{percent|${item.rate}}{unit|%}`
+ },
+ textStyle: {
+ rich: {
+ value: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 10],
+ },
+ unit: {
+ color: '#82baff',
+ fontSize: 12,
+ fontWeight: 600,
+ padding: [0, 10, 0, 0],
+ },
+ percent: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 0],
+ },
+ title: {
+ fontSize: 12,
+ padding: [0, 0, 0, 0],
+ },
+ },
+ },
+ }
+})
+
+// 鎻愮ず妗�
+const landTooltip = {
+ trigger: 'item',
+ formatter: '{a} <br/>{b} : {c}鍏� ({d}%)',
+}
+
+// 鍙屽眰鐜舰楗煎浘
+const landSeries = ref([
+ {
+ name: '浜у搧閲囪喘閲戦鍒嗘瀽',
+ type: 'pie',
+ radius: ['40%', '60%'],
+ center: ['25%', '50%'],
+ itemStyle: {
+ borderColor: '#0a1c3a',
+ borderWidth: 2,
+ color: function (params) {
+ return landColors[params.dataIndex % landColors.length]
+ },
+ },
+ label: {
+ show: false
+ },
+ minAngle: 15,
+ data: dataList.value,
+ animationType: 'scale',
+ animationEasing: 'elasticOut',
+ animationDelay: function () {
+ return Math.random() * 200
+ },
+ },
+ {
+ // 鍐呭湀
+ type: 'pie',
+ radius: ['40%', '45%'],
+ center: ['25%', '50%'],
+ silent: true,
+ label: {
+ show: false,
+ },
+ labelLine: {
+ show: false,
+ },
+ itemStyle: {
+ color: 'rgba(0, 127, 255, 0.25)',
+ },
+ data: [1],
+ },
+])
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const pieOptions = {
+ backgroundColor: 'transparent',
+ textStyle: { color: '#B8C8E0' },
+}
+
+// 浣跨敤灏佽鐨勮儗鏅綅缃皟鏁存柟娉�
+// 鍥捐〃涓績鏄� ['25%', '50%']锛岃儗鏅渶瑕佸榻愬埌杩欎釜浣嶇疆
+const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
+ wrapperRef: pieWrapperRef,
+ backgroundRef: pieBackgroundRef,
+ left: '25%', // 鍥捐〃涓績 X 鏄� 25%
+ top: '50%', // 鍥捐〃涓績 Y 鏄� 50%
+ offsetX: '-51.5%', // X 杞村亸绉�
+ offsetY: '-50%', // Y 杞村亸绉�
+ watchData: dataList // 鐩戝惉鏁版嵁鍙樺寲锛岃嚜鍔ㄨ皟鏁翠綅缃�
+})
+
+const fetchData = () => {
+ rawMaterialPurchaseAmountRatio()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const items = res.data
+ cardItems.value = items.map((item) => ({
+ label: item.name,
+ value: item.value,
+ unit: '鍏�',
+ rate: item.rate,
+ }))
+ dataList.value = items.map((it) => ({
+ name: it.name,
+ value: parseFloat(it.value) || 0,
+ rate: it.rate,
+ children: [],
+ }))
+ landSeries.value[0].data = dataList.value
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇鍘熸潗鏂欓噰璐噾棰濆崰姣斿け璐�:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+ initBackground()
+})
+
+onBeforeUnmount(() => {
+ cleanupBackground()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+
+.pie-chart-wrapper {
+ position: relative;
+ width: 100%;
+ height: 320px;
+ background: transparent;
+}
+
+
+.pie-background {
+ position: absolute;
+ width: 310px;
+ height: 310px;
+ background-image: url('@/assets/BI/鐜懓鍥捐竟妗�.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ z-index: 1;
+ pointer-events: none;
+ /* 浣嶇疆鐢� JS 鍔ㄦ�佽缃紝榛樿灞呬腑 */
+ left: 25%;
+ top: 50%;
+ transform: translate(-51.5%, -50%);
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue b/src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
new file mode 100644
index 0000000..f5dac31
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/components/left-top.vue
@@ -0,0 +1,227 @@
+<template>
+ <div>
+ <PanelHeader title="閿�鍞搧鍒嗗竷" />
+ <div class="main-panel panel-item-customers">
+ <CarouselCards :items="cardItems" :visible-count="3" />
+ <div class="pie-chart-wrapper" ref="pieWrapperRef">
+ <div class="pie-background" ref="pieBackgroundRef"></div>
+ <Echarts
+ ref="echartsRef"
+ :chartStyle="chartStyle"
+ :legend="pieLegend"
+ :series="pieSeries"
+ :tooltip="pieTooltip"
+ :color="pieColors"
+ :options="pieOptions"
+ style="height: 320px"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
+import { productSalesAnalysis } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+import CarouselCards from './CarouselCards.vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import { useChartBackground } from '@/hooks/useChartBackground.js'
+
+const pieWrapperRef = ref(null)
+const pieBackgroundRef = ref(null)
+
+/**
+ * @introduction 鎶婃暟缁勪腑key鍊肩浉鍚岀殑閭d竴椤规彁鍙栧嚭鏉ワ紝缁勬垚涓�涓璞�
+ * @param {鍙傛暟绫诲瀷} array 浼犲叆鐨勬暟缁� [{a:"1",b:"2"},{a:"2",b:"3"}]
+ * @param {鍙傛暟绫诲瀷} key 灞炴�у悕 a
+ * @return {杩斿洖绫诲瀷璇存槑}
+ */
+function array2obj(array, key) {
+ const resObj = {}
+ for (let i = 0; i < array.length; i++) {
+ resObj[array[i][key]] = array[i]
+ }
+ return resObj
+}
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const echartsRef = ref(null)
+const pieDatas = ref([])
+const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
+
+const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
+
+const pieLegend = computed(() => {
+ const data = pieDatas.value.map((d, idx) => ({
+ name: d.name,
+ icon: 'circle',
+ textStyle: {
+ fontSize: 18,
+ color: pieColors[idx % pieColors.length],
+ },
+ }))
+
+ return {
+ orient: 'vertical',
+ top: 'center',
+ left: '52%',
+ itemGap: 30,
+ data: data,
+ formatter: function (name) {
+ const item = pieObjData.value[name]
+ if (!item) return name
+ return `{title|${name}}{value|${item.value}}{unit|鍏儅{percent|${item.rate}}{unit|%}`
+ },
+ textStyle: {
+ rich: {
+ value: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 10],
+ },
+ unit: {
+ color: '#82baff',
+ fontSize: 12,
+ fontWeight: 600,
+ padding: [0, 10, 0, 0],
+ },
+ percent: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 0],
+ },
+ title: {
+ fontSize: 12,
+ padding: [0, 0, 0, 0],
+ },
+ },
+ },
+ }
+})
+
+const pieTooltip = {
+ trigger: 'item',
+ formatter: '{a} <br/>{b} : {c}鍏� ({d}%)',
+}
+
+const pieSeries = computed(() => [
+ {
+ name: '浜у搧閿�鍞噾棰濆垎鏋�',
+ type: 'pie',
+ radius: '60%',
+ center: ['25%', '50%'],
+ itemStyle: {
+ borderColor: '#0a1c3a',
+ borderWidth: 2,
+ },
+ label: {
+ show: false
+ },
+ minAngle: 15,
+ data: pieDatas.value,
+ animationType: 'scale',
+ animationEasing: 'elasticOut',
+ animationDelay: function () {
+ return Math.random() * 200
+ },
+ },
+])
+
+const pieOptions = {
+ backgroundColor: 'transparent',
+ textStyle: { color: '#B8C8E0' },
+}
+
+const cardItems = ref([])
+
+// 浣跨敤灏佽鐨勮儗鏅綅缃皟鏁存柟娉曪紙涓庡叾浠栨枃浠朵繚鎸佷竴鑷达級
+const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
+ wrapperRef: pieWrapperRef,
+ backgroundRef: pieBackgroundRef,
+ left: '25%', // 鍥捐〃涓績 X 鏄� 25%
+ top: '50%', // 鍥捐〃涓績 Y 鏄� 50%
+ offsetX: '-51.5%', // X 杞村亸绉�
+ offsetY: '-50%', // Y 杞村亸绉�
+ watchData: pieDatas // 鐩戝惉鏁版嵁鍙樺寲锛岃嚜鍔ㄨ皟鏁翠綅缃�
+})
+
+const fetchData = () => {
+ productSalesAnalysis()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const items = res.data
+ cardItems.value = items.map((item) => ({
+ label: item.name,
+ value: item.value,
+ unit: '鍏�',
+ rate: item.rate,
+ }))
+ pieDatas.value = items.map((item) => ({
+ name: item.name,
+ value: parseFloat(item.value) || 0,
+ rate: item.rate,
+ }))
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇浜у搧閿�鍞噾棰濆垎鏋愬け璐�:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+ initBackground()
+})
+
+onBeforeUnmount(() => {
+ cleanupBackground()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+.pie-chart-wrapper {
+ position: relative;
+ width: 100%;
+ height: 320px;
+}
+.pie-background {
+ position: absolute;
+ left: 25%;
+ top: 50%;
+ transform: translate(-51.5%, -50%);
+ width: 310px;
+ height: 310px;
+ background-image: url('@/assets/BI/鐜懓鍥捐竟妗�.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ z-index: 1;
+ pointer-events: none;
+}
+</style>
diff --git a/src/views/reportAnalysis/PSIDataAnalysis/index.vue b/src/views/reportAnalysis/PSIDataAnalysis/index.vue
new file mode 100644
index 0000000..f81e482
--- /dev/null
+++ b/src/views/reportAnalysis/PSIDataAnalysis/index.vue
@@ -0,0 +1,305 @@
+<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">PSI 鏁版嵁鍒嗘瀽</div>
+ </div>
+
+ <!-- 涓昏鍐呭鍖哄煙 -->
+ <div class="dashboard-content">
+ <!-- 宸︿晶鍖哄煙 -->
+ <div class="left-panel">
+ <LeftTop />
+
+ <LeftBottom />
+ </div>
+
+ <!-- 涓棿鍖哄煙 -->
+ <div class="center-panel">
+ <CenterTop />
+ <CenterCenter/>
+ <CenterBottom />
+ </div>
+
+ <!-- 鍙充晶鍖哄煙 -->
+ <div class="right-panel">
+ <RightBottom />
+ <RightTop />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
+import autofit from 'autofit.js'
+import LeftBottom from './components/left-bottom.vue'
+import CenterCenter from './components/center-center.vue'
+import RightTop from '../dataDashboard/components/basic/right-top.vue'
+import RightBottom from '../dataDashboard/components/basic/right-bottom.vue'
+import useUserStore from '@/store/modules/user'
+import LeftTop from './components/left-top.vue'
+import CenterTop from './components/center-top.vue'
+import CenterBottom from './components/center-bottom.vue'
+
+// 鍏ㄥ睆鐩稿叧鐘舵��
+const isFullscreen = ref(false);
+
+// 缂╂斁姣斾緥
+const scaleRatio = ref(1)
+// 璁捐灏哄锛堝熀鍑嗗昂瀵革級- 鏍规嵁瀹為檯璁捐绋胯皟鏁�
+const designWidth = 1920
+const designHeight = 1080
+
+// 鐢ㄦ埛store
+const userStore = useUserStore()
+
+/** 涓� dataDashboard 鍏辩敤娉ㄥ叆鍚嶏紝瀛愮粍浠讹紙鍚鐢ㄧ殑 right-top/right-bottom锛夋瘡鍒嗛挓鍒锋柊 */
+const DASHBOARD_REFRESH_MS = 60 * 1000
+const dataDashboardRefreshTick = ref(0)
+provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
+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 = () => {
+ // 寤惰繜鎵ц锛岀‘淇滵OM鏇存柊瀹屾垚
+ setTimeout(() => {
+ calculateScale()
+ }, 100)
+}
+
+// 鍏ㄥ睆鍔熻兘瀹炵幇 - 閽堝scale-container鍏冪礌
+const toggleFullscreen = () => {
+ const element = document.querySelector('.scale-container')
+
+ if (!element) return
+
+ if (!isFullscreen.value) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen()
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen()
+ }
+ }
+}
+
+// 鐩戝惉鍏ㄥ睆鍙樺寲浜嬩欢
+const handleFullscreenChange = () => {
+ const fullscreenElement = document.fullscreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement
+ isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
+
+ // 鍏ㄥ睆鐘舵�佸彉鍖栨椂锛屽欢杩熼噸鏂拌绠楃缉鏀炬瘮渚嬶紙纭繚DOM鏇存柊瀹屾垚锛�
+ setTimeout(() => {
+ calculateScale()
+ }, 200)
+}
+
+// 鐢熷懡鍛ㄦ湡閽╁瓙
+onMounted(() => {
+ // 浣跨敤nextTick纭繚DOM瀹屽叏娓叉煋鍚庡啀鍒濆鍖�
+ nextTick(() => {
+ // 璁$畻鍒濆缂╂斁姣斾緥
+ calculateScale()
+ })
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('fullscreenchange', handleFullscreenChange)
+ window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.addEventListener('MSFullscreenChange', handleFullscreenChange)
+
+ dashboardPollTimer = setInterval(() => {
+ dataDashboardRefreshTick.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)
+ // 绉婚櫎鎴戜滑娣诲姞鐨刟utofit鍔ㄦ�佽皟鏁寸洃鍚櫒
+ if (window._autofitUpdateHandler) {
+ window.removeEventListener('resize', window._autofitUpdateHandler)
+ delete window._autofitUpdateHandler
+ }
+ // 鍏抽棴autofit
+ autofit.off()
+})
+</script>
+
+<style scoped>
+/* 澶栭儴缂╂斁瀹瑰櫒 - 鍗犳嵁鏁翠釜瑙嗗彛 */
+.scale-container {
+position: relative;
+width: 100%;
+/* 椤甸潰鍦ㄥ父瑙勫竷灞�涓嬶紙鏈夐《鏍忥級榛樿鍑忓幓 84px锛岄伩鍏嶅唴瀹硅瑁佸垏 */
+height: calc(100vh - 84px);
+display: flex;
+align-items: center;
+justify-content: center;
+background-color: #000;
+overflow: hidden;
+}
+
+/* 鍐呴儴鍐呭鍖哄煙 - 鍥哄畾璁捐灏哄 */
+.data-dashboard {
+position: relative;
+width: 1920px;
+height: 1080px;
+background-image: url("@/assets/BI/backImage@2x.png");
+background-size: cover;
+background-position: center;
+background-repeat: no-repeat;
+transform-origin: center center;
+}
+
+/* 鍏ㄥ睆鐘舵�佺殑鏍峰紡 - 浣滅敤浜巗cale-container */
+.scale-container:fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* Webkit娴忚鍣ㄥ墠缂� */
+.scale-container:-webkit-full-screen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* MS娴忚鍣ㄥ墠缂� */
+.scale-container:-ms-fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+
+.dashboard-header {
+position: relative;
+z-index: 1;
+height: 86px;
+background-image: url("@/assets/BI/biaoti.png");
+background-size: cover;
+background-repeat: no-repeat;
+display: flex;
+align-items: center;
+justify-content: center;
+}
+
+.factory-name {
+font-weight: 600;
+font-size: 52px;
+color: #FFFFFF;
+top: 16px;
+position: absolute;
+}
+
+.fullscreen-btn {
+position: absolute;
+top: 10px;
+left: 20px;
+width: 40px;
+height: 40px;
+background: rgba(0, 20, 60, 0.8);
+border: 1px solid rgba(0, 212, 255, 0.3);
+border-radius: 6px;
+color: #00d4ff;
+cursor: pointer;
+display: flex;
+align-items: center;
+justify-content: center;
+transition: all 0.3s;
+z-index: 10000;
+}
+
+.fullscreen-btn:hover {
+background: rgba(0, 30, 90, 0.9);
+border-color: rgba(0, 212, 255, 0.5);
+}
+
+.dashboard-content {
+position: relative;
+z-index: 1;
+display: flex;
+gap: 30px;
+padding: 0 30px;
+height: calc(100% - 86px);
+overflow: hidden;
+}
+
+/* 纭繚鍚勯潰鏉胯兘澶熸纭樉绀� */
+.left-panel, .center-panel, .right-panel {
+overflow: hidden;
+}
+
+.left-panel,
+.right-panel {
+flex: 1;
+display: flex;
+flex-direction: column;
+gap: 24px;
+width: 520px;
+}
+
+.center-panel {
+flex: 1.5;
+display: flex;
+flex-direction: column;
+gap: 20px;
+}
+
+</style>
\ No newline at end of file
diff --git a/src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue b/src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue
new file mode 100644
index 0000000..0c57b25
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/DateTypeSwitch.vue
@@ -0,0 +1,94 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="date-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍛�</el-radio-button>
+ <el-radio-button :label="2">鏈�</el-radio-button>
+ <el-radio-button :label="3">瀛e害</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍛�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+// 鐩戝惉澶栭儴鍊煎彉鍖�
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+// 澶勭悊鍊煎彉鍖�
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.date-type-switch {
+ display: inline-flex;
+}
+
+/* 鏈�変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+/* 绗竴涓寜閽乏渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+/* 鏈�鍚庝竴涓寜閽彸渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+/* 鎸夐挳涔嬮棿鐨勫垎闅旂嚎 */
+.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* 閫変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+/* 鎮仠鏁堟灉 */
+.date-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+/* 閫変腑鐘舵�佹偓鍋� */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue b/src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue
new file mode 100644
index 0000000..313f1df
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/PanelHeader.vue
@@ -0,0 +1,33 @@
+<template>
+ <div class="panel-header">
+ <span class="panel-title">{{ title }}</span>
+ </div>
+</template>
+
+<script setup>
+defineProps({
+ title: {
+ type: String,
+ required: true,
+ default: ''
+ }
+})
+</script>
+
+<style scoped>
+.panel-header {
+ background-image: url("@/assets/BI/kehuhetongback@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.panel-title {
+ width: 100%;
+ font-weight: 500;
+ font-size: 16px;
+ color: #D9ECFF;
+ padding-left: 46px;
+ line-height: 36px;
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue b/src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue
new file mode 100644
index 0000000..c28c2fa
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/basic/center-bottom.vue
@@ -0,0 +1,198 @@
+<template>
+ <div>
+ <PanelHeader title="浜哄憳鍒嗗竷" />
+ <div class="main-panel 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>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed, inject, watch } from 'vue'
+import { deptStaffDistribution } from '@/api/viewIndex.js'
+import PanelHeader from '../PanelHeader.vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import roseBorderImg from '@/assets/BI/鐜懓鍥捐竟妗�.png'
+
+/**
+ * @introduction 鎶婃暟缁勪腑key鍊肩浉鍚岀殑閭d竴椤规彁鍙栧嚭鏉ワ紝缁勬垚涓�涓璞�
+ * @param {鍙傛暟绫诲瀷} array 浼犲叆鐨勬暟缁� [{a:"1",b:"2"},{a:"2",b:"3"}]
+ * @param {鍙傛暟绫诲瀷} key 灞炴�у悕 a
+ * @return {杩斿洖绫诲瀷璇存槑}
+ */
+function array2obj(array, key) {
+ const resObj = {}
+ for (let i = 0; i < array.length; i++) {
+ resObj[array[i][key]] = array[i]
+ }
+ return resObj
+}
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const echartsRef = ref(null)
+const pieDatas = ref([])
+const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
+
+const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
+
+const pieLegend = computed(() => {
+ const data = pieDatas.value.map((d, idx) => ({
+ name: d.name,
+ icon: 'circle',
+ textStyle: {
+ fontSize: 18,
+ color: pieColors[idx % pieColors.length],
+ },
+ }))
+
+ return {
+ orient: 'vertical',
+ top: 'center',
+ left: '50%',
+ itemGap: 30,
+ data: data,
+ formatter: function (name) {
+ const item = pieObjData.value[name]
+ if (!item) return name
+ return `{title|${name}}{value|${item.value}}{unit|浜簘{percent|${item.rate}}{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: pieDatas.value,
+ animationType: 'scale',
+ animationEasing: 'elasticOut',
+ animationDelay: function () {
+ return Math.random() * 200
+ },
+ },
+])
+
+const pieOptions = {
+ backgroundColor: 'transparent',
+ textStyle: { color: '#B8C8E0' },
+}
+
+const getDeptStaffDistribution = () => {
+ deptStaffDistribution().then(res => {
+ if (res.code === 200) {
+ pieDatas.value = res.data.items.map(item => ({
+ name: item.name,
+ value: parseInt(item.value),
+ rate: item.rate
+ }))
+ }
+ }).catch(err => {
+ console.error('鑾峰彇閮ㄩ棬浜哄憳鍒嗗竷鏁版嵁澶辫触:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ getDeptStaffDistribution()
+ })
+}
+
+onMounted(() => {
+ getDeptStaffDistribution()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 370px;
+}
+
+.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;
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue b/src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
new file mode 100644
index 0000000..a7d0174
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/basic/center-top.vue
@@ -0,0 +1,537 @@
+<template>
+ <div>
+ <!-- 椤堕儴缁熻鍗$墖 -->
+ <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">{{ totalStaff }}</span>
+ <div class="card-compare" :class="compareClass(staffYoY)">
+ <span>鍚屾瘮</span>
+ <span class="compare-value">{{ formatPercent(staffYoY) }}</span>
+ <span class="compare-icon">{{ staffYoY >= 0 ? '鈫�' : '鈫�' }}</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">{{ totalCustomers }}</span>
+ <div class="card-compare" :class="compareClass(customersYoY)">
+ <span>鍚屾瘮</span>
+ <span class="compare-value">{{ formatPercent(customersYoY) }}</span>
+ <span class="compare-icon">{{ customersYoY >= 0 ? '鈫�' : '鈫�' }}</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">{{ totalSuppliers }}</span>
+ <div class="card-compare" :class="compareClass(suppliersYoY)">
+ <span>鍚屾瘮</span>
+ <span class="compare-value">{{ formatPercent(suppliersYoY) }}</span>
+ <span class="compare-icon">{{ suppliersYoY >= 0 ? '鈫�' : '鈫�' }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 璁惧缁熻 -->
+ <div class="equipment-stats">
+ <div class="equipment-header">
+ <img
+ src="@/assets/BI/shujutongjiicon@2x.png"
+ alt="鍥炬爣"
+ class="equipment-icon"
+ />
+ <span class="equipment-title">璁惧缁熻</span>
+ </div>
+ <div class="equipment-items">
+ <div class="equipment-item">
+ <span class="equipment-value">{{ equipmentNum }}</span>
+ <span class="equipment-label">璁惧鎬绘暟</span>
+ </div>
+ <div class="equipment-item">
+ <span class="equipment-value">{{ equipmentRepair }}</span>
+ <span class="equipment-label">寰呯淮淇澶�</span>
+ </div>
+ <div class="equipment-item">
+ <span class="equipment-value">{{ equipmentMaintain }}</span>
+ <span class="equipment-label">寰呬繚鍏昏澶�</span>
+ </div>
+ <div class="equipment-item">
+ <span class="equipment-value">{{ totalMeasuring }}</span>
+ <span class="equipment-label">璁¢噺鍣ㄥ叿鎬绘暟</span>
+ </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>
+ </div>
+ <div class="event-content">
+ <ul class="todo-list" v-if="todoList.length > 0" ref="refTodoList">
+ <li v-for="item in todoList" :key="item.id">
+ <div
+ style="
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ width: 100%;
+ gap: 20px;
+ "
+ >
+ <div class="todo-division">寰呭姙浜嬬敱锛歿{ item.approveReason }}</div>
+ <div style="display: flex;justify-content: space-between;align-items: center;"
+ >
+ <div class="todo-title">鐢宠绫诲瀷锛歿{ item.approveTypeName }}</div>
+ <div class="todo-division">鐢宠閮ㄩ棬锛歿{ item.approveDeptName }}</div>
+ <div class="todo-time">{{ item.approveTime }}</div>
+ </div>
+
+ </div>
+ </li>
+ </ul>
+ <div v-else style="text-align: center">鏆傛棤鏁版嵁</div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick, inject, watch } from 'vue'
+import { homeTodos, summaryStatistics } from '@/api/viewIndex.js'
+import { getLedgerPage } from '@/api/equipmentManagement/ledger.js'
+import { getRepairPage } from '@/api/equipmentManagement/repair.js'
+import { getUpkeepPage } from '@/api/equipmentManagement/upkeep.js'
+import { measuringInstrumentListPage } from '@/api/equipmentManagement/measurementEquipment.js'
+
+// 缁熻鏁版嵁
+const totalStaff = ref(0)
+const totalCustomers = ref(0)
+const totalSuppliers = ref(0)
+// 鍚屾瘮
+const staffYoY = ref(0)
+const customersYoY = ref(0)
+const suppliersYoY = ref(0)
+const equipmentNum = ref(0)
+const equipmentRepair = ref(0)
+const equipmentMaintain = ref(0)
+const totalMeasuring = ref(0)
+
+// 寰呭姙浜嬮」
+const todoList = ref([])
+const refTodoList = ref(null)
+
+const formatPercent = (val) => {
+ const num = Number(val) || 0
+ return `${Math.abs(num).toFixed(2)}%`
+}
+
+const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
+
+// 鑾峰彇鍛樺伐銆佸鎴枫�佷緵搴斿晢鏁伴噺
+const getNum = () => {
+ summaryStatistics().then((res) => {
+ totalStaff.value = res.data.totalStaff
+ staffYoY.value = res.data.staffGrowthRate
+ totalCustomers.value = res.data.totalCustomer
+ customersYoY.value = res.data.customerGrowthRate
+ totalSuppliers.value = res.data.totalSupplier
+ suppliersYoY.value = res.data.supplierGrowthRate
+ }).catch(err => {
+ console.error('鑾峰彇鍩虹缁熻鏁版嵁澶辫触:', err)
+ })
+}
+
+// 鑾峰彇璁惧鐩稿叧鏁伴噺
+const getLedgerNum = () => {
+ const params = {
+ pageNum: -1,
+ pageSize: -1,
+ }
+ getLedgerPage(params).then((res) => {
+ equipmentNum.value = res.data.total
+ })
+ getRepairPage({ ...params, status: 0 }).then((res) => {
+ equipmentRepair.value = res.data.total
+ })
+ getUpkeepPage({ ...params, status: 0 }).then((res) => {
+ equipmentMaintain.value = res.data.total
+ })
+ measuringInstrumentListPage(params).then((res) => {
+ totalMeasuring.value = res.data.total
+ })
+}
+
+const destroyTodoListScroll = () => {
+ const todoListEl = refTodoList.value
+ if (todoListEl) {
+ if (todoListEl._animationFrame) {
+ cancelAnimationFrame(todoListEl._animationFrame)
+ todoListEl._animationFrame = null
+ }
+ if (todoListEl._pauseTimer) {
+ clearInterval(todoListEl._pauseTimer)
+ todoListEl._pauseTimer = null
+ }
+ }
+}
+
+// 鍒濆鍖栧緟鍔炰簨椤瑰垪琛ㄦ粴鍔ㄥ姛鑳�
+const initTodoListScroll = () => {
+ destroyTodoListScroll()
+ const todoListEl = refTodoList.value
+ // 寮哄埗鍚敤婊氬姩锛屼笉妫�鏌ヤ换浣曟潯浠�
+ if (todoListEl) {
+ // 鍒涘缓涓�涓厠闅嗛」锛岀敤浜庡疄鐜版棤缂濇粴鍔�
+ const scrollItems = Array.from(todoListEl.querySelectorAll('li'))
+ if (scrollItems.length > 0) {
+ // 纭繚鏈夎冻澶熺殑椤圭洰鐢ㄤ簬婊氬姩
+ // 濡傛灉椤圭洰澶皯锛屽澶嶅埗鍑犳浠ョ‘淇濇粴鍔ㄦ晥鏋�
+ if (scrollItems.length < 4) {
+ const originalItems = [...scrollItems]
+ for (let i = 0; i < 4; i++) {
+ originalItems.forEach((item) => {
+ const clone = item.cloneNode(true)
+ todoListEl.appendChild(clone)
+ })
+ }
+ // 閲嶆柊鑾峰彇鎵�鏈夐」鐩�
+ scrollItems.push(
+ ...Array.from(todoListEl.querySelectorAll('li')).slice(
+ scrollItems.length
+ )
+ )
+ }
+ const itemHeight = scrollItems[0]?.offsetHeight || 0
+ const containerHeight = todoListEl.clientHeight
+ const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
+
+ // 鍏嬮殕鍓嶅嚑涓」鐩苟娣诲姞鍒板垪琛ㄦ湯灏撅紝瀹炵幇鏃犵紳婊氬姩
+ for (let i = 0; i < cloneCount; i++) {
+ const clone = scrollItems[i % scrollItems.length].cloneNode(true)
+ todoListEl.appendChild(clone)
+ }
+
+ let scrollPosition = 0
+ const scrollSpeed = 1.5 // 澧炲姞婊氬姩閫熷害锛屼娇婊氬姩鏇村姞鏄庢樉
+ const pauseTime = 3000 // 婊氬姩鏆傚仠鏃堕棿
+ let isPaused = false
+ let lastTimestamp = 0
+
+ // 杩炵画婊氬姩鍔ㄧ敾鍑芥暟
+ function scrollAnimation(timestamp) {
+ if (!lastTimestamp) lastTimestamp = timestamp
+ const deltaTime = timestamp - lastTimestamp
+ lastTimestamp = timestamp
+
+ if (!isPaused) {
+ scrollPosition += scrollSpeed * (deltaTime / 16) // 鏍囧噯鍖栦负60fps鐨勯�熷害
+
+ // 褰撴粴鍔ㄨ秴杩囧師濮嬪唴瀹归暱搴︽椂锛岄噸缃綅缃疄鐜版棤缂濇粴鍔�
+ const maxScroll = Math.max(
+ todoListEl.scrollHeight -
+ containerHeight -
+ cloneCount * itemHeight,
+ itemHeight * scrollItems.length
+ )
+ if (scrollPosition >= maxScroll) {
+ scrollPosition = 0
+ todoListEl.scrollTop = 0
+ } else {
+ todoListEl.scrollTop = scrollPosition
+ }
+ }
+
+ todoListEl._animationFrame = requestAnimationFrame(scrollAnimation)
+ }
+
+ // 鍚姩婊氬姩鍔ㄧ敾
+ todoListEl._animationFrame = requestAnimationFrame(scrollAnimation)
+
+ // 璁剧疆婊氬姩-鏆傚仠-婊氬姩鐨勫惊鐜晥鏋�
+ const pauseTimer = setInterval(() => {
+ isPaused = !isPaused
+ }, pauseTime)
+
+ // 娓呯悊瀹氭椂鍣�
+ todoListEl._pauseTimer = pauseTimer
+ }
+ }
+}
+
+// 寰呭姙浜嬮」
+const todoInfoS = () => {
+ destroyTodoListScroll()
+ homeTodos().then((res) => {
+ todoList.value = res.data
+ // 鍦ㄨ幏鍙栧埌寰呭姙浜嬮」鏁版嵁鍚庯紝鍒濆鍖栨粴鍔ㄥ姛鑳�
+ nextTick(() => {
+ initTodoListScroll()
+ })
+ })
+}
+
+const refreshCenterTopData = () => {
+ getNum()
+ getLedgerNum()
+ todoInfoS()
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ refreshCenterTopData()
+ })
+}
+
+onMounted(() => {
+ refreshCenterTopData()
+})
+
+onBeforeUnmount(() => {
+ destroyTodoListScroll()
+})
+</script>
+
+<style scoped>
+.stats-cards {
+ display: flex;
+ gap: 30px;
+}
+
+.stat-card {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ background-image: url('@/assets/BI/border@2x.png');
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 142px;
+}
+
+.card-icon {
+ width: 100px;
+ height: 100px;
+ margin: 20px 20px 0 10px;
+}
+
+.card-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.card-value {
+ font-weight: 500;
+ font-size: 40px;
+ background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 19px;
+ color: rgba(208, 231, 255, 0.7);
+}
+
+.card-compare {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 15px;
+ color: #d0e7ff;
+}
+
+.card-compare > span:first-child {
+ font-size: 13px;
+ opacity: 0.8;
+}
+
+.compare-value {
+ font-weight: 600;
+}
+
+.compare-icon {
+ font-size: 14px;
+ position: relative;
+ top: -1px; /* 杞诲井涓婄Щ锛岃绠ご涓庢枃瀛楀瀭鐩村眳涓榻� */
+}
+
+.compare-up .compare-value,
+.compare-up .compare-icon {
+ color: #00c853;
+}
+
+.compare-down .compare-value,
+.compare-down .compare-icon {
+ color: #ff5252;
+}
+
+.equipment-stats {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ height: 240px;
+ padding-top: 0px;
+}
+
+.equipment-header {
+ font-weight: 500;
+ font-size: 21px;
+ display: flex;
+ border-bottom: 1px solid;
+ border-image: linear-gradient(
+ 270deg,
+ rgba(0, 126, 255, 0) 0%,
+ rgba(0, 126, 255, 0.4549) 35%,
+ #007eff 78%,
+ #007eff 100%
+ )
+ 1;
+ padding-bottom: 2px;
+}
+
+.equipment-title {
+ font-weight: 500;
+ font-size: 18px;
+ background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ line-height: 50px;
+}
+
+.equipment-icon {
+ width: 50px;
+ height: 50px;
+}
+
+.equipment-items {
+ display: flex;
+ justify-content: space-around;
+ gap: 30px;
+}
+
+.equipment-item {
+ text-align: center;
+}
+
+.equipment-value {
+ display: block;
+ font-weight: 500;
+ font-size: 40px;
+ color: #ffffff;
+ width: 120px;
+ height: 110px;
+ line-height: 110px;
+ background-image: url('@/assets/BI/shujutongji@2x.png');
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ margin-bottom: 8px;
+}
+
+.equipment-label {
+ font-weight: 500;
+ font-size: 16px;
+ color: #fffffe;
+}
+
+.event-info {
+ background-image: url('@/assets/BI/shijianmingchengbeijing@2x.png');
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ padding: 20px;
+ padding-top: 10px;
+ height: 186px;
+}
+
+.event-header {
+ display: flex;
+ align-items: center;
+}
+
+.event-icon {
+ width: 40px;
+ height: 40px;
+}
+
+.event-title {
+ font-weight: 500;
+ font-size: 18px;
+ color: #fffffe;
+ line-height: 30px;
+}
+
+.todo-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ height: 120px; /* 鎸夌敤鎴疯姹傝皟鏁撮珮搴� */
+ overflow: hidden;
+ font-size: 15px;
+}
+
+.todo-list li {
+ border-radius: 8px;
+ margin-bottom: 12px;
+ padding: 12px 40px;
+ height: 74px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.todo-title {
+ font-weight: 400;
+ font-size: 16px;
+ color: #fffffe;
+ position: relative;
+}
+
+
+
+.todo-division {
+ font-weight: 400;
+ font-size: 16px;
+ color: #fffffe;
+ position: relative;
+}
+
+.todo-division::before {
+ content: '';
+ position: absolute;
+ left: -20px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 6px;
+ height: 6px;
+ background: #498ceb;
+ border-radius: 50%;
+}
+
+.todo-time {
+ font-weight: 400;
+ font-size: 16px;
+ color: #fffffe;
+ background: rgba(24, 93, 190, 0.4);
+border-radius: 5px 5px 5px 5px;
+padding: 5px 10px;
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue b/src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue
new file mode 100644
index 0000000..bab6024
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue
@@ -0,0 +1,267 @@
+<template>
+ <div>
+ <PanelHeader title="瀹㈡埛钀ユ敹璐$尞鏁板�煎垎鏋�" />
+ <div class="main-panel panel-item-customers">
+ <div class="filters-row">
+ <el-select
+ v-model="customerValue"
+ class="customer-select"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ clearable
+ filterable
+ popper-class="customer-select-dropdown"
+ :teleported="false"
+ @change="handleFilterChange"
+ >
+ <el-option
+ v-for="item in customerOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+
+ <DateTypeSwitch v-model="dateType" @change="handleFilterChange" />
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="barSeries1"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from '../PanelHeader.vue'
+import DateTypeSwitch from '../DateTypeSwitch.vue'
+import { customerRevenueAnalysis } from '@/api/viewIndex.js'
+import { listCustomer } from '@/api/basicData/customer.js'
+
+const dateType = ref(1) // 1=鍛� 2=鏈� 3=瀛e害
+const customerValue = ref(null)
+const customerOptions = ref([])
+
+// 钀ユ敹鍒嗘瀽鏁版嵁
+const revenueData = ref({
+ items: []
+})
+
+const chartStyle = {
+ width: '100%',
+ height: '150%',
+}
+
+const grid = {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true,
+}
+
+const barLegend = {
+ show: false,
+ textStyle: { color: '#B8C8E0' },
+ data: ['钀ユ敹'],
+}
+
+const barSeries1 = ref([
+ {
+ name: '钀ユ敹',
+ type: 'bar',
+ barGap: 0,
+ emphasis: {
+ focus: 'series',
+ },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 1,
+ x2: 0,
+ y2: 0,
+ colorStops: [
+ // linear-gradient(360deg, rgba(0,164,237,0) 0%, #4EE4FF 100%)
+ { offset: 0, color: 'rgba(0,164,237,0)' },
+ { offset: 1, color: '#4EE4FF' },
+ ],
+ },
+ },
+ data: [],
+ },
+])
+
+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 xAxis1 = ref([
+ {
+ type: 'category',
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0' },
+ data: [],
+ },
+])
+
+const yAxis1 = [
+ {
+ type: 'value',
+ axisLabel: { color: '#B8C8E0' },
+ },
+]
+
+// 鑾峰彇瀹㈡埛钀ユ敹鍒嗘瀽鏁版嵁
+const getCustomerRevenueAnalysis = () => {
+ if (customerOptions.value.length > 0 && !customerValue.value) {
+ // 榛樿閫変腑绗竴涓鎴�
+ customerValue.value = customerOptions.value[0].value
+ }
+
+ if (!customerValue.value) return
+
+ const params = {
+ customerId: customerValue.value,
+ type: dateType.value
+ }
+
+ customerRevenueAnalysis(params)
+ .then((res) => {
+ xAxis1.value[0].data = []
+ barSeries1.value[0].data = []
+
+ const items = res.data?.items || []
+ items.forEach((item) => {
+ xAxis1.value[0].data.push(item.name)
+ barSeries1.value[0].data.push(item.value)
+ })
+ revenueData.value = res.data
+ })
+ .catch((error) => {
+ console.error('鑾峰彇瀹㈡埛钀ユ敹鍒嗘瀽澶辫触:', error)
+ })
+}
+
+const fetchCustomerOptions = async () => {
+ try {
+ const params = { pageNum: 1, pageSize: 200 }
+ const res = await listCustomer(params)
+ const records = res?.records || res?.data?.records || res?.rows || []
+ customerOptions.value = records.map((r) => ({
+ label: r.customerName || r.name || r.customer || '-',
+ value: r.id ?? r.customerId ?? r.customerCode ?? r.customerName,
+ }))
+
+ // 鑾峰彇鍒伴�夐」鍚庯紝濡傛灉杩樻病閫変腑锛岄粯璁ら�変腑绗竴涓�
+ if (customerOptions.value.length > 0 && !customerValue.value) {
+ customerValue.value = customerOptions.value[0].value
+ getCustomerRevenueAnalysis()
+ }
+ } catch (e) {
+ // 鎺ュ彛寮傚父鏃剁粰涓�缁勬ā鎷熷鎴凤紝淇濊瘉UI鍙敤
+ customerOptions.value = [
+ { label: '鍗庝笢绮惧瘑', value: '鍗庝笢绮惧瘑' },
+ { label: '鏄熻景鐢靛瓙', value: '鏄熻景鐢靛瓙' },
+ { label: '鍚埅绉戞妧', value: '鍚埅绉戞妧' },
+ { label: '閾瘹鍒堕��', value: '閾瘹鍒堕��' },
+ { label: '杩滄櫙鏉愭枡', value: '杩滄櫙鏉愭枡' },
+ ]
+ }
+}
+
+const handleFilterChange = () => {
+ getCustomerRevenueAnalysis()
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ getCustomerRevenueAnalysis()
+ })
+}
+
+onMounted(() => {
+ fetchCustomerOptions()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.filters-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.customer-select {
+ width: 180px;
+}
+
+/* 涓嬫媺妗嗛鏍硷細涓� DateTypeSwitch 淇濇寔涓�鑷达紙娣辫壊鍗婇�忔槑銆佹祬鑹叉枃瀛椼�佺粏杈规锛� */
+.customer-select :deep(.el-input__wrapper),
+.customer-select :deep(.el-select__wrapper) {
+ background-color: rgba(26, 88, 176, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ box-shadow: none;
+}
+
+.customer-select :deep(.el-input__inner) {
+ color: rgba(184, 200, 224, 0.9);
+}
+
+.customer-select :deep(.el-input__inner::placeholder) {
+ color: rgba(184, 200, 224, 0.6);
+}
+
+.customer-select :deep(.el-select__caret),
+.customer-select :deep(.el-icon) {
+ color: rgba(184, 200, 224, 0.8);
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 478px;
+}
+</style>
+
+<style>
+/* 鍏ㄥ睆妯″紡涓嬩笅鎷夋灞傜骇淇 */
+.customer-select-dropdown {
+ z-index: 10001 !important;
+}
+
+/* 纭繚鍦ㄥ叏灞忓鍣ㄥ唴鐨勪笅鎷夋涔熻兘姝e父鏄剧ず */
+.scale-container:fullscreen .customer-select-dropdown,
+.scale-container:-webkit-full-screen .customer-select-dropdown,
+.scale-container:-ms-fullscreen .customer-select-dropdown {
+ z-index: 10001 !important;
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue b/src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
new file mode 100644
index 0000000..16791de
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/basic/left-top.vue
@@ -0,0 +1,257 @@
+<template>
+ <div>
+ <PanelHeader title="浜у搧澶х被" />
+ <div class="panel-item-customers">
+ <div class="pie-chart-wrapper" ref="pieWrapperRef">
+ <div class="pie-background" ref="pieBackgroundRef"></div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :legend="landLegend"
+ :series="landSeries"
+ :tooltip="landTooltip"
+ :color="landColors"
+ :options="pieOptions"
+ style="height: 100%"
+ class="land-chart"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, inject, watch } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from '../PanelHeader.vue'
+import { productCategoryDistribution } from '@/api/viewIndex.js'
+import { useChartBackground } from '@/hooks/useChartBackground.js'
+
+const pieWrapperRef = ref(null)
+const pieBackgroundRef = ref(null)
+const chart = ref(null)
+
+// 鏁版嵁鍒楄〃锛堟潵鑷帴鍙o級
+const dataList = ref([])
+
+// 棰滆壊鍒楄〃
+const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
+
+// label 瀵屾枃鏈細涓烘瘡涓鑹茬敓鎴愪竴涓皬鍦嗙偣鏍峰紡锛堢‘淇濆湪 label 涓彲瑙侊級
+const dotRich = landColors.reduce((acc, color, idx) => {
+ acc[`dot${idx}`] = {
+ width: 8,
+ height: 8,
+ borderRadius: 8,
+ backgroundColor: color,
+ align: 'center',
+ }
+ return acc
+}, {})
+
+// 鍥句緥閰嶇疆锛堝彸渚х珫鎺掞級
+const landLegend = {
+ show: false,
+ icon: 'circle',
+ data: [],
+ right: '8%',
+ top: '40%',
+ orient: 'vertical',
+ itemGap: 14,
+ itemWidth: 6,
+ itemHeight: 6,
+ textStyle: {
+ fontSize: 12,
+ rich: {
+ unit: {
+ color: '#fff',
+ fontSize: 12,
+ padding: [0, 10, 0, 0],
+ },
+ text: {
+ width: 60,
+ color: '#fff',
+ fontSize: 12,
+ },
+ },
+ },
+ formatter: function (name) {
+ const list = dataList.value || []
+ const item = list.find((d) => d.name === name)
+ if (!item) return name
+ const val = Number(item.value || 0)
+ const totalValue = list.reduce((sum, it) => sum + Number(it.value || 0), 0)
+ const percent = totalValue ? ((val / totalValue) * 100).toFixed(2) : '0.00'
+ return `{text|${name}}${val}{unit| 鍏》}${percent}{unit|%}`
+ },
+}
+
+// 鎻愮ず妗�
+const landTooltip = {
+ // triggerOn: 'hover',
+ alwaysShowContent: true,
+ position: function (pt) {
+ return [pt[0], 130]
+ },
+ formatter: function (params) {
+ return `${params.name} (${params.value}绫�)`
+ },
+}
+
+// 鍙屽眰鐜舰楗煎浘
+const landSeries = ref([
+ {
+ 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 rawVal = params?.data?.value
+ const parentValue = typeof rawVal === 'number' && !Number.isNaN(rawVal) ? rawVal : (Number(rawVal) || 0)
+ const dotKey = `dot${(params?.dataIndex || 0) % landColors.length}`
+ const dot = `{${dotKey}|} `
+ const parentLine = `${dot}{parent|${parentName} (${parentValue}绫�)}`
+ if (!children.length) return parentLine
+ // 鐖剁骇鍏ㄩ儴鏄剧ず锛涘瓙绾ф渶澶� 5 涓紝瓒呭嚭鏄剧ず鐪佺暐鍙�
+ const displayed = children.slice(0, 5).map((c) => `{child|${c.name}}`)
+ if (children.length > 5) displayed.push('{child|鈥')
+ return [parentLine, ...displayed].join('\n')
+ },
+ },
+ labelLine: {
+ show: true,
+ length: 20,
+ length2: 20,
+ lineStyle: {
+ color: '#B8C8E0',
+ },
+ },
+ itemStyle: {
+ color: function (params) {
+ return landColors[params.dataIndex % landColors.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%', // X 杞村亸绉伙紝鍙姩鎬佽皟鏁�
+ offsetY: '-39%', // Y 杞村亸绉伙紝鍙姩鎬佽皟鏁�
+ watchData: dataList // 鐩戝惉鏁版嵁鍙樺寲锛岃嚜鍔ㄨ皟鏁翠綅缃�
+})
+
+const loadData = async () => {
+ try {
+ const res = await productCategoryDistribution()
+ const items = res?.data?.items || []
+ dataList.value = items.map((it) => ({
+ name: it.name,
+ value: Number(it.value || 0),
+ rate: it.rate,
+ children: Array.isArray(it.children) ? it.children : [],
+ }))
+ landLegend.data = dataList.value.map((d) => d.name)
+ landSeries.value[0].data = dataList.value
+ // 鏁版嵁鍔犺浇瀹屾垚鍚庤皟鏁磋儗鏅綅缃�
+ adjustBackgroundPosition()
+ } catch (e) {
+ console.error('鑾峰彇浜у搧澶х被鍒嗗竷澶辫触:', e)
+ dataList.value = []
+ landLegend.data = []
+ landSeries.value[0].data = []
+ }
+}
+
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ loadData()
+ })
+}
+
+onMounted(() => {
+ loadData()
+ initBackground()
+})
+
+onBeforeUnmount(() => {
+ cleanupBackground()
+})
+</script>
+
+<style scoped>
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 420px;
+}
+
+.pie-chart-wrapper {
+ position: relative;
+ width: 100%;
+ height: 320px;
+ 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;
+ /* 榛樿灞呬腑锛屼細鍦� JS 涓姩鎬佽皟鏁� */
+ left: 50%;
+ top: 50%;
+ transform: translate(-51.5%, -39%);
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue b/src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
new file mode 100644
index 0000000..6fcd80f
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/basic/right-bottom.vue
@@ -0,0 +1,336 @@
+<template>
+ <div>
+ <PanelHeader title="瀹㈡埛璐$尞鎺掑悕" />
+ <div class="panel-item-customers">
+ <div class="switch-container">
+ <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="{ show: false }"
+ :series="series"
+ :tooltip="tooltip"
+ :xAxis="xAxis"
+ :yAxis="yAxis"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 360px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed, inject, watch } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from '../PanelHeader.vue'
+import DateTypeSwitch from '../DateTypeSwitch.vue'
+import { customerContributionRanking } from '@/api/viewIndex.js'
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const dateType = ref(1) // 1=鍛� 2=鏈� 3=瀛e害
+
+// 椋炴満鍥炬爣 SVG path锛堜笌 right-top 涓�鑷达級
+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'
+
+// 棰滆壊閰嶇疆锛堜笌 right-top 涓�鑷达級
+const color = {
+ 0: '#ff5676',
+ 1: '#ffd83e',
+ 2: '#fbff94',
+ 3: '#7daeff',
+}
+
+// 鍘熷鏁版嵁锛堢粺涓�鎴� { NAME, NUM }锛�
+const dataArr = ref([])
+
+const dataArray = computed(() => {
+ const sortedAsc = [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
+ return sortedAsc.length > 5 ? sortedAsc.slice(-5) : sortedAsc
+})
+
+const total = computed(() => dataArray.value.reduce((sum, v) => sum + Number(v.NUM || 0), 0))
+
+const xdataName = computed(() => dataArray.value.map((v) => v.NAME))
+
+const dataNum = computed(() => {
+ return dataArray.value.map((v, i) => {
+ const index = dataArray.value.length - i - 1
+ const isTop3 = index < 3
+
+ return {
+ value: Number(v.NUM),
+ 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(0, ...dataNum.value.map((v) => v.value))
+ return dataNum.value.map(() => maxValue + 200)
+})
+
+const tooltip = computed(() => ({
+ trigger: 'axis',
+ textStyle: { fontSize: '100%' },
+ formatter: function (params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ result += `<div>${item.marker} ${item.seriesName}: ${item.value}鍏�</div>`
+ })
+ return result
+ },
+}))
+
+const grid = computed(() => ({ top: 0, left: '20%', right: '10%', bottom: 0 }))
+
+const xAxis = computed(() => [
+ {
+ splitLine: { show: false },
+ axisLine: { show: false },
+ axisLabel: { show: false },
+ axisTick: { show: false },
+ },
+])
+
+const yAxis = computed(() => [
+ {
+ type: 'category',
+ inverse: false,
+ data: xdataName.value,
+ 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: dataNum.value.map((item) => item.value),
+ axisLabel: {
+ formatter: (params, index) => {
+ const value = typeof params === 'object' ? params.value : params
+ const percent = total.value ? ((value / total.value) * 100).toFixed(0) : 0
+ const rank = dataArray.value.length - index
+ const isTop3 = rank < 4
+
+ return `{a${isTop3 ? rank : ''}|${percent} }{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: 25,
+ tooltip: { show: false },
+ itemStyle: {
+ color: 'rgba(255,255,255,.1)',
+ barBorderRadius: [0, 20, 20, 0],
+ },
+ data: bgData.value,
+ },
+ {
+ name: '閲戦娓愬彉',
+ type: 'bar',
+ barWidth: 25,
+ 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 normalizeItem = (item) => {
+ const name =
+ item?.NAME ??
+ item?.name ??
+ item?.customerName ??
+ item?.customer ??
+ item?.label ??
+ '-'
+
+ const num =
+ item?.NUM ??
+ item?.num ??
+ item?.value ??
+ item?.amount ??
+ item?.money ??
+ 0
+
+ return { NAME: String(name), NUM: Number(num) || 0 }
+}
+
+const getMockListByType = (type) => {
+ // 妯℃嫙鍋囨暟鎹紙閲戦璐$尞鎺掑悕锛�
+ // type: 1=鍛� 2=鏈� 3=瀛e害
+ if (type === 2) {
+ return [
+ { NAME: '鍗庝笢绮惧瘑', NUM: 5120000 },
+ { NAME: '鏄熻景鐢靛瓙', NUM: 3860000 },
+ { NAME: '鍚埅绉戞妧', NUM: 2720000 },
+ { NAME: '閾瘹鍒堕��', NUM: 2160000 },
+ { NAME: '杩滄櫙鏉愭枡', NUM: 1430000 },
+ { NAME: '寰锋鼎璐告槗', NUM: 910000 },
+ { NAME: '瀹忚揪閰嶅', NUM: 680000 },
+ ]
+ }
+ if (type === 3) {
+ return [
+ { NAME: '鍗庝笢绮惧瘑', NUM: 16800000 },
+ { NAME: '鏄熻景鐢靛瓙', NUM: 12960000 },
+ { NAME: '鍚埅绉戞妧', NUM: 9720000 },
+ { NAME: '閾瘹鍒堕��', NUM: 7560000 },
+ { NAME: '杩滄櫙鏉愭枡', NUM: 5430000 },
+ { NAME: '寰锋鼎璐告槗', NUM: 3910000 },
+ { NAME: '瀹忚揪閰嶅', NUM: 2680000 },
+ ]
+ }
+ return [
+ { NAME: '鍗庝笢绮惧瘑', NUM: 1280000 },
+ { NAME: '鏄熻景鐢靛瓙', NUM: 860000 },
+ { NAME: '鍚埅绉戞妧', NUM: 720000 },
+ { NAME: '閾瘹鍒堕��', NUM: 560000 },
+ { NAME: '杩滄櫙鏉愭枡', NUM: 430000 },
+ { NAME: '寰锋鼎璐告槗', NUM: 310000 },
+ { NAME: '瀹忚揪閰嶅', NUM: 180000 },
+ ]
+}
+
+const setMockData = (type) => {
+ dataArr.value = getMockListByType(type).map(normalizeItem)
+}
+
+const fetchCustomerRanking = () => {
+ customerContributionRanking({ type: dateType.value })
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ dataArr.value = res.data.map(item => ({
+ NAME: item.customerName,
+ NUM: item.totalAmount
+ }))
+ } else {
+ setMockData(dateType.value)
+ }
+ })
+ .catch((error) => {
+ console.error('鑾峰彇瀹㈡埛閲戦璐$尞鎺掑悕澶辫触:', error)
+ setMockData(dateType.value)
+ })
+}
+
+const handleDateTypeChange = () => {
+ fetchCustomerRanking()
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchCustomerRanking()
+ })
+}
+
+onMounted(() => {
+ fetchCustomerRanking()
+})
+</script>
+
+<style scoped>
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+
+.switch-container {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 16px;
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue b/src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
new file mode 100644
index 0000000..96e4548
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/components/basic/right-top.vue
@@ -0,0 +1,365 @@
+<template>
+ <div>
+ <PanelHeader title="渚涘簲鍟嗛噰璐帓鍚�" />
+ <div class="panel-item-customers">
+ <div class="switch-container">
+ <DateTypeSwitch v-model="radio1" @change="handleDateTypeChange" />
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :series="series"
+ :tooltip="tooltip"
+ :xAxis="xAxis"
+ :yAxis="yAxis"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 360px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed, inject, watch } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from '../PanelHeader.vue'
+import DateTypeSwitch from '../DateTypeSwitch.vue'
+import { supplierPurchaseRanking } from '@/api/viewIndex.js'
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const radio1 = ref(1)
+
+// 椋炴満鍥炬爣 SVG path
+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 dataArr = ref([])
+
+// 鎺掑簭鍚庣殑鏁版嵁
+const dataArray = computed(() => {
+ return [...dataArr.value].sort((a, b) => a.NUM - b.NUM)
+})
+
+// 璁$畻鎬绘暟
+const total = computed(() => {
+ return dataArray.value.reduce((sum, v) => sum + Number(v.NUM), 0)
+})
+
+// x杞存暟鎹紙鍚嶇О锛�
+const xdataName = computed(() => {
+ return dataArray.value.map((v) => v.NAME)
+})
+
+// y杞存暟鎹紙鏁板�硷紝甯︽牱寮忥級
+const dataNum = computed(() => {
+ return dataArray.value.map((v, i) => {
+ const index = dataArray.value.length - i - 1
+ const isTop3 = index < 3
+
+ return {
+ value: Number(v.NUM),
+ 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(...dataNum.value.map((v) => v.value))
+ return dataNum.value.map(() => maxValue + 200)
+})
+
+// tooltip
+const tooltip = computed(() => {
+ return {
+ trigger: 'axis',
+ textStyle: { fontSize: '100%' },
+ formatter: function (params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ result += `<div>${params[0].marker}${params[0].value}鍏�</div>`
+ return result
+ },
+ }
+})
+
+// grid
+const grid = computed(() => {
+ return { top: 0, left: '20%', right: '10%', bottom: 0 }
+})
+
+// xAxis
+const xAxis = computed(() => {
+ return [
+ {
+ splitLine: { show: false },
+ axisLine: { show: false },
+ axisLabel: { show: false },
+ axisTick: { show: false },
+ },
+ ]
+})
+
+// yAxis
+const yAxis = computed(() => {
+ return [
+ {
+ type: 'category',
+ inverse: false,
+ data: xdataName.value,
+ 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))
+ }
+ // 澶氳鏂囨湰锛屾瘡琛岄兘濂楀悓涓�涓� rich 鏍峰紡
+ 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: dataNum.value.map((item) => item.value),
+ axisLabel: {
+ formatter: (params, index) => {
+ const value = typeof params === 'object' ? params.value : params
+ const percent = ((value / total.value) * 100).toFixed(0)
+ const rank = dataArray.value.length - index
+ const isTop3 = rank < 4
+
+ return `{a${isTop3 ? rank : ''}|${percent} }{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 },
+ },
+ ]
+})
+
+// series
+const series = computed(() => {
+ return [
+ {
+ z: 6,
+ type: 'pictorialBar',
+ data: dataNum.value,
+ },
+ {
+ z: 6,
+ type: 'bar',
+ barWidth: 25,
+ tooltip: { show: false },
+ itemStyle: {
+ color: 'rgba(255,255,255,.1)',
+ barBorderRadius: [0, 20, 20, 0],
+ },
+ data: bgData.value,
+ },
+ {
+ type: 'bar',
+ barWidth: 25,
+ 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 fetchSupplierRanking = () => {
+ supplierPurchaseRanking({ type: radio1.value })
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ dataArr.value = res.data.map(item => ({
+ NAME: item.supplierName,
+ NUM: item.totalAmount
+ }))
+ } else {
+ // 濡傛灉娌℃湁鏁版嵁锛屼娇鐢ㄦā鎷熸暟鎹�
+ dataArr.value = [
+ { NAME: '渚涘簲鍟咥', NUM: 102 },
+ { NAME: '渚涘簲鍟咮', NUM: 122 },
+ { NAME: '渚涘簲鍟咰', NUM: 282 },
+ { NAME: '渚涘簲鍟咲', NUM: 453 },
+ { NAME: '渚涘簲鍟咵', NUM: 753 },
+ ]
+ }
+ })
+ .catch((error) => {
+ console.error('鑾峰彇渚涘簲鍟嗛噰璐帓鍚嶅け璐�:', error)
+ // 浣跨敤妯℃嫙鏁版嵁
+ dataArr.value = [
+ { NAME: '渚涘簲鍟咥', NUM: 102 },
+ { NAME: '渚涘簲鍟咮', NUM: 122 },
+ { NAME: '渚涘簲鍟咰', NUM: 282 },
+ { NAME: '渚涘簲鍟咲', NUM: 453 },
+ { NAME: '渚涘簲鍟咵', NUM: 753 },
+ ]
+ })
+}
+
+// 澶勭悊鏃ユ湡绫诲瀷鍒囨崲
+const handleDateTypeChange = (value) => {
+ fetchSupplierRanking()
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchSupplierRanking()
+ })
+}
+
+onMounted(() => {
+ fetchSupplierRanking()
+})
+</script>
+
+<style scoped>
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+
+.switch-container {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 16px;
+}
+
+.section-title {
+ font-weight: 500;
+ font-size: 16px;
+ color: #d9ecff;
+}
+</style>
diff --git a/src/views/reportAnalysis/dataDashboard/index.vue b/src/views/reportAnalysis/dataDashboard/index.vue
new file mode 100644
index 0000000..ff53a7b
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/index.vue
@@ -0,0 +1,305 @@
+<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 autofit from 'autofit.js'
+import LeftTop from './components/basic/left-top.vue'
+import LeftBottom from './components/basic/left-bottom.vue'
+import CenterTop from './components/basic/center-top.vue'
+import CenterBottom from './components/basic/center-bottom.vue'
+import RightTop from './components/basic/right-top.vue'
+import RightBottom from './components/basic/right-bottom.vue'
+import useUserStore from '@/store/modules/user'
+
+// 鍏ㄥ睆鐩稿叧鐘舵��
+const isFullscreen = ref(false);
+
+// 缂╂斁姣斾緥
+const scaleRatio = ref(1)
+// 璁捐灏哄锛堝熀鍑嗗昂瀵革級- 鏍规嵁瀹為檯璁捐绋胯皟鏁�
+const designWidth = 1920
+const designHeight = 1080
+
+// 鐢ㄦ埛store
+const userStore = useUserStore()
+
+// 澶у睆鎺ュ彛杞闂撮殧
+const DASHBOARD_REFRESH_MS = 60 * 1000
+const dataDashboardRefreshTick = ref(0)
+provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
+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 = () => {
+ // 寤惰繜鎵ц锛岀‘淇滵OM鏇存柊瀹屾垚
+ setTimeout(() => {
+ calculateScale()
+ }, 100)
+}
+
+// 鍏ㄥ睆鍔熻兘瀹炵幇 - 閽堝scale-container鍏冪礌
+const toggleFullscreen = () => {
+ const element = document.querySelector('.scale-container')
+
+ if (!element) return
+
+ if (!isFullscreen.value) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen()
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen()
+ }
+ }
+}
+
+// 鐩戝惉鍏ㄥ睆鍙樺寲浜嬩欢
+const handleFullscreenChange = () => {
+ const fullscreenElement = document.fullscreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement
+ isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
+
+ // 鍏ㄥ睆鐘舵�佸彉鍖栨椂锛屽欢杩熼噸鏂拌绠楃缉鏀炬瘮渚嬶紙纭繚DOM鏇存柊瀹屾垚锛�
+ setTimeout(() => {
+ calculateScale()
+ }, 200)
+}
+
+// 鐢熷懡鍛ㄦ湡閽╁瓙
+onMounted(() => {
+ // 浣跨敤nextTick纭繚DOM瀹屽叏娓叉煋鍚庡啀鍒濆鍖�
+ nextTick(() => {
+ // 璁$畻鍒濆缂╂斁姣斾緥
+ calculateScale()
+ })
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('fullscreenchange', handleFullscreenChange)
+ window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.addEventListener('MSFullscreenChange', handleFullscreenChange)
+
+ dashboardPollTimer = setInterval(() => {
+ dataDashboardRefreshTick.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)
+ // 绉婚櫎鎴戜滑娣诲姞鐨刟utofit鍔ㄦ�佽皟鏁寸洃鍚櫒
+ if (window._autofitUpdateHandler) {
+ window.removeEventListener('resize', window._autofitUpdateHandler)
+ delete window._autofitUpdateHandler
+ }
+ // 鍏抽棴autofit
+ autofit.off()
+})
+</script>
+
+<style scoped>
+/* 澶栭儴缂╂斁瀹瑰櫒 - 鍗犳嵁鏁翠釜瑙嗗彛 */
+.scale-container {
+position: relative;
+width: 100%;
+/* 椤甸潰鍦ㄥ父瑙勫竷灞�涓嬶紙鏈夐《鏍忥級榛樿鍑忓幓 84px锛岄伩鍏嶅唴瀹硅瑁佸垏 */
+height: calc(100vh - 84px);
+display: flex;
+align-items: center;
+justify-content: center;
+background-color: #000;
+overflow: hidden;
+}
+
+/* 鍐呴儴鍐呭鍖哄煙 - 鍥哄畾璁捐灏哄 */
+.data-dashboard {
+position: relative;
+width: 1920px;
+height: 1080px;
+background-image: url("@/assets/BI/backImage@2x.png");
+background-size: cover;
+background-position: center;
+background-repeat: no-repeat;
+transform-origin: center center;
+}
+
+/* 鍏ㄥ睆鐘舵�佺殑鏍峰紡 - 浣滅敤浜巗cale-container */
+.scale-container:fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* Webkit娴忚鍣ㄥ墠缂� */
+.scale-container:-webkit-full-screen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* MS娴忚鍣ㄥ墠缂� */
+.scale-container:-ms-fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+
+.dashboard-header {
+position: relative;
+z-index: 1;
+height: 86px;
+background-image: url("@/assets/BI/biaoti.png");
+background-size: cover;
+background-repeat: no-repeat;
+display: flex;
+align-items: center;
+justify-content: center;
+}
+
+.factory-name {
+font-weight: 600;
+font-size: 52px;
+color: #FFFFFF;
+top: 16px;
+position: absolute;
+}
+
+.fullscreen-btn {
+position: absolute;
+top: 10px;
+left: 20px;
+width: 40px;
+height: 40px;
+background: rgba(0, 20, 60, 0.8);
+border: 1px solid rgba(0, 212, 255, 0.3);
+border-radius: 6px;
+color: #00d4ff;
+cursor: pointer;
+display: flex;
+align-items: center;
+justify-content: center;
+transition: all 0.3s;
+z-index: 10000;
+}
+
+.fullscreen-btn:hover {
+background: rgba(0, 30, 90, 0.9);
+border-color: rgba(0, 212, 255, 0.5);
+}
+
+.dashboard-content {
+position: relative;
+z-index: 1;
+display: flex;
+gap: 30px;
+padding: 0 30px;
+height: calc(100% - 86px);
+overflow: hidden;
+}
+
+/* 纭繚鍚勯潰鏉胯兘澶熸纭樉绀� */
+.left-panel, .center-panel, .right-panel {
+overflow: hidden;
+}
+
+.left-panel,
+.right-panel {
+flex: 1;
+display: flex;
+flex-direction: column;
+gap: 24px;
+width: 520px;
+}
+
+.center-panel {
+flex: 1.5;
+display: flex;
+flex-direction: column;
+gap: 20px;
+}
+
+</style>
\ No newline at end of file
diff --git a/src/views/reportAnalysis/dataDashboard/index0.vue b/src/views/reportAnalysis/dataDashboard/index0.vue
new file mode 100644
index 0000000..ff92e41
--- /dev/null
+++ b/src/views/reportAnalysis/dataDashboard/index0.vue
@@ -0,0 +1,2037 @@
+<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">{{ userStore.currentFactoryName }}</div>
+ </div>
+
+ <!-- 涓昏鍐呭鍖哄煙 -->
+ <div class="dashboard-content">
+ <!-- 宸︿晶鍖哄煙 -->
+ <div class="left-panel">
+ <!-- 瀹㈡埛淇℃伅缁熻鍒嗘瀽 -->
+ <div class="panel-header">
+ <span class="panel-title">鍦ㄥ埗鍝佺粺璁″垎鏋�</span>
+ </div>
+ <div class="panel-item-customers">
+ <div class="quality-cards">
+ <div class="quality-cardSec">
+ <div class="quality-card one"></div>
+ <div class="quality-cardTitle">
+ <div>鎬诲湪鍒舵暟閲�</div>
+ <div>{{workInProcessStatistics.totalQuantity}}浠�</div>
+ </div>
+ </div>
+ <div class="quality-cardSec">
+ <div class="quality-card two"></div>
+ <div class="quality-cardTitle">
+ <div>骞冲潎鍛ㄨ浆澶╂暟</div>
+ <div>{{workInProcessStatistics.avgTurnoverDays}}澶�</div>
+ </div>
+ </div>
+ <div class="quality-cardSec">
+ <div class="quality-card three"></div>
+ <div class="quality-cardTitle">
+ <div>鍛ㄨ浆鏁堢巼</div>
+ <div>{{workInProcessStatistics.turnoverEfficiency}}%</div>
+ </div>
+ </div>
+ </div>
+ <!-- 宸ュ簭鍦ㄥ埗鍝佹暟閲忔煴鐘跺浘 -->
+ <div style="height: 70%">
+ <Echarts ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="workInProcessBarLegend"
+ :series="workInProcessBarSeries"
+ :tooltip="tooltip"
+ :xAxis="workInProcessXAxis"
+ :yAxis="workInProcessYAxis"
+ :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
+ style="height: 100%"></Echarts>
+ </div>
+ </div>
+
+ <!-- 璐ㄩ噺缁熻 -->
+ <div class="panel-header">
+ <span class="panel-title">杩�4鏈堣川閲忕粺璁�</span>
+ </div>
+ <div class="main-panel">
+ <div class="panel-item-customers">
+ <div class="quality-cards">
+ <div class="quality-cardSec">
+ <div class="quality-card one"></div>
+ <div class="quality-cardTitle">
+ <div>鍘熸潗鏂欐鏁�</div>
+ <div>{{qualityStatisticsObject.supplierNum}}浠�</div>
+ </div>
+ </div>
+ <div class="quality-cardSec">
+ <div class="quality-card two"></div>
+ <div class="quality-cardTitle">
+ <div>杩囩▼妫�鏁�</div>
+ <div>{{qualityStatisticsObject.processNum}}浠�</div>
+ </div>
+ </div>
+ <div class="quality-cardSec">
+ <div class="quality-card three"></div>
+ <div class="quality-cardTitle">
+ <div>鍑哄巶妫�鏁�</div>
+ <div>{{qualityStatisticsObject.factoryNum}}浠�</div>
+ </div>
+ </div>
+ </div>
+ <Echarts ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="barSeries1"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
+ style="height: 260px"></Echarts>
+ </div>
+ </div>
+ </div>
+
+ <!-- 涓棿鍖哄煙 -->
+ <div class="center-panel">
+ <!-- 椤堕儴缁熻鍗$墖 -->
+ <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">{{totalStaff}}</span>
+ </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">{{totalCustomers}}</span>
+ </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">{{totalSuppliers}}</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- 璁惧缁熻 -->
+ <div class="equipment-stats">
+ <div class="equipment-header">
+ <img src="@/assets/BI/shujutongjiicon@2x.png" alt="鍥炬爣" class="equipment-icon" />
+ <span class="equipment-title">璁惧缁熻</span>
+ </div>
+ <div class="equipment-items">
+ <div class="equipment-item">
+ <span class="equipment-value">{{equipmentNum}}</span>
+ <span class="equipment-label">璁惧鎬绘暟</span>
+ </div>
+ <div class="equipment-item">
+ <span class="equipment-value">{{equipmentRepair}}</span>
+ <span class="equipment-label">寰呯淮淇澶�</span>
+ </div>
+ <div class="equipment-item">
+ <span class="equipment-value">{{equipmentMaintain}}</span>
+ <span class="equipment-label">寰呬繚鍏昏澶�</span>
+ </div>
+ <div class="equipment-item">
+ <span class="equipment-value">{{totalMeasuring}}</span>
+ <span class="equipment-label">璁¢噺鍣ㄥ叿鎬绘暟</span>
+ </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>
+ </div>
+ <div class="event-content">
+ <ul class="todo-list" v-if="todoList.length > 0" ref="refTodoList">
+ <li v-for="item in todoList" :key="item.id">
+ <div style="display: flex;flex-direction: column;justify-content: space-between;width: 100%;gap: 20px">
+ <div style="display: flex;justify-content: space-between;align-items: center;">
+ <div class="todo-title">寰呭姙缂栧彿锛歿{item.approveId}}</div>
+ <div class="todo-division">閮ㄩ棬锛歿{item.approveDeptName}}</div>
+ <div class="todo-time">{{item.approveTime}}</div>
+ </div>
+ <div class="todo-division">寰呭姙浜嬬敱锛歿{item.approveReason}}</div>
+ </div>
+ </li>
+ </ul>
+ <div v-else style="text-align: center">
+ 鏆傛棤鏁版嵁
+ </div>
+ </div>
+ </div>
+
+ <div class="financial-header">
+ <span class="financial-title">鍚勭敓浜ц鍗曠殑瀹屾垚杩涘害缁熻</span>
+ </div>
+ <div class="main-panel">
+ <div class="panel-item-customers">
+ <div class="order-statistics-cards" style="margin-bottom: 0px;">
+ <div class="quality-cardSec">
+ <div class="quality-card four"></div>
+ <div class="quality-cardTitle">
+ <div>鎬昏鍗曟暟</div>
+ <div>{{orderStatisticsObject.totalOrderCount}}浠�</div>
+ </div>
+ </div>
+ <div class="quality-cardSec">
+ <div class="quality-card five"></div>
+ <div class="quality-cardTitle">
+ <div>鏈畬鎴愯鍗曟暟</div>
+ <div>{{orderStatisticsObject.uncompletedOrderCount}}浠�</div>
+ </div>
+ </div>
+ <div class="quality-cardSec">
+ <div class="quality-card six"></div>
+ <div class="quality-cardTitle">
+ <div>閮ㄥ垎瀹屾垚璁㈠崟鏁�</div>
+ <div>{{orderStatisticsObject.partialCompletedOrderCount}}浠�</div>
+ </div>
+ </div>
+ <div class="quality-cardSec">
+ <div class="quality-card seven"></div>
+ <div class="quality-cardTitle">
+ <div>宸插畬鎴愯鍗曟暟</div>
+ <div>{{orderStatisticsObject.completedOrderCount}}浠�</div>
+ </div>
+ </div>
+ </div>
+ <div class="progress-table-container" ref="progressTableRef" style="margin-top: 0px;" @scroll="handleTableScroll">
+ <table class="progress-table">
+ <thead>
+ <tr>
+ <th>鐢熶骇璁㈠崟鍙�</th>
+ <th>浜у搧鍚嶇О</th>
+ <th>瑙勬牸</th>
+ <th>闇�姹傛暟閲�</th>
+ <th>瀹屾垚鏁伴噺</th>
+ <th>瀹屾垚杩涘害</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="(item, index) in progressTableData"
+ :key="index"
+ :ref="el => setRowRef(el, index)"
+ :class="{ 'row-under-header': isRowUnderHeader(index) }"
+ >
+ <td>{{ item.npsNo || '-' }}</td>
+ <td>{{ item.productCategory || '-' }}</td>
+ <td>{{ item.specificationModel || '-' }}</td>
+ <td>{{ item.quantity || 0 }}</td>
+ <td>{{ item.completeQuantity || 0 }}</td>
+ <td>
+ <el-progress
+ :percentage="calculateProgress(item)"
+ :color="progressColor(calculateProgress(item))"
+ :status="calculateProgress(item) >= 100 ? 'success' : ''"
+ :stroke-width="8"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鍙充晶鍖哄煙 -->
+ <div class="right-panel">
+ <!-- 搴旀敹搴斾粯缁熻 -->
+ <div class="panel-header">
+ <span class="panel-title">搴旀敹搴斾粯缁熻</span>
+ </div>
+ <div class="panel-item-customers">
+ <div style="display: flex;justify-content: space-between;margin-bottom: 20px;">
+ <div class="section-title">搴旀敹搴斾粯缁熻</div>
+<!-- <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable" class="custom-radio-group">-->
+<!-- <el-radio-button label="鎸夊懆" :value="1" />-->
+<!-- <el-radio-button label="鎸夋湀" :value="2" />-->
+<!-- <el-radio-button label="鎸夊搴�" :value="3" />-->
+<!-- </el-radio-group>-->
+ </div>
+ <Echarts ref="chart"
+ :color="barColors2"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend2"
+ :series="barSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis"
+ :yAxis="yAxis"
+ :options="{backgroundColor: 'transparent', textStyle: {color: '#B8C8E0'}}"
+ style="height: 260px"></Echarts>
+ </div>
+
+ <!-- 鍥炴涓庡紑绁ㄥ垎鏋� -->
+ <div class="panel-header">
+ <span class="panel-title">杩戜竴鏈堝洖娆句笌寮�绁ㄥ垎鏋�</span>
+ </div>
+ <div class="panel-item-customers" style="padding-top: 60px;">
+ <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
+ :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" :options="{backgroundColor: 'transparent', textStyle: {color: '#FFFFFF'}}" style="height: 270px;"></Echarts>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import * as echarts from 'echarts'
+import { ref, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import autofit from 'autofit.js'
+import Echarts from "@/components/Echarts/echarts.vue";
+import useUserStore from '@/store/modules/user'
+import {
+ analysisCustomerContractAmounts, getAmountHalfYear,
+ homeTodos,
+ qualityStatistics,
+ statisticsReceivablePayable,
+ getProgressStatistics,
+ getWorkInProcessTurnover
+} from "@/api/viewIndex.js";
+import {staffOnJobListPage} from "@/api/personnelManagement/employeeRecord.js";
+import { listCustomer } from '@/api/basicData/customer.js'
+import {listSupplier} from "@/api/basicData/supplierManageFile.js";
+import {getLedgerPage} from "@/api/equipmentManagement/ledger.js";
+import {getRepairPage} from "@/api/equipmentManagement/repair.js";
+import {getUpkeepPage} from "@/api/equipmentManagement/upkeep.js";
+import {measuringInstrumentListPage} from "@/api/equipmentManagement/measurementEquipment.js";
+
+// 鍏ㄥ睆鐩稿叧鐘舵��
+const isFullscreen = ref(false);
+
+// 缂╂斁姣斾緥
+const scaleRatio = ref(1)
+// 璁捐灏哄锛堝熀鍑嗗昂瀵革級- 鏍规嵁瀹為檯璁捐绋胯皟鏁�
+const designWidth = 1920
+const designHeight = 1080
+
+// 鐢ㄦ埛store
+const userStore = useUserStore()
+
+// 鍝嶅簲寮忔暟鎹�
+const currentTime = ref('')
+const currentDate = ref('')
+const timer = ref(null)
+const charts = ref([])
+
+// 鍥捐〃寮曠敤
+const customerPieChartRef = ref(null)
+const salesBarChartRef = ref(null)
+const dataBarChartRef = ref(null)
+const financialAreaChartRef = ref(null)
+const realtimeLineChartRef = ref(null)
+const refContractList = ref(null)
+const refTodoList = ref(null)
+const progressTableRef = ref(null)
+const timerScroll = ref(null)
+const progressTableScrollTimer = ref(null)
+const isTableScrolling = ref(false)
+const tableScrollTimeout = ref(null)
+const tableRowRefs = ref([])
+const rowsUnderHeader = ref(new Set())
+
+const chartStylePie = {
+ width: '100%',
+ height: '100%' // 璁剧疆鍥捐〃瀹瑰櫒鐨勯珮搴�
+}
+const materialPieSeries = ref([
+ {
+ type: 'pie',
+ radius: ['0%', '90%'],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderColor: '#fff',
+ borderWidth: 0
+ },
+ label: {
+ show: false
+ },
+ data: []
+ }
+])
+const pieLegend = reactive({
+ show: false,
+})
+const sum = ref(0)
+const totalStaff = ref(0)
+const totalCustomers = ref(0)
+const totalSuppliers = ref(0)
+const yny = ref(0)
+const chain = ref(0)
+const equipmentNum = ref(0)
+const equipmentRepair = ref(0)
+const equipmentMaintain = ref(0)
+const totalMeasuring = ref(0)
+const pieTooltip = reactive({
+ trigger: 'item',
+ formatter: function (params) {
+ // 鍔ㄦ�佺敓鎴愭彁绀轰俊鎭紝鍩轰簬鏁版嵁椤圭殑 name 灞炴��
+ const description = params.name === '鏈湀鍥炴閲戦' ? '鏈湀鍥炴閲戦' : '搴旀敹娆鹃噾棰�';
+ return `<div style="color: #B8C8E0">${description} ${params.value}鍏� ${params.percent}%</div>`;
+ },
+ position: 'right'
+})
+
+const qualityStatisticsObject = ref({
+ supplierNum: 0,
+ processNum: 0,
+ factoryNum: 0,
+})
+
+// 璁㈠崟缁熻瀵硅薄
+const orderStatisticsObject = ref({
+ totalOrderCount: 0,
+ uncompletedOrderCount: 0,
+ partialCompletedOrderCount: 0,
+ completedOrderCount: 0,
+})
+
+// 鍦ㄥ埗鍝佸懆杞粺璁″璞�
+const workInProcessStatistics = ref({
+ totalQuantity: 0,
+ avgTurnoverDays: 0,
+ turnoverEfficiency: 0,
+})
+const chartStyle = {
+ width: '100%',
+ height: '150%' // 璁剧疆鍥捐〃瀹瑰櫒鐨勯珮搴�
+}
+const barSeries = ref([
+ {
+ name: '搴斾粯閲戦',
+ type: 'bar',
+ data: [],
+ label: {
+ show: true,
+ },
+ itemStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#00A4ED' },
+ { offset: 1, color: '#4EE4FF' }
+ ])
+ }
+ },
+ {
+ name: '搴旀敹閲戦',
+ type: 'bar',
+ data: [],
+ label: {
+ show: true,
+ },
+ itemStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: '#537EF5' },
+ { offset: 1, color: '#9061F8' }
+ ])
+ }
+ }
+])
+const radio1 = ref(1)
+const barColors2 = ['#5181DB', '#D369E0', '#F2CA6D', '#60CCA8']
+const grid = {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
+}
+const lineLegend = {
+ show: true,
+ textStyle: { color: '#B8C8E0' },
+ data: ['寮�绁�', '鍥炴']
+}
+const lineSeries = ref([
+ {
+ type: 'line',
+ data: [],
+ label: {
+ show: true
+ },
+ showSymbol: true, // 鏄剧ず鍦嗙偣
+ },
+])
+const tooltipLine = {
+ trigger: 'axis',
+}
+const yAxis2 = ref([
+ {
+ type: 'value',
+ }
+])
+const xAxis2 = ref([
+ {
+ type: 'category',
+ data: [],
+ axisLabel: {
+ interval: 0,
+ formatter: function(value) {
+ return value.replace(/~/g, '\n');
+ },
+ }
+ }
+])
+const barLegend2 = {
+ show: true,
+ textStyle: { color: '#B8C8E0' },
+ data: ['搴斾粯閲戦', '搴旀敹閲戦']
+}
+const barLegend = {
+ show: true,
+ textStyle: { color: '#B8C8E0' },
+ data: ['鍘熸潗鏂欏悎鏍兼暟', '杩囩▼鍚堟牸鏁�', '鍑轰笉鍚堟牸鏁�']
+}
+const barLegend1 = {
+ show: false,
+ textStyle: { color: '#B8C8E0' },
+ data: []
+}
+const barSeries11 = ref([
+ {
+ name: '鐢熶骇璁㈠崟缁熻',
+ type: 'bar',
+ barGap: 0,
+ emphasis: {
+ focus: 'series'
+ },
+ itemStyle: {
+ // 浣跨敤鍑芥暟鏍规嵁鏁版嵁绱㈠紩杩斿洖涓嶅悓棰滆壊
+ color: function(params) {
+ const colorStops = [
+ [
+ { offset: 1, color: '#00A4ED' },
+ { offset: 0, color: '#4EE4FF' }
+ ],
+ [
+ { offset: 1, color: '#3378FF' },
+ { offset: 0, color: '#4E8AFF' }
+ ],
+ [
+ { offset: 1, color: '#FF6B6B' },
+ { offset: 0, color: '#FF8E8E' }
+ ],
+ [
+ { offset: 1, color: '#537EF5' },
+ { offset: 0, color: '#9061F8' }
+ ]
+ ]
+ const stops = colorStops[params.dataIndex] || colorStops[0]
+ return {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: stops
+ }
+ }
+ },
+ data: []
+ }
+])
+const barSeries1 = ref([
+ {
+ name: '鍘熸潗鏂欏悎鏍兼暟',
+ type: 'bar',
+ barGap: 0,
+ emphasis: {
+ focus: 'series'
+ },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: '#00A4ED' },
+ { offset: 0, color: '#4EE4FF' }
+ ]
+ }
+ },
+ data: []
+ },
+ {
+ name: '杩囩▼鍚堟牸鏁�',
+ type: 'bar',
+ emphasis: {
+ focus: 'series'
+ },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: '#3378FF' },
+ { offset: 0, color: '#4E8AFF' }
+ ]
+ }
+ },
+ data: []
+ },
+ {
+ name: '鍑哄巶鍚堟牸鏁�',
+ type: 'bar',
+ emphasis: {
+ focus: 'series'
+ },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: '#537EF5' },
+ { offset: 0, color: '#9061F8' }
+ ]
+ }
+ },
+ data: []
+ },
+])
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ },
+ formatter: function (params) {
+ let result = params[0].axisValueLabel + '<br/>';
+ params.forEach(item => {
+ result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`;
+ });
+ return result;
+ }
+}
+const xAxis = [{
+ type: 'value',
+}]
+const yAxis = [{
+ type: 'category',
+ data: ['搴旀敹搴斾粯缁熻']
+}]
+const xAxis1 = ref([{
+ type: 'category',
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0' },
+ data: []
+}])
+const yAxis1 = [{
+ type: 'value',
+ axisLabel: { color: '#B8C8E0' }
+}]
+const xAxis3 = ref([{
+ type: 'category',
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0' },
+ data: []
+}])
+const yAxis3 = [{
+ type: 'value',
+ axisLabel: { color: '#B8C8E0' }
+}]
+
+// 鍦ㄥ埗鍝佸伐搴忔煴鐘跺浘閰嶇疆
+const workInProcessXAxis = ref([{
+ type: 'category',
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0' },
+ data: []
+}])
+const workInProcessYAxis = [{
+ type: 'value',
+ axisLabel: { color: '#B8C8E0' },
+ name: ''
+}]
+const workInProcessBarLegend = {
+ show: false,
+ textStyle: { color: '#B8C8E0' },
+ data: []
+}
+const workInProcessBarSeries = ref([
+ {
+ name: '鍦ㄥ埗鍝佹暟閲�',
+ type: 'bar',
+ barWidth: 25, // 鍥哄畾鏌辩姸鍥惧搴︿负40px
+ barGap: 0,
+ emphasis: {
+ focus: 'series'
+ },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 0, color: '#4EE4FF' },
+ { offset: 1, color: '#00A4ED' }
+ ]
+ }
+ },
+ label: {
+ show: true,
+ position: 'top',
+ color: '#B8C8E0'
+ },
+ data: []
+ }
+])
+
+// 寰呭姙浜嬮」
+const todoList = ref([])
+
+// 鐢熶骇璁㈠崟瀹屾垚杩涘害琛ㄦ牸鏁版嵁
+const progressTableData = ref([])
+
+// 璁$畻瀹屾垚杩涘害鐧惧垎姣�
+const calculateProgress = (item) => {
+ if (!item) return 0
+ // 浼樺厛浣跨敤completionStatus瀛楁
+ if (item.completionStatus !== undefined && item.completionStatus !== null) {
+ const percentage = Number(item.completionStatus)
+ if (isNaN(percentage)) return 0
+ return Math.min(Math.max(Math.round(percentage), 0), 100)
+ }
+ // 濡傛灉娌℃湁completionStatus锛屽垯鏍规嵁瀹屾垚鏁伴噺鍜岄渶姹傛暟閲忚绠�
+ if (!item.quantity || item.quantity === 0) return 0
+ const percentage = (item.completeQuantity || 0) / item.quantity * 100
+ return Math.min(Math.max(Math.round(percentage), 0), 100)
+}
+
+// 鏍规嵁杩涘害鐧惧垎姣旇繑鍥為鑹�
+const progressColor = (percentage) => {
+ const p = percentage || 0
+ if (p < 30) return "#f56c6c"
+ if (p < 50) return "#e6a23c"
+ if (p < 80) return "#409eff"
+ return "#67c23a"
+}
+
+// 璁$畻缂╂斁姣斾緥
+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)
+
+ // 瑙﹀彂鍥捐〃resize
+ charts.value.forEach(chart => {
+ if (chart && chart.resize) {
+ chart.resize()
+ }
+ })
+}
+
+// 绐楀彛澶у皬鍙樺寲澶勭悊
+const handleResize = () => {
+ // 寤惰繜鎵ц锛岀‘淇滵OM鏇存柊瀹屾垚
+ setTimeout(() => {
+ calculateScale()
+ }, 100)
+}
+
+// 閿�姣佸浘琛ㄥ疄渚�
+const disposeCharts = () => {
+ charts.value.forEach(chart => {
+ if (chart && chart.dispose) {
+ chart.dispose()
+ }
+ })
+ charts.value = []
+}
+// 鍚堝悓閲戦
+const analysisCustomer = () => {
+ analysisCustomerContractAmounts().then((res) => {
+ sum.value = res.data.sum
+ yny.value = res.data.yny
+ chain.value = res.data.chain
+ // 涓烘瘡涓暟鎹」鍒嗛厤闅忔満棰滆壊
+ materialPieSeries.value[0].data = res.data.item.map(item => ({
+ ...item,
+ itemStyle: { color: getRandomColor() }
+ }))
+ })
+}
+// 鍦ㄥ埗鍝佸懆杞粺璁�
+const workInProcessTurnoverInfo = () => {
+ getWorkInProcessTurnover().then((res) => {
+ console.log("鍦ㄥ埗鍝佸懆杞粺璁℃暟鎹�:", res)
+
+ if (!res || !res.data) {
+ console.warn('鍦ㄥ埗鍝佸懆杞粺璁℃暟鎹负绌�')
+ return
+ }
+
+ // 浠庢帴鍙h幏鍙栫粺璁℃暟鎹�
+ workInProcessStatistics.value = {
+ totalQuantity: res.data.totalOrderCount || 0,
+ avgTurnoverDays: res.data.averageTurnoverDays || 0,
+ turnoverEfficiency: res.data.turnoverEfficiency || 0,
+ }
+
+ // 璁剧疆宸ュ簭鏌辩姸鍥炬暟鎹�
+ // X杞达細processDetails (宸ュ簭璇︽儏鏁扮粍)
+ // Y杞达細processQuantityDetails (宸ュ簭鏁伴噺璇︽儏鏁扮粍)
+ if (res.data.processDetails && Array.isArray(res.data.processDetails)) {
+ // 璁剧疆X杞存暟鎹紙宸ュ簭鍚嶇О锛�
+ workInProcessXAxis.value[0].data = res.data.processDetails
+ } else {
+ workInProcessXAxis.value[0].data = []
+ }
+
+ if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) {
+ // 璁剧疆Y杞存暟鎹紙鍦ㄥ埗鍝佹暟閲忥級
+ workInProcessBarSeries.value[0].data = res.data.processQuantityDetails
+ } else {
+ workInProcessBarSeries.value[0].data = []
+ }
+ }).catch((error) => {
+ console.error('鑾峰彇鍦ㄥ埗鍝佸懆杞粺璁″け璐�:', error)
+ })
+}
+// 璐ㄦ缁熻
+const qualityStatisticsInfo = () => {
+ qualityStatistics().then((res) => {
+ res.data.item.forEach(item => {
+ xAxis1.value[0].data.push(item.date)
+ barSeries1.value[0].data.push(item.supplierNum)
+ barSeries1.value[1].data.push(item.processNum)
+ barSeries1.value[2].data.push(item.factoryNum)
+ })
+ qualityStatisticsObject.value.supplierNum = res.data.supplierNum
+ qualityStatisticsObject.value.processNum = res.data.processNum
+ qualityStatisticsObject.value.factoryNum = res.data.factoryNum
+ })
+}
+// 鍚勭敓浜ц鍗曠殑瀹屾垚杩涘害缁熻
+const progressStatisticsInfo = () => {
+ // 浠庣粺璁℃帴鍙h幏鍙栫粺璁℃暟鎹�
+ getProgressStatistics().then((res) => {
+ console.log("鐢熶骇璁㈠崟瀹屾垚杩涘害缁熻鏁版嵁:", res)
+
+ if (!res || !res.data) {
+ console.warn('鐢熶骇璁㈠崟瀹屾垚杩涘害缁熻鏁版嵁涓虹┖')
+ return
+ }
+
+ // 浠庢帴鍙h幏鍙栫粺璁℃暟鎹�
+ orderStatisticsObject.value = {
+ totalOrderCount: res.data.totalOrderCount || 0,
+ uncompletedOrderCount: res.data.uncompletedOrderCount || 0,
+ partialCompletedOrderCount: res.data.partialCompletedOrderCount || 0,
+ completedOrderCount: res.data.completedOrderCount || 0
+ }
+ progressTableData.value = res.data.completedOrderDetails || []
+ // 閲嶇疆琛屽紩鐢�
+ tableRowRefs.value = []
+ rowsUnderHeader.value.clear()
+
+ // 鍦ㄨ幏鍙栧埌鏁版嵁鍚庯紝鍒濆鍖栨粴鍔ㄥ姛鑳�
+ nextTick(() => {
+ initProgressTableScroll()
+ })
+ }).catch((error) => {
+ console.error('鑾峰彇鐢熶骇璁㈠崟瀹屾垚杩涘害缁熻澶辫触:', error)
+ })
+}
+// 璐㈠姟缁熻
+// const accountStatisticsInfo = () => {
+// listPageAnalysis().then((res) => {
+// xAxis3.value[0].data = res.data.days
+// barSeries11.value[0].data = res.data.totalIncome
+// })
+// }
+const getNum = () => {
+ const params = {
+ pageNum: -1,
+ pageSize: -1,
+ }
+ staffOnJobListPage({...params, staffState: 1}).then(res => {
+ totalStaff.value = res.data.total
+ })
+ listCustomer(params).then((res) => {
+ totalCustomers.value = res.total;
+ });
+ listSupplier(params).then((res) => {
+ totalSuppliers.value = res.data.total
+ });
+}
+const getLedgerNum = () => {
+ const params = {
+ pageNum: -1,
+ pageSize: -1,
+ }
+ getLedgerPage(params).then((res) => {
+ equipmentNum.value = res.data.total
+ });
+ getRepairPage({...params, status:0}).then((res) => {
+ equipmentRepair.value = res.data.total
+ });
+ getUpkeepPage({...params, status:0}).then((res) => {
+ equipmentMaintain.value = res.data.total
+ });
+ measuringInstrumentListPage(params).then((res) => {
+ totalMeasuring.value = res.data.total
+ });
+}
+// 寰呭姙浜嬮」
+const todoInfoS = () => {
+ homeTodos().then((res) => {
+ todoList.value = res.data
+ // 鍦ㄨ幏鍙栧埌寰呭姙浜嬮」鏁版嵁鍚庯紝鍒濆鍖栨粴鍔ㄥ姛鑳�
+ nextTick(() => {
+ initTodoListScroll()
+ })
+ })
+}
+// 搴斾粯搴旀敹缁熻
+const statisticsReceivable = (type = radio1.value) => {
+ statisticsReceivablePayable({ type }).then((res) => {
+ const data = res?.data || {}
+ const payableMoney = Number(data.payableMoney ?? 0)
+ const receivableMoney = Number(data.receivableMoney ?? 0)
+ // 璁剧疆搴斾粯閲戦鏁版嵁
+ barSeries.value[0].data = [
+ { value: payableMoney }
+ ]
+ // 璁剧疆搴旀敹閲戦鏁版嵁
+ barSeries.value[1].data = [
+ { value: receivableMoney }
+ ]
+ })
+}
+const getAmountHalfYearNum = async () => {
+ const res = await getAmountHalfYear()
+ console.log(res)
+ const monthName = []
+ const receiptAmount = []
+ const invoiceAmount = []
+ res.data.forEach(item => {
+ monthName.push(item.month)
+ receiptAmount.push(item.receiptAmount)
+ invoiceAmount.push(item.invoiceAmount)
+ })
+ // 姝g‘鍝嶅簲寮忚祴鍊硷細鍒涘缓鏂扮殑 xAxis 鍜� series 瀵硅薄
+ xAxis2.value[0].data = monthName
+ xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~'));
+ lineSeries.value = [
+ {
+ name: '寮�绁�',
+ type: 'line',
+ data: receiptAmount,
+ stack: 'Total',
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color: 'rgba(131, 207, 255, 1)'
+ },
+ {
+ offset: 1,
+ color: 'rgba(186, 228, 255, 1)'
+ }
+ ])
+ },
+ itemStyle: {
+ color: '#2D99FF',
+ borderColor: '#2D99FF'
+ },
+ emphasis: {
+ focus: 'series'
+ },
+ lineStyle: {
+ width: 0
+ },
+ showSymbol: true,
+ },
+ {
+ name: '鍥炴',
+ type: 'line',
+ data: invoiceAmount,
+ stack: 'Total',
+ lineStyle: {
+ width: 0
+ },
+ itemStyle: {
+ color: '#83CFFF',
+ borderColor: '#83CFFF'
+ },
+ showSymbol: true,
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ {
+ offset: 0,
+ color: 'rgba(54, 153, 255, 1)'
+ },
+ {
+ offset: 1,
+ color: 'rgba(89, 169, 254, 1)'
+ }
+ ])
+ },
+ emphasis: {
+ focus: 'series'
+ },
+ }
+ ]
+}
+
+// 鑷姩杞崲鍛ㄣ�佹湀銆佸搴︾殑瀹氭椂鍣�
+const autoSwitchTimer = ref(null)
+
+// 璁剧疆琛屽紩鐢�
+const setRowRef = (el, index) => {
+ if (el) {
+ tableRowRefs.value[index] = el
+ }
+}
+
+// 鍒ゆ柇琛屾槸鍚﹀湪琛ㄥご涓嬫柟
+const isRowUnderHeader = (index) => {
+ return rowsUnderHeader.value.has(index)
+}
+
+// 澶勭悊琛ㄦ牸婊氬姩浜嬩欢
+const handleTableScroll = () => {
+ const tableContainer = progressTableRef.value
+ if (!tableContainer) return
+
+ const thead = tableContainer.querySelector('thead')
+ if (!thead) return
+
+ const theadHeight = thead.offsetHeight
+ const containerRect = tableContainer.getBoundingClientRect()
+ const containerTop = containerRect.top
+ const theadBottom = containerTop + theadHeight
+
+ // 娓呯┖涔嬪墠鐨勮褰�
+ rowsUnderHeader.value.clear()
+
+ // 妫�鏌ユ瘡涓�琛屾槸鍚﹀湪琛ㄥご涓嬫柟锛堣琛ㄥご閬尅锛�
+ tableRowRefs.value.forEach((row, index) => {
+ if (row) {
+ const rowRect = row.getBoundingClientRect()
+ const rowTop = rowRect.top
+ const rowBottom = rowRect.bottom
+
+ // 濡傛灉琛屼笌琛ㄥご鏈夐噸鍙狅紙琛屽湪琛ㄥご涓嬫柟琚伄鎸★級
+ // 琛岀殑椤堕儴鍦ㄨ〃澶村簳閮ㄤ笅鏂癸紝浣嗚鐨勫簳閮ㄥ湪琛ㄥご搴曢儴涓婃柟锛岃鏄庤閬尅
+ if (rowTop < theadBottom && rowBottom > containerTop) {
+ rowsUnderHeader.value.add(index)
+ }
+ }
+ })
+
+ // 娓呴櫎涔嬪墠鐨勫畾鏃跺櫒
+ if (tableScrollTimeout.value) {
+ clearTimeout(tableScrollTimeout.value)
+ }
+
+ // 婊氬姩鍋滄鍚庢竻绌烘贰鍖栨爣璁�
+ tableScrollTimeout.value = setTimeout(() => {
+ rowsUnderHeader.value.clear()
+ }, 150)
+}
+
+// 鍒濆鍖栫敓浜ц鍗曡繘搴﹁〃鏍兼粴鍔ㄥ姛鑳�
+const initProgressTableScroll = () => {
+ const tableContainer = progressTableRef.value
+ if (!tableContainer) return
+
+ // 娓呯悊涔嬪墠鐨勬粴鍔ㄥ姩鐢诲拰瀹氭椂鍣�
+ if (progressTableScrollTimer.value) {
+ cancelAnimationFrame(progressTableScrollTimer.value)
+ progressTableScrollTimer.value = null
+ }
+ if (tableContainer._pauseTimer) {
+ clearInterval(tableContainer._pauseTimer)
+ tableContainer._pauseTimer = null
+ }
+
+ const tbody = tableContainer.querySelector('tbody')
+ if (!tbody) return
+
+ // 娓呯悊涔嬪墠鍙兘瀛樺湪鐨勫厠闅嗚锛堜繚鐣欏師濮嬫暟鎹锛�
+ // 鍘熷鏁版嵁琛岀殑鏁伴噺搴旇绛変簬 progressTableData.value.length
+ const originalCount = progressTableData.value.length
+ const allRows = Array.from(tbody.querySelectorAll('tr'))
+ if (allRows.length > originalCount) {
+ // 绉婚櫎鎵�鏈夎秴杩囧師濮嬫暟閲忕殑琛岋紙杩欎簺鏄厠闅嗙殑琛岋級
+ for (let i = originalCount; i < allRows.length; i++) {
+ allRows[i].remove()
+ }
+ }
+
+ const scrollItems = Array.from(tbody.querySelectorAll('tr'))
+ if (scrollItems.length === 0) return
+
+ // 鑾峰彇鍘熷鏁版嵁椤规暟閲�
+ const originalItemCount = scrollItems.length
+
+ // 璁$畻瀹瑰櫒楂樺害鍜岃〃澶撮珮搴�
+ const thead = tableContainer.querySelector('thead')
+ const theadHeight = thead ? thead.offsetHeight : 40
+ const containerHeight = tableContainer.clientHeight
+ const visibleHeight = containerHeight - theadHeight
+
+ // 璁$畻鍘熷鏁版嵁鐨勬�婚珮搴�
+ const itemHeight = scrollItems[0]?.offsetHeight || 40
+ const totalContentHeight = itemHeight * originalItemCount
+
+ // 濡傛灉鏁版嵁閲忎笉澶燂紝瀹瑰櫒鍙互瀹屽叏鏄剧ず鎵�鏈夋暟鎹紝灏变笉闇�瑕佹粴鍔ㄥ拰鍏嬮殕
+ if (totalContentHeight <= visibleHeight) {
+ // 鏁版嵁閲忓皯锛屼笉闇�瑕佹粴鍔紝鐩存帴杩斿洖
+ return
+ }
+
+ // 鏁版嵁閲忚冻澶燂紝闇�瑕佹粴鍔紝杩涜鍏嬮殕浠ュ疄鐜版棤缂濇粴鍔�
+ const cloneCount = Math.ceil(visibleHeight / itemHeight) + 2
+
+ // 鍏嬮殕鍓嶅嚑涓」鐩苟娣诲姞鍒板垪琛ㄦ湯灏撅紝瀹炵幇鏃犵紳婊氬姩
+ for (let i = 0; i < cloneCount; i++) {
+ const clone = scrollItems[i % originalItemCount].cloneNode(true)
+ tbody.appendChild(clone)
+ }
+
+ let scrollPosition = 0
+ const scrollSpeed = 1.5
+ const pauseTime = 3000
+ let isPaused = false
+ let lastTimestamp = 0
+
+ // 杩炵画婊氬姩鍔ㄧ敾鍑芥暟
+ function scrollAnimation(timestamp) {
+ if (!lastTimestamp) lastTimestamp = timestamp
+ const deltaTime = timestamp - lastTimestamp
+ lastTimestamp = timestamp
+
+ if (!isPaused) {
+ scrollPosition += scrollSpeed * (deltaTime / 16)
+
+ // 璁$畻鏈�澶ф粴鍔ㄤ綅缃紙鍘熷鍐呭鐨勯珮搴︼級
+ const maxScroll = itemHeight * originalItemCount
+
+ // 褰撴粴鍔ㄨ秴杩囧師濮嬪唴瀹归暱搴︽椂锛岄噸缃綅缃疄鐜版棤缂濇粴鍔�
+ if (scrollPosition >= maxScroll) {
+ scrollPosition = 0
+ tableContainer.scrollTop = 0
+ } else {
+ tableContainer.scrollTop = scrollPosition
+ }
+ }
+
+ progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
+ }
+
+ // 鍚姩婊氬姩鍔ㄧ敾
+ progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
+
+ // 璁剧疆婊氬姩-鏆傚仠-婊氬姩鐨勫惊鐜晥鏋�
+ const pauseTimer = setInterval(() => {
+ isPaused = !isPaused
+ }, pauseTime)
+
+ // 娓呯悊瀹氭椂鍣�
+ tableContainer._pauseTimer = pauseTimer
+}
+
+// 鍒濆鍖栧緟鍔炰簨椤瑰垪琛ㄦ粴鍔ㄥ姛鑳�
+const initTodoListScroll = () => {
+ const todoList = refTodoList.value
+ // 寮哄埗鍚敤婊氬姩锛屼笉妫�鏌ヤ换浣曟潯浠�
+ if (todoList) {
+ // 鍒涘缓涓�涓厠闅嗛」锛岀敤浜庡疄鐜版棤缂濇粴鍔�
+ const scrollItems = Array.from(todoList.querySelectorAll('li'))
+ if (scrollItems.length > 0) {
+ // 纭繚鏈夎冻澶熺殑椤圭洰鐢ㄤ簬婊氬姩
+ // 濡傛灉椤圭洰澶皯锛屽澶嶅埗鍑犳浠ョ‘淇濇粴鍔ㄦ晥鏋�
+ if (scrollItems.length < 4) {
+ const originalItems = [...scrollItems]
+ for (let i = 0; i < 4; i++) {
+ originalItems.forEach(item => {
+ const clone = item.cloneNode(true)
+ todoList.appendChild(clone)
+ })
+ }
+ // 閲嶆柊鑾峰彇鎵�鏈夐」鐩�
+ scrollItems.push(...Array.from(todoList.querySelectorAll('li')).slice(scrollItems.length));
+ }
+ const itemHeight = scrollItems[0]?.offsetHeight || 0
+ const containerHeight = todoList.clientHeight
+ const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
+
+ // 鍏嬮殕鍓嶅嚑涓」鐩苟娣诲姞鍒板垪琛ㄦ湯灏撅紝瀹炵幇鏃犵紳婊氬姩
+ for (let i = 0; i < cloneCount; i++) {
+ const clone = scrollItems[i % scrollItems.length].cloneNode(true)
+ todoList.appendChild(clone)
+ }
+
+ let scrollPosition = 0
+ const scrollSpeed = 1.5 // 澧炲姞婊氬姩閫熷害锛屼娇婊氬姩鏇村姞鏄庢樉
+ const pauseTime = 3000 // 婊氬姩鏆傚仠鏃堕棿
+ let isPaused = false
+ let lastTimestamp = 0
+
+ // 杩炵画婊氬姩鍔ㄧ敾鍑芥暟
+ function scrollAnimation(timestamp) {
+ if (!lastTimestamp) lastTimestamp = timestamp
+ const deltaTime = timestamp - lastTimestamp
+ lastTimestamp = timestamp
+
+ if (!isPaused) {
+ scrollPosition += scrollSpeed * (deltaTime / 16) // 鏍囧噯鍖栦负60fps鐨勯�熷害
+
+ // 褰撴粴鍔ㄨ秴杩囧師濮嬪唴瀹归暱搴︽椂锛岄噸缃綅缃疄鐜版棤缂濇粴鍔�
+ const maxScroll = Math.max(todoList.scrollHeight - containerHeight - cloneCount * itemHeight, itemHeight * scrollItems.length)
+ if (scrollPosition >= maxScroll) {
+ scrollPosition = 0
+ todoList.scrollTop = 0
+ } else {
+ todoList.scrollTop = scrollPosition
+ }
+ }
+
+ todoList._animationFrame = requestAnimationFrame(scrollAnimation)
+ }
+
+ // 鍚姩婊氬姩鍔ㄧ敾
+ todoList._animationFrame = requestAnimationFrame(scrollAnimation)
+
+ // 璁剧疆婊氬姩-鏆傚仠-婊氬姩鐨勫惊鐜晥鏋�
+ const pauseTimer = setInterval(() => {
+ isPaused = !isPaused
+ }, pauseTime)
+
+ // 娓呯悊瀹氭椂鍣�
+ todoList._pauseTimer = pauseTimer
+ }
+ }
+}
+const getRandomColor = () => {
+ // 鐢熸垚娴呰壊锛歊銆丟銆丅 鍒嗛噺閮藉湪 150-255 涔嬮棿
+ const r = Math.floor(Math.random() * 106) + 150; // 150-255
+ const g = Math.floor(Math.random() * 106) + 150; // 150-255
+ const b = Math.floor(Math.random() * 106) + 150; // 150-255
+ // 灏� RGB 杞崲涓哄崄鍏繘鍒堕鑹�
+ return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
+}
+
+// 鏇存柊鏃堕棿
+const updateTime = () => {
+ const now = new Date()
+ currentTime.value = now.toLocaleTimeString('zh-CN', { hour12: false })
+ currentDate.value = now.toLocaleDateString('zh-CN', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ weekday: 'long'
+ })
+}
+
+// 鍒濆鍖栨椂闂�
+const initTime = () => {
+ updateTime()
+ timer.value = setInterval(updateTime, 1000)
+}
+// 鍏ㄥ睆鍔熻兘瀹炵幇 - 閽堝scale-container鍏冪礌
+const toggleFullscreen = () => {
+ const element = document.querySelector('.scale-container')
+
+ if (!element) return
+
+ if (!isFullscreen.value) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen()
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen()
+ }
+ }
+}
+
+// 鐩戝惉鍏ㄥ睆鍙樺寲浜嬩欢
+const handleFullscreenChange = () => {
+ const fullscreenElement = document.fullscreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement
+ isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
+
+ // 鍏ㄥ睆鐘舵�佸彉鍖栨椂锛屽欢杩熼噸鏂拌绠楃缉鏀炬瘮渚嬶紙纭繚DOM鏇存柊瀹屾垚锛�
+ setTimeout(() => {
+ calculateScale()
+ }, 200)
+}
+
+// 鐢熷懡鍛ㄦ湡閽╁瓙
+onMounted(() => {
+ initTime()
+ // 浣跨敤nextTick纭繚DOM瀹屽叏娓叉煋鍚庡啀鍒濆鍖栧浘琛�
+ nextTick(() => {
+ // 璁$畻鍒濆缂╂斁姣斾緥
+ calculateScale()
+
+ // 鍒濆鍖朼utofit鑷�傚簲锛堝鏋滈渶瑕佷繚鐣檃utofit锛屽彲浠ヤ繚鐣欙紝浣嗕富瑕佺缉鏀剧敱scale-container鎺у埗锛�
+ // autofit.init({ dh: 800, dw: 1280, el: '.data-dashboard', resize: true }, false)
+
+ // 娣诲姞鑷姩婊氬姩鍔ㄧ敾鏁堟灉 - 瀹㈡埛淇℃伅鍒楄〃
+ const contractList = refContractList.value
+ if (contractList && contractList.scrollHeight > contractList.clientHeight) {
+ // 鍒涘缓涓�涓厠闅嗛」锛岀敤浜庡疄鐜版棤缂濇粴鍔�
+ const scrollItems = Array.from(contractList.querySelectorAll('li'))
+ const itemHeight = scrollItems[0]?.offsetHeight || 0
+ const containerHeight = contractList.clientHeight
+ const cloneCount = Math.ceil(containerHeight / itemHeight) + 2
+
+ // 鍏嬮殕鍓嶅嚑涓」鐩苟娣诲姞鍒板垪琛ㄦ湯灏撅紝瀹炵幇鏃犵紳婊氬姩
+ for (let i = 0; i < cloneCount; i++) {
+ const clone = scrollItems[i % scrollItems.length].cloneNode(true)
+ contractList.appendChild(clone)
+ }
+
+ let scrollPosition = 0
+ const scrollSpeed = 1.5 // 澧炲姞婊氬姩閫熷害锛屼娇婊氬姩鏇村姞鏄庢樉
+ const pauseTime = 3000 // 婊氬姩鏆傚仠鏃堕棿
+ let isPaused = false
+ let lastTimestamp = 0
+
+ // 杩炵画婊氬姩鍔ㄧ敾鍑芥暟
+ function scrollAnimation(timestamp) {
+ if (!lastTimestamp) lastTimestamp = timestamp
+ const deltaTime = timestamp - lastTimestamp
+ lastTimestamp = timestamp
+
+ if (!isPaused) {
+ scrollPosition += scrollSpeed * (deltaTime / 16) // 鏍囧噯鍖栦负60fps鐨勯�熷害
+
+ // 褰撴粴鍔ㄨ秴杩囧師濮嬪唴瀹归暱搴︽椂锛岄噸缃綅缃疄鐜版棤缂濇粴鍔�
+ if (scrollPosition >= contractList.scrollHeight - containerHeight - cloneCount * itemHeight) {
+ scrollPosition = 0
+ contractList.scrollTop = 0
+ } else {
+ contractList.scrollTop = scrollPosition
+ }
+ }
+
+ timerScroll.value = requestAnimationFrame(scrollAnimation)
+ }
+
+ // 鍚姩婊氬姩鍔ㄧ敾
+ timerScroll.value = requestAnimationFrame(scrollAnimation)
+
+ // 璁剧疆婊氬姩-鏆傚仠-婊氬姩鐨勫惊鐜晥鏋�
+ const pauseTimer = setInterval(() => {
+ isPaused = !isPaused
+ }, pauseTime)
+
+ // 娓呯悊瀹氭椂鍣�
+ contractList._pauseTimer = pauseTimer
+ }
+
+ // 寰呭姙浜嬮」鍒楄〃婊氬姩鍔熻兘宸茬Щ鑷硉odoInfoS鍑芥暟涓紝鍦ㄨ幏鍙栨暟鎹悗鍒濆鍖�
+ })
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('fullscreenchange', handleFullscreenChange)
+ window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.addEventListener('MSFullscreenChange', handleFullscreenChange)
+ analysisCustomer()
+ workInProcessTurnoverInfo()
+ qualityStatisticsInfo()
+ // accountStatisticsInfo()
+ progressStatisticsInfo()
+ getNum()
+ getLedgerNum()
+ todoInfoS()
+ statisticsReceivable()
+ getAmountHalfYearNum()
+
+ // 璁剧疆鑷姩杞崲鍛ㄣ�佹湀銆佸搴︾殑瀹氭椂鍣紝姣�10绉掑垏鎹竴娆�
+ autoSwitchTimer.value = setInterval(() => {
+ // 寰幆鍒囨崲锛�1(鍛�) -> 2(鏈�) -> 3(瀛e害) -> 1(鍛�)
+ radio1.value = radio1.value === 3 ? 1 : radio1.value + 1
+ statisticsReceivable()
+ }, 10000) // 10绉掑垏鎹竴娆�
+})
+
+onBeforeUnmount(() => {
+ if (timer.value) {
+ clearInterval(timer.value)
+ }
+ if (timerScroll.value) {
+ cancelAnimationFrame(timerScroll.value)
+ }
+ // 娓呯悊婊氬姩鍒楄〃鐨勬殏鍋滃畾鏃跺櫒
+ const contractList = refContractList.value
+ if (contractList && contractList._pauseTimer) {
+ clearInterval(contractList._pauseTimer)
+ }
+
+ // 娓呯悊寰呭姙浜嬮」鍒楄〃鐨勫姩鐢诲拰瀹氭椂鍣�
+ const todoList = refTodoList.value
+ if (todoList) {
+ if (todoList._animationFrame) {
+ cancelAnimationFrame(todoList._animationFrame)
+ todoList._animationFrame = null
+ }
+ if (todoList._pauseTimer) {
+ clearInterval(todoList._pauseTimer)
+ todoList._pauseTimer = null
+ }
+ }
+
+ // 娓呯悊鐢熶骇璁㈠崟杩涘害琛ㄦ牸鐨勫姩鐢诲拰瀹氭椂鍣�
+ const progressTable = progressTableRef.value
+ if (progressTable) {
+ if (progressTableScrollTimer.value) {
+ cancelAnimationFrame(progressTableScrollTimer.value)
+ progressTableScrollTimer.value = null
+ }
+ if (progressTable._pauseTimer) {
+ clearInterval(progressTable._pauseTimer)
+ progressTable._pauseTimer = null
+ }
+ }
+
+ // 娓呯悊琛ㄦ牸婊氬姩瀹氭椂鍣�
+ if (tableScrollTimeout.value) {
+ clearTimeout(tableScrollTimeout.value)
+ tableScrollTimeout.value = null
+ }
+
+ // 娓呯悊鑷姩杞崲鍛ㄣ�佹湀銆佸搴︾殑瀹氭椂鍣�
+ if (autoSwitchTimer.value) {
+ clearInterval(autoSwitchTimer.value)
+ autoSwitchTimer.value = null
+ }
+
+ window.removeEventListener('resize', handleResize)
+ window.removeEventListener('fullscreenchange', handleFullscreenChange)
+ window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
+ // 绉婚櫎鎴戜滑娣诲姞鐨刟utofit鍔ㄦ�佽皟鏁寸洃鍚櫒
+ if (window._autofitUpdateHandler) {
+ window.removeEventListener('resize', window._autofitUpdateHandler)
+ delete window._autofitUpdateHandler
+ }
+ disposeCharts()
+ // 鍏抽棴autofit
+ autofit.off()
+})
+</script>
+
+<style scoped>
+/* 澶栭儴缂╂斁瀹瑰櫒 - 鍗犳嵁鏁翠釜瑙嗗彛 */
+.scale-container {
+ position: relative;
+ width: 100%;
+ /* 椤甸潰鍦ㄥ父瑙勫竷灞�涓嬶紙鏈夐《鏍忥級榛樿鍑忓幓 84px锛岄伩鍏嶅唴瀹硅瑁佸垏 */
+ height: calc(100vh - 84px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: #000;
+ overflow: hidden;
+}
+
+/* 鍐呴儴鍐呭鍖哄煙 - 鍥哄畾璁捐灏哄 */
+.data-dashboard {
+ position: relative;
+ width: 1920px;
+ height: 1080px;
+ background-image: url("@/assets/BI/backImage@2x.png");
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ transform-origin: center center;
+}
+
+/* 鍏ㄥ睆鐘舵�佺殑鏍峰紡 - 浣滅敤浜巗cale-container */
+.scale-container:fullscreen {
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ background-color: #000;
+ z-index: 9999;
+}
+
+/* Webkit娴忚鍣ㄥ墠缂� */
+.scale-container:-webkit-full-screen {
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ background-color: #000;
+ z-index: 9999;
+}
+
+/* MS娴忚鍣ㄥ墠缂� */
+.scale-container:-ms-fullscreen {
+ width: 100vw;
+ height: 100vh;
+ margin: 0;
+ padding: 0;
+ background-color: #000;
+ z-index: 9999;
+}
+
+
+.dashboard-header {
+ position: relative;
+ z-index: 1;
+ height: 86px;
+ background-image: url("@/assets/BI/biaoti.png");
+ background-size: cover;
+ background-repeat: no-repeat;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.factory-name {
+ font-weight: 600;
+font-size: 52px;
+color: #FFFFFF;
+top: 16px;
+position: absolute;
+}
+
+.fullscreen-btn {
+ position: absolute;
+ top: 10px;
+ left: 20px;
+ width: 40px;
+ height: 40px;
+ background: rgba(0, 20, 60, 0.8);
+ border: 1px solid rgba(0, 212, 255, 0.3);
+ border-radius: 6px;
+ color: #00d4ff;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s;
+ z-index: 10000;
+}
+
+.fullscreen-btn:hover {
+ background: rgba(0, 30, 90, 0.9);
+ border-color: rgba(0, 212, 255, 0.5);
+}
+
+.dashboard-content {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ gap: 30px;
+ padding: 0 30px;
+ height: calc(100% - 86px);
+ overflow: hidden;
+}
+
+/* 纭繚鍚勯潰鏉胯兘澶熸纭樉绀� */
+.left-panel, .center-panel, .right-panel {
+ overflow: hidden;
+}
+
+.left-panel,
+.right-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ width: 520px;
+}
+
+.center-panel {
+ flex: 1.5;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+.panel-item-customers {
+ border: 1px solid #1A58B0;
+ padding: 18px;
+ width: 100%;
+ height: 540px;
+}
+.panel-title-second {
+ height: 60px;
+ display: flex;
+ gap: 12px;
+ margin-bottom: 20px;
+ align-items: center;
+}
+.quality-cards {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ height: 54px;
+ justify-content: space-between;
+ align-items: center;
+}
+.quality-cardSec {
+ display: flex;
+}
+.quality-cardTitle {
+ font-weight: 400;
+ font-size: 14px;
+ color: #FFFFFF;
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+}
+.quality-card {
+ width: 80px;
+ height: 60px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.quality-card.one {
+ background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
+}
+.quality-card.two {
+ background-image: url("@/assets/BI/guochengyijianicon@2x.png");
+}
+.quality-card.three {
+ background-image: url("@/assets/BI/chuchangyijianicon@2x.png");
+}
+
+/* 璁㈠崟缁熻鍗$墖鏍峰紡 */
+.order-statistics-cards {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ height: 94px;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+}
+
+.quality-card.four {
+ background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
+}
+
+.quality-card.five {
+ background-image: url("@/assets/BI/guochengyijianicon@2x.png");
+}
+
+.quality-card.six {
+ background-image: url("@/assets/BI/chuchangyijianicon@2x.png");
+}
+
+.quality-card.seven {
+ background-image: url("@/assets/BI/yuancailiaoyijianicon@2x.png");
+}
+.panel-title-icon {
+ width: 60px;
+ height: 60px;
+ background-image: url("@/assets/BI/hetongicon.png");
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.panel-header {
+ background-image: url("@/assets/BI/kehuhetongback@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.panel-title {
+ width: 100%;
+ font-weight: 500;
+ font-size: 16px;
+ color: #D9ECFF;
+ padding-left: 46px;
+ line-height: 36px;
+}
+.total-customers {
+ background-image: url("@/assets/BI/hetongjineback@2x.png");
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ width: 90%;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ padding: 0 20px;
+ gap: 20px;
+}
+
+.total-customers .label {
+ font-weight: 500;
+ font-size: 16px;
+ color: #FFFFFF;
+}
+
+.total-customers .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;
+}
+
+.contract-list {
+ margin-top: 16px;
+ font-size: 14px;
+ color: #666;
+ list-style: none;
+ padding: 0;
+ height: 82%;
+ overflow-y: auto;
+ width: 460px;
+ /* 闅愯棌婊氬姩鏉′絾淇濈暀婊氬姩鍔熻兘 */
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE鍜孍dge */
+}
+
+/* Chrome銆丼afari鍜孫pera */
+.contract-list::-webkit-scrollbar {
+ display: none;
+}
+.line {
+ position: relative;
+ width: 230px;
+}
+.line::after {
+ content: '';
+ position: absolute;
+ right: 2px;
+ top: 0;
+ bottom: 0;
+ width: 1px;
+ background-color: #C9C5C5;
+ border-radius: 2px;
+}
+.contract-list li {
+ margin-top: 10px;
+}
+.stats-cards {
+ display: flex;
+ gap: 30px;
+}
+
+.stat-card {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ background-image: url("@/assets/BI/border@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 142px;
+}
+
+.card-icon {
+ width: 100px;
+ height: 100px;
+ margin: 20px 20px 0 10px;
+}
+
+.card-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.card-value {
+ font-weight: 500;
+ font-size: 40px;
+ background: linear-gradient(360deg, #008BFD 0%, #FFFFFF 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 19px;
+ color: rgba(208,231,255,0.7);
+}
+
+.equipment-stats {
+ border: 1px solid #1A58B0;
+ padding: 18px;
+ height: 240px;
+}
+.equipment-header {
+ font-weight: 500;
+ font-size: 21px;
+ display: flex;
+ border-bottom: 1px solid;
+ border-image: linear-gradient( 270deg, rgba(0,126,255,0) 0%, rgba(0,126,255,0.4549) 35%, #007EFF 78%, #007EFF 100%) 1;
+ padding-bottom: 2px;
+}
+.equipment-title {
+ font-weight: 500;
+ font-size: 21px;
+ background: linear-gradient(360deg, #056DFF 0%, #43E8FC 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ line-height: 50px;
+}
+.equipment-icon {
+ width: 50px;
+ height: 50px;
+}
+.equipment-items {
+ display: flex;
+ justify-content: space-around;
+ gap: 30px;
+}
+
+.equipment-item {
+ text-align: center;
+}
+
+.equipment-value {
+ display: block;
+ font-weight: 500;
+ font-size: 40px;
+ color: #FFFFFF;
+ width: 120px;
+ height: 110px;
+ line-height: 110px;
+ background-image: url("@/assets/BI/shujutongji@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ margin-bottom: 8px;
+}
+
+.equipment-label {
+ font-weight: 500;
+ font-size: 21px;
+ color: #FFFFFE;
+}
+
+.event-info {
+ background-image: url("@/assets/BI/shijianmingchengbeijing@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ padding: 20px;
+ height: 186px;
+}
+.event-header {
+ display: flex;
+ align-items: center;
+}
+.event-icon {
+ width: 40px;
+ height: 40px;
+}
+.event-title {
+ font-weight: 500;
+ font-size: 24px;
+ color: #FFFFFE;
+ line-height: 30px;
+}
+.todo-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ height: 120px; /* 鎸夌敤鎴疯姹傝皟鏁撮珮搴� */
+ overflow: hidden;
+ font-size: 15px;
+}
+.todo-list li {
+ border-radius: 8px;
+ margin-bottom: 12px;
+ padding: 12px 40px;
+ height: 74px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.todo-title {
+ font-weight: 400;
+ font-size: 20px;
+ color: #FFFFFE;
+ position: relative;
+}
+.todo-title::before {
+ content: ''; /* 蹇呴渶锛岃〃绀鸿繖閲屾湁涓�涓唴瀹� */
+ position: absolute;
+ left: -10px; /* 瀹氫綅鍒板乏渚� */
+ top: 50%; /* 鍨傜洿灞呬腑 */
+ transform: translateY(-50%); /* 寰皟鍨傜洿灞呬腑 */
+ width: 6px; /* 鍦嗙殑鐩村緞 */
+ height: 6px; /* 鍦嗙殑鐩村緞 */
+ background: #498CEB;
+ border-radius: 50%; /* 璁╁叾鍙樻垚鍦嗗舰 */
+}
+.todo-division {
+ font-weight: 400;
+ font-size: 20px;
+ color: #FFFFFE;
+}
+.todo-time {
+ font-weight: 400;
+ font-size: 20px;
+ color: #FFFFFE;
+}
+.financial-header {
+ background-image: url("@/assets/BI/caiwufenxiback@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+.financial-title {
+ width: 100%;
+ font-weight: 500;
+ font-size: 16px;
+ color: #D9ECFF;
+ padding-left: 46px;
+ line-height: 36px;
+}
+
+/* 鑷畾涔夊崟閫夋寜閽粍鏍峰紡 */
+.custom-radio-group :deep(.el-radio-button__inner) {
+ background-color: transparent;
+ color: white;
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.custom-radio-group :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background-color: rgba(255, 255, 255, 0.2);
+ color: white;
+ border-color: rgba(255, 255, 255, 0.5);
+ box-shadow: -1px 0 0 0 rgba(255, 255, 255, 0.5);
+}
+
+/* 鐢熶骇璁㈠崟杩涘害琛ㄦ牸鏍峰紡 */
+.progress-table-container {
+ height: 200px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ margin-top: 10px;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE鍜孍dge */
+}
+
+.progress-table-container::-webkit-scrollbar {
+ display: none; /* Chrome銆丼afari鍜孫pera */
+}
+
+.progress-table {
+ width: 100%;
+ border-collapse: collapse;
+ color: #B8C8E0;
+ font-size: 12px;
+ table-layout: fixed;
+}
+
+.progress-table thead {
+ position: sticky;
+ top: 0;
+ background-color: rgba(26, 88, 176, 0.9);
+ z-index: 10;
+}
+
+.progress-table th {
+ padding: 8px 6px;
+ text-align: left;
+ font-weight: 500;
+ border-bottom: 1px solid rgba(184, 200, 224, 0.3);
+ color: #B8C8E0;
+ font-size: 12px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.progress-table th:nth-child(1) { width: 15%; } /* 鐢熶骇璁㈠崟鍙� */
+.progress-table th:nth-child(2) { width: 15%; } /* 浜у搧鍚嶇О */
+.progress-table th:nth-child(3) { width: 15%; } /* 瑙勬牸 */
+.progress-table th:nth-child(4) { width: 12%; } /* 闇�姹傛暟閲� */
+.progress-table th:nth-child(5) { width: 12%; } /* 瀹屾垚鏁伴噺 */
+.progress-table th:nth-child(6) { width: 31%; } /* 瀹屾垚杩涘害 */
+
+.progress-table td {
+ padding: 8px 6px;
+ border-bottom: 1px solid rgba(184, 200, 224, 0.1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 12px;
+ transition: opacity 0.3s ease;
+}
+
+.progress-table tbody tr:hover {
+ background-color: rgba(184, 200, 224, 0.1);
+}
+
+.progress-table tbody tr.row-under-header {
+ opacity: 0.5;
+}
+
+/* el-progress 缁勪欢鏍峰紡璋冩暣 */
+.progress-table :deep(.el-progress) {
+ width: 100%;
+}
+
+.progress-table :deep(.el-progress-bar__outer) {
+ background-color: rgba(184, 200, 224, 0.2);
+}
+
+.progress-table :deep(.el-progress__text) {
+ color: #B8C8E0;
+ font-size: 11px;
+}
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue b/src/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue
new file mode 100644
index 0000000..0c57b25
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue
@@ -0,0 +1,94 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="date-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍛�</el-radio-button>
+ <el-radio-button :label="2">鏈�</el-radio-button>
+ <el-radio-button :label="3">瀛e害</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍛�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+// 鐩戝惉澶栭儴鍊煎彉鍖�
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+// 澶勭悊鍊煎彉鍖�
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.date-type-switch {
+ display: inline-flex;
+}
+
+/* 鏈�変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+/* 绗竴涓寜閽乏渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+/* 鏈�鍚庝竴涓寜閽彸渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+/* 鎸夐挳涔嬮棿鐨勫垎闅旂嚎 */
+.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* 閫変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+/* 鎮仠鏁堟灉 */
+.date-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+/* 閫変腑鐘舵�佹偓鍋� */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/PanelHeader.vue b/src/views/reportAnalysis/financialAnalysis/components/PanelHeader.vue
new file mode 100644
index 0000000..313f1df
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/PanelHeader.vue
@@ -0,0 +1,33 @@
+<template>
+ <div class="panel-header">
+ <span class="panel-title">{{ title }}</span>
+ </div>
+</template>
+
+<script setup>
+defineProps({
+ title: {
+ type: String,
+ required: true,
+ default: ''
+ }
+})
+</script>
+
+<style scoped>
+.panel-header {
+ background-image: url("@/assets/BI/kehuhetongback@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.panel-title {
+ width: 100%;
+ font-weight: 500;
+ font-size: 16px;
+ color: #D9ECFF;
+ padding-left: 46px;
+ line-height: 36px;
+}
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/ProductTypeSwitch.vue b/src/views/reportAnalysis/financialAnalysis/components/ProductTypeSwitch.vue
new file mode 100644
index 0000000..98d07eb
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/ProductTypeSwitch.vue
@@ -0,0 +1,98 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="product-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button
+ v-for="opt in options"
+ :key="opt.label"
+ :label="opt.label"
+ >
+ {{ opt.text }}
+ </el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: [Number, String],
+ default: 1, // 榛樿閫変腑绗竴涓�
+ },
+ // 鍙厤缃�夐」锛岄粯璁ゆ槸鍘熺粍浠剁殑銆屽師鏉愭枡 / 鍗婃垚鍝� / 鎴愬搧銆�
+ options: {
+ type: Array,
+ default: () => [
+ { label: 1, text: '鍘熸潗鏂�' },
+ { label: 3, text: '鍗婃垚鍝�' },
+ { label: 2, text: '鎴愬搧' },
+ ],
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.product-type-switch {
+ display: inline-flex;
+}
+
+.product-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+.product-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/center-bottom.vue b/src/views/reportAnalysis/financialAnalysis/components/center-bottom.vue
new file mode 100644
index 0000000..20a612d
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/center-bottom.vue
@@ -0,0 +1,107 @@
+<template>
+ <div>
+ <PanelHeader title="鍒╂鼎瓒嬪娍" />
+ <div class="main-panel panel-item-customers">
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="barSeries1"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from './PanelHeader.vue'
+import { profitTrendAnalysis } from '@/api/viewIndex.js'
+
+const chartStyle = { width: '100%', height: '150%' }
+const grid = { left: '3%', right: '4%', bottom: '3%', top: '4%', containLabel: true }
+const barLegend = { show: false, textStyle: { color: '#B8C8E0' }, data: ['鍒╂鼎'] }
+const barSeries1 = ref([
+ {
+ name: '鍒╂鼎',
+ type: 'bar',
+ barGap: 0,
+ barWidth: 30,
+ emphasis: { focus: 'series' },
+ 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: [],
+ },
+])
+
+const xAxis1 = ref([
+ {
+ type: 'category',
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0' },
+ data: [],
+ },
+])
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ formatter(params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ result += `<div>${item.marker} ${item.seriesName}: ${item.value} 鍏�</div>`
+ })
+ return result
+ },
+}
+
+const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }]
+
+const fetchData = () => {
+ profitTrendAnalysis()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const list = res.data
+ xAxis1.value[0].data = list.map((d) => d.name)
+ barSeries1.value[0].data = list.map((d) => parseFloat(d.value) || 0)
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇鍒╂鼎瓒嬪娍鍒嗘瀽澶辫触:', err)
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 432px;
+}
+</style>
+
diff --git a/src/views/reportAnalysis/financialAnalysis/components/center-center.vue b/src/views/reportAnalysis/financialAnalysis/components/center-center.vue
new file mode 100644
index 0000000..7d32ebd
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/center-center.vue
@@ -0,0 +1,191 @@
+<template>
+ <div>
+ <!-- 璁惧缁熻 -->
+ <div class="equipment-stats">
+ <div class="equipment-header">
+ <img
+ src="@/assets/BI/shujutongjiicon@2x.png"
+ alt="鍥炬爣"
+ class="equipment-icon"
+ />
+ <span class="equipment-title">鏀舵敮瀵规瘮鍒嗘瀽</span>
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="lineLegend"
+ :series="lineSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import * as echarts from 'echarts'
+import Echarts from '@/components/Echarts/echarts.vue'
+import { incomeExpenseAnalysis } from '@/api/viewIndex.js'
+
+const chartStyle = { width: '100%', height: '100%' }
+const grid = {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ top: '16%',
+ containLabel: true,
+}
+
+const lineLegend = {
+ show: true,
+ top: '2%',
+ left: 'center',
+ itemGap: 24,
+ itemWidth: 12,
+ itemHeight: 12,
+ textStyle: { color: '#B8C8E0', fontSize: 14 },
+ data: [
+ { name: '鏀跺叆', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
+ { name: '鏀嚭', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
+ ],
+}
+
+const xAxis1 = ref([
+ {
+ type: 'category',
+ data: [],
+ axisTick: { show: false },
+ axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
+ axisLabel: { color: '#B8C8E0', fontSize: 12 },
+ splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
+ },
+])
+
+const yAxis1 = [
+ {
+ type: 'value',
+ name: '鍗曚綅: 鍏�',
+ nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0', fontSize: 12 },
+ splitLine: { lineStyle: { color: '#B8C8E0' } },
+ },
+]
+
+const lineSeries = ref([
+ {
+ name: '鏀跺叆',
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
+ itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
+ { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
+ ]),
+ },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+ {
+ name: '鏀嚭',
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
+ itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
+ { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
+ ]),
+ },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+])
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'line' },
+ formatter(params) {
+ let result = params[0].axisValue + '<br/>'
+ params.forEach((item) => {
+ result += `${item.marker} ${item.seriesName}: ${item.value} 鍏�<br/>`
+ })
+ return result
+ },
+}
+
+const fetchData = () => {
+ incomeExpenseAnalysis()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const list = res.data
+ xAxis1.value[0].data = list.map((d) => d.date)
+ lineSeries.value[0].data = list.map((d) => Number(d.income) || 0)
+ lineSeries.value[1].data = list.map((d) => Number(d.expense) || 0)
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇鏀舵敮瀵规瘮鍒嗘瀽澶辫触:', err)
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.equipment-stats {
+ border: 1px solid #1a58b0;
+ padding: 0 18px 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.equipment-header {
+ font-weight: 500;
+ font-size: 21px;
+ display: flex;
+ border-bottom: 1px solid;
+ border-image: linear-gradient(
+ 270deg,
+ rgba(0, 126, 255, 0) 0%,
+ rgba(0, 126, 255, 0.4549) 35%,
+ #007eff 78%,
+ #007eff 100%
+ )
+ 1;
+ padding-bottom: 2px;
+}
+
+.equipment-title {
+ font-weight: 500;
+ font-size: 18px;
+ background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ line-height: 50px;
+}
+
+.equipment-icon {
+ width: 50px;
+ height: 50px;
+}
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/center-top.vue b/src/views/reportAnalysis/financialAnalysis/components/center-top.vue
new file mode 100644
index 0000000..becb376
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/center-top.vue
@@ -0,0 +1,327 @@
+<template>
+ <div>
+ <!-- 椤堕儴鏀舵敮鍗$墖 -->
+ <div class="finance-cards">
+ <!-- 鏈堝害鏀跺叆 -->
+ <div class="finance-card income-card">
+ <div class="icon-box">
+ <img src="@/assets/BI/icon@2x.png" alt="鍥炬爣" class="card-icon" />
+ </div>
+ <div class="card-body">
+ <div class="card-left">
+ <div class="card-title">鏈堝害鏀跺叆</div>
+ <div class="card-amount">
+ <span>{{ formatAmountWanNumber(income.amount) }}</span>
+ <span v-if="isWanAmount(income.amount)" class="card-amount-unit">涓�</span>
+ </div>
+ </div>
+ <div class="card-right">
+ <div class="metric-row">
+ <span class="metric-label">鍥炴鐜�</span>
+ <span class="metric-value" :class="metricClass(income.repayRate)">
+ {{ formatPercent(income.repayRate.value) }}
+ <span class="arrow">{{ metricArrow(income.repayRate) }}</span>
+ </span>
+ </div>
+ <div class="metric-row">
+ <span class="metric-label">閫炬湡鏁�</span>
+ <span class="metric-value metric-up">
+ {{ formatAmountWanNumber(income.overdueCount) }}
+ <span
+ v-if="isWanAmount(income.overdueCount)"
+ class="metric-unit"
+ >
+ 涓�
+ </span>
+ </span>
+ </div>
+ <div class="metric-row">
+ <span class="metric-label">閫炬湡鐜�</span>
+ <span class="metric-value" :class="metricClass(income.overdueRate)">
+ {{ formatPercent(income.overdueRate.value) }}
+ <span class="arrow">{{ metricArrow(income.overdueRate) }}</span>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- 鏈堝害鏀嚭 -->
+ <div class="finance-card expense-card">
+ <div class="icon-box">
+ <img src="@/assets/BI/icon@2x.png" alt="鍥炬爣" class="card-icon" />
+ </div>
+ <div class="card-body">
+ <div class="card-left">
+ <div class="card-title">鏈堝害鏀嚭</div>
+ <div class="card-amount">
+ <span>{{ formatAmountWanNumber(expense.amount) }}</span>
+ <span v-if="isWanAmount(expense.amount)" class="card-amount-unit">涓�</span>
+ </div>
+ </div>
+ <div class="card-right">
+ <div class="metric-row">
+ <span class="metric-label">浠樻鐜�</span>
+ <span class="metric-value" :class="metricClass(expense.netProfit)">
+ {{ formatPercent(expense.netProfit.value) }}
+ <span class="arrow">{{ metricArrow(expense.netProfit) }}</span>
+ </span>
+ </div>
+ <div class="metric-row">
+ <span class="metric-label">姣涘埄娑�</span>
+ <span class="metric-value metric-down">
+ {{ formatAmountWanNumber(expense.grossProfit) }}
+ <span
+ v-if="isWanAmount(expense.grossProfit)"
+ class="metric-unit"
+ >
+ 涓�
+ </span>
+ </span>
+ </div>
+ <div class="metric-row">
+ <span class="metric-label">鍒╂鼎鐜�</span>
+ <span class="metric-value" :class="metricClass(expense.profitRate)">
+ {{ formatPercent(expense.profitRate.value) }}
+ <span class="arrow">{{ metricArrow(expense.profitRate) }}</span>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</template>
+
+<script setup>
+import { onMounted, ref } from 'vue'
+import { getMonthlyIncome, getMonthlyExpenditure } from '@/api/viewIndex'
+
+const income = ref({
+ amount: 0,
+ repayRate: { value: 0, trend: 0 },
+ overdueCount: 0,
+ overdueRate: { value: 0, trend: 0 },
+})
+
+const expense = ref({
+ amount: 0,
+ netProfit: { value: 0, trend: 0 },
+ grossProfit: 0,
+ profitRate: { value: 0, trend: 0 },
+})
+
+const toNumber = (val) => {
+ const num = Number(val)
+ return Number.isFinite(num) ? num : 0
+}
+
+const fetchMonthlyIncome = async () => {
+ try {
+ const res = await getMonthlyIncome()
+ const data = res?.data || {}
+
+ income.value.amount = toNumber(data.monthlyIncome)
+ const collectionRate = toNumber(data.collectionRate)
+ const overdueRate = toNumber(data.overdueRate)
+ income.value.repayRate = {
+ value: collectionRate,
+ trend: collectionRate >= 0 ? 1 : -1,
+ }
+ income.value.overdueCount = toNumber(data.overdueNum)
+ income.value.overdueRate = {
+ value: overdueRate,
+ trend: overdueRate >= 0 ? 1 : -1,
+ }
+ } catch {
+ income.value.amount = 0
+ income.value.repayRate = { value: 0, trend: 0 }
+ income.value.overdueCount = 0
+ income.value.overdueRate = { value: 0, trend: 0 }
+ }
+}
+
+const fetchMonthlyExpenditure = async () => {
+ try {
+ const res = await getMonthlyExpenditure()
+ const data = res?.data || {}
+
+ expense.value.amount = toNumber(data.monthlyExpenditure)
+ const paymentRate = toNumber(data.paymentRate)
+ expense.value.netProfit = {
+ value: paymentRate,
+ trend: paymentRate >= 0 ? 1 : -1,
+ }
+ expense.value.grossProfit = toNumber(data.grossProfit)
+
+ const profitMarginRate = toNumber(data.profitMarginRate)
+ expense.value.profitRate = {
+ value: profitMarginRate,
+ trend: profitMarginRate >= 0 ? 1 : -1,
+ }
+ } catch {
+ expense.value.amount = 0
+ expense.value.netProfit = { value: 0, trend: 0 }
+ expense.value.grossProfit = 0
+ expense.value.profitRate = { value: 0, trend: 0 }
+ }
+}
+
+const isWanAmount = (val) => {
+ const num = toNumber(val)
+ return Math.abs(num) >= 10000
+}
+
+const formatAmountWanNumber = (val) => {
+ const num = toNumber(val)
+ if (Math.abs(num) >= 10000) {
+ return (num / 10000).toFixed(2)
+ }
+ return num.toFixed(2)
+}
+
+const formatPercent = (val) => {
+ const num = toNumber(val)
+ // 鐧惧垎姣斿睍绀哄缁堢敤缁濆鍊硷紝灏忔暟淇濈暀涓や綅
+ return `${Math.abs(num).toFixed(2)}%`
+}
+
+const metricClass = (metric) => {
+ if (metric?.trend === undefined || metric?.trend === null) return 'metric-up'
+ return Number(metric.trend) >= 0 ? 'metric-up' : 'metric-down'
+}
+
+const metricArrow = (metric) => {
+ if (metric?.trend === undefined || metric?.trend === null) return ''
+ return Number(metric.trend) >= 0 ? '鈫�' : '鈫�'
+}
+
+onMounted(() => {
+ fetchMonthlyIncome()
+ fetchMonthlyExpenditure()
+})
+</script>
+
+<style scoped>
+.finance-cards {
+ display: flex;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.finance-card {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ padding: 18px 10px;
+ background-image: url('@/assets/BI/border@2x.png');
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ min-height: 138px;
+}
+
+.icon-box {
+ width: 92px;
+ height: 92px;
+ /* border: 1px dashed rgba(208, 231, 255, 0.55); */
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 18px;
+}
+
+.card-icon {
+ width: 78px;
+ height: 78px;
+}
+
+.card-body {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 18px;
+}
+
+.card-left {
+ min-width: 90px;
+}
+
+.card-title {
+ font-weight: 400;
+ font-size: 18px;
+ color: rgba(208, 231, 255, 0.7);
+}
+
+.card-amount {
+ font-weight: 500;
+ font-size: 36px;
+ line-height: 1.1;
+ margin-top: 8px;
+ display: inline-flex;
+ align-items: baseline;
+ white-space: nowrap;
+ background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.card-amount-unit {
+ font-size: 20px;
+ margin-left: 4px;
+}
+
+.card-right {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ padding-right: 6px;
+}
+
+.metric-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ color: #d0e7ff;
+ white-space: nowrap;
+}
+
+.metric-label {
+ margin-right: 12px;
+}
+
+.metric-label {
+ opacity: 0.8;
+}
+
+.metric-value {
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+}
+
+.metric-unit {
+ font-size: 12px;
+ margin-left: 2px;
+}
+
+.metric-value .arrow {
+ font-size: 13px;
+ margin-left: 4px;
+}
+
+.metric-up {
+ color: #00c853;
+}
+
+.metric-down {
+ color: #ff5252;
+}
+
+
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue b/src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue
new file mode 100644
index 0000000..3fe95d6
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/left-bottom.vue
@@ -0,0 +1,288 @@
+<template>
+ <div>
+ <PanelHeader title="鏋勬垚鍒嗘瀽" />
+ <div class="main-panel panel-item-customers">
+ <div class="filters-row">
+ <ProductTypeSwitch
+ v-model="amountType"
+ :options="amountTypeOptions"
+ @change="handleTypeChange"
+ />
+ </div>
+ <!-- <CarouselCards :items="cardItems" :visible-count="3" /> -->
+ <div class="pie-chart-wrapper" ref="pieWrapperRef">
+ <div class="pie-background" ref="pieBackgroundRef"></div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :legend="landLegend"
+ :series="landSeries"
+ :tooltip="landTooltip"
+ :color="landColors"
+ :options="pieOptions"
+ style="height: 320px"
+ class="land-chart"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from './PanelHeader.vue'
+import ProductTypeSwitch from './ProductTypeSwitch.vue'
+import { expenseCompositionAnalysis } from '@/api/viewIndex.js'
+import { useChartBackground } from '@/hooks/useChartBackground.js'
+
+const pieWrapperRef = ref(null)
+const pieBackgroundRef = ref(null)
+
+/**
+ * @introduction 鎶婃暟缁勪腑key鍊肩浉鍚岀殑閭d竴椤规彁鍙栧嚭鏉ワ紝缁勬垚涓�涓璞�
+ * @param {鍙傛暟绫诲瀷} array 浼犲叆鐨勬暟缁� [{a:"1",b:"2"},{a:"2",b:"3"}]
+ * @param {鍙傛暟绫诲瀷} key 灞炴�у悕 a
+ * @return {杩斿洖绫诲瀷璇存槑}
+ */
+function array2obj(array, key) {
+ const resObj = {}
+ for (let i = 0; i < array.length; i++) {
+ resObj[array[i][key]] = array[i]
+ }
+ return resObj
+}
+
+// 褰撳墠绫诲瀷锛�1=鏀嚭 2=鏀跺叆
+const amountType = ref(1)
+
+const amountTypeOptions = [
+ { label: 1, text: '浜у搧' },
+ { label: 2, text: '瀹㈡埛' },
+]
+
+// 鏁版嵁鍒楄〃锛堟潵鑷帴鍙o級
+const dataList = ref([])
+
+// 鍗$墖鏁版嵁
+const cardItems = ref([])
+
+// 棰滆壊鍒楄〃
+const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
+
+const landObjData = computed(() => array2obj(dataList.value, 'name'))
+
+// 鍥句緥閰嶇疆锛堝彸渚х珫鎺掞級
+const landLegend = computed(() => {
+ const data = dataList.value.map((d, idx) => ({
+ name: d.name,
+ icon: 'circle',
+ textStyle: {
+ fontSize: 18,
+ color: landColors[idx % landColors.length],
+ },
+ }))
+
+ return {
+ orient: 'vertical',
+ top: 'center',
+ left: '52%',
+ itemGap: 30,
+ show: true,
+ data: data,
+ formatter: function (name) {
+ const item = landObjData.value[name]
+ if (!item) return name
+ const num = Number(item.value)
+ const isWan = num > 10000
+ const displayValue = isWan ? (num / 10000).toFixed(2) : num
+ const displayUnit = isWan ? '涓囧厓' : '鍏�'
+ return `{title|${name}}{value|${displayValue}}{unit|${displayUnit}}{percent|${item.rate}}{unit|%}`
+ },
+ textStyle: {
+ rich: {
+ value: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 10],
+ },
+ unit: {
+ color: '#82baff',
+ fontSize: 12,
+ fontWeight: 600,
+ padding: [0, 10, 0, 0],
+ },
+ percent: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 0],
+ },
+ title: {
+ fontSize: 12,
+ padding: [0, 0, 0, 0],
+ },
+ },
+ },
+ }
+})
+
+// 鎻愮ず妗�
+const landTooltip = {
+ trigger: 'item',
+ formatter: '{a} <br/>{b} : {c}鍏� ({d}%)',
+}
+
+// 鍙屽眰鐜舰楗煎浘
+// 鍙屽眰鐜舰楗煎浘
+const landSeries = ref([
+ {
+ name: '鏋勬垚鍒嗘瀽',
+ type: 'pie',
+ radius: ['40%', '60%'],
+ center: ['25%', '50%'],
+ itemStyle: {
+ borderColor: '#0a1c3a',
+ borderWidth: 2,
+ color: function (params) {
+ return landColors[params.dataIndex % landColors.length]
+ },
+ },
+ label: {
+ show: false
+ },
+ minAngle: 15,
+ data: dataList.value,
+ animationType: 'scale',
+ animationEasing: 'elasticOut',
+ animationDelay: function () {
+ return Math.random() * 200
+ },
+ },
+ {
+ // 鍐呭湀
+ type: 'pie',
+ radius: ['40%', '45%'],
+ center: ['25%', '50%'],
+ silent: true,
+ label: {
+ show: false,
+ },
+ labelLine: {
+ show: false,
+ },
+ itemStyle: {
+ color: 'rgba(0, 127, 255, 0.25)',
+ },
+ data: [1],
+ },
+])
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const pieOptions = {
+ backgroundColor: 'transparent',
+ textStyle: { color: '#B8C8E0' },
+}
+
+// 浣跨敤灏佽鐨勮儗鏅綅缃皟鏁存柟娉�
+// 鍥捐〃涓績鏄� ['25%', '50%']锛岃儗鏅渶瑕佸榻愬埌杩欎釜浣嶇疆
+const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
+ wrapperRef: pieWrapperRef,
+ backgroundRef: pieBackgroundRef,
+ left: '25%', // 鍥捐〃涓績 X 鏄� 25%
+ top: '50%', // 鍥捐〃涓績 Y 鏄� 50%
+ offsetX: '-51.5%', // X 杞村亸绉�
+ offsetY: '-50%', // Y 杞村亸绉�
+ watchData: dataList // 鐩戝惉鏁版嵁鍙樺寲锛岃嚜鍔ㄨ皟鏁翠綅缃�
+})
+
+const fetchData = () => {
+ expenseCompositionAnalysis({ type: amountType.value })
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const items = res.data
+ cardItems.value = items.map((item) => ({
+ label: item.name,
+ value: item.value,
+ unit: '鍏�',
+ rate: item.rate,
+ }))
+ dataList.value = items.map((it) => ({
+ name: it.name,
+ value: parseFloat(it.value) || 0,
+ rate: it.rate,
+ children: [],
+ }))
+ landSeries.value[0].data = dataList.value
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇璐圭敤鏋勬垚鍒嗘瀽澶辫触:', err)
+ })
+}
+
+const handleTypeChange = () => {
+ fetchData()
+}
+
+onMounted(() => {
+ fetchData()
+ initBackground()
+})
+
+onBeforeUnmount(() => {
+ cleanupBackground()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.filters-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+
+.pie-chart-wrapper {
+ position: relative;
+ width: 100%;
+ height: 320px;
+ background: transparent;
+}
+
+
+.pie-background {
+ position: absolute;
+ width: 310px;
+ height: 310px;
+ background-image: url('@/assets/BI/鐜懓鍥捐竟妗�.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ z-index: 1;
+ pointer-events: none;
+ /* 浣嶇疆鐢� JS 鍔ㄦ�佽缃紝榛樿灞呬腑 */
+ left: 25%;
+ top: 50%;
+ transform: translate(-51.5%, -50%);
+}
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/components/left-top.vue b/src/views/reportAnalysis/financialAnalysis/components/left-top.vue
new file mode 100644
index 0000000..c735dba
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/components/left-top.vue
@@ -0,0 +1,134 @@
+<template>
+ <div>
+ <PanelHeader title="鍥炴涓庡紑绁ㄥ垎鏋�" />
+ <div class="main-panel panel-item-customers">
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="barSeries1"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { getAmountHalfYear } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+
+const chartStyle = {
+ width: '100%',
+ height: '160%',
+}
+
+const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
+const barLegend = {
+ show: true,
+ textStyle: { color: '#B8C8E0' },
+ data: ['寮�绁ㄩ噾棰�', '鍥炴閲戦'],
+}
+const barSeries1 = ref([
+ {
+ name: '寮�绁ㄩ噾棰�',
+ type: 'bar',
+ barWidth: 20,
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(0, 164, 237, 0)' },
+ { offset: 0, color: 'rgba(78, 228, 255, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+ {
+ name: '鍥炴閲戦',
+ type: 'bar',
+ barGap: '40%',
+ barWidth: 20,
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
+ { offset: 0, color: 'rgba(144, 97, 248, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+])
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ formatter(params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ result += `<div>${item.marker} ${item.seriesName}: ${item.value} 鍏�</div>`
+ })
+ return result
+ },
+}
+
+const xAxis1 = ref([{ type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] }])
+const yAxis1 = [{ type: 'value', axisLabel: { color: '#B8C8E0' } }]
+
+const fetchData = () => {
+ getAmountHalfYear()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const items = res.data
+ xAxis1.value[0].data = items.map((item) => item.month)
+ barSeries1.value[0].data = items.map(
+ (item) => parseFloat(item.invoiceAmount) || 0
+ )
+ barSeries1.value[1].data = items.map(
+ (item) => parseFloat(item.receiptAmount) || 0
+ )
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇杩戝崐骞村洖娆句笌寮�绁ㄦ暟鎹け璐�:', err)
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+</style>
diff --git a/src/views/reportAnalysis/financialAnalysis/index.vue b/src/views/reportAnalysis/financialAnalysis/index.vue
new file mode 100644
index 0000000..8d90a59
--- /dev/null
+++ b/src/views/reportAnalysis/financialAnalysis/index.vue
@@ -0,0 +1,291 @@
+<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 />
+ <CenterCenter/>
+ <CenterBottom />
+ </div>
+
+ <!-- 鍙充晶鍖哄煙 -->
+ <div class="right-panel">
+ <RightBottom />
+ <RightTop />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import autofit from 'autofit.js'
+import LeftBottom from './components/left-bottom.vue'
+import CenterCenter from './components/center-center.vue'
+import RightTop from '../dataDashboard/components/basic/right-top.vue'
+import RightBottom from '../dataDashboard/components/basic/right-bottom.vue'
+import useUserStore from '@/store/modules/user'
+import LeftTop from './components/left-top.vue'
+import CenterTop from './components/center-top.vue'
+import CenterBottom from './components/center-bottom.vue'
+
+// 鍏ㄥ睆鐩稿叧鐘舵��
+const isFullscreen = ref(false);
+
+// 缂╂斁姣斾緥
+const scaleRatio = ref(1)
+// 璁捐灏哄锛堝熀鍑嗗昂瀵革級- 鏍规嵁瀹為檯璁捐绋胯皟鏁�
+const designWidth = 1920
+const designHeight = 1080
+
+// 鐢ㄦ埛store
+const userStore = useUserStore()
+
+// 璁$畻缂╂斁姣斾緥
+const calculateScale = () => {
+ const container = document.querySelector('.scale-container')
+ if (!container) return
+
+ // 鑾峰彇瀹瑰櫒鐨勫疄闄呭昂瀵�
+ const rect = container.getBoundingClientRect?.()
+ const containerWidth = container.clientWidth || rect?.width || window.innerWidth
+ const containerHeight = container.clientHeight || rect?.height || window.innerHeight
+
+ // 璁$畻瀹介珮缂╂斁姣斾緥锛屽彇杈冨皬鍊间互淇濊瘉鍐呭瀹屾暣鏄剧ず锛堢瓑姣旂缉鏀撅級
+ const scaleX = containerWidth / designWidth
+ const scaleY = containerHeight / designHeight
+ scaleRatio.value = Math.min(scaleX, scaleY)
+}
+
+// 绐楀彛澶у皬鍙樺寲澶勭悊
+const handleResize = () => {
+ // 寤惰繜鎵ц锛岀‘淇滵OM鏇存柊瀹屾垚
+ setTimeout(() => {
+ calculateScale()
+ }, 100)
+}
+
+// 鍏ㄥ睆鍔熻兘瀹炵幇 - 閽堝scale-container鍏冪礌
+const toggleFullscreen = () => {
+ const element = document.querySelector('.scale-container')
+
+ if (!element) return
+
+ if (!isFullscreen.value) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen()
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen()
+ }
+ }
+}
+
+// 鐩戝惉鍏ㄥ睆鍙樺寲浜嬩欢
+const handleFullscreenChange = () => {
+ const fullscreenElement = document.fullscreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement
+ isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
+
+ // 鍏ㄥ睆鐘舵�佸彉鍖栨椂锛屽欢杩熼噸鏂拌绠楃缉鏀炬瘮渚嬶紙纭繚DOM鏇存柊瀹屾垚锛�
+ setTimeout(() => {
+ calculateScale()
+ }, 200)
+}
+
+// 鐢熷懡鍛ㄦ湡閽╁瓙
+onMounted(() => {
+ // 浣跨敤nextTick纭繚DOM瀹屽叏娓叉煋鍚庡啀鍒濆鍖�
+ nextTick(() => {
+ // 璁$畻鍒濆缂╂斁姣斾緥
+ calculateScale()
+ })
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('fullscreenchange', handleFullscreenChange)
+ window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.addEventListener('MSFullscreenChange', handleFullscreenChange)
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('resize', handleResize)
+ window.removeEventListener('fullscreenchange', handleFullscreenChange)
+ window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
+ // 绉婚櫎鎴戜滑娣诲姞鐨刟utofit鍔ㄦ�佽皟鏁寸洃鍚櫒
+ if (window._autofitUpdateHandler) {
+ window.removeEventListener('resize', window._autofitUpdateHandler)
+ delete window._autofitUpdateHandler
+ }
+ // 鍏抽棴autofit
+ autofit.off()
+})
+</script>
+
+<style scoped>
+/* 澶栭儴缂╂斁瀹瑰櫒 - 鍗犳嵁鏁翠釜瑙嗗彛 */
+.scale-container {
+position: relative;
+width: 100%;
+/* 椤甸潰鍦ㄥ父瑙勫竷灞�涓嬶紙鏈夐《鏍忥級榛樿鍑忓幓 84px锛岄伩鍏嶅唴瀹硅瑁佸垏 */
+height: calc(100vh - 84px);
+display: flex;
+align-items: center;
+justify-content: center;
+background-color: #000;
+overflow: hidden;
+}
+
+/* 鍐呴儴鍐呭鍖哄煙 - 鍥哄畾璁捐灏哄 */
+.data-dashboard {
+position: relative;
+width: 1920px;
+height: 1080px;
+background-image: url("@/assets/BI/backImage@2x.png");
+background-size: cover;
+background-position: center;
+background-repeat: no-repeat;
+transform-origin: center center;
+}
+
+/* 鍏ㄥ睆鐘舵�佺殑鏍峰紡 - 浣滅敤浜巗cale-container */
+.scale-container:fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* Webkit娴忚鍣ㄥ墠缂� */
+.scale-container:-webkit-full-screen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* MS娴忚鍣ㄥ墠缂� */
+.scale-container:-ms-fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+
+.dashboard-header {
+position: relative;
+z-index: 1;
+height: 86px;
+background-image: url("@/assets/BI/biaoti.png");
+background-size: cover;
+background-repeat: no-repeat;
+display: flex;
+align-items: center;
+justify-content: center;
+}
+
+.factory-name {
+font-weight: 600;
+font-size: 52px;
+color: #FFFFFF;
+top: 16px;
+position: absolute;
+}
+
+.fullscreen-btn {
+position: absolute;
+top: 10px;
+left: 20px;
+width: 40px;
+height: 40px;
+background: rgba(0, 20, 60, 0.8);
+border: 1px solid rgba(0, 212, 255, 0.3);
+border-radius: 6px;
+color: #00d4ff;
+cursor: pointer;
+display: flex;
+align-items: center;
+justify-content: center;
+transition: all 0.3s;
+z-index: 10000;
+}
+
+.fullscreen-btn:hover {
+background: rgba(0, 30, 90, 0.9);
+border-color: rgba(0, 212, 255, 0.5);
+}
+
+.dashboard-content {
+position: relative;
+z-index: 1;
+display: flex;
+gap: 30px;
+padding: 0 30px;
+height: calc(100% - 86px);
+overflow: hidden;
+}
+
+/* 纭繚鍚勯潰鏉胯兘澶熸纭樉绀� */
+.left-panel, .center-panel, .right-panel {
+overflow: hidden;
+}
+
+.left-panel,
+.right-panel {
+flex: 1;
+display: flex;
+flex-direction: column;
+gap: 24px;
+width: 520px;
+}
+
+.center-panel {
+flex: 1.5;
+display: flex;
+flex-direction: column;
+gap: 20px;
+}
+
+</style>
\ No newline at end of file
diff --git a/src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue b/src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue
new file mode 100644
index 0000000..0498824
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/CarouselCards.vue
@@ -0,0 +1,306 @@
+<template>
+ <div class="carousel-cards">
+ <button
+ v-if="canScrollLeft"
+ class="nav-button nav-button-left"
+ @click="scrollLeftFn"
+ >
+ <img src="@/assets/BI/jiantou.png" alt="宸︾澶�" />
+ </button>
+ <div
+ class="cards-container"
+ :style="{ '--visible-count': visibleCount }"
+ ref="cardsContainerRef"
+ >
+ <div
+ v-for="(item, index) in items"
+ :key="index"
+ class="card-item"
+ >
+ <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div>
+ <div class="card-title">
+ <div class="card-label">{{ item.label }}</div>
+ <div class="card-value">
+ <span class="value-number">{{ item.value }}</span>
+ <span class="value-unit">{{ item.unit }}</span>
+ </div>
+ <div v-if="item.rate ?? item.ratio ?? item.percent" class="card-rate">
+ <span class="rate-value">{{ item.rate ?? item.ratio ?? item.percent }}%</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <button
+ v-if="canScrollRight"
+ class="nav-button nav-button-right"
+ @click="scrollRightFn"
+ >
+ <img src="@/assets/BI/jiantou.png" alt="鍙崇澶�" />
+ </button>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
+
+const props = defineProps({
+ items: {
+ type: Array,
+ default: () => [],
+ validator: (value) => {
+ return value.every(item =>
+ item && typeof item.label !== 'undefined' &&
+ typeof item.value !== 'undefined' &&
+ typeof item.unit !== 'undefined'
+ )
+ }
+ },
+ visibleCount: {
+ type: Number,
+ default: 3
+ }
+})
+
+const cardsContainerRef = ref(null)
+const currentScrollLeft = ref(0)
+const maxScrollLeft = ref(0)
+
+// 妫�鏌ユ槸鍚﹀彲浠ュ悜宸︽粴鍔�
+const canScrollLeft = computed(() => {
+ return currentScrollLeft.value > 0
+})
+
+// 妫�鏌ユ槸鍚﹀彲浠ュ悜鍙虫粴鍔�
+const canScrollRight = computed(() => {
+ return currentScrollLeft.value < maxScrollLeft.value
+})
+
+// 鏇存柊婊氬姩鐘舵��
+const updateScrollState = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ currentScrollLeft.value = container.scrollLeft
+ maxScrollLeft.value = container.scrollWidth - container.clientWidth
+}
+
+// 鍚戝乏婊氬姩
+const scrollLeftFn = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ const scrollItems = Array.from(container.querySelectorAll('.card-item'))
+ if (scrollItems.length === 0) return
+
+ const itemWidth = scrollItems[0]?.offsetWidth || 0
+ const gap = 12
+ const scrollDistance = itemWidth + gap
+
+ container.scrollBy({
+ left: -scrollDistance,
+ behavior: 'smooth'
+ })
+
+ // 寤惰繜鏇存柊鐘舵�侊紝绛夊緟婊氬姩鍔ㄧ敾瀹屾垚
+ setTimeout(() => {
+ updateScrollState()
+ }, 300)
+}
+
+// 鍚戝彸婊氬姩
+const scrollRightFn = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ const scrollItems = Array.from(container.querySelectorAll('.card-item'))
+ if (scrollItems.length === 0) return
+
+ const itemWidth = scrollItems[0]?.offsetWidth || 0
+ const gap = 12
+ const scrollDistance = itemWidth + gap
+
+ container.scrollBy({
+ left: scrollDistance,
+ behavior: 'smooth'
+ })
+
+ // 寤惰繜鏇存柊鐘舵�侊紝绛夊緟婊氬姩鍔ㄧ敾瀹屾垚
+ setTimeout(() => {
+ updateScrollState()
+ }, 300)
+}
+
+// 鐩戝惉 items 鍙樺寲锛屾洿鏂版粴鍔ㄧ姸鎬�
+watch(() => props.items, () => {
+ nextTick(() => {
+ updateScrollState()
+ })
+}, { deep: true })
+
+onMounted(() => {
+ nextTick(() => {
+ updateScrollState()
+ // 鐩戝惉婊氬姩浜嬩欢
+ const container = cardsContainerRef.value
+ if (container) {
+ container.addEventListener('scroll', updateScrollState)
+ }
+ })
+})
+
+onBeforeUnmount(() => {
+ // 娓呯悊婊氬姩浜嬩欢鐩戝惉鍣�
+ const container = cardsContainerRef.value
+ if (container) {
+ container.removeEventListener('scroll', updateScrollState)
+ }
+})
+</script>
+
+<style scoped>
+.carousel-cards {
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.cards-container {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
+ padding-bottom: 4px;
+ scroll-behavior: smooth;
+}
+
+.cards-container::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+.nav-button {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 32px;
+ height: 32px;
+ background: rgba(26, 88, 176, 0.6);
+ border: 1px solid rgba(26, 88, 176, 0.8);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 10;
+ transition: all 0.3s ease;
+ padding: 0;
+}
+
+.nav-button:hover {
+ background: rgba(26, 88, 176, 0.8);
+ transform: translateY(-50%) scale(1.1);
+}
+
+.nav-button-left {
+ left: -16px;
+}
+
+.nav-button-left img {
+ width: 16px;
+ height: 16px;
+ transform: rotate(180deg);
+}
+
+.nav-button-right {
+ right: -16px;
+}
+
+.nav-button-right img {
+ width: 16px;
+ height: 16px;
+}
+
+.card-item {
+ flex: 0 0 calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
+ min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
+ display: flex;
+ align-items: center;
+ background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
+ border-radius: 8px 8px 8px 8px;
+ padding: 12px 16px;
+ transition: all 0.3s ease;
+}
+
+.card-item:hover {
+ transform: translateY(-2px);
+}
+
+.card-icon {
+ width: 80px;
+ height: 60px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ flex-shrink: 0;
+ margin-right: 12px;
+}
+
+.card-title {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ flex: 1;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 14px;
+ color: #FFFFFF;
+ margin-bottom: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+}
+
+.card-value {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+}
+
+.card-rate {
+ margin-top: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 400;
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.85);
+}
+
+.rate-label {
+ opacity: 0.85;
+}
+
+.rate-value {
+ font-weight: 500;
+}
+
+.value-number {
+ font-weight: 400;
+ font-size: 14px;
+ color: #FFFFFF;
+ line-height: 1;
+}
+
+.value-unit {
+ font-size: 14px;
+ color: #FFFFFF;
+ font-weight: 400;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue b/src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue
new file mode 100644
index 0000000..0c57b25
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/DateTypeSwitch.vue
@@ -0,0 +1,94 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="date-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍛�</el-radio-button>
+ <el-radio-button :label="2">鏈�</el-radio-button>
+ <el-radio-button :label="3">瀛e害</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍛�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+// 鐩戝惉澶栭儴鍊煎彉鍖�
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+// 澶勭悊鍊煎彉鍖�
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.date-type-switch {
+ display: inline-flex;
+}
+
+/* 鏈�変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+/* 绗竴涓寜閽乏渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+/* 鏈�鍚庝竴涓寜閽彸渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+/* 鎸夐挳涔嬮棿鐨勫垎闅旂嚎 */
+.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* 閫変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+/* 鎮仠鏁堟灉 */
+.date-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+/* 閫変腑鐘舵�佹偓鍋� */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue b/src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue
new file mode 100644
index 0000000..313f1df
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/PanelHeader.vue
@@ -0,0 +1,33 @@
+<template>
+ <div class="panel-header">
+ <span class="panel-title">{{ title }}</span>
+ </div>
+</template>
+
+<script setup>
+defineProps({
+ title: {
+ type: String,
+ required: true,
+ default: ''
+ }
+})
+</script>
+
+<style scoped>
+.panel-header {
+ background-image: url("@/assets/BI/kehuhetongback@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.panel-title {
+ width: 100%;
+ font-weight: 500;
+ font-size: 16px;
+ color: #D9ECFF;
+ padding-left: 46px;
+ line-height: 36px;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue b/src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue
new file mode 100644
index 0000000..87cde44
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/ProductTypeSwitch.vue
@@ -0,0 +1,85 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="product-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍘熸潗鏂�</el-radio-button>
+ <el-radio-button :label="3">鍗婃垚鍝�</el-radio-button>
+ <el-radio-button :label="2">鎴愬搧</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍘熸潗鏂�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.product-type-switch {
+ display: inline-flex;
+}
+
+.product-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+.product-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue b/src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue
new file mode 100644
index 0000000..f4f4024
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/center-bottom.vue
@@ -0,0 +1,362 @@
+<template>
+ <div>
+ <PanelHeader title="鐢熶骇璁㈠崟瀹屾垚杩涘害" />
+ <div class="main-panel">
+ <div class="panel-item-customers">
+ <CarouselCards :items="cardItems" :visible-count="4" />
+ <div
+ class="progress-table-container"
+ ref="progressTableRef"
+ style="margin-top: 0px;"
+ @scroll="handleTableScroll"
+ >
+ <table class="progress-table">
+ <thead>
+ <tr>
+ <th>鐢熶骇璁㈠崟鍙�</th>
+ <th>浜у搧鍚嶇О</th>
+ <th>瑙勬牸</th>
+ <th>闇�姹傛暟閲�</th>
+ <th>瀹屾垚鏁伴噺</th>
+ <th>瀹屾垚杩涘害</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="(item, index) in progressTableData"
+ :key="index"
+ :ref="(el) => setRowRef(el, index)"
+ :class="{ 'row-under-header': isRowUnderHeader(index) }"
+ >
+ <td>{{ item.npsNo || '-' }}</td>
+ <td>{{ item.productCategory || '-' }}</td>
+ <td>{{ item.specificationModel || '-' }}</td>
+ <td>{{ item.quantity || 0 }}</td>
+ <td>{{ item.completeQuantity || 0 }}</td>
+ <td>
+ <el-progress
+ :percentage="calculateProgress(item)"
+ :color="progressColor(calculateProgress(item))"
+ :status="calculateProgress(item) >= 100 ? 'success' : ''"
+ :stroke-width="8"
+ />
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick, inject, watch } from 'vue'
+import { getProgressStatistics } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+import CarouselCards from './CarouselCards.vue'
+
+const progressTableRef = ref(null)
+const progressTableScrollTimer = ref(null)
+const tableScrollTimeout = ref(null)
+const tableRowRefs = ref([])
+const rowsUnderHeader = ref(new Set())
+
+// 璁㈠崟缁熻瀵硅薄
+const orderStatisticsObject = ref({
+ totalOrderCount: 0,
+ uncompletedOrderCount: 0,
+ partialCompletedOrderCount: 0,
+ completedOrderCount: 0,
+})
+
+// 杞挱鍗$墖鏁版嵁锛堢敱 orderStatisticsObject 鍚屾锛�
+const cardItems = ref([])
+
+// 鐢熶骇璁㈠崟瀹屾垚杩涘害琛ㄦ牸鏁版嵁
+const progressTableData = ref([])
+
+// 璁$畻瀹屾垚杩涘害鐧惧垎姣�
+const calculateProgress = (item) => {
+ if (!item) return 0
+ if (item.completionStatus !== undefined && item.completionStatus !== null) {
+ const percentage = Number(item.completionStatus)
+ if (isNaN(percentage)) return 0
+ return Math.min(Math.max(Math.round(percentage), 0), 100)
+ }
+ if (!item.quantity || item.quantity === 0) return 0
+ const percentage = ((item.completeQuantity || 0) / item.quantity) * 100
+ return Math.min(Math.max(Math.round(percentage), 0), 100)
+}
+
+// 鏍规嵁杩涘害鐧惧垎姣旇繑鍥為鑹�
+const progressColor = (percentage) => {
+ const p = percentage || 0
+ if (p < 30) return '#f56c6c'
+ if (p < 50) return '#e6a23c'
+ if (p < 80) return '#409eff'
+ return '#67c23a'
+}
+
+const setRowRef = (el, index) => {
+ if (el) {
+ tableRowRefs.value[index] = el
+ }
+}
+
+const isRowUnderHeader = (index) => rowsUnderHeader.value.has(index)
+
+const handleTableScroll = () => {
+ const tableContainer = progressTableRef.value
+ if (!tableContainer) return
+ const thead = tableContainer.querySelector('thead')
+ if (!thead) return
+ const theadHeight = thead.offsetHeight
+ const containerRect = tableContainer.getBoundingClientRect()
+ const containerTop = containerRect.top
+ const theadBottom = containerTop + theadHeight
+ rowsUnderHeader.value.clear()
+ tableRowRefs.value.forEach((row, index) => {
+ if (row) {
+ const rowRect = row.getBoundingClientRect()
+ const rowTop = rowRect.top
+ const rowBottom = rowRect.bottom
+ if (rowTop < theadBottom && rowBottom > containerTop) {
+ rowsUnderHeader.value.add(index)
+ }
+ }
+ })
+ if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
+ tableScrollTimeout.value = setTimeout(() => {
+ rowsUnderHeader.value.clear()
+ }, 150)
+}
+
+const stopProgressTableScroll = () => {
+ if (progressTableScrollTimer.value) {
+ cancelAnimationFrame(progressTableScrollTimer.value)
+ progressTableScrollTimer.value = null
+ }
+ const tableContainer = progressTableRef.value
+ if (tableContainer?._pauseTimer) {
+ clearInterval(tableContainer._pauseTimer)
+ tableContainer._pauseTimer = null
+ }
+}
+
+const initProgressTableScroll = () => {
+ const tableContainer = progressTableRef.value
+ if (!tableContainer) return
+ stopProgressTableScroll()
+ const tbody = tableContainer.querySelector('tbody')
+ if (!tbody) return
+ const originalCount = progressTableData.value.length
+ const allRows = Array.from(tbody.querySelectorAll('tr'))
+ if (allRows.length > originalCount) {
+ for (let i = originalCount; i < allRows.length; i++) {
+ allRows[i].remove()
+ }
+ }
+ const scrollItems = Array.from(tbody.querySelectorAll('tr'))
+ if (scrollItems.length === 0) return
+ const originalItemCount = scrollItems.length
+ const thead = tableContainer.querySelector('thead')
+ const theadHeight = thead ? thead.offsetHeight : 40
+ const containerHeight = tableContainer.clientHeight
+ const visibleHeight = containerHeight - theadHeight
+ const itemHeight = scrollItems[0]?.offsetHeight || 40
+ const totalContentHeight = itemHeight * originalItemCount
+ if (totalContentHeight <= visibleHeight) return
+ const cloneCount = Math.ceil(visibleHeight / itemHeight) + 2
+ for (let i = 0; i < cloneCount; i++) {
+ const clone = scrollItems[i % originalItemCount].cloneNode(true)
+ tbody.appendChild(clone)
+ }
+ let scrollPosition = 0
+ const scrollSpeed = 1.5
+ const pauseTime = 3000
+ let isPaused = false
+ let lastTimestamp = 0
+ function scrollAnimation(timestamp) {
+ if (!lastTimestamp) lastTimestamp = timestamp
+ const deltaTime = timestamp - lastTimestamp
+ lastTimestamp = timestamp
+ if (!isPaused) {
+ scrollPosition += scrollSpeed * (deltaTime / 16)
+ const maxScroll = itemHeight * originalItemCount
+ if (scrollPosition >= maxScroll) {
+ scrollPosition = 0
+ tableContainer.scrollTop = 0
+ } else {
+ tableContainer.scrollTop = scrollPosition
+ }
+ }
+ progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
+ }
+ progressTableScrollTimer.value = requestAnimationFrame(scrollAnimation)
+ const pauseTimer = setInterval(() => {
+ isPaused = !isPaused
+ }, pauseTime)
+ tableContainer._pauseTimer = pauseTimer
+}
+
+const progressStatisticsInfo = () => {
+ getProgressStatistics()
+ .then((res) => {
+ stopProgressTableScroll()
+ if (!res || !res.data) return
+ const obj = {
+ totalOrderCount: res.data.totalOrderCount || 0,
+ uncompletedOrderCount: res.data.uncompletedOrderCount || 0,
+ partialCompletedOrderCount: res.data.partialCompletedOrderCount || 0,
+ completedOrderCount: res.data.completedOrderCount || 0,
+ }
+ orderStatisticsObject.value = obj
+ cardItems.value = [
+ { label: '鎬昏鍗曟暟', value: obj.totalOrderCount, unit: '浠�' },
+ { label: '鏈畬鎴愯鍗曟暟', value: obj.uncompletedOrderCount, unit: '浠�' },
+ { label: '閮ㄥ垎瀹屾垚璁㈠崟鏁�', value: obj.partialCompletedOrderCount, unit: '浠�' },
+ { label: '宸插畬鎴愯鍗曟暟', value: obj.completedOrderCount, unit: '浠�' },
+ ]
+ progressTableData.value = res.data.completedOrderDetails || []
+ tableRowRefs.value = []
+ rowsUnderHeader.value.clear()
+ nextTick(() => {
+ initProgressTableScroll()
+ })
+ })
+ .catch((err) => {
+ console.error('鑾峰彇鐢熶骇璁㈠崟瀹屾垚杩涘害缁熻澶辫触:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ progressStatisticsInfo()
+ })
+}
+
+onMounted(() => {
+ progressStatisticsInfo()
+})
+
+onBeforeUnmount(() => {
+ stopProgressTableScroll()
+ if (tableScrollTimeout.value) clearTimeout(tableScrollTimeout.value)
+ const tableContainer = progressTableRef.value
+ if (tableContainer?._pauseTimer) {
+ clearInterval(tableContainer._pauseTimer)
+ }
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 428px;
+}
+
+.progress-table-container {
+ height: 320px;
+ overflow-y: auto;
+ overflow-x: hidden;
+ margin-top: 10px;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+
+.progress-table-container::-webkit-scrollbar {
+ display: none;
+}
+
+.progress-table {
+ width: 100%;
+ border-collapse: collapse;
+ color: #b8c8e0;
+ font-size: 12px;
+ table-layout: fixed;
+}
+
+.progress-table thead {
+ position: sticky;
+ top: 0;
+ background-color: rgba(26, 88, 176, 0.9);
+ z-index: 10;
+}
+
+.progress-table th {
+ padding: 8px 6px;
+ text-align: left;
+ font-weight: 500;
+ border-bottom: 1px solid rgba(184, 200, 224, 0.3);
+ color: #b8c8e0;
+ font-size: 12px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.progress-table th:nth-child(1) {
+ width: 15%;
+}
+
+.progress-table th:nth-child(2) {
+ width: 15%;
+}
+
+.progress-table th:nth-child(3) {
+ width: 15%;
+}
+
+.progress-table th:nth-child(4) {
+ width: 12%;
+}
+
+.progress-table th:nth-child(5) {
+ width: 12%;
+}
+
+.progress-table th:nth-child(6) {
+ width: 31%;
+}
+
+.progress-table td {
+ padding: 8px 6px;
+ border-bottom: 1px solid rgba(184, 200, 224, 0.1);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 12px;
+ transition: opacity 0.3s ease;
+}
+
+.progress-table tbody tr:hover {
+ background-color: rgba(184, 200, 224, 0.1);
+}
+
+.progress-table tbody tr.row-under-header {
+ opacity: 0.5;
+}
+
+.progress-table :deep(.el-progress) {
+ width: 100%;
+}
+
+.progress-table :deep(.el-progress-bar__outer) {
+ background-color: rgba(184, 200, 224, 0.2);
+}
+
+.progress-table :deep(.el-progress__text) {
+ color: #b8c8e0;
+ font-size: 11px;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/center-center.vue b/src/views/reportAnalysis/productionAnalysis/components/center-center.vue
new file mode 100644
index 0000000..a65f4f8
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/center-center.vue
@@ -0,0 +1,200 @@
+<template>
+ <div>
+ <!-- 璁惧缁熻 -->
+ <div class="equipment-stats">
+ <div class="equipment-header">
+ <img
+ src="@/assets/BI/shujutongjiicon@2x.png"
+ alt="鍥炬爣"
+ class="equipment-icon"
+ />
+ <span class="equipment-title">鎶曞叆浜у嚭鍒嗘瀽</span>
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="lineLegend"
+ :series="lineSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import * as echarts from 'echarts'
+import Echarts from '@/components/Echarts/echarts.vue'
+import { inputOutputAnalysis } from '@/api/viewIndex.js'
+
+const chartStyle = { width: '100%', height: '100%' }
+const grid = {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ top: '16%',
+ containLabel: true,
+}
+
+const lineLegend = {
+ show: true,
+ top: '2%',
+ left: 'center',
+ itemGap: 24,
+ itemWidth: 12,
+ itemHeight: 12,
+ textStyle: { color: '#B8C8E0', fontSize: 14 },
+ data: [
+ { name: '浜у嚭', itemStyle: { color: 'rgba(11, 137, 254, 1)' } },
+ { name: '鎶曞叆', itemStyle: { color: 'rgba(11, 249, 254, 1)' } },
+ ],
+}
+
+const xAxis1 = ref([
+ {
+ type: 'category',
+ data: [],
+ axisTick: { show: false },
+ axisLine: { show: false, lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
+ axisLabel: { color: '#B8C8E0', fontSize: 12 },
+ splitLine: { show: false, lineStyle: { type: 'dashed', color: 'rgba(184, 200, 224, 0.2)' } },
+ },
+])
+
+const yAxis1 = [
+ {
+ type: 'value',
+ name: '鍗曚綅: 浠�',
+ nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 0] },
+ axisLine: { show: false },
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0', fontSize: 12 },
+ splitLine: { lineStyle: { color: '#B8C8E0' } },
+ },
+]
+
+const lineSeries = ref([
+ {
+ name: '浜у嚭',
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(11, 137, 254, 1)', width: 2 },
+ itemStyle: { color: 'rgba(11, 137, 254, 1)', borderWidth: 0 },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(11, 137, 254, 0.40)' },
+ { offset: 1, color: 'rgba(11, 137, 254, 0.05)' },
+ ]),
+ },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+ {
+ name: '鎶曞叆',
+ type: 'line',
+ smooth: false,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(11, 249, 254, 1)', width: 2 },
+ itemStyle: { color: 'rgba(11, 249, 254, 1)', borderWidth: 0 },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(11, 249, 254, 0.5)' },
+ { offset: 1, color: 'rgba(11, 249, 254, 0.05)' },
+ ]),
+ },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+])
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'line' },
+ borderWidth: 1,
+ textStyle: { fontSize: 12 },
+ formatter(params) {
+ let result = params[0].axisValue + '<br/>'
+ params.forEach((item) => {
+ result += `${item.marker} ${item.seriesName}: ${item.value} 浠�<br/>`
+ })
+ return result
+ },
+}
+
+const fetchData = () => {
+ inputOutputAnalysis()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const list = res.data
+ xAxis1.value[0].data = list.map((d) => d.date)
+ lineSeries.value[0].data = list.map((d) => Number(d.outputSum) || 0)
+ lineSeries.value[1].data = list.map((d) => Number(d.inputSum) || 0)
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇鎶曞叆浜у嚭鍒嗘瀽澶辫触:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.equipment-stats {
+ border: 1px solid #1a58b0;
+ padding: 0 18px 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.equipment-header {
+ font-weight: 500;
+ font-size: 21px;
+ display: flex;
+ border-bottom: 1px solid;
+ border-image: linear-gradient(
+ 270deg,
+ rgba(0, 126, 255, 0) 0%,
+ rgba(0, 126, 255, 0.4549) 35%,
+ #007eff 78%,
+ #007eff 100%
+ )
+ 1;
+ padding-bottom: 2px;
+}
+
+.equipment-title {
+ font-weight: 500;
+ font-size: 18px;
+ background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ line-height: 50px;
+}
+
+.equipment-icon {
+ width: 50px;
+ height: 50px;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/center-top.vue b/src/views/reportAnalysis/productionAnalysis/components/center-top.vue
new file mode 100644
index 0000000..a806150
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/center-top.vue
@@ -0,0 +1,144 @@
+<template>
+ <div>
+ <!-- 椤堕儴缁熻鍗$墖 -->
+ <div class="stats-cards">
+ <div
+ v-for="item in statItems"
+ :key="item.name"
+ class="stat-card"
+ >
+ <img src="@/assets/BI/icon@2x.png" alt="鍥炬爣" class="card-icon" />
+ <div class="card-content">
+ <span class="card-label">{{ item.name }}</span>
+ <span class="card-value">{{ item.value }}</span>
+ <div class="card-compare" :class="compareClass(Number(item.rate))">
+ <span>鍚屾瘮</span>
+ <span class="compare-value">{{ formatPercent(item.rate) }}</span>
+ <span class="compare-icon">{{ Number(item.rate) >= 0 ? '鈫�' : '鈫�' }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import { orderCount } from '@/api/viewIndex.js'
+
+const statItems = ref([])
+
+const formatPercent = (val) => {
+ const num = Number(val) || 0
+ return `${num.toFixed(2)}%`
+}
+
+const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
+
+const fetchData = () => {
+ orderCount()
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ statItems.value = res.data.map((item) => ({
+ name: item.name,
+ value: item.value,
+ rate: item.rate,
+ }))
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇璁㈠崟鏁伴噺缁熻澶辫触:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.stats-cards {
+ display: flex;
+ gap: 30px;
+}
+
+.stat-card {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ background-image: url('@/assets/BI/border@2x.png');
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 142px;
+}
+
+.card-icon {
+ width: 100px;
+ height: 100px;
+ margin: 20px 20px 0 10px;
+}
+
+.card-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.card-value {
+ font-weight: 500;
+ font-size: 40px;
+ background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 16px;
+ color: rgba(208, 231, 255, 0.7);
+}
+
+.card-compare {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 15px;
+ color: #d0e7ff;
+}
+
+.card-compare > span:first-child {
+ font-size: 13px;
+ opacity: 0.8;
+}
+
+.compare-value {
+ font-weight: 600;
+}
+
+.compare-icon {
+ font-size: 14px;
+ position: relative;
+ top: -1px; /* 杞诲井涓婄Щ锛岃绠ご涓庢枃瀛楀瀭鐩村眳涓榻� */
+}
+
+.compare-up .compare-value,
+.compare-up .compare-icon {
+ color: #00c853;
+}
+
+.compare-down .compare-value,
+.compare-down .compare-icon {
+ color: #ff5252;
+}
+
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue b/src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue
new file mode 100644
index 0000000..b7c7358
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue
@@ -0,0 +1,179 @@
+<template>
+ <div>
+ <PanelHeader title="鍦ㄥ埗鍝佺粺璁″垎鏋�" />
+ <div class="main-panel panel-item-customers">
+ <CarouselCards :items="cardItems" :visible-count="3" />
+ <div class="chart-wrapper">
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="workInProcessBarLegend"
+ :series="workInProcessBarSeries"
+ :tooltip="tooltip"
+ :xAxis="workInProcessXAxis"
+ :yAxis="workInProcessYAxis"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 100%"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from './PanelHeader.vue'
+import CarouselCards from './CarouselCards.vue'
+import { getWorkInProcessTurnover } from '@/api/viewIndex.js'
+
+// 鍦ㄥ埗鍝佸懆杞粺璁″璞�
+const workInProcessStatistics = ref({
+ totalQuantity: 0,
+ avgTurnoverDays: 0,
+ turnoverEfficiency: 0,
+})
+
+// 杞挱鍗$墖鏁版嵁锛堢敱 workInProcessStatistics 鍚屾锛�
+const cardItems = ref([])
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const grid = {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true,
+}
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'shadow' },
+ formatter: function (params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ result += `<div style="color: #B8C8E0">${item.marker} ${item.seriesName}: ${item.value}</div>`
+ })
+ return result
+ },
+}
+
+// 鍦ㄥ埗鍝佸伐搴忔煴鐘跺浘閰嶇疆
+const workInProcessXAxis = ref([
+ {
+ type: 'category',
+ axisTick: { show: false },
+ axisLabel: { color: '#B8C8E0' },
+ data: [],
+ },
+])
+const workInProcessYAxis = [
+ {
+ type: 'value',
+ axisLabel: { color: '#B8C8E0' },
+ name: '',
+ },
+]
+const workInProcessBarLegend = {
+ show: false,
+ textStyle: { color: '#B8C8E0' },
+ data: [],
+}
+const workInProcessBarSeries = ref([
+ {
+ name: '鍦ㄥ埗鍝佹暟閲�',
+ type: 'bar',
+ barWidth: 25,
+ barGap: 0,
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(0,164,237,0)' },
+ { offset: 0, color: '#4EE4FF' },
+ ],
+ },
+ },
+ label: {
+ show: true,
+ position: 'top',
+ color: '#B8C8E0',
+ },
+ data: [],
+ },
+])
+
+const workInProcessTurnoverInfo = () => {
+ getWorkInProcessTurnover()
+ .then((res) => {
+ if (!res || !res.data) return
+ const stats = {
+ totalQuantity: res.data.totalOrderCount || 0,
+ avgTurnoverDays: res.data.averageTurnoverDays || 0,
+ turnoverEfficiency: res.data.turnoverEfficiency || 0,
+ }
+ workInProcessStatistics.value = stats
+ cardItems.value = [
+ { label: '鎬诲湪鍒舵暟閲�', value: stats.totalQuantity, unit: '浠�' },
+ { label: '骞冲潎鍛ㄨ浆澶╂暟', value: stats.avgTurnoverDays, unit: '澶�' },
+ { label: '鍛ㄨ浆鏁堢巼', value: stats.turnoverEfficiency, unit: '%' },
+ ]
+ if (res.data.processDetails && Array.isArray(res.data.processDetails)) {
+ workInProcessXAxis.value[0].data = res.data.processDetails
+ } else {
+ workInProcessXAxis.value[0].data = []
+ }
+ if (res.data.processQuantityDetails && Array.isArray(res.data.processQuantityDetails)) {
+ workInProcessBarSeries.value[0].data = res.data.processQuantityDetails
+ } else {
+ workInProcessBarSeries.value[0].data = []
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇鍦ㄥ埗鍝佸懆杞粺璁″け璐�:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ workInProcessTurnoverInfo()
+ })
+}
+
+onMounted(() => {
+ workInProcessTurnoverInfo()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ border-radius: 16px;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+ overflow: hidden;
+}
+
+.chart-wrapper {
+ height: 70%;
+ flex: 1;
+ min-height: 200px;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/left-top.vue b/src/views/reportAnalysis/productionAnalysis/components/left-top.vue
new file mode 100644
index 0000000..37c82f0
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -0,0 +1,236 @@
+<template>
+ <div>
+ <PanelHeader title="宸ュ簭浜у嚭鍒嗘瀽" />
+ <div class="main-panel panel-item-customers">
+ <div class="filters-row">
+ <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
+ </div>
+ <div class="pie-chart-wrapper" ref="pieWrapperRef">
+ <div class="pie-background" ref="pieBackgroundRef"></div>
+ <Echarts
+ ref="echartsRef"
+ :chartStyle="chartStyle"
+ :legend="pieLegend"
+ :series="pieSeries"
+ :tooltip="pieTooltip"
+ :color="pieColors"
+ :options="pieOptions"
+ style="height: 320px"
+ />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, computed, inject, watch } from 'vue'
+import { processOutputAnalysis } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import DateTypeSwitch from '@/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue'
+import { useChartBackground } from '@/hooks/useChartBackground.js'
+
+const pieWrapperRef = ref(null)
+const pieBackgroundRef = ref(null)
+
+const dateType = ref(1) // 1=鍛� 2=鏈� 3=瀛e害
+
+function array2obj(array, key) {
+ const resObj = {}
+ for (let i = 0; i < array.length; i++) {
+ resObj[array[i][key]] = array[i]
+ }
+ return resObj
+}
+
+const chartStyle = {
+ width: '100%',
+ height: '100%',
+}
+
+const echartsRef = ref(null)
+const pieDatas = ref([])
+const pieColors = ['#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF', '#43e8fc', '#27EBE7']
+
+const pieObjData = computed(() => array2obj(pieDatas.value, 'name'))
+
+const pieLegend = computed(() => {
+ const data = pieDatas.value.map((d, idx) => ({
+ name: d.name,
+ icon: 'circle',
+ textStyle: {
+ fontSize: 18,
+ color: pieColors[idx % pieColors.length],
+ },
+ }))
+
+ return {
+ orient: 'vertical',
+ top: 'center',
+ left: '52%',
+ itemGap: 30,
+ data: data,
+ formatter: function (name) {
+ const item = pieObjData.value[name]
+ if (!item) return name
+ return `{title|${name}}{value|${item.value}}{unit|浠秨{percent|${item.rate}}{unit|%}`
+ },
+ textStyle: {
+ rich: {
+ value: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 10],
+ },
+ unit: {
+ color: '#82baff',
+ fontSize: 12,
+ fontWeight: 600,
+ padding: [0, 10, 0, 0],
+ },
+ percent: {
+ color: '#43e8fc',
+ fontSize: 14,
+ fontWeight: 600,
+ padding: [0, 0, 0, 0],
+ },
+ title: {
+ fontSize: 12,
+ padding: [0, 0, 0, 0],
+ },
+ },
+ },
+ }
+})
+
+const pieTooltip = {
+ trigger: 'item',
+ formatter: '{a} <br/>{b} : {c}浠� ({d}%)',
+}
+
+const pieSeries = computed(() => [
+ {
+ name: '宸ュ簭浜у嚭鍒嗘瀽',
+ type: 'pie',
+ radius: '60%',
+ center: ['25%', '50%'],
+ itemStyle: {
+ borderColor: '#0a1c3a',
+ borderWidth: 2,
+ },
+ label: {
+ show: false
+ },
+ minAngle: 15,
+ data: pieDatas.value,
+ animationType: 'scale',
+ animationEasing: 'elasticOut',
+ animationDelay: function () {
+ return Math.random() * 200
+ },
+ },
+])
+
+const pieOptions = {
+ backgroundColor: 'transparent',
+ textStyle: { color: '#B8C8E0' },
+}
+
+// 浣跨敤灏佽鐨勮儗鏅綅缃皟鏁存柟娉�
+// 鍥捐〃涓績鏄� ['25%', '50%']锛岃儗鏅渶瑕佸榻愬埌杩欎釜浣嶇疆
+const { init: initBackground, cleanup: cleanupBackground } = useChartBackground({
+ wrapperRef: pieWrapperRef,
+ backgroundRef: pieBackgroundRef,
+ left: '25%', // 鍥捐〃涓績 X 鏄� 25%
+ top: '50%', // 鍥捐〃涓績 Y 鏄� 50%
+ offsetX: '-51.5%', // X 杞村亸绉�
+ offsetY: '-50%', // Y 杞村亸绉�
+ watchData: pieDatas // 鐩戝惉鏁版嵁鍙樺寲锛岃嚜鍔ㄨ皟鏁翠綅缃�
+})
+
+const fetchData = () => {
+ processOutputAnalysis({ dateType: dateType.value })
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const items = res.data
+ pieDatas.value = items.map((item) => ({
+ name: item.name,
+ value: parseFloat(item.value) || 0,
+ rate: item.rate,
+ }))
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇宸ュ簭浜у嚭鍒嗘瀽澶辫触:', err)
+ })
+}
+
+const handleDateTypeChange = () => {
+ fetchData()
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+ initBackground()
+})
+
+onBeforeUnmount(() => {
+ cleanupBackground()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ border-radius: 16px;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+ overflow: hidden;
+}
+
+.filters-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.pie-chart-wrapper {
+ position: relative;
+ width: 100%;
+ height: 320px;
+ background: transparent;
+}
+
+.pie-background {
+ position: absolute;
+ width: 310px;
+ height: 310px;
+ background-image: url('@/assets/BI/鐜懓鍥捐竟妗�.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ z-index: 1;
+ pointer-events: none;
+ /* 浣嶇疆鐢� JS 鍔ㄦ�佽缃紝榛樿灞呬腑 */
+ left: 25%;
+ top: 50%;
+ transform: translate(-51.5%, -50%);
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue b/src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
new file mode 100644
index 0000000..62356e7
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
@@ -0,0 +1,194 @@
+<template>
+ <div>
+ <PanelHeader title="鐢熶骇鏍哥畻鍒嗘瀽" />
+ <div class="main-panel panel-item-customers">
+ <div class="filters-row">
+ <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="chartSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import { productionAccountingAnalysis } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+import DateTypeSwitch from './DateTypeSwitch.vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+
+const dateType = ref(1)
+
+const chartStyle = {
+ width: '100%',
+ height: '140%',
+}
+
+const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
+
+const barLegend = {
+ show: true,
+ textStyle: { color: '#B8C8E0' },
+ data: ['瀹屾垚鏁伴噺', '宸ヨ祫閲戦', '鍚堟牸鐜�'],
+}
+
+const chartSeries = ref([
+ {
+ name: '瀹屾垚鏁伴噺',
+ type: 'bar',
+ barWidth: 20,
+ barGap: '40%',
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(0, 164, 237, 0)' },
+ { offset: 0, color: 'rgba(78, 228, 255, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+ {
+ name: '宸ヨ祫閲戦',
+ type: 'bar',
+ barGap: '40%',
+ barWidth: 20,
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
+ { offset: 0, color: 'rgba(144, 97, 248, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+ {
+ name: '鍚堟牸鐜�',
+ type: 'line',
+ yAxisIndex: 1,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
+ itemStyle: { color: 'rgba(90, 216, 166, 1)' },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+])
+
+const xAxis1 = ref([
+ { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
+])
+
+const yAxis1 = [
+ {
+ type: 'value',
+ name: '鏁伴噺/閲戦',
+ axisLabel: { color: '#B8C8E0' },
+ nameTextStyle: { color: '#B8C8E0' },
+ // splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
+ },
+ {
+ type: 'value',
+ name: '鍚堟牸鐜�(%)',
+ min: 0,
+ max: 100,
+ axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
+ nameTextStyle: { color: '#B8C8E0' },
+ splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
+ },
+]
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ formatter(params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ const unit = item.seriesName === '鍚堟牸鐜�' ? '%' : (item.seriesName === '宸ヨ祫閲戦' ? ' 鍏�' : ' 涓�')
+ result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
+ })
+ return result
+ },
+}
+
+const handleDateTypeChange = () => {
+ fetchData()
+}
+
+const fetchData = () => {
+ productionAccountingAnalysis({ type: dateType.value })
+ .then((res) => {
+ if (res.code === 200 && Array.isArray(res.data)) {
+ const items = res.data
+
+ xAxis1.value[0].data = items.map(item => item.dateStr)
+ chartSeries.value[0].data = items.map(item => Number(item.numberOfCompleted) || 0)
+ chartSeries.value[1].data = items.map(item => Number(item.amount) || 0)
+ chartSeries.value[2].data = items.map(item => Number(item.passRate) || 0)
+ }
+ })
+ .catch((err) => {
+ console.error('鏁版嵁鍔犺浇澶辫触', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.filters-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+ box-sizing: border-box;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/reportAnalysis/productionAnalysis/components/right-top.vue b/src/views/reportAnalysis/productionAnalysis/components/right-top.vue
new file mode 100644
index 0000000..5ec836f
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/components/right-top.vue
@@ -0,0 +1,188 @@
+<template>
+ <div>
+ <PanelHeader title="宸ュ崟鎵ц鏁堢巼鍒嗘瀽" />
+ <div class="main-panel panel-item-customers">
+ <div class="filters-row">
+ <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
+ </div>
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="chartSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, inject, watch } from 'vue'
+import { workOrderEfficiencyAnalysis } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import DateTypeSwitch from './DateTypeSwitch.vue'
+
+const dateType = ref(1) // 1=鍛� 2=鏈� 3=瀛e害
+
+const chartStyle = {
+ width: '100%',
+ height: '140%',
+}
+
+const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
+
+const barLegend = {
+ show: true,
+ textStyle: { color: '#B8C8E0' },
+ data: ['寮�宸�', '瀹屾垚', '鑹搧鐜�'],
+}
+
+const chartSeries = ref([
+ {
+ name: '寮�宸�',
+ type: 'bar',
+ barWidth: 20,
+ barGap: '40%',
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(0, 164, 237, 0)' },
+ { offset: 0, color: 'rgba(78, 228, 255, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+ {
+ name: '瀹屾垚',
+ type: 'bar',
+ barGap: '40%',
+ barWidth: 20,
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
+ { offset: 0, color: 'rgba(144, 97, 248, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+ {
+ name: '鑹搧鐜�',
+ type: 'line',
+ yAxisIndex: 1,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
+ itemStyle: { color: 'rgba(90, 216, 166, 1)' },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+])
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ formatter(params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ const unit = item.seriesName === '鑹搧鐜�' ? '%' : '浠�'
+ result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
+ })
+ return result
+ },
+}
+
+const xAxis1 = ref([
+ { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
+])
+
+const yAxis1 = [
+ { type: 'value', name: '浠�', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
+ {
+ type: 'value',
+ name: '鑹搧鐜�(%)',
+ min: 0,
+ max: 100,
+ axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
+ nameTextStyle: { color: '#B8C8E0' },
+ splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
+ },
+]
+
+const handleDateTypeChange = () => {
+ fetchData()
+}
+
+const fetchData = () => {
+ workOrderEfficiencyAnalysis({ dateType: dateType.value })
+ .then((res) => {
+ if (res.code !== 200 || !Array.isArray(res.data)) return
+ const items = res.data
+ xAxis1.value[0].data = items.map((d) => d.date)
+ // 寮�宸�
+ chartSeries.value[0].data = items.map((d) => Number(d.startQuantity) || 0)
+ // 瀹屾垚
+ chartSeries.value[1].data = items.map((d) => Number(d.finishQuantity) || 0)
+ // 鑹搧鐜�
+ chartSeries.value[2].data = items.map((d) => Math.min(100, parseFloat(d.yieldRate) || 0))
+ })
+ .catch((err) => {
+ console.error('鑾峰彇宸ュ崟鎵ц鏁堢巼鍒嗘瀽澶辫触:', err)
+ })
+}
+
+const dataDashboardRefreshTick = inject('dataDashboardRefreshTick', null)
+if (dataDashboardRefreshTick) {
+ watch(dataDashboardRefreshTick, () => {
+ fetchData()
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.filters-row {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+</style>
diff --git a/src/views/reportAnalysis/productionAnalysis/index.vue b/src/views/reportAnalysis/productionAnalysis/index.vue
new file mode 100644
index 0000000..7e03bbd
--- /dev/null
+++ b/src/views/reportAnalysis/productionAnalysis/index.vue
@@ -0,0 +1,306 @@
+<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 />
+ <CenterCenter/>
+ <CenterBottom />
+ </div>
+
+ <!-- 鍙充晶鍖哄煙 -->
+ <div class="right-panel">
+
+ <RightTop />
+ <RightBottom />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick, provide } from 'vue'
+import autofit from 'autofit.js'
+import LeftBottom from './components/left-bottom.vue'
+import CenterCenter from './components/center-center.vue'
+import RightTop from './components/right-top.vue'
+import RightBottom from './components/right-bottom.vue'
+import useUserStore from '@/store/modules/user'
+import LeftTop from './components/left-top.vue'
+import CenterTop from './components/center-top.vue'
+import CenterBottom from './components/center-bottom.vue'
+
+// 鍏ㄥ睆鐩稿叧鐘舵��
+const isFullscreen = ref(false);
+
+// 缂╂斁姣斾緥
+const scaleRatio = ref(1)
+// 璁捐灏哄锛堝熀鍑嗗昂瀵革級- 鏍规嵁瀹為檯璁捐绋胯皟鏁�
+const designWidth = 1920
+const designHeight = 1080
+
+// 鐢ㄦ埛store
+const userStore = useUserStore()
+
+/** 涓庡叾瀹冮┚椹惰埍鍏辩敤娉ㄥ叆鍚嶏紝瀛愮粍浠舵瘡鍒嗛挓鍒锋柊鎺ュ彛鏁版嵁 */
+const DASHBOARD_REFRESH_MS = 60 * 1000
+const dataDashboardRefreshTick = ref(0)
+provide('dataDashboardRefreshTick', dataDashboardRefreshTick)
+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 = () => {
+ // 寤惰繜鎵ц锛岀‘淇滵OM鏇存柊瀹屾垚
+ setTimeout(() => {
+ calculateScale()
+ }, 100)
+}
+
+// 鍏ㄥ睆鍔熻兘瀹炵幇 - 閽堝scale-container鍏冪礌
+const toggleFullscreen = () => {
+ const element = document.querySelector('.scale-container')
+
+ if (!element) return
+
+ if (!isFullscreen.value) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen()
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen()
+ }
+ }
+}
+
+// 鐩戝惉鍏ㄥ睆鍙樺寲浜嬩欢
+const handleFullscreenChange = () => {
+ const fullscreenElement = document.fullscreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement
+ isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
+
+ // 鍏ㄥ睆鐘舵�佸彉鍖栨椂锛屽欢杩熼噸鏂拌绠楃缉鏀炬瘮渚嬶紙纭繚DOM鏇存柊瀹屾垚锛�
+ setTimeout(() => {
+ calculateScale()
+ }, 200)
+}
+
+// 鐢熷懡鍛ㄦ湡閽╁瓙
+onMounted(() => {
+ // 浣跨敤nextTick纭繚DOM瀹屽叏娓叉煋鍚庡啀鍒濆鍖�
+ nextTick(() => {
+ // 璁$畻鍒濆缂╂斁姣斾緥
+ calculateScale()
+ })
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('fullscreenchange', handleFullscreenChange)
+ window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.addEventListener('MSFullscreenChange', handleFullscreenChange)
+
+ dashboardPollTimer = setInterval(() => {
+ dataDashboardRefreshTick.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)
+ // 绉婚櫎鎴戜滑娣诲姞鐨刟utofit鍔ㄦ�佽皟鏁寸洃鍚櫒
+ if (window._autofitUpdateHandler) {
+ window.removeEventListener('resize', window._autofitUpdateHandler)
+ delete window._autofitUpdateHandler
+ }
+ // 鍏抽棴autofit
+ autofit.off()
+})
+</script>
+
+<style scoped>
+/* 澶栭儴缂╂斁瀹瑰櫒 - 鍗犳嵁鏁翠釜瑙嗗彛 */
+.scale-container {
+position: relative;
+width: 100%;
+/* 椤甸潰鍦ㄥ父瑙勫竷灞�涓嬶紙鏈夐《鏍忥級榛樿鍑忓幓 84px锛岄伩鍏嶅唴瀹硅瑁佸垏 */
+height: calc(100vh - 84px);
+display: flex;
+align-items: center;
+justify-content: center;
+background-color: #000;
+overflow: hidden;
+}
+
+/* 鍐呴儴鍐呭鍖哄煙 - 鍥哄畾璁捐灏哄 */
+.data-dashboard {
+position: relative;
+width: 1920px;
+height: 1080px;
+background-image: url("@/assets/BI/backImage@2x.png");
+background-size: cover;
+background-position: center;
+background-repeat: no-repeat;
+transform-origin: center center;
+}
+
+/* 鍏ㄥ睆鐘舵�佺殑鏍峰紡 - 浣滅敤浜巗cale-container */
+.scale-container:fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* Webkit娴忚鍣ㄥ墠缂� */
+.scale-container:-webkit-full-screen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* MS娴忚鍣ㄥ墠缂� */
+.scale-container:-ms-fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+
+.dashboard-header {
+position: relative;
+z-index: 1;
+height: 86px;
+background-image: url("@/assets/BI/biaoti.png");
+background-size: cover;
+background-repeat: no-repeat;
+display: flex;
+align-items: center;
+justify-content: center;
+}
+
+.factory-name {
+font-weight: 600;
+font-size: 52px;
+color: #FFFFFF;
+top: 16px;
+position: absolute;
+}
+
+.fullscreen-btn {
+position: absolute;
+top: 10px;
+left: 20px;
+width: 40px;
+height: 40px;
+background: rgba(0, 20, 60, 0.8);
+border: 1px solid rgba(0, 212, 255, 0.3);
+border-radius: 6px;
+color: #00d4ff;
+cursor: pointer;
+display: flex;
+align-items: center;
+justify-content: center;
+transition: all 0.3s;
+z-index: 10000;
+}
+
+.fullscreen-btn:hover {
+background: rgba(0, 30, 90, 0.9);
+border-color: rgba(0, 212, 255, 0.5);
+}
+
+.dashboard-content {
+position: relative;
+z-index: 1;
+display: flex;
+gap: 30px;
+padding: 0 30px;
+height: calc(100% - 86px);
+overflow: hidden;
+}
+
+/* 纭繚鍚勯潰鏉胯兘澶熸纭樉绀� */
+.left-panel, .center-panel, .right-panel {
+overflow: hidden;
+}
+
+.left-panel,
+.right-panel {
+flex: 1;
+display: flex;
+flex-direction: column;
+gap: 24px;
+width: 520px;
+}
+
+.center-panel {
+flex: 1.5;
+display: flex;
+flex-direction: column;
+gap: 20px;
+}
+
+</style>
\ No newline at end of file
diff --git a/src/views/reportAnalysis/projectProfit/index.vue b/src/views/reportAnalysis/projectProfit/index.vue
new file mode 100644
index 0000000..f61cbe5
--- /dev/null
+++ b/src/views/reportAnalysis/projectProfit/index.vue
@@ -0,0 +1,126 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true" label-width="80px">
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <el-input v-model="filters.customerName" placeholder="璇疯緭鍏ュ鎴峰悕绉�" clearable style="width: 240px"/>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData"> 鎼滅储 </el-button>
+ <el-button @click="resetFilters"> 閲嶇疆 </el-button>
+ <el-button @click="handleOut"> 瀵煎嚭 </el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableLoading="loading"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total
+ }"
+ :isShowSummary="true"
+ :summaryMethod="summarizeMainTable"
+ @pagination="changePage"
+ ></PIMTable>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { usePaginationApi } from "@/hooks/usePaginationApi";
+import { getPurchaseList } from "@/api/procurementManagement/projectProfit";
+import { onMounted, getCurrentInstance } from "vue";
+import { ElMessageBox } from "element-plus";
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+ name: "椤圭洰鍒╂鼎",
+});
+
+const {
+ loading,
+ filters,
+ columns,
+ dataList,
+ pagination,
+ getTableData,
+ resetFilters,
+ onCurrentChange,
+} = usePaginationApi(
+ getPurchaseList,
+ {
+ customerName: undefined,
+ },
+ [
+ {
+ label: "閿�鍞悎鍚屽彿",
+ align: "center",
+ prop: "customerContractNo",
+ },
+ {
+ label: "瀹㈡埛鍚嶇О",
+ align: "center",
+ prop: "customerName",
+ },
+ {
+ label: "鍚堝悓閲戦",
+ align: "center",
+ prop: "contractAmount",
+ },
+ {
+ label: "閲囪喘閲戦",
+ align: "center",
+ prop: "purchaseAmount",
+ },
+ {
+ label: "鍒╂鼎",
+ align: "center",
+ prop: "balance",
+ },
+ {
+ label: "鍒╂鼎鐜�",
+ align: "center",
+ prop: "balanceRatio",
+ },
+ ]
+);
+
+const changePage = ({ page, limit }) => {
+ pagination.currentPage = page;
+ pagination.pageSize = limit;
+ onCurrentChange(page);
+};
+
+// 涓昏〃鍚堣鏂规硶
+const summarizeMainTable = (param) => {
+ return proxy.summarizeTable(param, ["contractAmount", "purchaseAmount", "balance"]);
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/purchase/report/export", {}, "椤圭洰鍒╂鼎.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+<style lang="scss" scoped>
+.table_list {
+ margin-top: unset;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue b/src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue
new file mode 100644
index 0000000..0498824
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/CarouselCards.vue
@@ -0,0 +1,306 @@
+<template>
+ <div class="carousel-cards">
+ <button
+ v-if="canScrollLeft"
+ class="nav-button nav-button-left"
+ @click="scrollLeftFn"
+ >
+ <img src="@/assets/BI/jiantou.png" alt="宸︾澶�" />
+ </button>
+ <div
+ class="cards-container"
+ :style="{ '--visible-count': visibleCount }"
+ ref="cardsContainerRef"
+ >
+ <div
+ v-for="(item, index) in items"
+ :key="index"
+ class="card-item"
+ >
+ <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div>
+ <div class="card-title">
+ <div class="card-label">{{ item.label }}</div>
+ <div class="card-value">
+ <span class="value-number">{{ item.value }}</span>
+ <span class="value-unit">{{ item.unit }}</span>
+ </div>
+ <div v-if="item.rate ?? item.ratio ?? item.percent" class="card-rate">
+ <span class="rate-value">{{ item.rate ?? item.ratio ?? item.percent }}%</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <button
+ v-if="canScrollRight"
+ class="nav-button nav-button-right"
+ @click="scrollRightFn"
+ >
+ <img src="@/assets/BI/jiantou.png" alt="鍙崇澶�" />
+ </button>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue'
+
+const props = defineProps({
+ items: {
+ type: Array,
+ default: () => [],
+ validator: (value) => {
+ return value.every(item =>
+ item && typeof item.label !== 'undefined' &&
+ typeof item.value !== 'undefined' &&
+ typeof item.unit !== 'undefined'
+ )
+ }
+ },
+ visibleCount: {
+ type: Number,
+ default: 3
+ }
+})
+
+const cardsContainerRef = ref(null)
+const currentScrollLeft = ref(0)
+const maxScrollLeft = ref(0)
+
+// 妫�鏌ユ槸鍚﹀彲浠ュ悜宸︽粴鍔�
+const canScrollLeft = computed(() => {
+ return currentScrollLeft.value > 0
+})
+
+// 妫�鏌ユ槸鍚﹀彲浠ュ悜鍙虫粴鍔�
+const canScrollRight = computed(() => {
+ return currentScrollLeft.value < maxScrollLeft.value
+})
+
+// 鏇存柊婊氬姩鐘舵��
+const updateScrollState = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ currentScrollLeft.value = container.scrollLeft
+ maxScrollLeft.value = container.scrollWidth - container.clientWidth
+}
+
+// 鍚戝乏婊氬姩
+const scrollLeftFn = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ const scrollItems = Array.from(container.querySelectorAll('.card-item'))
+ if (scrollItems.length === 0) return
+
+ const itemWidth = scrollItems[0]?.offsetWidth || 0
+ const gap = 12
+ const scrollDistance = itemWidth + gap
+
+ container.scrollBy({
+ left: -scrollDistance,
+ behavior: 'smooth'
+ })
+
+ // 寤惰繜鏇存柊鐘舵�侊紝绛夊緟婊氬姩鍔ㄧ敾瀹屾垚
+ setTimeout(() => {
+ updateScrollState()
+ }, 300)
+}
+
+// 鍚戝彸婊氬姩
+const scrollRightFn = () => {
+ const container = cardsContainerRef.value
+ if (!container) return
+
+ const scrollItems = Array.from(container.querySelectorAll('.card-item'))
+ if (scrollItems.length === 0) return
+
+ const itemWidth = scrollItems[0]?.offsetWidth || 0
+ const gap = 12
+ const scrollDistance = itemWidth + gap
+
+ container.scrollBy({
+ left: scrollDistance,
+ behavior: 'smooth'
+ })
+
+ // 寤惰繜鏇存柊鐘舵�侊紝绛夊緟婊氬姩鍔ㄧ敾瀹屾垚
+ setTimeout(() => {
+ updateScrollState()
+ }, 300)
+}
+
+// 鐩戝惉 items 鍙樺寲锛屾洿鏂版粴鍔ㄧ姸鎬�
+watch(() => props.items, () => {
+ nextTick(() => {
+ updateScrollState()
+ })
+}, { deep: true })
+
+onMounted(() => {
+ nextTick(() => {
+ updateScrollState()
+ // 鐩戝惉婊氬姩浜嬩欢
+ const container = cardsContainerRef.value
+ if (container) {
+ container.addEventListener('scroll', updateScrollState)
+ }
+ })
+})
+
+onBeforeUnmount(() => {
+ // 娓呯悊婊氬姩浜嬩欢鐩戝惉鍣�
+ const container = cardsContainerRef.value
+ if (container) {
+ container.removeEventListener('scroll', updateScrollState)
+ }
+})
+</script>
+
+<style scoped>
+.carousel-cards {
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.cards-container {
+ display: flex;
+ gap: 12px;
+ width: 100%;
+ overflow-x: auto;
+ overflow-y: hidden;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE and Edge */
+ padding-bottom: 4px;
+ scroll-behavior: smooth;
+}
+
+.cards-container::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+.nav-button {
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 32px;
+ height: 32px;
+ background: rgba(26, 88, 176, 0.6);
+ border: 1px solid rgba(26, 88, 176, 0.8);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ z-index: 10;
+ transition: all 0.3s ease;
+ padding: 0;
+}
+
+.nav-button:hover {
+ background: rgba(26, 88, 176, 0.8);
+ transform: translateY(-50%) scale(1.1);
+}
+
+.nav-button-left {
+ left: -16px;
+}
+
+.nav-button-left img {
+ width: 16px;
+ height: 16px;
+ transform: rotate(180deg);
+}
+
+.nav-button-right {
+ right: -16px;
+}
+
+.nav-button-right img {
+ width: 16px;
+ height: 16px;
+}
+
+.card-item {
+ flex: 0 0 calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
+ min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count));
+ display: flex;
+ align-items: center;
+ background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%);
+ border-radius: 8px 8px 8px 8px;
+ padding: 12px 16px;
+ transition: all 0.3s ease;
+}
+
+.card-item:hover {
+ transform: translateY(-2px);
+}
+
+.card-icon {
+ width: 80px;
+ height: 60px;
+ background-size: cover;
+ background-position: center;
+ background-repeat: no-repeat;
+ flex-shrink: 0;
+ margin-right: 12px;
+}
+
+.card-title {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ flex: 1;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 14px;
+ color: #FFFFFF;
+ margin-bottom: 4px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: 100%;
+}
+
+.card-value {
+ display: flex;
+ align-items: baseline;
+ gap: 4px;
+}
+
+.card-rate {
+ margin-top: 4px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-weight: 400;
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.85);
+}
+
+.rate-label {
+ opacity: 0.85;
+}
+
+.rate-value {
+ font-weight: 500;
+}
+
+.value-number {
+ font-weight: 400;
+ font-size: 14px;
+ color: #FFFFFF;
+ line-height: 1;
+}
+
+.value-unit {
+ font-size: 14px;
+ color: #FFFFFF;
+ font-weight: 400;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue b/src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue
new file mode 100644
index 0000000..0c57b25
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/DateTypeSwitch.vue
@@ -0,0 +1,94 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="date-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍛�</el-radio-button>
+ <el-radio-button :label="2">鏈�</el-radio-button>
+ <el-radio-button :label="3">瀛e害</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍛�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+// 鐩戝惉澶栭儴鍊煎彉鍖�
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+// 澶勭悊鍊煎彉鍖�
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.date-type-switch {
+ display: inline-flex;
+}
+
+/* 鏈�変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+/* 绗竴涓寜閽乏渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+/* 鏈�鍚庝竴涓寜閽彸渚у渾瑙� */
+.date-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+/* 鎸夐挳涔嬮棿鐨勫垎闅旂嚎 */
+.date-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+/* 閫変腑鐘舵�佺殑鏍峰紡 */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+/* 鎮仠鏁堟灉 */
+.date-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+/* 閫変腑鐘舵�佹偓鍋� */
+.date-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue b/src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue
new file mode 100644
index 0000000..313f1df
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/PanelHeader.vue
@@ -0,0 +1,33 @@
+<template>
+ <div class="panel-header">
+ <span class="panel-title">{{ title }}</span>
+ </div>
+</template>
+
+<script setup>
+defineProps({
+ title: {
+ type: String,
+ required: true,
+ default: ''
+ }
+})
+</script>
+
+<style scoped>
+.panel-header {
+ background-image: url("@/assets/BI/kehuhetongback@2x.png");
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+.panel-title {
+ width: 100%;
+ font-weight: 500;
+ font-size: 16px;
+ color: #D9ECFF;
+ padding-left: 46px;
+ line-height: 36px;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue b/src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue
new file mode 100644
index 0000000..87cde44
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/ProductTypeSwitch.vue
@@ -0,0 +1,85 @@
+<template>
+ <el-radio-group
+ v-model="currentValue"
+ class="product-type-switch"
+ @change="handleChange"
+ >
+ <el-radio-button :label="1">鍘熸潗鏂�</el-radio-button>
+ <el-radio-button :label="3">鍗婃垚鍝�</el-radio-button>
+ <el-radio-button :label="2">鎴愬搧</el-radio-button>
+ </el-radio-group>
+</template>
+
+<script setup>
+import { ref, watch } from 'vue'
+
+const props = defineProps({
+ modelValue: {
+ type: Number,
+ default: 1, // 榛樿閫変腑"鍘熸潗鏂�"
+ },
+})
+
+const emit = defineEmits(['update:modelValue', 'change'])
+
+const currentValue = ref(props.modelValue)
+
+watch(
+ () => props.modelValue,
+ (newVal) => {
+ currentValue.value = newVal
+ }
+)
+
+const handleChange = (value) => {
+ emit('update:modelValue', value)
+ emit('change', value)
+}
+</script>
+
+<style scoped>
+.product-type-switch {
+ display: inline-flex;
+}
+
+.product-type-switch :deep(.el-radio-button__inner) {
+ background-color: rgba(26, 88, 176, 0.3);
+ color: rgba(184, 200, 224, 0.8);
+ border-color: rgba(255, 255, 255, 0.2);
+ border-radius: 0;
+ padding: 6px 20px;
+ font-size: 14px;
+ transition: all 0.3s;
+}
+
+.product-type-switch :deep(.el-radio-button:first-child .el-radio-button__inner) {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:last-child .el-radio-button__inner) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.product-type-switch :deep(.el-radio-button:not(:last-child) .el-radio-button__inner) {
+ border-right: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner) {
+ background: linear-gradient(180deg, #3378ff 0%, #00a4ed 100%);
+ color: #ffffff;
+ border-color: rgba(51, 120, 255, 0.8);
+ box-shadow: none;
+}
+
+.product-type-switch :deep(.el-radio-button__inner:hover) {
+ color: rgba(184, 200, 224, 1);
+ border-color: rgba(255, 255, 255, 0.3);
+}
+
+.product-type-switch :deep(.el-radio-button__original-radio:checked + .el-radio-button__inner:hover) {
+ background: linear-gradient(180deg, #4e8aff 0%, #4ee4ff 100%);
+ color: #ffffff;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue b/src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue
new file mode 100644
index 0000000..fe875d0
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue
@@ -0,0 +1,211 @@
+<template>
+ <div>
+ <div class="chart-header">
+ <div class="chart-header-title">
+ <PanelHeader title="瀹屾垚妫�楠屾暟" />
+ </div>
+ <div class="warn-range" @click="handleRangeClick">杩�7澶�</div>
+ </div>
+ <div class="main-panel panel-item-customers">
+ <Echarts
+ ref="chart"
+ :chartStyle="chartStyle"
+ :grid="grid"
+ :legend="barLegend"
+ :series="chartSeries"
+ :tooltip="tooltip"
+ :xAxis="xAxis1"
+ :yAxis="yAxis1"
+ :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
+ style="height: 260px"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { completedInspectionCount } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+
+const chartStyle = {
+ width: '100%',
+ height: '140%',
+}
+
+const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
+
+const barLegend = {
+ show: true,
+ textStyle: { color: '#B8C8E0' },
+ data: ['鍚堟牸', '涓嶅悎鏍�', '鍚堟牸鐜�'],
+}
+
+const chartSeries = ref([
+ {
+ name: '鍚堟牸',
+ type: 'bar',
+ barWidth: 20,
+ barGap: '40%',
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(0, 164, 237, 0)' },
+ { offset: 0, color: 'rgba(78, 228, 255, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+ {
+ name: '涓嶅悎鏍�',
+ type: 'bar',
+ barGap: '40%',
+ barWidth: 20,
+ emphasis: { focus: 'series' },
+ itemStyle: {
+ color: {
+ type: 'linear',
+ x: 0,
+ y: 0,
+ x2: 0,
+ y2: 1,
+ colorStops: [
+ { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
+ { offset: 0, color: 'rgba(144, 97, 248, 1)' },
+ ],
+ },
+ },
+ data: [],
+ },
+ {
+ name: '鍚堟牸鐜�',
+ type: 'line',
+ yAxisIndex: 1,
+ showSymbol: true,
+ symbol: 'circle',
+ symbolSize: 8,
+ lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
+ itemStyle: { color: 'rgba(90, 216, 166, 1)' },
+ data: [],
+ emphasis: { focus: 'series' },
+ },
+])
+
+const tooltip = {
+ trigger: 'axis',
+ axisPointer: { type: 'cross' },
+ formatter(params) {
+ let result = params[0].axisValueLabel + '<br/>'
+ params.forEach((item) => {
+ const unit = item.seriesName === '鍚堟牸鐜�' ? '%' : '浠�'
+ result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
+ })
+ return result
+ },
+}
+
+const xAxis1 = ref([
+ { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
+])
+
+const yAxis1 = [
+ { type: 'value', name: '浠�', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
+ {
+ type: 'value',
+ name: '鍚堟牸鐜�(%)',
+ min: 0,
+ max: 100,
+ axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
+ nameTextStyle: { color: '#B8C8E0' },
+ splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
+ },
+]
+
+const fetchData = () => {
+ completedInspectionCount()
+ .then((res) => {
+ if (res?.code === 200 && Array.isArray(res?.data)) {
+ const items = res.data
+ // 鏇存柊X杞存棩鏈熸暟鎹�
+ xAxis1.value[0].data = items.map((d) => d.dateStr || '')
+ // 鏇存柊鍚堟牸鏁帮紙榛勮壊鏌辩姸鍥撅級
+ chartSeries.value[0].data = items.map((d) => Number(d.qualifiedCount) || 0)
+ // 鏇存柊涓嶅悎鏍兼暟锛堢传鑹叉煴鐘跺浘锛�
+ chartSeries.value[1].data = items.map((d) => Number(d.unqualifiedCount) || 0)
+ // 鏇存柊鍚堟牸鐜囷紙钃濊壊鎶樼嚎鍥撅級
+ chartSeries.value[2].data = items.map((d) => Number(d.passRate) || 0)
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇瀹屾垚妫�楠屾暟鏁版嵁澶辫触:', err)
+ })
+}
+
+const handleRangeClick = () => {
+ // 鍏堟寜鎴浘鍋氶潤鎬�"杩�7澶�"锛屽悗缁湁鐪熷疄绛涢�夐渶姹傚啀鎺ュ叆
+ fetchData()
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.chart-header {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+}
+
+.chart-header-title {
+ flex: 1;
+ min-width: 0;
+ width: 100%;
+}
+
+.warn-range {
+ position: absolute;
+ right: 0;
+ top: 0;
+ height: 32px;
+ padding: 0 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ color: #ffffff;
+ font-weight: 600;
+ font-size: 14px;
+ background: linear-gradient(180deg, rgba(51, 120, 255, 1) 0%, rgba(0, 164, 237, 1) 100%);
+ border: 1px solid rgba(78, 228, 255, 0.25);
+ cursor: pointer;
+ z-index: 10;
+}
+
+.warn-range:hover {
+ background: linear-gradient(180deg, rgba(51, 140, 255, 1) 0%, rgba(0, 184, 237, 1) 100%);
+}
+
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 436px;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/center-center.vue b/src/views/reportAnalysis/qualityAnalysis/components/center-center.vue
new file mode 100644
index 0000000..8d28f7a
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/center-center.vue
@@ -0,0 +1,355 @@
+<template>
+ <div>
+ <div class="warn-panel">
+ <div class="warn-header">
+ <div class="warn-header-left">
+ <div class="warn-badge"></div>
+ <span class="warn-title">涓嶅悎鏍奸璀�</span>
+ </div>
+ <div class="warn-range" @click="handleRangeClick">杩�7澶�</div>
+ </div>
+
+ <div class="warn-body">
+ <div class="warn-list" role="list">
+ <div v-for="item in warnings" :key="item.id" class="warn-item" role="listitem" @click="openWarning(item)">
+ <div class="warn-tag" :class="tagClass(item.type)">{{ item.parentProductTitle }}-{{ item.productTitle }}
+ </div>
+ <div class="warn-text" :title="item.title">{{ item.title }}</div>
+ <div class="warn-action" @click.stop="openWarning(item)">鏌ョ湅</div>
+ <div class="warn-date">{{ item.date }}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { computed, getCurrentInstance, ref, onMounted } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import { nonComplianceWarning } from '@/api/viewIndex.js'
+
+const { proxy } = getCurrentInstance() || {}
+
+const warnings = ref([])
+
+// 鍗犳瘮鏁版嵁
+const ratios = ref({
+ rawMaterialRatio: 0,
+ semiFinishedProductRatio: 0,
+ finishedProductRatio: 0,
+})
+
+const TAG_COLORS = {
+ raw: '#7C4DFF',
+ final: '#F5A000',
+ semi: '#FF66CC',
+}
+
+const tagClass = (type) => {
+ if (type === 'raw') return 'tag-raw'
+ if (type === 'final') return 'tag-final'
+ return 'tag-semi'
+}
+
+// 鏍规嵁productTitle鏄犲皠绫诲瀷
+const mapProductTitleToType = (productTitle) => {
+ if (productTitle === '鍘熸潗鏂�') return 'raw'
+ if (productTitle === '鍗婃垚鍝�') return 'semi'
+ if (productTitle === '鎴愬搧') return 'final'
+ return 'raw' // 榛樿鍊�
+}
+
+const pieChartStyle = { width: '100%', height: '100%' }
+
+const pieOptions = {
+ backgroundColor: 'transparent',
+ textStyle: { color: '#B8C8E0' },
+}
+
+const pieTooltip = {
+ trigger: 'item',
+ formatter: (p) => `${p.name}锛�${p.value}%`,
+}
+
+const pieData = computed(() => {
+ return [
+ { name: '鍘熸潗鏂�', value: ratios.value.rawMaterialRatio, itemStyle: { color: TAG_COLORS.raw } },
+ { name: '鍗婃垚鍝�', value: ratios.value.semiFinishedProductRatio, itemStyle: { color: TAG_COLORS.semi } },
+ { name: '鎴愬搧', value: ratios.value.finishedProductRatio, itemStyle: { color: TAG_COLORS.final } },
+ ]
+})
+
+const pieSeries = computed(() => {
+ return [
+ {
+ type: 'pie',
+ radius: ['0%', '68%'],
+ center: ['50%', '50%'],
+ startAngle: 90,
+ clockwise: true,
+ avoidLabelOverlap: true,
+ label: { show: false },
+ labelLine: { show: false },
+ itemStyle: {
+ borderColor: '#071a3a',
+ borderWidth: 4,
+ shadowBlur: 14,
+ shadowColor: 'rgba(0, 0, 0, 0.35)',
+ },
+ data: pieData.value,
+ },
+ {
+ // 鍐呭湀鏆楃幆锛屽寮哄眰娆�
+ type: 'pie',
+ radius: ['70%', '74%'],
+ center: ['50%', '50%'],
+ silent: true,
+ label: { show: false },
+ labelLine: { show: false },
+ itemStyle: { color: 'rgba(78, 228, 255, 0.12)' },
+ data: [1],
+ },
+ ]
+})
+
+const fetchWarnings = async () => {
+ try {
+ const res = await nonComplianceWarning()
+ if (res?.code === 200 && res?.data) {
+ const data = res.data
+
+ // 鏇存柊鍗犳瘮鏁版嵁
+ ratios.value = {
+ rawMaterialRatio: data.rawMaterialRatio ?? 0,
+ semiFinishedProductRatio: data.semiFinishedProductRatio ?? 0,
+ finishedProductRatio: data.finishedProductRatio ?? 0,
+ }
+
+ // 鏇存柊璀﹀憡鍒楄〃
+ const children = data.children || []
+ warnings.value = children.map((item, idx) => {
+ const type = mapProductTitleToType(item.parentProductTitle)
+ const date = item.date ? item.date.replace(/-/g, '.') : ''
+ return {
+ id: item.id ?? `warning-${idx}`,
+ type,
+ parentProductTitle: item.parentProductTitle || '鍘熸潗鏂�',
+ productTitle: item.productTitle || '鍘熸潗鏂�',
+ title: item.description || '涓嶅悎鏍奸璀�',
+ date,
+ }
+ })
+ }
+ } catch (e) {
+ // 鎺ュ彛澶辫触鍒欎繚鎸佺┖鏁版嵁
+ console.error('鑾峰彇涓嶅悎鏍奸璀﹀け璐�:', e)
+ }
+}
+
+const openWarning = (item) => {
+ const title = `銆�${item.parentProductTitle}-${item.productTitle}銆�${item.title}`
+ if (proxy?.$modal?.alert) {
+ proxy.$modal.alert(title)
+ return
+ }
+ // 鍏滃簳锛氭病鏈夊叏灞� modal 鏃剁敤 console
+ console.log('warning:', { ...item })
+}
+
+const handleRangeClick = () => {
+ // 鍏堟寜鎴浘鍋氶潤鎬佲�滆繎7澶┾�濓紝鍚庣画鏈夌湡瀹炵瓫閫夐渶姹傚啀鎺ュ叆
+}
+
+onMounted(() => {
+ fetchWarnings()
+})
+</script>
+
+<style scoped>
+.warn-panel {
+ border: 1px solid #1a58b0;
+ padding: 0 18px 18px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.warn-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ border-bottom: 1px solid;
+ border-image: linear-gradient(270deg,
+ rgba(0, 126, 255, 0) 0%,
+ rgba(0, 126, 255, 0.4549) 35%,
+ #007eff 78%,
+ #007eff 100%) 1;
+ padding: 10px 0 6px;
+}
+
+.warn-header-left {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+}
+
+.warn-badge {
+ width: 18px;
+ height: 18px;
+ background: linear-gradient(180deg, #2aa8ff 0%, #4ee4ff 100%);
+ clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
+ box-shadow: 0 0 12px rgba(78, 228, 255, 0.25);
+}
+
+.warn-title {
+ font-weight: 600;
+ font-size: 18px;
+ background: linear-gradient(360deg, #056dff 0%, #43e8fc 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ line-height: 24px;
+}
+
+.warn-range {
+ height: 32px;
+ padding: 0 14px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 4px;
+ color: #ffffff;
+ font-weight: 600;
+ background: linear-gradient(180deg, rgba(51, 120, 255, 1) 0%, rgba(0, 164, 237, 1) 100%);
+ border: 1px solid rgba(78, 228, 255, 0.25);
+ cursor: pointer;
+}
+
+.warn-body {
+ display: grid;
+ gap: 18px;
+ align-items: stretch;
+ min-height: 260px;
+}
+
+.warn-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding-top: 6px;
+}
+
+.warn-item {
+ display: grid;
+ grid-template-columns: 130px 1fr auto 110px;
+ align-items: center;
+ gap: 12px;
+ color: #b8c8e0;
+ font-size: 14px;
+ line-height: 1.2;
+ padding: 8px 0;
+ border-radius: 4px;
+ transition: background-color 0.2s, color 0.2s;
+}
+
+.warn-item:hover {
+ color: #ff4d4f;
+ background-color: rgba(255, 77, 79, 0.06);
+}
+
+.warn-item:hover .warn-text {
+ color: #ff4d4f;
+}
+
+.warn-tag {
+ height: 28px;
+ padding: 0 10px;
+ border-radius: 4px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ font-weight: 700;
+ color: #ffffff;
+}
+
+.tag-raw {
+ background: #7c4dff;
+}
+
+.tag-final {
+ background: #f5a000;
+}
+
+.tag-semi {
+ background: #ff66cc;
+}
+
+.warn-text {
+ color: #e8f1ff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.warn-action {
+ color: #ff4d4f;
+ font-weight: 700;
+ white-space: nowrap;
+ cursor: pointer;
+}
+
+.warn-date {
+ color: rgba(184, 200, 224, 0.75);
+ white-space: nowrap;
+}
+
+.warn-chart {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.chart-frame {
+ width: 100%;
+ height: 260px;
+ border: 2px dashed rgba(184, 200, 224, 0.35);
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: radial-gradient(circle at 50% 50%, rgba(78, 228, 255, 0.08) 0%, rgba(0, 0, 0, 0) 65%);
+}
+
+/* 澶栧湀鍒诲害鐜� */
+.chart-frame::before {
+ content: '';
+ position: absolute;
+ width: 220px;
+ height: 220px;
+ border-radius: 50%;
+ background: repeating-conic-gradient(from 0deg, rgba(78, 228, 255, 0.75) 0 1deg, rgba(78, 228, 255, 0) 1deg 9deg);
+ -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%);
+ mask: radial-gradient(circle, transparent 62%, #000 63%);
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+/* 鍗佸瓧杈呭姪绾� */
+.chart-frame::after {
+ content: '';
+ position: absolute;
+ width: 240px;
+ height: 240px;
+ background:
+ linear-gradient(to right, rgba(78, 228, 255, 0) 0%, rgba(78, 228, 255, 0.55) 50%, rgba(78, 228, 255, 0) 100%),
+ linear-gradient(to bottom, rgba(78, 228, 255, 0) 0%, rgba(78, 228, 255, 0.55) 50%, rgba(78, 228, 255, 0) 100%);
+ background-size: 100% 1px, 1px 100%;
+ background-position: center, center;
+ background-repeat: no-repeat;
+ opacity: 0.35;
+ pointer-events: none;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/center-top.vue b/src/views/reportAnalysis/qualityAnalysis/components/center-top.vue
new file mode 100644
index 0000000..8e46770
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/center-top.vue
@@ -0,0 +1,149 @@
+<template>
+ <div>
+ <!-- 椤堕儴缁熻鍗$墖 -->
+ <div class="stats-cards">
+ <div v-for="item in statItems" :key="item.name" class="stat-card">
+ <img src="@/assets/BI/icon@2x.png" alt="鍥炬爣" class="card-icon" />
+ <div class="card-content">
+ <span class="card-label">{{ item.name }}</span>
+ <span class="card-value">{{ item.value }}</span>
+ <div class="card-compare" :class="compareClass(Number(item.rate))">
+ <span>鍚屾瘮</span>
+ <span class="compare-value">{{ formatPercent(item.rate) }}</span>
+ <span class="compare-icon">{{ Number(item.rate) >= 0 ? '鈫�' : '鈫�' }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { qualityInspectionCount } from '@/api/viewIndex.js'
+
+const statItems = ref([])
+
+const formatPercent = (val) => {
+ const num = Number(val) || 0
+ return `${num.toFixed(2)}%`
+}
+
+const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
+
+const fetchData = () => {
+ qualityInspectionCount()
+ .then((res) => {
+ if (res.code === 200 && res.data) {
+ const data = res.data
+
+ statItems.value = [
+ {
+ name: '鎬绘楠屾暟',
+ value: data.totalCount ?? 0,
+ rate: data.totalCountGrowthRate ?? 0,
+ },
+ {
+ name: '浠婃棩寰呭畬鎴愭暟',
+ value: data.todayPendingCount ?? 0,
+ rate: data.todayPendingCountGrowthRate ?? 0,
+ },
+ {
+ name: '浠婃棩宸插畬鎴愭暟',
+ value: data.todayCompletedCount ?? 0,
+ rate: data.todayCompletedCountGrowthRate ?? 0,
+ },
+ ]
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇璐ㄩ噺妫�楠岀粺璁″け璐�:', err)
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.stats-cards {
+ display: flex;
+ gap: 30px;
+}
+
+.stat-card {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ background-image: url('@/assets/BI/border@2x.png');
+ background-size: 100% 100%;
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 142px;
+}
+
+.card-icon {
+ width: 100px;
+ height: 100px;
+ margin: 20px 20px 0 10px;
+}
+
+.card-content {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.card-value {
+ font-weight: 500;
+ font-size: 40px;
+ background: linear-gradient(360deg, #008bfd 0%, #ffffff 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.card-label {
+ font-weight: 400;
+ font-size: 16px;
+ color: rgba(208, 231, 255, 0.7);
+}
+
+.card-compare {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 15px;
+ color: #d0e7ff;
+ white-space: nowrap;
+ flex-wrap: nowrap;
+}
+
+.card-compare>span:first-child {
+ font-size: 13px;
+ opacity: 0.8;
+}
+
+.compare-value {
+ font-weight: 600;
+}
+
+.compare-icon {
+ font-size: 14px;
+ position: relative;
+ top: -1px;
+ /* 杞诲井涓婄Щ锛岃绠ご涓庢枃瀛楀瀭鐩村眳涓榻� */
+}
+
+.compare-up .compare-value,
+.compare-up .compare-icon {
+ color: #00c853;
+}
+
+.compare-down .compare-value,
+.compare-down .compare-icon {
+ color: #ff5252;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/left-top.vue b/src/views/reportAnalysis/qualityAnalysis/components/left-top.vue
new file mode 100644
index 0000000..cce4894
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/left-top.vue
@@ -0,0 +1,448 @@
+<template>
+ <div>
+ <PanelHeader title="璐ㄩ噺鎸囨爣鍚堟牸鍒嗘瀽" />
+ <div class="main-panel panel-item-customers">
+ <div v-for="section in sections" :key="section.key" class="inspect-block">
+ <div class="filters-row">
+ <div class="filters-row-left">
+ <span></span>
+ <p>{{ section.title }}</p>
+ </div>
+ <DateTypeSwitch v-model="section.dateType" @change="(v) => handleDateTypeChange(section.key, v)" />
+ </div>
+
+ <div class="inspect-body">
+ <div class="ring">
+ <Echarts :chartStyle="ringChartStyle" :series="buildRingSeries(section)" :tooltip="ringTooltip"
+ :legend="{ show: false }" :options="ringOptions" />
+ </div>
+
+ <div class="stats">
+ <div class="stat-row">
+ <div class="stat-left">
+ <span class="dot dot-qualified"></span>
+ <span class="stat-label">鍚堟牸鏁�</span>
+ </div>
+ <div class="stat-right">
+ <span class="stat-value">{{ section.qualifiedCount }}</span>
+ <span class="stat-percent">{{ formatPercent(section.qualifiedRate) }}</span>
+ </div>
+ </div>
+ <div class="stat-row">
+ <div class="stat-left">
+ <span class="dot dot-unqualified"></span>
+ <span class="stat-label">涓嶅悎鏍兼暟</span>
+ </div>
+ <div class="stat-right">
+ <span class="stat-value">{{ section.unqualifiedCount }}</span>
+ <span class="stat-percent">{{ formatPercent(section.unqualifiedRate) }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { reactive, onMounted } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from './PanelHeader.vue'
+import DateTypeSwitch from './DateTypeSwitch.vue'
+import { rawMaterialDetection, processDetection, factoryDetection } from '@/api/viewIndex.js'
+
+const QUALIFIED_COLOR = '#4EE4FF'
+const UNQUALIFIED_COLOR = '#3378FF'
+const TRACK_COLOR = 'rgba(78, 228, 255, 0.12)'
+
+const apiMap = {
+ raw: rawMaterialDetection,
+ process: processDetection,
+ final: factoryDetection,
+}
+
+
+const fetchSectionData = async (section) => {
+ const api = apiMap[section.key]
+ if (!api) return
+
+ try {
+ const res = await api({
+ type: section.dateType,
+ })
+
+ if (res?.code === 200 && res?.data) {
+ const data = res.data
+ section.qualifiedCount = Number(data.qualifiedCount || 0)
+ section.unqualifiedCount = Number(data.unqualifiedCount || 0)
+ section.qualifiedRate = Number(data.qualifiedRate || 0)
+ section.unqualifiedRate = Number(data.unqualifiedRate || 0)
+ }
+ } catch (err) {
+ console.error(`${section.key} 鎺ュ彛璇锋眰澶辫触`, err)
+ }
+}
+
+
+const sections = reactive([
+ {
+ key: 'raw',
+ title: '鍘熸潗鏂欐娴�',
+ dateType: 1,
+ qualifiedCount: 0,
+ unqualifiedCount: 0,
+ qualifiedRate: 0,
+ unqualifiedRate: 0,
+ },
+ {
+ key: 'process',
+ title: '杩囩▼妫�娴�',
+ dateType: 1,
+ qualifiedCount: 0,
+ unqualifiedCount: 0,
+ qualifiedRate: 0,
+ unqualifiedRate: 0,
+ },
+ {
+ key: 'final',
+ title: '鎴愬搧鍑哄巶妫�娴�',
+ dateType: 1,
+ qualifiedCount: 0,
+ unqualifiedCount: 0,
+ qualifiedRate: 0,
+ unqualifiedRate: 0,
+ },
+])
+
+const ringChartStyle = {
+ width: '110px',
+ height: '110px',
+}
+
+const ringOptions = {
+ backgroundColor: 'transparent',
+ textStyle: { color: '#B8C8E0' },
+}
+
+const ringTooltip = {
+ show: false,
+}
+
+const calcRates = (qualifiedCount, unqualifiedCount) => {
+ const total = Number(qualifiedCount || 0) + Number(unqualifiedCount || 0)
+ if (total <= 0) return { qualifiedRate: 0, unqualifiedRate: 0 }
+ const qualifiedRate = Math.round((Number(qualifiedCount || 0) / total) * 100)
+ const unqualifiedRate = Math.max(0, 100 - qualifiedRate)
+ return { qualifiedRate, unqualifiedRate }
+}
+
+const formatPercent = (v) => `${Number(v || 0)}%`
+
+const buildRingSeries = (section) => {
+ const qualified = Number(section.qualifiedCount || 0)
+ const unqualified = Number(section.unqualifiedCount || 0)
+ const total = qualified + unqualified
+
+ return [
+ {
+ type: 'pie',
+ radius: ['68%', '82%'],
+ center: ['50%', '50%'],
+ silent: true,
+ label: { show: false },
+ labelLine: { show: false },
+ itemStyle: { color: TRACK_COLOR },
+ data: [1],
+ },
+ {
+ name: section.title,
+ type: 'pie',
+ radius: ['68%', '82%'],
+ center: ['50%', '50%'],
+ silent: true,
+ label: { show: false },
+ labelLine: { show: false },
+ startAngle: 90,
+ clockwise: true,
+ minAngle: total > 0 ? 8 : 0,
+ itemStyle: {
+ borderColor: 'rgba(10, 28, 58, 0.95)',
+ borderWidth: 2,
+ },
+ data: [
+ {
+ value: qualified,
+ name: '鍚堟牸鏁�',
+ itemStyle: {
+ color: QUALIFIED_COLOR,
+ shadowBlur: 16,
+ shadowColor: 'rgba(78, 228, 255, 0.45)',
+ },
+ },
+ {
+ value: unqualified,
+ name: '涓嶅悎鏍兼暟',
+ itemStyle: {
+ color: UNQUALIFIED_COLOR,
+ shadowBlur: 10,
+ shadowColor: 'rgba(51, 120, 255, 0.35)',
+ },
+ },
+ ],
+ },
+ {
+ type: 'pie',
+ radius: ['52%', '56%'],
+ center: ['50%', '50%'],
+ silent: true,
+ label: { show: false },
+ labelLine: { show: false },
+ itemStyle: { color: 'rgba(0, 127, 255, 0.22)' },
+ data: [1],
+ },
+ ]
+}
+
+const handleDateTypeChange = (key, dateType) => {
+ const section = sections.find((s) => s.key === key)
+ if (!section) return
+ section.dateType = dateType
+ // 鍒囨崲鏃ユ湡绫诲瀷鏃堕噸鏂拌幏鍙栨暟鎹�
+ fetchSectionData(section)
+}
+
+// 缁勪欢鎸傝浇鏃惰幏鍙栨墍鏈塻ection鐨勬暟鎹�
+onMounted(() => {
+ sections.forEach((section) => {
+ fetchSectionData(section)
+ })
+})
+</script>
+
+<style scoped lang="scss">
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ gap: 0;
+}
+
+.filters-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 10px;
+
+ .filters-row-left {
+ width: 50%;
+ color: white;
+ /* 鐢╢lex鏇夸唬float锛岃瀛愬厓绱犲榻愭洿绋冲畾 */
+ display: flex;
+ align-items: center;
+
+ span {
+ /* 鏍稿績锛氱埗绾х浉瀵瑰畾浣嶏紝浣滀负浼厓绱犲熀鍑� */
+ position: relative;
+ display: inline-block;
+ /* 缁欎吉鍏冪礌鍜屾枃瀛楃暀绌洪棿 */
+ padding-left: 22px;
+ /* 鏂囧瓧鍨傜洿灞呬腑 */
+ line-height: 23px;
+ margin-right: 8px;
+
+ &::after {
+ content: '';
+ display: inline-block;
+ width: 16px;
+ height: 16px;
+ clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
+ background: #217AFF;
+ position: absolute;
+ top: 50%;
+ left: 0;
+ transform: translateY(-50%);
+ /* 纭繚鑿卞舰鍦ㄦ笎鍙樺潡涓婃柟 */
+ z-index: 1;
+ }
+
+ &::before {
+ content: '';
+ display: inline-block;
+ width: 18px;
+ height: 7px;
+ border-radius: 8px;
+ background: linear-gradient(360deg, rgba(33, 133, 255, 0.4) 0%, rgba(33, 221, 255, 0) 100%);
+ position: absolute;
+ top: 50%;
+ left: -1px;
+ /* 绮惧噯璐村湪鑿卞舰姝d笅鏂� */
+ transform: translateY(calc(0% + 8px));
+ z-index: 0;
+ }
+ }
+
+ p {
+ width: 100px;
+ height: 23px;
+ /* 娓愬彉璧峰鑹插拰鑿卞舰缁熶竴锛屾洿鍗忚皟 */
+ background: linear-gradient(90deg, #217AFF 0%, rgba(33, 221, 255, 0) 100%);
+ /* 绮惧噯鍨傜洿灞呬腑 */
+ line-height: 26px;
+ text-align: center;
+ color: white;
+ /* 鐢ㄩ珮搴︾殑涓�鍗婂仛鍦嗚锛岀‘淇濆乏杈规槸瀹岀編鍗婂渾 */
+ border-radius: 12px 0 0 12px;
+ /* 鍙�夛細鍔犱竴鐐瑰乏鍐呰竟璺濓紝璁╂枃瀛椾笉璐磋竟 */
+ padding-left: 4px;
+ }
+ }
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 14px 18px;
+ width: 100%;
+ height: 958px;
+ box-sizing: border-box;
+}
+
+.inspect-block {
+ flex: 1 1 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 8px 0;
+ gap: 6px;
+ position: relative;
+}
+
+.inspect-block:not(:last-child)::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 1px;
+ background: linear-gradient(90deg, rgba(33, 122, 255, 0) 0%, rgba(33, 122, 255, 0.55) 50%, rgba(33, 122, 255, 0) 100%);
+ pointer-events: none;
+}
+
+.inspect-body {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ gap: 18px;
+}
+
+.ring {
+ width: 120px;
+ height: 120px;
+ flex: 0 0 120px;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* 澶栧湀鍒诲害锛堢偣鐘剁幆锛� */
+.ring::before {
+ content: '';
+ position: absolute;
+ inset: -8px;
+ border-radius: 50%;
+ background: repeating-conic-gradient(from 0deg,
+ rgba(78, 228, 255, 0.75) 0 1deg,
+ rgba(78, 228, 255, 0) 1deg 9deg);
+ -webkit-mask: radial-gradient(circle, transparent 62%, #000 63%);
+ mask: radial-gradient(circle, transparent 62%, #000 63%);
+ opacity: 0.35;
+ pointer-events: none;
+}
+
+/* 鏌斿拰鍙戝厜鑳屾櫙 */
+.ring::after {
+ content: '';
+ position: absolute;
+ inset: -20px;
+ border-radius: 50%;
+ background: radial-gradient(circle, rgba(78, 228, 255, 0.18) 0%, rgba(78, 228, 255, 0.06) 40%, rgba(0, 0, 0, 0) 70%);
+ filter: blur(0.2px);
+ pointer-events: none;
+}
+
+.stats {
+ width: 240px;
+ flex: 0 0 240px;
+ display: grid;
+ grid-template-rows: 1fr 1fr;
+ gap: 10px;
+}
+
+.stat-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ height: 100%;
+ padding: 10px 14px;
+ border-radius: 4px;
+ border: 1px solid rgba(78, 228, 255, 0.22);
+ background: linear-gradient(90deg, rgba(33, 122, 255, 0.28) 0%, rgba(10, 28, 58, 0.35) 55%, rgba(10, 28, 58, 0.2) 100%);
+ box-shadow: inset 0 0 18px rgba(16, 45, 95, 0.25);
+}
+
+.stat-left {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ color: #b8c8e0;
+ font-size: 12px;
+}
+
+.dot {
+ width: 10px;
+ height: 10px;
+ border-radius: 2px;
+ display: inline-block;
+ box-shadow: 0 0 10px rgba(78, 228, 255, 0.25);
+}
+
+.dot-qualified {
+ background: rgba(184, 200, 224, 0.85);
+}
+
+.dot-unqualified {
+ background: #4ee4ff;
+}
+
+.stat-right {
+ display: inline-flex;
+ align-items: baseline;
+ gap: 14px;
+}
+
+.stat-value {
+ color: #ffffff;
+ font-size: 14px;
+ font-weight: 600;
+ min-width: 40px;
+ text-align: right;
+ text-shadow: 0 0 10px rgba(78, 228, 255, 0.15);
+}
+
+.stat-percent {
+ color: rgba(184, 200, 224, 0.95);
+ font-size: 12px;
+ min-width: 40px;
+ text-align: right;
+}
+
+/* 璁╁垏鎹㈡寜閽洿璐磋繎鎴浘锛堟洿绱у噾锛� */
+:deep(.date-type-switch .el-radio-button__inner) {
+ padding: 4px 16px;
+ font-size: 12px;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue b/src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue
new file mode 100644
index 0000000..f4b0a0c
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue
@@ -0,0 +1,189 @@
+<template>
+ <div>
+ <PanelHeader title="涓嶅悎鏍兼鍝佸鐞嗗垎鏋�" />
+ <div class="panel-item-customers">
+ <div class="pie-chart-wrapper" ref="pieWrapperRef">
+ <div class="pie-background" ref="pieBackgroundRef"></div>
+ <Echarts ref="chart" :chartStyle="chartStyle" :legend="landLegend" :series="computedSeries"
+ :tooltip="landTooltip" :color="landColors" :options="pieOptions" style="height: 100%" class="land-chart" />
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
+import Echarts from '@/components/Echarts/echarts.vue'
+import PanelHeader from './PanelHeader.vue'
+import { unqualifiedProductProcessingAnalysis } from '@/api/viewIndex.js'
+import { useChartBackground } from '@/hooks/useChartBackground.js'
+
+const pieWrapperRef = ref(null)
+const pieBackgroundRef = ref(null)
+const chart = ref(null)
+
+// 鏁版嵁鍒楄〃
+const dataList = ref([])
+
+// 棰滆壊鍒楄〃
+const landColors = ['#26FFCB', '#24CBFF', '#35FBF4', '#2651FF', '#D1E4F5', '#5782F7', '#2F67EF', '#82BAFF']
+
+// label 瀵屾枃鏈牱寮�
+const dotRich = landColors.reduce((acc, color, idx) => {
+ acc[`dot${idx}`] = {
+ width: 8,
+ height: 8,
+ borderRadius: 8,
+ backgroundColor: color,
+ align: 'center',
+ }
+ return acc
+}, {})
+
+// 鍥句緥閰嶇疆
+const landLegend = ref({
+ show: false,
+ icon: 'circle',
+ data: [],
+ right: '8%',
+ top: '40%',
+ orient: 'vertical',
+ textStyle: {
+ color: '#fff',
+ rich: {
+ unit: { color: '#fff', fontSize: 12, padding: [0, 10, 0, 0] },
+ text: { width: 60, color: '#fff', fontSize: 12 },
+ }
+ }
+})
+
+// 鎻愮ず妗嗛厤缃�
+const landTooltip = {
+ trigger: 'item',
+ alwaysShowContent: false,
+ position: function (pt) {
+ return [pt[0], 130]
+ },
+ formatter: function (params) {
+ // 纭繚 params.data 瀛樺湪
+ if (!params.data) return ''
+ const { name, value, rate } = params.data
+ return `${name}<br/>鏁伴噺锛�${value}涓�<br/>鍗犳瘮锛�${rate}%`
+ },
+}
+
+// 浣跨敤璁$畻灞炴�у鐞� Series
+const computedSeries = computed(() => {
+ return [
+ {
+ name: '涓嶅悎鏍兼鍝佸鐞嗗垎鏋�',
+ type: 'pie',
+ radius: ['35%', '55%'],
+ center: ['50%', '50%'],
+ label: {
+ show: true,
+ position: 'outside',
+ color: '#fff',
+ rich: {
+ ...dotRich,
+ parent: { fontSize: 14, fontWeight: 600, color: '#fff', lineHeight: 20 },
+ child: { fontSize: 12, color: '#fff', lineHeight: 18 },
+ },
+ formatter: function (params) {
+ if (!params.data) return ''
+ const dotKey = `dot${params.dataIndex % landColors.length}`
+ return `{${dotKey}|} {parent|${params.data.name} (${params.data.value}涓�)}`
+ },
+ },
+ labelLine: {
+ show: true,
+ length: 20,
+ lineStyle: { color: '#B8C8E0' },
+ },
+ data: dataList.value,
+ },
+ {
+ // 鍐呭湀瑁呴グ
+ type: 'pie',
+ radius: ['35%', '40%'],
+ center: ['50%', '50%'],
+ silent: true,
+ label: { show: false },
+ itemStyle: { color: 'rgba(0, 127, 255, 0.25)' },
+ data: [1],
+ },
+ ]
+})
+
+const chartStyle = { width: '100%', height: '126%' }
+const pieOptions = { backgroundColor: 'transparent' }
+
+// 鑳屾櫙澶勭悊閽╁瓙
+const { adjustBackgroundPosition, init: initBackground, cleanup: cleanupBackground } = useChartBackground({
+ wrapperRef: pieWrapperRef,
+ backgroundRef: pieBackgroundRef,
+ offsetX: '-51.5%',
+ offsetY: '-39%',
+ watchData: dataList
+})
+
+const loadData = async () => {
+ try {
+ const res = await unqualifiedProductProcessingAnalysis()
+ if (res && res.code === 200) {
+ dataList.value = (res.data || []).map((it) => ({
+ name: it.name,
+ value: Number(it.value || 0),
+ rate: it.rate,
+ }))
+ landLegend.value.data = dataList.value.map((d) => d.name)
+
+ // 鏁版嵁鏇存柊鍚庡井璋冭儗鏅�
+ setTimeout(() => {
+ adjustBackgroundPosition()
+ }, 100)
+ }
+ } catch (e) {
+ console.error('鑾峰彇鏁版嵁澶辫触:', e)
+ }
+}
+
+onMounted(() => {
+ loadData()
+ initBackground()
+})
+
+onBeforeUnmount(() => {
+ cleanupBackground()
+})
+</script>
+
+<style scoped>
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+}
+
+.pie-chart-wrapper {
+ position: relative;
+ width: 100%;
+ height: 320px;
+}
+
+.pie-background {
+ position: absolute;
+ width: 360px;
+ height: 360px;
+ background-image: url('@/assets/BI/鐜懓鍥捐竟妗�.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+ z-index: 1;
+ pointer-events: none;
+ left: 50%;
+ top: 50%;
+ transform: translate(-51.5%, -39%);
+}
+</style>
\ No newline at end of file
diff --git a/src/views/reportAnalysis/qualityAnalysis/components/right-top.vue b/src/views/reportAnalysis/qualityAnalysis/components/right-top.vue
new file mode 100644
index 0000000..890e99a
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/components/right-top.vue
@@ -0,0 +1,132 @@
+<template>
+ <div>
+ <PanelHeader title="涓嶅悎鏍间骇鍝佹帓鍚�" />
+ <div class="main-panel panel-item-customers">
+ <div class="main-panel-container">
+ <div style="color: white" class="main-panel-box" v-for="(item, index) in panelList" :key="index">
+ <!-- <div style="flex: 1" class="main-panel-box-left">{{ item.rank }}</div> -->
+ <div style="flex: 1" class="main-panel-box-left">{{ item.productName }}</div>
+ <div style="flex: 3" class="main-panel-box-right">
+ <!-- <div class="main-panel-box-right-title">{{ item.productName }}</div> -->
+ <div class="main-panel-box-right-text">
+ <span>鎬绘暟閲忥細{{ item.total }}</span>
+ <span>宸插畬鎴愶細{{ item.finished }}</span>
+ <span>鍚堟牸鐜囷細{{ item.qualifiedRate }}%</span>
+ </div>
+ <div class="main-panel-box-right-progress">
+ <el-progress :percentage="item.percentage" :format="format" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { unqualifiedProductRanking } from '@/api/viewIndex.js'
+import PanelHeader from './PanelHeader.vue'
+
+const panelList = ref([])
+
+const format = (percentage) => {
+ return `${percentage}%`
+}
+
+const fetchData = () => {
+ unqualifiedProductRanking()
+ .then((res) => {
+ if (res?.code === 200 && Array.isArray(res?.data)) {
+ const data = res.data
+ panelList.value = data.map((item, index) => {
+ const total = Number(item.totalCount) || 0
+ const finished = Number(item.completedCount) || 0
+ const passRate = Number(item.passRate) || 0
+
+ return {
+ rank: `Top${index + 1}`,
+ productName: item.productName || `浜у搧${index + 1}`,
+ total: total.toFixed(2),
+ finished: finished.toFixed(2),
+ qualifiedRate: passRate.toFixed(2),
+ percentage: Math.min(100, Math.max(0, passRate)), // 纭繚鐧惧垎姣斿湪0-100涔嬮棿
+ }
+ })
+ }
+ })
+ .catch((err) => {
+ console.error('鑾峰彇宸ュ崟鎵ц鏁堢巼鍒嗘瀽鏁版嵁澶辫触:', err)
+ })
+}
+
+onMounted(() => {
+ fetchData()
+})
+</script>
+
+<style scoped>
+.main-panel-box {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ height: 40px;
+
+ .main-panel-box-left {
+ background: red;
+ border-radius: 20px;
+ text-align: center;
+ line-height: 32px;
+ margin: 0 20px;
+ }
+
+ .main-panel-box-right {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+
+ .main-panel-box-right-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #ffffff;
+ margin-bottom: 6px;
+ }
+
+ .main-panel-box-right-text {
+ font-size: 12px;
+ display: flex;
+ justify-content: space-between;
+ padding-right: 60px;
+ margin-bottom: 4px;
+ }
+
+ .main-panel-box-right-progress {
+ :deep(.el-progress__text) {
+ color: white !important;
+ }
+ }
+ }
+}
+
+.main-panel-container {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ height: 100%;
+ overflow-y: auto;
+}
+
+.main-panel {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.panel-item-customers {
+ border: 1px solid #1a58b0;
+ padding: 18px;
+ width: 100%;
+ height: 449px;
+ overflow: hidden;
+}
+</style>
diff --git a/src/views/reportAnalysis/qualityAnalysis/index.vue b/src/views/reportAnalysis/qualityAnalysis/index.vue
new file mode 100644
index 0000000..59fcc9b
--- /dev/null
+++ b/src/views/reportAnalysis/qualityAnalysis/index.vue
@@ -0,0 +1,288 @@
+<template>
+ <div class="scale-container">
+ <div class="data-dashboard" :style="{ transform: `scale(${scaleRatio})` }">
+ <!-- 鍏ㄥ睆鎸夐挳 - 绉诲姩鍒板乏涓婅 -->
+ <button class="fullscreen-btn" @click="toggleFullscreen" :title="isFullscreen ? '閫�鍑哄叏灞�' : '鍏ㄥ睆鏄剧ず'">
+ <svg v-if="!isFullscreen" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
+ </svg>
+ <svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
+ </svg>
+ </button>
+
+ <!-- 椤堕儴鏍囬鏍� -->
+ <div class="dashboard-header">
+ <div class="factory-name">璐ㄩ噺鏁版嵁鍒嗘瀽</div>
+ </div>
+
+ <!-- 涓昏鍐呭鍖哄煙 -->
+ <div class="dashboard-content">
+ <!-- 宸︿晶鍖哄煙 -->
+ <div class="left-panel">
+ <LeftTop />
+ </div>
+
+ <!-- 涓棿鍖哄煙 -->
+ <div class="center-panel">
+ <CenterTop />
+ <CenterCenter/>
+ <CenterBottom />
+ </div>
+
+ <!-- 鍙充晶鍖哄煙 -->
+ <div class="right-panel">
+ <RightTop />
+ <RightBottom />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
+import autofit from 'autofit.js'
+import CenterCenter from './components/center-center.vue'
+import RightTop from './components/right-top.vue'
+import RightBottom from './components/right-bottom.vue'
+import useUserStore from '@/store/modules/user'
+import LeftTop from './components/left-top.vue'
+import CenterTop from './components/center-top.vue'
+import CenterBottom from './components/center-bottom.vue'
+
+// 鍏ㄥ睆鐩稿叧鐘舵��
+const isFullscreen = ref(false);
+
+// 缂╂斁姣斾緥
+const scaleRatio = ref(1)
+// 璁捐灏哄锛堝熀鍑嗗昂瀵革級- 鏍规嵁瀹為檯璁捐绋胯皟鏁�
+const designWidth = 1920
+const designHeight = 1080
+
+// 鐢ㄦ埛store
+const userStore = useUserStore()
+
+// 璁$畻缂╂斁姣斾緥
+const calculateScale = () => {
+ const container = document.querySelector('.scale-container')
+ if (!container) return
+
+ // 鑾峰彇瀹瑰櫒鐨勫疄闄呭昂瀵�
+ const rect = container.getBoundingClientRect?.()
+ const containerWidth = container.clientWidth || rect?.width || window.innerWidth
+ const containerHeight = container.clientHeight || rect?.height || window.innerHeight
+
+ // 璁$畻瀹介珮缂╂斁姣斾緥锛屽彇杈冨皬鍊间互淇濊瘉鍐呭瀹屾暣鏄剧ず锛堢瓑姣旂缉鏀撅級
+ const scaleX = containerWidth / designWidth
+ const scaleY = containerHeight / designHeight
+ scaleRatio.value = Math.min(scaleX, scaleY)
+}
+
+// 绐楀彛澶у皬鍙樺寲澶勭悊
+const handleResize = () => {
+ // 寤惰繜鎵ц锛岀‘淇滵OM鏇存柊瀹屾垚
+ setTimeout(() => {
+ calculateScale()
+ }, 100)
+}
+
+// 鍏ㄥ睆鍔熻兘瀹炵幇 - 閽堝scale-container鍏冪礌
+const toggleFullscreen = () => {
+ const element = document.querySelector('.scale-container')
+
+ if (!element) return
+
+ if (!isFullscreen.value) {
+ if (element.requestFullscreen) {
+ element.requestFullscreen()
+ } else if (element.webkitRequestFullscreen) {
+ element.webkitRequestFullscreen()
+ } else if (element.msRequestFullscreen) {
+ element.msRequestFullscreen()
+ }
+ } else {
+ if (document.exitFullscreen) {
+ document.exitFullscreen()
+ } else if (document.webkitExitFullscreen) {
+ document.webkitExitFullscreen()
+ } else if (document.msExitFullscreen) {
+ document.msExitFullscreen()
+ }
+ }
+}
+
+// 鐩戝惉鍏ㄥ睆鍙樺寲浜嬩欢
+const handleFullscreenChange = () => {
+ const fullscreenElement = document.fullscreenElement ||
+ document.webkitFullscreenElement ||
+ document.msFullscreenElement
+ isFullscreen.value = fullscreenElement && fullscreenElement.classList.contains('scale-container')
+
+ // 鍏ㄥ睆鐘舵�佸彉鍖栨椂锛屽欢杩熼噸鏂拌绠楃缉鏀炬瘮渚嬶紙纭繚DOM鏇存柊瀹屾垚锛�
+ setTimeout(() => {
+ calculateScale()
+ }, 200)
+}
+
+// 鐢熷懡鍛ㄦ湡閽╁瓙
+onMounted(() => {
+ // 浣跨敤nextTick纭繚DOM瀹屽叏娓叉煋鍚庡啀鍒濆鍖�
+ nextTick(() => {
+ // 璁$畻鍒濆缂╂斁姣斾緥
+ calculateScale()
+ })
+
+ window.addEventListener('resize', handleResize)
+ window.addEventListener('fullscreenchange', handleFullscreenChange)
+ window.addEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.addEventListener('MSFullscreenChange', handleFullscreenChange)
+})
+
+onBeforeUnmount(() => {
+ window.removeEventListener('resize', handleResize)
+ window.removeEventListener('fullscreenchange', handleFullscreenChange)
+ window.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
+ window.removeEventListener('MSFullscreenChange', handleFullscreenChange)
+ // 绉婚櫎鎴戜滑娣诲姞鐨刟utofit鍔ㄦ�佽皟鏁寸洃鍚櫒
+ if (window._autofitUpdateHandler) {
+ window.removeEventListener('resize', window._autofitUpdateHandler)
+ delete window._autofitUpdateHandler
+ }
+ // 鍏抽棴autofit
+ autofit.off()
+})
+</script>
+
+<style scoped>
+/* 澶栭儴缂╂斁瀹瑰櫒 - 鍗犳嵁鏁翠釜瑙嗗彛 */
+.scale-container {
+position: relative;
+width: 100%;
+/* 椤甸潰鍦ㄥ父瑙勫竷灞�涓嬶紙鏈夐《鏍忥級榛樿鍑忓幓 84px锛岄伩鍏嶅唴瀹硅瑁佸垏 */
+height: calc(100vh - 84px);
+display: flex;
+align-items: center;
+justify-content: center;
+background-color: #000;
+overflow: hidden;
+}
+
+/* 鍐呴儴鍐呭鍖哄煙 - 鍥哄畾璁捐灏哄 */
+.data-dashboard {
+position: relative;
+width: 1920px;
+height: 1080px;
+background-image: url("@/assets/BI/backImage@2x.png");
+background-size: cover;
+background-position: center;
+background-repeat: no-repeat;
+transform-origin: center center;
+}
+
+/* 鍏ㄥ睆鐘舵�佺殑鏍峰紡 - 浣滅敤浜巗cale-container */
+.scale-container:fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* Webkit娴忚鍣ㄥ墠缂� */
+.scale-container:-webkit-full-screen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+/* MS娴忚鍣ㄥ墠缂� */
+.scale-container:-ms-fullscreen {
+width: 100vw;
+height: 100vh;
+margin: 0;
+padding: 0;
+background-color: #000;
+z-index: 9999;
+}
+
+
+.dashboard-header {
+position: relative;
+z-index: 1;
+height: 86px;
+background-image: url("@/assets/BI/biaoti.png");
+background-size: cover;
+background-repeat: no-repeat;
+display: flex;
+align-items: center;
+justify-content: center;
+}
+
+.factory-name {
+font-weight: 600;
+font-size: 52px;
+color: #FFFFFF;
+top: 16px;
+position: absolute;
+}
+
+.fullscreen-btn {
+position: absolute;
+top: 10px;
+left: 20px;
+width: 40px;
+height: 40px;
+background: rgba(0, 20, 60, 0.8);
+border: 1px solid rgba(0, 212, 255, 0.3);
+border-radius: 6px;
+color: #00d4ff;
+cursor: pointer;
+display: flex;
+align-items: center;
+justify-content: center;
+transition: all 0.3s;
+z-index: 10000;
+}
+
+.fullscreen-btn:hover {
+background: rgba(0, 30, 90, 0.9);
+border-color: rgba(0, 212, 255, 0.5);
+}
+
+.dashboard-content {
+position: relative;
+z-index: 1;
+display: flex;
+gap: 30px;
+padding: 0 30px;
+height: calc(100% - 86px);
+overflow: hidden;
+}
+
+/* 纭繚鍚勯潰鏉胯兘澶熸纭樉绀� */
+.left-panel, .center-panel, .right-panel {
+overflow: hidden;
+}
+
+.left-panel,
+.right-panel {
+flex: 1;
+display: flex;
+flex-direction: column;
+gap: 24px;
+width: 520px;
+}
+
+.center-panel {
+flex: 1.5;
+display: flex;
+flex-direction: column;
+gap: 20px;
+}
+
+</style>
\ No newline at end of file
diff --git a/src/views/reportAnalysis/reportManagement.vue b/src/views/reportAnalysis/reportManagement.vue
new file mode 100644
index 0000000..3c87550
--- /dev/null
+++ b/src/views/reportAnalysis/reportManagement.vue
@@ -0,0 +1,733 @@
+<template>
+ <div class="report-management">
+ <!-- 椤甸潰鏍囬 -->
+ <div class="page-header">
+ <h2>鎶ヨ〃绠$悊</h2>
+ <p>鎻愪緵鏍峰搧杩涘害銆佽澶囦娇鐢ㄣ�佹娴嬮」鐩�侀鐢ㄨ褰曠瓑鍚勭被鑷姩缁熻鎶ヨ〃</p>
+ </div>
+
+ <!-- 绛涢�夋潯浠� -->
+ <el-card class="filter-card" shadow="never">
+ <el-form :model="filterForm" inline>
+ <el-form-item label="鏃堕棿鑼冨洿">
+ <el-date-picker
+ v-model="filterForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ @change="handleFilterChange"
+ />
+ </el-form-item>
+ <el-form-item label="鎶ヨ〃绫诲瀷">
+ <el-select v-model="filterForm.reportType" placeholder="璇烽�夋嫨鎶ヨ〃绫诲瀷" @change="handleFilterChange">
+ <el-option label="鏍峰搧杩涘害鎶ヨ〃" value="sample" />
+ <el-option label="璁惧浣跨敤鎶ヨ〃" value="equipment" />
+ <el-option label="妫�娴嬮」鐩姤琛�" value="inspection" />
+ <el-option label="棰嗙敤璁板綍鎶ヨ〃" value="usage" />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleFilterChange">鏌ヨ</el-button>
+ <el-button @click="resetFilter">閲嶇疆</el-button>
+ <el-button type="success" @click="exportReport">瀵煎嚭鎶ヨ〃</el-button>
+ </el-form-item>
+ </el-form>
+ </el-card>
+
+ <!-- 缁熻鍗$墖 -->
+ <div class="statistics-cards">
+ <el-row :gutter="20">
+ <el-col :span="6">
+ <el-card class="stat-card" shadow="hover">
+ <div class="stat-content">
+ <div class="stat-icon">
+ <el-icon><Box /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-number">{{ statistics.totalSamples }}</div>
+ <div class="stat-label">鎬绘牱鍝佹暟</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card" shadow="hover">
+ <div class="stat-content">
+ <div class="stat-icon">
+ <el-icon><Tools /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-number">{{ statistics.activeEquipment }}</div>
+ <div class="stat-label">鍦ㄧ敤璁惧</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card" shadow="hover">
+ <div class="stat-content">
+ <div class="stat-icon">
+ <el-icon><Document /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-number">{{ statistics.completedInspections }}</div>
+ <div class="stat-label">宸插畬鎴愭娴�</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="6">
+ <el-card class="stat-card" shadow="hover">
+ <div class="stat-content">
+ <div class="stat-icon">
+ <el-icon><ShoppingCart /></el-icon>
+ </div>
+ <div class="stat-info">
+ <div class="stat-number">{{ statistics.totalUsage }}</div>
+ <div class="stat-label">鎬婚鐢ㄦ鏁�</div>
+ </div>
+ </div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 鍥捐〃鍖哄煙 -->
+ <div class="charts-container">
+ <el-row :gutter="20">
+ <!-- 鏍峰搧杩涘害鍥捐〃 -->
+ <el-col :span="12">
+ <el-card class="chart-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span>鏍峰搧杩涘害缁熻</span>
+ <el-button type="text" @click="refreshSampleChart">鍒锋柊</el-button>
+ </div>
+ </template>
+ <div ref="sampleChartRef" class="chart-container"></div>
+ </el-card>
+ </el-col>
+
+ <!-- 璁惧浣跨敤鍥捐〃 -->
+ <el-col :span="12">
+ <el-card class="chart-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span>璁惧浣跨敤鐜囩粺璁�</span>
+ <el-button type="text" @click="refreshEquipmentChart">鍒锋柊</el-button>
+ </div>
+ </template>
+ <div ref="equipmentChartRef" class="chart-container"></div>
+ </el-card>
+ </el-col>
+ </el-row>
+
+ <el-row :gutter="20" style="margin-top: 20px;">
+ <!-- 妫�娴嬮」鐩粺璁� -->
+ <el-col :span="12">
+ <el-card class="chart-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span>妫�娴嬮」鐩垎甯�</span>
+ <el-button type="text" @click="refreshInspectionChart">鍒锋柊</el-button>
+ </div>
+ </template>
+ <div ref="inspectionChartRef" class="chart-container"></div>
+ </el-card>
+ </el-col>
+
+ <!-- 棰嗙敤璁板綍瓒嬪娍 -->
+ <el-col :span="12">
+ <el-card class="chart-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span>棰嗙敤璁板綍瓒嬪娍</span>
+ <el-button type="text" @click="refreshUsageChart">鍒锋柊</el-button>
+ </div>
+ </template>
+ <div ref="usageChartRef" class="chart-container"></div>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 璇︾粏鏁版嵁琛ㄦ牸 -->
+ <el-card class="table-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <span>璇︾粏鏁版嵁</span>
+ <div>
+ <el-button type="primary" size="small" @click="refreshTable">鍒锋柊</el-button>
+ <el-button type="success" size="small" @click="exportTable">瀵煎嚭</el-button>
+ </div>
+ </div>
+ </template>
+
+ <el-table
+ :data="tableData"
+ style="width: 100%"
+ v-loading="tableLoading"
+ stripe
+ border
+ >
+ <el-table-column prop="id" label="缂栧彿" width="80" />
+ <el-table-column prop="name" label="鍚嶇О" />
+ <el-table-column prop="type" label="绫诲瀷" width="120" />
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="progress" label="杩涘害" width="120">
+ <template #default="scope">
+ <el-progress :percentage="scope.row.progress" :status="getProgressStatus(scope.row.progress)" />
+ </template>
+ </el-table-column>
+ <el-table-column prop="createTime" label="鍒涘缓鏃堕棿" width="180" />
+ <el-table-column prop="updateTime" label="鏇存柊鏃堕棿" width="180" />
+ <el-table-column label="鎿嶄綔" width="150" fixed="right">
+ <template #default="scope">
+ <el-button type="text" size="small" @click="viewDetail(scope.row)">鏌ョ湅</el-button>
+ <el-button type="text" size="small" @click="editItem(scope.row)">缂栬緫</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <div class="pagination-container">
+ <el-pagination
+ v-model:current-page="pagination.currentPage"
+ v-model:page-size="pagination.pageSize"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import * as echarts from 'echarts'
+import { Box, Tools, Document, ShoppingCart } from '@element-plus/icons-vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const filterForm = reactive({
+ dateRange: [],
+ reportType: 'sample'
+})
+
+const statistics = reactive({
+ totalSamples: 1250,
+ activeEquipment: 45,
+ completedInspections: 890,
+ totalUsage: 2340
+})
+
+const tableData = ref([])
+const tableLoading = ref(false)
+const pagination = reactive({
+ currentPage: 1,
+ pageSize: 20,
+ total: 0
+})
+
+// 鍥捐〃寮曠敤
+const sampleChartRef = ref(null)
+const equipmentChartRef = ref(null)
+const inspectionChartRef = ref(null)
+const usageChartRef = ref(null)
+
+// 鍥捐〃瀹炰緥
+let sampleChart = null
+let equipmentChart = null
+let inspectionChart = null
+let usageChart = null
+
+// 鍒濆鍖栨暟鎹�
+const initData = () => {
+ // 妯℃嫙琛ㄦ牸鏁版嵁
+ tableData.value = [
+ {
+ id: 'SP001',
+ name: '鏍峰搧A-001',
+ type: '閲戝睘鏉愭枡',
+ status: '妫�娴嬩腑',
+ progress: 75,
+ createTime: '2025-01-15 09:30:00',
+ updateTime: '2025-01-20 14:20:00'
+ },
+ {
+ id: 'SP002',
+ name: '鏍峰搧B-002',
+ type: '濉戞枡鍒跺搧',
+ status: '宸插畬鎴�',
+ progress: 100,
+ createTime: '2025-01-10 10:15:00',
+ updateTime: '2025-01-18 16:45:00'
+ },
+ {
+ id: 'SP003',
+ name: '鏍峰搧C-003',
+ type: '鐢靛瓙鍏冧欢',
+ status: '寰呮娴�',
+ progress: 0,
+ createTime: '2025-01-22 08:45:00',
+ updateTime: '2025-01-22 08:45:00'
+ },
+ {
+ id: 'EQ001',
+ name: '妫�娴嬭澶嘇',
+ type: '鍏夎氨浠�',
+ status: '浣跨敤涓�',
+ progress: 60,
+ createTime: '2025-01-05 14:20:00',
+ updateTime: '2025-01-20 11:30:00'
+ },
+ {
+ id: 'EQ002',
+ name: '妫�娴嬭澶嘊',
+ type: '鏄惧井闀�',
+ status: '绌洪棽',
+ progress: 0,
+ createTime: '2025-01-08 16:10:00',
+ updateTime: '2025-01-19 09:15:00'
+ }
+ ]
+
+ pagination.total = tableData.value.length
+}
+
+// 鍒濆鍖栨牱鍝佽繘搴﹀浘琛�
+const initSampleChart = () => {
+ if (sampleChartRef.value) {
+ sampleChart = echarts.init(sampleChartRef.value)
+ const option = {
+ title: {
+ text: '鏍峰搧杩涘害鍒嗗竷',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item',
+ formatter: '{a} <br/>{b}: {c} ({d}%)'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ series: [
+ {
+ name: '鏍峰搧鐘舵��',
+ type: 'pie',
+ radius: ['40%', '70%'],
+ avoidLabelOverlap: false,
+ label: {
+ show: false,
+ position: 'center'
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: '18',
+ fontWeight: 'bold'
+ }
+ },
+ labelLine: {
+ show: false
+ },
+ data: [
+ { value: 450, name: '宸插畬鎴�' },
+ { value: 320, name: '妫�娴嬩腑' },
+ { value: 280, name: '寰呮娴�' },
+ { value: 200, name: '宸叉殏鍋�' }
+ ]
+ }
+ ]
+ }
+ sampleChart.setOption(option)
+ }
+}
+
+// 鍒濆鍖栬澶囦娇鐢ㄥ浘琛�
+const initEquipmentChart = () => {
+ if (equipmentChartRef.value) {
+ equipmentChart = echarts.init(equipmentChartRef.value)
+ const option = {
+ title: {
+ text: '璁惧浣跨敤鐜�',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'shadow'
+ }
+ },
+ xAxis: {
+ type: 'category',
+ data: ['鍏夎氨浠�', '鏄惧井闀�', '纭害璁�', '鎷夊姏鏈�', '鍐插嚮鏈�', '閲戠浉浠�']
+ },
+ yAxis: {
+ type: 'value',
+ name: '浣跨敤鐜�(%)'
+ },
+ series: [
+ {
+ name: '浣跨敤鐜�',
+ type: 'bar',
+ data: [85, 60, 75, 90, 45, 70],
+ label: {
+ show: true,
+ position: 'inside',
+ align: 'center',
+ verticalAlign: 'middle',
+ formatter: '{c}%',
+ color: '#fff'
+ },
+ itemStyle: {
+ color: function(params) {
+ const colors = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#9C27B0']
+ return colors[params.dataIndex]
+ }
+ }
+ }
+ ]
+ }
+ equipmentChart.setOption(option)
+ }
+}
+
+// 鍒濆鍖栨娴嬮」鐩浘琛�
+const initInspectionChart = () => {
+ if (inspectionChartRef.value) {
+ inspectionChart = echarts.init(inspectionChartRef.value)
+ const option = {
+ title: {
+ text: '妫�娴嬮」鐩垎甯�',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'item'
+ },
+ legend: {
+ orient: 'vertical',
+ left: 'left'
+ },
+ series: [
+ {
+ name: '妫�娴嬮」鐩�',
+ type: 'pie',
+ radius: '50%',
+ data: [
+ { value: 335, name: '鐗╃悊鎬ц兘' },
+ { value: 310, name: '鍖栧鍒嗘瀽' },
+ { value: 234, name: '灏哄娴嬮噺' },
+ { value: 135, name: '澶栬妫�鏌�' },
+ { value: 148, name: '鍏朵粬妫�娴�' }
+ ],
+ emphasis: {
+ itemStyle: {
+ shadowBlur: 10,
+ shadowOffsetX: 0,
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
+ }
+ }
+ }
+ ]
+ }
+ inspectionChart.setOption(option)
+ }
+}
+
+// 鍒濆鍖栭鐢ㄨ褰曞浘琛�
+const initUsageChart = () => {
+ if (usageChartRef.value) {
+ usageChart = echarts.init(usageChartRef.value)
+ const option = {
+ title: {
+ text: '棰嗙敤璁板綍瓒嬪娍',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis'
+ },
+ legend: {
+ data: ['棰嗙敤娆℃暟', '褰掕繕娆℃暟']
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: ['1鏈�', '2鏈�', '3鏈�', '4鏈�', '5鏈�', '6鏈�', '7鏈�', '8鏈�', '9鏈�', '10鏈�', '11鏈�', '12鏈�']
+ },
+ yAxis: {
+ type: 'value'
+ },
+ series: [
+ {
+ name: '棰嗙敤娆℃暟',
+ type: 'line',
+ stack: 'Total',
+ data: [120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330]
+ },
+ {
+ name: '褰掕繕娆℃暟',
+ type: 'line',
+ stack: 'Total',
+ data: [110, 125, 95, 128, 85, 220, 200, 175, 185, 225, 280, 320]
+ }
+ ]
+ }
+ usageChart.setOption(option)
+ }
+}
+
+// 浜嬩欢澶勭悊鍑芥暟
+const handleFilterChange = () => {
+ ElMessage.success('绛涢�夋潯浠跺凡鏇存柊')
+ // 杩欓噷鍙互鏍规嵁绛涢�夋潯浠堕噸鏂板姞杞芥暟鎹�
+}
+
+const resetFilter = () => {
+ filterForm.dateRange = []
+ filterForm.reportType = 'sample'
+ ElMessage.info('绛涢�夋潯浠跺凡閲嶇疆')
+}
+
+const exportReport = () => {
+ ElMessage.success('鎶ヨ〃瀵煎嚭鍔熻兘寮�鍙戜腑...')
+}
+
+const refreshSampleChart = () => {
+ initSampleChart()
+ ElMessage.success('鏍峰搧杩涘害鍥捐〃宸插埛鏂�')
+}
+
+const refreshEquipmentChart = () => {
+ initEquipmentChart()
+ ElMessage.success('璁惧浣跨敤鍥捐〃宸插埛鏂�')
+}
+
+const refreshInspectionChart = () => {
+ initInspectionChart()
+ ElMessage.success('妫�娴嬮」鐩浘琛ㄥ凡鍒锋柊')
+}
+
+const refreshUsageChart = () => {
+ initUsageChart()
+ ElMessage.success('棰嗙敤璁板綍鍥捐〃宸插埛鏂�')
+}
+
+const refreshTable = () => {
+ tableLoading.value = true
+ setTimeout(() => {
+ tableLoading.value = false
+ ElMessage.success('琛ㄦ牸鏁版嵁宸插埛鏂�')
+ }, 1000)
+}
+
+const exportTable = () => {
+ ElMessage.success('琛ㄦ牸瀵煎嚭鍔熻兘寮�鍙戜腑...')
+}
+
+const handleSizeChange = (val) => {
+ pagination.pageSize = val
+ // 閲嶆柊鍔犺浇鏁版嵁
+}
+
+const handleCurrentChange = (val) => {
+ pagination.currentPage = val
+ // 閲嶆柊鍔犺浇鏁版嵁
+}
+
+const getStatusType = (status) => {
+ const statusMap = {
+ '宸插畬鎴�': 'success',
+ '妫�娴嬩腑': 'warning',
+ '寰呮娴�': 'info',
+ '宸叉殏鍋�': 'danger',
+ '浣跨敤涓�': 'primary',
+ '绌洪棽': 'info'
+ }
+ return statusMap[status] || 'info'
+}
+
+const getProgressStatus = (progress) => {
+ if (progress === 100) return 'success'
+ if (progress >= 80) return 'warning'
+ if (progress >= 50) return ''
+ return 'exception'
+}
+
+const viewDetail = (row) => {
+ ElMessage.info(`鏌ョ湅璇︽儏: ${row.name}`)
+}
+
+const editItem = (row) => {
+ ElMessage.info(`缂栬緫椤圭洰: ${row.name}`)
+}
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ initData()
+ nextTick(() => {
+ initSampleChart()
+ initEquipmentChart()
+ initInspectionChart()
+ initUsageChart()
+ })
+
+ // 鐩戝惉绐楀彛澶у皬鍙樺寲锛岄噸鏂拌皟鏁村浘琛ㄥぇ灏�
+ window.addEventListener('resize', () => {
+ sampleChart?.resize()
+ equipmentChart?.resize()
+ inspectionChart?.resize()
+ usageChart?.resize()
+ })
+})
+</script>
+
+<style scoped>
+.report-management {
+ padding: 20px;
+ background-color: #f5f5f5;
+ min-height: 100vh;
+}
+
+.page-header {
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+.page-header h2 {
+ color: #303133;
+ margin-bottom: 8px;
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.page-header p {
+ color: #909399;
+ font-size: 14px;
+ margin: 0;
+}
+
+.filter-card {
+ margin-bottom: 20px;
+}
+
+.statistics-cards {
+ margin-bottom: 20px;
+}
+
+.stat-card {
+ height: 120px;
+}
+
+.stat-content {
+ display: flex;
+ align-items: center;
+ height: 100%;
+}
+
+.stat-icon {
+ width: 60px;
+ height: 60px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 20px;
+ font-size: 24px;
+ color: white;
+}
+
+.stat-card:nth-child(1) .stat-icon {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.stat-card:nth-child(2) .stat-icon {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.stat-card:nth-child(3) .stat-icon {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.stat-card:nth-child(4) .stat-icon {
+ background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
+}
+
+.stat-info {
+ flex: 1;
+}
+
+.stat-number {
+ font-size: 28px;
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 8px;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #909399;
+}
+
+.charts-container {
+ margin-bottom: 20px;
+}
+
+.chart-card {
+ margin-bottom: 20px;
+}
+
+.card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.chart-container {
+ height: 300px;
+ width: 100%;
+}
+
+.table-card {
+ margin-bottom: 20px;
+}
+
+.pagination-container {
+ margin-top: 20px;
+ text-align: right;
+}
+
+:deep(.el-card__header) {
+ padding: 15px 20px;
+ border-bottom: 1px solid #ebeef5;
+ background-color: #fafafa;
+}
+
+:deep(.el-card__body) {
+ padding: 20px;
+}
+
+:deep(.el-table) {
+ margin-bottom: 20px;
+}
+
+:deep(.el-progress) {
+ margin: 0;
+}
+
+:deep(.el-tag) {
+ margin: 0;
+}
+</style>
diff --git a/src/views/reportAnalysis/taxComparison/index.vue b/src/views/reportAnalysis/taxComparison/index.vue
new file mode 100644
index 0000000..d27ab07
--- /dev/null
+++ b/src/views/reportAnalysis/taxComparison/index.vue
@@ -0,0 +1,118 @@
+<template>
+ <div class="app-container">
+ <el-form :model="filters" :inline="true">
+ <el-form-item label="鏃ユ湡">
+ <el-date-picker
+ style="width: 240px"
+ v-model="filters.month"
+ value-format="YYYY-MM"
+ format="YYYY-MM"
+ type="month"
+ placeholder="閫夋嫨鏈堜唤"
+ clearable
+ @change="getTableData"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="getTableData"> 鎼滅储 </el-button>
+ <el-button @click="resetFilters"> 閲嶇疆 </el-button>
+ <el-button @click="handleOut"> 瀵煎嚭 </el-button>
+ </el-form-item>
+ </el-form>
+ <div class="table_list">
+ <PIMTable
+ rowKey="id"
+ :column="columns"
+ :tableData="dataList"
+ :page="{
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ total: pagination.total,
+ }"
+ @pagination="changePage"
+ />
+ </div>
+ </div>
+</template>
+
+<script setup>
+import { usePaginationApi } from "@/hooks/usePaginationApi";
+import { onMounted, getCurrentInstance } from "vue";
+import { getTaxList } from "@/api/procurementManagement/taxComparison";
+import { ElMessageBox } from "element-plus";
+
+const { proxy } = getCurrentInstance();
+
+defineOptions({
+ name: "澧炲�肩◣姣斿",
+});
+
+const {
+ loading,
+ filters,
+ columns,
+ dataList,
+ pagination,
+ getTableData,
+ resetFilters,
+ onCurrentChange,
+} = usePaginationApi(
+ getTaxList,
+ {
+ month: [], // 鏉ョエ鏃ユ湡
+ },
+ [
+ {
+ label: "鏈堜唤",
+ prop: "month",
+ align: "center",
+ },
+ {
+ label: "閿�椤圭◣棰�",
+ prop: "jtaxAmount",
+ align: "center",
+ },
+ {
+ label: "杩涢」绋庨",
+ prop: "xtaxAmount",
+ align: "center",
+ },
+ {
+ label: "閿�-杩�",
+ prop: "taxAmount",
+ align: "center",
+ },
+ ],
+ {}
+);
+
+const changePage = ({ page }) => {
+ pagination.currentPage = page;
+ onCurrentChange(page);
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/purchase/report/exportTwo", {}, "澧炲�肩◣姣斿.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+onMounted(() => {
+ getTableData();
+});
+</script>
+
+<style lang="scss" scoped>
+.table_list {
+ margin-top: unset;
+}
+</style>
diff --git a/src/views/safeProduction/accidentReportingRecord/index.vue b/src/views/safeProduction/accidentReportingRecord/index.vue
new file mode 100644
index 0000000..5a6c345
--- /dev/null
+++ b/src/views/safeProduction/accidentReportingRecord/index.vue
@@ -0,0 +1,863 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">浜嬫晠鍚嶇О锛�</span>
+ <el-input v-model="searchForm.accidentName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ヤ簨鏁呭悕绉版悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span class="search_title ml10">浜嬫晠绫诲瀷锛�</span>
+ <el-select v-model="searchForm.accidentType"
+ clearable
+ @change="handleQuery"
+ style="width: 240px">
+ <el-option v-for="item in accidentTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板浜嬫晠</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <!-- 鏂板/缂栬緫浜嬫晠寮圭獥 -->
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="800px"
+ :close-on-click-modal="false">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="150px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浜嬫晠缂栧彿"
+ prop="accidentCode">
+ <el-input v-model="form.accidentCode"
+ placeholder="璇疯緭鍏ヤ簨鏁呯紪鍙�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜嬫晠鍚嶇О"
+ prop="accidentName">
+ <el-input v-model="form.accidentName"
+ placeholder="璇疯緭鍏ヤ簨鏁呭悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浜嬫晠鍙戠敓鏃堕棿锛�"
+ prop="happenTime">
+ <el-date-picker style="width: 100%"
+ v-model="form.happenTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm:ss"
+ type="datetime"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜嬫晠鍙戠敓浣嶇疆"
+ prop="happenLocation">
+ <el-input v-model="form.happenLocation"
+ placeholder="璇疯緭鍏ヤ簨鏁呭彂鐢熶綅缃�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浜嬫晠绛夌骇"
+ prop="accidentGrade">
+ <el-select v-model="form.accidentGrade"
+ placeholder="璇烽�夋嫨浜嬫晠绛夌骇"
+ style="width: 100%">
+ <el-option v-for="item in accidentGradeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浜嬫晠绫诲瀷"
+ prop="accidentType">
+ <el-select v-model="form.accidentType"
+ placeholder="璇烽�夋嫨浜嬫晠绫诲瀷"
+ style="width: 100%">
+ <el-option v-for="item in accidentTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浜哄憳鎹熷け鎯呭喌 "
+ prop="personLoss">
+ <el-input v-model="form.personLoss"
+ placeholder="璇疯緭鍏ヤ汉鍛樻崯澶辨儏鍐�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐩存帴璐骇鎹熷け锛堝厓锛�"
+ prop="assetLoss">
+ <el-input v-model="form.assetLoss"
+ type="number"
+ placeholder="璇疯緭鍏ョ洿鎺ヨ储浜ф崯澶�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="涓婃姤浜�"
+ prop="createUser">
+ <el-select v-model="form.createUser"
+ placeholder="璇烽�夋嫨"
+ disabled
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓婃姤鏃堕棿"
+ prop="createTime">
+ <el-date-picker style="width: 100%"
+ v-model="form.createTime"
+ value-format="YYYY-MM-DD HH:mm:ss"
+ format="YYYY-MM-DD HH:mm:ss"
+ type="datetime"
+ disabled
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="浜嬫晠鐩存帴鍘熷洜"
+ prop="accidentCause">
+ <el-input v-model="form.accidentCause"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ簨鏁呯洿鎺ュ師鍥�" />
+ </el-form-item>
+ <el-form-item label="浜嬫晠鏍规湰鍘熷洜"
+ prop="rootCause">
+ <el-input v-model="form.rootCause"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ヤ簨鏁呮牴鏈師鍥�" />
+ </el-form-item>
+ <el-form-item label="鐢熶骇褰卞搷鎯呭喌"
+ prop="productionLoss">
+ <el-input v-model="form.productionLoss"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ョ敓浜у奖鍝嶆儏鍐�" />
+ </el-form-item>
+ <el-form-item label="鐜板満搴旀�ュ缃帾鏂�"
+ prop="handleMeasures">
+ <el-input v-model="form.handleMeasures"
+ type="textarea"
+ :rows="3"
+ placeholder="鐜板満搴旀�ュ缃帾鏂�" />
+ </el-form-item>
+ <el-form-item label="澶囨敞"
+ prop="remark">
+ <el-input v-model="form.remark"
+ type="textarea"
+ :rows="3"
+ placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鏌ョ湅浜嬫晠璇︽儏寮圭獥 -->
+ <el-dialog v-model="viewDialogVisible"
+ title="浜嬫晠璇︽儏"
+ width="900px"
+ :close-on-click-modal="false">
+ <div class="knowledge-detail">
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="浜嬫晠缂栫爜">
+ <span>{{ currentKnowledge.accidentCode }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="浜嬫晠鍚嶇О">
+ <span>{{ currentKnowledge.accidentName }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="浜嬫晠鍙戠敓鏃堕棿">
+ {{ currentKnowledge.happenTime }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浜嬫晠鍙戠敓鍦扮偣">
+ {{ currentKnowledge.happenLocation }}
+ </el-descriptions-item>
+ <el-descriptions-item label="浜嬫晠绛夌骇">
+ <el-tag :type="accidentGradeType(currentKnowledge.accidentGrade)">{{ currentKnowledge.accidentGrade }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="浜嬫晠绫诲瀷">
+ <el-tag type="info">{{ accidentTypeLabel(currentKnowledge.accidentType) }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="浜哄憳鎹熷け鎯呭喌">
+ {{ currentKnowledge.personLoss }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鐩存帴璐骇鎹熷け锛堝厓锛�">
+ {{ currentKnowledge.assetLoss }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓婃姤浜�">
+ {{ currentKnowledge.createUserName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="涓婃姤鏃堕棿">
+ {{ currentKnowledge.createTime }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <div class="detail-section">
+ <h4>浜嬫晠鐩存帴鍘熷洜</h4>
+ <div class="detail-content">{{ currentKnowledge.accidentCause }}</div>
+ </div>
+ <div class="detail-section">
+ <h4>浜嬫晠鏍规湰鍘熷洜</h4>
+ <div class="detail-content">{{ currentKnowledge.rootCause }}</div>
+ </div>
+ <div class="detail-section">
+ <h4>鐢熶骇褰卞搷鎯呭喌</h4>
+ <div class="detail-content">{{ currentKnowledge.productionLoss }}</div>
+ </div>
+ <div class="detail-section">
+ <h4>鐜板満搴旀�ュ缃帾鏂�</h4>
+ <div class="detail-content">{{ currentKnowledge.handleMeasures }}</div>
+ </div>
+ <div class="detail-section">
+ <h4>澶囨敞</h4>
+ <div class="detail-content">{{ currentKnowledge.remark }}</div>
+ </div>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="viewDialogVisible = false">鍏抽棴</el-button>
+ <!-- <el-button type="success" @click="markAsFavorite">鏀惰棌@</el-button> -->
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ computed,
+ } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import { userListNoPage } from "@/api/system/user.js";
+ import useUserStore from "@/store/modules/user";
+ import {
+ safeAccidentListPage,
+ safeAccidentAdd,
+ safeAccidentUpdate,
+ safeAccidentDel,
+ } from "@/api/safeProduction/accidentReportingRecord.js";
+ import dayjs from "dayjs";
+ const userStore = useUserStore();
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ accidentCode: [
+ { required: true, message: "璇疯緭鍏ヤ簨鏁呯紪鍙�", trigger: "blur" },
+ ],
+ accidentName: [
+ { required: true, message: "璇疯緭鍏ヤ簨鏁呭悕绉�", trigger: "blur" },
+ ],
+ happenTime: [
+ { required: true, message: "璇烽�夋嫨浜嬫晠鍙戠敓鏃堕棿", trigger: "change" },
+ ],
+ happenLocation: [
+ { required: true, message: "璇疯緭鍏ヤ簨鏁呭彂鐢熶綅缃�", trigger: "blur" },
+ ],
+ accidentGrade: [
+ { required: true, message: "璇烽�夋嫨浜嬫晠绛夌骇", trigger: "change" },
+ ],
+ accidentType: [
+ { required: true, message: "璇烽�夋嫨浜嬫晠绫诲瀷", trigger: "change" },
+ ],
+ };
+
+ // 鍝嶅簲寮忔暟鎹�
+ const data = reactive({
+ searchForm: {
+ accidentName: "",
+ accidentType: "",
+ },
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+ selectedIds: [],
+ form: {
+ accidentCode: "", // 浜嬫晠缂栫爜
+ accidentName: "", // 浜嬫晠鍚嶇О
+ happenTime: "", // 浜嬫晠鍙戠敓鏃堕棿
+ happenLocation: "", // 浜嬫晠鍙戠敓鍦扮偣
+ accidentGrade: "", // 浜嬫晠绛夌骇
+ accidentType: "", // 浜嬫晠绫诲瀷
+ personLoss: "", // 浜哄憳浼や骸
+ assetLoss: "", // 璧勪骇鎹熷け
+ createUser: "", // 鍒涘缓鐢ㄦ埛
+ createTime: "", // 鍒涘缓鏃堕棿
+ createUserName: "", // 鍒涘缓鐢ㄦ埛鍚�
+ accidentCause: "", // 浜嬫晠鐩存帴鍘熷洜
+ rootCause: "", // 浜嬫晠鏍规湰鍘熷洜
+ productionLoss: "", // 鐢熶骇鎹熷け
+ handleMeasures: "", // 搴旀�ュ缃帾鏂�
+ remark: "", // 澶囨敞
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ viewDialogVisible: false,
+ currentKnowledge: {},
+ });
+
+ const {
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ selectedIds,
+ form,
+ dialogVisible,
+ dialogTitle,
+ dialogType,
+ viewDialogVisible,
+ currentKnowledge,
+ } = toRefs(data);
+
+ // 琛ㄥ崟寮曠敤
+ const formRef = ref();
+
+ // 琛ㄦ牸鍒楅厤缃�
+ const tableColumn = ref([
+ {
+ label: "浜嬫晠缂栫爜",
+ prop: "accidentCode",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "浜嬫晠鍚嶇О",
+ prop: "accidentName",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "浜嬫晠鍙戠敓鏃堕棿",
+ prop: "happenTime",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "浜嬫晠鍙戠敓浣嶇疆",
+ prop: "happenLocation",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "浜嬫晠绛夌骇",
+ prop: "accidentGrade",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "浜嬫晠绫诲瀷",
+ prop: "accidentType",
+ showOverflowTooltip: true,
+ formatData: params => {
+ return accidentTypeLabel(params);
+ },
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 200,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ },
+ {
+ name: "鏌ョ湅",
+ type: "text",
+ clickFun: row => {
+ viewKnowledge(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const userList = ref([]);
+ // 鐢熷懡鍛ㄦ湡
+ onMounted(() => {
+ getCurrentFactoryName();
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ getList();
+ startAutoRefresh();
+ });
+ const handleChange = userId => {
+ const selectedUser = userList.value.find(user => user.userId === userId);
+ if (selectedUser) {
+ form.value.coreResponsorUserName = selectedUser.nickName;
+ }
+ };
+ // 寮�濮嬭嚜鍔ㄥ埛鏂�
+ const startAutoRefresh = () => {
+ setInterval(() => {
+ getList();
+ }, 600000); // 10鍒嗛挓鍒锋柊涓�娆� (10 * 60 * 1000 = 600000ms)
+ };
+
+ // 鏌ヨ鏁版嵁
+ const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+ };
+ const accidentGradeType = val => {
+ switch (val) {
+ case "杞诲井浜嬫晠":
+ return "info";
+ case "涓�鑸簨鏁�":
+ return "info";
+ case "杈冨ぇ浜嬫晠":
+ return "warning";
+ case "閲嶅ぇ浜嬫晠":
+ return "danger";
+ default:
+ return "info";
+ }
+ };
+ const accidentGradeOptions = [
+ {
+ label: "杞诲井浜嬫晠",
+ value: "杞诲井浜嬫晠",
+ },
+ {
+ label: "涓�鑸簨鏁�",
+ value: "涓�鑸簨鏁�",
+ },
+ {
+ label: "杈冨ぇ浜嬫晠",
+ value: "杈冨ぇ浜嬫晠",
+ },
+ {
+ label: "閲嶅ぇ浜嬫晠",
+ value: "閲嶅ぇ浜嬫晠",
+ },
+ ];
+ const { proxy } = getCurrentInstance();
+ const { accident_type } = proxy.useDict("accident_type");
+ const accidentTypeOptions = computed(() => accident_type?.value || []);
+ const accidentTypeLabel = val => {
+ const item = accidentTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item ? item.label : val;
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ safeAccidentListPage({ ...page.value, ...searchForm.value })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.value.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鍒嗛〉澶勭悊
+ const pagination = obj => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ getList();
+ };
+ const currentUserId = ref("");
+ const currentUserName = ref("");
+ const getCurrentFactoryName = async () => {
+ let res = await userStore.getInfo();
+ currentUserId.value = res.user.userId;
+ currentUserName.value = res.user.nickName;
+ };
+ // 閫夋嫨鍙樺寲澶勭悊
+ const handleSelectionChange = selection => {
+ selectedIds.value = selection.map(item => item.id);
+ };
+
+ // 鎵撳紑琛ㄥ崟
+ const openForm = (type, row = null) => {
+ dialogType.value = type;
+ if (type === "add") {
+ dialogTitle.value = "鏂板浜嬫晠";
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form.value, {
+ accidentCode: "", // 浜嬫晠缂栫爜
+ accidentName: "", // 浜嬫晠鍚嶇О
+ happenTime: "", // 浜嬫晠鍙戠敓鏃堕棿
+ happenLocation: "", // 浜嬫晠鍙戠敓鍦扮偣
+ accidentGrade: "", // 浜嬫晠绛夌骇
+ accidentType: "", // 浜嬫晠绫诲瀷
+ personLoss: "", // 浜哄憳浼や骸
+ assetLoss: "", // 璧勪骇鎹熷け
+ createUser: currentUserId.value, // 鍒涘缓鐢ㄦ埛
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 鍒涘缓鏃堕棿
+ createUserName: currentUserName.value, // 鍒涘缓鐢ㄦ埛鍚�
+ accidentCause: "", // 浜嬫晠鐩存帴鍘熷洜
+ rootCause: "", // 浜嬫晠鏍规湰鍘熷洜
+ productionLoss: "", // 鐢熶骇鎹熷け
+ handleMeasures: "", // 搴旀�ュ缃帾鏂�
+ remark: "", // 澶囨敞
+ });
+ } else if (type === "edit" && row) {
+ dialogTitle.value = "缂栬緫浜嬫晠";
+ Object.assign(form.value, {
+ id: row.id,
+ accidentCode: row.accidentCode, // 浜嬫晠缂栫爜
+ accidentName: row.accidentName, // 浜嬫晠鍚嶇О
+ happenTime: row.happenTime, // 浜嬫晠鍙戠敓鏃堕棿
+ happenLocation: row.happenLocation, // 浜嬫晠鍙戠敓鍦扮偣
+ accidentGrade: row.accidentGrade, // 浜嬫晠绛夌骇
+ accidentType: row.accidentType, // 浜嬫晠绫诲瀷
+ personLoss: row.personLoss, // 浜哄憳浼や骸
+ assetLoss: row.assetLoss, // 璧勪骇鎹熷け
+ createUser: row.createUser, // 鍒涘缓鐢ㄦ埛
+ createTime: row.createTime, // 鍒涘缓鏃堕棿
+ createUserName: row.createUserName, // 鍒涘缓鐢ㄦ埛鍚�
+ accidentCause: row.accidentCause, // 浜嬫晠鐩存帴鍘熷洜
+ rootCause: row.rootCause, // 浜嬫晠鏍规湰鍘熷洜
+ productionLoss: row.productionLoss, // 鐢熶骇鎹熷け
+ handleMeasures: row.handleMeasures, // 搴旀�ュ缃帾鏂�
+ remark: row.remark, // 澶囨敞
+ });
+ }
+ dialogVisible.value = true;
+ };
+
+ // 鏌ョ湅浜嬫晠璇︽儏
+ const viewKnowledge = row => {
+ currentKnowledge.value = { ...row };
+ viewDialogVisible.value = true;
+ };
+ const getApplyScopeLabel = scope => {
+ const scopeMap = {
+ all: "鍏ㄤ綋鍛樺伐",
+ manager: "绠$悊灞�",
+ hr: "浜轰簨閮ㄩ棬",
+ finance: "璐㈠姟閮ㄩ棬",
+ tech: "鎶�鏈儴闂�",
+ };
+ return scopeMap[scope] || scope;
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩绫诲瀷
+ const getTypeTagType = type => {
+ const typeMap = {
+ contract: "success",
+ approval: "warning",
+ solution: "primary",
+ experience: "info",
+ guide: "danger",
+ };
+ return typeMap[type] || "info";
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩鏂囨湰
+ const getTypeLabel = type => {
+ return getKnowledgeTypeLabel(type);
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩绫诲瀷
+ const getEfficiencyTagType = efficiency => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info",
+ };
+ return typeMap[efficiency] || "info";
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩鏂囨湰
+ const getEfficiencyLabel = efficiency => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌",
+ };
+ return efficiencyMap[efficiency] || efficiency;
+ };
+
+ // 鑾峰彇鏁堢巼鎻愬崌鐧惧垎姣�
+ const getEfficiencyScore = efficiency => {
+ const scoreMap = {
+ high: 40,
+ medium: 25,
+ low: 15,
+ };
+ return scoreMap[efficiency] || 0;
+ };
+
+ // 鑾峰彇骞冲潎鑺傜渷鏃堕棿
+ const getTimeSaved = efficiency => {
+ const timeMap = {
+ high: "2-3澶�",
+ medium: "1-2澶�",
+ low: "0.5-1澶�",
+ };
+ return timeMap[efficiency] || "鏈煡";
+ };
+ // 鎻愪氦搴旀�ラ妗堣〃鍗�
+ const submitForm = async () => {
+ try {
+ await formRef.value.validate();
+ if (dialogType.value === "add") {
+ safeAccidentAdd({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("娣诲姞鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ } else {
+ safeAccidentUpdate({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+ };
+
+ // 鍒犻櫎搴旀�ラ妗�
+ const handleDelete = () => {
+ if (selectedIds.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑搴旀�ラ妗�");
+ return;
+ }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // console.log(selectedIds.value);
+ safeAccidentDel(selectedIds.value).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ selectedIds.value = [];
+ getList();
+ }
+ });
+ })
+ .catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+ };
+
+ // 瀛楀吀宸ュ叿
+ const knowledgeTypeOptions = computed(() => knowledge_type?.value || []);
+ const getKnowledgeTypeLabel = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item ? item.label : val;
+ };
+ const getKnowledgeTypeTagType = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item?.elTagType || "info";
+ };
+ const handleExport = () => {
+ proxy.download(
+ "/knowledgeBase/export",
+ { ...searchForm.value },
+ "搴旀�ラ妗堝簱.xlsx"
+ );
+ };
+</script>
+
+<style scoped>
+ .auto-refresh-info {
+ margin-bottom: 15px;
+ }
+
+ .auto-refresh-info .el-alert {
+ border-radius: 8px;
+ }
+
+ .dialog-footer {
+ text-align: right;
+ }
+
+ .knowledge-detail {
+ padding: 20px 0;
+ }
+
+ .detail-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+ }
+
+ .detail-section {
+ margin-top: 24px;
+ }
+
+ .detail-section h4 {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+ }
+
+ .detail-content {
+ background: #f8f9fa;
+ padding: 16px;
+ border-radius: 6px;
+ line-height: 1.6;
+ color: #606266;
+ white-space: pre-wrap;
+ }
+
+ .key-points {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .usage-stats {
+ margin-top: 16px;
+ }
+
+ .stat-item {
+ text-align: center;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ }
+
+ .stat-number {
+ font-size: 24px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 8px;
+ }
+
+ .stat-label {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .exec-steps-container {
+ border: 1px solid #e4e7ed;
+ border-radius: 4px;
+ padding: 15px;
+ background-color: #f9fafc;
+ }
+
+ .exec-step-item {
+ margin-bottom: 10px;
+ padding: 10px;
+ background-color: #ffffff;
+ border: 1px solid #e4e7ed;
+ border-radius: 4px;
+ }
+
+ .step-header {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .exec-step-view {
+ margin-bottom: 8px;
+ padding-left: 20px;
+ position: relative;
+ }
+
+ .step-number {
+ position: absolute;
+ left: 0;
+ font-weight: bold;
+ color: #409eff;
+ }
+
+ .step-title {
+ font-weight: bold;
+ margin-right: 5px;
+ }
+
+ .no-data {
+ color: #909399;
+ font-style: italic;
+ }
+</style>
diff --git a/src/views/safeProduction/dangerInvestigation/index.vue b/src/views/safeProduction/dangerInvestigation/index.vue
new file mode 100644
index 0000000..a278058
--- /dev/null
+++ b/src/views/safeProduction/dangerInvestigation/index.vue
@@ -0,0 +1,1181 @@
+<template>
+ <div class="app-container">
+ <!-- <div class="search_form">
+ <el-form :model="searchForm"
+ :inline="true">
+ <el-form-item label="闅愭偅缂栧彿锛�">
+ <el-input v-model="searchForm.hiddenCode"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery"> 鎼滅储 </el-button>
+ </el-form-item>
+ </el-form>
+ </div> -->
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">
+ 鏂板闅愭偅
+ </el-button>
+ <!-- <el-button type="primary"
+ plain
+ @click="handleImport">瀵煎叆</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button> -->
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ <!-- <el-button type="primary"
+ plain
+ @click="handlePrint">鎵撳嵃</el-button> -->
+ </div>
+ </div>
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys"
+ :row-key="(row) => row.id"
+ :row-class-name="getRowClass"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)">
+ <el-table-column align="center"
+ type="selection"
+ width="55"
+ fixed="left" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="闅愭偅缂栧彿"
+ prop="hiddenCode"
+ width="180"
+ show-overflow-tooltip />
+ <el-table-column label="闅愭偅鎻忚堪"
+ prop="hiddenDesc"
+ show-overflow-tooltip />
+ <el-table-column label="闅愭偅鍏蜂綋浣嶇疆"
+ prop="location"
+ show-overflow-tooltip />
+ <el-table-column label="涓婃姤浜�"
+ prop="createUserName"
+ width="180"
+ show-overflow-tooltip />
+ <el-table-column label="涓婃姤鏃堕棿"
+ prop="createTime"
+ show-overflow-tooltip />
+ <el-table-column label="鏁存敼瀹屾垚鏈熼檺"
+ prop="rectifyTime"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="鏁存敼璐d换浜�"
+ prop="rectifyUserName"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="鏁存敼璐d换浜鸿仈绯绘柟寮�"
+ prop="rectifyUserMobile"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="鏁存敼鍏蜂綋鎺柦"
+ prop="rectifyMeasures"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="瀹為檯鏁存敼瀹屾垚鏃堕棿"
+ prop="rectifyActualTime"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="楠屾敹鎰忚"
+ prop="verifyRemark"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="楠屾敹鏃堕棿"
+ prop="verifyTime"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column label="楠屾敹缁撴灉"
+ prop="verifyResult"
+ width="120"
+ show-overflow-tooltip>
+ <template #default="scope">
+ <el-tag v-if="scope.row.verifyResult"
+ :type="scope.row.verifyResult === '閫氳繃' ? 'success' : 'danger'">
+ {{ scope.row.verifyResult }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right"
+ label="鎿嶄綔"
+ min-width="150"
+ align="center">
+ <template #default="scope">
+ <!-- <el-button link
+ type="primary"
+ size="small"
+ @click="openForm('edit', scope.row)">缂栬緫</el-button> -->
+ <el-button link
+ type="primary"
+ size="small"
+ @click="downLoadFile(scope.row)">闄勪欢</el-button>
+ <el-button link
+ type="primary"
+ size="small"
+ :disabled="scope.row.isRectify || scope.row.rectifyActualTime"
+ @click="openForm('edit2', scope.row)">鏁存敼</el-button>
+ <el-button link
+ type="primary"
+ size="small"
+ :disabled="!scope.row.rectifyActualTime || scope.row.verifyTime"
+ @click="openForm('edit3', scope.row)">楠屾敹</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
+ </div>
+ <FormDialog v-model="dialogFormVisible"
+ :title="getTitle(operationType)"
+ :width="'70%'"
+ :operation-type="operationType"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia">
+ <el-form :model="form"
+ v-if="operationType === 'add' || operationType === 'edit'"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="闅愭偅缂栧彿锛�"
+ prop="hiddenCode">
+ <el-input v-model="form.hiddenCode"
+ placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�"
+ disabled
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="闅愭偅鍏蜂綋浣嶇疆锛�"
+ prop="location">
+ <el-input v-model="form.location"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="闅愭偅鎻忚堪锛�"
+ prop="hiddenDesc">
+ <el-input v-model="form.hiddenDesc"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="闅愭偅绫诲瀷锛�"
+ prop="type">
+ <el-select v-model="form.type"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in typeList"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="闅愭偅椋庨櫓绛夌骇锛�"
+ prop="riskLevel">
+ <el-select v-model="form.riskLevel"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in riskLevelList"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="涓婃姤浜猴細"
+ prop="createUser">
+ <el-select v-model="form.createUser"
+ placeholder="璇烽�夋嫨"
+ disabled
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓婃姤鏃堕棿锛�"
+ prop="createTime">
+ <el-date-picker style="width: 100%"
+ disabled
+ v-model="form.createTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鏁存敼璐d换浜猴細"
+ prop="rectifyUserId">
+ <el-select v-model="form.rectifyUserId"
+ placeholder="璇烽�夋嫨"
+ @change="handleChange2"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁存敼瀹屾垚鏈熼檺"
+ prop="rectifyTime">
+ <el-date-picker style="width: 100%"
+ v-model="form.rectifyTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <div v-if="operationType === 'edit2' || operationType === 'edit3'"
+ class="classtitle">闅愭偅璇︽儏</div>
+ <el-descriptions :column="2"
+ style="margin-bottom: 20px;"
+ v-if="operationType === 'edit2' || operationType === 'edit3'"
+ border>
+ <el-descriptions-item label="闅愭偅缂栧彿">
+ <span class="detail-title">{{ form.hiddenCode }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="闅愭偅鍏蜂綋浣嶇疆">
+ <span class="detail-title">{{ form.location }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item :span="2"
+ label="闅愭偅鎻忚堪">
+ <span class="detail-title">{{ form.hiddenDesc }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="闅愭偅绫诲瀷">
+ <span class="detail-title">{{ TypeLabel(form.type) }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="闅愭偅椋庨櫓绛夌骇">
+ <el-tag :type="getTypeTagType(form.riskLevel)">
+ {{ form.riskLevel }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="涓婃姤浜�">
+ <span class="detail-title">{{ form.createUserName }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="涓婃姤鏃堕棿">
+ <span class="detail-title">{{ form.createTime }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏁存敼璐d换浜�">
+ <span class="detail-title">{{ form.rectifyUserName }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏁存敼璐d换浜鸿仈绯绘柟寮�">
+ <span class="detail-title">{{ form.rectifyUserMobile }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鏁存敼瀹屾垚鏈熼檺">
+ <span class="detail-title">{{ form.rectifyTime }}</span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div class="classtitle"
+ v-if="operationType === 'edit3'"
+ style="margin-top: 40px;">鏁存敼璇︽儏</div>
+ <el-descriptions :column="2"
+ style="margin-bottom: 20px;"
+ v-if="operationType === 'edit3'"
+ border>
+ <el-descriptions-item label="鏁存敼鍏蜂綋鎺柦"
+ :span="2">
+ <span class="detail-title">{{ form2.rectifyMeasures }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹為檯鏁存敼瀹屾垚鏃堕棿">
+ <span class="detail-title">{{ form2.rectifyActualTime }}</span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div class="classtitle"
+ v-if="operationType === 'edit2' || operationType === 'edit3'"
+ style="margin-top: 40px;margin-bottom: 30px;">楠屾敹鎯呭喌</div>
+ <el-form :model="form2"
+ v-if="operationType === 'edit2'"
+ label-width="140px"
+ label-position="top"
+ :rules="rules2"
+ ref="formRef2">
+ <el-form-item label="鏁存敼鍏蜂綋鎺柦锛�"
+ prop="rectifyMeasures">
+ <el-input v-model="form2.rectifyMeasures"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏ユ暣鏀瑰叿浣撴帾鏂�"
+ clearable />
+ </el-form-item>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瀹為檯鏁存敼瀹屾垚鏃堕棿锛�"
+ prop="rectifyActualTime">
+ <el-date-picker style="width: 100%"
+ v-model="form2.rectifyActualTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <el-form :model="form3"
+ v-if="operationType === 'edit3'"
+ label-width="140px"
+ label-position="top"
+ :rules="rules3"
+ ref="formRef3">
+ <el-form-item label="楠屾敹鎰忚锛�"
+ prop="verifyRemark">
+ <el-input v-model="form3.verifyRemark"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏ラ獙鏀舵剰瑙�"
+ clearable />
+ </el-form-item>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="楠屾敹鏃堕棿锛�"
+ prop="verifyTime">
+ <el-date-picker style="width: 100%"
+ disabled
+ v-model="form3.verifyTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="楠屾敹缁撴灉锛�"
+ prop="verifyResult">
+ <el-select v-model="form3.verifyResult"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option label="閫氳繃"
+ value="閫氳繃" />
+ <el-option label="涓嶉�氳繃"
+ value="涓嶉�氳繃" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="楠屾敹浜猴細"
+ prop="verifyUserId">
+ <el-select v-model="form3.verifyUserId"
+ disabled
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+ <FileListDialog
+ v-if="fileListDialogVisible"
+ :record-id="currentRecordId"
+ record-type="safe_hidden"
+ v-model:visible="fileListDialogVisible"/>
+
+ </div>
+</template>
+
+<script setup>
+ import { getToken } from "@/utils/auth";
+ import pagination from "@/components/PIMTable/Pagination.vue";
+ import { onMounted, ref, getCurrentInstance, computed } from "vue";
+ import { ElMessageBox, ElMessage } from "element-plus";
+ import useUserStore from "@/store/modules/user";
+ import { userListNoPage } from "@/api/system/user.js";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
+ import {
+ dangerInvestigationListPage,
+ safeHiddenAdd,
+ safeHiddenUpdate,
+ safeHiddenDel,
+ fileListPage,
+ } from "@/api/safeProduction/dangerInvestigation.js";
+ import useFormData from "@/hooks/useFormData.js";
+ import dayjs from "dayjs";
+
+ const userStore = useUserStore();
+ const { proxy } = getCurrentInstance();
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const userList = ref([]);
+ const tableLoading = ref(false);
+ const currentRecordId = ref(0);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ });
+ const total = ref(0);
+ const getTitle = type => {
+ if (type === "add") {
+ return "鏂板闅愭偅";
+ } else if (type === "edit") {
+ return "淇敼闅愭偅";
+ } else if (type === "edit2") {
+ return "鏁存敼椤甸潰";
+ } else if (type === "edit3") {
+ return "楠屾敹椤甸潰";
+ }
+ };
+ // 鑾峰彇绫诲瀷鏍囩绫诲瀷
+ const getTypeTagType = type => {
+ const typeMap = {
+ 杈冨ぇ椋庨櫓: "warning",
+ 浣庨闄�: "info",
+ 涓�鑸闄�: "info",
+ 閲嶅ぇ椋庨櫓: "danger",
+ };
+ return typeMap[type] || "info";
+ };
+ // 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+ const operationType = ref("");
+ const dialogFormVisible = ref(false);
+ const data = reactive({
+ searchForm: {
+ hiddenCode: "", // 闅愭偅缂栧彿
+ },
+ form: {
+ hiddenCode: "", // 闅愭偅缂栧彿
+ location: "", // 闅愭偅浣嶇疆
+ hiddenDesc: "", // 闅愭偅鎻忚堪
+ createUser: "", // 涓婃姤浜�
+ createUserName: "",
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 涓婃姤鏃堕棿
+ rectifyUserId: "", // 鏁存敼璐d换浜�
+ rectifyUserName: "",
+ rectifyTime: "", // 鏁存敼瀹屾垚鏈熼檺
+ rectifyUserMobile: "", // 鏁存敼璐d换浜烘墜鏈哄彿
+ riskLevel: "", // 闅愭偅椋庨櫓绛夌骇
+ type: "", // 闅愭偅绫诲瀷
+ },
+
+ rules: {
+ location: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ hiddenDesc: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ riskLevel: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ type: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ createUser: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ rectifyUserId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ rectifyTime: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+ });
+ const rules2 = {
+ rectifyActualTime: [{ required: true, message: "璇烽�夋嫨", trigger: "blur" }],
+ rectifyMeasures: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ };
+ const rules3 = {
+ verifyTime: [{ required: true, message: "璇烽�夋嫨", trigger: "blur" }],
+ verifyRemark: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ verifyResult: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ acceptDesc: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ };
+ const { hidden_danger_type } = proxy.useDict("hidden_danger_type");
+ const typeList = computed(() => hidden_danger_type?.value || []);
+ const TypeLabel = val => {
+ const item = typeList.value.find(i => String(i.value) === String(val));
+ return item ? item.label : val;
+ };
+ const form2 = ref({
+ rectifyActualTime: "", // 瀹為檯鏁存敼瀹屾垚鏃堕棿
+ rectifyMeasures: "", // 鏁存敼鍏蜂綋鎺柦
+ });
+ const form3 = ref({
+ verifyTime: "", // 楠屾敹鏃堕棿
+ verifyRemark: "", // 楠屾敹澶囨敞
+ acceptDesc: "", // 楠屾敹鎻忚堪
+ verifyUserId: "", // 楠屾敹浜�
+ verifyUserName: "",
+ verifyResult: "", // 楠屾敹缁撴灉
+ });
+ const riskLevelList = ref([
+ {
+ value: "閲嶅ぇ椋庨櫓",
+ label: "閲嶅ぇ椋庨櫓",
+ },
+ {
+ value: "杈冨ぇ椋庨櫓",
+ label: "杈冨ぇ椋庨櫓",
+ },
+ {
+ value: "涓�鑸闄�",
+ label: "涓�鑸闄�",
+ },
+ {
+ value: "浣庨闄�",
+ label: "浣庨闄�",
+ },
+ ]);
+ const { form, rules } = toRefs(data);
+ const { form: searchForm } = useFormData(data.searchForm);
+ // 浜у搧琛ㄥ崟寮规鏁版嵁
+ const productFormVisible = ref(false);
+
+ const quotationLoading = ref(false);
+ const quotationList = ref([]);
+ const quotationSearchForm = reactive({
+ quotationNo: "",
+ customer: "",
+ });
+
+ const handleChange2 = userId => {
+ const selectedUser = userList.value.find(user => user.userId === userId);
+ if (selectedUser) {
+ form.value.rectifyUserName = selectedUser.nickName;
+ form.value.rectifyUserMobile = selectedUser.phonenumber;
+ }
+ };
+
+ // 瀵煎叆鐩稿叧
+ const importUploadRef = ref(null);
+ const importUpload = reactive({
+ title: "瀵煎叆閿�鍞彴璐�",
+ open: false,
+ url: import.meta.env.VITE_APP_BASE_API + "/sales/ledger/import",
+ headers: { Authorization: "Bearer " + getToken() },
+ isUploading: false,
+ beforeUpload: file => {
+ const isExcel = file.name.endsWith(".xlsx") || file.name.endsWith(".xls");
+ const isLt10M = file.size / 1024 / 1024 < 10;
+ if (!isExcel) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢鍙兘鏄� xlsx/xls 鏍煎紡!");
+ return false;
+ }
+ if (!isLt10M) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!");
+ return false;
+ }
+ return true;
+ },
+ onChange: (file, fileList) => {
+ console.log("鏂囦欢鐘舵�佹敼鍙�", file, fileList);
+ },
+ onProgress: (event, file, fileList) => {
+ console.log("涓婁紶涓�...", event.percent);
+ },
+ onSuccess: (response, file, fileList) => {
+ console.log("涓婁紶鎴愬姛", response, file, fileList);
+ importUpload.isUploading = false;
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess("瀵煎叆鎴愬姛");
+ importUpload.open = false;
+ if (importUploadRef.value) {
+ importUploadRef.value.clearFiles();
+ }
+ getList();
+ } else {
+ proxy.$modal.msgError(response.msg || "瀵煎叆澶辫触");
+ }
+ },
+ onError: (error, file, fileList) => {
+ console.error("涓婁紶澶辫触", error, file, fileList);
+ importUpload.isUploading = false;
+ proxy.$modal.msgError("瀵煎叆澶辫触锛岃閲嶈瘯");
+ },
+ });
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ // 鍙湁鍦ㄧ偣鍑绘悳绱㈡寜閽椂鎵嶉噸缃〉鐮佸埌绗竴椤�
+ // 閬垮厤琛ㄥ崟瀛楁change浜嬩欢骞叉壈鍒嗛〉
+ if (arguments.length === 0) {
+ page.current = 1;
+ }
+ expandedRowKeys.value = [];
+ getList();
+ };
+ const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const { entryDate, ...rest } = searchForm;
+ // 灏嗚寖鍥存棩鏈熷瓧娈典紶閫掔粰鍚庣
+ const params = { ...rest, ...page };
+ // 绉婚櫎褰曞叆鏃ユ湡鐨勯粯璁ゅ�艰缃紝鍙繚鐣欒寖鍥存棩鏈熷瓧娈�
+ delete params.entryDate;
+ return dangerInvestigationListPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ total.value = res.data.total;
+ tableData.value.forEach(item => {
+ // console.log(item.rectifyUserId, currentUserId.value, "=======");
+ if (Number(item.rectifyUserId) != Number(currentUserId.value)) {
+ item.isRectify = true;
+ } else {
+ item.isRectify = false;
+ }
+ });
+ return res;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥炶鑺傜偣
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId);
+ if (foundNode) {
+ return foundNode; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝杩斿洖璇ヨ妭鐐�
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ console.log("selection", selectedRows.value);
+ };
+ const expandedRowKeys = ref([]);
+ // 鎵撳紑寮规
+ const openForm = async (type, row) => {
+ console.log("row", row);
+ operationType.value = type;
+ form3.value = {
+ verifyTime: "", // 楠屾敹鏃堕棿
+ verifyRemark: "", // 楠屾敹澶囨敞
+ verifyResult: "", // 楠屾敹鎻忚堪
+ verifyUserId: "", // 楠屾敹浜�
+ };
+ form2.value = {
+ rectifyActualTime: "", // 瀹為檯鏁存敼瀹屾垚鏃堕棿
+ rectifyMeasures: "", // 鏁存敼鍏蜂綋鎺柦
+ };
+ if (type === "add") {
+ form.value = {
+ hiddenCode: "", // 闅愭偅缂栧彿
+ location: "", // 闅愭偅浣嶇疆
+ hiddenDesc: "", // 闅愭偅鎻忚堪
+ createUser: Number(currentUserId.value), // 涓婃姤浜�
+ createUserName: currentUserName.value,
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 涓婃姤鏃堕棿
+ rectifyUserId: "", // 鏁存敼璐d换浜�
+ rectifyUserName: "",
+ rectifyTime: "", // 鏁存敼瀹屾垚鏈熼檺
+ rectifyUserMobile: "", // 鏁存敼璐d换浜烘墜鏈哄彿
+ riskLevel: "", // 闅愭偅椋庨櫓绛夌骇
+ type: "", // 闅愭偅绫诲瀷
+ };
+ } else if (type === "edit") {
+ form.value = {
+ id: row.id,
+ hiddenCode: row.hiddenCode, // 闅愭偅缂栧彿
+ location: row.location, // 闅愭偅浣嶇疆
+ hiddenDesc: row.hiddenDesc, // 闅愭偅鎻忚堪
+ createUser: row.createUser, // 涓婃姤浜�
+ createUserName: row.createUserName,
+ createTime: row.createTime, // 涓婃姤鏃堕棿
+ rectifyUserId: row.rectifyUserId, // 鏁存敼璐d换浜�
+ rectifyUserName: row.rectifyUserName,
+ rectifyTime: row.rectifyTime, // 鏁存敼瀹屾垚鏈熼檺
+ rectifyUserMobile: row.rectifyUserMobile, // 鏁存敼璐d换浜烘墜鏈哄彿
+ riskLevel: row.riskLevel, // 闅愭偅椋庨櫓绛夌骇
+ type: row.type, // 闅愭偅绫诲瀷
+ };
+ } else if (type === "edit2") {
+ form.value = {
+ id: row.id,
+ hiddenCode: row.hiddenCode, // 闅愭偅缂栧彿
+ location: row.location, // 闅愭偅浣嶇疆
+ hiddenDesc: row.hiddenDesc, // 闅愭偅鎻忚堪
+ createUser: row.createUser, // 涓婃姤浜�
+ createUserName: row.createUserName,
+ createTime: row.createTime, // 涓婃姤鏃堕棿
+ rectifyUserId: row.rectifyUserId, // 鏁存敼璐d换浜�
+ rectifyUserName: row.rectifyUserName,
+ rectifyTime: row.rectifyTime, // 鏁存敼瀹屾垚鏈熼檺
+ rectifyUserMobile: row.rectifyUserMobile, // 鏁存敼璐d换浜烘墜鏈哄彿
+ riskLevel: row.riskLevel, // 闅愭偅椋庨櫓绛夌骇
+ type: row.type, // 闅愭偅绫诲瀷
+ };
+ form2.value = {
+ rectifyActualTime: row.rectifyActualTime, // 瀹為檯鏁存敼瀹屾垚鏃堕棿
+ rectifyMeasures: row.rectifyMeasures, // 鏁存敼鍏蜂綋鎺柦
+ };
+ } else if (type === "edit3") {
+ form.value = {
+ id: row.id,
+ hiddenCode: row.hiddenCode, // 闅愭偅缂栧彿
+ location: row.location, // 闅愭偅浣嶇疆
+ hiddenDesc: row.hiddenDesc, // 闅愭偅鎻忚堪
+ createUser: row.createUser, // 涓婃姤浜�
+ createUserName: row.createUserName,
+ createTime: row.createTime, // 涓婃姤鏃堕棿
+ rectifyUserId: row.rectifyUserId, // 鏁存敼璐d换浜�
+ rectifyUserName: row.rectifyUserName,
+ rectifyTime: row.rectifyTime, // 鏁存敼瀹屾垚鏈熼檺
+ rectifyUserMobile: row.rectifyUserMobile, // 鏁存敼璐d换浜烘墜鏈哄彿
+ riskLevel: row.riskLevel, // 闅愭偅椋庨櫓绛夌骇
+ type: row.type, // 闅愭偅绫诲瀷
+ };
+ form2.value = {
+ rectifyActualTime: row.rectifyActualTime, // 瀹為檯鏁存敼瀹屾垚鏃堕棿
+ rectifyMeasures: row.rectifyMeasures, // 鏁存敼鍏蜂綋鎺柦
+ };
+ form3.value = {
+ verifyTime: row.verifyTime, // 楠屾敹鏃堕棿
+ verifyRemark: row.verifyRemark, // 楠屾敹澶囨敞
+ verifyResult: row.verifyResult, // 楠屾敹鎻忚堪
+ verifyUserId: row.verifyUserId, // 楠屾敹浜�
+ };
+ console.log("form3.value", form3.value);
+ if (!form3.value.verifyUserId || form3.value.verifyUserId === "null") {
+ form3.value.verifyUserId = Number(currentUserId.value); // 楠屾敹浜�
+ }
+ if (!form3.value.verifyTime || form3.value.verifyTime === "null") {
+ form3.value.verifyTime = dayjs().format("YYYY-MM-DD"); // 楠屾敹鎻忚堪
+ }
+ }
+ dialogFormVisible.value = true;
+ };
+ const getCurrentUserInfo = () => {
+ getInfo;
+ };
+ const fetchQuotationList = async () => {
+ quotationLoading.value = true;
+ try {
+ const params = {
+ // 鍏煎鍚庣鍒嗛〉瀛楁锛氳繖閲屾部鐢ㄦ姤浠烽〉闈㈠凡鏈夊彲鐢ㄧ殑瀛楁鍛藉悕
+ currentPage: 1,
+ pageSize: 100,
+ ...quotationSearchForm,
+ status: "閫氳繃",
+ };
+ const res = await getQuotationList(params);
+ quotationList.value = res?.data?.records || [];
+ } finally {
+ quotationLoading.value = false;
+ }
+ };
+
+ // 鎻愪氦琛ㄥ崟
+ const submitForm = () => {
+ console.log("operationType.value", operationType.value);
+
+ if (operationType.value == "add") {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ safeHiddenAdd(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ });
+ } else if (operationType.value == "edit") {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ safeHiddenUpdate(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ });
+ } else if (operationType.value == "edit2") {
+ console.log("form2.value", form2.value);
+ proxy.$refs["formRef2"].validate(valid => {
+ if (valid) {
+ safeHiddenUpdate({ ...form2.value, ...form.value }).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ });
+ } else if (operationType.value == "edit3") {
+ proxy.$refs["formRef3"].validate(valid => {
+ if (valid) {
+ safeHiddenUpdate({
+ ...form3.value,
+ ...form2.value,
+ ...form.value,
+ }).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ });
+ }
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ proxy.resetForm("formRef2");
+ dialogFormVisible.value = false;
+ };
+ // 鍏抽棴浜у搧寮规
+ const closeProductDia = () => {
+ proxy.resetForm("productFormRef");
+ productFormVisible.value = false;
+ };
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ safeHiddenDel(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ const isPeople = rectifyUserId => {
+ return Number(rectifyUserId) == Number(currentUserId.value);
+ };
+
+ /**
+ * 鍒ゆ柇鏄惁鍙互鍙戣揣
+ * 鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐у拰瀹℃牳鎷掔粷鐨勬椂鍊欐墠鍙互鍙戣揣
+ * @param row 琛屾暟鎹�
+ */
+ const canShip = row => {
+ // 浜у搧鐘舵�佸繀椤绘槸鍏呰冻锛坅pproveStatus === 1锛�
+ if (row.approveStatus !== 1) {
+ return false;
+ }
+
+ // 鑾峰彇鍙戣揣鐘舵��
+ const shippingStatus = row.shippingStatus;
+
+ // 濡傛灉宸插彂璐э紙鏈夊彂璐ф棩鏈熸垨杞︾墝鍙凤級锛屼笉鑳藉啀娆″彂璐�
+ if (row.shippingDate || row.shippingCarNumber) {
+ return false;
+ }
+
+ // 鍙戣揣鐘舵�佸繀椤绘槸"寰呭彂璐�"鎴�"瀹℃牳鎷掔粷"
+ const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
+ return statusStr === "寰呭彂璐�" || statusStr === "瀹℃牳鎷掔粷";
+ };
+ const filePagination = ref({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ /**
+ * 涓嬭浇鏂囦欢
+ *
+ * @param row 涓嬭浇鏂囦欢鐨勭浉鍏充俊鎭璞�
+ */
+ const fileListRef = ref(null);
+ const fileListDialogVisible = ref(false);
+ const currentFileRow = ref(null);
+ const downLoadFile = row => {
+ currentRecordId.value = row.id;
+ fileListDialogVisible.value = true;
+ };
+ const currentUserId = ref("");
+ const currentUserName = ref("");
+ const getCurrentFactoryName = async () => {
+ let res = await userStore.getInfo();
+ currentUserId.value = res.user.userId;
+ currentUserName.value = res.user.nickName;
+ };
+
+ /**
+ * 鑾峰彇琛岀被鍚嶏紝鐢ㄤ簬鍒ゆ柇鏄惁杩囨湡鏈暣鏀�
+ * @param row 琛屾暟鎹�
+ * @returns 绫诲悕
+ */
+ const getRowClass = ({ row }) => {
+ const now = new Date();
+
+ // 妫�鏌ユ槸鍚﹁秴杩囨暣鏀规湡闄愪笖鏈疄闄呮暣鏀�
+ if (row.rectifyTime && !row.rectifyActualTime) {
+ const rectifyTime = new Date(row.rectifyTime);
+ if (now > rectifyTime) {
+ return "overdue-row";
+ }
+ }
+
+ return "";
+ };
+
+ onMounted(() => {
+ getCurrentFactoryName();
+ getList();
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ });
+</script>
+
+<style scoped lang="scss">
+ .ml-10 {
+ margin-left: 10px;
+ }
+
+ .table_list {
+ margin-top: unset;
+ }
+
+ :deep(.warning-row) {
+ background-color: #fef0f0 !important;
+ }
+
+ :deep(.warning-row td) {
+ // color: #cf1322 !important;
+ }
+
+ :deep(.overdue-row) {
+ background-color: #ffffff !important;
+ }
+
+ :deep(.overdue-row td) {
+ color: #e1707a !important;
+ }
+
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+ }
+ .print-preview-dialog {
+ .el-dialog__body {
+ padding: 0;
+ max-height: 80vh;
+ overflow-y: auto;
+ }
+ }
+
+ .print-preview-container {
+ .print-preview-header {
+ padding: 15px;
+ border-bottom: 1px solid #e4e7ed;
+ text-align: center;
+
+ .el-button {
+ margin: 0 10px;
+ }
+ }
+
+ .print-preview-content {
+ padding: 20px;
+ background-color: #f5f5f5;
+ min-height: 400px;
+ }
+ }
+
+ .print-page {
+ width: 220mm;
+ height: 90mm;
+ padding: 10mm;
+ margin: 0 auto;
+ background: white;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ margin-bottom: 10px;
+ box-sizing: border-box;
+ }
+
+ .delivery-note {
+ width: 100%;
+ height: 100%;
+ font-family: "SimSun", serif;
+ font-size: 10px;
+ line-height: 1.2;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .header {
+ text-align: center;
+ margin-bottom: 8px;
+
+ .company-name {
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+
+ .document-title {
+ font-size: 16px;
+ font-weight: bold;
+ }
+ }
+
+ .info-section {
+ margin-bottom: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .info-row {
+ line-height: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 60px;
+ font-size: 14px;
+ }
+
+ .value {
+ margin-right: 20px;
+ min-width: 80px;
+ font-size: 14px;
+ }
+ }
+ }
+
+ .table-section {
+ margin-bottom: 4px;
+ flex: 1;
+
+ .product-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid #000;
+
+ th,
+ td {
+ border: 1px solid #000;
+ padding: 6px;
+ text-align: center;
+ font-size: 14px;
+ line-height: 1.4;
+ }
+
+ th {
+ font-weight: bold;
+ }
+
+ .total-label {
+ text-align: right;
+ font-weight: bold;
+ }
+
+ .total-value {
+ font-weight: bold;
+ }
+ }
+ }
+
+ .footer-section {
+ .footer-row {
+ display: flex;
+ margin-bottom: 3px;
+ line-height: 20px;
+ justify-content: space-between;
+
+ .footer-item {
+ display: flex;
+ margin-right: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 80px;
+ font-size: 14px;
+ }
+
+ .value {
+ min-width: 80px;
+ font-size: 14px;
+ }
+
+ &.address-item {
+ .address-value {
+ min-width: 200px;
+ }
+ }
+ }
+ }
+ }
+
+ @media print {
+ .app-container {
+ display: none;
+ }
+
+ .print-page {
+ box-shadow: none;
+ margin: 0;
+ padding: 10mm;
+ padding-left: 20mm;
+ page-break-inside: avoid;
+ page-break-after: always;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+ }
+ .classtitle {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+ margin-bottom: 12px;
+ }
+</style>
diff --git a/src/views/safeProduction/emergencyPlanReview/index.vue b/src/views/safeProduction/emergencyPlanReview/index.vue
new file mode 100644
index 0000000..1850c18
--- /dev/null
+++ b/src/views/safeProduction/emergencyPlanReview/index.vue
@@ -0,0 +1,847 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">搴旀�ラ妗堝悕绉帮細</span>
+ <el-input v-model="searchForm.planName"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ簲鎬ラ妗堝悕绉版悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span class="search_title ml10">搴旀�ラ妗堢被鍨嬶細</span>
+ <el-select v-model="searchForm.planType"
+ clearable
+ @change="handleQuery"
+ style="width: 240px">
+ <el-option v-for="item in emergencyPlanTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板搴旀�ラ妗�</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <!-- 鏂板/缂栬緫搴旀�ラ妗堝脊绐� -->
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="800px"
+ :close-on-click-modal="false">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="150px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="搴旀�ラ妗堢紪鐮侊細"
+ prop="planCode">
+ <el-input v-model="form.planCode"
+ placeholder="璇疯緭鍏ュ簲鎬ラ妗堢紪鐮�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="搴旀�ラ妗堝悕绉帮細"
+ prop="planName">
+ <el-input v-model="form.planName"
+ placeholder="璇疯緭鍏ュ簲鎬ラ妗堝悕绉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戝竷鐢熸晥鏃堕棿锛�"
+ prop="publishTime">
+ <el-date-picker style="width: 100%"
+ v-model="form.publishTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏍稿績璐d换浜猴細"
+ prop="coreResponsorUserId">
+ <el-select v-model="form.coreResponsorUserId"
+ placeholder="璇烽�夋嫨"
+ @change="handleChange"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="棰勬绫诲瀷锛�"
+ prop="planType">
+ <el-select v-model="form.planType"
+ placeholder="璇烽�夋嫨棰勬绫诲瀷"
+ style="width: 100%">
+ <el-option v-for="item in emergencyPlanTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞锛�"
+ prop="remark">
+ <el-input v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="閫傜敤鑼冨洿锛�"
+ prop="applyScope">
+ <el-checkbox-group v-model="form.applyScope">
+ <el-checkbox label="all">鍏ㄤ綋鍛樺伐</el-checkbox>
+ <el-checkbox label="manager">绠$悊灞�</el-checkbox>
+ <el-checkbox label="hr">浜轰簨閮ㄩ棬</el-checkbox>
+ <el-checkbox label="finance">璐㈠姟閮ㄩ棬</el-checkbox>
+ <el-checkbox label="tech">鎶�鏈儴闂�</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ <el-form-item label="搴旀�ュ缃楠わ細"
+ prop="execSteps">
+ <div class="exec-steps-container"
+ style="width:100%">
+ <div v-for="(step, index) in execStepsList"
+ :key="index"
+ class="exec-step-item">
+ <div class="step-header">
+ <div>
+ <el-input v-model="step.step"
+ placeholder="姝ラ"
+ style="width: 200px; margin-right: 10px" />
+ <el-button type="danger"
+ size="small"
+ @click="removeExecStep(index)"
+ style="margin-left: 10px">鍒犻櫎</el-button>
+ </div>
+ <div style="margin-top: 5px;width: 100%;">
+ <el-input v-model="step.description"
+ placeholder="鎺柦"
+ type="textarea"
+ :rows="2"
+ style="flex: 1" />
+ </div>
+ </div>
+ </div>
+ <el-button type="primary"
+ size="small"
+ @click="addExecStep"
+ style="margin-top: 10px">娣诲姞姝ラ</el-button>
+ </div>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鏌ョ湅搴旀�ラ妗堣鎯呭脊绐� -->
+ <el-dialog v-model="viewDialogVisible"
+ title="搴旀�ラ妗堣鎯�"
+ width="900px"
+ :close-on-click-modal="false">
+ <div class="knowledge-detail">
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="搴旀�ラ妗堝悕绉�"
+ :span="2">
+ <span class="detail-title">{{ currentKnowledge.planName }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="搴旀�ラ妗堢紪鐮�">
+ <span>{{ currentKnowledge.planCode }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙戝竷鐢熸晥鏃堕棿">
+ {{ currentKnowledge.publishTime }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鏍稿績璐d换浜�">
+ {{ currentKnowledge.coreResponsorUserName }}
+ </el-descriptions-item>
+ <el-descriptions-item label="棰勬绫诲瀷">
+ <el-tag type="warning"> {{ emergencyPlanTypeLabel(currentKnowledge.planType) }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="澶囨敞">
+ {{ currentKnowledge.remark }}
+ </el-descriptions-item>
+ </el-descriptions>
+ <div class="detail-section">
+ <h4>閫傜敤鑼冨洿</h4>
+ <div class="key-points">
+ <el-tag v-for="(point, index) in currentKnowledge.applyScope.split(',')"
+ :key="index"
+ type="primary"
+ style="margin-right: 8px; margin-bottom: 8px;">
+ {{ getApplyScopeLabel(point.trim()) }}
+ </el-tag>
+ </div>
+ </div>
+ <div class="detail-section">
+ <h4>搴旀�ュ缃楠�</h4>
+ <div class="detail-content">
+ <div v-if="currentKnowledge.execSteps">
+ <div v-for="(step, index) in JSON.parse(currentKnowledge.execSteps)"
+ :key="index"
+ class="exec-step-view">
+ <!-- <span class="step-number">{{ index + 1 }}.</span> -->
+ <span class="step-title">{{ step.step }}锛�</span>
+ <span>{{ step.description }}</span>
+ </div>
+ </div>
+ <div v-else
+ class="no-data">鏃犲簲鎬ュ缃楠�</div>
+ </div>
+ </div>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="viewDialogVisible = false">鍏抽棴</el-button>
+ <!-- <el-button type="success" @click="markAsFavorite">鏀惰棌@</el-button> -->
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ computed,
+ } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import { userListNoPage } from "@/api/system/user.js";
+ import {
+ safeContingencyPlanListPage,
+ safeContingencyPlanAdd,
+ safeContingencyPlanUpdate,
+ safeContingencyPlanDel,
+ } from "@/api/safeProduction/emergencyPlanReview.js";
+
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ planCode: [
+ { required: true, message: "璇疯緭鍏ュ簲鎬ラ妗堢紪鐮�", trigger: "blur" },
+ ],
+ applyScope: [
+ { required: true, message: "璇烽�夋嫨閫傜敤鑼冨洿", trigger: "change" },
+ ],
+ planType: [{ required: true, message: "璇烽�夋嫨棰勬绫诲瀷", trigger: "change" }],
+ planName: [
+ { required: true, message: "璇疯緭鍏ュ簲鎬ラ妗堝悕绉�", trigger: "blur" },
+ ],
+ publishTime: [
+ { required: true, message: "璇烽�夋嫨鍙戝竷鐢熸晥鏃堕棿", trigger: "change" },
+ ],
+ coreResponsorUserId: [
+ { required: true, message: "璇烽�夋嫨鏍稿績璐d换浜�", trigger: "change" },
+ ],
+ };
+
+ // 鍝嶅簲寮忔暟鎹�
+ const data = reactive({
+ searchForm: {
+ planName: "",
+ planType: "",
+ },
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+ selectedIds: [],
+ form: {
+ planCode: "", // 棰勬缂栧彿
+ applyScope: [], // 閫傜敤鑼冨洿
+ planType: "", // 棰勬绫诲瀷
+ planName: "", // 棰勬鍚嶇О
+ publishTime: "", // 鍙戝竷鏃堕棿
+ coreResponsorUserId: "", // 鏍稿績璐熻矗浜虹敤鎴稩D
+ coreResponsorUserName: "", // 鏍稿績璐熻矗浜虹敤鎴峰悕
+ remark: "", // 澶囨敞
+ execSteps: "", // 鎵ц姝ラ
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ viewDialogVisible: false,
+ currentKnowledge: {},
+ });
+
+ const {
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ selectedIds,
+ form,
+ dialogVisible,
+ dialogTitle,
+ dialogType,
+ viewDialogVisible,
+ currentKnowledge,
+ } = toRefs(data);
+
+ // 琛ㄥ崟寮曠敤
+ const formRef = ref();
+ const execStepsList = ref([]);
+
+ // 琛ㄦ牸鍒楅厤缃�
+ const tableColumn = ref([
+ {
+ label: "搴旀�ラ妗堢紪鐮�",
+ prop: "planCode",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "搴旀�ラ妗堝悕绉�",
+ prop: "planName",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍙戝竷鐢熸晥鏃堕棿",
+ prop: "publishTime",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鏍稿績璐d换浜�",
+ prop: "coreResponsorUserName",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "棰勬绫诲瀷",
+ prop: "planType",
+ showOverflowTooltip: true,
+ formatData: params => {
+ return emergencyPlanTypeLabel(params);
+ },
+ },
+ {
+ label: "澶囨敞",
+ prop: "remark",
+ showOverflowTooltip: true,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 200,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ },
+ {
+ name: "鏌ョ湅",
+ type: "text",
+ clickFun: row => {
+ viewKnowledge(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const userList = ref([]);
+ // 鐢熷懡鍛ㄦ湡
+ onMounted(() => {
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ getList();
+ startAutoRefresh();
+ });
+ const handleChange = userId => {
+ const selectedUser = userList.value.find(user => user.userId === userId);
+ if (selectedUser) {
+ form.value.coreResponsorUserName = selectedUser.nickName;
+ }
+ };
+
+ // 搴旀�ュ缃楠ょ鐞�
+ const addExecStep = () => {
+ const stepNumber = execStepsList.value.length + 1;
+ execStepsList.value.push({
+ step: `姝ラ${stepNumber}`,
+ description: "",
+ });
+ };
+
+ const removeExecStep = index => {
+ execStepsList.value.splice(index, 1);
+ };
+
+ const initExecSteps = execSteps => {
+ if (execSteps) {
+ try {
+ execStepsList.value = JSON.parse(execSteps);
+ } catch (e) {
+ execStepsList.value = [];
+ }
+ } else {
+ execStepsList.value = [];
+ }
+ };
+ // 寮�濮嬭嚜鍔ㄥ埛鏂�
+ const startAutoRefresh = () => {
+ setInterval(() => {
+ getList();
+ }, 600000); // 10鍒嗛挓鍒锋柊涓�娆� (10 * 60 * 1000 = 600000ms)
+ };
+
+ // 鏌ヨ鏁版嵁
+ const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ safeContingencyPlanListPage({ ...page.value, ...searchForm.value })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.value.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鍒嗛〉澶勭悊
+ const pagination = obj => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ getList();
+ };
+
+ // 閫夋嫨鍙樺寲澶勭悊
+ const handleSelectionChange = selection => {
+ selectedIds.value = selection.map(item => item.id);
+ };
+
+ // 鎵撳紑琛ㄥ崟
+ const openForm = (type, row = null) => {
+ dialogType.value = type;
+ if (type === "add") {
+ dialogTitle.value = "鏂板搴旀�ラ妗�";
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form.value, {
+ planCode: "", // 棰勬缂栧彿
+ applyScope: [], // 閫傜敤鑼冨洿
+ planType: "", // 棰勬绫诲瀷
+ planName: "", // 棰勬鍚嶇О
+ publishTime: "", // 鍙戝竷鏃堕棿
+ coreResponsorUserId: "", // 鏍稿績璐熻矗浜虹敤鎴稩D
+ coreResponsorUserName: "", // 鏍稿績璐熻矗浜虹敤鎴峰悕
+ remark: "", // 澶囨敞
+ execSteps: "", // 鎵ц姝ラ
+ });
+ initExecSteps("");
+ } else if (type === "edit" && row) {
+ dialogTitle.value = "缂栬緫搴旀�ラ妗�";
+ Object.assign(form.value, {
+ id: row.id,
+ planCode: row.planCode, // 棰勬缂栧彿
+ applyScope: row.applyScope ? row.applyScope.split(",") : [], // 閫傜敤鑼冨洿
+ planType: row.planType, // 棰勬绫诲瀷
+ planName: row.planName, // 棰勬鍚嶇О
+ publishTime: row.publishTime, // 鍙戝竷鏃堕棿
+ coreResponsorUserId: row.coreResponsorUserId, // 鏍稿績璐熻矗浜虹敤鎴稩D
+ coreResponsorUserName: row.coreResponsorUserName, // 鏍稿績璐熻矗浜虹敤鎴峰悕
+ remark: row.remark, // 澶囨敞
+ execSteps: row.execSteps, // 鎵ц姝ラ
+ });
+ initExecSteps(row.execSteps);
+ }
+ dialogVisible.value = true;
+ };
+
+ // 鏌ョ湅搴旀�ラ妗堣鎯�
+ const viewKnowledge = row => {
+ currentKnowledge.value = { ...row };
+ viewDialogVisible.value = true;
+ };
+ const getApplyScopeLabel = scope => {
+ const scopeMap = {
+ all: "鍏ㄤ綋鍛樺伐",
+ manager: "绠$悊灞�",
+ hr: "浜轰簨閮ㄩ棬",
+ finance: "璐㈠姟閮ㄩ棬",
+ tech: "鎶�鏈儴闂�",
+ };
+ return scopeMap[scope] || scope;
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩绫诲瀷
+ const getTypeTagType = type => {
+ const typeMap = {
+ contract: "success",
+ approval: "warning",
+ solution: "primary",
+ experience: "info",
+ guide: "danger",
+ };
+ return typeMap[type] || "info";
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩鏂囨湰
+ const getTypeLabel = type => {
+ return getKnowledgeTypeLabel(type);
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩绫诲瀷
+ const getEfficiencyTagType = efficiency => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info",
+ };
+ return typeMap[efficiency] || "info";
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩鏂囨湰
+ const getEfficiencyLabel = efficiency => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌",
+ };
+ return efficiencyMap[efficiency] || efficiency;
+ };
+
+ // 鑾峰彇鏁堢巼鎻愬崌鐧惧垎姣�
+ const getEfficiencyScore = efficiency => {
+ const scoreMap = {
+ high: 40,
+ medium: 25,
+ low: 15,
+ };
+ return scoreMap[efficiency] || 0;
+ };
+
+ // 鑾峰彇骞冲潎鑺傜渷鏃堕棿
+ const getTimeSaved = efficiency => {
+ const timeMap = {
+ high: "2-3澶�",
+ medium: "1-2澶�",
+ low: "0.5-1澶�",
+ };
+ return timeMap[efficiency] || "鏈煡";
+ };
+ const { proxy } = getCurrentInstance();
+ const { emergency_plan_type } = proxy.useDict("emergency_plan_type");
+ const emergencyPlanTypeOptions = computed(
+ () => emergency_plan_type?.value || []
+ );
+ const emergencyPlanTypeLabel = val => {
+ const item = emergencyPlanTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item ? item.label : val;
+ };
+ // 鎻愪氦搴旀�ラ妗堣〃鍗�
+ const submitForm = async () => {
+ try {
+ await formRef.value.validate();
+
+ // 楠岃瘉搴旀�ュ缃楠�
+ for (let i = 0; i < execStepsList.value.length; i++) {
+ const step = execStepsList.value[i];
+ if (!step.step || !step.step.trim()) {
+ ElMessage.error(`绗�${i + 1}鏉℃楠ょ殑"姝ラ"涓嶈兘涓虹┖`);
+ return;
+ }
+ if (!step.description || !step.description.trim()) {
+ ElMessage.error(`绗�${i + 1}鏉℃楠ょ殑"鎺柦"涓嶈兘涓虹┖`);
+ return;
+ }
+ }
+
+ // 灏嗗簲鎬ュ缃楠よ浆鎹负JSON瀛楃涓�
+ form.value.execSteps = JSON.stringify(execStepsList.value);
+ if (dialogType.value === "add") {
+ // 鏂板搴旀�ラ妗�
+ form.value.applyScope = form.value.applyScope.join(",");
+ safeContingencyPlanAdd({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("娣诲姞鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ } else {
+ form.value.applyScope = form.value.applyScope.join(",");
+ safeContingencyPlanUpdate({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+ };
+
+ // 鍒犻櫎搴旀�ラ妗�
+ const handleDelete = () => {
+ if (selectedIds.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑搴旀�ラ妗�");
+ return;
+ }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // console.log(selectedIds.value);
+ safeContingencyPlanDel(selectedIds.value).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ selectedIds.value = [];
+ getList();
+ }
+ });
+ })
+ .catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+ };
+
+ // 瀵煎嚭
+
+ // 瀛楀吀宸ュ叿
+ const knowledgeTypeOptions = computed(() => knowledge_type?.value || []);
+ const getKnowledgeTypeLabel = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item ? item.label : val;
+ };
+ const getKnowledgeTypeTagType = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item?.elTagType || "info";
+ };
+ const handleExport = () => {
+ proxy.download(
+ "/knowledgeBase/export",
+ { ...searchForm.value },
+ "搴旀�ラ妗堝簱.xlsx"
+ );
+ };
+</script>
+
+<style scoped>
+ .auto-refresh-info {
+ margin-bottom: 15px;
+ }
+
+ .auto-refresh-info .el-alert {
+ border-radius: 8px;
+ }
+
+ .dialog-footer {
+ text-align: right;
+ }
+
+ .knowledge-detail {
+ padding: 20px 0;
+ }
+
+ .detail-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+ }
+
+ .detail-section {
+ margin-top: 24px;
+ }
+
+ .detail-section h4 {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+ }
+
+ .detail-content {
+ background: #f8f9fa;
+ padding: 16px;
+ border-radius: 6px;
+ line-height: 1.6;
+ color: #606266;
+ white-space: pre-wrap;
+ }
+
+ .key-points {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .usage-stats {
+ margin-top: 16px;
+ }
+
+ .stat-item {
+ text-align: center;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ }
+
+ .stat-number {
+ font-size: 24px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 8px;
+ }
+
+ .stat-label {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ .exec-steps-container {
+ border: 1px solid #e4e7ed;
+ border-radius: 8px;
+ padding: 20px;
+ background-color: #f9fafc;
+ margin-top: 10px;
+ }
+
+ .exec-step-item {
+ margin-bottom: 12px;
+ padding: 12px;
+ background-color: #ffffff;
+ border: 1px solid #e4e7ed;
+ border-radius: 6px;
+ transition: all 0.3s ease;
+ }
+
+ .exec-step-item:hover {
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+ border-color: #c6e2ff;
+ }
+
+ .step-header {
+ display: flex;
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 8px;
+ }
+
+ .exec-step-view {
+ margin-bottom: 16px;
+ padding-left: 24px;
+ position: relative;
+ line-height: 1.6;
+ }
+
+ .exec-step-view::before {
+ content: "";
+ position: absolute;
+ left: 10px;
+ top: 20px;
+ bottom: -16px;
+ width: 2px;
+ background-color: #eaeaea;
+ }
+
+ .exec-step-view:last-child::before {
+ display: none;
+ }
+
+ .step-number {
+ position: absolute;
+ left: 0;
+ top: 0;
+ width: 20px;
+ height: 20px;
+ line-height: 20px;
+ text-align: center;
+ font-weight: bold;
+ color: #ffffff;
+ background-color: #409eff;
+ border-radius: 50%;
+ font-size: 12px;
+ z-index: 1;
+ }
+
+ .step-title {
+ font-weight: 600;
+ margin-right: 8px;
+ color: #395a9c;
+ }
+
+ .no-data {
+ color: #909399;
+ font-style: italic;
+ text-align: center;
+ padding: 20px;
+ background-color: #f8f9fa;
+ border-radius: 4px;
+ margin-top: 10px;
+ }
+</style>
diff --git a/src/views/safeProduction/hazardSourceLedger/index.vue b/src/views/safeProduction/hazardSourceLedger/index.vue
new file mode 100644
index 0000000..416202e
--- /dev/null
+++ b/src/views/safeProduction/hazardSourceLedger/index.vue
@@ -0,0 +1,750 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">鍗遍櫓婧愬悕绉帮細</span>
+ <el-input v-model="searchForm.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ嵄闄╂簮鍚嶇О鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span class="search_title ml10">鍗遍櫓婧愮被鍨嬶細</span>
+ <el-select v-model="searchForm.type"
+ clearable
+ @change="handleQuery"
+ style="width: 240px">
+ <el-option v-for="item in knowledgeTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板鍗遍櫓婧�</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ :rowClassName="getRowClass"></PIMTable>
+ </div>
+ <!-- 鏂板/缂栬緫鐭ヨ瘑寮圭獥 -->
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="800px"
+ :close-on-click-modal="false">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍗遍櫓婧愮紪鐮�"
+ prop="code">
+ <el-input v-model="form.code"
+ placeholder="璇疯緭鍏ュ嵄闄╂簮缂栫爜" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍗遍櫓婧愬悕绉�"
+ prop="name">
+ <el-input v-model="form.name"
+ placeholder="璇疯緭鍏ュ嵄闄╂簮鍚嶇О" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鎵�鍦ㄤ綅缃�"
+ prop="location">
+ <el-input v-model="form.location"
+ placeholder="璇疯緭鍏ユ墍鍦ㄤ綅缃�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绠℃帶鎺柦"
+ prop="controlMeasures">
+ <el-input v-model="form.controlMeasures"
+ placeholder="璇疯緭鍏ョ鎺ф帾鏂�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍗遍櫓婧愮被鍨�"
+ prop="type">
+ <el-select v-model="form.type"
+ placeholder="璇烽�夋嫨鍗遍櫓婧愮被鍨�"
+ style="width: 100%">
+ <el-option v-for="item in knowledgeTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="搴撳瓨鏁伴噺"
+ prop="stockQty">
+ <el-input v-model="form.stockQty"
+ type="number"
+ min="0"
+ placeholder="璇疯緭鍏ュ簱瀛樻暟閲�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绠℃帶璐d换浜�"
+ prop="principalUserId">
+ <el-select v-model="form.principalUserId"
+ placeholder="璇烽�夋嫨绠℃帶璐d换浜�"
+ @change="handleUserChange"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐d换浜鸿仈绯荤數璇�"
+ prop="principalMobile">
+ <el-input v-model="form.principalMobile"
+ readonly
+ placeholder="鑷姩甯﹀嚭" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="椋庨櫓绛夌骇"
+ prop="riskLevel">
+ <el-select v-model="form.riskLevel"
+ placeholder="璇烽�夋嫨椋庨櫓绛夌骇"
+ clearable>
+ <el-option v-for="item in riskLevelOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="瑙勬牸 / 椋庨櫓鎻忚堪"
+ prop="specInfo">
+ <el-input v-model="form.specInfo"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ヨ鏍� / 椋庨櫓鎻忚堪" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鏌ョ湅鐭ヨ瘑璇︽儏寮圭獥 -->
+ <el-dialog v-model="viewDialogVisible"
+ title="鍗遍櫓婧愯鎯�"
+ width="900px"
+ :close-on-click-modal="false">
+ <div class="knowledge-detail">
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="鍗遍櫓婧愬悕绉�"
+ :span="2">
+ <span class="detail-title">{{ currentKnowledge.name }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍗遍櫓婧愮紪鐮�">
+ {{ currentKnowledge.code }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍗遍櫓婧愮被鍨�">
+ <el-tag type="info">
+ {{ getTypeLabel(currentKnowledge.type) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵�鍦ㄤ綅缃�">
+ {{ currentKnowledge.location }}
+ </el-descriptions-item>
+ <el-descriptions-item label="绠℃帶鎺柦">
+ {{ currentKnowledge.controlMeasures }}
+ </el-descriptions-item>
+ <el-descriptions-item label="搴撳瓨鏁伴噺">
+ {{ currentKnowledge.stockQty }}
+ </el-descriptions-item>
+ <el-descriptions-item label="绠℃帶璐d换浜�">
+ {{ currentKnowledge.principalUserId }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璐d换浜鸿仈绯荤數璇�">
+ {{ currentKnowledge.principalMobile }}
+ </el-descriptions-item>
+ <el-descriptions-item label="椋庨櫓绛夌骇">
+ <el-tag :type="getTypeTagType(currentKnowledge.riskLevel)">
+ {{ currentKnowledge.riskLevel }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸 / 椋庨櫓鎻忚堪">
+ {{ currentKnowledge.specInfo }}
+ </el-descriptions-item>
+ </el-descriptions>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button @click="viewDialogVisible = false">鍏抽棴</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ computed,
+ } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import { userListNoPage } from "@/api/system/user.js";
+ import {
+ listKnowledgeBase,
+ delKnowledgeBase,
+ addKnowledgeBase,
+ updateKnowledgeBase,
+ } from "@/api/collaborativeApproval/knowledgeBase.js";
+ import {
+ safeHazardListPage,
+ safeHazardAdd,
+ safeHazardUpdate,
+ safeHazardDel,
+ } from "@/api/safeProduction/hazardSourceLedger.js";
+
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ code: [{ required: true, message: "璇疯緭鍏ュ嵄闄╂簮缂栫爜", trigger: "blur" }],
+ name: [{ required: true, message: "璇疯緭鍏ュ嵄闄╂簮鍚嶇О", trigger: "blur" }],
+ type: [{ required: true, message: "璇烽�夋嫨鍗遍櫓婧愮被鍨�", trigger: "change" }],
+ location: [{ required: true, message: "璇疯緭鍏ユ墍鍦ㄤ綅缃�", trigger: "blur" }],
+ controlMeasures: [
+ { required: true, message: "璇疯緭鍏ョ鎺ф帾鏂�", trigger: "blur" },
+ ],
+ stockQty: [{ required: true, message: "璇疯緭鍏ュ簱瀛樻暟閲�", trigger: "blur" }],
+ principalUser: [
+ { required: true, message: "璇疯緭鍏ョ鎺ц矗浠讳汉", trigger: "blur" },
+ ],
+ riskLevel: [{ required: true, message: "璇烽�夋嫨椋庨櫓绛夌骇", trigger: "change" }],
+ };
+
+ // 鍝嶅簲寮忔暟鎹�
+ const data = reactive({
+ searchForm: {
+ name: "",
+ type: "",
+ },
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+ selectedIds: [],
+ form: {
+ code: "",
+ name: "",
+ location: "",
+ controlMeasures: "",
+ riskLevel: "",
+ principalUser: "",
+ principalMobile: "",
+ specInfo: "",
+ principalUserId: "",
+ controlMeasures: "",
+ stockQty: "",
+ type: "",
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ viewDialogVisible: false,
+ currentKnowledge: {},
+ });
+
+ const {
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ selectedIds,
+ form,
+ dialogVisible,
+ dialogTitle,
+ dialogType,
+ viewDialogVisible,
+ currentKnowledge,
+ } = toRefs(data);
+
+ // 琛ㄥ崟寮曠敤
+ const formRef = ref();
+ const riskLevelOptions = ref([
+ { value: "浣庨闄�", label: "浣庨闄�" },
+ { value: "涓�鑸闄�", label: "涓�鑸闄�" },
+ { value: "杈冨ぇ椋庨櫓", label: "杈冨ぇ椋庨櫓" },
+ { value: "閲嶅ぇ椋庨櫓", label: "閲嶅ぇ椋庨櫓" },
+ ]);
+
+ // 琛ㄦ牸鍒楅厤缃�
+ const tableColumn = ref([
+ {
+ label: "鍗遍櫓婧愮紪鐮�",
+ prop: "code",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍗遍櫓婧愬悕绉�",
+ prop: "name",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍗遍櫓婧愮被鍨�",
+ prop: "type",
+ showOverflowTooltip: true,
+ formatData: params => {
+ return getTypeLabel(params);
+ },
+ },
+ {
+ label: "椋庨櫓绛夌骇",
+ prop: "riskLevel",
+ showOverflowTooltip: true,
+ dataType: "tag",
+ formatType: params => {
+ const typeMap = {
+ 浣庨闄�: "info",
+ 涓�鑸闄�: "info",
+ 杈冨ぇ椋庨櫓: "warning",
+ 閲嶅ぇ椋庨櫓: "danger",
+ };
+ return typeMap[params] || "info";
+ },
+ },
+ {
+ label: "鎵�鍦ㄤ綅缃�",
+ prop: "location",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "绠℃帶鎺柦",
+ prop: "controlMeasures",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "搴撳瓨鏁伴噺",
+ prop: "stockQty",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "绠℃帶璐d换浜�",
+ prop: "principalUser",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "璐d换浜鸿仈绯荤數璇�",
+ prop: "principalMobile",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "瑙勬牸 / 椋庨櫓鎻忚堪",
+ prop: "specInfo",
+ showOverflowTooltip: true,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 200,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ },
+ {
+ name: "鏌ョ湅",
+ type: "text",
+ clickFun: row => {
+ viewKnowledge(row);
+ },
+ },
+ ],
+ },
+ ]);
+ const userList = ref([]);
+ // 鐢熷懡鍛ㄦ湡
+ onMounted(() => {
+ getList();
+ startAutoRefresh();
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ });
+
+ // 澶勭悊鐢ㄦ埛閫夋嫨鍙樺寲
+ const handleUserChange = userId => {
+ const selectedUser = userList.value.find(user => user.userId === userId);
+ if (selectedUser) {
+ form.value.principalUser = selectedUser.nickName;
+ form.value.principalMobile = selectedUser.phonenumber;
+ }
+ };
+
+ // 寮�濮嬭嚜鍔ㄥ埛鏂�
+ const startAutoRefresh = () => {
+ setInterval(() => {
+ getList();
+ }, 600000); // 10鍒嗛挓鍒锋柊涓�娆� (10 * 60 * 1000 = 600000ms)
+ };
+
+ // 鏌ヨ鏁版嵁
+ const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ safeHazardListPage({ ...page.value, ...searchForm.value })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.value.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鍒嗛〉澶勭悊
+ const pagination = obj => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ getList();
+ };
+
+ // 閫夋嫨鍙樺寲澶勭悊
+ const handleSelectionChange = selection => {
+ selectedIds.value = selection.map(item => item.id);
+ };
+
+ // 鎵撳紑琛ㄥ崟
+ const openForm = (type, row = null) => {
+ dialogType.value = type;
+ if (type === "add") {
+ dialogTitle.value = "鏂板鍗遍櫓婧�";
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form.value, {
+ code: "",
+ name: "",
+ location: "",
+ controlMeasures: "",
+ riskLevel: "",
+ principalUser: "",
+ principalMobile: "",
+ specInfo: "",
+ principalUserId: "",
+ controlMeasures: "",
+ stockQty: "",
+ type: "",
+ });
+ } else if (type === "edit" && row) {
+ dialogTitle.value = "缂栬緫鍗遍櫓婧�";
+ Object.assign(form.value, {
+ id: row.id,
+ code: row.code,
+ name: row.name,
+ location: row.location,
+ controlMeasures: row.controlMeasures,
+ riskLevel: row.riskLevel,
+ principalUser: row.principalUser,
+ principalMobile: row.principalMobile,
+ specInfo: row.specInfo,
+ principalUserId: row.principalUserId,
+ controlMeasures: row.controlMeasures,
+ stockQty: row.stockQty,
+ type: row.type,
+ });
+ }
+ dialogVisible.value = true;
+ };
+
+ // 鏌ョ湅鍗遍櫓婧愯鎯�
+ const viewKnowledge = row => {
+ currentKnowledge.value = { ...row };
+ viewDialogVisible.value = true;
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩绫诲瀷
+ const getTypeTagType = type => {
+ const typeMap = {
+ 杈冨ぇ椋庨櫓: "warning",
+ 浣庨闄�: "info",
+ 涓�鑸闄�: "info",
+ 閲嶅ぇ椋庨櫓: "danger",
+ };
+ return typeMap[type] || "info";
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩鏂囨湰
+ const getTypeLabel = type => {
+ return getKnowledgeTypeLabel(type);
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩绫诲瀷
+ const getEfficiencyTagType = efficiency => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info",
+ };
+ return typeMap[efficiency] || "info";
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩鏂囨湰
+ const getEfficiencyLabel = efficiency => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌",
+ };
+ return efficiencyMap[efficiency] || efficiency;
+ };
+
+ // 鑾峰彇鏁堢巼鎻愬崌鐧惧垎姣�
+ const getEfficiencyScore = efficiency => {
+ const scoreMap = {
+ high: 40,
+ medium: 25,
+ low: 15,
+ };
+ return scoreMap[efficiency] || 0;
+ };
+
+ // 鑾峰彇骞冲潎鑺傜渷鏃堕棿
+ const getTimeSaved = efficiency => {
+ const timeMap = {
+ high: "2-3澶�",
+ medium: "1-2澶�",
+ low: "0.5-1澶�",
+ };
+ return timeMap[efficiency] || "鏈煡";
+ };
+
+ /**
+ * 鑾峰彇琛岀被鍚嶏紝鐢ㄤ簬鍒ゆ柇椋庨櫓绛夌骇鏄惁涓洪噸澶ч闄�
+ * @param row 琛屾暟鎹�
+ * @returns 绫诲悕
+ */
+ const getRowClass = ({ row }) => {
+ if (row.riskLevel === "閲嶅ぇ椋庨櫓") {
+ return "danger-row";
+ }
+ return "";
+ };
+
+ // 鎻愪氦鍗遍櫓婧愯〃鍗�
+ const submitForm = async () => {
+ try {
+ await formRef.value.validate();
+ if (dialogType.value === "add") {
+ // 鏂板鍗遍櫓婧愬彴璐�
+ safeHazardAdd({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("娣诲姞鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ } else {
+ // 鏇存柊鍗遍櫓婧愬彴璐�
+ safeHazardUpdate({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+ };
+
+ // 鍒犻櫎鍗遍櫓婧�
+ const handleDelete = () => {
+ if (selectedIds.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑鍗遍櫓婧�");
+ return;
+ }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // console.log(selectedIds.value);
+ safeHazardDel(selectedIds.value).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ selectedIds.value = [];
+ getList();
+ }
+ });
+ })
+ .catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+ };
+
+ // 瀵煎嚭
+ const { proxy } = getCurrentInstance();
+ const { hazard_source_type } = proxy.useDict("hazard_source_type");
+
+ // 瀛楀吀宸ュ叿
+ const knowledgeTypeOptions = computed(() => hazard_source_type?.value || []);
+ const getKnowledgeTypeLabel = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item ? item.label : val;
+ };
+ const getKnowledgeTypeTagType = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item?.elTagType || "info";
+ };
+ const handleExport = () => {
+ proxy.download(
+ "/knowledgeBase/export",
+ { ...searchForm.value },
+ "鐭ヨ瘑搴�.xlsx"
+ );
+ };
+</script>
+
+<style scoped>
+ .auto-refresh-info {
+ margin-bottom: 15px;
+ }
+
+ .auto-refresh-info .el-alert {
+ border-radius: 8px;
+ }
+
+ .dialog-footer {
+ text-align: right;
+ }
+
+ .knowledge-detail {
+ padding: 20px 0;
+ }
+
+ .detail-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+ }
+
+ .detail-section {
+ margin-top: 24px;
+ }
+
+ .detail-section h4 {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+ }
+
+ .detail-content {
+ background: #f8f9fa;
+ padding: 16px;
+ border-radius: 6px;
+ line-height: 1.6;
+ color: #606266;
+ white-space: pre-wrap;
+ }
+
+ .key-points {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .usage-stats {
+ margin-top: 16px;
+ }
+
+ .stat-item {
+ text-align: center;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ }
+
+ .stat-number {
+ font-size: 24px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 8px;
+ }
+
+ .stat-label {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.danger-row td) {
+ color: #e95a66 !important;
+ }
+</style>
diff --git a/src/views/safeProduction/hazardousMaterialsControl/index.vue b/src/views/safeProduction/hazardousMaterialsControl/index.vue
new file mode 100644
index 0000000..e43b4f6
--- /dev/null
+++ b/src/views/safeProduction/hazardousMaterialsControl/index.vue
@@ -0,0 +1,934 @@
+<template>
+ <div class="app-container">
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">鍗遍櫓婧愬悕绉帮細</span>
+ <el-input v-model="searchForm.name"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ュ嵄闄╂簮鍚嶇О鎼滅储"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span class="search_title ml10">鍗遍櫓婧愮被鍨嬶細</span>
+ <el-select v-model="searchForm.type"
+ clearable
+ @change="handleQuery"
+ style="width: 240px">
+ <el-option v-for="item in knowledgeTypeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鍘婚鐢�</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination1"
+ :total="page.total"></PIMTable>
+ </div>
+ <!-- 鏂板/缂栬緫鐭ヨ瘑寮圭獥 -->
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="800px"
+ :close-on-click-modal="false">
+ <el-form ref="formRef"
+ v-if="dialogType === 'add'"
+ :model="form"
+ :rules="rules"
+ label-width="140px"
+ label-position="top">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍗遍櫓婧�"
+ prop="safeHazardId">
+ <el-input v-model="valueItem.name"
+ readonly
+ @click="openSafeHazardSelect"
+ placeholder="璇烽�夋嫨鍗遍櫓婧�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鎵�鍦ㄤ綅缃�"
+ prop="location">
+ <el-input v-model="valueItem.location"
+ disabled
+ placeholder="鑷姩甯﹀嚭" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍗遍櫓婧愮被鍨�"
+ prop="type">
+ <el-input v-model="valueItem.type"
+ disabled
+ placeholder="鑷姩甯﹀嚭" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="椋庨櫓绛夌骇"
+ prop="riskLevel">
+ <el-input v-model="valueItem.riskLevel"
+ disabled
+ placeholder="鑷姩甯﹀嚭" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="搴撳瓨鏁伴噺"
+ prop="stockQty">
+ <el-input v-model="valueItem.stockQty"
+ disabled
+ placeholder="鑷姩甯﹀嚭" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="棰嗙敤鐢ㄩ��"
+ prop="applyPurpose">
+ <el-input v-model="form.applyPurpose"
+ placeholder="璇疯緭鍏ラ鐢ㄧ敤閫�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="棰嗙敤鏁伴噺"
+ prop="applyQty">
+ <el-input v-model="form.applyQty"
+ type="number"
+ min="0"
+ @input="handleApplyQtyChange"
+ placeholder="璇疯緭鍏ラ鐢ㄦ暟閲�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="棰嗙敤浜�"
+ prop="applyUserId">
+ <el-select v-model="form.applyUserId"
+ disabled
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="棰嗙敤鏃堕棿"
+ prop="applyTime">
+ <el-date-picker style="width: 100%"
+ disabled
+ v-model="form.applyTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <el-descriptions :column="2"
+ style="margin-bottom: 20px"
+ v-if="dialogType === 'edit' || dialogType === 'view'"
+ border>
+ <el-descriptions-item label="鍗遍櫓婧愮紪鐮�">
+ <span>{{ form.code }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍗遍櫓婧愬悕绉�">
+ <span>{{ form.name }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍗遍櫓婧愮被鍨�">
+ <el-tag type="info">
+ {{ getTypeLabel(form.type) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鎵�鍦ㄤ綅缃�">
+ <span>{{ form.location }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="椋庨櫓绛夌骇">
+ <el-tag :type="getTypeTagType(form.riskLevel)">
+ {{ form.riskLevel }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="棰嗙敤鐢ㄩ��">
+ <span>{{ form.applyPurpose }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="棰嗙敤鏃堕棿">
+ <span>{{ form.applyTime }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="棰嗙敤鏁伴噺">
+ <span>{{ form.applyQty }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item label="棰嗙敤浜�">
+ <span>{{ form.applyUserName }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item v-if="dialogType === 'view' && form.returnUserId"
+ label="褰掕繕浜�">
+ <span>{{ form.returnUserName }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item v-if="dialogType === 'view' && form.returnTime"
+ label="褰掕繕鏃堕棿">
+ <span>{{ form.returnTime }}</span>
+ </el-descriptions-item>
+ <el-descriptions-item v-if="dialogType === 'view' && form.returnUserId"
+ label="褰掕繕鎯呭喌璇存槑">
+ <span>{{ form.returnRemark }}</span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <el-form ref="formRef1"
+ v-if="dialogType === 'edit'"
+ :model="form"
+ :rules="rules1"
+ label-width="140px"
+ label-position="top">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="褰掕繕浜�"
+ prop="returnUserId">
+ <el-select v-model="form.returnUserId"
+ disabled
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰掕繕鏃堕棿"
+ prop="returnTime">
+ <el-date-picker style="width: 100%"
+ disabled
+ v-model="form.returnTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="褰掕繕鎯呭喌璇存槑"
+ prop="applyPurpose">
+ <el-input v-model="form.returnRemark"
+ type="textarea"
+ :rows="4"
+ placeholder="璇疯緭鍏ュ綊杩樻儏鍐佃鏄�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鍗遍櫓婧愰�夋嫨寮圭獥 -->
+ <el-dialog v-model="safeHazardSelectVisible"
+ title="閫夋嫨鍗遍櫓婧�"
+ width="800px"
+ :close-on-click-modal="false">
+ <div>
+ <el-table :data="safeHazardList"
+ border
+ ref="safeHazardTableRef"
+ v-loading="safeHazardLoading"
+ :selection="selectedSafeHazardIds"
+ @selection-change="handleSafeHazardSelectionChange"
+ style="width: 100%">
+ <el-table-column type="selection"
+ width="55"
+ :selectable="isSelectable" />
+ <el-table-column prop="code"
+ label="鍗遍櫓婧愮紪鐮�"
+ width="180"
+ show-overflow-tooltip />
+ <el-table-column prop="name"
+ label="鍗遍櫓婧愬悕绉�"
+ show-overflow-tooltip />
+ <el-table-column prop="type"
+ label="鍗遍櫓婧愮被鍨�"
+ width="120"
+ show-overflow-tooltip>
+ <template #default="scope">
+ {{ getTypeLabel(scope.row.type) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="location"
+ label="鎵�鍦ㄤ綅缃�"
+ width="180"
+ show-overflow-tooltip />
+ <el-table-column prop="riskLevel"
+ label="椋庨櫓绛夌骇"
+ width="100">
+ <template #default="scope">
+ <el-tag :type="getTypeTagType(scope.row.riskLevel)">
+ {{ scope.row.riskLevel }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="stockQty"
+ label="搴撳瓨鏁伴噺"
+ width="100" />
+ </el-table>
+ <pagination :total="safeHazardPage.total"
+ style="margin-top: 20px"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="safeHazardPage.current"
+ :limit="safeHazardPage.size"
+ @pagination="safeHazardPagination" />
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="handleSafeHazardSelect">纭畾</el-button>
+ <el-button @click="safeHazardSelectVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ computed,
+ } from "vue";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import pagination from "@/components/PIMTable/Pagination.vue";
+ import { userListNoPage } from "@/api/system/user.js";
+ import { safeHazardListPage } from "@/api/safeProduction/hazardSourceLedger.js";
+ import {
+ safeHazardRecordListPage,
+ safeHazardRecordDel,
+ safeHazardRecordAdd,
+ safeHazardRecordUpdate,
+ } from "@/api/safeProduction/hazardousMaterialsControl.js";
+ import dayjs from "dayjs";
+ import useUserStore from "@/store/modules/user";
+ const userStore = useUserStore();
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ applyPurpose: [
+ { required: true, message: "璇疯緭鍏ラ鐢ㄧ敤閫�", trigger: "blur" },
+ ],
+ applyQty: [{ required: true, message: "璇疯緭鍏ラ鐢ㄦ暟閲�", trigger: "blur" }],
+ safeHazardId: [
+ { required: true, message: "璇烽�夋嫨鍗遍櫓婧�", trigger: "change" },
+ ],
+ };
+ const rules1 = {
+ returnRemark: [
+ { required: true, message: "璇疯緭鍏ュ綊杩樻儏鍐佃鏄�", trigger: "blur" },
+ ],
+ };
+ // 鍝嶅簲寮忔暟鎹�
+ const data = reactive({
+ searchForm: {
+ name: "",
+ type: "",
+ },
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+ selectedIds: [],
+ form: {
+ applyPurpose: "", // 棰嗙敤鐢ㄩ��
+ applyTime: "", // 棰嗙敤鏃堕棿
+ applyQty: "", // 棰嗙敤鏁伴噺
+ applyUserId: "", // 棰嗙敤浜篒D
+ safeHazardId: "", // 鍗遍櫓婧怚D
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ viewDialogVisible: false,
+ currentKnowledge: {},
+ safeHazardSelectVisible: false,
+ safeHazardList: [],
+ safeHazardLoading: false,
+ safeHazardPage: {
+ current: 1,
+ size: 10,
+ total: 0,
+ },
+ });
+
+ const {
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ selectedIds,
+ form,
+ dialogVisible,
+ dialogTitle,
+ dialogType,
+ viewDialogVisible,
+ currentKnowledge,
+ safeHazardSelectVisible,
+ safeHazardList,
+ safeHazardLoading,
+ safeHazardPage,
+ } = toRefs(data);
+ const valueItem = ref({});
+
+ // 琛ㄥ崟寮曠敤
+ const formRef = ref();
+ const formRef1 = ref();
+ const riskLevelOptions = ref([
+ { value: "浣庨闄�", label: "浣庨闄�" },
+ { value: "涓�鑸闄�", label: "涓�鑸闄�" },
+ { value: "杈冨ぇ椋庨櫓", label: "杈冨ぇ椋庨櫓" },
+ { value: "閲嶅ぇ椋庨櫓", label: "閲嶅ぇ椋庨櫓" },
+ ]);
+
+ // 琛ㄦ牸鍒楅厤缃�
+ const tableColumn = ref([
+ {
+ label: "棰嗙敤鍗曞彿",
+ prop: "materialRecordCode",
+ width: 130,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍗遍櫓婧愮紪鐮�",
+ prop: "code",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍗遍櫓婧愬悕绉�",
+ prop: "name",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍗遍櫓婧愮被鍨�",
+ prop: "type",
+ showOverflowTooltip: true,
+ formatData: params => {
+ return getTypeLabel(params);
+ },
+ },
+ {
+ label: "椋庨櫓绛夌骇",
+ prop: "riskLevel",
+ showOverflowTooltip: true,
+ dataType: "tag",
+ formatType: params => {
+ const typeMap = {
+ 浣庨闄�: "info",
+ 涓�鑸闄�: "info",
+ 杈冨ぇ椋庨櫓: "warning",
+ 閲嶅ぇ椋庨櫓: "danger",
+ };
+ return typeMap[params] || "info";
+ },
+ },
+ {
+ label: "棰嗙敤鐢ㄩ��",
+ prop: "applyPurpose",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "棰嗙敤鏃堕棿",
+ prop: "applyTime",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "棰嗙敤鏁伴噺",
+ prop: "applyQty",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "褰掕繕鏃堕棿",
+ prop: "returnTime",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "褰掕繕浜�",
+ prop: "returnUserName",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "褰掕繕鎯呭喌璇存槑",
+ prop: "returnRemark",
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鎵�鍦ㄤ綅缃�",
+ prop: "location",
+ showOverflowTooltip: true,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 130,
+ operation: [
+ {
+ name: "褰掕繕",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ disabled: row => row.returnUserId,
+ },
+ {
+ name: "鏌ョ湅",
+ type: "text",
+ clickFun: row => {
+ openForm("view", row);
+ },
+ },
+ ],
+ },
+ ]);
+ const userList = ref([]);
+ // 鐢熷懡鍛ㄦ湡
+ onMounted(() => {
+ getCurrentFactoryName();
+ getList();
+ startAutoRefresh();
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ });
+ const currentUserId = ref("");
+ const currentUserName = ref("");
+ const getCurrentFactoryName = async () => {
+ let res = await userStore.getInfo();
+ currentUserId.value = res.user.userId;
+ currentUserName.value = res.user.nickName;
+ };
+
+ // 澶勭悊鐢ㄦ埛閫夋嫨鍙樺寲
+ const handleUserChange = userId => {
+ const selectedUser = userList.value.find(user => user.userId === userId);
+ if (selectedUser) {
+ form.value.principalUser = selectedUser.nickName;
+ form.value.principalMobile = selectedUser.phonenumber;
+ }
+ };
+ const handleApplyQtyChange = () => {
+ if (Number(form.value.applyQty) < 0) {
+ ElMessage.error("棰嗙敤鏁伴噺涓嶈兘灏忎簬0");
+ form.value.applyQty = 0;
+ return;
+ }
+ if (form.value.applyQty > valueItem.value.stockQty) {
+ ElMessage.error("棰嗙敤鏁伴噺涓嶈兘澶т簬搴撳瓨鏁伴噺");
+ form.value.applyQty = "";
+ }
+ };
+ const selectedSafeHazardIds = ref([]);
+
+ // 寮�濮嬭嚜鍔ㄥ埛鏂�
+ const startAutoRefresh = () => {
+ setInterval(() => {
+ getList();
+ }, 600000); // 10鍒嗛挓鍒锋柊涓�娆� (10 * 60 * 1000 = 600000ms)
+ };
+
+ // 鏌ヨ鏁版嵁
+ const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+ };
+
+ const getList = () => {
+ tableLoading.value = true;
+ safeHazardRecordListPage({ ...page.value, ...searchForm.value })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.value.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ const openSafeHazardSelect = async () => {
+ await fetchSafeHazardList();
+ safeHazardSelectVisible.value = true;
+ };
+
+ const fetchSafeHazardList = () => {
+ safeHazardLoading.value = true;
+ return safeHazardListPage({
+ current: safeHazardPage.value.current,
+ size: safeHazardPage.value.size,
+ })
+ .then(res => {
+ safeHazardList.value = res.data.records;
+ safeHazardPage.value.total = res.data.total;
+ })
+ .finally(() => {
+ safeHazardLoading.value = false;
+ });
+ };
+
+ const isSelectable = row => {
+ // 鍙湁搴撳瓨鏁伴噺澶т簬0鐨勮鎵嶈兘琚�夋嫨
+ return Number(row.stockQty) > 0;
+ };
+
+ const handleSafeHazardSelectionChange = selection => {
+ // 鍙繚鐣欐渶鍚庝竴涓�変腑鐨勯」
+ if (selection.length > 1) {
+ const lastSelected = selection[selection.length - 1];
+ selectedSafeHazardIds.value = [lastSelected];
+ proxy.$refs.safeHazardTableRef.clearSelection();
+ proxy.$refs.safeHazardTableRef.toggleRowSelection(lastSelected, true);
+ } else if (selection.length === 1) {
+ selectedSafeHazardIds.value = [selection[0]];
+ } else {
+ selectedSafeHazardIds.value = [];
+ }
+ };
+
+ const handleSafeHazardSelect = () => {
+ if (!selectedSafeHazardIds.value.length) {
+ ElMessage.error("璇烽�夋嫨涓�涓嵄闄╂簮");
+ return;
+ }
+
+ valueItem.value = {
+ ...selectedSafeHazardIds.value[0],
+ };
+ valueItem.value.type = getTypeLabel(valueItem.value.type);
+ form.value.safeHazardId = selectedSafeHazardIds.value[0].id;
+ safeHazardSelectVisible.value = false;
+ };
+
+ const safeHazardPagination = obj => {
+ safeHazardPage.value.current = obj.page;
+ safeHazardPage.value.size = obj.limit;
+ fetchSafeHazardList();
+ };
+
+ // 鍒嗛〉澶勭悊
+ const pagination1 = obj => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ getList();
+ };
+
+ // 閫夋嫨鍙樺寲澶勭悊
+ const handleSelectionChange = selection => {
+ selectedIds.value = selection.map(item => item.id);
+ };
+
+ // 鎵撳紑琛ㄥ崟
+ const openForm = (type, row = null) => {
+ dialogType.value = type;
+ if (type === "add") {
+ dialogTitle.value = "棰嗙敤鍗遍櫓婧�";
+ // 閲嶇疆琛ㄥ崟
+ form.value = {};
+ Object.assign(form.value, {
+ applyPurpose: "", // 棰嗙敤鐢ㄩ��
+ applyTime: dayjs().format("YYYY-MM-DD"), // 棰嗙敤鏃堕棿
+ applyQty: "", // 棰嗙敤鏁伴噺
+ applyUserId: currentUserId.value, // 棰嗙敤浜篒D
+ safeHazardId: "", // 鍗遍櫓婧怚D
+ });
+ valueItem.value = {};
+ } else if (type === "edit" && row) {
+ dialogTitle.value = "褰掕繕";
+ Object.assign(form.value, {
+ id: row.id, // 涓婚敭ID
+ materialRecordCode: row.materialRecordCode, // 鍗曞彿
+ applyPurpose: row.applyPurpose, // 棰嗙敤鐢ㄩ��
+ applyTime: row.applyTime, // 棰嗙敤鏃堕棿
+ applyQty: row.applyQty, // 棰嗙敤鏁伴噺
+ applyUserId: row.applyUserId, // 棰嗙敤浜篒D
+ safeHazardId: row.safeHazardId, // 鍗遍櫓婧怚D
+ applyUserName: row.applyUserName, // 棰嗙敤浜哄鍚�
+ code: row.code, // 鍗遍櫓婧愮紪鐮�
+ name: row.name, // 鍗遍櫓婧愬悕绉�
+ type: row.type, // 鍗遍櫓婧愮被鍨�
+ location: row.location, // 鎵�鍦ㄤ綅缃�
+ riskLevel: row.riskLevel, // 椋庨櫓绛夌骇
+ type: row.type, // 鍗遍櫓婧愮被鍨�
+ returnTime: dayjs().format("YYYY-MM-DD"), // 褰掕繕鏃堕棿
+ returnUserName: "", // 褰掕繕浜哄鍚�
+ returnUserId: currentUserId.value, // 褰掕繕浜篒D
+ returnRemark: "", // 褰掕繕鎯呭喌璇存槑
+ });
+ } else if (type === "view") {
+ dialogTitle.value = "鏌ョ湅";
+ form.value = { ...row };
+ }
+ dialogVisible.value = true;
+ };
+
+ // 鏌ョ湅鍗遍櫓婧愯鎯�
+ const viewKnowledge = row => {
+ currentKnowledge.value = { ...row };
+ viewDialogVisible.value = true;
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩绫诲瀷
+ const getTypeTagType = type => {
+ const typeMap = {
+ 杈冨ぇ椋庨櫓: "warning",
+ 浣庨闄�: "info",
+ 涓�鑸闄�: "info",
+ 閲嶅ぇ椋庨櫓: "danger",
+ };
+ return typeMap[type] || "info";
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩鏂囨湰
+ const getTypeLabel = type => {
+ return getKnowledgeTypeLabel(type);
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩绫诲瀷
+ const getEfficiencyTagType = efficiency => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info",
+ };
+ return typeMap[efficiency] || "info";
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩鏂囨湰
+ const getEfficiencyLabel = efficiency => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌",
+ };
+ return efficiencyMap[efficiency] || efficiency;
+ };
+
+ // 鑾峰彇鏁堢巼鎻愬崌鐧惧垎姣�
+ const getEfficiencyScore = efficiency => {
+ const scoreMap = {
+ high: 40,
+ medium: 25,
+ low: 15,
+ };
+ return scoreMap[efficiency] || 0;
+ };
+
+ // 鑾峰彇骞冲潎鑺傜渷鏃堕棿
+ const getTimeSaved = efficiency => {
+ const timeMap = {
+ high: "2-3澶�",
+ medium: "1-2澶�",
+ low: "0.5-1澶�",
+ };
+ return timeMap[efficiency] || "鏈煡";
+ };
+
+ // 鎻愪氦鍗遍櫓婧愯〃鍗�
+ const submitForm = async () => {
+ try {
+ if (dialogType.value === "add") {
+ await formRef.value.validate();
+ safeHazardRecordAdd({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("娣诲姞鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ } else if (dialogType.value === "edit") {
+ await formRef1.value.validate();
+ safeHazardRecordUpdate({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ } else if (dialogType.value === "view") {
+ // 鏌ョ湅妯″紡涓嬩笉鎻愪氦琛ㄥ崟
+ dialogVisible.value = false;
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+ };
+
+ // 鍒犻櫎鍗遍櫓婧愯褰�
+ const handleDelete = () => {
+ if (selectedIds.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑璁板綍");
+ return;
+ }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // console.log(selectedIds.value);
+ safeHazardRecordDel(selectedIds.value).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ selectedIds.value = [];
+ getList();
+ }
+ });
+ })
+ .catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+ };
+
+ // 瀵煎嚭
+ const { proxy } = getCurrentInstance();
+ const { hazard_source_type } = proxy.useDict("hazard_source_type");
+
+ // 瀛楀吀宸ュ叿
+ const knowledgeTypeOptions = computed(() => hazard_source_type?.value || []);
+ const getKnowledgeTypeLabel = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item ? item.label : val;
+ };
+ const getKnowledgeTypeTagType = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item?.elTagType || "info";
+ };
+ const handleExport = () => {
+ proxy.download(
+ "/knowledgeBase/export",
+ { ...searchForm.value },
+ "鐭ヨ瘑搴�.xlsx"
+ );
+ };
+</script>
+
+<style scoped>
+ .auto-refresh-info {
+ margin-bottom: 15px;
+ }
+
+ .auto-refresh-info .el-alert {
+ border-radius: 8px;
+ }
+
+ .dialog-footer {
+ text-align: right;
+ }
+
+ .knowledge-detail {
+ padding: 20px 0;
+ }
+
+ .detail-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+ }
+
+ .detail-section {
+ margin-top: 24px;
+ }
+
+ .detail-section h4 {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+ }
+
+ .detail-content {
+ background: #f8f9fa;
+ padding: 16px;
+ border-radius: 6px;
+ line-height: 1.6;
+ color: #606266;
+ white-space: pre-wrap;
+ }
+
+ .key-points {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .usage-stats {
+ margin-top: 16px;
+ }
+
+ .stat-item {
+ text-align: center;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ }
+
+ .stat-number {
+ font-size: 24px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 8px;
+ }
+
+ .stat-label {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.danger-row td) {
+ color: #e95a66 !important;
+ }
+</style>
diff --git a/src/views/safeProduction/safeQualifications/index.vue b/src/views/safeProduction/safeQualifications/index.vue
new file mode 100644
index 0000000..39111be
--- /dev/null
+++ b/src/views/safeProduction/safeQualifications/index.vue
@@ -0,0 +1,893 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm"
+ :inline="true">
+ <el-form-item label="瑙勭▼璧勮川鍚嶇О锛�">
+ <el-input v-model="searchForm.name"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item label="瑙勭▼璧勮川绫诲瀷锛�">
+ <el-select v-model="searchForm.type"
+ placeholder="璇烽�夋嫨"
+ clearable
+ style="width: 200px"
+ prefix-icon="Search"
+ @change="handleQuery">
+ <el-option v-for="item in type_qualification"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瑙勭▼璧勮川缂栧彿锛�">
+ <el-input v-model="searchForm.code"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery" />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary"
+ @click="handleQuery"> 鎼滅储 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">
+ 鏂板瑙勭▼璧勮川
+ </el-button>
+ <!-- <el-button type="primary"
+ plain
+ @click="handleImport">瀵煎叆</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button> -->
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ <!-- <el-button type="primary"
+ plain
+ @click="handlePrint">鎵撳嵃</el-button> -->
+ </div>
+ </div>
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys"
+ :row-key="(row) => row.id"
+ :row-class-name="getRowClass"
+ style="width: 100%"
+ height="calc(100vh - 18.5em)">
+ <el-table-column align="center"
+ type="selection"
+ width="55"
+ fixed="left" />
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="瑙勭▼璧勮川鍚嶇О"
+ prop="name"
+ width="180"
+ show-overflow-tooltip />
+ <el-table-column label="瑙勭▼璧勮川缂栧彿"
+ prop="code"
+ show-overflow-tooltip />
+ <el-table-column label="瑙勭▼璧勮川绫诲瀷"
+ prop="type"
+ show-overflow-tooltip>
+ <template #default="scope">
+ {{ type_qualification.find(item => item.value === scope.row.type)?.label || '-' }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鐗堟湰鍙�"
+ prop="version"
+ width="180"
+ show-overflow-tooltip />
+ <el-table-column label="澶囨敞"
+ prop="remark"
+ show-overflow-tooltip />
+ <el-table-column label="鏈夋晥鏈�"
+ prop="effectiveTime"
+ width="120"
+ show-overflow-tooltip />
+ <el-table-column fixed="right"
+ label="鎿嶄綔"
+ min-width="100"
+ align="center">
+ <template #default="scope">
+ <el-button link
+ type="primary"
+ size="small"
+ @click="openForm('edit', scope.row)">缂栬緫</el-button>
+ <el-button link
+ type="primary"
+ size="small"
+ @click="openFileDialog(scope.row)">闄勪欢</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
+ </div>
+ <FormDialog v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板瑙勭▼璧勮川椤甸潰' : '缂栬緫瑙勭▼璧勮川椤甸潰'"
+ :width="'70%'"
+ :operation-type="operationType"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瑙勭▼璧勮川鍚嶇О锛�"
+ prop="name">
+ <el-input v-model="form.name"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勭▼璧勮川缂栧彿锛�"
+ prop="code">
+ <el-input v-model="form.code"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瑙勭▼璧勮川绫诲瀷锛�"
+ prop="type">
+ <el-select v-model="form.type"
+ placeholder="璇烽�夋嫨"
+ clearable>
+ <el-option v-for="item in type_qualification"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐗堟湰鍙凤細"
+ prop="version">
+ <el-input v-model="form.version"
+ placeholder="璇疯緭鍏�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鏈夋晥鏈燂細"
+ prop="effectiveTime">
+ <el-date-picker style="width: 100%"
+ v-model="form.effectiveTime"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶囨敞路锛�"
+ prop="remark">
+ <el-input v-model="form.remark"
+ placeholder="璇疯緭鍏�"
+ clearable
+ type="textarea"
+ :rows="1"
+ :disabled="operationType === 'view'" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+<!-- todo 闄勪欢棰勮鐩稿叧 -->
+ <FileList v-if="fileDialogVisible" v-model:visible="fileDialogVisible" record-type="safe_certification" :record-id="recordId" />
+ </div>
+</template>
+
+<script setup>
+ import { getToken } from "@/utils/auth";
+ import pagination from "@/components/PIMTable/Pagination.vue";
+ import { onMounted, ref, getCurrentInstance } from "vue";
+ import { ElMessageBox, ElMessage } from "element-plus";
+ import useUserStore from "@/store/modules/user";
+ import { userListNoPage } from "@/api/system/user.js";
+ import FormDialog from "@/components/Dialog/FormDialog.vue";
+ import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
+ import {
+ qualificationsListPage,
+ safeCertificationAdd,
+ safeCertificationUpdate,
+ safeCertificationDel,
+ fileListPage,
+ safeCertificationFileAdd,
+ safeCertificationFileDel,
+ } from "@/api/safeProduction/safeQualifications.js";
+ import useFormData from "@/hooks/useFormData.js";
+ import request from "@/utils/request";
+ import dayjs from "dayjs";
+ const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+ const userStore = useUserStore();
+ const { proxy } = getCurrentInstance();
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const userList = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ });
+ const total = ref(0);
+
+ // 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+ const operationType = ref("");
+ const dialogFormVisible = ref(false);
+ const data = reactive({
+ searchForm: {
+ name: "", // 瑙勭▼璧勮川鍚嶇О
+ code: "", // 瑙勭▼璧勮川缂栧彿
+ type: "", // 瑙勭▼璧勮川绫诲瀷
+ },
+ form: {
+ salesContractNo: "",
+ salesman: "",
+ customerId: "",
+ entryPerson: "",
+ entryDate: "",
+ maintenanceTime: "",
+ executionDate: "",
+ },
+ rules: {
+ code: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ name: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ type: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ effectiveTime: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ version: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ },
+ });
+ // 瑙勭▼璧勮川绫诲瀷閫夐」
+ const { type_qualification } = proxy.useDict("type_qualification");
+ const { form, rules } = toRefs(data);
+ const { form: searchForm } = useFormData(data.searchForm);
+ // 浜у搧琛ㄥ崟寮规鏁版嵁
+ const productFormVisible = ref(false);
+
+ const quotationLoading = ref(false);
+ const quotationList = ref([]);
+ const quotationSearchForm = reactive({
+ quotationNo: "",
+ customer: "",
+ });
+
+ // 瀵煎叆鐩稿叧
+ const importUploadRef = ref(null);
+ const importUpload = reactive({
+ title: "瀵煎叆閿�鍞彴璐�",
+ open: false,
+ url: import.meta.env.VITE_APP_BASE_API + "/sales/ledger/import",
+ headers: { Authorization: "Bearer " + getToken() },
+ isUploading: false,
+ beforeUpload: file => {
+ const isExcel = file.name.endsWith(".xlsx") || file.name.endsWith(".xls");
+ const isLt10M = file.size / 1024 / 1024 < 10;
+ if (!isExcel) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢鍙兘鏄� xlsx/xls 鏍煎紡!");
+ return false;
+ }
+ if (!isLt10M) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!");
+ return false;
+ }
+ return true;
+ },
+ onChange: (file, fileList) => {
+ console.log("鏂囦欢鐘舵�佹敼鍙�", file, fileList);
+ },
+ onProgress: (event, file, fileList) => {
+ console.log("涓婁紶涓�...", event.percent);
+ },
+ onSuccess: (response, file, fileList) => {
+ console.log("涓婁紶鎴愬姛", response, file, fileList);
+ importUpload.isUploading = false;
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess("瀵煎叆鎴愬姛");
+ importUpload.open = false;
+ if (importUploadRef.value) {
+ importUploadRef.value.clearFiles();
+ }
+ getList();
+ } else {
+ proxy.$modal.msgError(response.msg || "瀵煎叆澶辫触");
+ }
+ },
+ onError: (error, file, fileList) => {
+ console.error("涓婁紶澶辫触", error, file, fileList);
+ importUpload.isUploading = false;
+ proxy.$modal.msgError("瀵煎叆澶辫触锛岃閲嶈瘯");
+ },
+ });
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ // 鍙湁鍦ㄧ偣鍑绘悳绱㈡寜閽椂鎵嶉噸缃〉鐮佸埌绗竴椤�
+ // 閬垮厤琛ㄥ崟瀛楁change浜嬩欢骞叉壈鍒嗛〉
+ if (arguments.length === 0) {
+ page.current = 1;
+ }
+ expandedRowKeys.value = [];
+ getList();
+ };
+ const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ const { entryDate, ...rest } = searchForm;
+ // 灏嗚寖鍥存棩鏈熷瓧娈典紶閫掔粰鍚庣
+ const params = { ...rest, ...page };
+ // 绉婚櫎褰曞叆鏃ユ湡鐨勯粯璁ゅ�艰缃紝鍙繚鐣欒寖鍥存棩鏈熷瓧娈�
+ delete params.entryDate;
+ return qualificationsListPage(params)
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ total.value = res.data.total;
+ return res;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+ };
+
+ const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥炶鑺傜偣
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId);
+ if (foundNode) {
+ return foundNode; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝杩斿洖璇ヨ妭鐐�
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ console.log("selection", selectedRows.value);
+ };
+ const expandedRowKeys = ref([]);
+ // 鎵撳紑寮规
+ const openForm = async (type, row) => {
+ operationType.value = type;
+ if (type === "add") {
+ form.value = {
+ salesContractNo: "",
+ salesman: "",
+ customerId: "",
+ entryPerson: "",
+ entryDate: "",
+ maintenanceTime: "",
+ executionDate: "",
+ };
+ } else {
+ // 鍏抽敭锛氱紪杈戞椂涓嶈鐩存帴寮曠敤琛ㄦ牸琛屽璞★紝閬垮厤鍙栨秷/閲嶇疆鏃舵妸鍒楄〃鏁版嵁涓�璧锋竻绌�
+ // 浣跨敤娣辨嫹璐濇柇寮�寮曠敤鍏崇郴
+ form.value = JSON.parse(JSON.stringify(row || {}));
+ }
+ dialogFormVisible.value = true;
+ };
+
+ const fetchQuotationList = async () => {
+ quotationLoading.value = true;
+ try {
+ const params = {
+ // 鍏煎鍚庣鍒嗛〉瀛楁锛氳繖閲屾部鐢ㄦ姤浠烽〉闈㈠凡鏈夊彲鐢ㄧ殑瀛楁鍛藉悕
+ currentPage: 1,
+ pageSize: 100,
+ ...quotationSearchForm,
+ status: "閫氳繃",
+ };
+ const res = await getQuotationList(params);
+ quotationList.value = res?.data?.records || [];
+ } finally {
+ quotationLoading.value = false;
+ }
+ };
+
+ // 鎻愪氦琛ㄥ崟
+ const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (valid) {
+ if (operationType.value === "add") {
+ safeCertificationAdd(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ } else {
+ safeCertificationUpdate(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ }
+ });
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ };
+ // 鍏抽棴浜у搧寮规
+ const closeProductDia = () => {
+ proxy.resetForm("productFormRef");
+ productFormVisible.value = false;
+ };
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.id);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ safeCertificationDel(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+
+ /**
+ * 鍒ゆ柇鏄惁鍙互鍙戣揣
+ * 鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐у拰瀹℃牳鎷掔粷鐨勬椂鍊欐墠鍙互鍙戣揣
+ * @param row 琛屾暟鎹�
+ */
+ const canShip = row => {
+ // 浜у搧鐘舵�佸繀椤绘槸鍏呰冻锛坅pproveStatus === 1锛�
+ if (row.approveStatus !== 1) {
+ return false;
+ }
+
+ // 鑾峰彇鍙戣揣鐘舵��
+ const shippingStatus = row.shippingStatus;
+
+ // 濡傛灉宸插彂璐э紙鏈夊彂璐ф棩鏈熸垨杞︾墝鍙凤級锛屼笉鑳藉啀娆″彂璐�
+ if (row.shippingDate || row.shippingCarNumber) {
+ return false;
+ }
+
+ // 鍙戣揣鐘舵�佸繀椤绘槸"寰呭彂璐�"鎴�"瀹℃牳鎷掔粷"
+ const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
+ return statusStr === "寰呭彂璐�" || statusStr === "瀹℃牳鎷掔粷";
+ };
+
+ /**
+ * 涓嬭浇鏂囦欢
+ *
+ * @param row 涓嬭浇鏂囦欢鐨勭浉鍏充俊鎭璞�
+ */
+ const fileListRef = ref(null);
+ const fileListDialogVisible = ref(false);
+ const currentFileRow = ref(null);
+ const filePagination = ref({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+
+ // 鎵撳紑闄勪欢寮圭獥
+ const recordId =ref(0)
+ const fileDialogVisible = ref(false)
+
+ // 鎵撳紑闄勪欢寮规
+ const openFileDialog = async (row) => {
+ recordId.value = row.id
+ fileDialogVisible.value = true
+ }
+
+ const currentFactoryName = ref("");
+ const getCurrentFactoryName = async () => {
+ let res = await userStore.getInfo();
+ currentFactoryName.value = res.user.currentFactoryName;
+ };
+
+ /**
+ * 鑾峰彇琛岀被鍚嶏紝鐢ㄤ簬鍒ゆ柇褰撳墠鏃ユ湡鏄惁鎺ヨ繎鏈夋晥鏈�15澶╁唴
+ * @param row 琛屾暟鎹�
+ * @returns 绫诲悕
+ */
+ const getRowClass = ({ row }) => {
+ if (!row.effectiveTime) return "";
+
+ const now = new Date();
+ const effectiveTime = new Date(row.effectiveTime);
+ const diffTime = effectiveTime - now;
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+
+ if (diffDays >= 0 && diffDays <= 15) {
+ return "warning-row";
+ }
+
+ return "";
+ };
+
+ onMounted(() => {
+ getList();
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ getCurrentFactoryName();
+ });
+ // 涓婁紶闄勪欢
+ const handleUpload = async () => {
+ if (!currentFileRow.value) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨鏁版嵁");
+ return null;
+ }
+
+ return new Promise(resolve => {
+ // 鍒涘缓涓�涓殣钘忕殑鏂囦欢杈撳叆鍏冪礌
+ const input = document.createElement("input");
+ input.type = "file";
+ input.style.display = "none";
+ input.onchange = async e => {
+ const file = e.target.files[0];
+ if (!file) {
+ resolve(null);
+ return;
+ }
+
+ try {
+ // 浣跨敤 FormData 涓婁紶鏂囦欢
+ const formData = new FormData();
+ formData.append("file", file);
+
+ const uploadRes = await request({
+ url: "/file/upload",
+ method: "post",
+ data: formData,
+ headers: {
+ "Content-Type": "multipart/form-data",
+ Authorization: `Bearer ${getToken()}`,
+ },
+ });
+
+ if (uploadRes.code === 200) {
+ // 淇濆瓨闄勪欢淇℃伅
+ const fileData = {
+ safeCertificationId: currentFileRow.value.id,
+ name: uploadRes.data.originalName || file.name,
+ url: uploadRes.data.tempPath || uploadRes.data.url,
+ };
+
+ const saveRes = await safeCertificationFileAdd(fileData);
+ if (saveRes.code === 200) {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ // 閲嶆柊鍔犺浇鏂囦欢鍒楄〃
+ const listRes = await fileListPage({
+ safeCertificationId: currentFileRow.value.id,
+ current: filePagination.value.current,
+ size: filePagination.value.size,
+ });
+ if (listRes.code === 200 && fileListRef.value) {
+ const fileList = (listRes.data?.records || []).map(item => ({
+ name: item.name,
+ url: item.url,
+ id: item.id,
+ ...item,
+ }));
+ fileListRef.value.setList(fileList);
+ filePagination.value.total = listRes.data?.total || 0;
+ }
+ // 杩斿洖鏂版枃浠朵俊鎭�
+ resolve({
+ name: fileData.name,
+ url: fileData.url,
+ id: saveRes.data?.id,
+ });
+ } else {
+ proxy.$modal.msgError(saveRes.msg || "鏂囦欢淇濆瓨澶辫触");
+ resolve(null);
+ }
+ } else {
+ proxy.$modal.msgError(uploadRes.msg || "鏂囦欢涓婁紶澶辫触");
+ resolve(null);
+ }
+ } catch (error) {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ resolve(null);
+ } finally {
+ document.body.removeChild(input);
+ }
+ };
+
+ document.body.appendChild(input);
+ input.click();
+ });
+ };
+ const paginationSearch = async (page, size) => {
+ filePagination.value.current = page;
+ filePagination.value.size = size;
+ const listRes = await fileListPage({
+ safeCertificationId: currentFileRow.value.id,
+ current: filePagination.value.current,
+ size: filePagination.value.size,
+ });
+ if (listRes.code === 200) {
+ const fileList = (listRes.data?.records || []).map(item => ({
+ name: item.name,
+ url: item.url,
+ id: item.id,
+ ...item,
+ }));
+
+ fileListRef.value.setList(fileList);
+ filePagination.value.total = listRes.data?.total || 0;
+ }
+ };
+ // 鍒犻櫎闄勪欢
+ const handleFileDelete = async row => {
+ try {
+ const res = await safeCertificationFileDel([row.id]);
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ // 閲嶆柊鍔犺浇鏂囦欢鍒楄〃
+ if (currentFileRow.value && fileListRef.value) {
+ const listRes = await fileListPage({
+ safeCertificationId: currentFileRow.value.id,
+ current: filePagination.value.current,
+ size: filePagination.value.size,
+ });
+ if (listRes.code === 200) {
+ const fileList = (listRes.data?.records || []).map(item => ({
+ name: item.name,
+ url: item.url,
+ id: item.id,
+ ...item,
+ }));
+ fileListRef.value.setList(fileList);
+ filePagination.value.total = listRes.data?.total || 0;
+ }
+ }
+ return true; // 杩斿洖 true 琛ㄧず鍒犻櫎鎴愬姛锛岀粍浠朵細鏇存柊鍒楄〃
+ } else {
+ proxy.$modal.msgError(res.msg || "鍒犻櫎澶辫触");
+ return false;
+ }
+ } catch (error) {
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ return false;
+ }
+ };
+</script>
+
+<style scoped lang="scss">
+ .ml-10 {
+ margin-left: 10px;
+ }
+
+ .table_list {
+ margin-top: unset;
+ }
+
+ :deep(.warning-row) {
+ background-color: #fef0f0 !important;
+ }
+
+ :deep(.warning-row td) {
+ // color: #cf1322 !important;
+ }
+
+ .actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+ }
+ .print-preview-dialog {
+ .el-dialog__body {
+ padding: 0;
+ max-height: 80vh;
+ overflow-y: auto;
+ }
+ }
+
+ .print-preview-container {
+ .print-preview-header {
+ padding: 15px;
+ border-bottom: 1px solid #e4e7ed;
+ text-align: center;
+
+ .el-button {
+ margin: 0 10px;
+ }
+ }
+
+ .print-preview-content {
+ padding: 20px;
+ background-color: #f5f5f5;
+ min-height: 400px;
+ }
+ }
+
+ .print-page {
+ width: 220mm;
+ height: 90mm;
+ padding: 10mm;
+ margin: 0 auto;
+ background: white;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ margin-bottom: 10px;
+ box-sizing: border-box;
+ }
+
+ .delivery-note {
+ width: 100%;
+ height: 100%;
+ font-family: "SimSun", serif;
+ font-size: 10px;
+ line-height: 1.2;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .header {
+ text-align: center;
+ margin-bottom: 8px;
+
+ .company-name {
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+
+ .document-title {
+ font-size: 16px;
+ font-weight: bold;
+ }
+ }
+
+ .info-section {
+ margin-bottom: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .info-row {
+ line-height: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 60px;
+ font-size: 14px;
+ }
+
+ .value {
+ margin-right: 20px;
+ min-width: 80px;
+ font-size: 14px;
+ }
+ }
+ }
+
+ .table-section {
+ margin-bottom: 4px;
+ flex: 1;
+
+ .product-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid #000;
+
+ th,
+ td {
+ border: 1px solid #000;
+ padding: 6px;
+ text-align: center;
+ font-size: 14px;
+ line-height: 1.4;
+ }
+
+ th {
+ font-weight: bold;
+ }
+
+ .total-label {
+ text-align: right;
+ font-weight: bold;
+ }
+
+ .total-value {
+ font-weight: bold;
+ }
+ }
+ }
+
+ .footer-section {
+ .footer-row {
+ display: flex;
+ margin-bottom: 3px;
+ line-height: 20px;
+ justify-content: space-between;
+
+ .footer-item {
+ display: flex;
+ margin-right: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 80px;
+ font-size: 14px;
+ }
+
+ .value {
+ min-width: 80px;
+ font-size: 14px;
+ }
+
+ &.address-item {
+ .address-value {
+ min-width: 200px;
+ }
+ }
+ }
+ }
+ }
+
+ @media print {
+ .app-container {
+ display: none;
+ }
+
+ .print-page {
+ box-shadow: none;
+ margin: 0;
+ padding: 10mm;
+ padding-left: 20mm;
+ page-break-inside: avoid;
+ page-break-after: always;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+ }
+</style>
diff --git a/src/views/safeProduction/safeWorkApproval/components/approvalDia.vue b/src/views/safeProduction/safeWorkApproval/components/approvalDia.vue
new file mode 100644
index 0000000..8d40a2e
--- /dev/null
+++ b/src/views/safeProduction/safeWorkApproval/components/approvalDia.vue
@@ -0,0 +1,530 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible"
+ :title="operationType === 'approval' ? '瀹℃壒' : '璇︽儏'"
+ width="700px"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ ref="formRef">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="娴佺▼缂栧彿锛�"
+ prop="approveId">
+ <el-input v-model="form.approveId"
+ placeholder="鑷姩缂栧彿"
+ clearable
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鐢宠閮ㄩ棬锛�">
+ <el-select disabled
+ v-model="form.approveDeptId"
+ placeholder="閫夋嫨閮ㄩ棬">
+ <el-option v-for="user in productOptions"
+ :key="user.deptId"
+ :label="user.deptName"
+ :value="user.deptId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="!isQuotationApproval && !isPurchaseApproval">
+ <el-col :span="24">
+ <el-form-item :label="props.approveType == 5 ? '閲囪喘鍚堝悓鍙凤細' : '瀹℃壒浜嬬敱锛�'"
+ prop="approveReason">
+ <el-input v-model="form.approveReason"
+ placeholder="璇疯緭鍏�"
+ clearable
+ type="textarea"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀹℃壒浜洪�夋嫨锛堝姩鎬佽妭鐐癸級 -->
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鐢宠浜猴細"
+ prop="approveUser">
+ <el-select v-model="form.approveUser"
+ placeholder="閫夋嫨浜哄憳"
+ disabled>
+ <el-option v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢宠鏃ユ湡锛�"
+ prop="approveTime">
+ <el-date-picker v-model="form.approveTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%"
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <!-- 鎶ヤ环瀹℃壒锛氬睍绀烘姤浠疯鎯咃紙澶嶇敤閿�鍞姤浠�"鏌ョ湅璇︽儏瀵硅瘽妗�"鍐呭缁撴瀯锛� -->
+ <div v-if="isQuotationApproval"
+ style="margin: 10px 0 18px;">
+ <el-divider content-position="left">鎶ヤ环璇︽儏</el-divider>
+ <el-skeleton :loading="quotationLoading"
+ animated>
+ <template #template>
+ <el-skeleton-item variant="h3"
+ style="width: 30%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ </template>
+ <template #default>
+ <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo"
+ description="鏈煡璇㈠埌瀵瑰簲鎶ヤ环璇︽儏" />
+ <template v-else>
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="鎶ヤ环鍗曞彿">{{ currentQuotation.quotationNo }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ currentQuotation.customer }}</el-descriptions-item>
+ <el-descriptions-item label="涓氬姟鍛�">{{ currentQuotation.salesperson }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ヤ环鏃ユ湡">{{ currentQuotation.quotationDate }}</el-descriptions-item>
+ <el-descriptions-item label="鏈夋晥鏈熻嚦">{{ currentQuotation.validDate }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ヤ环鎬婚"
+ :span="2">
+ <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }}
+ </span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="currentQuotation.products || []"
+ border
+ style="width: 100%">
+ <el-table-column prop="product"
+ label="浜у搧鍚嶇О" />
+ <el-table-column prop="specification"
+ label="瑙勬牸鍨嬪彿" />
+ <el-table-column prop="unit"
+ label="鍗曚綅" />
+ <el-table-column prop="unitPrice"
+ label="鍗曚环">
+ <template #default="scope">楼{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <div v-if="currentQuotation.remark"
+ style="margin-top: 20px;">
+ <h4>澶囨敞</h4>
+ <p>{{ currentQuotation.remark }}</p>
+ </div>
+ </template>
+ </template>
+ </el-skeleton>
+ </div>
+ <!-- 閲囪喘瀹℃壒锛氬睍绀洪噰璐鎯� -->
+ <div v-if="isPurchaseApproval"
+ style="margin: 10px 0 18px;">
+ <el-divider content-position="left">閲囪喘璇︽儏</el-divider>
+ <el-skeleton :loading="purchaseLoading"
+ animated>
+ <template #template>
+ <el-skeleton-item variant="h3"
+ style="width: 30%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ <el-skeleton-item variant="text"
+ style="width: 100%" />
+ </template>
+ <template #default>
+ <el-empty v-if="!currentPurchase || !currentPurchase.purchaseContractNumber"
+ description="鏈煡璇㈠埌瀵瑰簲閲囪喘璇︽儏" />
+ <template v-else>
+ <el-descriptions :column="2"
+ border>
+ <el-descriptions-item label="閲囪喘鍚堝悓鍙�">{{ currentPurchase.purchaseContractNumber }}</el-descriptions-item>
+ <el-descriptions-item label="渚涘簲鍟嗗悕绉�">{{ currentPurchase.supplierName }}</el-descriptions-item>
+ <el-descriptions-item label="椤圭洰鍚嶇О">{{ currentPurchase.projectName }}</el-descriptions-item>
+ <el-descriptions-item label="閿�鍞悎鍚屽彿">{{ currentPurchase.salesContractNo }}</el-descriptions-item>
+ <el-descriptions-item label="绛捐鏃ユ湡">{{ currentPurchase.executionDate }}</el-descriptions-item>
+ <el-descriptions-item label="褰曞叆鏃ユ湡">{{ currentPurchase.entryDate }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡">{{ currentPurchase.paymentMethod }}</el-descriptions-item>
+ <el-descriptions-item label="鍚堝悓閲戦"
+ :span="2">
+ <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">
+ 楼{{ Number(currentPurchase.contractAmount ?? 0).toFixed(2) }}
+ </span>
+ </el-descriptions-item>
+ </el-descriptions>
+ <div style="margin-top: 20px;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="currentPurchase.productData || []"
+ border
+ style="width: 100%">
+ <el-table-column prop="productCategory"
+ label="浜у搧鍚嶇О" />
+ <el-table-column prop="specificationModel"
+ label="瑙勬牸鍨嬪彿" />
+ <el-table-column prop="unit"
+ label="鍗曚綅" />
+ <el-table-column prop="quantity"
+ label="鏁伴噺" />
+ <el-table-column prop="taxInclusiveUnitPrice"
+ label="鍚◣鍗曚环">
+ <template #default="scope">楼{{ Number(scope.row.taxInclusiveUnitPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ <el-table-column prop="taxInclusiveTotalPrice"
+ label="鍚◣鎬讳环">
+ <template #default="scope">楼{{ Number(scope.row.taxInclusiveTotalPrice ?? 0).toFixed(2) }}</template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </template>
+ </template>
+ </el-skeleton>
+ </div>
+ <el-form :model="{ activities }"
+ ref="formRef"
+ label-position="top">
+ <el-steps :active="getActiveStep()"
+ finish-status="success"
+ process-status="process"
+ align-center
+ direction="vertical">
+ <el-step v-for="(activity, index) in activities"
+ :key="index"
+ finish-status="success"
+ :title="getNodeTitle(index, activities.length)"
+ :description="activity.approveNodeUser"
+ :icon="getNodeIcon(activity, index)">
+ <template #icon>
+ <el-icon v-if="activity.approveNodeStatus === 2"
+ color="red"
+ :size="22">
+ <WarningFilled />
+ </el-icon>
+ <el-icon v-else-if="activity.isShen"
+ color="#1890ff"
+ :size="22">
+ <Edit />
+ </el-icon>
+ <el-icon v-else-if="activity.approveNodeStatus === 1"
+ color="#67C23A"
+ :size="26">
+ <Check />
+ </el-icon>
+ <el-icon v-else
+ color="#C0C4CC"
+ :size="22">
+ <MoreFilled />
+ </el-icon>
+ </template>
+ <template #title>
+ <span style="color: #000000">{{ getNodeTitle(index, activities.length) }}</span>
+ </template>
+ <template #description>
+ <div class="node-user">
+ <div class="avatar-wrapper">
+ <img :src="userStore.avatar"
+ class="user-avatar"
+ alt="" />
+ </div>
+ <span style="color: #000000">{{ activity.approveNodeUser }}-{{activity.isApproval}}</span>
+ </div>
+ <div v-if="!activity.isShen"
+ class="node-reason">
+ <span>瀹℃壒鎰忚锛�</span>{{ activity.approveNodeReason }}
+ </div>
+ <div v-if="!activity.isShen"
+ class="node-reason">
+ <span>绛惧悕锛�</span>
+ <img :src="activity.urlTem"
+ class="signImg"
+ alt=""
+ v-if="activity.urlTem" />
+ </div>
+ <div v-else-if="activity.isShen">
+ <el-form-item :prop="'activities.' + index + '.approveNodeReason'"
+ :rules="[{ required: true, message: '瀹℃壒鎰忚涓嶈兘涓虹┖', trigger: 'blur' }]">
+ <el-input v-model="activity.approveNodeReason"
+ clearable
+ type="textarea"
+ :disabled="operationType === 'view'"></el-input>
+ </el-form-item>
+ </div>
+ </template>
+ </el-step>
+ </el-steps>
+ </el-form>
+ <template #footer
+ v-if="operationType === 'approval'">
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm(2)">涓嶉�氳繃</el-button>
+ <el-button type="primary"
+ @click="submitForm(1)">閫氳繃</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import {
+ computed,
+ getCurrentInstance,
+ nextTick,
+ reactive,
+ ref,
+ toRefs,
+ } from "vue";
+ import {
+ approveProcessDetails,
+ getDept,
+ updateApproveNode,
+ } from "@/api/collaborativeApproval/approvalProcess.js";
+ import useUserStore from "@/store/modules/user.js";
+ import { userListNoPageByTenantId } from "@/api/system/user.js";
+ import {
+ WarningFilled,
+ Edit,
+ Check,
+ MoreFilled,
+ } from "@element-plus/icons-vue";
+ import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
+ import { getPurchaseByCode } from "@/api/procurementManagement/procurementLedger.js";
+ const emit = defineEmits(["close"]);
+ const { proxy } = getCurrentInstance();
+
+ const props = defineProps({
+ approveType: {
+ type: [Number, String],
+ default: 0,
+ },
+ });
+
+ const dialogFormVisible = ref(false);
+ const operationType = ref("");
+ const activities = ref([]);
+ const formRef = ref(null);
+ const userStore = useUserStore();
+ const productOptions = ref([]);
+ const userList = ref([]);
+ const quotationLoading = ref(false);
+ const currentQuotation = ref({});
+ const purchaseLoading = ref(false);
+ const currentPurchase = ref({});
+ const isQuotationApproval = computed(() => Number(props.approveType) === 6);
+ const isPurchaseApproval = computed(() => Number(props.approveType) === 5);
+
+ const data = reactive({
+ form: {
+ approveTime: "",
+ approveId: "",
+ approveUser: "",
+ approveDeptId: "",
+ approveReason: "",
+ checkResult: "",
+ },
+ });
+ const { form } = toRefs(data);
+
+ // 鑺傜偣鏍囬
+ const getNodeTitle = (index, len) => {
+ if (index === len - 1) return "缁撴潫";
+ return "瀹℃壒";
+ };
+
+ // 鑾峰彇褰撳墠婵�娲绘楠�
+ const getActiveStep = () => {
+ // 濡傛灉鎵�鏈� isShen 閮戒负 false锛岃繑鍥炴渶鍚庝竴涓楠わ紙鍏ㄩ儴瀹屾垚锛�
+ const hasActive = activities.value.some(a => a.isShen === true);
+ if (!hasActive) return activities.value.length;
+ // 褰撳墠鑺傜偣绱㈠紩
+ return activities.value.findIndex(a => a.isShen == true);
+ };
+ // 姝ラicon
+ const getNodeIcon = (activity, index) => {
+ if (activity.approveNodeStatus === 2) return "el-icon-warning"; // 涓嶉�氳繃
+ if (activity.isShen) return "Edit";
+ return "";
+ };
+
+ // 鎵撳紑寮规
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ currentQuotation.value = {};
+ currentPurchase.value = {};
+ userListNoPageByTenantId().then(res => {
+ userList.value = res.data;
+ });
+ form.value = { ...row };
+ // 绔嬪嵆娓呴櫎琛ㄥ崟楠岃瘉鐘舵�侊紙鍥犱负瀛楁鏄痙isabled鐨勶紝涓嶉渶瑕侀獙璇侊級
+ nextTick(() => {
+ if (formRef.value) {
+ formRef.value.clearValidate();
+ }
+ });
+ // 纭繚閫夐」鍔犺浇瀹屾垚鍚庡啀鍖归厤鍊肩被鍨�
+ getProductOptions().then(() => {
+ // 纭繚鍊肩被鍨嬪尮閰嶏紙濡傛灉閫夐」宸插姞杞斤級
+ if (productOptions.value.length > 0 && form.value.approveDeptId) {
+ const matchedOption = productOptions.value.find(
+ opt =>
+ opt.deptId == form.value.approveDeptId ||
+ String(opt.deptId) === String(form.value.approveDeptId)
+ );
+ if (matchedOption) {
+ form.value.approveDeptId = matchedOption.deptId;
+ }
+ }
+ // 鍐嶆娓呴櫎楠岃瘉锛岀‘淇濋�夐」鍔犺浇鍚庡�煎尮閰嶆纭�
+ nextTick(() => {
+ if (formRef.value) {
+ formRef.value.clearValidate();
+ }
+ });
+ });
+
+ // 鎶ヤ环瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"鎶ヤ环鍗曞彿"鍘绘煡鎶ヤ环鍒楄〃
+ if (isQuotationApproval.value) {
+ const quotationNo = row?.approveReason;
+ if (quotationNo) {
+ quotationLoading.value = true;
+ getQuotationList({ quotationNo })
+ .then(res => {
+ const records = res?.data?.records || [];
+ currentQuotation.value = records[0] || {};
+ })
+ .finally(() => {
+ quotationLoading.value = false;
+ });
+ }
+ }
+
+ // 閲囪喘瀹℃壒锛氱敤瀹℃壒浜嬬敱瀛楁鎵胯浇鐨�"閲囪喘鍚堝悓鍙�"鍘绘煡閲囪喘璇︽儏
+ if (isPurchaseApproval.value) {
+ const purchaseContractNumber = row?.approveReason;
+ if (purchaseContractNumber) {
+ purchaseLoading.value = true;
+ getPurchaseByCode({ purchaseContractNumber })
+ .then(res => {
+ currentPurchase.value = res;
+ })
+ .catch(err => {
+ console.error("鏌ヨ閲囪喘璇︽儏澶辫触:", err);
+ proxy.$modal.msgError("鏌ヨ閲囪喘璇︽儏澶辫触");
+ })
+ .finally(() => {
+ purchaseLoading.value = false;
+ });
+ }
+ }
+
+ approveProcessDetails(row.approveId).then(res => {
+ activities.value = res.data;
+ // 澧炲姞isApproval瀛楁
+ activities.value.forEach(item => {
+ if (item.url && item.url.includes("word")) {
+ item.urlTem = item.url.replaceAll("word", "img");
+ } else {
+ item.urlTem = item.url;
+ }
+ if (item.approveNodeStatus === 2) {
+ item.isApproval = "宸查┏鍥�";
+ } else if (item.approveNodeStatus === 1) {
+ item.isApproval = "宸插悓鎰�";
+ } else {
+ item.isApproval = "鏈鎵�";
+ }
+ });
+ });
+ };
+ const getProductOptions = () => {
+ return getDept().then(res => {
+ productOptions.value = res.data;
+ });
+ };
+ // 鎻愪氦瀹℃壒
+ const submitForm = status => {
+ const filteredActivities = activities.value.filter(
+ activity => activity.isShen
+ );
+ if (!filteredActivities || filteredActivities.length === 0) {
+ proxy.$modal.msgError("鏈壘鍒板緟瀹℃壒鐨勮妭鐐�");
+ return;
+ }
+ const currentActivity = filteredActivities[0];
+ if (!currentActivity) {
+ proxy.$modal.msgError("鏈壘鍒板緟瀹℃壒鐨勮妭鐐�");
+ return;
+ }
+ currentActivity.approveNodeStatus = status;
+ // 鍒ゆ柇鏄惁涓烘渶鍚庝竴姝�
+ const isLast =
+ activities.value.findIndex(a => a.isShen) === activities.value.length - 1;
+ updateApproveNode({ ...currentActivity, isLast }).then(() => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ quotationLoading.value = false;
+ currentQuotation.value = {};
+ purchaseLoading.value = false;
+ currentPurchase.value = {};
+ emit("close");
+ };
+ defineExpose({
+ openDialog,
+ });
+</script>
+
+<style scoped>
+ .node-user {
+ margin: 10px 0;
+ font-size: 16px;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ }
+ .node-status {
+ color: #1890ff;
+ margin-left: 8px;
+ font-size: 14px;
+ }
+ .node-reason {
+ font-size: 15px;
+ color: #333;
+ margin: 10px 0;
+ }
+ .user-avatar {
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ border-radius: 50px;
+ }
+ .signImg {
+ cursor: pointer;
+ width: 200px;
+ height: 60px;
+ }
+</style>
\ No newline at end of file
diff --git a/src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue b/src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue
new file mode 100644
index 0000000..86acdf8
--- /dev/null
+++ b/src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue
@@ -0,0 +1,422 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板瀹℃壒娴佺▼' : '缂栬緫瀹℃壒娴佺▼'"
+ width="50%"
+ @close="closeDia">
+ <el-form :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="娴佺▼缂栧彿锛�"
+ prop="approveId">
+ <el-input v-model="form.approveId"
+ placeholder="鑷姩缂栧彿"
+ clearable
+ disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="鐢宠閮ㄩ棬锛�"
+ prop="approveDeptName">
+ <!-- <el-input v-model="form.approveDeptName" placeholder="璇疯緭鍏�" clearable/>-->
+ <el-select v-model="form.approveDeptId"
+ placeholder="閫夋嫨閮ㄩ棬"
+ @change="handleDeptChange">
+ <el-option v-for="user in productOptions"
+ :key="user.deptId"
+ :label="user.deptName"
+ :value="user.deptId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item :label="props.approveType == 5 ? '閲囪喘鍚堝悓鍙凤細' : '瀹℃壒浜嬬敱锛�'"
+ prop="approveReason">
+ <el-input v-model="form.approveReason"
+ placeholder="璇疯緭鍏�"
+ clearable
+ type="textarea" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 璇峰亣鏃堕棿锛堜粎褰� approveType 涓� 2 鏃舵樉绀猴級 -->
+ <el-row :gutter="30"
+ v-if="props.approveType == 2">
+ <el-col :span="12">
+ <el-form-item label="璇峰亣寮�濮嬫椂闂达細"
+ prop="startDate">
+ <el-date-picker v-model="form.startDate"
+ type="date"
+ placeholder="璇烽�夋嫨寮�濮嬫棩鏈�"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇峰亣缁撴潫鏃堕棿锛�"
+ prop="endDate">
+ <el-date-picker v-model="form.endDate"
+ type="date"
+ placeholder="璇烽�夋嫨缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 鎶ラ攢閲戦锛堜粎褰� approveType 涓� 4 鏃舵樉绀猴級 -->
+ <el-row v-if="props.approveType == 4">
+ <el-col :span="24">
+ <el-form-item label="鎶ラ攢閲戦锛�"
+ prop="price">
+ <el-input-number v-model="form.price"
+ placeholder="璇疯緭鍏ユ姤閿�閲戦"
+ :min="0"
+ :precision="2"
+ :step="0.01"
+ style="width: 100%"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 鍑哄樊鍦扮偣锛堜粎褰� approveType 涓� 3 鏃舵樉绀猴級 -->
+ <el-row v-if="props.approveType == 3">
+ <el-col :span="24">
+ <el-form-item label="鍑哄樊鍦扮偣锛�"
+ prop="location">
+ <el-input v-model="form.location"
+ placeholder="璇疯緭鍏ュ嚭宸湴鐐�"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <!-- 瀹℃壒浜洪�夋嫨锛堝姩鎬佽妭鐐癸級 -->
+ <el-row>
+ <el-col :span="24">
+ <el-form-item>
+ <template #label>
+ <span>瀹℃壒浜洪�夋嫨锛�</span>
+ <el-button type="primary"
+ @click="addApproverNode"
+ style="margin-left: 8px;">鏂板鑺傜偣</el-button>
+ </template>
+ <div style="display: flex; align-items: flex-end; flex-wrap: wrap;">
+ <div v-for="(node, index) in approverNodes"
+ :key="node.id"
+ style="margin-right: 30px; text-align: center; margin-bottom: 10px;">
+ <div>
+ <span>瀹℃壒浜�</span>
+ 鈫�
+ </div>
+ <el-select v-model="node.userId"
+ placeholder="閫夋嫨浜哄憳"
+ style="width: 120px; margin-bottom: 8px;">
+ <el-option v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId" />
+ </el-select>
+ <div>
+ <el-button type="danger"
+ size="small"
+ @click="removeApproverNode(index)"
+ v-if="approverNodes.length > 1">鍒犻櫎</el-button>
+ </div>
+ </div>
+ </div>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鐢宠浜猴細"
+ prop="approveUser">
+ <el-select v-model="form.approveUser"
+ placeholder="閫夋嫨浜哄憳"
+ filterable
+ default-first-option
+ :reserve-keyword="false">
+ <el-option v-for="user in userList"
+ :key="user.userId"
+ :label="user.nickName"
+ :value="user.userId" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐢宠鏃ユ湡锛�"
+ prop="approveTime">
+ <el-date-picker v-model="form.approveTime"
+ type="date"
+ placeholder="璇烽�夋嫨鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ clearable
+ style="width: 100%" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢鏉愭枡锛�"
+ prop="remark">
+ <FileUpload v-model:file-list="fileList" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+ import { ref, reactive, toRefs, getCurrentInstance } from "vue";
+ import {
+ approveProcessAdd,
+ approveProcessGetInfo,
+ approveProcessUpdate,
+ getDept,
+ } from "@/api/collaborativeApproval/approvalProcess.js";
+ import { delLedgerFile } from "@/api/salesManagement/salesLedger.js";
+ import { userListNoPageByTenantId } from "@/api/system/user.js";
+ import { getToken } from "@/utils/auth";
+ const { proxy } = getCurrentInstance();
+ const emit = defineEmits(["close"]);
+ import useUserStore from "@/store/modules/user";
+ import { getCurrentDate } from "@/utils/index.js";
+ import log from "@/views/monitor/job/log.vue";
+ import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+ const userStore = useUserStore();
+
+ const dialogFormVisible = ref(false);
+ const operationType = ref("");
+ const fileList = ref([]);
+ const data = reactive({
+ form: {
+ approveTime: "",
+ approveId: "",
+ approveUser: "",
+ approveDeptId: "",
+ approveDeptName: "",
+ approveReason: "",
+ checkResult: "",
+ storageBlobDTOS: [],
+ approverList: [], // 鏂板瀛楁锛屽瓨鍌ㄦ墍鏈夎妭鐐圭殑瀹℃壒浜篿d
+ startDate: "", // 璇峰亣寮�濮嬫椂闂�
+ endDate: "", // 璇峰亣缁撴潫鏃堕棿
+ price: null, // 鎶ラ攢閲戦
+ location: "", // 鍑哄樊鍦扮偣
+ },
+ rules: {
+ approveTime: [{ required: false, message: "璇疯緭鍏�", trigger: "change" }],
+ approveId: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ approveUser: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ approveDeptName: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ approveReason: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ checkResult: [{ required: false, message: "璇疯緭鍏�", trigger: "blur" }],
+ startDate: [
+ { required: true, message: "璇烽�夋嫨璇峰亣寮�濮嬫椂闂�", trigger: "change" },
+ ],
+ endDate: [
+ { required: true, message: "璇烽�夋嫨璇峰亣缁撴潫鏃堕棿", trigger: "change" },
+ ],
+ price: [{ required: true, message: "璇疯緭鍏ユ姤閿�閲戦", trigger: "blur" }],
+ location: [{ required: true, message: "璇疯緭鍏ュ嚭宸湴鐐�", trigger: "blur" }],
+ },
+ });
+ const { form, rules } = toRefs(data);
+ const productOptions = ref([]);
+ const currentApproveStatus = ref(0);
+ const props = defineProps({
+ approveType: {
+ type: [Number, String],
+ default: 0,
+ },
+ });
+
+ // 瀹℃壒浜鸿妭鐐圭浉鍏�
+ const approverNodes = ref([{ id: 1, userId: null }]);
+ let nextApproverId = 2;
+ const userList = ref([]);
+ function addApproverNode() {
+ approverNodes.value.push({ id: nextApproverId++, userId: null });
+ }
+ function removeApproverNode(index) {
+ approverNodes.value.splice(index, 1);
+ }
+ // 澶勭悊閮ㄩ棬閫夋嫨鍙樺寲
+ const handleDeptChange = deptId => {
+ if (deptId) {
+ const selectedDept = productOptions.value.find(
+ dept => dept.deptId === deptId
+ );
+ if (selectedDept) {
+ form.value.approveDeptName = selectedDept.deptName;
+ }
+ } else {
+ form.value.approveDeptName = "";
+ }
+ };
+ // 鎵撳紑寮规
+ const openDialog = (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ userListNoPageByTenantId().then(res => {
+ userList.value = res.data;
+ });
+ form.value = {};
+ approverNodes.value = [{ id: 1, userId: null }];
+ form.value.approveUser = userStore.id;
+ form.value.approveTime = getCurrentDate();
+
+ // 鑾峰彇褰撳墠鐢ㄦ埛淇℃伅骞惰缃儴闂↖D
+ form.value.approveDeptId = userStore.currentDeptId;
+
+ // 鍔犺浇閮ㄩ棬閫夐」锛屽苟鍦ㄥ姞杞藉畬鎴愬悗璁剧疆閮ㄩ棬鍚嶇О
+ getProductOptions();
+ if (operationType.value === "edit") {
+ fileList.value = row.storageBlobVOs;
+ currentApproveStatus.value = row.approveStatus;
+ approveProcessGetInfo({ id: row.approveId, approveReason: "1" }).then(
+ res => {
+ form.value = { ...res.data };
+ form.value.storageBlobDTOS = res.data.storageBlobVOS;
+ // 鍙嶆樉瀹℃壒浜�
+ if (res.data && res.data.approveUserIds) {
+ const userIds = res.data.approveUserIds.split(",");
+ approverNodes.value = userIds.map((userId, idx) => ({
+ id: idx + 1,
+ userId: parseInt(userId.trim()),
+ }));
+ nextApproverId = userIds.length + 1;
+ } else {
+ approverNodes.value = [{ id: 1, userId: null }];
+ nextApproverId = 2;
+ }
+ }
+ );
+ }
+ };
+ const getProductOptions = () => {
+ return getDept().then(res => {
+ productOptions.value = res.data;
+ // 濡傛灉宸叉湁閮ㄩ棬ID锛岃嚜鍔ㄨ缃儴闂ㄥ悕绉帮紙鐢ㄤ簬楠岃瘉锛�
+ if (form.value.approveDeptId && productOptions.value.length > 0) {
+ const matchedDept = productOptions.value.find(
+ dept =>
+ dept.deptId == form.value.approveDeptId ||
+ String(dept.deptId) === String(form.value.approveDeptId)
+ );
+ if (matchedDept) {
+ form.value.approveDeptName = matchedDept.deptName;
+ }
+ }
+ });
+ };
+ function convertIdToValue(data) {
+ return data.map(item => {
+ const { id, children, ...rest } = item;
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+
+ return newItem;
+ });
+ }
+ // 鎻愪氦浜у搧琛ㄥ崟
+ const submitForm = () => {
+ // 鏀堕泦鎵�鏈夎妭鐐圭殑瀹℃壒浜篿d
+ form.value.approveUserIds = approverNodes.value
+ .map(node => node.userId)
+ .join(",");
+ form.value.approveType = props.approveType;
+ // 瀹℃壒浜哄繀濉牎楠�
+ const hasEmptyApprover = approverNodes.value.some(node => !node.userId);
+ if (hasEmptyApprover) {
+ proxy.$modal.msgError("璇蜂负鎵�鏈夊鎵硅妭鐐归�夋嫨瀹℃壒浜猴紒");
+ return;
+ }
+ // 褰� approveType 涓� 2 鏃讹紝鏍¢獙璇峰亣鏃堕棿
+ if (props.approveType == 2) {
+ if (!form.value.startDate) {
+ proxy.$modal.msgError("璇烽�夋嫨璇峰亣寮�濮嬫椂闂达紒");
+ return;
+ }
+ if (!form.value.endDate) {
+ proxy.$modal.msgError("璇烽�夋嫨璇峰亣缁撴潫鏃堕棿锛�");
+ return;
+ }
+ // 鏍¢獙缁撴潫鏃堕棿涓嶈兘鏃╀簬寮�濮嬫椂闂�
+ if (new Date(form.value.endDate) < new Date(form.value.startDate)) {
+ proxy.$modal.msgError("璇峰亣缁撴潫鏃堕棿涓嶈兘鏃╀簬寮�濮嬫椂闂达紒");
+ return;
+ }
+ }
+ // 褰� approveType 涓� 3 鏃讹紝鏍¢獙鍑哄樊鍦扮偣
+ if (props.approveType == 3) {
+ if (!form.value.location || form.value.location.trim() === "") {
+ proxy.$modal.msgError("璇疯緭鍏ュ嚭宸湴鐐癸紒");
+ return;
+ }
+ }
+ // 褰� approveType 涓� 4 鏃讹紝鏍¢獙鎶ラ攢閲戦
+ if (props.approveType == 4) {
+ if (!form.value.price || form.value.price <= 0) {
+ proxy.$modal.msgError("璇疯緭鍏ユ湁鏁堢殑鎶ラ攢閲戦锛�");
+ return;
+ }
+ }
+ form.value.storageBlobDTOS = fileList.value;
+ proxy.$refs.formRef.validate(valid => {
+ if (valid) {
+ if (operationType.value === "add" || currentApproveStatus.value == 3) {
+ approveProcessAdd(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ } else {
+ approveProcessUpdate(form.value).then(res => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ });
+ }
+ }
+ });
+ };
+ // 鍏抽棴寮规
+ const closeDia = () => {
+ fileList.value = [];
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit("close");
+ };
+
+ defineExpose({
+ openDialog,
+ });
+</script>
+
+<style scoped>
+</style>
\ No newline at end of file
diff --git a/src/views/safeProduction/safeWorkApproval/fileList.vue b/src/views/safeProduction/safeWorkApproval/fileList.vue
new file mode 100644
index 0000000..c3b7597
--- /dev/null
+++ b/src/views/safeProduction/safeWorkApproval/fileList.vue
@@ -0,0 +1,67 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="闄勪欢" width="40%" :before-close="handleClose" draggable>
+ <el-table :data="tableData" border height="40vh">
+ <el-table-column label="闄勪欢鍚嶇О" prop="name" min-width="400" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+ <template #default="scope">
+ <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">涓嬭浇</el-button>
+ <el-button link type="primary" size="small" @click="lookFile(scope.row)">棰勮</el-button>
+ <el-button link type="danger" size="small" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import filePreview from '@/components/filePreview/index.vue'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { delCommonFile } from '@/api/publicApi/commonFile.js'
+
+const dialogVisible = ref(false)
+const tableData = ref([])
+const { proxy } = getCurrentInstance();
+const filePreviewRef = ref()
+const handleClose = () => {
+ dialogVisible.value = false
+}
+const open = (list) => {
+ dialogVisible.value = true
+ tableData.value = list
+}
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+
+}
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+// 鍒犻櫎闄勪欢
+const handleDelete = (row) => {
+ ElMessageBox.confirm(`纭鍒犻櫎闄勪欢"${row.name}"鍚楋紵`, '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ delCommonFile([row.id]).then(() => {
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ // 浠庡垪琛ㄤ腑绉婚櫎宸插垹闄ょ殑闄勪欢
+ const index = tableData.value.findIndex(item => item.id === row.id)
+ if (index !== -1) {
+ tableData.value.splice(index, 1)
+ }
+ }).catch(() => {
+ ElMessage.error('鍒犻櫎澶辫触')
+ })
+ }).catch(() => {
+ ElMessage.info('宸插彇娑堝垹闄�')
+ })
+}
+defineExpose({
+ open
+})
+</script>
+
+<style></style>
\ No newline at end of file
diff --git a/src/views/safeProduction/safeWorkApproval/index.vue b/src/views/safeProduction/safeWorkApproval/index.vue
new file mode 100644
index 0000000..bfd1d90
--- /dev/null
+++ b/src/views/safeProduction/safeWorkApproval/index.vue
@@ -0,0 +1,371 @@
+<template>
+ <div class="app-container">
+ <!-- 鏍囩椤靛垏鎹笉鍚岀殑瀹℃壒绫诲瀷 -->
+ <div class="search_form mb20">
+ <div>
+ <span class="search_title">娴佺▼缂栧彿锛�</span>
+ <el-input v-model="searchForm.approveId"
+ style="width: 240px"
+ placeholder="璇疯緭鍏ユ祦绋嬬紪鍙锋悳绱�"
+ @change="handleQuery"
+ clearable
+ :prefix-icon="Search" />
+ <span class="search_title ml10">瀹℃壒鐘舵�侊細</span>
+ <el-select v-model="searchForm.approveStatus"
+ clearable
+ @change="handleQuery"
+ style="width: 240px">
+ <el-option label="寰呭鏍�"
+ :value="0" />
+ <el-option label="瀹℃牳涓�"
+ :value="1" />
+ <el-option label="瀹℃牳瀹屾垚"
+ :value="2" />
+ <el-option label="瀹℃牳鏈�氳繃"
+ :value="3" />
+ <el-option label="宸查噸鏂版彁浜�"
+ :value="4" />
+ </el-select>
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板</el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <PIMTable rowKey="id"
+ :column="tableColumnCopy"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"></PIMTable>
+ </div>
+ <info-form-dia ref="infoFormDia"
+ @close="handleQuery"
+ :approveType="currentApproveType"></info-form-dia>
+ <approval-dia ref="approvalDia"
+ @close="handleQuery"
+ :approveType="currentApproveType"></approval-dia>
+ <FileList ref="fileListRef" />
+ </div>
+</template>
+
+<script setup>
+ import FileList from "./fileList.vue";
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ computed,
+ reactive,
+ toRefs,
+ nextTick,
+ getCurrentInstance,
+ } from "vue";
+ import { ElMessageBox } from "element-plus";
+ import { useRoute } from "vue-router";
+ import InfoFormDia from "@/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue";
+ import ApprovalDia from "@/views/collaborativeApproval/approvalProcess/components/approvalDia.vue";
+ import {
+ approveProcessDelete,
+ approveProcessListPage,
+ } from "@/api/collaborativeApproval/approvalProcess.js";
+ import useUserStore from "@/store/modules/user";
+
+ const userStore = useUserStore();
+ const route = useRoute();
+
+ // 褰撳墠瀹℃壒绫诲瀷锛屾牴鎹�変腑鐨勬爣绛鹃〉璁$畻
+ const currentApproveType = computed(() => {
+ return Number(8);
+ });
+
+ // 鏍囩椤靛垏鎹㈠鐞�
+ const handleTabChange = tabName => {
+ // 鍒囨崲鏍囩椤垫椂閲嶇疆鎼滅储鏉′欢鍜屽垎椤碉紝骞堕噸鏂板姞杞芥暟鎹�
+ searchForm.value.approveId = "";
+ searchForm.value.approveStatus = "";
+ page.current = 1;
+ getList();
+ };
+
+ const data = reactive({
+ searchForm: {
+ approveId: "",
+ approveStatus: "",
+ },
+ });
+ const { searchForm } = toRefs(data);
+
+ // 鍔ㄦ�佽〃鏍煎垪閰嶇疆锛屾牴鎹鎵圭被鍨嬬敓鎴愬垪
+ const tableColumnCopy = computed(() => {
+ // 鍩虹鍒楅厤缃�
+ const baseColumns = [
+ {
+ label: "瀹℃壒鐘舵��",
+ prop: "approveStatus",
+ dataType: "tag",
+ width: 100,
+ formatData: params => {
+ if (params == 0) {
+ return "寰呭鏍�";
+ } else if (params == 1) {
+ return "瀹℃牳涓�";
+ } else if (params == 2) {
+ return "瀹℃牳瀹屾垚";
+ } else if (params == 4) {
+ return "宸查噸鏂版彁浜�";
+ } else {
+ return "涓嶉�氳繃";
+ }
+ },
+ formatType: params => {
+ if (params == 0) {
+ return "warning";
+ } else if (params == 1) {
+ return "primary";
+ } else if (params == 2) {
+ return "success";
+ } else if (params == 4) {
+ return "info";
+ } else {
+ return "danger";
+ }
+ },
+ },
+ {
+ label: "娴佺▼缂栧彿",
+ prop: "approveId",
+ width: 170,
+ },
+ {
+ label: "鐢宠閮ㄩ棬",
+ prop: "approveDeptName",
+ width: 220,
+ },
+ {
+ label: "瀹℃壒浜嬬敱",
+ prop: "approveReason",
+ width: 200,
+ },
+ {
+ label: "鐢宠浜�",
+ prop: "approveUserName",
+ width: 120,
+ },
+ ];
+
+ // 鏃ユ湡鍒楋紙鏍规嵁绫诲瀷鍔ㄦ�侀厤缃級
+ baseColumns.push(
+ {
+ label: "鐢宠鏃ユ湡",
+ prop: "approveTime",
+ width: 200,
+ },
+ {
+ label: "缁撴潫鏃ユ湡",
+ prop: "approveOverTime",
+ width: 120,
+ }
+ );
+
+ // 褰撳墠瀹℃壒浜哄垪
+ baseColumns.push({
+ label: "褰撳墠瀹℃壒浜�",
+ prop: "approveUserCurrentName",
+ width: 120,
+ });
+
+ // 鎿嶄綔鍒�
+ baseColumns.push({
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 230,
+ operation: [
+ {
+ name: "缂栬緫",
+ type: "text",
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ disabled: row =>
+ row.approveStatus == 2 ||
+ row.approveStatus == 1 ||
+ row.approveStatus == 4,
+ },
+ {
+ name: "瀹℃牳",
+ type: "text",
+ clickFun: row => {
+ openApprovalDia("approval", row);
+ },
+ disabled: row =>
+ row.approveUserCurrentId == null ||
+ row.approveStatus == 2 ||
+ row.approveStatus == 3 ||
+ row.approveStatus == 4 ||
+ row.approveUserCurrentId !== userStore.id,
+ },
+ {
+ name: "璇︽儏",
+ type: "text",
+ clickFun: row => {
+ openApprovalDia("view", row);
+ },
+ },
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ downLoadFile(row);
+ },
+ },
+ ],
+ });
+
+ return baseColumns;
+ });
+ const tableData = ref([]);
+ const selectedRows = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ total: 0,
+ });
+ const infoFormDia = ref();
+ const approvalDia = ref();
+ const { proxy } = getCurrentInstance();
+
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const fileListRef = ref(null);
+ const downLoadFile = row => {
+ fileListRef.value.open(row.commonFileList);
+ };
+ const pagination = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ approveProcessListPage({
+ ...page,
+ ...searchForm.value,
+ approveType: currentApproveType.value,
+ })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+ // 瀵煎嚭
+ const handleOut = () => {
+ const type = currentApproveType.value;
+ const urlMap = {
+ 0: "/approveProcess/exportZero",
+ 1: "/approveProcess/exportOne",
+ 2: "/approveProcess/exportTwo",
+ 3: "/approveProcess/exportThree",
+ 4: "/approveProcess/exportFour",
+ 5: "/approveProcess/exportFive",
+ 6: "/approveProcess/exportSix",
+ 7: "/approveProcess/exportSeven",
+ 8: "/approveProcess/exportEight",
+ };
+ const url = urlMap[type] || urlMap[0];
+ const nameMap = {
+ 0: "鍗忓悓瀹℃壒绠$悊琛�",
+ 1: "鍏嚭绠$悊瀹℃壒琛�",
+ 2: "璇峰亣绠$悊瀹℃壒琛�",
+ 3: "鍑哄樊绠$悊瀹℃壒琛�",
+ 4: "鎶ラ攢绠$悊瀹℃壒琛�",
+ 5: "閲囪喘鐢宠瀹℃壒琛�",
+ 6: "鎶ヤ环瀹℃壒琛�",
+ 7: "鍙戣揣瀹℃壒琛�",
+ 8: "鍗遍櫓浣滀笟瀹℃壒琛�",
+ };
+ const fileName = nameMap[type] || nameMap[0];
+ proxy.download(url, {}, `${fileName}.xlsx`);
+ };
+ // 琛ㄦ牸閫夋嫨鏁版嵁
+ const handleSelectionChange = selection => {
+ selectedRows.value = selection;
+ };
+
+ // 鎵撳紑鏂板銆佺紪杈戝脊妗�
+ const openForm = (type, row) => {
+ nextTick(() => {
+ infoFormDia.value?.openDialog(type, row);
+ });
+ };
+ // 鎵撳紑鏂板妫�楠屽脊妗�
+ const openApprovalDia = (type, row) => {
+ nextTick(() => {
+ approvalDia.value?.openDialog(type, row);
+ });
+ };
+
+ // 鍒犻櫎
+ const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length > 0) {
+ ids = selectedRows.value.map(item => item.approveId);
+ } else {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ approveProcessDelete(ids).then(res => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ };
+ onMounted(() => {
+ const approveId = route.query.approveId;
+
+ if (approveId) {
+ // 璁剧疆娴佺▼缂栧彿鏌ヨ鏉′欢
+ searchForm.value.approveId = String(approveId);
+ }
+
+ // 鏌ヨ鍒楄〃
+ getList();
+ });
+</script>
+
+<style scoped>
+ .approval-tabs {
+ margin-bottom: 10px;
+ }
+</style>
diff --git a/src/views/safeProduction/safetyTrainingAssessment/detail.vue b/src/views/safeProduction/safetyTrainingAssessment/detail.vue
new file mode 100644
index 0000000..9e3af6b
--- /dev/null
+++ b/src/views/safeProduction/safetyTrainingAssessment/detail.vue
@@ -0,0 +1,323 @@
+<template>
+ <div class="app-container">
+ <PageHeader content="鍩硅璁板綍">
+ </PageHeader>
+ <div class="search_form">
+ <div class="search_item">
+ <span class="search_title">浜哄憳鍚嶇О锛�</span>
+ <el-input v-model="searchForm.searchText"
+ style="width: 240px"
+ placeholder="杈撳叆瀹㈡埛鍚嶇О鎼滅储"
+ @change="searchName"
+ clearable
+ prefix-icon="Search" />
+ <el-button type="primary"
+ @click="searchName"
+ style="margin-left: 10px">鎼滅储</el-button>
+ </div>
+ <div class="search_item">
+ <span class="search_title">骞翠唤锛�</span>
+ <el-date-picker v-model="searchForm.invoiceDate"
+ type="year"
+ @change="searchDate"
+ placeholder="閫夋嫨骞�">
+ </el-date-picker>
+ <el-button type="primary"
+ @click="searchDate"
+ style="margin-left: 10px">鎼滅储</el-button>
+ <el-button type="primary"
+ @click="exportData"
+ style="margin-left: 20px;margin-right: 20px">瀵煎嚭</el-button>
+ </div>
+ </div>
+ <div style="display: flex">
+ <div class="table_list">
+ <el-table :data="tableData"
+ border
+ v-loading="tableLoading"
+ :row-key="(row) => row.id"
+ @row-click="rowClickMethod"
+ height="calc(100vh - 18.5em)">
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鍚嶇О"
+ prop="nickName"
+ show-overflow-tooltip
+ width="200" />
+ <el-table-column label="鎵�灞為儴闂�"
+ prop="deptNames"
+ show-overflow-tooltip
+ width="200" />
+ <el-table-column label="鑱旂郴鏂瑰紡"
+ prop="phonenumber"
+ show-overflow-tooltip
+ width="200" />
+ <!-- <el-table-column label="email"
+ prop="email"
+ show-overflow-tooltip
+ width="200" /> -->
+ <!-- <el-table-column label="搴旀敹閲戦(鍏�)"
+ prop="unReceiptPaymentAmount"
+ show-overflow-tooltip
+ width="200">
+ <template #default="{ row, column }">
+ <el-text type="danger">
+ {{ formattedNumber(row, column, row.unReceiptPaymentAmount) }}
+ </el-text>
+ </template>
+ </el-table-column> -->
+ </el-table>
+ <pagination v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange" />
+ </div>
+ <div class="table_list">
+ <el-table :data="receiptRecord"
+ border
+ :row-key="(row) => row.id"
+ height="calc(100vh - 18.5em)">
+ <el-table-column align="center"
+ label="搴忓彿"
+ type="index"
+ width="60" />
+ <el-table-column label="鍩硅鏃ユ湡"
+ prop="trainingDate"
+ show-overflow-tooltip />
+ <el-table-column label="鍩硅鍐呭"
+ prop="trainingContent"
+ show-overflow-tooltip />
+ <el-table-column label="鍩硅璇炬椂"
+ prop="classHour"
+ show-overflow-tooltip />
+ <el-table-column label="鑰冩牳缁撴灉"
+ prop="examinationResults"
+ show-overflow-tooltip>
+ <template #default="{ row }">
+ <el-tag :type="row.examinationResults === '鍚堟牸' ? 'success' : 'danger'">
+ {{ row.examinationResults }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script setup>
+ import { onMounted, ref } from "vue";
+ import Pagination from "@/components/PIMTable/Pagination.vue";
+ import {
+ safeTrainingDetailListPage,
+ safeTrainingDetailExport,
+ } from "@/api/safeProduction/safetyTrainingAssessment.js";
+ import { userListNoPage } from "@/api/system/user.js";
+ const { proxy } = getCurrentInstance();
+ const tableData = ref([]);
+ const receiptRecord = ref([]);
+ const tableLoading = ref(false);
+ const page = reactive({
+ current: 1,
+ size: 100,
+ });
+ const recordPage = reactive({
+ current: 1,
+ size: 100,
+ });
+ const total = ref(0);
+ const recordTotal = ref(0);
+ const data = reactive({
+ searchForm: {
+ searchText: "",
+ invoiceDate: "",
+ },
+ });
+ const customerId = ref("");
+ const { searchForm } = toRefs(data);
+ const originReceiptRecord = ref([]);
+ // 鏌ヨ鍒楄〃
+ /** 鎼滅储鎸夐挳鎿嶄綔 */
+ const handleQuery = () => {
+ page.current = 1;
+ getList();
+ };
+ const paginationChange = obj => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+ };
+ const tableDataCopy = ref([]);
+ const getList = () => {
+ tableLoading.value = true;
+ userListNoPage({}).then(res => {
+ console.log("res", res.data);
+ tableData.value = res.data;
+ tableDataCopy.value = res.data;
+ if (tableData.value.length > 0) {
+ customerId.value = tableData.value[0].userId;
+ receiptPaymentList(customerId.value);
+ tableLoading.value = false;
+ }
+ });
+ };
+ const exportData = () => {
+ safeTrainingDetailExport({
+ userId: customerId.value,
+ })
+ .then(res => {
+ // 鍒涘缓Blob瀵硅薄
+ const blob = new Blob([res], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ // 鍒涘缓涓嬭浇閾炬帴
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `璁板綍璇︽儏_${tableData.value[0].nickName}.docx`;
+
+ // 妯℃嫙鐐瑰嚮涓嬭浇
+ document.body.appendChild(link);
+ link.click();
+
+ // 娓呯悊涓存椂瀵硅薄
+ setTimeout(() => {
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ }, 100);
+
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ })
+ .catch(err => {
+ console.error("瀵煎嚭澶辫触:", err);
+ ElMessage.error("瀵煎嚭澶辫触锛岃閲嶈瘯");
+ });
+ };
+ const formattedNumber = (row, column, cellValue) => {
+ return parseFloat(cellValue).toFixed(2);
+ };
+ // 涓昏〃鍚堣鏂规硶
+ const summarizeMainTable = param => {
+ return proxy.summarizeTable(
+ param,
+ ["invoiceTotal", "receiptPaymentAmount", "unReceiptPaymentAmount"],
+ {
+ ticketsNum: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ futureTickets: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ }
+ );
+ };
+ // 瀛愯〃鍚堣鏂规硶
+ const summarizeMainTable1 = param => {
+ var summarizeTable = proxy.summarizeTable(
+ param,
+ ["invoiceAmount", "receiptAmount", "unReceiptAmount"],
+ {
+ ticketsNum: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ futureTickets: { noDecimal: true }, // 涓嶄繚鐣欏皬鏁�
+ }
+ );
+ // 鍙栨渶鍚庝竴琛屾暟鎹�;
+ if (receiptRecord.value?.length > 0) {
+ const index = tableData.value.findIndex(
+ item => item.id == customerId.value
+ );
+ summarizeTable[summarizeTable.length - 1] =
+ tableData.value[index].unReceiptPaymentAmount.toFixed(2);
+ } else {
+ summarizeTable[summarizeTable.length - 1] = 0.0;
+ }
+ return summarizeTable;
+ };
+ const goBack = () => {
+ proxy.$router.push({
+ path: "/safeProduction/safetyTrainingAssessment",
+ });
+ };
+ const searchName = () => {
+ tableData.value = tableDataCopy.value;
+ if (searchForm.value.searchText) {
+ tableData.value = tableData.value.filter(item =>
+ item.nickName.includes(searchForm.value.searchText)
+ );
+ customerId.value = tableData.value[0].userId;
+ }
+
+ receiptPaymentList(customerId.value);
+ };
+ const dateFormat = (date, format = "yyyy-MM-dd") => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ return format.replace("yyyy", year).replace("MM", month).replace("dd", day);
+ };
+ const searchDate = () => {
+ receiptRecord.value = originReceiptRecordCopy.value;
+ console.log("searchForm.value.invoiceDate", searchForm.value.invoiceDate);
+ if (searchForm.value.invoiceDate) {
+ const year = dateFormat(searchForm.value.invoiceDate, "yyyy");
+ receiptRecord.value = receiptRecord.value.filter(item => {
+ console.log("item.trainingDate", item.trainingDate);
+ return item.trainingDate.includes(year);
+ });
+ }
+ };
+ const originReceiptRecordCopy = ref([]);
+ const receiptPaymentList = id => {
+ const param = {
+ userId: id,
+ };
+ console.log("param", param);
+ safeTrainingDetailListPage(param).then(res => {
+ originReceiptRecord.value = res.data.records;
+ handlePagination({ page: 1, limit: recordPage.size });
+ recordTotal.value = res.data.length;
+ });
+ };
+
+ // 姹囨璁板綍鍒楄〃鍒嗛〉
+ const recordPaginationChange = pagination => {
+ handlePagination(pagination);
+ };
+
+ const rowClickMethod = row => {
+ customerId.value = row.userId;
+ receiptPaymentList(customerId.value);
+ };
+
+ const handlePagination = ({ page, limit }) => {
+ recordPage.current = page;
+ recordPage.size = limit;
+
+ const start = (page - 1) * limit;
+ const end = start + limit;
+
+ receiptRecord.value = originReceiptRecord.value.slice(start, end);
+ originReceiptRecordCopy.value = originReceiptRecord.value.slice(start, end);
+ };
+
+ onMounted(() => {
+ getList();
+ });
+</script>
+
+<style scoped lang="scss">
+ .table_list {
+ width: 50%;
+ }
+ .search_back {
+ cursor: pointer;
+ color: #0f497e;
+ }
+ .search_item {
+ width: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ margin-right: 20px;
+ }
+</style>
diff --git a/src/views/safeProduction/safetyTrainingAssessment/index.vue b/src/views/safeProduction/safetyTrainingAssessment/index.vue
new file mode 100644
index 0000000..38c2172
--- /dev/null
+++ b/src/views/safeProduction/safetyTrainingAssessment/index.vue
@@ -0,0 +1,1299 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <div>
+ <span class="search_title">鍩硅鏃ユ湡锛�</span>
+ <el-date-picker v-model="searchForm.trainingDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ @change="handleQuery"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ <el-button type="primary"
+ @click="handleQuery"
+ style="margin-left: 10px">
+ 鎼滅储
+ </el-button>
+ </div>
+ <div>
+ <el-button type="primary"
+ @click="openForm('add')">鏂板鍩硅</el-button>
+ <el-button type="primary"
+ @click="opendetail">鍩硅璁板綍</el-button>
+ <el-button type="danger"
+ plain
+ @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <div class="table_list">
+ <el-tabs v-model="searchForm.state"
+ @tab-click="tabhandleQuery">
+ <el-tab-pane label="鏈紑濮�"
+ :name="0"></el-tab-pane>
+ <el-tab-pane label="杩涜涓�"
+ :name="1"></el-tab-pane>
+ <el-tab-pane label="宸茬粨鏉�"
+ :name="2"></el-tab-pane>
+ </el-tabs>
+ <!-- state 鐘舵��(0锛氭湭寮�濮�1锛氳繘琛屼腑锛�2锛氬凡缁撴潫) -->
+ <PIMTable rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ :total="page.total"
+ :rowClassName="getRowClass"></PIMTable>
+ </div>
+ <!-- 鏂板/缂栬緫鐭ヨ瘑寮圭獥 -->
+ <el-dialog v-model="dialogVisible"
+ :title="dialogTitle"
+ width="800px"
+ :close-on-click-modal="false">
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="150px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍩硅鏃ユ湡"
+ prop="trainingDate">
+ <el-date-picker style="width: 100%"
+ v-model="form.trainingDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇剧▼缂栧彿"
+ prop="courseCode">
+ <el-input v-model="form.courseCode"
+ disabled
+ placeholder="淇濆瓨鍚庤嚜鍔ㄧ敓鎴�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍒涘缓鏃堕棿"
+ prop="createTime">
+ <el-date-picker style="width: 100%"
+ v-model="formCreateTimeDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="寮�濮嬫椂闂�"
+ prop="openingTime">
+ <el-time-picker v-model="form.openingTime"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ value-format="HH:mm:ss"
+ format="HH:mm:ss"
+ clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="缁撴潫鏃堕棿"
+ prop="endTime">
+ <el-time-picker v-model="form.endTime"
+ placeholder="璇烽�夋嫨"
+ style="width: 100%"
+ value-format="HH:mm:ss"
+ format="HH:mm:ss"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍩硅鐩爣"
+ prop="trainingObjectives">
+ <el-input v-model="form.trainingObjectives"
+ placeholder="璇疯緭鍏ュ煿璁洰鏍�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍙傚姞瀵硅薄"
+ prop="participants">
+ <el-input v-model="form.participants"
+ placeholder="璇疯緭鍏ュ弬鍔犲璞�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍩硅鍐呭"
+ prop="trainingContent">
+ <el-input v-model="form.trainingContent"
+ placeholder="璇疯緭鍏ュ煿璁唴瀹�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍩硅璁插笀"
+ prop="trainingLecturer">
+ <el-input v-model="form.trainingLecturer"
+ placeholder="璇疯緭鍏ュ煿璁甯�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璇剧▼瀛﹀垎"
+ prop="projectCredits">
+ <el-input v-model="form.projectCredits"
+ type="number"
+ min="0"
+ placeholder="璇疯緭鍏ヨ绋嬪鍒�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍩硅鏂瑰紡"
+ prop="trainingMode">
+ <el-select v-model="form.trainingMode"
+ placeholder="璇烽�夋嫨鍩硅鏂瑰紡"
+ clearable>
+ <el-option v-for="item in trainingModeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍩硅鍦扮偣"
+ prop="placeTraining">
+ <el-input v-model="form.placeTraining"
+ placeholder="璇疯緭鍏ュ煿璁湴鐐�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇炬椂"
+ prop="classHour">
+ <el-input v-model="form.classHour"
+ type="number"
+ min="0"
+ placeholder="璇疯緭鍏ヨ鏃�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm">纭畾</el-button>
+ <el-button @click="dialogVisible = false">鍙栨秷</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- 鏌ョ湅鐭ヨ瘑璇︽儏寮圭獥 -->
+ <el-dialog v-model="viewDialogVisible"
+ title="缁撴灉鏄庣粏"
+ width="900px"
+ :close-on-click-modal="false">
+ <div class="knowledge-detail">
+ <div class="classtitle">璇剧▼璇︽儏</div>
+ <el-descriptions size="mini"
+ border
+ :column="3">
+ <el-descriptions-item label="璇剧▼缂栧彿:">{{ currentKnowledge.courseCode }}</el-descriptions-item>
+ <el-descriptions-item label="鍩硅鍐呭:">{{ currentKnowledge.trainingContent }}</el-descriptions-item>
+ <el-descriptions-item label="鐘舵��:">
+ <el-tag :type="currentKnowledge.status === 0 ? 'success' : (currentKnowledge.status === 1 ? 'success' : 'info')">
+ {{ currentKnowledge.status === 0 ? '鏈紑濮�' : (currentKnowledge.status === 1 ? '杩涜涓�' : '宸茬粨鏉�') }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅璁插笀:">
+ {{ currentKnowledge.trainingLecturer }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅寮�濮嬫椂闂�:">
+ {{ currentKnowledge.trainingDate + ' ' + currentKnowledge.openingTime }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅缁撴潫鏃堕棿:">
+ {{ currentKnowledge.trainingDate + ' ' + currentKnowledge.endTime }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鐩爣:">
+ {{ currentKnowledge.trainingObjectives }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍙傚姞瀵硅薄:">
+ {{ currentKnowledge.participants }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鏂瑰紡:">
+ <el-tag type="primary">
+ {{ getTrainingModeLabel(currentKnowledge.trainingMode) }}
+ </el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="鍩硅鍦扮偣:">
+ {{ currentKnowledge.placeTraining }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇炬椂:">
+ {{ currentKnowledge.classHour }}
+ </el-descriptions-item>
+ <el-descriptions-item label="璇剧▼瀛﹀垎:">
+ {{ currentKnowledge.projectCredits }}
+ </el-descriptions-item>
+ <el-descriptions-item label="鎶ュ悕浜烘暟:">
+ {{ currentKnowledge.nums }}
+ </el-descriptions-item>
+ <el-descriptions-item label="闄勪欢鍒楄〃:">
+ <el-button type="primary"
+ size="small"
+ @click="openFileDialog(endform)">闄勪欢鍒楄〃</el-button>
+ </el-descriptions-item>
+ </el-descriptions>
+ <!-- <el-divider style="margin: 20px 0;" /> -->
+ <div class="classtitle"
+ style="margin-top: 40px;margin-bottom: 30px;">璇剧▼璇勪环</div>
+ <el-form ref="formRef"
+ :model="form"
+ :rules="rules"
+ label-position="top"
+ label-width="150px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璇勪环浜�:"
+ prop="courseCode">
+ <el-input v-model="endform.assessmentUserName"
+ disabled
+ placeholder="璇烽�夋嫨璇勪环浜�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璇勪环鏃堕棿:"
+ prop="trainingDate">
+ <el-date-picker style="width: 100%"
+ disabled
+ v-model="endform.assessmentDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鑰冩牳鏂瑰紡:"
+ prop="assessmentMethod">
+ <el-input v-model="endform.assessmentMethod"
+ placeholder="璇烽�夋嫨鑰冩牳鏂瑰紡" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏈璇剧▼缁煎悎璇勪环:"
+ prop="comprehensiveAssessment">
+ <el-input v-model="endform.comprehensiveAssessment"
+ placeholder="璇疯緭鍏ユ湰娆¤绋嬬患鍚堣瘎浠�" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="鍩硅鎽樿:"
+ prop="trainingAbstract">
+ <el-input v-model="endform.trainingAbstract"
+ type="textarea"
+ :rows="2"
+ placeholder="璇疯緭鍏ュ煿璁憳瑕�" />
+ </el-form-item>
+ <!-- <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢鏉愭枡锛�"
+ prop="remark">
+ <el-upload v-model:file-list="fileList"
+ :action="upload.url"
+ multiple
+ ref="fileUpload"
+ auto-upload
+ :headers="upload.headers"
+ :before-upload="handleBeforeUpload"
+ :on-error="handleUploadError"
+ :on-success="handleUploadSuccess"
+ :on-remove="handleRemove">
+ <el-button type="primary"
+ v-if="operationType !== 'view'">涓婁紶</el-button>
+ <template #tip
+ v-if="operationType !== 'view'">
+ <div class="el-upload__tip">
+ 鏂囦欢鏍煎紡鏀寔
+ doc锛宒ocx锛寈ls锛寈lsx锛宲pt锛宲ptx锛宲df锛宼xt锛寈ml锛宩pg锛宩peg锛宲ng锛実if锛宐mp锛宺ar锛寊ip锛�7z
+ </div>
+ </template>
+ </el-upload>
+ </el-form-item>
+ </el-col>
+ </el-row> -->
+ </el-form>
+ <div class="classtitle"
+ style="margin-top: 40px;">鑰冩牳鍒楄〃</div>
+ <el-table style="margin-top: 20px;"
+ :data="endform.safeTrainingDetailsDtoList"
+ border
+ fit
+ stripe
+ highlight-current-row>
+ <el-table-column prop="nickName"
+ label="濮撳悕" />
+ <el-table-column prop="phonenumber"
+ label="鐢佃瘽鍙风爜" />
+ <el-table-column prop="examinationResults"
+ label="鑰冩牳缁撴灉">
+ <template #default="scope">
+ <el-select v-model="scope.row.examinationResults"
+ placeholder="璇烽�夋嫨鑰冩牳缁撴灉">
+ <el-option label="鍚堟牸"
+ value="鍚堟牸" />
+ <el-option label="涓嶅悎鏍�"
+ value="涓嶅悎鏍�" />
+ </el-select>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <template #footer>
+ <span class="dialog-footer">
+ <el-button type="primary"
+ @click="submitForm2">鎻愪氦</el-button>
+ <el-button @click="viewDialogVisible = false">鍏抽棴</el-button>
+ </span>
+ </template>
+ </el-dialog>
+ <!-- todo 闄勪欢棰勮鐩稿叧 -->
+ <FileList v-if="fileDialogVisible" v-model:visible="fileDialogVisible" record-type="safe_training" :record-id="recordId" />
+ </div>
+</template>
+
+<script setup>
+ import { Search } from "@element-plus/icons-vue";
+ import {
+ onMounted,
+ ref,
+ reactive,
+ toRefs,
+ getCurrentInstance,
+ computed,
+ } from "vue";
+ import request from "@/utils/request";
+ import { getToken } from "@/utils/auth";
+ import { ElMessage, ElMessageBox } from "element-plus";
+ import PIMTable from "@/components/PIMTable/PIMTable.vue";
+ import { userListNoPage } from "@/api/system/user.js";
+ import {
+ safeTrainingListPage,
+ safeTrainingAdd,
+ safeTrainingExport,
+ safeTrainingDel,
+ safeTrainingFileListPage,
+ safeTrainingFileAdd,
+ safeTrainingFileDel,
+ safeTrainingSign,
+ safeTrainingGet,
+ safeTrainingSave,
+ } from "@/api/safeProduction/safetyTrainingAssessment.js";
+ import useUserStore from "@/store/modules/user";
+ import dayjs from "dayjs";
+ const userStore = useUserStore();
+ const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
+
+ // 琛ㄥ崟楠岃瘉瑙勫垯
+ const rules = {
+ trainingDate: [
+ { required: true, message: "璇烽�夋嫨鍩硅鏃ユ湡", trigger: "change" },
+ ],
+ openingTime: [
+ { required: true, message: "璇烽�夋嫨寮�濮嬫椂闂�", trigger: "change" },
+ ],
+ endTime: [{ required: true, message: "璇烽�夋嫨缁撴潫鏃堕棿", trigger: "change" }],
+ trainingContent: [
+ { required: true, message: "璇疯緭鍏ュ煿璁唴瀹�", trigger: "blur" },
+ ],
+ trainingLecturer: [
+ { required: true, message: "璇疯緭鍏ュ煿璁甯�", trigger: "blur" },
+ ],
+ classHour: [{ required: true, message: "璇疯緭鍏ヨ鏃�", trigger: "blur" }],
+ };
+ const upload = reactive({
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ });
+ // 鍝嶅簲寮忔暟鎹�
+ const data = reactive({
+ searchForm: {
+ trainingDate: "",
+ state: 0,
+ },
+ tableLoading: false,
+ page: {
+ current: 1,
+ size: 20,
+ total: 0,
+ },
+ tableData: [],
+ selectedIds: [],
+ form: {
+ courseCode: "", // 璇剧▼缂栧彿
+ trainingDate: "", // 鍩硅鏃ユ湡
+ openingTime: "", // 寮�濮嬫椂闂�
+ endTime: "", // 缁撴潫鏃堕棿
+ trainingObjectives: "", // 鍩硅鐩爣
+ participants: "", // 鍙傚姞瀵硅薄
+ trainingContent: "", // 鍩硅鍐呭
+ trainingLecturer: "", // 鍩硅璁插笀
+ projectCredits: "", // 椤圭洰瀛﹀垎
+ trainingMode: "", // 鍩硅鏂瑰紡
+ placeTraining: "", // 鍩硅鍦扮偣
+ classHour: "", // 璇炬椂
+ createTime: "", // 鍒涘缓鏃堕棿
+ },
+ dialogVisible: false,
+ dialogTitle: "",
+ dialogType: "add",
+ viewDialogVisible: false,
+ currentKnowledge: {},
+ });
+ const formCreateTimeDate = computed({
+ get: () => (form.value.createTime ? String(form.value.createTime).split(" ")[0] : ""),
+ set: (value) => {
+ form.value.createTime = value ? `${value} ${dayjs().format("HH:mm:ss")}` : "";
+ },
+ });
+
+ const {
+ searchForm,
+ tableLoading,
+ page,
+ tableData,
+ selectedIds,
+ form,
+ dialogVisible,
+ dialogTitle,
+ dialogType,
+ viewDialogVisible,
+ currentKnowledge,
+ } = toRefs(data);
+ const { proxy } = getCurrentInstance();
+ const { safe_training_methods } = proxy.useDict("safe_training_methods");
+ const trainingModeOptions = computed(() => safe_training_methods?.value || []);
+ const getTrainingModeLabel = val => {
+ const item = trainingModeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item ? item.label : val;
+ };
+ // 鍒囨崲tab鏌ヨ
+ const tabhandleQuery = val => {
+ searchForm.value.state = val.paneName;
+ console.log(searchForm.value.state, "searchForm.value.state");
+
+ handleQuery();
+ };
+ // 琛ㄥ崟寮曠敤
+ const formRef = ref();
+ const riskLevelOptions = ref([
+ { value: "浣庨闄�", label: "浣庨闄�" },
+ { value: "涓�鑸闄�", label: "涓�鑸闄�" },
+ { value: "杈冨ぇ椋庨櫓", label: "杈冨ぇ椋庨櫓" },
+ { value: "閲嶅ぇ椋庨櫓", label: "閲嶅ぇ椋庨櫓" },
+ ]);
+
+ const fileList = ref([]);
+
+ // 琛ㄦ牸鍒楅厤缃�
+ const tableColumn = ref([
+ {
+ label: "璇剧▼缂栧彿",
+ prop: "courseCode",
+ width: 150,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍩硅鏃ユ湡",
+ prop: "trainingDate",
+ width: 120,
+
+ showOverflowTooltip: true,
+ },
+ {
+ label: "寮�濮嬫椂闂�",
+ prop: "openingTime",
+ width: 120,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "缁撴潫鏃堕棿",
+ prop: "endTime",
+ width: 120,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍩硅鐩爣",
+ prop: "trainingObjectives",
+ width: 200,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍙傚姞瀵硅薄",
+ prop: "participants",
+ width: 200,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍩硅鍐呭",
+ prop: "trainingContent",
+ width: 200,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍩硅璁插笀",
+ prop: "trainingLecturer",
+ width: 200,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "椤圭洰瀛﹀垎",
+ prop: "projectCredits",
+ width: 120,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鍩硅鏂瑰紡",
+ prop: "trainingMode",
+ width: 120,
+ showOverflowTooltip: true,
+ formatData: params => {
+ return getTrainingModeLabel(params);
+ },
+ },
+ {
+ label: "鍩硅鍦扮偣",
+ prop: "placeTraining",
+ width: 200,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "璇炬椂",
+ prop: "classHour",
+ width: 120,
+ showOverflowTooltip: true,
+ },
+ {
+ label: "鎶ュ悕浜烘暟",
+ prop: "nums",
+ width: 120,
+ showOverflowTooltip: true,
+ },
+ {
+ dataType: "action",
+ label: "鎿嶄綔",
+ align: "center",
+ fixed: "right",
+ width: 300,
+ operation: [
+ {
+ name: "绛惧埌",
+ type: "text",
+ disabled: row => row.state !== 1,
+ clickFun: row => {
+ signIn(row);
+ },
+ },
+ {
+ name: "缂栬緫",
+ type: "text",
+ disabled: row => row.state !== 0,
+ clickFun: row => {
+ openForm("edit", row);
+ },
+ },
+ {
+ name: "瀵煎嚭",
+ type: "text",
+ clickFun: row => {
+ exportKnowledge(row);
+ },
+ color: "#C49000",
+ },
+ {
+ name: "闄勪欢",
+ type: "text",
+ clickFun: row => {
+ openFileDialog(row);
+ },
+ color: "#007AFF",
+ },
+
+ {
+ name: "缁撴灉鏄庣粏",
+ type: "text",
+ disabled: row => row.state == 0,
+ clickFun: row => {
+ viewResultDetail(row);
+ },
+ },
+ // {
+ // name: "鏌ョ湅",
+ // type: "text",
+ // clickFun: row => {
+ // viewKnowledge(row);
+ // },
+ // },
+ ],
+ },
+ ]);
+ const userList = ref([]);
+ // 鐢熷懡鍛ㄦ湡
+ onMounted(() => {
+ getCurrentFactoryName();
+ getList();
+ startAutoRefresh();
+ userListNoPage().then(res => {
+ userList.value = res.data;
+ });
+ });
+ const endform = ref({
+ assessmentUserId: "", //璇勪环浜�
+ assessmentUserName: "", //璇勪环浜哄鍚�
+ assessmentMethod: "", //鑰冩牳鏂瑰紡
+ assessmentDate: "", //璇勪环鏃堕棿
+ comprehensiveAssessment: "", //缁煎悎璇勪环
+ trainingAbstract: "", //鍩硅鎽樿
+ safeTrainingFileList: [], //鍩硅闄勪欢
+ safeTrainingDetailsDtoList: [], //鑰冩牳缁撴灉璇︽儏
+ });
+ const operationType = ref("edit");
+ const viewResultDetail = row => {
+ // fileList.value = [];
+ operationType.value = "edit";
+ safeTrainingGet({ id: row.id }).then(res => {
+ if (res.code === 200) {
+ console.log(res.data, "res.data");
+ currentKnowledge.value = JSON.parse(JSON.stringify(res.data));
+ currentKnowledge.value.nums = row.nums;
+ viewDialogVisible.value = true;
+ endform.value = { ...res.data };
+ endform.value.assessmentUserName = endform.value.assessmentUserName
+ ? endform.value.assessmentUserName
+ : currentUserName.value;
+ endform.value.assessmentUserId = endform.value.assessmentUserId
+ ? endform.value.assessmentUserId
+ : currentUserId.value;
+ endform.value.assessmentDate = dayjs().format("YYYY-MM-DD");
+ } else {
+ proxy.$modal.msgError(res.msg || "鏌ヨ璇︽儏澶辫触");
+ }
+ });
+ };
+
+ // 涓婁紶鍓嶆牎妫�
+ function handleBeforeUpload(file) {
+ proxy.$modal.loading("姝e湪涓婁紶鏂囦欢锛岃绋嶅��...");
+ return true;
+ }
+ // 涓婁紶澶辫触
+ function handleUploadError(err) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶辫触");
+ proxy.$modal.closeLoading();
+ }
+ // 涓婁紶鎴愬姛鍥炶皟
+ function handleUploadSuccess(res, file, uploadFiles) {
+ proxy.$modal.closeLoading();
+ if (res.code === 200) {
+ // 纭繚 tempFileIds 瀛樺湪涓斾负鏁扮粍
+ if (!endform.value.safeTrainingFileList) {
+ endform.value.safeTrainingFileList = [];
+ }
+ endform.value.safeTrainingFileList.push({
+ id: res.data.tempId,
+ fileName: res.data.originalName,
+ url: res.data.tempPath,
+ safeTrainingId: currentKnowledge.value.id,
+ });
+ proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ proxy.$refs.fileUpload.handleRemove(file);
+ }
+ }
+ // 绉婚櫎鏂囦欢
+ function handleRemove(file) {
+ if (operationType.value === "edit") {
+ let index = endform.value.safeTrainingFileList.findIndex(
+ item => item.fileName === file.name
+ );
+ if (index !== -1) {
+ endform.value.safeTrainingFileList.splice(index, 1);
+ }
+ }
+ }
+ const submitForm2 = () => {
+ endform.value.safeTrainingDetailsDtoList.forEach((item, index) => {
+ if (!item.examinationResults) {
+ proxy.$modal.msgError(`璇烽�夋嫨${item.nickName}鐨勮�冩牳缁撴灉`);
+ return;
+ }
+ });
+ console.log(endform.value, "endform.value");
+ proxy.$modal.loading("姝e湪鎻愪氦锛岃绋嶅��...");
+ safeTrainingSave(endform.value).then(res => {
+ proxy.$modal.closeLoading();
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ getList();
+ viewDialogVisible.value = false;
+ } else {
+ proxy.$modal.msgError(res.msg || "鎻愪氦澶辫触");
+ }
+ });
+ };
+ const opendetail = row => {
+ proxy.$router.push({
+ path: "/safeProduction/safetyTrainingAssessmentDetail",
+ });
+ };
+
+ const signIn = row => {
+ ElMessageBox.confirm("纭绛惧埌鍚楋紵", "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ safeTrainingSign({
+ safeTrainingId: row.id,
+ userId: currentUserId.value,
+ }).then(res => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("绛惧埌鎴愬姛");
+ getList();
+ } else {
+ proxy.$modal.msgError(res.msg || "绛惧埌澶辫触");
+ }
+ });
+ });
+ };
+
+ // 澶勭悊鐢ㄦ埛閫夋嫨鍙樺寲
+ const handleUserChange = userId => {
+ const selectedUser = userList.value.find(user => user.userId === userId);
+ if (selectedUser) {
+ form.value.principalUser = selectedUser.nickName;
+ form.value.principalMobile = selectedUser.phonenumber;
+ }
+ };
+
+ // 鎵撳紑闄勪欢寮圭獥
+ const recordId =ref(0)
+ const fileDialogVisible = ref(false)
+
+ // 鎵撳紑闄勪欢寮规
+ const openFileDialog = async (row) => {
+ recordId.value = row.id
+ fileDialogVisible.value = true
+ }
+
+ // 涓婁紶闄勪欢
+ const handleUpload = async () => {
+ if (!currentFileRow.value) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨鏁版嵁");
+ return null;
+ }
+
+ return new Promise(resolve => {
+ // 鍒涘缓涓�涓殣钘忕殑鏂囦欢杈撳叆鍏冪礌
+ const input = document.createElement("input");
+ input.type = "file";
+ input.style.display = "none";
+ input.onchange = async e => {
+ const file = e.target.files[0];
+ if (!file) {
+ resolve(null);
+ return;
+ }
+
+ try {
+ // 浣跨敤 FormData 涓婁紶鏂囦欢
+ const formData = new FormData();
+ formData.append("file", file);
+
+ const uploadRes = await request({
+ url: "/file/upload",
+ method: "post",
+ data: formData,
+ headers: {
+ "Content-Type": "multipart/form-data",
+ Authorization: `Bearer ${getToken()}`,
+ },
+ });
+
+ if (uploadRes.code === 200) {
+ // 淇濆瓨闄勪欢淇℃伅
+ const fileData = {
+ safeTrainingId: currentFileRow.value.id,
+ name: uploadRes.data.originalName || file.name,
+ url: uploadRes.data.tempPath || uploadRes.data.url,
+ };
+
+ const saveRes = await safeTrainingFileAdd(fileData);
+ if (saveRes.code === 200) {
+ proxy.$modal.msgSuccess("鏂囦欢涓婁紶鎴愬姛");
+ // 閲嶆柊鍔犺浇鏂囦欢鍒楄〃
+ const listRes = await safeTrainingFileListPage({
+ safeTrainingId: currentFileRow.value.id,
+ current: filePagination.value.current,
+ size: filePagination.value.size,
+ });
+ if (listRes.code === 200 && fileListRef.value) {
+ const fileList = (listRes.data?.records || []).map(item => ({
+ name: item.name,
+ url: item.url,
+ id: item.id,
+ ...item,
+ }));
+ fileListRef.value.setList(fileList);
+ filePagination.value.total = listRes.data?.total || 0;
+ }
+ // 杩斿洖鏂版枃浠朵俊鎭�
+ resolve({
+ name: fileData.name,
+ url: fileData.url,
+ id: saveRes.data?.id,
+ });
+ } else {
+ proxy.$modal.msgError(saveRes.msg || "鏂囦欢淇濆瓨澶辫触");
+ resolve(null);
+ }
+ } else {
+ proxy.$modal.msgError(uploadRes.msg || "鏂囦欢涓婁紶澶辫触");
+ resolve(null);
+ }
+ } catch (error) {
+ proxy.$modal.msgError("鏂囦欢涓婁紶澶辫触");
+ resolve(null);
+ } finally {
+ document.body.removeChild(input);
+ }
+ };
+
+ document.body.appendChild(input);
+ input.click();
+ });
+ };
+ const filePagination = ref({
+ current: 1,
+ size: 10,
+ total: 0,
+ });
+ const paginationSearch = async (page, size) => {
+ filePagination.value.current = page;
+ filePagination.value.size = size;
+ const listRes = await safeTrainingFileListPage({
+ safeTrainingId: currentFileRow.value.id,
+ current: filePagination.value.current,
+ size: filePagination.value.size,
+ });
+ if (listRes.code === 200) {
+ const fileList = (listRes.data?.records || []).map(item => ({
+ name: item.name,
+ url: item.url,
+ id: item.id,
+ ...item,
+ }));
+
+ fileListRef.value.setList(fileList);
+ filePagination.value.total = listRes.data?.total || 0;
+ }
+ };
+ // 鍒犻櫎闄勪欢
+ const handleFileDelete = async row => {
+ try {
+ const res = await safeTrainingFileDel([row.id]);
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ // 閲嶆柊鍔犺浇鏂囦欢鍒楄〃
+ if (currentFileRow.value && fileListRef.value) {
+ const listRes = await safeTrainingFileListPage({
+ safeTrainingId: currentFileRow.value.id,
+ current: filePagination.value.current,
+ size: filePagination.value.size,
+ });
+ if (listRes.code === 200) {
+ const fileList = (listRes.data?.records || []).map(item => ({
+ name: item.name,
+ url: item.url,
+ id: item.id,
+ ...item,
+ }));
+ fileListRef.value.setList(fileList);
+ filePagination.value.total = listRes.data?.total || 0;
+ }
+ }
+ return true; // 杩斿洖 true 琛ㄧず鍒犻櫎鎴愬姛锛岀粍浠朵細鏇存柊鍒楄〃
+ } else {
+ proxy.$modal.msgError(res.msg || "鍒犻櫎澶辫触");
+ return false;
+ }
+ } catch (error) {
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ return false;
+ }
+ };
+
+ // 寮�濮嬭嚜鍔ㄥ埛鏂�
+ const startAutoRefresh = () => {
+ setInterval(() => {
+ getList();
+ }, 600000); // 10鍒嗛挓鍒锋柊涓�娆� (10 * 60 * 1000 = 600000ms)
+ };
+
+ // 鏌ヨ鏁版嵁
+ const handleQuery = () => {
+ page.value.current = 1;
+ getList();
+ };
+ const exportKnowledge = row => {
+ safeTrainingExport(row)
+ .then(res => {
+ // 鍒涘缓Blob瀵硅薄
+ const blob = new Blob([res], {
+ type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+ });
+ // 鍒涘缓涓嬭浇閾炬帴
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `鍩硅璁板綍_${row.courseCode}.docx`;
+
+ // 妯℃嫙鐐瑰嚮涓嬭浇
+ document.body.appendChild(link);
+ link.click();
+
+ // 娓呯悊涓存椂瀵硅薄
+ setTimeout(() => {
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+ }, 100);
+
+ ElMessage.success("瀵煎嚭鎴愬姛");
+ })
+ .catch(err => {
+ console.error("瀵煎嚭澶辫触:", err);
+ ElMessage.error("瀵煎嚭澶辫触锛岃閲嶈瘯");
+ });
+ };
+ const getList = () => {
+ tableLoading.value = true;
+ safeTrainingListPage({ ...page.value, ...searchForm.value })
+ .then(res => {
+ tableLoading.value = false;
+ tableData.value = res.data.records;
+ page.value.total = res.data.total;
+ })
+ .catch(err => {
+ tableLoading.value = false;
+ });
+ };
+
+ // 鍒嗛〉澶勭悊
+ const pagination = obj => {
+ page.value.current = obj.page;
+ page.value.size = obj.limit;
+ getList();
+ };
+
+ // 閫夋嫨鍙樺寲澶勭悊
+ const handleSelectionChange = selection => {
+ selectedIds.value = selection.map(item => item.id);
+ };
+ const currentUserId = ref("");
+ const currentUserName = ref("");
+ const getCurrentFactoryName = async () => {
+ let res = await userStore.getInfo();
+ currentUserId.value = res.user.userId;
+ currentUserName.value = res.user.nickName;
+ };
+
+ // 鎵撳紑琛ㄥ崟
+ const openForm = (type, row = null) => {
+ dialogType.value = type;
+ if (type === "add") {
+ dialogTitle.value = "鏂板鍩硅";
+ // 閲嶇疆琛ㄥ崟
+ Object.assign(form.value, {
+ courseCode: "", // 璇剧▼缂栧彿
+ trainingDate: "", // 鍩硅鏃ユ湡
+ openingTime: "", // 寮�濮嬫椂闂�
+ endTime: "", // 缁撴潫鏃堕棿
+ trainingObjectives: "", // 鍩硅鐩爣
+ participants: "", // 鍙傚姞瀵硅薄
+ trainingContent: "", // 鍩硅鍐呭
+ trainingLecturer: "", // 鍩硅璁插笀
+ projectCredits: "", // 椤圭洰瀛﹀垎
+ trainingMode: "", // 鍩硅鏂瑰紡
+ placeTraining: "", // 鍩硅鍦扮偣
+ classHour: "", // 璇炬椂
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 鍒涘缓鏃堕棿
+ });
+ } else if (type === "edit" && row) {
+ dialogTitle.value = "缂栬緫鍩硅";
+ Object.assign(form.value, {
+ id: row.id,
+ courseCode: row.courseCode, // 璇剧▼缂栧彿
+ trainingDate: row.trainingDate, // 鍩硅鏃ユ湡
+ openingTime: row.openingTime, // 寮�濮嬫椂闂�
+ endTime: row.endTime, // 缁撴潫鏃堕棿
+ trainingObjectives: row.trainingObjectives, // 鍩硅鐩爣
+ participants: row.participants, // 鍙傚姞瀵硅薄
+ trainingContent: row.trainingContent, // 鍩硅鍐呭
+ trainingLecturer: row.trainingLecturer, // 鍩硅璁插笀
+ projectCredits: row.projectCredits, // 椤圭洰瀛﹀垎
+ trainingMode: row.trainingMode, // 鍩硅鏂瑰紡
+ placeTraining: row.placeTraining, // 鍩硅鍦扮偣
+ classHour: row.classHour, // 璇炬椂
+ createTime: row.createTime || "", // 鍒涘缓鏃堕棿
+ });
+ }
+ dialogVisible.value = true;
+ };
+
+ // 鑾峰彇绫诲瀷鏍囩绫诲瀷
+ const getTypeTagType = type => {
+ const typeMap = {
+ 杈冨ぇ椋庨櫓: "warning",
+ 浣庨闄�: "info",
+ 涓�鑸闄�: "info",
+ 閲嶅ぇ椋庨櫓: "danger",
+ };
+ return typeMap[type] || "info";
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩绫诲瀷
+ const getEfficiencyTagType = efficiency => {
+ const typeMap = {
+ high: "success",
+ medium: "warning",
+ low: "info",
+ };
+ return typeMap[efficiency] || "info";
+ };
+
+ // 鑾峰彇鏁堢巼鏍囩鏂囨湰
+ const getEfficiencyLabel = efficiency => {
+ const efficiencyMap = {
+ high: "鏄捐憲鎻愬崌",
+ medium: "涓�鑸彁鍗�",
+ low: "杞诲井鎻愬崌",
+ };
+ return efficiencyMap[efficiency] || efficiency;
+ };
+
+ // 鑾峰彇鏁堢巼鎻愬崌鐧惧垎姣�
+ const getEfficiencyScore = efficiency => {
+ const scoreMap = {
+ high: 40,
+ medium: 25,
+ low: 15,
+ };
+ return scoreMap[efficiency] || 0;
+ };
+
+ // 鑾峰彇骞冲潎鑺傜渷鏃堕棿
+ const getTimeSaved = efficiency => {
+ const timeMap = {
+ high: "2-3澶�",
+ medium: "1-2澶�",
+ low: "0.5-1澶�",
+ };
+ return timeMap[efficiency] || "鏈煡";
+ };
+
+ /**
+ * 鑾峰彇琛岀被鍚嶏紝鐢ㄤ簬鍒ゆ柇椋庨櫓绛夌骇鏄惁涓洪噸澶ч闄�
+ * @param row 琛屾暟鎹�
+ * @returns 绫诲悕
+ */
+ const getRowClass = ({ row }) => {
+ if (row.riskLevel === "閲嶅ぇ椋庨櫓") {
+ return "danger-row";
+ }
+ return "";
+ };
+
+ // 鎻愪氦鍩硅琛ㄥ崟
+ const submitForm = async () => {
+ try {
+ await formRef.value.validate();
+ if (dialogType.value === "add") {
+ // 鏂板鍩硅鍙拌处
+ safeTrainingAdd({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("娣诲姞鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ } else {
+ // 鏇存柊鍩硅鍙拌处
+ safeTrainingAdd({ ...form.value })
+ .then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鏇存柊鎴愬姛");
+ dialogVisible.value = false;
+ getList();
+ }
+ })
+ .catch(err => {
+ ElMessage.error(err.msg);
+ });
+ }
+ } catch (error) {
+ console.error("琛ㄥ崟楠岃瘉澶辫触:", error);
+ }
+ };
+
+ // 鍒犻櫎鍩硅
+ const handleDelete = () => {
+ if (selectedIds.value.length === 0) {
+ ElMessage.warning("璇烽�夋嫨瑕佸垹闄ょ殑鍩硅");
+ return;
+ }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ // console.log(selectedIds.value);
+ safeTrainingDel(selectedIds.value).then(res => {
+ if (res.code == 200) {
+ ElMessage.success("鍒犻櫎鎴愬姛");
+ selectedIds.value = [];
+ getList();
+ }
+ });
+ })
+ .catch(() => {
+ // 鐢ㄦ埛鍙栨秷
+ });
+ };
+
+ const getKnowledgeTypeTagType = val => {
+ const item = knowledgeTypeOptions.value.find(
+ i => String(i.value) === String(val)
+ );
+ return item?.elTagType || "info";
+ };
+ const handleExport = () => {
+ proxy.download(
+ "/knowledgeBase/export",
+ { ...searchForm.value },
+ "鐭ヨ瘑搴�.xlsx"
+ );
+ };
+</script>
+
+<style scoped>
+ .auto-refresh-info {
+ margin-bottom: 15px;
+ }
+
+ .auto-refresh-info .el-alert {
+ border-radius: 8px;
+ }
+
+ .dialog-footer {
+ text-align: right;
+ }
+
+ .knowledge-detail {
+ padding: 20px 0;
+ }
+
+ .detail-title {
+ font-size: 18px;
+ font-weight: bold;
+ color: #303133;
+ }
+
+ .detail-section {
+ margin-top: 24px;
+ }
+
+ .detail-section h4 {
+ margin: 0 0 12px 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+ }
+
+ .detail-content {
+ background: #f8f9fa;
+ padding: 16px;
+ border-radius: 6px;
+ line-height: 1.6;
+ color: #606266;
+ white-space: pre-wrap;
+ }
+
+ .key-points {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .usage-stats {
+ margin-top: 16px;
+ }
+
+ .stat-item {
+ text-align: center;
+ padding: 20px;
+ background: #f8f9fa;
+ border-radius: 8px;
+ }
+
+ .stat-number {
+ font-size: 24px;
+ font-weight: bold;
+ color: #409eff;
+ margin-bottom: 8px;
+ }
+
+ .stat-label {
+ font-size: 14px;
+ color: #909399;
+ }
+
+ :deep(.danger-row td) {
+ color: #e95a66 !important;
+ }
+ .classtitle {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ border-left: 4px solid #409eff;
+ padding-left: 12px;
+ margin-bottom: 12px;
+ }
+</style>
diff --git a/src/views/salesManagement/customerManagement/index.vue b/src/views/salesManagement/customerManagement/index.vue
new file mode 100644
index 0000000..f66b5f4
--- /dev/null
+++ b/src/views/salesManagement/customerManagement/index.vue
@@ -0,0 +1,423 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-input
+ v-model="searchForm.name"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ @keyup.enter="handleSearch"
+ >
+ <template #prefix>
+ <el-icon><Search /></el-icon>
+ </template>
+ </el-input>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.region" placeholder="璇烽�夋嫨鍖哄煙" clearable>
+ <el-option label="鍗庝笢鍖�" value="鍗庝笢鍖�"></el-option>
+ <el-option label="鍗庡崡鍖�" value="鍗庡崡鍖�"></el-option>
+ <el-option label="鍗庡寳鍖�" value="鍗庡寳鍖�"></el-option>
+ <el-option label="瑗垮崡鍖�" value="瑗垮崡鍖�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.level" placeholder="璇烽�夋嫨瀹㈡埛绛夌骇" clearable>
+ <el-option label="VIP瀹㈡埛" value="VIP瀹㈡埛"></el-option>
+ <el-option label="閲嶈瀹㈡埛" value="閲嶈瀹㈡埛"></el-option>
+ <el-option label="鏅�氬鎴�" value="鏅�氬鎴�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ <el-button style="float: right;" type="primary" @click="handleAdd">
+ 鏂板瀹㈡埛
+ </el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 瀹㈡埛鍒楄〃 -->
+ <el-table
+ :data="filteredList"
+ style="width: 100%"
+ v-loading="loading"
+ border
+ stripe
+ height="calc(100vh - 22em)"
+ >
+ <el-table-column prop="id" label="ID" width="80" align="center"/>
+ <el-table-column prop="name" label="瀹㈡埛鍚嶇О" width="150" />
+ <el-table-column prop="contactPerson" label="鑱旂郴浜�" width="100" />
+ <el-table-column prop="phone" label="鑱旂郴鐢佃瘽" width="140" />
+ <el-table-column prop="email" label="閭" />
+ <el-table-column prop="region" label="鍖哄煙" width="100" />
+ <el-table-column prop="level" label="瀹㈡埛绛夌骇" width="100">
+ <template #default="scope">
+ <el-tag :type="getLevelType(scope.row.level)">
+ {{ scope.row.level }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="salesperson" label="璐熻矗涓氬姟鍛�" width="120" />
+ <el-table-column prop="status" label="鐘舵��" width="80">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleEdit(scope.row)">缂栬緫</el-button>
+ <el-button link type="primary" @click="handleAllocation(scope.row)">鍒嗛厤</el-button>
+ <el-button link type="danger" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.currentPage"
+ :limit="pagination.pageSize"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="name">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ュ鎴峰悕绉�"></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴浜�" prop="contactPerson">
+ <el-input v-model="form.contactPerson" placeholder="璇疯緭鍏ヨ仈绯讳汉"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="phone">
+ <el-input v-model="form.phone" placeholder="璇疯緭鍏ヨ仈绯荤數璇�"></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍖哄煙" prop="region">
+ <el-select v-model="form.region" placeholder="璇烽�夋嫨鍖哄煙" style="width: 100%">
+ <el-option label="鍗庝笢鍖�" value="鍗庝笢鍖�"></el-option>
+ <el-option label="鍗庡崡鍖�" value="鍗庡崡鍖�"></el-option>
+ <el-option label="鍗庡寳鍖�" value="鍗庡寳鍖�"></el-option>
+ <el-option label="瑗垮崡鍖�" value="瑗垮崡鍖�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛绛夌骇" prop="level">
+ <el-select v-model="form.level" placeholder="璇烽�夋嫨瀹㈡埛绛夌骇" style="width: 100%">
+ <el-option label="VIP瀹㈡埛" value="VIP瀹㈡埛"></el-option>
+ <el-option label="閲嶈瀹㈡埛" value="閲嶈瀹㈡埛"></el-option>
+ <el-option label="鏅�氬鎴�" value="鏅�氬鎴�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璐熻矗涓氬姟鍛�" prop="salesperson">
+ <el-select v-model="form.salesperson" placeholder="璇烽�夋嫨涓氬姟鍛�" style="width: 100%">
+ <el-option label="闄堝織寮�" value="闄堝織寮�"></el-option>
+ <el-option label="鍒橀泤濠�" value="鍒橀泤濠�"></el-option>
+ <el-option label="鐜嬪缓鍥�" value="鐜嬪缓鍥�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%">
+ <el-option label="娲昏穬" value="娲昏穬"></el-option>
+ <el-option label="娼滃湪" value="娼滃湪"></el-option>
+ <el-option label="娴佸け" value="娴佸け"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 瀹㈡埛鍒嗛厤瀵硅瘽妗� -->
+ <el-dialog v-model="allocationDialogVisible" title="瀹㈡埛鍒嗛厤" width="500px">
+ <el-form label-width="100px">
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <span>{{ currentCustomer.name }}</span>
+ </el-form-item>
+ <el-form-item label="褰撳墠涓氬姟鍛�">
+ <span>{{ currentCustomer.salesperson }}</span>
+ </el-form-item>
+ <el-form-item label="閲嶆柊鍒嗛厤">
+ <el-select v-model="newSalesperson" placeholder="璇烽�夋嫨鏂颁笟鍔″憳" style="width: 100%">
+ <el-option label="闄堝織寮�" value="闄堝織寮�"></el-option>
+ <el-option label="鍒橀泤濠�" value="鍒橀泤濠�"></el-option>
+ <el-option label="鐜嬪缓鍥�" value="鐜嬪缓鍥�"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒嗛厤鍘熷洜">
+ <el-input v-model="allocationReason" type="textarea" rows="3" placeholder="璇疯緭鍏ュ垎閰嶅師鍥�"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="allocationDialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="saveAllocation">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Search } from '@element-plus/icons-vue'
+import Pagination from '@/components/PIMTable/Pagination.vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const searchForm = reactive({
+ name: '',
+ region: '',
+ level: ''
+})
+
+const customerList = ref([
+ {
+ id: 1,
+ name: '涓婃捣绉戞妧鏈夐檺鍏徃',
+ contactPerson: '闄堝織寮�',
+ phone: '021-12345678',
+ email: 'zhang@shanghai-tech.com',
+ region: '鍗庝笢鍖�',
+ level: 'VIP瀹㈡埛',
+ salesperson: '闄堝織寮�',
+ status: '娲昏穬'
+ },
+ {
+ id: 2,
+ name: '娣卞湷鐢靛瓙鏈夐檺鍏徃',
+ contactPerson: '鍒橀泤濠�',
+ phone: '0755-87654321',
+ email: 'li@shenzhen-elec.com',
+ region: '鍗庡崡鍖�',
+ level: '閲嶈瀹㈡埛',
+ salesperson: '鍒橀泤濠�',
+ status: '娲昏穬'
+ },
+ {
+ id: 3,
+ name: '鍖椾含璐告槗鍏徃',
+ contactPerson: '鐜嬪缓鍥�',
+ phone: '010-11223344',
+ email: 'wang@beijing-trade.com',
+ region: '鍗庡寳鍖�',
+ level: '鏅�氬鎴�',
+ salesperson: '鐜嬪缓鍥�',
+ status: '娼滃湪'
+ }
+])
+
+const pagination = reactive({
+ total: 3,
+ currentPage: 1,
+ pageSize: 10
+})
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('鏂板瀹㈡埛')
+const form = reactive({
+ name: '',
+ contactPerson: '',
+ phone: '',
+ email: '',
+ region: '',
+ level: '',
+ salesperson: '',
+ status: '娲昏穬'
+})
+
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ュ鎴峰悕绉�', trigger: 'blur' }],
+ contactPerson: [{ required: true, message: '璇疯緭鍏ヨ仈绯讳汉', trigger: 'blur' }],
+ phone: [{ required: true, message: '璇疯緭鍏ヨ仈绯荤數璇�', trigger: 'blur' }],
+ email: [{ required: true, message: '璇疯緭鍏ラ偖绠�', trigger: 'blur' }],
+ region: [{ required: true, message: '璇烽�夋嫨鍖哄煙', trigger: 'change' }],
+ level: [{ required: true, message: '璇烽�夋嫨瀹㈡埛绛夌骇', trigger: 'change' }],
+ salesperson: [{ required: true, message: '璇烽�夋嫨涓氬姟鍛�', trigger: 'change' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+}
+
+const isEdit = ref(false)
+const editId = ref(null)
+const allocationDialogVisible = ref(false)
+const currentCustomer = ref({})
+const newSalesperson = ref('')
+const allocationReason = ref('')
+const formRef = ref()
+
+// 璁$畻灞炴��
+const filteredList = computed(() => {
+ let list = customerList.value
+ if (searchForm.name) {
+ list = list.filter(item => item.name.includes(searchForm.name))
+ }
+ if (searchForm.region) {
+ list = list.filter(item => item.region === searchForm.region)
+ }
+ if (searchForm.level) {
+ list = list.filter(item => item.level === searchForm.level)
+ }
+ return list
+})
+
+// 鏂规硶
+const getLevelType = (level) => {
+ const levelMap = {
+ 'VIP瀹㈡埛': 'danger',
+ '閲嶈瀹㈡埛': 'warning',
+ '鏅�氬鎴�': 'info'
+ }
+ return levelMap[level] || 'info'
+}
+
+const getStatusType = (status) => {
+ const statusMap = {
+ '娲昏穬': 'success',
+ '娼滃湪': 'warning',
+ '娴佸け': 'danger'
+ }
+ return statusMap[status] || 'info'
+}
+
+const handleSearch = () => {
+ // 鎼滅储閫昏緫宸插湪computed涓鐞�
+}
+
+const resetSearch = () => {
+ searchForm.name = ''
+ searchForm.region = ''
+ searchForm.level = ''
+}
+
+const handleAdd = () => {
+ dialogTitle.value = '鏂板瀹㈡埛'
+ isEdit.value = false
+ form.name = ''
+ form.contactPerson = ''
+ form.phone = ''
+ form.email = ''
+ form.region = ''
+ form.level = ''
+ form.salesperson = ''
+ form.status = '娲昏穬'
+ dialogVisible.value = true
+}
+
+const handleEdit = (row) => {
+ dialogTitle.value = '缂栬緫瀹㈡埛'
+ isEdit.value = true
+ editId.value = row.id
+ Object.assign(form, row)
+ dialogVisible.value = true
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭鍒犻櫎璇ュ鎴峰悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ const index = customerList.value.findIndex(item => item.id === row.id)
+ if (index > -1) {
+ customerList.value.splice(index, 1)
+ pagination.total--
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ }
+ })
+}
+
+const handleAllocation = (row) => {
+ currentCustomer.value = row
+ newSalesperson.value = ''
+ allocationReason.value = ''
+ allocationDialogVisible.value = true
+}
+
+const saveAllocation = () => {
+ if (!newSalesperson.value) {
+ ElMessage.warning('璇烽�夋嫨鏂颁笟鍔″憳')
+ return
+ }
+
+ const index = customerList.value.findIndex(item => item.id === currentCustomer.value.id)
+ if (index > -1) {
+ customerList.value[index].salesperson = newSalesperson.value
+ ElMessage.success('瀹㈡埛鍒嗛厤鎴愬姛')
+ allocationDialogVisible.value = false
+ }
+}
+
+const handleSubmit = () => {
+ formRef.value.validate((valid) => {
+ if (valid) {
+ if (isEdit.value) {
+ // 缂栬緫
+ const index = customerList.value.findIndex(item => item.id === editId.value)
+ if (index > -1) {
+ customerList.value[index] = { ...form, id: editId.value }
+ ElMessage.success('缂栬緫鎴愬姛')
+ }
+ } else {
+ // 鏂板
+ const newId = Math.max(...customerList.value.map(item => item.id)) + 1
+ customerList.value.push({
+ ...form,
+ id: newId
+ })
+ pagination.total++
+ ElMessage.success('鏂板鎴愬姛')
+ }
+ dialogVisible.value = false
+ }
+ })
+}
+
+const handleCurrentChange = (val) => {
+ pagination.currentPage = val.page
+ pagination.pageSize = val.limit
+}
+</script>
+
+<style scoped>
+.search-row {
+ margin-bottom: 20px;
+}
+</style>
diff --git a/src/views/salesManagement/deliveryLedger/index.vue b/src/views/salesManagement/deliveryLedger/index.vue
new file mode 100644
index 0000000..c8890cc
--- /dev/null
+++ b/src/views/salesManagement/deliveryLedger/index.vue
@@ -0,0 +1,917 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="閿�鍞鍗曞彿锛�">
+ <el-input
+ v-model="searchForm.salesContractNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="杞︾墝鍙凤細">
+ <el-input
+ v-model="searchForm.shippingCarNumber"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="蹇�掑崟鍙凤細">
+ <el-input
+ v-model="searchForm.expressNumber"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ style="width: 200px"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery"> 鎼滅储</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ </div>
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ :row-key="(row) => row.id"
+ style="width: 100%"
+ height="calc(100vh - 21.5em)"
+ >
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column
+ label="閿�鍞鍗�"
+ prop="salesContractNo"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="鍙戣揣璁㈠崟鍙�"
+ prop="shippingNo"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="瀹㈡埛鍚嶇О"
+ prop="customerName"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="浜у搧鍚嶇О"
+ prop="productName"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="瑙勬牸鍨嬪彿"
+ prop="specificationModel"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="鍙戣揣鏃堕棿"
+ prop="shippingDate"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="鍙戣揣鏁伴噺"
+ prop="totalQuantity"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="鍙戣揣杞︾墝鍙�"
+ prop="shippingCarNumber"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="蹇�掑叕鍙�"
+ prop="expressCompany"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="蹇�掑崟鍙�"
+ prop="expressNumber"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="瀹℃牳鐘舵��"
+ prop="status"
+ align="center"
+ width="100"
+ >
+ <template #default="scope">
+ <el-tag :type="getApprovalStatusType(scope.row.status)">
+ {{ getApprovalStatusText(scope.row.status) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍑哄簱鍗曞彿"
+ prop="outboundBatches"
+ show-overflow-tooltip
+ width="130"
+ />
+ <el-table-column fixed="right" label="鎿嶄綔" width="220" align="center">
+ <template #default="scope">
+ <!-- <el-button-->
+ <!-- link-->
+ <!-- type="primary"-->
+ <!-- :disabled="!isApproved(scope.row.status)"-->
+ <!-- @click="openForm('edit', scope.row)">鍙戣揣-->
+ <!-- </el-button>-->
+ <el-button
+ link
+ type="primary"
+ style="color: #67c23a"
+ @click="openDetail(scope.row)"
+ >璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="danger"
+ :disabled="isApproving(scope.row.status)"
+ @click="handleDeleteSingle(scope.row)"
+ >鍒犻櫎
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+ <el-dialog
+ v-model="dialogFormVisible"
+ :title="operationType === 'add' ? '鏂板鍙戣揣鍙拌处' : '缂栬緫鍙戣揣鍙拌处'"
+ width="40%"
+ @close="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="120px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="鍙戣揣绫诲瀷锛�" prop="type">
+ <el-select
+ v-model="form.type"
+ placeholder="璇烽�夋嫨鍙戣揣绫诲瀷"
+ style="width: 100%"
+ @change="handleShippingTypeChange"
+ >
+ <el-option label="璐ц溅" value="璐ц溅" />
+ <el-option label="蹇��" value="蹇��" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="鍙戣揣鏃ユ湡锛�" prop="shippingDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.shippingDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨鍙戣揣鏃ユ湡"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24" v-if="form.type === '璐ц溅'">
+ <el-form-item label="鍙戣揣杞︾墝鍙凤細" prop="shippingCarNumber">
+ <el-input
+ v-model="form.shippingCarNumber"
+ placeholder="璇疯緭鍏ュ彂璐ц溅鐗屽彿"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" v-else>
+ <el-form-item label="蹇�掑叕鍙革細" prop="expressCompany">
+ <el-input
+ v-model="form.expressCompany"
+ placeholder="璇疯緭鍏ュ揩閫掑叕鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30" v-if="form.type === '蹇��'">
+ <el-col :span="24">
+ <el-form-item label="蹇�掑崟鍙凤細" prop="expressNumber">
+ <el-input
+ v-model="form.expressNumber"
+ placeholder="璇疯緭鍏ュ揩閫掑崟鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="鍙戣揣鍥剧墖锛�">
+ <ImageUpload v-model:file-list="deliveryFileList" :limit="9" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 璇︽儏寮规 -->
+ <el-dialog
+ v-model="detailDialogVisible"
+ title="鍙戣揣鍙拌处璇︽儏"
+ width="55%"
+ @close="closeDetail"
+ >
+ <div v-if="detailRow" class="detail-wrapper">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="閿�鍞鍗�">{{
+ detailRow.salesContractNo || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣璁㈠崟鍙�">{{
+ detailRow.shippingNo || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{
+ detailRow.customerName || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="浜у搧鍚嶇О">{{
+ detailRow.productName || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="瑙勬牸鍨嬪彿">{{
+ detailRow.specificationModel || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣绫诲瀷">{{
+ detailRow.type || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣鏃ユ湡">{{
+ detailRow.shippingDate || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="瀹℃牳鐘舵��">{{
+ getApprovalStatusText(detailRow.status)
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍙戣揣杞︾墝鍙�">{{
+ detailRow.shippingCarNumber || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="蹇�掑叕鍙�">{{
+ detailRow.expressCompany || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="蹇�掑崟鍙�" :span="2">{{
+ detailRow.expressNumber || "--"
+ }}</el-descriptions-item>
+ <el-descriptions-item label="鍑哄簱鍗曞彿" :span="2">{{
+ detailRow.outboundBatches || "--"
+ }}</el-descriptions-item>
+ </el-descriptions>
+ <el-table
+ :data="getDeliveryProductInfoList()"
+ border
+ size="small"
+ class="delivery-product-table"
+ style="width: 100%; margin-top: 16px"
+ >
+ <el-table-column
+ label="鎵瑰彿"
+ prop="batchNo"
+ min-width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="浜у搧鍚嶇О"
+ prop="productName"
+ min-width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="瑙勬牸鍨嬪彿"
+ prop="specificationModel"
+ min-width="160"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="鍙戣揣鏁伴噺"
+ prop="deliveryQuantity"
+ min-width="120"
+ align="center"
+ />
+ </el-table>
+ <ImagePreview :file-list="detailRow.storageBlobVOs || []" />
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDetail">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import pagination from "@/components/PIMTable/Pagination.vue";
+import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
+import { ElMessageBox } from "element-plus";
+import { getCurrentDate } from "@/utils/index.js";
+import {
+ deliveryLedgerListPage,
+ delDeliveryLedger,
+ deductStock,
+ getDeliveryDetail,
+} from "@/api/salesManagement/deliveryLedger.js";
+import { delLedgerFile } from "@/api/salesManagement/salesLedger.js";
+import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
+import ImagePreview from "@/components/AttachmentPreview/image/index.vue";
+
+const { proxy } = getCurrentInstance();
+const tableData = ref([]);
+const selectedRows = ref([]);
+const tableLoading = ref(false);
+const salesOrderOptions = ref([]);
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+const deliveryFileList = ref([]);
+// 璇︽儏寮规
+const detailDialogVisible = ref(false);
+const detailRow = ref(null);
+const detailProductList = ref([]);
+
+// 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+const operationType = ref("");
+const dialogFormVisible = ref(false);
+const data = reactive({
+ searchForm: {
+ salesContractNo: "", // 閿�鍞鍗曞彿
+ shippingCarNumber: "", // 杞︾墝鍙�
+ expressNumber: "", // 蹇�掑崟鍙�
+ },
+ form: {
+ id: null,
+ salesContractNo: "",
+ customerName: "",
+ specificationModel: "",
+ productName: "",
+ type: "璐ц溅", // 璐ц溅, 蹇��
+ shippingDate: "",
+ shippingCarNumber: "",
+ expressCompany: "",
+ expressNumber: "", // 蹇�掑崟鍙�
+ },
+ rules: {
+ salesContractNo: [
+ { required: true, message: "璇烽�夋嫨閿�鍞鍗�", trigger: "change" },
+ ],
+ customerName: [
+ { required: true, message: "璇疯緭鍏ュ鎴峰悕绉�", trigger: "blur" },
+ ],
+ type: [{ required: true, message: "璇烽�夋嫨鍙戣揣绫诲瀷", trigger: "change" }],
+ shippingDate: [
+ { required: true, message: "璇烽�夋嫨鍙戣揣鏃堕棿", trigger: "change" },
+ ],
+ shippingCarNumber: [
+ {
+ validator: (_, value, callback) =>
+ validateShippingCarNumber(value, callback),
+ trigger: "blur",
+ },
+ ],
+ expressCompany: [
+ {
+ validator: (_, value, callback) =>
+ validateExpressCompany(value, callback),
+ trigger: "blur",
+ },
+ ],
+ },
+});
+const { form, rules } = toRefs(data);
+const { searchForm } = toRefs(data);
+
+// 鏌ヨ鍒楄〃
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+
+const getList = () => {
+ tableLoading.value = true;
+ deliveryLedgerListPage({ ...searchForm.value, ...page })
+ .then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.data.records || [];
+ total.value = res.data.total || 0;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+};
+
+// 閿�鍞鍗曞彉鍖栨椂鑷姩濉厖瀹㈡埛鍚嶇О
+const handleSalesOrderChange = (value) => {
+ const selectedOrder = salesOrderOptions.value.find(
+ (item) => item.salesContractNo === value
+ );
+ if (selectedOrder) {
+ form.value.customerName = selectedOrder.customerName;
+ }
+};
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+
+// 鎵撳紑寮规
+const openForm = async (type, row) => {
+ // 鍙戣揣锛氫粎鈥滃鏍搁�氳繃鈥濆厑璁哥紪杈�
+ if (type === "edit" && row && !isApproved(row.status)) {
+ proxy.$modal.msgWarning("鍙湁瀹℃牳閫氳繃鐨勬暟鎹墠鍙互鍙戣揣");
+ return;
+ }
+
+ operationType.value = type;
+
+ if (type === "edit" && row) {
+ form.value = {
+ id: row.id ?? null,
+ salesContractNo: row.salesContractNo ?? "",
+ customerName: row.customerName ?? "",
+ type: row.type || "璐ц溅",
+ shippingDate: row.shippingDate || getCurrentDate(),
+ shippingCarNumber: row.shippingCarNumber ?? "",
+ expressCompany: row.expressCompany ?? "",
+ expressNumber: row.expressNumber ?? "",
+ };
+ deliveryFileList.value = row.storageBlobVOs || [];
+ }
+
+ dialogFormVisible.value = true;
+};
+
+// 鎵撳紑璇︽儏寮规
+const openDetail = async (row) => {
+ detailRow.value = row || null;
+ detailProductList.value = [];
+ detailDialogVisible.value = true;
+ if (!row?.id) return;
+ try {
+ const res = await getDeliveryDetail(row.id);
+ const detailData = res?.data;
+ detailRow.value = {
+ ...row,
+ ...(Array.isArray(detailData) ? {} : detailData || {}),
+ };
+ detailProductList.value = resolveDeliveryDetailList(detailData);
+ } catch (error) {
+ proxy.$modal.msgError("鍔犺浇鍙戣揣鍙拌处璇︽儏澶辫触");
+ }
+};
+const resolveDeliveryDetailList = (data) => {
+ if (Array.isArray(data)) return data;
+ if (!data || typeof data !== "object") return [];
+ return (
+ [
+ data.batchNoDetailList,
+ data.batchNoList,
+ data.shippingBatchList,
+ data.shippingInfoDetailList,
+ data.detailList,
+ data.batchDetailList,
+ data.rows,
+ data.records,
+ data.list,
+ data.data,
+ ].find((value) => Array.isArray(value) && value.length) || []
+ );
+};
+const getDeliveryProductInfoList = () => {
+ const row = detailRow.value;
+ if (!row) return [];
+ const normalizeBatchNoList = (value) => {
+ if (Array.isArray(value)) return value;
+ if (typeof value === "string" && value.includes(",")) {
+ return value
+ .split(",")
+ .map((item) => item.trim())
+ .filter(Boolean);
+ }
+ return value ? [value] : [];
+ };
+ const detailList = detailProductList.value.length
+ ? detailProductList.value
+ : [
+ row.batchNoDetailList,
+ row.batchNoList,
+ row.shippingBatchList,
+ row.shippingInfoDetailList,
+ row.detailList,
+ row.batchDetailList,
+ ].find((value) => Array.isArray(value) && value.length);
+ const batchNoList = normalizeBatchNoList(row.batchNo);
+ const toTableRow = (item = {}) => ({
+ batchNo:
+ typeof item === "string" || typeof item === "number"
+ ? item
+ : item.batchNo ?? item.batchNumber ?? row.batchNo ?? "--",
+ productName: item.productName ?? row.productName ?? "--",
+ specificationModel:
+ item.specificationModel ?? item.model ?? row.specificationModel ?? "--",
+ deliveryQuantity:
+ item.deliveryQuantity ??
+ item.quantity ??
+ item.shippingQuantity ??
+ row.deliveryQuantity ??
+ row.quantity ??
+ "--",
+ });
+ if (detailList?.length) {
+ return detailList.map(toTableRow);
+ }
+ if (batchNoList.length) {
+ return batchNoList.map((batchNo) => toTableRow({ batchNo }));
+ }
+ return [toTableRow()];
+};
+const closeDetail = () => {
+ detailDialogVisible.value = false;
+ detailRow.value = null;
+ detailProductList.value = [];
+};
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs["formRef"].validate((valid) => {
+ if (valid) {
+ const payload = {
+ id: form.value.id,
+ type: form.value.type,
+ shippingDate: form.value.shippingDate,
+ shippingCarNumber:
+ form.value.type === "璐ц溅" ? form.value.shippingCarNumber : "",
+ expressCompany:
+ form.value.type === "蹇��" ? form.value.expressCompany : "",
+ expressNumber:
+ form.value.type === "蹇��" ? form.value.expressNumber : "",
+ storageBlobDTOs: deliveryFileList.value || [],
+ };
+ deductStock(payload).then((res) => {
+ proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+ closeDia();
+ getList();
+ });
+ }
+ });
+};
+
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ deliveryFileList.value = []; // 娓呯┖鏂囦欢鍒楄〃
+ dialogFormVisible.value = false;
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/shippingInfo/export", {}, "鍙戣揣鍙拌处.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 鎵归噺鍒犻櫎
+const handleDelete = () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+
+ // 妫�鏌ラ�変腑鐨勮鏄惁鏈�"瀹℃牳涓�"鐘舵��
+ const approvingRows = selectedRows.value.filter((row) =>
+ isApproving(row.status)
+ );
+ if (approvingRows.length > 0) {
+ proxy.$modal.msgWarning("瀹℃牳涓殑鏁版嵁涓嶈兘鍒犻櫎");
+ return;
+ }
+
+ const ids = selectedRows.value.map((item) => item.id);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delDeliveryLedger(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 鍗曚釜鍒犻櫎
+const handleDeleteSingle = (row) => {
+ // 妫�鏌ユ槸鍚︿负"瀹℃牳涓�"鐘舵��
+ if (isApproving(row.status)) {
+ proxy.$modal.msgWarning("瀹℃牳涓殑鏁版嵁涓嶈兘鍒犻櫎");
+ return;
+ }
+
+ ElMessageBox.confirm("姝ゆ搷浣滃皢鍒犻櫎璇ヨ褰曪紝鏄惁纭锛�", "鍒犻櫎", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delDeliveryLedger([row.id]).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 鍙戣揣绫诲瀷鏍¢獙锛氳揣杞︽椂瑕佹眰杞︾墝锛屽揩閫掓椂瑕佹眰蹇�掑叕鍙�
+const validateShippingCarNumber = (value, callback) => {
+ if (form.value.type === "璐ц溅") {
+ if (!value) return callback(new Error("璇疯緭鍏ュ彂璐ц溅鐗屽彿"));
+ }
+ callback();
+};
+const validateExpressCompany = (value, callback) => {
+ if (form.value.type === "蹇��") {
+ if (!value) return callback(new Error("璇疯緭鍏ュ揩閫掑叕鍙�"));
+ }
+ callback();
+};
+
+// 鍙戣揣鍥剧墖涓婁紶鍓嶆牎妫�
+function handleDeliveryBeforeUpload(file) {
+ // 鏍℃鏂囦欢绫诲瀷
+ const isImage =
+ file.type === "image/png" ||
+ file.type === "image/jpeg" ||
+ file.type === "image/jpg";
+ if (!isImage) {
+ proxy.$modal.msgError("鍙兘涓婁紶 jpg銆乯peg銆乸ng 鏍煎紡鐨勫浘鐗�!");
+ return false;
+ }
+ // 鏍℃鏂囦欢澶у皬
+ const isLt10M = file.size / 1024 / 1024 < 10;
+ if (!isLt10M) {
+ proxy.$modal.msgError("涓婁紶鍥剧墖澶у皬涓嶈兘瓒呰繃 10MB!");
+ return false;
+ }
+ proxy.$modal.loading("姝e湪涓婁紶鍥剧墖锛岃绋嶅��...");
+ return true;
+}
+
+// 鍙戣揣鍥剧墖涓婁紶澶辫触
+function handleDeliveryUploadError(err) {
+ proxy.$modal.msgError("涓婁紶鍥剧墖澶辫触");
+ proxy.$modal.closeLoading();
+}
+
+// 鍙戣揣鍥剧墖涓婁紶鎴愬姛鍥炶皟
+function handleDeliveryUploadSuccess(res, file, uploadFiles) {
+ proxy.$modal.closeLoading();
+ if (res.code === 200) {
+ file.tempId = res.data.tempId;
+ proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+ } else {
+ proxy.$modal.msgError(res.msg);
+ proxy.$refs.deliveryFileUpload.handleRemove(file);
+ }
+}
+
+// 绉婚櫎鍙戣揣鍥剧墖
+function handleDeliveryRemove(file) {
+ console.log("file--", file);
+ // 濡傛灉鏄紪杈戞ā寮忎笖鏂囦欢鏈� id锛岄渶瑕佽皟鐢ㄦ帴鍙e垹闄�
+ if (operationType.value === "edit") {
+ let ids = [];
+ ids.push(file.uid);
+ delLedgerFile(ids)
+ .then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ // 浠庢枃浠跺垪琛ㄤ腑绉婚櫎
+ const index = deliveryFileList.value.findIndex(
+ (item) => item.uid === file.uid
+ );
+ if (index > -1) {
+ deliveryFileList.value.splice(index, 1);
+ }
+ })
+ .catch(() => {
+ proxy.$modal.msgError("鍒犻櫎澶辫触");
+ });
+ } else {
+ // 鏂板妯″紡鎴栨病鏈� id 鐨勬枃浠讹紝鐩存帴浠庡垪琛ㄤ腑绉婚櫎
+ const index = deliveryFileList.value.findIndex(
+ (item) => item.uid === file.uid
+ );
+ if (index > -1) {
+ deliveryFileList.value.splice(index, 1);
+ }
+ }
+}
+
+// 鍙戣揣绫诲瀷鍒囨崲鏃舵竻绌哄搴斿瓧娈�
+const handleShippingTypeChange = (val) => {
+ if (val === "璐ц溅") {
+ form.value.expressCompany = "";
+ form.value.expressNumber = "";
+ } else {
+ form.value.shippingCarNumber = "";
+ }
+};
+
+// 鑾峰彇瀹℃牳鐘舵�佹枃鏈�
+const getApprovalStatusText = (status) => {
+ if (status === null || status === undefined || status === "") {
+ return "寰呭鏍�";
+ }
+ // 濡傛灉鏄暟瀛�
+ if (typeof status === "number") {
+ const statusMap = {
+ 0: "寰呭鏍�",
+ 1: "瀹℃牳涓�",
+ 2: "瀹℃牳鎷掔粷",
+ 3: "瀹℃牳閫氳繃",
+ };
+ return statusMap[status] || "寰呭鏍�";
+ }
+ // 濡傛灉鏄瓧绗︿覆锛岀洿鎺ヨ繑鍥炴垨鏄犲皠
+ const statusStr = String(status).trim();
+ const statusTextMap = {
+ 寰呭鏍�: "寰呭鏍�",
+ 瀹℃牳涓�: "瀹℃牳涓�",
+ 瀹℃牳鎷掔粷: "瀹℃牳鎷掔粷",
+ 瀹℃牳閫氳繃: "瀹℃牳閫氳繃",
+ 宸插彂璐�: "宸插彂璐�",
+ 0: "寰呭鏍�",
+ 1: "瀹℃牳涓�",
+ 2: "瀹℃牳鎷掔粷",
+ 3: "瀹℃牳閫氳繃",
+ };
+ return statusTextMap[statusStr] || statusStr || "寰呭鏍�";
+};
+
+// 鑾峰彇瀹℃牳鐘舵�佹爣绛剧被鍨嬶紙棰滆壊锛�
+const getApprovalStatusType = (status) => {
+ if (status === null || status === undefined || status === "") {
+ return "info";
+ }
+ // 濡傛灉鏄暟瀛�
+ if (typeof status === "number") {
+ const typeMap = {
+ 0: "info", // 寰呭鏍� - 鐏拌壊
+ 1: "warning", // 瀹℃牳涓� - 榛勮壊
+ 2: "danger", // 瀹℃牳鎷掔粷 - 绾㈣壊
+ 3: "success", // 瀹℃牳閫氳繃 - 缁胯壊
+ };
+ return typeMap[status] || "info";
+ }
+ // 濡傛灉鏄瓧绗︿覆
+ const statusStr = String(status).trim();
+ const typeTextMap = {
+ 寰呭鏍�: "info",
+ 瀹℃牳涓�: "warning",
+ 瀹℃牳鎷掔粷: "danger",
+ 瀹℃牳閫氳繃: "success",
+ 宸插彂璐�: "success",
+ 0: "info",
+ 1: "warning",
+ 2: "danger",
+ 3: "success",
+ };
+ return typeTextMap[statusStr] || "info";
+};
+
+// 妫�鏌ュ鏍哥姸鎬佹槸鍚︿负"瀹℃牳閫氳繃"
+const isApproved = (status) => {
+ if (status === null || status === undefined || status === "") {
+ return false;
+ }
+ // 濡傛灉鏄暟瀛楋紝3 琛ㄧず瀹℃牳閫氳繃
+ if (typeof status === "number") {
+ return status === 3;
+ }
+ // 濡傛灉鏄瓧绗︿覆
+ const statusStr = String(status).trim();
+ return statusStr === "瀹℃牳閫氳繃" || statusStr === "3";
+};
+
+// 妫�鏌ュ鏍哥姸鎬佹槸鍚︿负"瀹℃牳涓�"
+const isApproving = (status) => {
+ if (status === null || status === undefined || status === "") {
+ return false;
+ }
+ // 濡傛灉鏄暟瀛楋紝1 琛ㄧず瀹℃牳涓�
+ if (typeof status === "number") {
+ return status === 1;
+ }
+ // 濡傛灉鏄瓧绗︿覆
+ const statusStr = String(status).trim();
+ return statusStr === "瀹℃牳涓�" || statusStr === "1";
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.table_list {
+ margin-top: unset;
+}
+
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+// 闅愯棌鍥剧墖涓婁紶缁勪欢鐨勯瑙堟寜閽紙鏀惧ぇ闀滐級
+:deep(.el-upload-list--picture-card .el-upload-list__item-actions) {
+ .el-upload-list__item-preview {
+ display: none;
+ }
+}
+
+.detail-wrapper {
+ padding: 8px 0;
+}
+
+.detail-images {
+ margin-top: 16px;
+}
+
+.detail-images-title {
+ font-weight: 600;
+ margin-bottom: 10px;
+ color: #303133;
+}
+
+.detail-image {
+ width: 120px;
+ height: 120px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ border-radius: 6px;
+}
+
+.detail-images-empty {
+ margin-top: 16px;
+ color: #909399;
+}
+</style>
diff --git a/src/views/salesManagement/indicatorStats/index.vue b/src/views/salesManagement/indicatorStats/index.vue
new file mode 100644
index 0000000..8ae15ed
--- /dev/null
+++ b/src/views/salesManagement/indicatorStats/index.vue
@@ -0,0 +1,715 @@
+<template>
+ <div class="app-container indicator-stats">
+ <!-- KPI 姹囨�� -->
+ <el-row :gutter="20" class="stats-row">
+ <el-col :xs="24" :sm="12" :md="8">
+ <div class="stat-card stat-card-blue">
+ <div class="stat-icon-wrapper">
+ <div class="stat-icon">
+ <el-icon :size="32"><Document /></el-icon>
+ </div>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ indicatorKpis.orderCount.toLocaleString() }}</div>
+ <div class="stat-label">璁㈠崟鏁伴噺</div>
+ </div>
+ <div class="stat-bg-decoration"></div>
+ </div>
+ </el-col>
+ <el-col :xs="24" :sm="12" :md="8">
+ <div class="stat-card stat-card-green">
+ <div class="stat-icon-wrapper">
+ <div class="stat-icon">
+ <el-icon :size="32"><Tickets /></el-icon>
+ </div>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">楼{{ indicatorKpis.salesAmount.toLocaleString() }}</div>
+ <div class="stat-label">閿�鍞</div>
+ </div>
+ <div class="stat-bg-decoration"></div>
+ </div>
+ </el-col>
+ <el-col :xs="24" :sm="12" :md="8">
+ <div class="stat-card stat-card-orange">
+ <div class="stat-icon-wrapper">
+ <div class="stat-icon">
+ <el-icon :size="32"><Van /></el-icon>
+ </div>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ indicatorKpis.shipRate }}</div>
+ <div class="stat-label">鍙戣揣鐜�</div>
+ </div>
+ <div class="stat-bg-decoration"></div>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 鍥捐〃鍖猴紙鍖呭惈绛涢�夋潯浠讹級 -->
+ <el-card class="chart-card" shadow="hover">
+ <template #header>
+ <div class="card-header">
+ <div class="header-left">
+ <span class="card-title">閿�鍞秼鍔垮垎鏋�</span>
+ <span class="card-subtitle">绛涢�夋潯浠朵粎褰卞搷涓嬫柟鍥捐〃鏁版嵁</span>
+ </div>
+ </div>
+ </template>
+
+ <!-- 鍥捐〃绛涢�夋潯浠� -->
+ <div class="chart-filter-section">
+ <el-row :gutter="16" class="search-row">
+ <el-col :xs="24" :sm="12" :md="6">
+ <div class="filter-item">
+ <label class="filter-label">浜у搧绫诲埆</label>
+ <el-tree-select
+ v-model="indicatorFilter.productCategory"
+ placeholder="璇烽�夋嫨浜у搧绫诲埆"
+ clearable
+ check-strictly
+ :data="productOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ />
+ </div>
+ </el-col>
+ <el-col :xs="24" :sm="12" :md="6">
+ <div class="filter-item">
+ <label class="filter-label">瀹㈡埛</label>
+ <el-select
+ v-model="indicatorFilter.customerName"
+ placeholder="璇烽�夋嫨瀹㈡埛"
+ clearable
+ filterable
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in customerOption"
+ :key="item.id"
+ :label="item.customerName"
+ :value="item.customerName"
+ />
+ </el-select>
+ </div>
+ </el-col>
+ <el-col :xs="24" :sm="12" :md="6">
+ <div class="filter-item">
+ <label class="filter-label">鏃ユ湡鑼冨洿</label>
+ <el-date-picker
+ v-model="indicatorFilter.dateRange"
+ type="monthrange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM"
+ style="width: 100%"
+ />
+ </div>
+ </el-col>
+ <el-col :xs="24" :sm="12" :md="6">
+ <div class="filter-item filter-buttons">
+ <el-button type="primary" :loading="loading" @click="applyIndicatorFilter">
+ <el-icon><Search /></el-icon>
+ 鏌ヨ鍥捐〃
+ </el-button>
+ <el-button @click="resetIndicatorFilter">
+ <el-icon><Refresh /></el-icon>
+ 閲嶇疆
+ </el-button>
+ </div>
+ </el-col>
+ </el-row>
+ </div>
+
+ <!-- 鍥捐〃灞曠ず鍖� -->
+ <div class="chart-container" v-loading="loading">
+ <div ref="indicatorChartRef" class="chart-wrapper"></div>
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
+import { Document, Van, Tickets, Search, Refresh } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+import { getTotalStatistics, getStatisticsTable } from '@/api/salesManagement/indicatorStats'
+import { productTreeList } from '@/api/basicData/product.js'
+import { customerList } from '@/api/salesManagement/salesLedger.js'
+import { ElMessage } from 'element-plus'
+
+const indicatorKpis = reactive({
+ orderCount: 0,
+ salesAmount: 0,
+ shipRate: 0
+})
+
+// 鏄惁灞曠ず閿�鍞洟闃熸槑缁嗚〃锛屾寜闇�寮�鍚�
+const showTeamPerformance = ref(false)
+const loading = ref(false)
+
+const indicatorFilter = reactive({
+ productCategory: '',
+ customerName: '',
+ dateRange: []
+})
+
+const indicatorChartRef = ref(null)
+let indicatorChart = null
+
+const productOptions = ref([])
+const customerOption = ref([])
+
+// 杞崲浜у搧鏍戞暟鎹紝灏� id 鏀逛负 value
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ }
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children)
+ }
+ return newItem
+ })
+}
+
+// 鑾峰彇浜у搧鏍戞暟鎹�
+const getProductOptions = () => {
+ return productTreeList().then((res) => {
+ productOptions.value = convertIdToValue(res)
+ }).catch((error) => {
+ console.error('鑾峰彇浜у搧鏍戝け璐�:', error)
+ ElMessage.error('鑾峰彇浜у搧绫诲埆澶辫触')
+ })
+}
+
+// 鑾峰彇瀹㈡埛鍒楄〃
+const getCustomerList = () => {
+ return customerList().then((res) => {
+ customerOption.value = res || []
+ }).catch((error) => {
+ console.error('鑾峰彇瀹㈡埛鍒楄〃澶辫触:', error)
+ ElMessage.error('鑾峰彇瀹㈡埛鍒楄〃澶辫触')
+ })
+}
+
+// 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+const findNodeLabelById = (nodes, id) => {
+ if (!id) return null
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === id) {
+ return nodes[i].label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const found = findNodeLabelById(nodes[i].children, id)
+ if (found) return found
+ }
+ }
+ return null
+}
+
+// 鑾峰彇澶撮儴缁熻鏁版嵁
+const fetchTotalStatistics = async () => {
+ try {
+ loading.value = true
+ const params = {}
+ if (indicatorFilter.customerName) {
+ params.customerName = indicatorFilter.customerName
+ }
+ if (indicatorFilter.productCategory) {
+ // 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+ const categoryName = findNodeLabelById(productOptions.value, indicatorFilter.productCategory)
+ if (categoryName) {
+ params.productCategory = categoryName
+ }
+ }
+ if (indicatorFilter.dateRange && indicatorFilter.dateRange.length === 2) {
+ params.entryDateStart = indicatorFilter.dateRange[0]
+ params.entryDateEnd = indicatorFilter.dateRange[1]
+ }
+ const res = await getTotalStatistics(params)
+ if (res && res.data) {
+ indicatorKpis.orderCount = res.data.total || 0
+ indicatorKpis.salesAmount = res.data.contractAmountTotal || 0
+ // 鍙戣揣鐜囧鏋滄帴鍙f病鏈夎繑鍥烇紝淇濇寔鍘熷�兼垨璁句负0
+ indicatorKpis.shipRate = res.data.shipRate || 0
+ }
+ } catch (error) {
+ console.error('鑾峰彇澶撮儴缁熻澶辫触:', error)
+ ElMessage.error('鑾峰彇缁熻鏁版嵁澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鑾峰彇鏌辩姸鍥炬暟鎹�
+const fetchStatisticsTable = async () => {
+ try {
+ loading.value = true
+ const params = {}
+ if (indicatorFilter.customerName) {
+ params.customerName = indicatorFilter.customerName
+ }
+ if (indicatorFilter.productCategory) {
+ // 鏍规嵁 id 鏌ユ壘浜у搧绫诲埆鍚嶇О
+ const categoryName = findNodeLabelById(productOptions.value, indicatorFilter.productCategory)
+ if (categoryName) {
+ params.productCategory = categoryName
+ }
+ }
+ if (indicatorFilter.dateRange && indicatorFilter.dateRange.length === 2) {
+ params.entryDateStart = indicatorFilter.dateRange[0]
+ params.entryDateEnd = indicatorFilter.dateRange[1]
+ }
+ const res = await getStatisticsTable(params)
+ if (res && res.data) {
+ updateChart(res.data)
+ }
+ } catch (error) {
+ console.error('鑾峰彇鍥捐〃鏁版嵁澶辫触:', error)
+ ElMessage.error('鑾峰彇鍥捐〃鏁版嵁澶辫触')
+ } finally {
+ loading.value = false
+ }
+}
+
+// 鏇存柊鍥捐〃
+const updateChart = (chartData) => {
+ if (!indicatorChartRef.value) return
+ if (indicatorChart) indicatorChart.dispose()
+ indicatorChart = echarts.init(indicatorChartRef.value)
+
+ // 鏍规嵁鎺ュ彛杩斿洖鐨勬暟鎹粨鏋勬洿鏂板浘琛�
+ // 鎺ュ彛杩斿洖: dateList, orderCountList, salesAmountList
+ const option = {
+ title: { text: '澶氱淮搴﹂攢鍞寚鏍囪秼鍔�', left: 'center' },
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['璁㈠崟鏁�', '閿�鍞'], top: 30 },
+ grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
+ xAxis: {
+ type: 'category',
+ data: chartData.dateList || []
+ },
+ yAxis: [
+ { type: 'value', name: '閲戦', position: 'left', axisLabel: { formatter: '{value}' } },
+ {
+ type: 'value',
+ name: '鏁伴噺',
+ position: 'right',
+ minInterval: 1,
+ axisLabel: {
+ formatter: (value) => {
+ const intValue = Math.round(value)
+ return intValue.toString()
+ }
+ }
+ }
+ ],
+ series: [
+ {
+ name: '璁㈠崟鏁�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: chartData.orderCountList || [],
+ itemStyle: { color: '#409eff' }
+ },
+ {
+ name: '閿�鍞',
+ type: 'bar',
+ yAxisIndex: 0,
+ data: chartData.salesAmountList || [],
+ itemStyle: { color: '#67c23a' }
+ }
+ ]
+ }
+ indicatorChart.setOption(option)
+}
+
+const initIndicatorChart = () => {
+ if (!indicatorChartRef.value) return
+ if (indicatorChart) indicatorChart.dispose()
+ indicatorChart = echarts.init(indicatorChartRef.value)
+ const option = {
+ title: { text: '澶氱淮搴﹂攢鍞寚鏍囪秼鍔�', left: 'center' },
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['璁㈠崟鏁�', '閿�鍞'], top: 30 },
+ grid: { left: '3%', right: '8%', bottom: '3%', containLabel: true },
+ xAxis: { type: 'category', data: [] },
+ yAxis: [
+ { type: 'value', name: '閲戦', position: 'left', axisLabel: { formatter: '{value}' } },
+ {
+ type: 'value',
+ name: '鏁伴噺',
+ position: 'right',
+ minInterval: 1,
+ axisLabel: {
+ formatter: (value) => {
+ const intValue = Math.round(value)
+ return intValue.toString()
+ }
+ }
+ }
+ ],
+ series: [
+ { name: '璁㈠崟鏁�', type: 'line', yAxisIndex: 1, data: [], itemStyle: { color: '#409eff' } },
+ { name: '閿�鍞', type: 'bar', yAxisIndex: 0, data: [], itemStyle: { color: '#67c23a' } }
+ ]
+ }
+ indicatorChart.setOption(option)
+}
+
+const applyIndicatorFilter = async () => {
+ // 绛涢�夋潯浠跺彧褰卞搷鍥捐〃鏁版嵁锛屼笉褰卞搷KPI姹囨��
+ await fetchStatisticsTable()
+}
+
+const resetIndicatorFilter = () => {
+ indicatorFilter.productCategory = ''
+ indicatorFilter.customerName = ''
+ indicatorFilter.dateRange = []
+ applyIndicatorFilter()
+}
+
+// 绐楀彛澶у皬鍙樺寲鏃惰皟鏁村浘琛ㄥぇ灏�
+const handleResize = () => {
+ if (indicatorChart) {
+ indicatorChart.resize()
+ }
+}
+
+onMounted(() => {
+ nextTick(() => {
+ initIndicatorChart()
+ getProductOptions()
+ getCustomerList()
+ fetchTotalStatistics()
+ fetchStatisticsTable()
+ })
+ // 鐩戝惉绐楀彛澶у皬鍙樺寲
+ window.addEventListener('resize', handleResize)
+})
+
+onUnmounted(() => {
+ // 绉婚櫎绐楀彛澶у皬鍙樺寲鐩戝惉鍣�
+ window.removeEventListener('resize', handleResize)
+ // 閿�姣佸浘琛ㄥ疄渚�
+ if (indicatorChart) {
+ indicatorChart.dispose()
+ indicatorChart = null
+ }
+})
+</script>
+
+<style scoped lang="scss">
+.indicator-stats {
+ padding: 20px;
+ min-height: calc(100vh - 84px);
+}
+
+.page-header {
+ margin-bottom: 24px;
+ padding: 20px 0;
+
+ .page-title {
+ font-size: 24px;
+ font-weight: 600;
+ color: #303133;
+ margin: 0 0 8px 0;
+ }
+
+ .page-desc {
+ font-size: 14px;
+ color: #909399;
+ margin: 0;
+ }
+}
+
+.stats-row {
+ margin-bottom: 24px;
+}
+
+.stat-card {
+ position: relative;
+ display: flex;
+ align-items: center;
+ padding: 24px;
+ background: #fff;
+ border-radius: 12px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ overflow: hidden;
+
+ &:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 8px 24px 0 rgba(0, 0, 0, 0.12);
+ }
+
+ .stat-icon-wrapper {
+ margin-right: 20px;
+
+ .stat-icon {
+ width: 64px;
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 12px;
+ transition: all 0.3s ease;
+ }
+ }
+
+ .stat-content {
+ flex: 1;
+ z-index: 1;
+
+ .stat-value {
+ font-size: 32px;
+ font-weight: 700;
+ color: #303133;
+ margin-bottom: 8px;
+ line-height: 1.2;
+ }
+
+ .stat-label {
+ font-size: 14px;
+ color: #909399;
+ font-weight: 500;
+ }
+ }
+
+ .stat-bg-decoration {
+ position: absolute;
+ right: -20px;
+ top: -20px;
+ width: 120px;
+ height: 120px;
+ border-radius: 50%;
+ opacity: 0.1;
+ z-index: 0;
+ }
+
+ &.stat-card-blue {
+ .stat-icon {
+ background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
+ color: #fff;
+ }
+
+ .stat-bg-decoration {
+ background: #409eff;
+ }
+ }
+
+ &.stat-card-green {
+ .stat-icon {
+ background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
+ color: #fff;
+ }
+
+ .stat-bg-decoration {
+ background: #67c23a;
+ }
+ }
+
+ &.stat-card-orange {
+ .stat-icon {
+ background: linear-gradient(135deg, #e6a23c 0%, #ebb563 100%);
+ color: #fff;
+ }
+
+ .stat-bg-decoration {
+ background: #e6a23c;
+ }
+ }
+}
+
+.chart-card,
+.table-card {
+ margin-bottom: 20px;
+ border-radius: 12px;
+ border: none;
+
+ :deep(.el-card__header) {
+ padding: 18px 20px;
+ border-bottom: 1px solid #ebeef5;
+ background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
+ }
+
+ :deep(.el-card__body) {
+ padding: 0;
+ }
+}
+
+.card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .header-left {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .card-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: #303133;
+ }
+
+ .card-subtitle {
+ font-size: 12px;
+ color: #909399;
+ font-weight: normal;
+ }
+}
+
+.chart-filter-section {
+ padding: 20px;
+ background: #fafbfc;
+ border-bottom: 1px solid #ebeef5;
+ margin-bottom: 0;
+}
+
+.search-row {
+ .filter-item {
+ margin-bottom: 0;
+
+ .filter-label {
+ display: block;
+ font-size: 13px;
+ color: #606266;
+ margin-bottom: 8px;
+ font-weight: 500;
+ }
+
+ &.filter-buttons {
+ display: flex;
+ align-items: flex-end;
+ gap: 10px;
+ padding-top: 28px;
+
+ .el-button {
+ flex: 1;
+ font-size: 14px;
+ }
+ }
+ }
+}
+
+.chart-container {
+ width: 100%;
+ overflow: hidden;
+ position: relative;
+ padding: 20px;
+ background: #fff;
+
+ .chart-wrapper {
+ width: 100%;
+ height: 420px;
+ min-width: 0;
+ }
+}
+
+.table-card {
+ :deep(.el-table) {
+ border-radius: 8px;
+ overflow: hidden;
+ }
+
+ :deep(.el-table__header-wrapper) {
+ .el-table__header {
+ th {
+ background: #f5f7fa;
+ color: #606266;
+ font-weight: 600;
+ }
+ }
+ }
+
+ :deep(.el-table__body-wrapper) {
+ .el-table__body {
+ tr:hover {
+ background-color: #f5f7fa;
+ }
+ }
+ }
+}
+
+// 鍝嶅簲寮忚璁�
+@media (max-width: 768px) {
+ .indicator-stats {
+ padding: 12px;
+ }
+
+ .stat-card {
+ padding: 20px;
+
+ .stat-content .stat-value {
+ font-size: 24px;
+ }
+
+ .stat-icon-wrapper .stat-icon {
+ width: 56px;
+ height: 56px;
+ }
+ }
+
+ .chart-filter-section {
+ padding: 16px;
+ }
+
+ .search-row {
+ .filter-item.filter-buttons {
+ padding-top: 0;
+ margin-top: 12px;
+ }
+ }
+
+ .chart-container {
+ padding: 16px;
+
+ .chart-wrapper {
+ height: 320px;
+ }
+ }
+
+ .card-header {
+ .header-left {
+ .card-title {
+ font-size: 15px;
+ }
+
+ .card-subtitle {
+ font-size: 11px;
+ }
+ }
+ }
+}
+
+@media (max-width: 576px) {
+ .page-header {
+ .page-title {
+ font-size: 20px;
+ }
+
+ .page-desc {
+ font-size: 12px;
+ }
+ }
+
+ .stat-card {
+ flex-direction: column;
+ text-align: center;
+
+ .stat-icon-wrapper {
+ margin-right: 0;
+ margin-bottom: 12px;
+ }
+ }
+}
+</style>
+
+
diff --git a/src/views/salesManagement/orderManagement/index.vue b/src/views/salesManagement/orderManagement/index.vue
new file mode 100644
index 0000000..f44c53f
--- /dev/null
+++ b/src/views/salesManagement/orderManagement/index.vue
@@ -0,0 +1,490 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-input
+ v-model="searchForm.orderNo"
+ placeholder="璇疯緭鍏ヨ鍗曞彿"
+ clearable
+ @keyup.enter="handleSearch"
+ >
+ <template #prefix>
+ <el-icon><Search /></el-icon>
+ </template>
+ </el-input>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.customer" placeholder="璇烽�夋嫨瀹㈡埛" clearable>
+ <el-option label="涓婃捣绉戞妧鏈夐檺鍏徃" value="涓婃捣绉戞妧鏈夐檺鍏徃"></el-option>
+ <el-option label="娣卞湷鐢靛瓙鏈夐檺鍏徃" value="娣卞湷鐢靛瓙鏈夐檺鍏徃"></el-option>
+ <el-option label="鍖椾含璐告槗鍏徃" value="鍖椾含璐告槗鍏徃"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨璁㈠崟鐘舵��" clearable>
+ <el-option label="寰呭鏍�" value="寰呭鏍�"></el-option>
+ <el-option label="宸插鏍�" value="宸插鏍�"></el-option>
+ <el-option label="宸插彂璐�" value="宸插彂璐�"></el-option>
+ <el-option label="宸插畬鎴�" value="宸插畬鎴�"></el-option>
+ <el-option label="宸插彇娑�" value="宸插彇娑�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ <el-button style="float: right;" type="primary" @click="handleAdd">
+ 鏂板璁㈠崟
+ </el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 璁㈠崟鍒楄〃 -->
+ <el-table
+ :data="filteredList"
+ style="width: 100%"
+ v-loading="loading"
+ border
+ stripe
+ height="calc(100vh - 22em)"
+ >
+ <el-table-column prop="id" label="ID" width="80" align="center"/>
+ <el-table-column prop="orderNo" label="璁㈠崟鍙�" width="150" />
+ <el-table-column prop="customer" label="瀹㈡埛鍚嶇О" />
+ <el-table-column prop="salesperson" label="涓氬姟鍛�" width="100" />
+ <el-table-column prop="orderDate" label="涓嬪崟鏃ユ湡" width="120" />
+ <el-table-column prop="amount" label="璁㈠崟閲戦" width="120">
+ <template #default="scope">
+ 楼{{ scope.row.amount.toFixed(2) }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="paymentMethod" label="浠樻鏂瑰紡" width="120" />
+ <el-table-column prop="status" label="璁㈠崟鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="250" fixed="right" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleView(scope.row)" style="color: #67C23A">鏌ョ湅</el-button>
+ <el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.status === '寰呭鏍�'">缂栬緫</el-button>
+ <el-button link type="primary" @click="handleReview(scope.row)" v-if="scope.row.status === '寰呭鏍�'">瀹℃牳</el-button>
+ <el-button link type="primary" @click="handleTransfer(scope.row)" v-if="scope.row.status === '宸插鏍�'">杞崟</el-button>
+ <el-button link type="danger" @click="handleCancel(scope.row)" v-if="scope.row.status === '寰呭鏍�'">鍙栨秷</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.currentPage"
+ :limit="pagination.pageSize"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <FormDialog v-model="dialogVisible" :title="dialogTitle" :width="'700px'" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customer">
+ <el-select v-model="form.customer" placeholder="璇烽�夋嫨瀹㈡埛" style="width: 100%">
+ <el-option label="涓婃捣绉戞妧鏈夐檺鍏徃" value="涓婃捣绉戞妧鏈夐檺鍏徃"></el-option>
+ <el-option label="娣卞湷鐢靛瓙鏈夐檺鍏徃" value="娣卞湷鐢靛瓙鏈夐檺鍏徃"></el-option>
+ <el-option label="鍖椾含璐告槗鍏徃" value="鍖椾含璐告槗鍏徃"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓氬姟鍛�" prop="salesperson">
+ <el-select v-model="form.salesperson" placeholder="璇烽�夋嫨涓氬姟鍛�" style="width: 100%">
+ <el-option label="闄堝織寮�" value="闄堝織寮�"></el-option>
+ <el-option label="鍒橀泤濠�" value="鍒橀泤濠�"></el-option>
+ <el-option label="鐜嬪缓鍥�" value="鐜嬪缓鍥�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璁㈠崟鏃ユ湡" prop="orderDate">
+ <el-date-picker
+ v-model="form.orderDate"
+ type="date"
+ placeholder="閫夋嫨璁㈠崟鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁㈠崟閲戦" prop="amount">
+ <el-input-number v-model="form.amount" :precision="2" :min="0" style="width: 100%"></el-input-number>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-select v-model="form.paymentMethod" placeholder="璇烽�夋嫨浠樻鏂瑰紡" style="width: 100%">
+ <el-option label="鍏ㄦ鍒颁粯" value="鍏ㄦ鍒颁粯"></el-option>
+ <el-option label="鍒嗘湡浠樻" value="鍒嗘湡浠樻"></el-option>
+ <el-option label="鏈堢粨" value="鏈堢粨"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璁㈠崟鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%">
+ <el-option label="寰呭鏍�" value="寰呭鏍�"></el-option>
+ <el-option label="宸插鏍�" value="宸插鏍�"></el-option>
+ <el-option label="宸插彂璐�" value="宸插彂璐�"></el-option>
+ <el-option label="宸插畬鎴�" value="宸插畬鎴�"></el-option>
+ <el-option label="宸插彇娑�" value="宸插彇娑�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="form.remark" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" rows="3"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </FormDialog>
+
+ <!-- 璁㈠崟瀹℃牳瀵硅瘽妗� -->
+ <FormDialog v-model="reviewDialogVisible" title="璁㈠崟瀹℃牳" :width="'500px'" @close="reviewDialogVisible = false" @confirm="saveReview" @cancel="reviewDialogVisible = false">
+ <el-form label-width="100px">
+ <el-form-item label="璁㈠崟鍙�">
+ <span>{{ currentOrder.orderNo }}</span>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <span>{{ currentOrder.customer }}</span>
+ </el-form-item>
+ <el-form-item label="璁㈠崟閲戦">
+ <span>楼{{ currentOrder.amount.toFixed(2) }}</span>
+ </el-form-item>
+ <el-form-item label="瀹℃牳缁撴灉" prop="reviewResult">
+ <el-radio-group v-model="reviewResult">
+ <el-radio label="閫氳繃">閫氳繃</el-radio>
+ <el-radio label="鎷掔粷">鎷掔粷</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="瀹℃牳鎰忚" prop="reviewComment">
+ <el-input type="textarea" v-model="reviewComment" rows="3" placeholder="璇疯緭鍏ュ鏍告剰瑙�"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="reviewDialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="saveReview">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </FormDialog>
+
+ <!-- 璁㈠崟杞崟瀵硅瘽妗� -->
+ <FormDialog v-model="transferDialogVisible" title="璁㈠崟杞崟" :width="'500px'" @close="transferDialogVisible = false" @confirm="saveTransfer" @cancel="transferDialogVisible = false">
+ <el-form label-width="100px">
+ <el-form-item label="璁㈠崟鍙�">
+ <span>{{ currentOrder.orderNo }}</span>
+ </el-form-item>
+ <el-form-item label="褰撳墠涓氬姟鍛�">
+ <span>{{ currentOrder.salesperson }}</span>
+ </el-form-item>
+ <el-form-item label="杞崟缁�" prop="newSalesperson">
+ <el-select v-model="newSalesperson" placeholder="璇烽�夋嫨鏂颁笟鍔″憳" style="width: 100%">
+ <el-option label="闄堝織寮�" value="闄堝織寮�"></el-option>
+ <el-option label="鍒橀泤濠�" value="鍒橀泤濠�"></el-option>
+ <el-option label="鐜嬪缓鍥�" value="鐜嬪缓鍥�"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="杞崟鍘熷洜" prop="transferReason">
+ <el-input type="textarea" v-model="transferReason" rows="3" placeholder="璇疯緭鍏ヨ浆鍗曞師鍥�"></el-input>
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Search } from '@element-plus/icons-vue'
+import Pagination from '@/components/PIMTable/Pagination.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const searchForm = reactive({
+ orderNo: '',
+ customer: '',
+ status: ''
+})
+
+const orderList = ref([
+ {
+ id: 1,
+ orderNo: 'ORD202312001',
+ customer: '涓婃捣绉戞妧鏈夐檺鍏徃',
+ salesperson: '闄堝織寮�',
+ orderDate: '2023-12-01',
+ amount: 50000.00,
+ paymentMethod: '鍏ㄦ鍒颁粯',
+ status: '寰呭鏍�',
+ remark: '閲嶈瀹㈡埛璁㈠崟'
+ },
+ {
+ id: 2,
+ orderNo: 'ORD202312002',
+ customer: '娣卞湷鐢靛瓙鏈夐檺鍏徃',
+ salesperson: '鍒橀泤濠�',
+ orderDate: '2023-12-02',
+ amount: 35000.00,
+ paymentMethod: '鍒嗘湡浠樻',
+ status: '宸插鏍�',
+ remark: '甯歌璁㈠崟'
+ },
+ {
+ id: 3,
+ orderNo: 'ORD202312003',
+ customer: '鍖椾含璐告槗鍏徃',
+ salesperson: '鐜嬪缓鍥�',
+ orderDate: '2023-12-03',
+ amount: 28000.00,
+ paymentMethod: '鏈堢粨',
+ status: '宸插彂璐�',
+ remark: '鏂板鎴疯鍗�'
+ }
+])
+
+const pagination = reactive({
+ total: 3,
+ currentPage: 1,
+ pageSize: 10
+})
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('鏂板璁㈠崟')
+const form = reactive({
+ customer: '',
+ salesperson: '',
+ orderDate: '',
+ amount: 0,
+ paymentMethod: '',
+ status: '寰呭鏍�',
+ remark: ''
+})
+
+const rules = {
+ customer: [{ required: true, message: '璇烽�夋嫨瀹㈡埛', trigger: 'change' }],
+ salesperson: [{ required: true, message: '璇烽�夋嫨涓氬姟鍛�', trigger: 'change' }],
+ orderDate: [{ required: true, message: '璇烽�夋嫨璁㈠崟鏃ユ湡', trigger: 'change' }],
+ amount: [{ required: true, message: '璇疯緭鍏ヨ鍗曢噾棰�', trigger: 'blur' }],
+ paymentMethod: [{ required: true, message: '璇烽�夋嫨浠樻鏂瑰紡', trigger: 'change' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+}
+
+const isEdit = ref(false)
+const editId = ref(null)
+const reviewDialogVisible = ref(false)
+const transferDialogVisible = ref(false)
+const currentOrder = ref({})
+const reviewResult = ref('')
+const reviewComment = ref('')
+const newSalesperson = ref('')
+const transferReason = ref('')
+const formRef = ref()
+
+// 璁$畻灞炴��
+const filteredList = computed(() => {
+ let list = orderList.value
+ if (searchForm.orderNo) {
+ list = list.filter(item => item.orderNo.includes(searchForm.orderNo))
+ }
+ if (searchForm.customer) {
+ list = list.filter(item => item.customer === searchForm.customer)
+ }
+ if (searchForm.status) {
+ list = list.filter(item => item.status === searchForm.status)
+ }
+ return list
+})
+
+// 鏂规硶
+const getStatusType = (status) => {
+ const statusMap = {
+ '寰呭鏍�': 'warning',
+ '宸插鏍�': 'primary',
+ '宸插彂璐�': 'success',
+ '宸插畬鎴�': 'success',
+ '宸插彇娑�': 'danger'
+ }
+ return statusMap[status] || 'info'
+}
+
+const handleSearch = () => {
+ // 鎼滅储閫昏緫宸插湪computed涓鐞�
+}
+
+const resetSearch = () => {
+ searchForm.orderNo = ''
+ searchForm.customer = ''
+ searchForm.status = ''
+}
+
+const handleAdd = () => {
+ dialogTitle.value = '鏂板璁㈠崟'
+ isEdit.value = false
+ form.customer = ''
+ form.salesperson = ''
+ form.orderDate = ''
+ form.amount = 0
+ form.paymentMethod = ''
+ form.status = '寰呭鏍�'
+ form.remark = ''
+ dialogVisible.value = true
+}
+
+const handleView = (row) => {
+ // 鏌ョ湅璁㈠崟璇︽儏
+ ElMessage.info('鏌ョ湅璁㈠崟璇︽儏鍔熻兘寰呭疄鐜�')
+}
+
+const handleEdit = (row) => {
+ dialogTitle.value = '缂栬緫璁㈠崟'
+ isEdit.value = true
+ editId.value = row.id
+ Object.assign(form, row)
+ dialogVisible.value = true
+}
+
+const handleReview = (row) => {
+ currentOrder.value = row
+ reviewResult.value = ''
+ reviewComment.value = ''
+ reviewDialogVisible.value = true
+}
+
+const handleTransfer = (row) => {
+ currentOrder.value = row
+ newSalesperson.value = ''
+ transferReason.value = ''
+ transferDialogVisible.value = true
+}
+
+const handleCancel = (row) => {
+ ElMessageBox.confirm('纭鍙栨秷璇ヨ鍗曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ const index = orderList.value.findIndex(item => item.id === row.id)
+ if (index > -1) {
+ orderList.value[index].status = '宸插彇娑�'
+ ElMessage.success('璁㈠崟宸插彇娑�')
+ }
+ })
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭鍒犻櫎璇ヨ鍗曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ const index = orderList.value.findIndex(item => item.id === row.id)
+ if (index > -1) {
+ orderList.value.splice(index, 1)
+ pagination.total--
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ }
+ })
+}
+
+const saveReview = () => {
+ if (!reviewResult.value) {
+ ElMessage.warning('璇烽�夋嫨瀹℃牳缁撴灉')
+ return
+ }
+
+ const index = orderList.value.findIndex(item => item.id === currentOrder.value.id)
+ if (index > -1) {
+ if (reviewResult.value === '閫氳繃') {
+ orderList.value[index].status = '宸插鏍�'
+ ElMessage.success('璁㈠崟瀹℃牳閫氳繃')
+ } else {
+ orderList.value[index].status = '宸插彇娑�'
+ ElMessage.success('璁㈠崟瀹℃牳鎷掔粷')
+ }
+ reviewDialogVisible.value = false
+ }
+}
+
+const saveTransfer = () => {
+ if (!newSalesperson.value) {
+ ElMessage.warning('璇烽�夋嫨鏂颁笟鍔″憳')
+ return
+ }
+
+ const index = orderList.value.findIndex(item => item.id === currentOrder.value.id)
+ if (index > -1) {
+ orderList.value[index].salesperson = newSalesperson.value
+ ElMessage.success('璁㈠崟杞崟鎴愬姛')
+ transferDialogVisible.value = false
+ }
+}
+
+const handleSubmit = () => {
+ formRef.value.validate((valid) => {
+ if (valid) {
+ if (isEdit.value) {
+ // 缂栬緫
+ const index = orderList.value.findIndex(item => item.id === editId.value)
+ if (index > -1) {
+ orderList.value[index] = { ...form, id: editId.value }
+ ElMessage.success('缂栬緫鎴愬姛')
+ }
+ } else {
+ // 鏂板
+ const newId = Math.max(...orderList.value.map(item => item.id)) + 1
+ const orderNo = `ORD${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(newId).padStart(3, '0')}`
+ orderList.value.push({
+ ...form,
+ id: newId,
+ orderNo: orderNo
+ })
+ pagination.total++
+ ElMessage.success('鏂板鎴愬姛')
+ }
+ dialogVisible.value = false
+ }
+ })
+}
+
+const handleCurrentChange = (val) => {
+ pagination.currentPage = val.page
+ pagination.pageSize = val.limit
+}
+</script>
+
+<style scoped>
+.search-row {
+ margin-bottom: 20px;
+}
+</style>
diff --git a/src/views/salesManagement/paymentShipping/index.vue b/src/views/salesManagement/paymentShipping/index.vue
new file mode 100644
index 0000000..56caf3b
--- /dev/null
+++ b/src/views/salesManagement/paymentShipping/index.vue
@@ -0,0 +1,497 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-input
+ v-model="searchForm.orderNo"
+ placeholder="璇疯緭鍏ヨ鍗曞彿"
+ clearable
+ @keyup.enter="handleSearch"
+ >
+ <template #prefix>
+ <el-icon><Search /></el-icon>
+ </template>
+ </el-input>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.paymentStatus" placeholder="璇烽�夋嫨浠樻鐘舵��" clearable>
+ <el-option label="鏈粯娆�" value="鏈粯娆�"></el-option>
+ <el-option label="宸蹭粯娆�" value="宸蹭粯娆�"></el-option>
+ <el-option label="閮ㄥ垎浠樻" value="閮ㄥ垎浠樻"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.shippingStatus" placeholder="璇烽�夋嫨鍙戣揣鐘舵��" clearable>
+ <el-option label="寰呭彂璐�" value="寰呭彂璐�"></el-option>
+ <el-option label="宸插彂璐�" value="宸插彂璐�"></el-option>
+ <el-option label="宸茬鏀�" value="宸茬鏀�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ <el-button style="float: right;" type="primary" @click="handleAdd">
+ 鏂板璁板綍
+ </el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鏀粯涓庡彂璐у垪琛� -->
+ <el-table
+ :data="recordList"
+ style="width: 100%"
+ v-loading="loading"
+ border
+ stripe
+ height="calc(100vh - 22em)"
+ >
+ <el-table-column prop="id" label="ID" width="80" align="center"/>
+ <el-table-column prop="orderNo" label="璁㈠崟鍙�" />
+ <el-table-column prop="customer" label="瀹㈡埛鍚嶇О" />
+ <el-table-column prop="orderAmount" label="璁㈠崟閲戦" width="120">
+ <template #default="scope">
+ 楼{{ scope.row.orderAmount }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="orderAmount" label="宸蹭粯娆鹃噾棰�" width="120">
+ <template #default="scope">
+ 楼{{ scope.row.paidAmount }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="paymentMethod" label="浠樻鏂瑰紡" width="120" />
+ <el-table-column prop="paymentStatus" label="浠樻鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getPaymentStatusType(scope.row.paymentStatus)">
+ {{ scope.row.paymentStatus }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="shippingStatus" label="鍙戣揣鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getShippingStatusType(scope.row.shippingStatus)">
+ {{ scope.row.shippingStatus }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="shippingDate" label="鍙戣揣鏃ユ湡" width="120" />
+ <el-table-column label="鎿嶄綔" width="250" fixed="right" align="center">
+ <template #default="scope">
+<!-- <el-button link type="primary" @click="handleView(scope.row)">鏌ョ湅</el-button>-->
+ <el-button link type="primary" @click="handlePayment(scope.row)" v-if="scope.row.paymentStatus !== '宸蹭粯娆�'">浠樻</el-button>
+ <el-button link type="primary" @click="handleShipping(scope.row)" v-if="scope.row.paymentStatus === '宸蹭粯娆�' && scope.row.shippingStatus === '寰呭彂璐�'">鍙戣揣</el-button>
+ <el-button link type="primary" @click="handleEdit(scope.row)">缂栬緫</el-button>
+ <el-button link type="danger" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.current"
+ :limit="pagination.size"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <FormDialog v-model="dialogVisible" :title="dialogTitle" :width="'700px'" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璁㈠崟鍙�" prop="orderNo">
+ <el-input v-model="form.orderNo" placeholder="璇疯緭鍏ヨ鍗曞彿" disabled></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customer">
+ <el-select v-model="form.customer" placeholder="璇烽�夋嫨瀹㈡埛" style="width: 100%">
+ <el-option label="涓婃捣绉戞妧鏈夐檺鍏徃" value="涓婃捣绉戞妧鏈夐檺鍏徃"></el-option>
+ <el-option label="娣卞湷鐢靛瓙鏈夐檺鍏徃" value="娣卞湷鐢靛瓙鏈夐檺鍏徃"></el-option>
+ <el-option label="鍖椾含璐告槗鍏徃" value="鍖椾含璐告槗鍏徃"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="璁㈠崟閲戦" prop="orderAmount">
+ <el-input-number v-model="form.orderAmount" :precision="2" :min="0" style="width: 100%"></el-input-number>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-select v-model="form.paymentMethod" placeholder="璇烽�夋嫨浠樻鏂瑰紡" style="width: 100%">
+ <el-option label="鍏ㄦ鍒颁粯" value="鍏ㄦ鍒颁粯"></el-option>
+ <el-option label="鍒嗘湡浠樻" value="鍒嗘湡浠樻"></el-option>
+ <el-option label="鏈堢粨" value="鏈堢粨"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浠樻鐘舵��" prop="paymentStatus">
+ <el-select v-model="form.paymentStatus" placeholder="璇烽�夋嫨浠樻鐘舵��" style="width: 100%">
+ <el-option label="鏈粯娆�" value="鏈粯娆�"></el-option>
+ <el-option label="宸蹭粯娆�" value="宸蹭粯娆�"></el-option>
+ <el-option label="閮ㄥ垎浠樻" value="閮ㄥ垎浠樻"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍙戣揣鐘舵��" prop="shippingStatus">
+ <el-select v-model="form.shippingStatus" placeholder="璇烽�夋嫨鍙戣揣鐘舵��" style="width: 100%">
+ <el-option label="寰呭彂璐�" value="寰呭彂璐�"></el-option>
+ <el-option label="宸插彂璐�" value="宸插彂璐�"></el-option>
+ <el-option label="宸茬鏀�" value="宸茬鏀�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍙戣揣鏃ユ湡" prop="shippingDate">
+ <el-date-picker
+ v-model="form.shippingDate"
+ type="date"
+ placeholder="閫夋嫨鍙戣揣鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐗╂祦鍗曞彿" prop="trackingNo">
+ <el-input v-model="form.trackingNo" placeholder="璇疯緭鍏ョ墿娴佸崟鍙�"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" v-model="form.remark" placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�" rows="3"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+ <el-button type="primary" @click="handleSubmit">纭� 瀹�</el-button>
+ </div>
+ </template>
+ </FormDialog>
+
+ <!-- 浠樻瀵硅瘽妗� -->
+ <FormDialog v-model="paymentDialogVisible" title="璁㈠崟浠樻" :width="'500px'" @close="paymentDialogVisible = false" @confirm="savePayment" @cancel="paymentDialogVisible = false">
+ <el-form label-width="100px">
+ <el-form-item label="璁㈠崟鍙�">
+ <span>{{ currentRecord.orderNo }}</span>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <span>{{ currentRecord.customer }}</span>
+ </el-form-item>
+ <el-form-item label="璁㈠崟閲戦">
+ <span>楼{{ currentRecord.orderAmount }}</span>
+ </el-form-item>
+ <el-form-item label="浠樻閲戦" prop="paymentAmount">
+ <el-input-number v-model="paymentAmount" :precision="2" :min="0" :max="currentRecord.orderAmount" style="width: 100%"></el-input-number>
+ </el-form-item>
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-select v-model="paymentMethod" placeholder="璇烽�夋嫨浠樻鏂瑰紡" style="width: 100%">
+ <el-option label="鐜伴噾" value="鐜伴噾"></el-option>
+ <el-option label="閾惰杞处" value="閾惰杞处"></el-option>
+ <el-option label="鏀粯瀹�" value="鏀粯瀹�"></el-option>
+ <el-option label="寰俊鏀粯" value="寰俊鏀粯"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="浠樻澶囨敞" prop="paymentRemark">
+ <el-input type="textarea" v-model="paymentRemark" rows="3" placeholder="璇疯緭鍏ヤ粯娆惧娉�"></el-input>
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+
+ <!-- 鍙戣揣瀵硅瘽妗� -->
+ <FormDialog v-model="shippingDialogVisible" title="璁㈠崟鍙戣揣" :width="'500px'" @close="shippingDialogVisible = false" @confirm="saveShipping" @cancel="shippingDialogVisible = false">
+ <el-form label-width="100px">
+ <el-form-item label="璁㈠崟鍙�">
+ <span>{{ currentRecord.orderNo }}</span>
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <span>{{ currentRecord.customer }}</span>
+ </el-form-item>
+ <el-form-item label="鍙戣揣鏃ユ湡" prop="shippingDate">
+ <el-date-picker
+ v-model="shippingDate"
+ type="date"
+ placeholder="閫夋嫨鍙戣揣鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ <el-form-item label="鐗╂祦鍏徃" prop="logisticsCompany">
+ <el-select v-model="logisticsCompany" placeholder="璇烽�夋嫨鐗╂祦鍏徃" style="width: 100%">
+ <el-option label="椤轰赴閫熻繍" value="椤轰赴閫熻繍"></el-option>
+ <el-option label="鍦嗛�氶�熼��" value="鍦嗛�氶�熼��"></el-option>
+ <el-option label="涓�氬揩閫�" value="涓�氬揩閫�"></el-option>
+ <el-option label="鐢抽�氬揩閫�" value="鐢抽�氬揩閫�"></el-option>
+ <el-option label="闊佃揪閫熼��" value="闊佃揪閫熼��"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐗╂祦鍗曞彿" prop="trackingNo">
+ <el-input v-model="trackingNo" placeholder="璇疯緭鍏ョ墿娴佸崟鍙�"></el-input>
+ </el-form-item>
+ <el-form-item label="鍙戣揣澶囨敞" prop="shippingRemark">
+ <el-input type="textarea" v-model="shippingRemark" rows="3" placeholder="璇疯緭鍏ュ彂璐у娉�"></el-input>
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed,onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus, Search } from '@element-plus/icons-vue'
+import {listPage,add,update,deletePaymentShipping} from "@/api/salesManagement/paymentShipping.js"
+import Pagination from '@/components/PIMTable/Pagination.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+
+const total = ref(0)
+onMounted(() => {
+ getList()
+})
+
+const getList = () => {
+ loading.value = true
+ listPage({...searchForm,...pagination}).then(res => {
+ if(res.code === 200){
+ recordList.value = res.data.records
+ total.value = res.data.total
+ loading.value = false
+ console.log(recordList.value)
+ }
+ })
+}
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const searchForm = reactive({
+ orderNo: '',
+ paymentStatus: '',
+ shippingStatus: ''
+})
+
+const recordList = ref([])
+
+const pagination = reactive({
+ current: 1,
+ size: 10
+})
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('鏂板璁板綍')
+const form = reactive({
+ orderNo: '',
+ customer: '',
+ orderAmount: 0,
+ paymentMethod: '',
+ paymentStatus: '鏈粯娆�',
+ shippingStatus: '寰呭彂璐�',
+ shippingDate: '',
+ trackingNo: '',
+ remark: ''
+})
+
+const rules = {
+ // orderNo: [{ required: true, message: '璇疯緭鍏ヨ鍗曞彿', trigger: 'blur' }],
+ customer: [{ required: true, message: '璇烽�夋嫨瀹㈡埛', trigger: 'change' }],
+ orderAmount: [{ required: true, message: '璇疯緭鍏ヨ鍗曢噾棰�', trigger: 'blur' }],
+ paymentMethod: [{ required: true, message: '璇烽�夋嫨浠樻鏂瑰紡', trigger: 'change' }],
+ paymentStatus: [{ required: true, message: '璇烽�夋嫨浠樻鐘舵��', trigger: 'change' }],
+ shippingStatus: [{ required: true, message: '璇烽�夋嫨鍙戣揣鐘舵��', trigger: 'change' }]
+}
+
+const isEdit = ref(false)
+const editId = ref(null)
+const paymentDialogVisible = ref(false)
+const shippingDialogVisible = ref(false)
+const currentRecord = ref({})
+const paymentAmount = ref(0)
+const paymentMethod = ref('')
+const paymentRemark = ref('')
+const shippingDate = ref('')
+const logisticsCompany = ref('')
+const trackingNo = ref('')
+const shippingRemark = ref('')
+const formRef = ref()
+
+// 鏂规硶
+const getPaymentStatusType = (status) => {
+ const statusMap = {
+ '鏈粯娆�': 'danger',
+ '宸蹭粯娆�': 'success',
+ '閮ㄥ垎浠樻': 'warning'
+ }
+ return statusMap[status] || 'info'
+}
+
+const getShippingStatusType = (status) => {
+ const statusMap = {
+ '寰呭彂璐�': 'warning',
+ '宸插彂璐�': 'primary',
+ '宸茬鏀�': 'success'
+ }
+ return statusMap[status] || 'info'
+}
+
+const handleSearch = () => {
+ // 鎼滅储閫昏緫宸插湪computed涓鐞�
+ getList()
+}
+
+const resetSearch = () => {
+ searchForm.orderNo = ''
+ searchForm.paymentStatus = ''
+ searchForm.shippingStatus = ''
+}
+
+const handleAdd = () => {
+ dialogTitle.value = '鏂板璁板綍'
+ isEdit.value = false
+ form.orderNo = ''
+ form.customer = ''
+ form.orderAmount = 0
+ form.paymentMethod = ''
+ form.paymentStatus = '鏈粯娆�'
+ form.shippingStatus = '寰呭彂璐�'
+ form.shippingDate = ''
+ form.trackingNo = ''
+ form.remark = ''
+ dialogVisible.value = true
+}
+
+const handleView = (row) => {
+ // 鏌ョ湅璁板綍璇︽儏
+ ElMessage.info('鏌ョ湅璁板綍璇︽儏鍔熻兘寰呭疄鐜�')
+}
+
+const handleEdit = (row) => {
+ dialogTitle.value = '缂栬緫璁板綍'
+ isEdit.value = true
+ editId.value = row.id
+ Object.assign(form, row)
+ dialogVisible.value = true
+}
+
+const handlePayment = (row) => {
+ currentRecord.value = row
+ paymentAmount.value = row.orderAmount - row.paidAmount
+ paymentMethod.value = ''
+ paymentRemark.value = ''
+ paymentDialogVisible.value = true
+}
+
+const handleShipping = (row) => {
+ currentRecord.value = row
+ shippingDate.value = ''
+ logisticsCompany.value = ''
+ trackingNo.value = ''
+ shippingRemark.value = ''
+ shippingDialogVisible.value = true
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭鍒犻櫎璇ヨ褰曞悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = [row.id]
+ deletePaymentShipping(ids).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getList()
+ }
+ })
+ })
+}
+
+const savePayment = () => {
+ if (!paymentMethod.value) {
+ ElMessage.warning('璇烽�夋嫨浠樻鏂瑰紡')
+ return
+ }
+ currentRecord.value.paidAmount = Number(currentRecord.value.paidAmount) + paymentAmount.value
+ if(currentRecord.value.paidAmount == currentRecord.value.orderAmount){
+ currentRecord.value.paymentStatus = '宸蹭粯娆�'
+ }else{
+ currentRecord.value.paymentStatus = '閮ㄥ垎浠樻'
+ }
+ update(currentRecord.value).then(res => {
+ if(res.code === 200){
+ ElMessage.success('浠樻淇℃伅宸蹭繚瀛�')
+ paymentDialogVisible.value = false
+ getList()
+ }
+ })
+
+}
+
+const saveShipping = () => {
+ if (!shippingDate.value || !logisticsCompany.value || !trackingNo.value) {
+ ElMessage.warning('璇峰~鍐欏畬鏁寸殑鍙戣揣淇℃伅')
+ return
+ }
+ currentRecord.value.shippingStatus = '宸插彂璐�'
+ update(currentRecord.value).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鍙戣揣淇℃伅宸蹭繚瀛�')
+ shippingDialogVisible.value = false
+ getList()
+ }
+ })
+}
+
+const handleSubmit = () => {
+ formRef.value.validate((valid) => {
+ if (valid) {
+ if (isEdit.value) {
+ // 缂栬緫
+ update(form).then(res => {
+ if(res.code === 200){
+ ElMessage.success('缂栬緫鎴愬姛')
+ getList()
+ }
+ })
+ } else {
+ // 鏂板
+ add(form).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鏂板鎴愬姛')
+ getList()
+ }
+ })
+ }
+ dialogVisible.value = false
+ }
+ })
+}
+
+const handleCurrentChange = (val) => {
+ pagination.current = val.page
+ pagination.size = val.limit
+}
+</script>
+
+<style scoped>
+.search-row {
+ margin-bottom: 20px;
+}
+</style>
diff --git a/src/views/salesManagement/returnOrder/components/detailDia.vue b/src/views/salesManagement/returnOrder/components/detailDia.vue
new file mode 100644
index 0000000..ecc663f
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/components/detailDia.vue
@@ -0,0 +1,352 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="閫�璐у崟璇︽儏" width="90%" @close="closeDia">
+ <div v-loading="loading">
+ <span class="descriptions">鍩烘湰淇℃伅</span>
+ <el-descriptions :column="4" border>
+ <el-descriptions-item label="閫�璐у崟鍙�">{{ detail.returnNo }}</el-descriptions-item>
+ <el-descriptions-item label="鍗曟嵁鐘舵��">
+ <el-tag :type="getStatusType(detail.status)">{{ getStatusText(detail.status) }}</el-tag>
+ </el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ detail.customerName }}</el-descriptions-item>
+ <el-descriptions-item label="閿�鍞崟鍙�">{{ detail.salesContractNo }}</el-descriptions-item>
+ <el-descriptions-item label="涓氬姟鍛�">{{ detail.salesman }}</el-descriptions-item>
+ <el-descriptions-item label="鍏宠仈鍙戣揣鍗曞彿">{{ detail.shippingNo }}</el-descriptions-item>
+ <!-- <el-descriptions-item label="椤圭洰鍚嶇О">{{ detail.projectName }}</el-descriptions-item> -->
+ <el-descriptions-item label="鍒跺崟浜�">{{ detail.maker }}</el-descriptions-item>
+ <el-descriptions-item label="鍒跺崟鏃堕棿">{{ detail.makeTime }}</el-descriptions-item>
+ <el-descriptions-item label="閫�璐у師鍥�">{{ detail.returnReason }}</el-descriptions-item>
+ <el-descriptions-item label="閫�娆炬�婚">{{ detail.refundAmount }}</el-descriptions-item>
+ </el-descriptions>
+
+ <div style="padding-top: 20px">
+ <span class="descriptions">浜у搧鍒楄〃</span>
+ <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
+ <template #totalReturnNum="{ row }">
+ {{ calcAlreadyReturned(row) }}
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="closeDia">鍏抽棴</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { ref } from "vue";
+import { returnManagementGetById, returnManagementGetByShippingId } from "@/api/salesManagement/returnOrder.js";
+
+const dialogVisible = ref(false);
+const loading = ref(false);
+const detail = ref({});
+const tableData = ref([]);
+const availableProducts = ref([]);
+
+const sameKey = (a, b) => a != null && b != null && String(a) === String(b);
+
+/** 涓� formDia 涓�鑷达細涓や唤鍒楄〃鎸� id 鍚堝苟锛岄伩鍏嶅彧鍙� productDtoData 鏃剁己鍑哄簱鍗曞彿/鎵规/鏁伴噺 */
+const mergeShippingProductLists = (data) => {
+ const lists = [data?.shippingProductVoList, data?.productDtoData].filter(Array.isArray);
+ if (!lists.length) return [];
+ const map = new Map();
+ for (const list of lists) {
+ for (const p of list) {
+ if (p == null) continue;
+ const key = p.id != null ? String(p.id) : null;
+ if (!key) continue;
+ const prev = map.get(key);
+ map.set(key, prev ? { ...prev, ...p } : { ...p });
+ }
+ }
+ return Array.from(map.values());
+};
+
+const pickShippingLine = (normalized) => {
+ const pid = normalized?.returnSaleLedgerProductId ?? normalized?.id;
+ const sid = normalized?.stockOutRecordId ?? normalized?.shippingProductId;
+ const direct = availableProducts.value.find(
+ (p) =>
+ sameKey(p?.id, pid) ||
+ sameKey(p?.stockOutRecordId, pid) ||
+ sameKey(p?.id, sid) ||
+ sameKey(p?.stockOutRecordId, sid)
+ );
+ if (direct) return direct;
+ const pmid = normalized?.productModelId;
+ if (pmid == null || pmid === "") return undefined;
+ const candidates = availableProducts.value.filter((p) => sameKey(p?.productModelId, pmid));
+ if (!candidates.length) return undefined;
+ if (candidates.length === 1) return candidates[0];
+ const spec = String(normalized?.specificationModel ?? normalized?.model ?? "");
+ if (spec) {
+ const hit = candidates.find((p) => {
+ const ps = String(p?.specificationModel ?? p?.model ?? "");
+ return ps && ps === spec;
+ });
+ if (hit) return hit;
+ }
+ return candidates[0];
+};
+
+const isEmptyText = (v) => v === "" || v == null || v === undefined;
+
+const firstFiniteNumber = (...vals) => {
+ for (const v of vals) {
+ if (v === "" || v == null || v === undefined) continue;
+ const n = Number(v);
+ if (Number.isFinite(n)) return n;
+ }
+ return undefined;
+};
+
+const firstNonEmptyText = (...vals) => {
+ const hit = vals.find((v) => !isEmptyText(v));
+ return hit === undefined ? "" : hit;
+};
+
+const calcAlreadyReturned = (row) => {
+ const total = Number(row?.stockOutNum ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
+ const un = Number(row?.unQuantity ?? 0);
+ if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
+ return Math.max(total - un, 0);
+};
+
+/** 璇︽儏琛ㄧ敤 productName / model锛涘悎骞舵椂鍕胯绌轰覆鐩栨帀鍑哄簱琛屽瓧娈� */
+const mergeDetailProductRow = (product, normalized) => {
+ const row = { ...product, ...normalized };
+ row.outboundBatches = firstNonEmptyText(
+ row.outboundBatches,
+ product?.outboundBatches,
+ product?.shippingNo,
+ product?.outboundNo,
+ normalized?.outboundBatches,
+ normalized?.outboundNo,
+ normalized?.shippingNo
+ );
+ row.batchNo = firstNonEmptyText(
+ row.batchNo,
+ product?.batchNo,
+ product?.batchNumber,
+ product?.lotNo,
+ product?.batchCode,
+ product?.shippingBatchNo,
+ normalized?.batchNo,
+ normalized?.batchNumber,
+ normalized?.lotNo,
+ normalized?.shippingBatchNo
+ );
+ const stock = firstFiniteNumber(
+ row.stockOutNum,
+ product?.stockOutNum,
+ product?.totalQuantity,
+ product?.shippingQuantity,
+ product?.deliveryQuantity,
+ product?.quantity,
+ product?.outQuantity,
+ normalized?.stockOutNum,
+ normalized?.totalQuantity,
+ normalized?.shippingQuantity,
+ normalized?.deliveryQuantity
+ );
+ if (stock !== undefined) row.stockOutNum = stock;
+ const un = firstFiniteNumber(
+ row.unQuantity,
+ product?.unQuantity,
+ product?.remainingQuantity,
+ product?.noReturnQuantity,
+ product?.canReturnQuantity,
+ product?.availableReturnNum,
+ normalized?.unQuantity,
+ normalized?.remainingQuantity,
+ normalized?.noReturnQuantity,
+ normalized?.canReturnQuantity
+ );
+ if (un !== undefined) row.unQuantity = un;
+ else {
+ const s = Number(row.stockOutNum);
+ const ret = Number(row.totalReturnNum ?? 0);
+ if (Number.isFinite(s) && s >= 0 && Number.isFinite(ret) && ret >= 0) {
+ row.unQuantity = Math.max(0, s - ret);
+ }
+ }
+ const returned = firstFiniteNumber(
+ row.totalReturnNum,
+ product?.totalReturnNum,
+ product?.totalReturnedNum,
+ normalized?.totalReturnNum,
+ normalized?.totalReturnedNum
+ );
+ if (returned !== undefined) row.totalReturnNum = returned;
+ else if (isEmptyText(row.totalReturnNum)) row.totalReturnNum = 0;
+ if (isEmptyText(row.unit)) {
+ row.unit = firstNonEmptyText(product?.unit, normalized?.unit);
+ }
+ row.productName = firstNonEmptyText(
+ row.productName,
+ normalized?.productName,
+ normalized?.productCategory,
+ product?.productName,
+ product?.productCategory
+ );
+ row.model = firstNonEmptyText(
+ row.model,
+ normalized?.model,
+ normalized?.specificationModel,
+ product?.model,
+ product?.specificationModel
+ );
+ return row;
+};
+
+const normalizeDetailRow = (raw) => {
+ const ledgerId =
+ raw?.returnSaleLedgerProductId ??
+ raw?.saleLedgerProductId ??
+ raw?.stockOutRecordId ??
+ raw?.shippingProductId;
+ const productId = ledgerId ?? raw?.id;
+ const num = Number(raw?.num ?? raw?.returnQuantity ?? 0);
+ return {
+ ...raw,
+ id: productId,
+ returnSaleLedgerProductId: productId,
+ productModelId: raw?.productModelId,
+ stockOutRecordId: raw?.stockOutRecordId,
+ shippingProductId: raw?.shippingProductId,
+ productName: raw?.productName ?? raw?.productCategory ?? raw?.productTypeName ?? "",
+ model: raw?.model ?? raw?.specificationModel ?? raw?.specModel ?? "",
+ outboundBatches: raw?.outboundBatches ?? raw?.outboundNo ?? raw?.shippingNo,
+ batchNo:
+ raw?.batchNo ??
+ raw?.batchNumber ??
+ raw?.lotNo ??
+ raw?.batchCode ??
+ raw?.shippingBatchNo,
+ stockOutNum:
+ raw?.stockOutNum ??
+ raw?.totalQuantity ??
+ raw?.shippingQuantity ??
+ raw?.deliveryQuantity ??
+ raw?.quantity,
+ totalReturnNum: raw?.totalReturnNum ?? raw?.totalReturnedNum,
+ unQuantity:
+ raw?.unQuantity ??
+ raw?.remainingQuantity ??
+ raw?.noReturnQuantity ??
+ raw?.canReturnQuantity,
+ returnQuantity: Number.isFinite(num) ? num : 0,
+ price: Number(raw?.taxInclusiveUnitPrice ?? raw?.price ?? 0),
+ amount: Number(raw?.amount ?? 0).toFixed(2),
+ isQuality: raw?.isQuality ?? 2,
+ remark: raw?.remark ?? "",
+ };
+};
+
+const tableColumn = [
+ {align: "center", label: "鍑哄簱鍗曞彿", prop: "outboundBatches"},
+ {align: "center", label: "鎵规鍙�", prop: "batchNo"},
+ {align: "center", label: "浜у搧澶х被", prop: "productName"},
+ {align: "center", label: "瑙勬牸鍨嬪彿", prop: "model"},
+ {align: "center", label: "鍗曚綅", prop: "unit", width: 80},
+ {align: "center", label: "鎬绘暟閲�", prop: "stockOutNum", width: 120},
+ {align: "center", label: "宸查��璐ф暟閲�", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum"},
+ {align: "center", label: "鏈��璐ф暟閲�", prop: "unQuantity", width: 120},
+ {align: "center", label: "閫�璐ф暟閲�", prop: "returnQuantity", width: 120},
+ {align: "center", label: "閫�璐т骇鍝佸崟浠�", prop: "price", width: 120},
+ {align: "center", label: "閫�璐т骇鍝侀噾棰�", prop: "amount", width: 120},
+ {align: "center", label: "鏄惁鏈夎川閲忛棶棰�", prop: "isQuality", width: 140, formatData: (v) => ({ "1": "鏄�", "2": "鍚�" }[String(v)] ?? v)},
+ {align: "center", label: "澶囨敞", prop: "remark", width: 150},
+];
+
+const getStatusType = (status) => {
+ const statusMap = {
+ 0: "warning",
+ 1: "success"
+ };
+ return statusMap[status] || "info";
+};
+
+const getStatusText = (status) => {
+ const statusMap = {
+ 0: "寰呭鐞�",
+ 1: "宸插鐞�"
+ };
+ return statusMap[status] || "鏈煡";
+};
+
+const openDialog = async (row) => {
+ if (!row?.id) return;
+ dialogVisible.value = true;
+ loading.value = true;
+ try {
+ const res = await returnManagementGetById({ returnManagementId: row.id });
+ detail.value = res?.data ?? res ?? {};
+
+ if (detail.value.shippingId) {
+ const productRes = await returnManagementGetByShippingId({ shippingId: detail.value.shippingId });
+ if (productRes.code === 200) {
+ availableProducts.value = mergeShippingProductLists(productRes.data);
+ }
+ }
+
+ const list =
+ detail.value?.returnSaleProducts ||
+ detail.value?.returnSaleProductList ||
+ detail.value?.returnSaleProductDtoData ||
+ [];
+
+ tableData.value = Array.isArray(list)
+ ? list.map((raw) => {
+ const normalized = normalizeDetailRow(raw);
+ const product = pickShippingLine(normalized);
+ return product ? mergeDetailProductRow(product, normalized) : normalized;
+ })
+ : [];
+
+ const headerShipNo = detail.value?.shippingNo;
+ if (headerShipNo && Array.isArray(tableData.value) && tableData.value.length) {
+ tableData.value = tableData.value.map((r) =>
+ isEmptyText(r.outboundBatches) ? { ...r, outboundBatches: headerShipNo } : r
+ );
+ }
+ } catch (e) {
+ console.error("Failed to load detail", e);
+ } finally {
+ loading.value = false;
+ }
+};
+
+const closeDia = () => {
+ dialogVisible.value = false;
+ detail.value = {};
+ tableData.value = [];
+ availableProducts.value = [];
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+.descriptions {
+ margin-bottom: 20px;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: 600;
+ padding-left: 12px;
+ position: relative;
+}
+.descriptions::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 1rem;
+ background-color: #002FA7;
+ border-radius: 2px;
+}
+</style>
diff --git a/src/views/salesManagement/returnOrder/components/formDia.vue b/src/views/salesManagement/returnOrder/components/formDia.vue
new file mode 100644
index 0000000..6a6d756
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/components/formDia.vue
@@ -0,0 +1,743 @@
+<template>
+ <div>
+ <el-dialog v-model="dialogFormVisible" :title="operationType === 'edit' ? '缂栬緫閫�璐у崟' : '鏂板閫�璐у崟'" width="90%" @close="closeDia">
+ <div>
+ <span class="descriptions">鍩烘湰淇℃伅</span>
+ <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef">
+ <el-row :gutter="30">
+ <el-col :span="4">
+ <el-form-item label="閫�璐у崟鍙凤細" prop="returnNo">
+ <el-input
+ :disabled="operationType === 'edit' || form.returnNoCheckbox"
+ v-model="form.returnNo"
+ placeholder="浣跨敤绯荤粺缂栧彿"
+ class="input-with-select"
+ >
+ <template v-if="operationType !== 'edit'" #append>
+ <el-checkbox v-model="form.returnNoCheckbox" @change="handleReturnNoCheckboxChange"></el-checkbox>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerId">
+ <el-select v-model="form.customerId" filterable placeholder="璇烽�夋嫨瀹㈡埛" @change="customerNameChange">
+ <el-option
+ v-for="item in customerNameOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍏宠仈鍙戣揣鍗曞彿锛�" prop="shippingId">
+ <el-select v-model="form.shippingId" filterable placeholder="璇烽�夋嫨鍑哄簱鍗曞彿" @change="outboundNoChange">
+ <el-option
+ v-for="item in outboundOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍒跺崟浜猴細" prop="maker">
+ <el-select v-model="form.maker" filterable placeholder="璇烽�夋嫨鍒跺崟浜�">
+ <el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="鍒跺崟鏃堕棿锛�" prop="makeTime">
+ <el-date-picker v-model="form.makeTime" type="datetime" style="width:100%" value-format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss" />
+ </el-form-item>
+ </el-col>
+ <!-- <el-col :span="4">
+ <el-form-item label="鐘舵�侊細" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��">
+ <el-option label="寰呭鐞�" :value="0" />
+ <el-option label="宸插鐞�" :value="1" />
+ </el-select>
+ </el-form-item>
+ </el-col> -->
+ <el-col :span="4">
+ <el-form-item label="閫�璐у師鍥狅細" prop="returnReason">
+ <el-input v-model="form.returnReason" placeholder="璇疯緭鍏ラ��璐у師鍥�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="4">
+ <el-form-item label="閫�娆炬�婚锛�" prop="refundAmount">
+ <el-input v-model="form.refundAmount" disabled placeholder="鑷姩璁$畻" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <hr>
+ <div style="padding-top: 20px">
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
+ <span class="descriptions" style="margin-bottom:0">浜у搧鍒楄〃</span>
+ <el-button type="primary" @click="openProductSelection" :disabled="!form.shippingId">娣诲姞浜у搧</el-button>
+ </div>
+ <PIMTable :isShowPagination="false" rowKey="id" :column="tableColumn" :tableData="tableData">
+ <template #totalReturnNum="{ row }">
+ {{ calcAlreadyReturned(row) }}
+ </template>
+ <template #returnQuantity="{ row }">
+ <el-input
+ v-model="row.returnQuantity"
+ style="width:100px"
+ placeholder="璇疯緭鍏�"
+ type="number"
+ @input="(val) => handleReturnQuantityChange(val, row)"
+ />
+ </template>
+ <template #price="{ row }">
+ <el-input
+ v-model="row.price"
+ style="width:100px"
+ placeholder="璇疯緭鍏�"
+ type="number"
+ @input="(val) => handlePriceChange(val, row)"
+ />
+ </template>
+ <template #amount="{ row }">
+ <el-input
+ v-model="row.amount"
+ style="width:100px"
+ placeholder="鑷姩璁$畻"
+ type="number"
+ disabled
+ />
+ </template>
+ <template #isQuality="{ row }">
+ <el-select v-model="row.isQuality" placeholder="璇烽�夋嫨" style="width:120px">
+ <el-option label="鏄�" :value="1" />
+ <el-option label="鍚�" :value="2" />
+ </el-select>
+ </template>
+ <template #remark="{ row }">
+ <el-input
+ v-model="row.remark"
+ style="width:130px"
+ placeholder="璇疯緭鍏�"
+ />
+ </template>
+ <template #action="{ index }">
+ <el-button type="danger" link @click="deleteRow(index)">鍒犻櫎</el-button>
+ </template>
+ </PIMTable>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭</el-button>
+ <el-button @click="closeDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <el-dialog v-model="productSelectionVisible" title="閫夋嫨浜у搧" width="70%" append-to-body>
+ <el-table
+ :data="availableProducts"
+ style="width: 100%"
+ @selection-change="handleSelectionChange"
+ ref="productTableRef"
+ row-key="id"
+ >
+ <el-table-column align="center" type="selection" width="55" />
+ <el-table-column align="center" prop="outboundBatches" label="鍑哄簱鍗曞彿" />
+ <el-table-column align="center" prop="batchNo" label="鎵规鍙�" />
+ <el-table-column align="center" prop="productCategory" label="浜у搧澶х被" />
+ <el-table-column align="center" prop="specificationModel" label="瑙勬牸鍨嬪彿" />
+ <el-table-column align="center" prop="unit" label="鍗曚綅" />
+ <el-table-column align="center" prop="stockOutNum" label="鎬绘暟閲�" />
+ <el-table-column align="center" prop="unQuantity" label="鏈��璐ф暟閲�" />
+ <el-table-column align="center" label="宸查��璐ф暟閲�">
+ <template #default="{ row }">{{ calcAlreadyReturned(row) }}</template>
+ </el-table-column>
+
+ </el-table>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="confirmProductSelection">纭娣诲姞</el-button>
+ <el-button @click="productSelectionVisible = false">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, getCurrentInstance } from "vue";
+import { returnManagementAdd, returnManagementUpdate, returnManagementGetByShippingId, getSalesLedger, returnManagementGetById } from "@/api/salesManagement/returnOrder.js";
+import useUserStore from "@/store/modules/user.js";
+import { userListNoPageByTenantId } from "@/api/system/user.js";
+import { listProject } from "@/api/projectManagement/project.js";
+import {listCustomer} from "@/api/basicData/customer.js";
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits(['close'])
+const dialogFormVisible = ref(false);
+const operationType = ref('');
+const formRef = ref(null);
+const userStore = useUserStore();
+
+const data = reactive({
+ form: {
+ returnNoCheckbox: true,
+ returnNo: "",
+ customerId: "",
+ shippingId: "",
+ projectId: "",
+ maker: "",
+ makeTime: "",
+ status: 0,
+ returnReason: "",
+ refundAmount: "",
+ },
+ rules: {
+ returnNo: [{
+ validator: (rule, value, callback) => {
+ if (form.value.returnNoCheckbox) return callback();
+ if (!value) return callback(new Error("璇疯緭鍏ラ��璐у崟鍙�"));
+ callback();
+ }, trigger: "blur"
+ }],
+ customerId: [{ required: true, message: "璇烽�夋嫨瀹㈡埛", trigger: "change" }],
+ shippingId: [{ required: true, message: "璇烽�夋嫨鍏宠仈鍑哄簱鍗曞彿", trigger: "change" }],
+ }
+});
+const { form, rules } = toRefs(data);
+
+const calcAlreadyReturned = (row) => {
+ const total = Number(row?.stockOutNum ?? row?.totalQuantity ?? row?.totalReturnNum ?? 0);
+ const un = Number(row?.unQuantity ?? 0);
+ if (!Number.isFinite(total) || !Number.isFinite(un)) return 0;
+ return Math.max(total - un, 0);
+};
+
+const tableColumn = ref([
+ {align: "center", label: "鍑哄簱鍗曞彿", prop: "outboundBatches" },
+ {align: "center", label: "鎵规鍙�", prop: "batchNo" },
+ {align: "center", label: "浜у搧澶х被", prop: "productCategory" },
+ {align: "center", label: "瑙勬牸鍨嬪彿", prop: "specificationModel" },
+ {align: "center", label: "鍗曚綅", prop: "unit", width: 80 },
+ {align: "center", label: "鎬绘暟閲�", prop: "stockOutNum", width: 120 },
+ {align: "center", label: "宸查��璐ф暟閲�", prop: "totalReturnNum", width: 120, dataType: "slot", slot: "totalReturnNum" },
+ {align: "center", label: "鏈��璐ф暟閲�", prop: "unQuantity", width: 120 },
+ {align: "center", label: "閫�璐ф暟閲�", prop: "returnQuantity", dataType: "slot", slot: "returnQuantity", width: 120 },
+ {align: "center", label: "閫�璐т骇鍝佸崟浠�", prop: "price", dataType: "slot", slot: "price", width: 120 },
+ {align: "center", label: "閫�璐т骇鍝侀噾棰�", prop: "amount", dataType: "slot", slot: "amount", width: 120 },
+ {align: "center", label: "鏄惁鏈夎川閲忛棶棰�", prop: "isQuality", dataType: "slot", slot: "isQuality", width: 140 },
+ {align: "center", label: "澶囨敞", prop: "remark", dataType: "slot", slot: "remark", width: 150 },
+ {align: "center", label: "鎿嶄綔" , prop: "action", dataType: "slot", slot: "action", width: 120 },
+]);
+const tableData = ref([]);
+const customerNameOptions = ref([]);
+const outboundOptions = ref([]);
+const userOptions = ref([]);
+const projectOptions = ref([]);
+
+const deleteRow = (index) => {
+ tableData.value.splice(index, 1);
+};
+
+const sameKey = (a, b) => a != null && b != null && String(a) === String(b);
+
+/** 鎺ュ彛鍙兘鎷嗘垚 shippingProductVoList / productDtoData 涓や唤锛屽彧鍙栧叾涓�浼氱己鎵规銆佹暟閲忕瓑瀛楁 */
+const mergeShippingProductLists = (data) => {
+ const lists = [data?.shippingProductVoList, data?.productDtoData].filter(Array.isArray);
+ if (!lists.length) return [];
+ const map = new Map();
+ for (const list of lists) {
+ for (const p of list) {
+ if (p == null) continue;
+ const key = p.id != null ? String(p.id) : null;
+ if (!key) continue;
+ const prev = map.get(key);
+ map.set(key, prev ? { ...prev, ...p } : { ...p });
+ }
+ }
+ return Array.from(map.values());
+};
+
+const pickShippingLine = (normalized) => {
+ const pid = normalized?.returnSaleLedgerProductId ?? normalized?.id;
+ const sid = normalized?.stockOutRecordId ?? normalized?.shippingProductId;
+ const direct = availableProducts.value.find(
+ (p) =>
+ sameKey(p?.id, pid) ||
+ sameKey(p?.stockOutRecordId, pid) ||
+ sameKey(p?.id, sid) ||
+ sameKey(p?.stockOutRecordId, sid)
+ );
+ if (direct) return direct;
+ const pmid = normalized?.productModelId;
+ if (pmid == null || pmid === "") return undefined;
+ const candidates = availableProducts.value.filter((p) => sameKey(p?.productModelId, pmid));
+ if (!candidates.length) return undefined;
+ if (candidates.length === 1) return candidates[0];
+ const spec = String(normalized?.specificationModel ?? normalized?.model ?? "");
+ if (spec) {
+ const hit = candidates.find((p) => {
+ const ps = String(p?.specificationModel ?? p?.model ?? "");
+ return ps && ps === spec;
+ });
+ if (hit) return hit;
+ }
+ return candidates[0];
+};
+
+const isEmptyText = (v) => v === "" || v == null || v === undefined;
+
+const firstFiniteNumber = (...vals) => {
+ for (const v of vals) {
+ if (v === "" || v == null || v === undefined) continue;
+ const n = Number(v);
+ if (Number.isFinite(n)) return n;
+ }
+ return undefined;
+};
+
+const firstNonEmptyText = (...vals) => {
+ const hit = vals.find((v) => !isEmptyText(v));
+ return hit === undefined ? "" : hit;
+};
+
+/** 璇︽儏鎺ュ彛瀛楁甯镐笉鍏紱{...product,...normalized} 浼氳 normalized 閲岀殑绌轰覆鐩栨帀鍑哄簱琛屼笂鐨勫睍绀哄瓧娈� */
+const mergeShippingLineWithDetail = (product, normalized) => {
+ const row = { ...product, ...normalized };
+ row.outboundBatches = firstNonEmptyText(
+ row.outboundBatches,
+ product?.outboundBatches,
+ product?.shippingNo,
+ product?.outboundNo,
+ normalized?.outboundBatches,
+ normalized?.outboundNo,
+ normalized?.shippingNo
+ );
+ row.batchNo = firstNonEmptyText(
+ row.batchNo,
+ product?.batchNo,
+ product?.batchNumber,
+ product?.lotNo,
+ product?.batchCode,
+ product?.shippingBatchNo,
+ normalized?.batchNo,
+ normalized?.batchNumber,
+ normalized?.lotNo,
+ normalized?.shippingBatchNo
+ );
+ const stock = firstFiniteNumber(
+ row.stockOutNum,
+ product?.stockOutNum,
+ product?.totalQuantity,
+ product?.shippingQuantity,
+ product?.deliveryQuantity,
+ product?.quantity,
+ product?.outQuantity,
+ normalized?.stockOutNum,
+ normalized?.totalQuantity,
+ normalized?.shippingQuantity,
+ normalized?.deliveryQuantity
+ );
+ if (stock !== undefined) row.stockOutNum = stock;
+ const un = firstFiniteNumber(
+ row.unQuantity,
+ product?.unQuantity,
+ product?.remainingQuantity,
+ product?.noReturnQuantity,
+ product?.canReturnQuantity,
+ product?.availableReturnNum,
+ normalized?.unQuantity,
+ normalized?.remainingQuantity,
+ normalized?.noReturnQuantity,
+ normalized?.canReturnQuantity
+ );
+ if (un !== undefined) row.unQuantity = un;
+ else {
+ const s = Number(row.stockOutNum);
+ const ret = Number(row.totalReturnNum ?? 0);
+ if (Number.isFinite(s) && s >= 0 && Number.isFinite(ret) && ret >= 0) {
+ row.unQuantity = Math.max(0, s - ret);
+ }
+ }
+ const returned = firstFiniteNumber(
+ row.totalReturnNum,
+ product?.totalReturnNum,
+ product?.totalReturnedNum,
+ normalized?.totalReturnNum,
+ normalized?.totalReturnedNum
+ );
+ if (returned !== undefined) row.totalReturnNum = returned;
+ else if (isEmptyText(row.totalReturnNum)) row.totalReturnNum = 0;
+ if (isEmptyText(row.unit)) {
+ row.unit = firstNonEmptyText(product?.unit, normalized?.unit);
+ }
+ if (isEmptyText(row.productCategory)) {
+ row.productCategory = firstNonEmptyText(
+ normalized?.productCategory,
+ normalized?.productName,
+ product?.productCategory,
+ product?.productName
+ );
+ }
+ if (isEmptyText(row.specificationModel)) {
+ row.specificationModel = firstNonEmptyText(
+ normalized?.specificationModel,
+ normalized?.model,
+ product?.specificationModel,
+ product?.model
+ );
+ }
+ return row;
+};
+
+const normalizeDetailRow = (raw) => {
+ const ledgerId =
+ raw?.returnSaleLedgerProductId ??
+ raw?.saleLedgerProductId ??
+ raw?.stockOutRecordId ??
+ raw?.shippingProductId;
+ const productId = ledgerId ?? raw?.id;
+ const returnSaleProductId = raw?.returnSaleProductId ?? raw?.id;
+ const num = Number(raw?.num ?? raw?.returnQuantity ?? 0);
+ return {
+ ...raw,
+ id: productId,
+ returnSaleProductId,
+ returnSaleLedgerProductId: productId,
+ productModelId: raw?.productModelId,
+ stockOutRecordId: raw?.stockOutRecordId,
+ shippingProductId: raw?.shippingProductId,
+ productCategory: raw?.productCategory ?? raw?.productName ?? raw?.productTypeName ?? "",
+ specificationModel: raw?.specificationModel ?? raw?.model ?? raw?.specModel ?? "",
+ outboundBatches: raw?.outboundBatches ?? raw?.outboundNo ?? raw?.shippingNo,
+ batchNo:
+ raw?.batchNo ??
+ raw?.batchNumber ??
+ raw?.lotNo ??
+ raw?.batchCode ??
+ raw?.shippingBatchNo,
+ stockOutNum:
+ raw?.stockOutNum ??
+ raw?.totalQuantity ??
+ raw?.shippingQuantity ??
+ raw?.deliveryQuantity ??
+ raw?.quantity,
+ totalReturnNum: raw?.totalReturnNum ?? raw?.totalReturnedNum,
+ unQuantity:
+ raw?.unQuantity ??
+ raw?.remainingQuantity ??
+ raw?.noReturnQuantity ??
+ raw?.canReturnQuantity,
+ num,
+ returnQuantity: Number.isFinite(num) ? num : 0,
+ price: Number(raw?.taxInclusiveUnitPrice ?? raw?.price ?? 0),
+ amount: Number(raw?.amount ?? 0).toFixed(2),
+ isQuality: raw?.isQuality ?? 2,
+ remark: raw?.remark ?? "",
+ };
+};
+
+const setFormForEdit = async (row) => {
+ const res = await returnManagementGetById({ returnManagementId: row?.id });
+ const detail = res?.data ?? res ?? {};
+
+ Object.assign(form.value, detail);
+ form.value.returnNoCheckbox = true;
+
+ if (form.value.customerId) {
+ await customerNameChange(form.value.customerId, false);
+ }
+ if (form.value.shippingId) {
+ await outboundNoChange(form.value.shippingId, false);
+ }
+
+ const list =
+ detail?.returnSaleProducts ||
+ detail?.returnSaleProductList ||
+ detail?.returnSaleProductDtoData ||
+ [];
+
+ tableData.value = Array.isArray(list)
+ ? list.map((raw) => {
+ const normalized = normalizeDetailRow(raw);
+ const product = pickShippingLine(normalized);
+ return product ? mergeShippingLineWithDetail(product, normalized) : normalized;
+ })
+ : [];
+
+ const headerShipNo = detail?.shippingNo ?? form.value?.shippingNo;
+ if (headerShipNo && Array.isArray(tableData.value) && tableData.value.length) {
+ tableData.value = tableData.value.map((r) =>
+ isEmptyText(r.outboundBatches) ? { ...r, outboundBatches: headerShipNo } : r
+ );
+ }
+
+ calculateTotalRefund();
+};
+
+const openDialog = async (type, row) => {
+ operationType.value = type;
+ dialogFormVisible.value = true;
+ proxy.resetForm("formRef");
+ await Promise.all([initCustomers(), initUsers(), initProjects()]);
+ if (type === "edit") {
+ await setFormForEdit(row);
+ } else {
+ tableData.value = [];
+ Object.assign(form.value, {
+ returnNoCheckbox: true,
+ returnNo: "",
+ customerId: "",
+ shippingId: "",
+ projectId: "",
+ maker: "",
+ makeTime: "",
+ status: 0,
+ returnReason: "",
+ refundAmount: "",
+ });
+ form.value.maker = userStore.nickName || userStore.name || "";
+ form.value.makeTime = new Date().toISOString().replace('T', ' ').split('.')[0]; // Default to now
+ form.value.status = 0; // Default status
+ }
+};
+
+const submitForm = () => {
+ proxy.$refs["formRef"].validate(valid => {
+ if (!valid) return;
+ const returnSaleProducts = (tableData.value || []).map(el => ({
+ stockOutRecordId: el.returnSaleLedgerProductId ?? el.id,
+ productModelId: el.productModelId,
+ unit: el.unit,
+ num: Number(el.num ?? el.returnQuantity ?? 0),
+ price: Number(el.price ?? 0),
+ amount: Number(el.amount ?? 0),
+ isQuality: el.isQuality ?? 2,
+ remark: el.remark ?? "",
+ id: operationType.value === "edit" ? (el.returnSaleProductId ?? "") : ""
+ }));
+ const payload = { ...form.value, returnSaleProducts };
+ delete payload.returnNoCheckbox;
+ if (operationType.value === "add" && form.value.returnNoCheckbox) delete payload.returnNo;
+ if (operationType.value === "add") {
+ returnManagementAdd(payload).then(() => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛");
+ closeDia();
+ });
+ } else {
+ returnManagementUpdate(payload).then(() => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛");
+ closeDia();
+ });
+ }
+ });
+};
+
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+ emit('close');
+};
+
+const initCustomers = async () => {
+ listCustomer({current: -1,size:-1, type: 0}).then((res) => {
+ customerNameOptions.value = res.data.records.map(item => ({
+ label: item.customerName,
+ value: item.customerName, // Keep value as name if needed for other logic, but request says customerId
+ id: item.id,
+ code: item.customerCode
+ }));
+ });
+};
+
+const initUsers = async () => {
+ const res = await userListNoPageByTenantId();
+ if (res?.data) {
+ userOptions.value = res.data.map(u => ({ label: u.nickName || u.userName, value: u.nickName || u.userName }));
+ }
+};
+
+const initProjects = async () => {
+ try {
+ const res = await listProject({ pageSize: 1000 });
+ if (res?.rows) {
+ projectOptions.value = res.rows.map(p => ({ label: p.projectName, value: p.id }));
+ }
+ } catch (e) {
+ console.error("Failed to load projects", e);
+ }
+};
+
+const handleReturnNoCheckboxChange = (checked) => {
+ if (checked) form.value.returnNo = "";
+ formRef.value?.validateField('returnNo');
+};
+
+const customerNameChange = async (val, clearDownstream = true) => {
+ // val is customerId now
+ if (clearDownstream) {
+ form.value.shippingId = "";
+ outboundOptions.value = [];
+ }
+
+ // Find customer name for getSalesLedger if it requires name
+ const customer = customerNameOptions.value.find(c => c.id === val);
+ if (!customer) return;
+
+ // Assuming getSalesLedger takes customerName. If it takes ID, adjust accordingly.
+ // Previous code used customerName. Let's try passing customerName.
+ getSalesLedger({
+ customerName: customer.label,
+ }).then(res => {
+ if(res.code === 200){
+ outboundOptions.value = res.data.map(item => ({
+ label: item.shippingNo, // Or whatever the outbound number field is
+ value: item.id,
+ }))
+ }
+ })
+};
+
+const outboundNoChange = async (val, clearTable = true) => {
+ // val is shippingId
+ let res = await returnManagementGetByShippingId({ shippingId: val });
+ if(res.code === 200){
+ // If backend returns project info, set it
+ if (res.data.projectId) form.value.projectId = res.data.projectId;
+
+ availableProducts.value = mergeShippingProductLists(res.data);
+ if (clearTable) tableData.value = [];
+ }
+};
+
+const handleReturnQuantityChange = (val, row) => {
+ if (val === "" || val === null) return;
+ const max = row.unQuantity === undefined || row.unQuantity === null ? Infinity : Number(row.unQuantity || 0);
+ const current = Number(val);
+
+ if (current > max) {
+ proxy.$nextTick(() => {
+ row.returnQuantity = max;
+ row.num = max;
+ });
+ proxy.$modal.msgWarning(`閫�璐ф暟閲忎笉鑳借秴杩囨湭閫�璐ф暟閲�(${max})`);
+ } else if (current < 0) {
+ proxy.$nextTick(() => {
+ row.returnQuantity = 0;
+ row.num = 0;
+ });
+ } else {
+ row.num = current;
+ }
+ calculateRowAmount(row);
+ calculateTotalRefund();
+};
+
+const handlePriceChange = (val, row) => {
+ if (val === "" || val === null) {
+ row.price = 0;
+ }
+ calculateRowAmount(row);
+ calculateTotalRefund();
+};
+
+const calculateRowAmount = (row) => {
+ const stockOutNum = Number(row.returnQuantity || 0);
+ const price = Number(row.price || 0);
+ row.amount = (stockOutNum * price).toFixed(2);
+};
+
+const calculateTotalRefund = () => {
+ const total = tableData.value.reduce((sum, row) => {
+ return sum + Number(row.amount || 0);
+ }, 0);
+ form.value.refundAmount = total.toFixed(2);
+};
+
+const availableProducts = ref([]);
+const productSelectionVisible = ref(false);
+const selectedProducts = ref([]);
+
+const openProductSelection = () => {
+ productSelectionVisible.value = true;
+ // Pre-select items already in tableData
+ proxy.$nextTick(() => {
+ if (proxy.$refs.productTableRef) {
+ proxy.$refs.productTableRef.clearSelection();
+ availableProducts.value.forEach(row => {
+ if (tableData.value.some(item => item.id === row.id)) {
+ proxy.$refs.productTableRef.toggleRowSelection(row, true);
+ }
+ });
+ }
+ });
+};
+
+const handleSelectionChange = (val) => {
+ selectedProducts.value = val;
+};
+
+// Removed checkSelectable to allow toggling existing items
+const confirmProductSelection = () => {
+ const newTableData = [];
+
+ selectedProducts.value.forEach(product => {
+ const existing = tableData.value.find(item => item.id === product.id);
+ if (existing) {
+ newTableData.push(existing);
+ } else {
+ newTableData.push({
+ ...product,
+ returnSaleLedgerProductId: product.id,
+ productModelId: product.productModelId,
+ returnQuantity: 0,
+ num: 0,
+ price: Number(product.taxInclusiveUnitPrice ?? 0),
+ amount: "0.00",
+ isQuality: 2,
+ remark: "",
+ productCategory: product.productCategory ?? product.productName ?? "",
+ productName: product.productName,
+ specificationModel: product.specificationModel ?? product.model ?? "",
+ unit: product.unit,
+ stockOutNum: product.stockOutNum,
+ totalReturnNum: product.totalReturnNum,
+ unQuantity: product.unQuantity
+ });
+ }
+ });
+
+ tableData.value = newTableData;
+ productSelectionVisible.value = false;
+};
+
+defineExpose({ openDialog });
+</script>
+
+<style scoped lang="scss">
+.descriptions {
+ margin-bottom: 20px;
+ display: inline-block;
+ font-size: 1rem;
+ font-weight: 600;
+ padding-left: 12px;
+ position: relative;
+}
+.descriptions::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 4px;
+ height: 1rem;
+ background-color: #002FA7;
+ border-radius: 2px;
+}
+</style>
diff --git a/src/views/salesManagement/returnOrder/index.vue b/src/views/salesManagement/returnOrder/index.vue
new file mode 100644
index 0000000..0a8257b
--- /dev/null
+++ b/src/views/salesManagement/returnOrder/index.vue
@@ -0,0 +1,219 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" class="demo-form-inline" :inline="true">
+ <el-form-item label="閫�璐у崟鍙�">
+ <el-input v-model="searchForm.returnNo" placeholder="璇疯緭鍏ラ��璐у崟鍙�" clearable />
+ </el-form-item>
+ <el-form-item label="瀹㈡埛鍚嶇О">
+ <el-input v-model="searchForm.customerName" placeholder="瀹㈡埛鍚嶇О" clearable />
+ </el-form-item>
+ <el-form-item label="閿�鍞崟鍙�">
+ <el-input v-model="searchForm.salesContractNo" placeholder="閿�鍞崟鍙�" clearable />
+ </el-form-item>
+ <el-form-item label="鍏宠仈鍙戣揣鍗曞彿">
+ <el-input v-model="searchForm.shippingNo" placeholder="鍏宠仈鍙戣揣鍗曞彿" clearable />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+ <el-button @click="handleReset">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div class="table_header" style="display: flex;justify-content: flex-end;margin-bottom: 10px;">
+ <el-button type="primary" @click="openForm('add')">鏂板缓閿�鍞��璐�</el-button>
+ <el-button type="danger" :disabled="selectedRows.length === 0 || selectedRows.some(row => row.status !== 0)" @click="handleDelete">鍒犻櫎</el-button>
+ </div>
+ <PIMTable
+ rowKey="id"
+ :column="tableColumn"
+ :tableData="tableData"
+ :page="page"
+ :isSelection="true"
+ @selection-change="handleSelectionChange"
+ :tableLoading="tableLoading"
+ @pagination="pagination"
+ >
+ <template #status="{ row }">
+ <el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
+ </template>
+ </PIMTable>
+ </div>
+ <form-dia ref="formDia" @close="handleQuery" />
+ <detail-dia ref="detailDia" />
+ </div>
+</template>
+
+<script setup>
+import { reactive, ref, toRefs, computed, getCurrentInstance, nextTick, onMounted } from "vue";
+import { ElMessageBox } from "element-plus";
+import FormDia from "./components/formDia.vue";
+import DetailDia from "./components/detailDia.vue";
+import { returnManagementList, returnManagementDel, returnManagementHandle } from "@/api/salesManagement/returnOrder.js";
+const { proxy } = getCurrentInstance();
+
+const formDia = ref();
+const detailDia = ref();
+const openForm = (type, row) => {
+ nextTick(() => formDia.value?.openDialog(type, row));
+};
+
+const openDetail = (row) => {
+ nextTick(() => detailDia.value?.openDialog(row));
+};
+
+const handleRowDelete = (row) => {
+ if (!row?.id) return;
+ ElMessageBox.confirm("璇ラ��璐у崟灏嗚鍒犻櫎锛屾槸鍚︾‘璁ゅ垹闄わ紵", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementDel([row.id]).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ });
+};
+
+const handleRowHandle = (row) => {
+ if (!row?.id) return;
+ ElMessageBox.confirm("鏄惁澶勭悊璇ラ��璐у崟锛熷鐞嗗悗灏嗘棤娉曚慨鏀�", "澶勭悊鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementHandle({ returnManagementId: String(row.id) }).then(() => {
+ proxy.$modal.msgSuccess("澶勭悊鎴愬姛");
+ getList();
+ });
+ });
+}
+
+const data = reactive({
+ searchForm: {
+ returnNo: "",
+ status: "",
+ customerName: "",
+ salesContractNo: "",
+ salesman: "",
+ shippingNo: "",
+ projectName: "",
+ salesLedgerId: "",
+ makeTime: ""
+ }
+});
+const { searchForm } = toRefs(data);
+
+const documentStatusOptions = ref([
+ { label: "寰呭鐞�", value: 0 },
+ { label: "宸插鐞�", value: 1 }
+]);
+
+const defaultColumns = [
+ { label: "閫�璐у崟鍙�", prop: "returnNo", minWidth: 160 },
+ { label: "鍗曟嵁鐘舵��", prop: "status", minWidth: 90, dataType: "slot", slot: "status" },
+ { label: "鍒跺崟鏃堕棿", prop: "makeTime", minWidth: 170 },
+ { label: "瀹㈡埛鍚嶇О", prop: "customerName", minWidth: 220 },
+ { label: "閿�鍞崟鍙�", prop: "salesContractNo", minWidth: 160 },
+ { label: "涓氬姟鍛�", prop: "salesman", minWidth: 120 },
+ { label: "鍏宠仈鍙戣揣鍗曞彿", prop: "shippingNo", minWidth: 170 },
+ { label: "椤圭洰鍚嶇О", prop: "projectName", minWidth: 180 },
+ { label: "鍒跺崟浜�", prop: "maker", minWidth: 120 },
+ {
+ label: "鎿嶄綔",
+ prop: "operation",
+ dataType: "action",
+ align: "center",
+ fixed: "right",
+ minWidth: 240,
+ operation: [
+ { name: "缂栬緫", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => openForm("edit", row) },
+ { name: "閫�娆惧鐞�", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => handleRowHandle(row) },
+ { name: "璇︽儏", type: "text", clickFun: (row) => openDetail(row) },
+ { name: "鍒犻櫎", disabled: (row) => row.status !== 0, type: "text", clickFun: (row) => handleRowDelete(row) },
+ ],
+ },
+];
+const tableColumn = defaultColumns;
+
+const tableData = ref([]);
+const tableLoading = ref(false);
+const page = reactive({ current: 1, size: 10, total: 0 });
+const selectedRows = ref([]);
+const tableHeight = computed(() => "calc(100% - 80px)");
+
+const handleReset = () => {
+ Object.keys(searchForm.value).forEach(k => searchForm.value[k] = "");
+ handleQuery();
+};
+const handleSelectionChange = (selection) => {
+ selectedRows.value = selection;
+};
+const handleQuery = () => {
+ page.current = 1;
+ getList();
+};
+const pagination = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ returnManagementList({ ...searchForm.value, ...page }).then(res => {
+ tableLoading.value = false;
+ tableData.value = res?.data?.records || [];
+ page.total = res?.data?.total || 0;
+ }).finally(() => tableLoading.value = false);
+};
+const handleOut = () => {
+ ElMessageBox.alert("瀵煎嚭鍔熻兘寰呮帴鍏ユ帴鍙�", "鎻愮ず");
+};
+const handleDelete = () => {
+ let ids = [];
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ ids = selectedRows.value.map(i => i.id);
+ console.log(ids);
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "鍒犻櫎鎻愮ず", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ }).then(() => {
+ returnManagementDel( ids ).then(() => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ });
+};
+
+const getStatusType = (status) => {
+ const statusMap = {
+ 0: "warning",
+ 1: "success"
+ };
+ return statusMap[status] || "info";
+};
+
+const getStatusText = (status) => {
+ const statusMap = {
+ 0: "寰呭鐞�",
+ 1: "宸插鐞�"
+ };
+ return statusMap[status] || "鏈煡";
+};
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped lang="scss">
+.table_list {
+ margin-top: unset;
+}
+</style>
diff --git a/src/views/salesManagement/salesLedger/fileList.vue b/src/views/salesManagement/salesLedger/fileList.vue
new file mode 100644
index 0000000..eb4d401
--- /dev/null
+++ b/src/views/salesManagement/salesLedger/fileList.vue
@@ -0,0 +1,43 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="闄勪欢" width="40%" :before-close="handleClose">
+ <el-table :data="tableData" border height="40vh">
+ <el-table-column label="闄勪欢鍚嶇О" prop="name" min-width="400" show-overflow-tooltip />
+ <el-table-column fixed="right" label="鎿嶄綔" width="100" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="downLoadFile(scope.row)">涓嬭浇</el-button>
+ <el-button link type="primary" @click="lookFile(scope.row)">棰勮</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import filePreview from '@/components/filePreview/index.vue'
+
+const dialogVisible = ref(false)
+const tableData = ref([])
+const { proxy } = getCurrentInstance();
+const filePreviewRef = ref()
+const handleClose = () => {
+ dialogVisible.value = false
+}
+const open = (list) => {
+ dialogVisible.value = true
+ tableData.value = list
+}
+const downLoadFile = (row) => {
+ proxy.$download.byUrl(row.url, row.originalFilename);
+
+}
+const lookFile = (row) => {
+ filePreviewRef.value.open(row.url)
+}
+defineExpose({
+ open
+})
+</script>
+
+<style></style>
\ No newline at end of file
diff --git a/src/views/salesManagement/salesLedger/index.vue b/src/views/salesManagement/salesLedger/index.vue
new file mode 100644
index 0000000..604ba82
--- /dev/null
+++ b/src/views/salesManagement/salesLedger/index.vue
@@ -0,0 +1,3178 @@
+<template>
+ <div class="app-container">
+ <div class="search_form">
+ <el-form :model="searchForm" :inline="true">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�">
+ <el-input
+ v-model="searchForm.customerName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="閿�鍞悎鍚屽彿锛�">
+ <el-input
+ v-model="searchForm.salesContractNo"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="椤圭洰鍚嶇О锛�">
+ <el-input
+ v-model="searchForm.projectName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ prefix-icon="Search"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="褰曞叆鏃ユ湡锛�">
+ <el-date-picker
+ v-model="searchForm.entryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="daterange"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="changeDaterange"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="handleQuery"> 鎼滅储 </el-button>
+ </el-form-item>
+ </el-form>
+ </div>
+ <div class="table_list">
+ <div class="actions">
+ <div></div>
+ <div>
+ <el-button type="primary" @click="openForm('add')">
+ 鏂板鍙拌处
+ </el-button>
+ <el-button type="primary" plain @click="handleImport"
+ >瀵煎叆
+ </el-button>
+ <el-button @click="handleOut">瀵煎嚭</el-button>
+ <el-button type="danger" plain @click="handleDelete">鍒犻櫎 </el-button>
+ <el-button type="primary" plain @click="handlePrint">鎵撳嵃 </el-button>
+ </div>
+ </div>
+ <el-table
+ :data="tableData"
+ border
+ v-loading="tableLoading"
+ @selection-change="handleSelectionChange"
+ :expand-row-keys="expandedRowKeys"
+ :row-key="(row) => row.id"
+ :row-class-name="tableRowClassName"
+ show-summary
+ style="width: 100%"
+ :summary-method="summarizeMainTable"
+ @expand-change="expandChange"
+ height="calc(100vh - 18.5em)"
+ >
+ <el-table-column
+ align="center"
+ type="selection"
+ width="55"
+ fixed="left"
+ />
+ <el-table-column type="expand" width="60" fixed="left">
+ <template #default="props">
+ <el-table
+ :data="props.row.children"
+ border
+ show-summary
+ :summary-method="
+ (param) => summarizeChildrenTable(param, props.row)
+ "
+ >
+ <el-table-column align="center" label="搴忓彿" type="index" />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+ <el-table-column label="鍗曚綅" prop="unit" />
+ <el-table-column label="浜у搧鐘舵��" width="100px" align="center">
+ <template #default="scope">
+ <el-tag v-if="scope.row.approveStatus === 1" type="success"
+ >鍏呰冻
+ </el-tag>
+ <el-tag
+ v-else-if="
+ scope.row.approveStatus === 0 &&
+ scope.row.noQuantity === 0
+ "
+ type="success"
+ >宸插嚭搴�
+ </el-tag>
+ <el-tag v-else type="danger">涓嶈冻 </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙戣揣鐘舵��" width="140" align="center">
+ <template #default="scope">
+ <el-tag :type="getShippingStatusType(scope.row)" size="small">
+ {{ getShippingStatusText(scope.row) }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="蹇�掑叕鍙�"
+ prop="expressCompany"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="蹇�掑崟鍙�"
+ prop="expressNumber"
+ show-overflow-tooltip
+ />
+ <el-table-column label="鍙戣揣杞︾墝" minWidth="100px" align="center">
+ <template #default="scope">
+ <div>
+ <el-tag type="success" v-if="scope.row.shippingCarNumber"
+ >{{ scope.row.shippingCarNumber }}
+ </el-tag>
+ <el-tag v-else type="info">- </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍙戣揣鏃ユ湡" minWidth="100px" align="center">
+ <template #default="scope">
+ <div>
+ <div v-if="scope.row.shippingDate">
+ {{ scope.row.shippingDate }}
+ </div>
+ <el-tag v-else type="info">- </el-tag>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏁伴噺" prop="quantity" />
+ <el-table-column label="寰呭彂璐ф暟閲�" prop="noQuantity" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" />
+ <el-table-column
+ label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="sensitiveAmountFormatter"
+ />
+ <el-table-column
+ label="鍚◣鎬讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ :formatter="sensitiveAmountFormatter"
+ />
+ <el-table-column
+ label="涓嶅惈绋庢�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ :formatter="sensitiveAmountFormatter"
+ />
+ <!--鎿嶄綔-->
+ <el-table-column Width="60px" label="鎿嶄綔" align="center">
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ :disabled="!canShip(scope.row)"
+ @click="openDeliveryForm(scope.row)"
+ >
+ 鍙戣揣
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </template>
+ </el-table-column>
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column
+ label="閿�鍞悎鍚屽彿"
+ prop="salesContractNo"
+ width="180"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="瀹㈡埛鍚嶇О"
+ prop="customerName"
+ width="300"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="涓氬姟鍛�"
+ prop="salesman"
+ width="100"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="椤圭洰鍚嶇О"
+ prop="projectName"
+ width="180"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="浠樻鏂瑰紡"
+ prop="paymentMethod"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="鍚堝悓閲戦(鍏�)"
+ prop="contractAmount"
+ width="220"
+ show-overflow-tooltip
+ :formatter="formattedNumber"
+ />
+ <el-table-column
+ label="褰曞叆浜�"
+ prop="entryPersonName"
+ width="100"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="褰曞叆鏃ユ湡"
+ prop="entryDate"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="绛捐鏃ユ湡"
+ prop="executionDate"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="浜や粯鏃ユ湡"
+ prop="deliveryDate"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ label="澶囨敞"
+ prop="remarks"
+ width="200"
+ show-overflow-tooltip
+ />
+ <el-table-column fixed="right" label="鎿嶄綔" width="220" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="openForm('view', scope.row)"
+ >璇︽儏
+ </el-button>
+ <el-button
+ link
+ type="primary"
+ @click="openForm('edit', scope.row)"
+ :disabled="
+ !scope.row.isEdit ||
+ scope.row.hasProductionRecord ||
+ !canEditLedger(scope.row)
+ "
+ >缂栬緫
+ </el-button>
+ <el-button link type="primary" @click="openFileDialog(scope.row)"
+ >闄勪欢
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="page.current"
+ :limit="page.size"
+ @pagination="paginationChange"
+ />
+ </div>
+ <FormDialog
+ v-model="dialogFormVisible"
+ :title="
+ operationType === 'add'
+ ? '鏂板閿�鍞彴璐﹂〉闈�'
+ : operationType === 'edit'
+ ? '缂栬緫閿�鍞彴璐﹂〉闈�'
+ : '閿�鍞彴璐﹁鎯�'
+ "
+ :width="'70%'"
+ :operation-type="operationType"
+ @close="closeDia"
+ @confirm="submitForm"
+ @cancel="closeDia"
+ >
+ <el-form
+ :model="form"
+ label-width="140px"
+ label-position="top"
+ :rules="rules"
+ ref="formRef"
+ >
+ <!-- 鎶ヤ环鍗曞鍏ュ叆鍙o細鏀惧湪琛ㄥ崟椤堕儴锛岄�夋嫨鍚庡弽鏄惧鎴�/涓氬姟鍛樼瓑 -->
+ <el-row v-if="operationType === 'add'" style="margin-bottom: 10px">
+ <el-col :span="24" style="text-align: right">
+ <el-button type="primary" plain @click="openQuotationDialog">
+ 浠庨攢鍞姤浠峰鍏�
+ </el-button>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="閿�鍞悎鍚屽彿锛�" prop="salesContractNo">
+ <div
+ style="
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ "
+ >
+ <el-checkbox
+ v-model="form.autoGenerateContractNo"
+ v-if="operationType === 'add'"
+ >鑷姩鐢熸垚
+ </el-checkbox>
+ <el-input
+ v-model="form.salesContractNo"
+ :placeholder="
+ form.autoGenerateContractNo ? '淇濆瓨鍚庤嚜鍔ㄧ敓鎴�' : '璇疯緭鍏�'
+ "
+ clearable
+ :disabled="
+ form.autoGenerateContractNo || operationType === 'view'
+ "
+ />
+ </div>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓氬姟鍛橈細" prop="salesman">
+ <el-select
+ v-model="form.salesman"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :disabled="operationType === 'view'"
+ filterable
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.nickName"
+ :label="item.nickName"
+ :value="item.nickName"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О锛�" prop="customerId">
+ <el-select
+ v-model="form.customerId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :disabled="operationType === 'view'"
+ filterable
+ >
+ <el-option
+ v-for="item in customerOption"
+ :key="item.id"
+ :label="item.customerName"
+ :value="item.id"
+ >
+ {{
+ item.customerName + "鈥斺��" + item.taxpayerIdentificationNumber
+ }}
+ </el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="椤圭洰鍚嶇О锛�" prop="projectName">
+ <el-input
+ v-model="form.projectName"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="operationType === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="绛捐鏃ユ湡锛�" prop="executionDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.executionDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :disabled="operationType === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡">
+ <el-input
+ v-model="form.paymentMethod"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :disabled="operationType === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="褰曞叆浜猴細" prop="entryPerson">
+ <el-select
+ v-model="form.entryPerson"
+ filterable
+ default-first-option
+ :reserve-keyword="false"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :disabled="operationType === 'view'"
+ @change="changs"
+ >
+ <el-option
+ v-for="item in userList"
+ :key="item.userId"
+ :label="item.nickName"
+ :value="item.userId"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰曞叆鏃ユ湡锛�" prop="entryDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.entryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :disabled="operationType === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="浜よ揣鏃ユ湡锛�" prop="entryDate">
+ <el-date-picker
+ style="width: 100%"
+ v-model="form.deliveryDate"
+ value-format="YYYY-MM-DD"
+ format="YYYY-MM-DD"
+ type="date"
+ placeholder="璇烽�夋嫨"
+ clearable
+ :disabled="operationType === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-form-item label="浜у搧淇℃伅锛�" prop="entryDate">
+ <el-button
+ v-if="operationType !== 'view'"
+ type="primary"
+ @click="openProductForm('add')"
+ >娣诲姞
+ </el-button>
+ <el-button
+ v-if="operationType !== 'view'"
+ plain
+ type="danger"
+ @click="deleteProduct"
+ >鍒犻櫎
+ </el-button>
+ </el-form-item>
+ </el-row>
+ <el-table
+ :data="productData"
+ border
+ @selection-change="productSelected"
+ show-summary
+ :summary-method="summarizeMainTable"
+ >
+ <el-table-column
+ align="center"
+ type="selection"
+ width="55"
+ v-if="operationType !== 'view'"
+ :selectable="(row) => !isProductShipped(row)"
+ />
+ <el-table-column
+ align="center"
+ label="搴忓彿"
+ type="index"
+ width="60"
+ />
+ <el-table-column label="浜у搧澶х被" prop="productCategory" />
+ <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+ <el-table-column label="鍗曚綅" prop="unit" />
+ <el-table-column label="鏁伴噺" prop="quantity" />
+ <el-table-column label="绋庣巼(%)" prop="taxRate" />
+ <el-table-column
+ label="鍚◣鍗曚环(鍏�)"
+ prop="taxInclusiveUnitPrice"
+ :formatter="formattedNumber"
+ />
+ <el-table-column
+ label="鍚◣鎬讳环(鍏�)"
+ prop="taxInclusiveTotalPrice"
+ :formatter="formattedNumber"
+ />
+ <el-table-column
+ label="涓嶅惈绋庢�讳环(鍏�)"
+ prop="taxExclusiveTotalPrice"
+ :formatter="formattedNumber"
+ />
+ <el-table-column label="鏄惁鐢熶骇" prop="isProduction" width="150">
+ <template #default="scope">
+ <el-tag :type="scope.row.isProduction ? 'success' : 'info'">
+ {{ scope.row.isProduction ? "鏄�" : "鍚�" }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column
+ fixed="right"
+ label="鎿嶄綔"
+ min-width="60"
+ align="center"
+ v-if="operationType !== 'view'"
+ >
+ <template #default="scope">
+ <el-button
+ link
+ type="primary"
+ size="small"
+ :disabled="isProductShipped(scope.row)"
+ @click="openProductForm('edit', scope.row, scope.$index)"
+ >缂栬緫
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="澶囨敞锛�" prop="remarks">
+ <el-input
+ v-model="form.remarks"
+ placeholder="璇疯緭鍏�"
+ clearable
+ type="textarea"
+ :rows="2"
+ :disabled="operationType === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row v-if="operationType !== 'view'" :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="闄勪欢鏉愭枡锛�" prop="salesLedgerFiles">
+ <FileUpload
+ v-model:file-list="fileList"
+ :disabled="operationType === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+ <!-- 浠庢姤浠峰崟瀵煎叆锛堜粎瀹℃壒閫氳繃锛� -->
+ <el-dialog
+ v-model="quotationDialogVisible"
+ title="閫夋嫨瀹℃壒閫氳繃鐨勯攢鍞姤浠峰崟"
+ width="80%"
+ :close-on-click-modal="false"
+ >
+ <div
+ style="
+ margin-bottom: 12px;
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ "
+ >
+ <el-input
+ v-model="quotationSearchForm.quotationNo"
+ placeholder="璇疯緭鍏ユ姤浠峰崟鍙�"
+ clearable
+ style="max-width: 260px"
+ @change="fetchQuotationList"
+ />
+ <el-input
+ v-model="quotationSearchForm.customer"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ clearable
+ style="max-width: 260px"
+ @change="fetchQuotationList"
+ />
+ <el-button type="primary" @click="fetchQuotationList">鎼滅储 </el-button>
+ <el-button @click="resetQuotationSearch">閲嶇疆</el-button>
+ </div>
+ <el-table
+ :data="quotationList"
+ border
+ stripe
+ v-loading="quotationLoading"
+ height="420px"
+ >
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column
+ prop="quotationNo"
+ label="鎶ヤ环鍗曞彿"
+ width="180"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="customer"
+ label="瀹㈡埛鍚嶇О"
+ min-width="220"
+ show-overflow-tooltip
+ />
+ <el-table-column
+ prop="salesperson"
+ label="涓氬姟鍛�"
+ width="120"
+ show-overflow-tooltip
+ />
+ <el-table-column prop="quotationDate" label="鎶ヤ环鏃ユ湡" width="140" />
+ <el-table-column
+ prop="status"
+ label="瀹℃壒鐘舵��"
+ width="120"
+ align="center"
+ />
+ <el-table-column
+ prop="totalAmount"
+ label="鎶ヤ环閲戦(鍏�)"
+ width="160"
+ align="right"
+ >
+ <template #default="scope">
+ {{ Number(scope.row.totalAmount ?? 0).toFixed(2) }}
+ </template>
+ </el-table-column>
+ <el-table-column fixed="right" label="鎿嶄綔" width="120" align="center">
+ <template #default="scope">
+ <el-button type="primary" link @click="applyQuotation(scope.row)"
+ >閫夋嫨
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-show="quotationPage.total > 0"
+ :total="quotationPage.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="quotationPage.current"
+ :limit="quotationPage.size"
+ @pagination="quotationPaginationChange"
+ />
+ <template #footer>
+ <el-button @click="quotationDialogVisible = false">鍏抽棴</el-button>
+ </template>
+ </el-dialog>
+ <FormDialog
+ v-model="productFormVisible"
+ :title="productOperationType === 'add' ? '鏂板浜у搧' : '缂栬緫浜у搧'"
+ :width="'40%'"
+ :operation-type="productOperationType"
+ @close="closeProductDia"
+ @confirm="submitProduct"
+ @cancel="closeProductDia"
+ >
+ <el-form
+ :model="productForm"
+ label-width="140px"
+ label-position="top"
+ :rules="productRules"
+ ref="productFormRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="浜у搧澶х被锛�" prop="productCategory">
+ <el-tree-select
+ v-model="productForm.productCategory"
+ placeholder="璇烽�夋嫨"
+ clearable
+ filterable
+ check-strictly
+ @change="getModels"
+ :data="productOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="瑙勬牸鍨嬪彿锛�" prop="productModelId">
+ <el-select
+ v-model="productForm.productModelId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="getProductModel"
+ filterable
+ >
+ <el-option
+ v-for="item in modelOptions"
+ :key="item.id"
+ :label="item.model"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍗曚綅锛�" prop="unit">
+ <el-input
+ v-model="productForm.unit"
+ placeholder="璇疯緭鍏�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绋庣巼(%)锛�" prop="taxRate">
+ <el-select
+ v-model="productForm.taxRate"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="calculateFromTaxRate"
+ >
+ <el-option
+ v-for="dict in tax_rate"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鍗曚环(鍏�)锛�" prop="taxInclusiveUnitPrice">
+ <el-input-number
+ :step="0.01"
+ :min="0"
+ v-model="productForm.taxInclusiveUnitPrice"
+ style="width: 100%"
+ :precision="2"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromUnitPrice"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏁伴噺锛�" prop="quantity">
+ <el-input-number
+ :step="0.1"
+ :min="0"
+ v-model="productForm.quantity"
+ placeholder="璇疯緭鍏�"
+ clearable
+ :precision="2"
+ @change="calculateFromQuantity"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍚◣鎬讳环(鍏�)锛�" prop="taxInclusiveTotalPrice">
+ <el-input
+ v-model="productForm.taxInclusiveTotalPrice"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromTotalPrice"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item
+ label="涓嶅惈绋庢�讳环(鍏�)锛�"
+ prop="taxExclusiveTotalPrice"
+ >
+ <el-input
+ v-model="productForm.taxExclusiveTotalPrice"
+ placeholder="璇疯緭鍏�"
+ clearable
+ @change="calculateFromExclusiveTotalPrice"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="12">
+ <el-form-item label="鍙戠エ绫诲瀷锛�" prop="invoiceType">
+ <el-select
+ v-model="productForm.invoiceType"
+ placeholder="璇烽�夋嫨"
+ clearable
+ >
+ <el-option label="澧炴櫘绁�" value="澧炴櫘绁�" />
+ <el-option label="澧炰笓绁�" value="澧炰笓绁�" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄惁鐢熶骇锛�" prop="isProduction">
+ <el-radio-group v-model="productForm.isProduction">
+ <el-radio label="鏄�" :value="true" />
+ <el-radio label="鍚�" :value="false" />
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+ <!-- 瀵煎叆寮圭獥 -->
+ <FormDialog
+ v-model="importUpload.open"
+ :title="importUpload.title"
+ :width="'600px'"
+ @close="importUpload.open = false"
+ @confirm="submitImportFile"
+ @cancel="importUpload.open = false"
+ >
+ <el-upload
+ ref="importUploadRef"
+ :limit="1"
+ accept=".xlsx,.xls"
+ :action="importUpload.url"
+ :headers="importUpload.headers"
+ :before-upload="importUpload.beforeUpload"
+ :on-success="importUpload.onSuccess"
+ :on-error="importUpload.onError"
+ :on-progress="importUpload.onProgress"
+ :on-change="importUpload.onChange"
+ :auto-upload="false"
+ drag
+ >
+ <i class="el-icon-upload"></i>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip">
+ 浠呮敮鎸� xls/xlsx锛屽ぇ灏忎笉瓒呰繃 10MB銆�
+ <el-button link type="primary" @click="downloadTemplate"
+ >涓嬭浇瀵煎叆妯℃澘
+ </el-button>
+ </div>
+ </template>
+ </el-upload>
+ </FormDialog>
+ <!-- // todo 闄勪欢棰勮鐩稿叧 -->
+ <!-- 闄勪欢鍒楄〃寮圭獥 -->
+ <FileList
+ v-if="fileDialogVisible"
+ v-model:visible="fileDialogVisible"
+ record-type="sales_ledger"
+ :record-id="recordId"
+ />
+ <!-- 鎵撳嵃棰勮寮圭獥 -->
+ <el-dialog
+ v-model="printPreviewVisible"
+ title="鎵撳嵃棰勮"
+ width="90%"
+ :close-on-click-modal="false"
+ class="print-preview-dialog"
+ >
+ <div class="print-preview-container">
+ <div class="print-preview-header">
+ <el-button type="primary" @click="executePrint">鎵ц鎵撳嵃 </el-button>
+ <el-button @click="printPreviewVisible = false">鍏抽棴棰勮</el-button>
+ </div>
+ <div class="print-preview-content">
+ <div
+ v-if="printData.length === 0"
+ style="text-align: center; padding: 50px; color: #999"
+ >
+ 鏆傛棤鎵撳嵃鏁版嵁
+ </div>
+ <div
+ v-else
+ style="
+ text-align: center;
+ padding: 10px;
+ color: #666;
+ font-size: 14px;
+ background: #e8f4fd;
+ margin-bottom: 10px;
+ "
+ >
+ 鍏� {{ printData.length }} 鏉℃暟鎹緟鎵撳嵃
+ </div>
+ <div
+ v-for="(item, index) in printData"
+ :key="index"
+ class="print-page"
+ >
+ <div class="delivery-note">
+ <div class="header">
+ <div class="document-title">闆跺敭鍙戣揣鍗�</div>
+ </div>
+ <div class="info-section">
+ <div class="info-row">
+ <div>
+ <span class="label">鍙戣揣鏃ユ湡锛�</span>
+ <span class="value">{{ formatDate(item.createTime) }}</span>
+ </div>
+ <div>
+ <span class="label">鍙戣揣杞︾墝鍙凤細</span>
+ <span class="value">{{ item.shippingCarNumber }}</span>
+ </div>
+ </div>
+ <div class="info-row">
+ <div>
+ <span class="label">瀹㈡埛鍚嶇О锛�</span>
+ <span class="value">{{ item.customerName }}</span>
+ </div>
+ <span class="label">鍗曞彿锛�</span>
+ <span class="value">{{ item.salesContractNo }}</span>
+ </div>
+ </div>
+ <div class="table-section">
+ <table class="product-table">
+ <thead>
+ <tr>
+ <th>浜у搧鍚嶇О</th>
+ <th>瑙勬牸鍨嬪彿</th>
+ <th>鍗曚綅</th>
+ <th>鍗曚环</th>
+ <th>闆跺敭鏁伴噺</th>
+ <th>闆跺敭閲戦</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="product in item.products" :key="product.id">
+ <td>{{ product.productCategory || "" }}</td>
+ <td>{{ product.specificationModel || "" }}</td>
+ <td>{{ product.unit || "" }}</td>
+ <td>{{ product.taxInclusiveUnitPrice || "0" }}</td>
+ <td>{{ product.quantity || "0" }}</td>
+ <td>{{ product.taxInclusiveTotalPrice || "0" }}</td>
+ </tr>
+ <tr v-if="!item.products || item.products.length === 0">
+ <td colspan="6" style="text-align: center; color: #999">
+ 鏆傛棤浜у搧鏁版嵁
+ </td>
+ </tr>
+ </tbody>
+ <tfoot>
+ <tr>
+ <td class="label">鍚堣</td>
+ <td class="total-value"></td>
+ <td class="total-value"></td>
+ <td class="total-value"></td>
+ <td class="total-value">
+ {{ getTotalQuantity(item.products) }}
+ </td>
+ <td class="total-value">
+ {{ getTotalAmount(item.products) }}
+ </td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ <div class="footer-section">
+ <div class="footer-row">
+ <div class="footer-item">
+ <span class="label">鏀惰揣鐢佃瘽锛�</span>
+ <span class="value"></span>
+ </div>
+ <div class="footer-item">
+ <span class="label">鏀惰揣浜猴細</span>
+ <span class="value"></span>
+ </div>
+ <div class="footer-item address-item">
+ <span class="label">鏀惰揣鍦板潃锛�</span>
+ <span class="value address-value"></span>
+ </div>
+ </div>
+ <div class="footer-row">
+ <div class="footer-item">
+ <span class="label">鎿嶄綔鍛橈細</span>
+ <span class="value">{{
+ userStore.nickName || "鎾曞紑鍓�"
+ }}</span>
+ </div>
+ <div class="footer-item">
+ <span class="label">鎵撳嵃鏃ユ湡锛�</span>
+ <span class="value">{{ formatDateTime(new Date()) }}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </el-dialog>
+ <!-- 鍙戣揣寮规 -->
+ <el-dialog
+ v-model="deliveryFormVisible"
+ title="鍙戣揣淇℃伅"
+ width="40%"
+ @close="closeDeliveryDia"
+ >
+ <el-form
+ :model="deliveryForm"
+ label-width="120px"
+ label-position="top"
+ :rules="deliveryRules"
+ ref="deliveryFormRef"
+ >
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="鍙戣揣绫诲瀷锛�" prop="type">
+ <el-select
+ v-model="deliveryForm.type"
+ placeholder="璇烽�夋嫨鍙戣揣绫诲瀷"
+ style="width: 100%"
+ @change="handleDeliveryTypeChange"
+ >
+ <el-option label="璐ц溅" value="璐ц溅" />
+ <el-option label="蹇��" value="蹇��" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="寰呭彂璐ф暟閲忥細">
+ <el-input
+ :model-value="currentDeliveryRow?.noQuantity"
+ disabled
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24" v-if="deliveryForm.type === '璐ц溅'">
+ <el-form-item label="鍙戣揣杞︾墝鍙凤細" prop="shippingCarNumber">
+ <el-input
+ v-model="deliveryForm.shippingCarNumber"
+ placeholder="璇疯緭鍏ュ彂璐ц溅鐗屽彿"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24" v-else>
+ <el-form-item label="蹇�掑叕鍙革細" prop="expressCompany">
+ <el-input
+ v-model="deliveryForm.expressCompany"
+ placeholder="璇疯緭鍏ュ揩閫掑叕鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30" v-if="deliveryForm.type === '蹇��'">
+ <el-col :span="24">
+ <el-form-item label="蹇�掑崟鍙凤細" prop="expressNumber">
+ <el-input
+ v-model="deliveryForm.expressNumber"
+ placeholder="璇疯緭鍏ュ揩閫掑崟鍙�"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="鍙戣揣鍥剧墖锛�">
+ <ImageUpload v-model:file-list="deliveryFileList" :limit="9" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="30">
+ <el-col :span="24">
+ <el-form-item label="搴撳瓨锛�" prop="batchNo">
+ <el-table
+ :data="deliveryForm.batchNoList"
+ border
+ size="small"
+ max-height="260"
+ style="width: 100%"
+ >
+ <el-table-column label="鎵瑰彿" prop="batchNo" min-width="180" />
+ <el-table-column
+ label="浜у搧澶х被"
+ prop="productName"
+ min-width="100"
+ />
+ <el-table-column
+ label="瑙勬牸鍨嬪彿"
+ prop="model"
+ min-width="100"
+ />
+ <el-table-column label="鍗曚綅" prop="unit" min-width="100" />
+ <el-table-column
+ label="搴撳瓨鏁伴噺"
+ min-width="120"
+ align="center"
+ >
+ <template #default="scope">
+ {{ getDeliveryBatchQuantity(scope.row) }}
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鍙戣揣鏁伴噺"
+ min-width="160"
+ align="center"
+ >
+ <template #default="scope">
+ <el-input-number
+ v-model="scope.row.deliveryQuantity"
+ :min="0"
+ :max="getDeliveryBatchDeliveryMax(scope.row)"
+ :precision="2"
+ :step="0.01"
+ controls-position="right"
+ @change="handleDeliveryBatchQuantityChange(scope.row)"
+ style="width: 100%"
+ />
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitDelivery"
+ >纭鍙戣揣
+ </el-button>
+ <el-button @click="closeDeliveryDia">鍙栨秷</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { getToken } from "@/utils/auth";
+import pagination from "@/components/PIMTable/Pagination.vue";
+import { onMounted, ref, getCurrentInstance } from "vue";
+import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js";
+import { ElMessageBox, ElMessage } from "element-plus";
+import useUserStore from "@/store/modules/user";
+import { userListNoPage } from "@/api/system/user.js";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
+import {
+ ledgerListPage,
+ productList,
+ customerList,
+ addOrUpdateSalesLedger,
+ getSalesLedgerWithProducts,
+ delLedger,
+ addOrUpdateSalesLedgerProduct,
+ delProduct,
+ delLedgerFile,
+ getProductInventory,
+} from "@/api/salesManagement/salesLedger.js";
+import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
+import { modelList, productTreeList } from "@/api/basicData/product.js";
+import useFormData from "@/hooks/useFormData.js";
+import dayjs from "dayjs";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
+import { getCurrentDate } from "@/utils/index.js";
+import { listCustomer } from "@/api/basicData/customer.js";
+
+const FileList = defineAsyncComponent(() =>
+ import("@/components/Dialog/FileList.vue")
+);
+
+const router = useRouter();
+const route = useRoute();
+const userStore = useUserStore();
+const { proxy } = getCurrentInstance();
+const { tax_rate } = proxy.useDict("tax_rate");
+const tableData = ref([]);
+const productData = ref([]);
+const selectedRows = ref([]);
+const productSelectedRows = ref([]);
+const userList = ref([]);
+const customerOption = ref([]);
+const productOptions = ref([]);
+const modelOptions = ref([]);
+const tableLoading = ref(false);
+const page = reactive({
+ current: 1,
+ size: 100,
+});
+const total = ref(0);
+const fileList = ref([]);
+const deliveryFileList = ref([]);
+
+// 鐢ㄦ埛淇℃伅琛ㄥ崟寮规鏁版嵁
+const operationType = ref("");
+const dialogFormVisible = ref(false);
+const data = reactive({
+ searchForm: {
+ customerName: "", // 瀹㈡埛鍚嶇О
+ salesContractNo: "", // 閿�鍞悎鍚岀紪鍙�
+ entryDate: null, // 褰曞叆鏃ユ湡
+ entryDateStart: undefined,
+ entryDateEnd: undefined,
+ },
+ form: {
+ salesContractNo: "",
+ autoGenerateContractNo: true,
+ salesman: "",
+ customerId: "",
+ entryPerson: "",
+ entryDate: "",
+ deliveryDate: "",
+ maintenanceTime: "",
+ productData: [],
+ executionDate: "",
+ hasProductionRecord: false,
+ createTime: "",
+ },
+ rules: {
+ salesman: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ customerId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ entryPerson: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ entryDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ deliveryDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ executionDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+});
+const { form, rules } = toRefs(data);
+const { form: searchForm } = useFormData(data.searchForm);
+// 浜у搧琛ㄥ崟寮规鏁版嵁
+const productFormVisible = ref(false);
+const productOperationType = ref("");
+const currentId = ref("");
+const productFormData = reactive({
+ productForm: {
+ productCategory: "",
+ specificationModel: "",
+ unit: "",
+ quantity: "",
+ taxInclusiveUnitPrice: "",
+ taxRate: "",
+ taxInclusiveTotalPrice: "",
+ taxExclusiveTotalPrice: "",
+ invoiceType: "",
+ isProduction: false,
+ },
+ productRules: {
+ productCategory: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ productModelId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ specificationModel: [
+ { required: true, message: "璇烽�夋嫨", trigger: "change" },
+ ],
+ unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+ taxInclusiveUnitPrice: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ taxRate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ taxInclusiveTotalPrice: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ taxExclusiveTotalPrice: [
+ { required: true, message: "璇疯緭鍏�", trigger: "blur" },
+ ],
+ invoiceType: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ isProduction: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+ },
+});
+const { productForm, productRules } = toRefs(productFormData);
+// 闃叉寰幆璁$畻鐨勬爣蹇�
+const isCalculating = ref(false);
+// 鎵撳嵃鐩稿叧
+const printPreviewVisible = ref(false);
+const printData = ref([]);
+
+// 鎶ヤ环鍗曞鍏ョ浉鍏�
+const quotationDialogVisible = ref(false);
+const quotationLoading = ref(false);
+const quotationList = ref([]);
+const quotationSearchForm = reactive({
+ quotationNo: "",
+ customer: "",
+});
+// 鎶ヤ环鍗曞脊妗嗗垎椤�
+const quotationPage = reactive({
+ current: 1,
+ size: 10,
+ total: 0,
+});
+const selectedQuotation = ref(null);
+
+// 鍙戣揣鐩稿叧
+const deliveryFormVisible = ref(false);
+const currentDeliveryRow = ref(null);
+const getDeliveryBatchQuantity = (item) => {
+ const quantity =
+ item?.qualitity ??
+ item?.quantity ??
+ item?.unLockedQuantity ??
+ item?.qualifiedUnLockedQuantity ??
+ item?.qualifiedQuantity ??
+ item?.stockQuantity;
+ return quantity ?? 0;
+};
+const getCurrentDeliveryRowQuantity = () => {
+ return Number(currentDeliveryRow.value?.noQuantity || 0);
+};
+const getDeliveryBatchDeliveryMax = (row) => {
+ const productQuantity = getCurrentDeliveryRowQuantity();
+ const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0);
+ const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce(
+ (sum, item) => {
+ if (item?.id === row?.id) return sum;
+ return sum + Number(item?.deliveryQuantity || 0);
+ },
+ 0
+ );
+ const remainingProductQuantity = Math.max(
+ 0,
+ productQuantity - otherBatchTotal
+ );
+ return Math.max(0, Math.min(batchQuantity, remainingProductQuantity));
+};
+const handleDeliveryBatchQuantityChange = (row) => {
+ const productQuantity = getCurrentDeliveryRowQuantity();
+ const batchQuantity = Number(getDeliveryBatchQuantity(row) || 0);
+ const otherBatchTotal = (deliveryForm.value.batchNoList || []).reduce(
+ (sum, item) => {
+ if (item?.id === row?.id) return sum;
+ return sum + Number(item?.deliveryQuantity || 0);
+ },
+ 0
+ );
+ const remainingProductQuantity = Math.max(
+ 0,
+ productQuantity - otherBatchTotal
+ );
+ const currentValue = Number(row?.deliveryQuantity || 0);
+
+ if (currentValue > batchQuantity) {
+ row.deliveryQuantity = batchQuantity;
+ proxy.$modal.msgWarning("鍙戣揣鏁伴噺涓嶈兘澶т簬搴撳瓨鏁伴噺");
+ } else if (currentValue > remainingProductQuantity) {
+ row.deliveryQuantity = remainingProductQuantity;
+ proxy.$modal.msgWarning("鎵�鏈夋壒娆″彂璐ф暟閲忎箣鍜屼笉鑳藉ぇ浜庡緟鍙戣揣鏁伴噺");
+ }
+};
+const getSelectedDeliveryBatchRows = () => {
+ return (deliveryForm.value.batchNoList || []).filter(
+ (item) => Number(item?.deliveryQuantity || 0) > 0
+ );
+};
+const getDeliveryBatchNoList = async (productModelId) => {
+ if (!productModelId) return [];
+ const res = await getStockInventoryByModelId(productModelId);
+ const rawList = Array.isArray(res?.data)
+ ? res.data
+ : res?.data?.records || res?.data?.rows || [];
+ const seenIds = new Set();
+ return rawList
+ .filter((item) => {
+ if (!item?.id || !item?.batchNo || seenIds.has(item.id)) {
+ return false;
+ }
+ seenIds.add(item.id);
+ return true;
+ })
+ .map((item) => ({
+ ...item,
+ deliveryQuantity: 0,
+ }));
+};
+const validateDeliveryShippingCarNumber = (_rule, value, callback) => {
+ if (deliveryForm.value.type === "璐ц溅" && !value) {
+ return callback(new Error("璇疯緭鍏ュ彂璐ц溅鐗屽彿"));
+ }
+ callback();
+};
+const validateDeliveryExpressCompany = (_rule, value, callback) => {
+ if (deliveryForm.value.type === "蹇��" && !value) {
+ return callback(new Error("璇疯緭鍏ュ揩閫掑叕鍙�"));
+ }
+ callback();
+};
+const deliveryFormData = reactive({
+ deliveryForm: {
+ shippingCarNumber: "",
+ expressCompany: "",
+ expressNumber: "",
+ type: "璐ц溅", // 璐ц溅, 蹇��
+ },
+ deliveryRules: {
+ shippingCarNumber: [
+ { validator: validateDeliveryShippingCarNumber, trigger: "blur" },
+ ],
+ expressCompany: [
+ { validator: validateDeliveryExpressCompany, trigger: "blur" },
+ ],
+ type: [{ required: true, message: "璇烽�夋嫨鍙戣揣绫诲瀷", trigger: "change" }],
+ },
+});
+const { deliveryForm, deliveryRules } = toRefs(deliveryFormData);
+
+// 瀵煎叆鐩稿叧
+const importUploadRef = ref(null);
+const importUpload = reactive({
+ title: "瀵煎叆閿�鍞彴璐�",
+ open: false,
+ url: import.meta.env.VITE_APP_BASE_API + "/sales/ledger/import",
+ headers: { Authorization: "Bearer " + getToken() },
+ isUploading: false,
+ beforeUpload: (file) => {
+ const isExcel = file.name.endsWith(".xlsx") || file.name.endsWith(".xls");
+ const isLt10M = file.size / 1024 / 1024 < 10;
+ if (!isExcel) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢鍙兘鏄� xlsx/xls 鏍煎紡!");
+ return false;
+ }
+ if (!isLt10M) {
+ proxy.$modal.msgError("涓婁紶鏂囦欢澶у皬涓嶈兘瓒呰繃 10MB!");
+ return false;
+ }
+ return true;
+ },
+ onChange: (file, fileList) => {
+ console.log("鏂囦欢鐘舵�佹敼鍙�", file, fileList);
+ },
+ onProgress: (event, file, fileList) => {
+ console.log("涓婁紶涓�...", event.percent);
+ },
+ onSuccess: (response, file, fileList) => {
+ console.log("涓婁紶鎴愬姛", response, file, fileList);
+ importUpload.isUploading = false;
+ if (response.code === 200) {
+ proxy.$modal.msgSuccess("瀵煎叆鎴愬姛");
+ importUpload.open = false;
+ if (importUploadRef.value) {
+ importUploadRef.value.clearFiles();
+ }
+ getList();
+ } else {
+ proxy.$modal.msgError(response.msg || "瀵煎叆澶辫触");
+ }
+ },
+ onError: (error, file, fileList) => {
+ console.error("涓婁紶澶辫触", error, file, fileList);
+ importUpload.isUploading = false;
+ proxy.$modal.msgError("瀵煎叆澶辫触锛岃閲嶈瘯");
+ },
+});
+
+const changeDaterange = (value) => {
+ if (value) {
+ searchForm.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
+ searchForm.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
+ } else {
+ searchForm.entryDateStart = undefined;
+ searchForm.entryDateEnd = undefined;
+ }
+ handleQuery();
+};
+
+// 鏌ヨ鍒楄〃
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+const handleQuery = () => {
+ // 鍙湁鍦ㄧ偣鍑绘悳绱㈡寜閽椂鎵嶉噸缃〉鐮佸埌绗竴椤�
+ // 閬垮厤琛ㄥ崟瀛楁change浜嬩欢骞叉壈鍒嗛〉
+ if (arguments.length === 0) {
+ page.current = 1;
+ }
+ expandedRowKeys.value = [];
+ getList();
+};
+const paginationChange = (obj) => {
+ page.current = obj.page;
+ page.size = obj.limit;
+ getList();
+};
+const getList = () => {
+ tableLoading.value = true;
+ const { entryDate, ...rest } = searchForm;
+ // 灏嗚寖鍥存棩鏈熷瓧娈典紶閫掔粰鍚庣
+ const params = { ...rest, ...page };
+ // 绉婚櫎褰曞叆鏃ユ湡鐨勯粯璁ゅ�艰缃紝鍙繚鐣欒寖鍥存棩鏈熷瓧娈�
+ delete params.entryDate;
+ return ledgerListPage(params)
+ .then((res) => {
+ tableLoading.value = false;
+ tableData.value = res.records;
+ tableData.value.map((item) => {
+ item.children = [];
+ });
+ total.value = res.total;
+ return res;
+ })
+ .catch(() => {
+ tableLoading.value = false;
+ });
+};
+// 鑾峰彇浜у搧澶х被tree鏁版嵁
+const getProductOptions = () => {
+ // 杩斿洖 Promise锛屼究浜庡湪缂栬緫浜у搧鏃剁瓑寰呭姞杞藉畬鎴�
+ return productTreeList().then((res) => {
+ productOptions.value = convertIdToValue(res);
+ return productOptions.value;
+ });
+};
+const formattedNumber = (row, column, cellValue) => {
+ if (cellValue === undefined || cellValue === null || cellValue === "") {
+ return "0.00";
+ }
+ return parseFloat(cellValue).toFixed(2);
+};
+const findLedgerRecordByRow = (row) => {
+ if (!row) return null;
+ if (
+ row.maintainer !== undefined ||
+ row.maintainerName !== undefined ||
+ row.entryPerson !== undefined ||
+ row.entryPersonName !== undefined
+ ) {
+ return row;
+ }
+ if (row.salesLedgerId !== undefined && row.salesLedgerId !== null) {
+ return (
+ tableData.value.find(
+ (item) => String(item.id) === String(row.salesLedgerId)
+ ) || null
+ );
+ }
+ return null;
+};
+const isCurrentUserMaintainer = (row) => {
+ const ledgerRecord = findLedgerRecordByRow(row);
+ if (!ledgerRecord) return true;
+ const currentUserId = String(userStore.id ?? "");
+ const currentNickName = String(userStore.nickName ?? "").trim();
+ const maintainerId = ledgerRecord.maintainerId ?? ledgerRecord.entryPerson;
+ const maintainerName =
+ ledgerRecord.maintainerName ??
+ ledgerRecord.maintainer ??
+ ledgerRecord.entryPersonName;
+ if (
+ maintainerId !== undefined &&
+ maintainerId !== null &&
+ String(maintainerId) !== ""
+ ) {
+ return String(maintainerId) === currentUserId;
+ }
+ if (
+ maintainerName !== undefined &&
+ maintainerName !== null &&
+ String(maintainerName).trim() !== ""
+ ) {
+ return String(maintainerName).trim() === currentNickName;
+ }
+ return true;
+};
+const canEditLedger = (row) => isCurrentUserMaintainer(row);
+const canDeleteLedger = (row) => isCurrentUserMaintainer(row);
+const sensitiveAmountFormatter = (row, column, cellValue) => {
+ if (!isCurrentUserMaintainer(row)) {
+ return "*****";
+ }
+ return formattedNumber(row, column, cellValue);
+};
+// 鑾峰彇tree瀛愭暟鎹�
+const getModels = (value) => {
+ productForm.value.productCategory = findNodeById(productOptions.value, value);
+ modelList({ id: value }).then((res) => {
+ modelOptions.value = res;
+ });
+};
+const getProductModel = (value) => {
+ const index = modelOptions.value.findIndex((item) => item.id === value);
+ if (index !== -1) {
+ productForm.value.specificationModel = modelOptions.value[index].model;
+ productForm.value.unit = modelOptions.value[index].unit;
+ } else {
+ productForm.value.specificationModel = null;
+ productForm.value.unit = null;
+ }
+};
+const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥炶鑺傜偣
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundNode = findNodeById(nodes[i].children, productId);
+ if (foundNode) {
+ return foundNode; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝杩斿洖璇ヨ妭鐐�
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+};
+
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item;
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+
+ return newItem;
+ });
+}
+
+// 鏍规嵁鍚嶇О鍙嶆煡浜у搧澶х被 id锛屼究浜庝粎瀛樺悕绉版椂鐨勫弽鏄�
+function findNodeIdByLabel(nodes, label) {
+ if (!label) return null;
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (node.label === label) return node.value;
+ if (node.children && node.children.length > 0) {
+ const found = findNodeIdByLabel(node.children, label);
+ if (found !== null && found !== undefined) return found;
+ }
+ }
+ return null;
+}
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = (selection) => {
+ // 杩囨护鎺夊瓙鏁版嵁
+ selectedRows.value = selection.filter((item) => item.children !== undefined);
+ console.log("selection", selectedRows.value);
+};
+const productSelected = (selectedRows) => {
+ productSelectedRows.value = selectedRows;
+};
+const expandedRowKeys = ref([]);
+// 灞曞紑琛�
+const expandChange = (row, expandedRows) => {
+ if (expandedRows.length > 0) {
+ expandedRowKeys.value = [];
+ try {
+ productList({ salesLedgerId: row.id, type: 1 }).then((res) => {
+ const index = tableData.value.findIndex((item) => item.id === row.id);
+ if (index > -1) {
+ tableData.value[index].children = res.data;
+ }
+ expandedRowKeys.value.push(row.id);
+ });
+ } catch (error) {
+ console.log(error);
+ }
+ } else {
+ expandedRowKeys.value = [];
+ }
+};
+
+// 娣诲姞琛ㄨ绫诲悕鏂规硶
+const tableRowClassName = ({ row }) => {
+ if (!row.deliveryDate) return "";
+ if (row.isFh) return "";
+
+ const diff = row.deliveryDaysDiff;
+ if (diff === 15) {
+ return "yellow";
+ } else if (diff === 10) {
+ return "pink";
+ } else if (diff === 2) {
+ return "purple";
+ } else if (diff < 2) {
+ return "red";
+ }
+};
+// 涓昏〃鍚堣鏂规硶
+const summarizeMainTable = (param) => {
+ return proxy.summarizeTable(param, [
+ "contractAmount",
+ "taxInclusiveTotalPrice",
+ "taxExclusiveTotalPrice",
+ ]);
+};
+// 瀛愯〃鍚堣鏂规硶
+const summarizeChildrenTable = (param, parentRow) => {
+ if (!isCurrentUserMaintainer(parentRow)) {
+ const { columns } = param;
+ return columns.map((column, index) => {
+ if (index === 0) {
+ return "鍚堣";
+ }
+ if (
+ [
+ "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice",
+ "taxExclusiveTotalPrice",
+ ].includes(column.property)
+ ) {
+ return "*****";
+ }
+ return "";
+ });
+ }
+ return proxy.summarizeTable(param, [
+ "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice",
+ "taxExclusiveTotalPrice",
+ ]);
+};
+// 鎵撳紑寮规
+const openForm = async (type, row) => {
+ if (type === "edit" && row && !canEditLedger(row)) {
+ proxy.$modal.msgWarning("褰撳墠绯荤粺鐧诲綍浜轰笉鏄淮鎶や汉锛屼笉鑳界紪杈戞暟鎹�");
+ return;
+ }
+ operationType.value = type;
+ form.value = {};
+ productData.value = [];
+ selectedQuotation.value = null;
+ let userLists = await userListNoPage();
+ userList.value = userLists.data;
+ listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
+ customerOption.value = res.data.records;
+ });
+ form.value.entryPerson = userStore.id;
+ if (type === "add") {
+ // 鏂板鏃惰缃綍鍏ユ棩鏈熶负褰撳ぉ
+ form.value.entryDate = getCurrentDate();
+ // 绛捐鏃ユ湡榛樿涓哄綋澶�
+ form.value.executionDate = getCurrentDate();
+ // 鍒涘缓鏃堕棿榛樿涓哄綋澶�
+ form.value.createTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
+ // 榛樿鑷姩鐢熸垚閿�鍞悎鍚屽彿
+ form.value.autoGenerateContractNo = true;
+ } else {
+ currentId.value = row.id;
+ getSalesLedgerWithProducts({ id: row.id, type: 1 }).then((res) => {
+ form.value = { ...res };
+ form.value.entryPerson = Number(res.entryPerson);
+ productData.value = form.value.productData;
+ fileList.value = form.value.storageBlobVOs;
+ // 缂栬緫鏃惰缃嚜鍔ㄧ敓鎴愪负false锛屽厑璁告墜鍔ㄤ慨鏀�
+ form.value.autoGenerateContractNo = false;
+ });
+ }
+ // let userAll = await userStore.getInfo()
+ // userList.value.forEach(element => {
+ // if(userAll.user.nickName === element.nickName && userAll.user.userName === element.userName) {
+ // form.value.entryPerson = userAll.user.userId // 璁剧疆榛樿涓氬姟鍛樹负褰撳墠鐢ㄦ埛
+ // }
+ // });
+ form.value.entryDate = getCurrentDate(); // 璁剧疆榛樿褰曞叆鏃ユ湡涓哄綋鍓嶆棩鏈�
+ dialogFormVisible.value = true;
+};
+
+// 鎵撳紑鎶ヤ环鍗曢�夋嫨寮圭獥锛堜粎瀹℃壒閫氳繃锛�
+const openQuotationDialog = async () => {
+ if (operationType.value === "view") return;
+ quotationDialogVisible.value = true;
+ // 鎵撳紑寮圭獥鏃堕噸缃垎椤靛埌绗竴椤�
+ quotationPage.current = 1;
+ // 鍏堢‘淇濆鎴峰垪琛ㄥ凡鍔犺浇锛屼究浜庡悗缁洖濉� customerId
+ if (!customerOption.value || customerOption.value.length === 0) {
+ try {
+ listCustomer({ current: -1, size: -1 }).then((res) => {
+ customerOption.value = res.data.records;
+ });
+ } catch (e) {
+ // ignore锛屽厑璁哥敤鎴峰悗缁墜鍔ㄩ�夋嫨瀹㈡埛
+ }
+ }
+ await fetchQuotationList();
+};
+
+const fetchQuotationList = async () => {
+ quotationLoading.value = true;
+ try {
+ const params = {
+ // 鍚庣鍒嗛〉瀛楁锛歝urrent / size
+ current: quotationPage.current,
+ size: quotationPage.size,
+ ...quotationSearchForm,
+ status: "閫氳繃",
+ };
+ const res = await getQuotationList(params);
+ quotationList.value = res?.data?.records || [];
+ quotationPage.total = res?.data?.total || 0;
+ } finally {
+ quotationLoading.value = false;
+ }
+};
+
+const resetQuotationSearch = async () => {
+ quotationSearchForm.quotationNo = "";
+ quotationSearchForm.customer = "";
+ quotationPage.current = 1;
+ await fetchQuotationList();
+};
+
+// 鎶ヤ环鍗曞脊妗嗗垎椤靛垏鎹�
+const quotationPaginationChange = (obj) => {
+ quotationPage.current = obj.page;
+ quotationPage.size = obj.limit;
+ fetchQuotationList();
+};
+
+// 閫変腑鎶ヤ环鍗曞悗鍥炲~鍒板彴璐﹁〃鍗�
+const applyQuotation = (row) => {
+ if (!row) return;
+ selectedQuotation.value = row;
+
+ // 涓氬姟鍛�
+ form.value.salesman = (row.salesperson || "").trim();
+
+ // 瀹㈡埛鍚嶇О -> customerId
+ const qCustomerName = String(row.customer || "").trim();
+ const customer = (customerOption.value || []).find((c) => {
+ const name = String(c.customerName || "").trim();
+ return (
+ name === qCustomerName ||
+ name.includes(qCustomerName) ||
+ qCustomerName.includes(name)
+ );
+ });
+ if (customer?.id) {
+ form.value.customerId = customer.id;
+ } else {
+ // 濡傛灉鎵句笉鍒帮紝淇濈暀鍘熷�硷紙鍏佽鐢ㄦ埛鎵嬪姩閫夋嫨/涓嶆墦鏂凡鏈夎緭鍏ワ級
+ form.value.customerId = form.value.customerId || "";
+ }
+
+ // 浜у搧淇℃伅鏄犲皠锛氭姤浠� products -> 鍙拌处 productData
+ const products = Array.isArray(row.products) ? row.products : [];
+ productData.value = products.map((p) => {
+ const quantity = Number(p.quantity ?? 0) || 0;
+ const unitPrice = Number(p.unitPrice ?? 0) || 0;
+ const taxRate = "13"; // 榛樿 13%锛屼究浜庣洿鎺ユ彁浜わ紙濡傞渶鍙湪浜у搧涓嚜琛屼慨鏀癸級
+ const taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2);
+ const taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
+ taxInclusiveTotalPrice,
+ taxRate
+ );
+ return {
+ // 鍙拌处瀛楁
+ productCategory: p.product || p.productName || "",
+ specificationModel: p.specification || "",
+ unit: p.unit || "",
+ quantity: quantity,
+ taxRate: taxRate,
+ taxInclusiveUnitPrice: unitPrice.toFixed(2),
+ taxInclusiveTotalPrice: taxInclusiveTotalPrice,
+ taxExclusiveTotalPrice: taxExclusiveTotalPrice,
+ invoiceType: "澧炴櫘绁�",
+ isProduction: true,
+ productId: p.productId,
+ productModelId: p.productModelId,
+ };
+ });
+
+ quotationDialogVisible.value = false;
+};
+
+function changs(val) {
+ console.log(val);
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+ proxy.$refs["formRef"].validate((valid) => {
+ if (valid) {
+ console.log("productData.value--", productData.value);
+ if (productData.value !== null && productData.value.length > 0) {
+ form.value.productData = proxy.HaveJson(productData.value);
+ } else {
+ proxy.$modal.msgWarning("璇锋坊鍔犱骇鍝佷俊鎭�");
+ return;
+ }
+ form.value.storageBlobDTOs = fileList;
+ form.value.type = 1;
+ if (form.value.autoGenerateContractNo) {
+ form.value.salesContractNo = "";
+ }
+ addOrUpdateSalesLedger(form.value).then((res) => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeDia();
+ expandedRowKeys.value = [];
+ getList();
+ });
+ }
+ });
+};
+// 鍏抽棴寮规
+const closeDia = () => {
+ proxy.resetForm("formRef");
+ dialogFormVisible.value = false;
+};
+
+const productIndex = ref(0);
+// 鎵撳紑浜у搧寮规
+const openProductForm = async (type, row, index) => {
+ // 缂栬緫鏃舵鏌ヤ骇鍝佹槸鍚﹀凡鍙戣揣鎴栧鏍搁�氳繃
+ if (type === "edit" && isProductShipped(row)) {
+ proxy.$modal.msgWarning("宸插彂璐ф垨瀹℃牳閫氳繃鐨勪骇鍝佷笉鑳界紪杈�");
+ return;
+ }
+
+ productOperationType.value = type;
+ productForm.value = {};
+ if (type === "add") {
+ productForm.value.isProduction = true;
+ }
+ proxy.resetForm("productFormRef");
+ if (type === "edit") {
+ productForm.value = { ...row };
+ productIndex.value = index;
+ // 缂栬緫鏃舵牴鎹骇鍝佸ぇ绫诲悕绉板弽鏌� tree 鑺傜偣 id锛屽苟鍔犺浇瑙勬牸鍨嬪彿鍒楄〃
+ try {
+ const options =
+ productOptions.value && productOptions.value.length > 0
+ ? productOptions.value
+ : await getProductOptions();
+ const categoryId = findNodeIdByLabel(
+ options,
+ productForm.value.productCategory
+ );
+ if (categoryId) {
+ const models = await modelList({ id: categoryId });
+ modelOptions.value = models || [];
+ // 鏍规嵁褰撳墠瑙勬牸鍨嬪彿鍚嶇О鍙嶆煡骞惰缃� productModelId锛屼究浜庝笅鎷夋鏄剧ず宸查�夊��
+ const currentModel = (modelOptions.value || []).find(
+ (m) => m.model === productForm.value.specificationModel
+ );
+ if (currentModel) {
+ productForm.value.productModelId = currentModel.id;
+ }
+ }
+ } catch (e) {
+ // 鍔犺浇澶辫触鏃朵繚鎸佸彲缂栬緫锛屼笉涓柇寮圭獥
+ console.error("鍔犺浇浜у搧瑙勬牸鍨嬪彿澶辫触", e);
+ }
+ } else {
+ getProductOptions();
+ }
+ productFormVisible.value = true;
+};
+// 鎻愪氦浜у搧琛ㄥ崟
+const submitProduct = () => {
+ proxy.$refs["productFormRef"].validate((valid) => {
+ if (valid) {
+ if (operationType.value === "edit") {
+ submitProductEdit();
+ } else {
+ if (productOperationType.value === "add") {
+ productData.value.push({ ...productForm.value });
+ } else {
+ productData.value[productIndex.value] = { ...productForm.value };
+ }
+ closeProductDia();
+ }
+ }
+ });
+};
+const submitProductEdit = () => {
+ productForm.value.salesLedgerId = currentId.value;
+ productForm.value.type = 1;
+ addOrUpdateSalesLedgerProduct(productForm.value).then((res) => {
+ proxy.$modal.msgSuccess("鎻愪氦鎴愬姛");
+ closeProductDia();
+ getSalesLedgerWithProducts({ id: currentId.value, type: 1 }).then((res) => {
+ productData.value = res.productData;
+ });
+ });
+};
+// 鍒犻櫎浜у搧
+const deleteProduct = () => {
+ if (productSelectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+
+ // 妫�鏌ユ槸鍚︽湁宸插彂璐ф垨瀹℃牳閫氳繃鐨勪骇鍝�
+ const shippedProducts = productSelectedRows.value.filter((row) =>
+ isProductShipped(row)
+ );
+ if (shippedProducts.length > 0) {
+ proxy.$modal.msgWarning("宸插彂璐ф垨瀹℃牳閫氳繃鐨勪骇鍝佷笉鑳藉垹闄�");
+ return;
+ }
+
+ if (operationType.value === "add") {
+ productData.value = productData.value.filter(
+ (item) => !productSelectedRows.value.includes(item)
+ );
+ productSelectedRows.value = [];
+ } else {
+ let ids = [];
+ if (productSelectedRows.value.length > 0) {
+ ids = productSelectedRows.value.map((item) => item.id);
+ }
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delProduct(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ closeProductDia();
+ getSalesLedgerWithProducts({ id: currentId.value, type: 1 }).then(
+ (res) => {
+ productData.value = res.productData;
+ }
+ );
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+ }
+};
+// 鍏抽棴浜у搧寮规
+const closeProductDia = () => {
+ proxy.resetForm("productFormRef");
+ productFormVisible.value = false;
+};
+// 瀵煎叆
+const handleImport = () => {
+ importUpload.title = "瀵煎叆閿�鍞彴璐�";
+ importUpload.open = true;
+ if (importUploadRef.value) {
+ importUploadRef.value.clearFiles();
+ }
+};
+
+// 涓嬭浇瀵煎叆妯℃澘
+const downloadTemplate = () => {
+ proxy.download("/sales/ledger/exportTemplate", {}, "閿�鍞彴璐﹀鍏ユā鏉�.xlsx");
+};
+
+// 鎻愪氦瀵煎叆鏂囦欢
+const submitImportFile = () => {
+ importUpload.isUploading = true;
+ proxy.$refs["importUploadRef"].submit();
+};
+
+// 瀵煎嚭
+const handleOut = () => {
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚鍑猴紝鏄惁纭瀵煎嚭锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ proxy.download("/sales/ledger/export", {}, "閿�鍞彴璐�.xlsx");
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+/** 鍒ゆ柇鍗曚釜浜у搧鏄惁宸插彂璐э紙鏍规嵁shippingStatus鍒ゆ柇锛屽凡鍙戣揣鎴栧鏍搁�氳繃涓嶅彲缂栬緫鍜屽垹闄わ級 */
+const isProductShipped = (product) => {
+ if (!product) return false;
+ const status = String(product.shippingStatus || "").trim();
+ // 濡傛灉鍙戣揣鐘舵�佹槸"宸插彂璐�"鎴�"瀹℃牳閫氳繃"锛屽垯涓嶅彲缂栬緫鍜屽垹闄�
+ return status === "宸插彂璐�" || status === "瀹℃牳閫氳繃";
+};
+
+/** 鍒ゆ柇閿�鍞鍗曚笅鏄惁瀛樺湪宸插彂璐�/鍙戣揣瀹屾垚鐨勪骇鍝侊紙涓嶅彲鍒犻櫎锛� */
+const hasShippedProducts = (products) => {
+ if (!products || !products.length) return false;
+ return products.some((p) => {
+ const status = String(p.shippingStatus || "").trim();
+ // 鏈夊彂璐ф棩鏈熸垨杞︾墝鍙疯涓哄凡鍙戣揣
+ if (p.shippingDate || p.shippingCarNumber) return true;
+ // 宸茶繘琛屽彂璐с�佸彂璐у畬鎴愩�佸凡鍙戣揣 鍧囦笉鍙垹闄�
+ return (
+ status === "宸茶繘琛屽彂璐�" || status === "鍙戣揣瀹屾垚" || status === "宸插彂璐�"
+ );
+ });
+};
+
+// 鍒犻櫎
+const handleDelete = async () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨鏁版嵁");
+ return;
+ }
+ const unauthorizedRows = selectedRows.value.filter(
+ (row) => !canDeleteLedger(row)
+ );
+ if (unauthorizedRows.length > 0) {
+ proxy.$modal.msgWarning("褰撳墠鐧诲綍鐢ㄦ埛涓嶆槸褰曞叆浜猴紝涓嶈兘鍒犻櫎璇ユ暟鎹�");
+ return;
+ }
+ const ids = selectedRows.value.map((item) => item.id);
+
+ // 妫�鏌ユ槸鍚︽湁宸茶繘琛屽彂璐ф垨鍙戣揣瀹屾垚鐨勯攢鍞鍗曪紝鑻ユ湁鍒欎笉鍏佽鍒犻櫎
+ const cannotDeleteNames = [];
+ for (const row of selectedRows.value) {
+ let products =
+ row.children && row.children.length > 0 ? row.children : null;
+ if (!products) {
+ try {
+ const res = await productList({ salesLedgerId: row.id, type: 1 });
+ products = res.data || [];
+ } catch {
+ products = [];
+ }
+ }
+ if (hasShippedProducts(products)) {
+ cannotDeleteNames.push(row.salesContractNo || `ID:${row.id}`);
+ }
+ }
+ if (cannotDeleteNames.length > 0) {
+ proxy.$modal.msgWarning(
+ "宸茶繘琛屽彂璐ф垨鍙戣揣瀹屾垚鐨勯攢鍞鍗曚笉鑳藉垹闄わ細" + cannotDeleteNames.join("銆�")
+ );
+ return;
+ }
+
+ ElMessageBox.confirm("閫変腑鐨勫唴瀹瑰皢琚垹闄わ紝鏄惁纭鍒犻櫎锛�", "瀵煎嚭", {
+ confirmButtonText: "纭",
+ cancelButtonText: "鍙栨秷",
+ type: "warning",
+ })
+ .then(() => {
+ delLedger(ids).then((res) => {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ getList();
+ });
+ })
+ .catch(() => {
+ proxy.$modal.msg("宸插彇娑�");
+ });
+};
+
+// 鎵撳嵃鍔熻兘
+ const handlePrint = async () => {
+ if (selectedRows.value.length === 0) {
+ proxy.$modal.msgWarning("璇烽�夋嫨瑕佹墦鍗扮殑鏁版嵁");
+ return;
+ }
+
+ // 鏄剧ず鍔犺浇鐘舵��
+ proxy.$modal.loading("姝e湪鑾峰彇浜у搧鏁版嵁锛岃绋嶅��...");
+
+ try {
+ // 涓烘瘡涓�変腑鐨勯攢鍞彴璐﹁褰曟煡璇㈠搴旂殑浜у搧鏁版嵁
+ const printDataWithProducts = [];
+
+ for (const row of selectedRows.value) {
+ try {
+ // 璋冪敤productList鎺ュ彛鏌ヨ浜у搧鏁版嵁
+ const productRes = await productList({
+ salesLedgerId: row.id,
+ type: 1,
+ });
+
+ // 灏嗕骇鍝佹暟鎹暣鍚堝埌閿�鍞彴璐﹁褰曚腑
+ const rowWithProducts = {
+ ...row,
+ products: productRes.data || [],
+ };
+
+ printDataWithProducts.push(rowWithProducts);
+ } catch (error) {
+ console.error(`鑾峰彇閿�鍞彴璐� ${row.id} 鐨勪骇鍝佹暟鎹け璐�:`, error);
+ // 鍗充娇鏌愪釜璁板綍鐨勪骇鍝佹暟鎹幏鍙栧け璐ワ紝涔熻鍖呭惈璇ヨ褰�
+ printDataWithProducts.push({
+ ...row,
+ products: [],
+ });
+ }
+ }
+
+ printData.value = printDataWithProducts;
+ console.log("鎵撳嵃鏁版嵁锛堝寘鍚骇鍝侊級:", printData.value);
+ printPreviewVisible.value = true;
+ } catch (error) {
+ console.error("鑾峰彇浜у搧鏁版嵁澶辫触:", error);
+ proxy.$modal.msgError("鑾峰彇浜у搧鏁版嵁澶辫触锛岃閲嶈瘯");
+ } finally {
+ proxy.$modal.closeLoading();
+ }
+};
+// 鎵ц鎵撳嵃
+const executePrint = () => {
+ console.log("寮�濮嬫墽琛屾墦鍗帮紝鏁版嵁鏉℃暟:", printData.value.length);
+ console.log("鎵撳嵃鏁版嵁:", printData.value);
+
+ // 鍒涘缓涓�涓柊鐨勬墦鍗扮獥鍙�
+ const printWindow = window.open("", "_blank", "width=800,height=600");
+
+ // 鏋勫缓鎵撳嵃鍐呭
+ let printContent = `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="UTF-8">
+ <title>鎵撳嵃棰勮</title>
+ <style>
+ body {
+ margin: 0;
+ padding: 0;
+ font-family: "SimSun", serif;
+ background: white;
+ }
+ .print-page {
+ width: 200mm;
+ height: 75mm;
+ padding: 10mm;
+ padding-left: 20mm;
+ background: white;
+ box-sizing: border-box;
+ page-break-after: always;
+ page-break-inside: avoid;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+ .delivery-note {
+ width: 100%;
+ height: 100%;
+ font-size: 12px;
+ line-height: 1.2;
+ display: flex;
+ flex-direction: column;
+ color: #000;
+ }
+ .header {
+ text-align: center;
+ margin-bottom: 8px;
+ }
+ .company-name {
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+ .document-title {
+ font-size: 16px;
+ font-weight: bold;
+ }
+ .info-section {
+ margin-bottom: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ }
+ .info-row {
+ line-height: 20px;
+ }
+ .label {
+ font-weight: bold;
+ width: 60px;
+ font-size: 12px;
+ }
+ .value {
+ margin-right: 20px;
+ min-width: 80px;
+ font-size: 12px;
+ }
+ .table-section {
+ margin-bottom: 40px;
+ // flex: 0.6;
+ }
+ .product-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid #000;
+ }
+ .product-table th, .product-table td {
+ border: 1px solid #000;
+ padding: 6px;
+ text-align: center;
+ font-size: 12px;
+ line-height: 1.4;
+ }
+ .product-table th {
+ font-weight: bold;
+ }
+ .total-value {
+ font-weight: bold;
+ }
+ .footer-section {
+ margin-top: auto;
+ }
+ .footer-row {
+ display: flex;
+ margin-bottom: 3px;
+ line-height: 22px;
+ justify-content: space-between;
+ }
+ .footer-item {
+ display: flex;
+ margin-right: 20px;
+ }
+ .footer-item .label {
+ font-weight: bold;
+ width: 80px;
+ font-size: 12px;
+ }
+ .footer-item .value {
+ min-width: 80px;
+ font-size: 12px;
+ }
+ .address-item .address-value {
+ min-width: 200px;
+ }
+ @media print {
+ body {
+ margin: 0;
+ padding: 0;
+ }
+ .print-page {
+ margin: 0;
+ padding: 10mm;
+ /* padding-left: 20mm; */
+ page-break-inside: avoid;
+ page-break-after: always;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ `;
+
+ // 涓烘瘡鏉℃暟鎹敓鎴愭墦鍗伴〉闈�
+ printData.value.forEach((item, index) => {
+ printContent += `
+ <div class="print-page">
+ <div class="delivery-note">
+ <div class="header">
+ <div class="document-title">闆跺敭鍙戣揣鍗�</div>
+ </div>
+
+ <div class="info-section">
+ <div class="info-row">
+ <div>
+ <span class="label">鍙戣揣鏃ユ湡锛�</span>
+ <span class="value">${formatDate(
+ item.createTime
+ )}</span>
+ </div>
+ <div>
+ <span class="label">瀹㈡埛鍚嶇О锛�</span>
+ <span class="value">${
+ item.customerName
+ }</span>
+ </div>
+ </div>
+ <div class="info-row">
+ <span class="label">鍗曞彿锛�</span>
+ <span class="value">${
+ item.salesContractNo ||
+ ""
+ }</span>
+ </div>
+ </div>
+
+ <div class="table-section">
+ <table class="product-table">
+ <thead>
+ <tr>
+ <th>浜у搧鍚嶇О</th>
+ <th>瑙勬牸鍨嬪彿</th>
+ <th>鍗曚綅</th>
+ <th>鍗曚环</th>
+ <th>闆跺敭鏁伴噺</th>
+ <th>闆跺敭閲戦</th>
+ </tr>
+ </thead>
+ <tbody>
+ ${
+ item.products &&
+ item
+ .products
+ .length >
+ 0
+ ? item.products
+ .map(
+ (
+ product
+ ) => `
+ <tr>
+ <td>${
+ product.productCategory ||
+ ""
+ }</td>
+ <td>${
+ product.specificationModel ||
+ ""
+ }</td>
+ <td>${
+ product.unit ||
+ ""
+ }</td>
+ <td>${
+ product.taxInclusiveUnitPrice ||
+ "0"
+ }</td>
+ <td>${
+ product.quantity ||
+ "0"
+ }</td>
+ <td>${
+ product.taxInclusiveTotalPrice ||
+ "0"
+ }</td>
+ </tr>
+ `
+ )
+ .join(
+ ""
+ )
+ : '<tr><td colspan="6" style="text-align: center; color: #999;">鏆傛棤浜у搧鏁版嵁</td></tr>'
+ }
+ </tbody>
+ <tfoot>
+ <tr>
+ <td class="label">鍚堣</td>
+ <td class="total-value"></td>
+ <td class="total-value"></td>
+ <td class="total-value"></td>
+ <td class="total-value">${getTotalQuantityForPrint(
+ item.products
+ )}</td>
+ <td class="total-value">${getTotalAmountForPrint(
+ item.products
+ )}</td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+
+ <div class="footer-section">
+ <div class="footer-row">
+ <div class="footer-item">
+ <span class="label">鏀惰揣鐢佃瘽锛�</span>
+ <span class="value"></span>
+ </div>
+ <div class="footer-item">
+ <span class="label">鏀惰揣浜猴細</span>
+ <span class="value"></span>
+ </div>
+ <div class="footer-item address-item">
+ <span class="label">鏀惰揣鍦板潃锛�</span>
+ <span class="value address-value"></span>
+ </div>
+ </div>
+ <div class="footer-row">
+ <div class="footer-item">
+ <span class="label">鎿嶄綔鍛橈細</span>
+ <span class="value">${
+ userStore.nickName ||
+ "鎾曞紑鍓�"
+ }</span>
+ </div>
+ <div class="footer-item">
+ <span class="label">鎵撳嵃鏃ユ湡锛�</span>
+ <span class="value">${formatDateTime(
+ new Date()
+ )}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ `;
+ });
+
+ printContent += `
+ </body>
+ </html>
+ `;
+
+ // 鍐欏叆鍐呭鍒版柊绐楀彛
+ printWindow.document.write(printContent);
+ printWindow.document.close();
+
+ // 绛夊緟鍐呭鍔犺浇瀹屾垚鍚庢墦鍗�
+ printWindow.onload = () => {
+ setTimeout(() => {
+ printWindow.print();
+ printWindow.close();
+ printPreviewVisible.value = false;
+ }, 500);
+ };
+};
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (dateString) => {
+ if (!dateString) return getCurrentDate();
+ return dayjs(dateString).format("YYYY/MM/DD HH:mm:ss");
+};
+// 鏍煎紡鍖栨棩鏈熸椂闂�
+const formatDateTime = (date) => {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, "0");
+ const day = String(date.getDate()).padStart(2, "0");
+ const hours = String(date.getHours()).padStart(2, "0");
+ const minutes = String(date.getMinutes()).padStart(2, "0");
+ const seconds = String(date.getSeconds()).padStart(2, "0");
+ return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`;
+};
+// 璁$畻浜у搧鎬绘暟閲�
+const getTotalQuantity = (products) => {
+ if (!products || products.length === 0) return "0";
+ const total = products.reduce((sum, product) => {
+ return sum + (parseFloat(product.quantity) || 0);
+ }, 0);
+ return total.toFixed(2);
+};
+
+// 璁$畻浜у搧鎬婚噾棰�
+const getTotalAmount = (products) => {
+ if (!products || products.length === 0) return "0";
+ const total = products.reduce((sum, product) => {
+ return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0);
+ }, 0);
+ return total.toFixed(2);
+};
+
+// 鐢ㄤ簬鎵撳嵃鐨勮绠楀嚱鏁�
+const getTotalQuantityForPrint = (products) => {
+ if (!products || products.length === 0) return "0";
+ const total = products.reduce((sum, product) => {
+ return sum + (parseFloat(product.quantity) || 0);
+ }, 0);
+ return total.toFixed(2);
+};
+
+const getTotalAmountForPrint = (products) => {
+ if (!products || products.length === 0) return "0";
+ const total = products.reduce((sum, product) => {
+ return sum + (parseFloat(product.taxInclusiveTotalPrice) || 0);
+ }, 0);
+ return total.toFixed(2);
+};
+
+const mathNum = () => {
+ console.log("productForm.value", productForm.value);
+ if (!productForm.value.taxInclusiveUnitPrice) {
+ return;
+ }
+ if (!productForm.value.quantity) {
+ return;
+ }
+ // 鍚◣鎬讳环璁$畻
+ productForm.value.taxInclusiveTotalPrice =
+ proxy.calculateTaxIncludeTotalPrice(
+ productForm.value.taxInclusiveUnitPrice,
+ productForm.value.quantity
+ );
+ if (productForm.value.taxRate) {
+ // 涓嶅惈绋庢�讳环璁$畻
+ productForm.value.taxExclusiveTotalPrice =
+ proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ );
+ }
+};
+
+// 鏍规嵁鍚◣鎬讳环璁$畻鍚◣鍗曚环鍜屾暟閲�
+const calculateFromTotalPrice = () => {
+ if (isCalculating.value) return;
+
+ const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice);
+ const quantity = parseFloat(productForm.value.quantity);
+
+ if (!totalPrice || !quantity || quantity <= 0) {
+ return;
+ }
+
+ isCalculating.value = true;
+
+ // 璁$畻鍚◣鍗曚环 = 鍚◣鎬讳环 / 鏁伴噺
+ productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2);
+
+ // 濡傛灉鏈夌◣鐜囷紝璁$畻涓嶅惈绋庢�讳环
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice =
+ proxy.calculateTaxExclusiveTotalPrice(
+ totalPrice,
+ productForm.value.taxRate
+ );
+ }
+
+ isCalculating.value = false;
+};
+
+// 鏍规嵁涓嶅惈绋庢�讳环璁$畻鍚◣鍗曚环鍜屾暟閲�
+const calculateFromExclusiveTotalPrice = () => {
+ if (!productForm.value.taxRate) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨绋庣巼");
+ return;
+ }
+ if (isCalculating.value) return;
+
+ const exclusiveTotalPrice = parseFloat(
+ productForm.value.taxExclusiveTotalPrice
+ );
+ const quantity = parseFloat(productForm.value.quantity);
+ const taxRate = parseFloat(productForm.value.taxRate);
+
+ if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) {
+ return;
+ }
+
+ isCalculating.value = true;
+
+ // 鍏堣绠楀惈绋庢�讳环 = 涓嶅惈绋庢�讳环 / (1 - 绋庣巼/100)
+ const taxRateDecimal = taxRate / 100;
+ const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal);
+ productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2);
+
+ // 璁$畻鍚◣鍗曚环 = 鍚◣鎬讳环 / 鏁伴噺
+ productForm.value.taxInclusiveUnitPrice = (
+ inclusiveTotalPrice / quantity
+ ).toFixed(2);
+
+ isCalculating.value = false;
+};
+
+// 鏍规嵁鏁伴噺鍙樺寲璁$畻鎬讳环
+const calculateFromQuantity = () => {
+ if (!productForm.value.taxRate) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨绋庣巼");
+ return;
+ }
+ if (isCalculating.value) return;
+
+ const quantity = parseFloat(productForm.value.quantity);
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice);
+
+ if (!quantity || quantity <= 0 || !unitPrice) {
+ return;
+ }
+
+ isCalculating.value = true;
+
+ // 璁$畻鍚◣鎬讳环
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2);
+
+ // 濡傛灉鏈夌◣鐜囷紝璁$畻涓嶅惈绋庢�讳环
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice =
+ proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ );
+ }
+
+ isCalculating.value = false;
+};
+
+// 鏍规嵁鍚◣鍗曚环鍙樺寲璁$畻鎬讳环
+const calculateFromUnitPrice = () => {
+ if (!productForm.value.taxRate) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨绋庣巼");
+ return;
+ }
+ if (isCalculating.value) return;
+
+ const quantity = parseFloat(productForm.value.quantity);
+ const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice);
+
+ if (!quantity || quantity <= 0 || !unitPrice) {
+ return;
+ }
+
+ isCalculating.value = true;
+
+ // 璁$畻鍚◣鎬讳环
+ productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2);
+
+ // 濡傛灉鏈夌◣鐜囷紝璁$畻涓嶅惈绋庢�讳环
+ if (productForm.value.taxRate) {
+ productForm.value.taxExclusiveTotalPrice =
+ proxy.calculateTaxExclusiveTotalPrice(
+ productForm.value.taxInclusiveTotalPrice,
+ productForm.value.taxRate
+ );
+ }
+
+ isCalculating.value = false;
+};
+
+// 鏍规嵁绋庣巼鍙樺寲璁$畻涓嶅惈绋庢�讳环
+const calculateFromTaxRate = () => {
+ if (!productForm.value.taxRate) {
+ proxy.$modal.msgWarning("璇峰厛閫夋嫨绋庣巼");
+ return;
+ }
+ if (isCalculating.value) return;
+
+ const inclusiveTotalPrice = parseFloat(
+ productForm.value.taxInclusiveTotalPrice
+ );
+ const taxRate = parseFloat(productForm.value.taxRate);
+
+ if (!inclusiveTotalPrice || !taxRate) {
+ return;
+ }
+
+ isCalculating.value = true;
+
+ // 璁$畻涓嶅惈绋庢�讳环
+ productForm.value.taxExclusiveTotalPrice =
+ proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate);
+
+ isCalculating.value = false;
+};
+/**
+ * 鑾峰彇鍙戣揣鐘舵�佹枃鏈�
+ * @param row 琛屾暟鎹�
+ */
+const getShippingStatusText = (row) => {
+ // 濡傛灉宸插彂璐э紙鏈夊彂璐ф棩鏈熸垨杞︾墝鍙凤級锛屾樉绀�"宸插彂璐�"
+ // if (row.shippingDate || row.shippingCarNumber) {
+ // return "宸插彂璐�";
+ // }
+
+ // 鑾峰彇鍙戣揣鐘舵�佸瓧娈�
+ const status = row.shippingStatus;
+
+ // 濡傛灉鐘舵�佷负绌烘垨鏈畾涔夛紝榛樿涓�"寰呭彂璐�"
+ if (status === null || status === undefined || status === "") {
+ return "寰呭彂璐�";
+ }
+
+ // 鐘舵�佹槸瀛楃涓�
+ const statusStr = String(status).trim();
+ const statusTextMap = {
+ 寰呭彂璐�: "寰呭彂璐�",
+ 寰呭鏍�: "寰呭鏍�",
+ 瀹℃牳涓�: "瀹℃牳涓�",
+ 瀹℃牳鎷掔粷: "瀹℃牳鎷掔粷",
+ 瀹℃牳閫氳繃: "瀹℃牳閫氳繃",
+ 宸插彂璐�: "宸插彂璐�",
+ 閮ㄥ垎鍙戣揣: "閮ㄥ垎鍙戣揣",
+ };
+ return statusTextMap[statusStr] || "寰呭彂璐�";
+};
+
+/**
+ * 鑾峰彇鍙戣揣鐘舵�佹爣绛剧被鍨嬶紙棰滆壊锛�
+ * @param row 琛屾暟鎹�
+ */
+const getShippingStatusType = (row) => {
+ // 濡傛灉宸插彂璐э紙鏈夊彂璐ф棩鏈熸垨杞︾墝鍙凤級锛屾樉绀虹豢鑹�
+ if (row.shippingStatus === "宸插彂璐�") {
+ return "success";
+ }
+
+ // 鑾峰彇鍙戣揣鐘舵�佸瓧娈�
+ const status = row.shippingStatus;
+
+ // 濡傛灉鐘舵�佷负绌烘垨鏈畾涔夛紝榛樿涓虹伆鑹诧紙寰呭彂璐э級
+ if (status === null || status === undefined || status === "") {
+ return "info";
+ }
+
+ // 鐘舵�佹槸瀛楃涓�
+ const statusStr = String(status).trim();
+ const typeTextMap = {
+ 寰呭彂璐�: "info",
+ 寰呭鏍�: "info",
+ 瀹℃牳涓�: "warning",
+ 瀹℃牳鎷掔粷: "danger",
+ 瀹℃牳閫氳繃: "success",
+ 宸插彂璐�: "success",
+ 閮ㄥ垎鍙戣揣: "warning",
+ };
+ return typeTextMap[statusStr] || "info";
+};
+
+/**
+ * 鍒ゆ柇鏄惁鍙互鍙戣揣
+ * 鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐у拰瀹℃牳鎷掔粷鐨勬椂鍊欐墠鍙互鍙戣揣
+ * @param row 琛屾暟鎹�
+ */
+const canShip = (row) => {
+ // 浜у搧鐘舵�佸繀椤绘槸鍏呰冻锛坅pproveStatus === 1锛�
+ if (row.approveStatus !== 1) {
+ return false;
+ }
+
+ // 鑾峰彇鍙戣揣鐘舵��
+ const shippingStatus = row.shippingStatus;
+
+ // 濡傛灉宸插彂璐э紙鏈夊彂璐ф棩鏈熸垨杞︾墝鍙凤級锛屼笉鑳藉啀娆″彂璐�
+ if (shippingStatus === "宸插彂璐�") {
+ return false;
+ }
+
+ // 鍙戣揣鐘舵�佸繀椤绘槸"寰呭彂璐�"鎴�"瀹℃牳鎷掔粷"
+ const statusStr = shippingStatus ? String(shippingStatus).trim() : "";
+ return (
+ statusStr === "寰呭彂璐�" ||
+ statusStr === "瀹℃牳鎷掔粷" ||
+ statusStr === "閮ㄥ垎鍙戣揣"
+ );
+};
+
+// 鎵撳紑闄勪欢寮圭獥
+const recordId = ref(0);
+const fileDialogVisible = ref(false);
+
+// 鎵撳紑闄勪欢寮规
+const openFileDialog = async (row) => {
+ recordId.value = row.id;
+ fileDialogVisible.value = true;
+};
+
+// 鎵撳紑鍙戣揣寮规
+const openDeliveryForm = async (row) => {
+ // 妫�鏌ユ槸鍚﹀彲浠ュ彂璐�
+ if (!canShip(row)) {
+ proxy.$modal.msgWarning(
+ "鍙湁鍦ㄤ骇鍝佺姸鎬佹槸鍏呰冻锛屽彂璐х姸鎬佹槸寰呭彂璐ф垨瀹℃牳鎷掔粷鐨勬椂鍊欐墠鍙互鍙戣揣"
+ );
+ return;
+ }
+
+ currentDeliveryRow.value = row;
+ const batchNoList = await getDeliveryBatchNoList(
+ row.productModelId || row.modelId
+ );
+ deliveryForm.value = {
+ shippingCarNumber: "",
+ expressCompany: "",
+ expressNumber: "",
+ type: "璐ц溅",
+ batchNo: [],
+ batchNoList,
+ };
+ deliveryFileList.value = [];
+ deliveryFormVisible.value = true;
+};
+
+// 鎻愪氦鍙戣揣琛ㄥ崟
+const submitDelivery = () => {
+ proxy.$refs["deliveryFormRef"].validate((valid) => {
+ if (valid) {
+ const selectedBatchRows = getSelectedDeliveryBatchRows();
+ if (selectedBatchRows.length === 0) {
+ proxy.$modal.msgWarning("璇疯嚦灏戝~鍐欎竴涓壒鍙风殑鍙戣揣鏁伴噺");
+ return;
+ }
+ const totalDeliveryQuantity = selectedBatchRows.reduce(
+ (sum, item) => sum + Number(item.deliveryQuantity || 0),
+ 0
+ );
+ const currentRowNoQuantity = Number(
+ currentDeliveryRow.value?.noQuantity || 0
+ );
+ if (
+ currentRowNoQuantity > 0 &&
+ totalDeliveryQuantity > currentRowNoQuantity
+ ) {
+ proxy.$modal.msgWarning("鎵瑰彿鍙戣揣鎬绘暟涓嶈兘瓒呰繃寰呭彂璐ф暟閲�");
+ return;
+ }
+ // 淇濆瓨褰撳墠灞曞紑鐨勮ID锛屼互渚垮彂璐у悗閲嶆柊鍔犺浇瀛愯〃鏍兼暟鎹�
+ const currentExpandedKeys = [...expandedRowKeys.value];
+ const salesLedgerId = currentDeliveryRow.value.salesLedgerId;
+ deliveryForm.value.batchNo = selectedBatchRows.map((item) => item.id);
+ const productModelId =
+ currentDeliveryRow.value.productModelId ||
+ currentDeliveryRow.value.modelId;
+ addShippingInfo({
+ salesLedgerId: salesLedgerId,
+ salesLedgerProductId: currentDeliveryRow.value.id,
+ type: deliveryForm.value.type,
+ shippingCarNumber:
+ deliveryForm.value.type === "璐ц溅"
+ ? deliveryForm.value.shippingCarNumber
+ : "",
+ expressCompany:
+ deliveryForm.value.type === "蹇��"
+ ? deliveryForm.value.expressCompany
+ : "",
+ expressNumber:
+ deliveryForm.value.type === "蹇��"
+ ? deliveryForm.value.expressNumber
+ : "",
+ storageBlobDTOs: deliveryFileList.value || [],
+ batchNo: deliveryForm.value.batchNo,
+ batchNoDetailList: selectedBatchRows.map((item) => ({
+ stockInventoryId: item.id,
+ batchNo: item.batchNo,
+ quantity: Number(item.deliveryQuantity || 0),
+ productModelId: productModelId,
+ })),
+ }).then(() => {
+ proxy.$modal.msgSuccess("鍙戣揣鎴愬姛");
+ closeDeliveryDia();
+ // 鍒锋柊涓昏〃鏁版嵁
+ getList().then(() => {
+ // 濡傛灉涔嬪墠鏈夊睍寮�鐨勮锛岄噸鏂板姞杞借繖浜涜鐨勫瓙琛ㄦ牸鏁版嵁
+ if (currentExpandedKeys.length > 0) {
+ // 浣跨敤 Promise.all 骞惰鍔犺浇鎵�鏈夊睍寮�琛岀殑瀛愯〃鏍兼暟鎹�
+ const loadPromises = currentExpandedKeys.map((ledgerId) => {
+ return productList({ salesLedgerId: ledgerId, type: 1 }).then(
+ (res) => {
+ const index = tableData.value.findIndex(
+ (item) => item.id === ledgerId
+ );
+ if (index > -1) {
+ tableData.value[index].children = res.data;
+ }
+ }
+ );
+ });
+ Promise.all(loadPromises).then(() => {
+ // 鎭㈠灞曞紑鐘舵��
+ expandedRowKeys.value = currentExpandedKeys;
+ });
+ }
+ });
+ });
+ }
+ });
+};
+
+// 鍏抽棴鍙戣揣寮规
+const handleDeliveryTypeChange = (val) => {
+ if (val === "璐ц溅") {
+ deliveryForm.value.expressCompany = "";
+ deliveryForm.value.expressNumber = "";
+ } else {
+ deliveryForm.value.shippingCarNumber = "";
+ }
+};
+
+const closeDeliveryDia = () => {
+ proxy.resetForm("deliveryFormRef");
+ deliveryFileList.value = [];
+ deliveryFormVisible.value = false;
+ currentDeliveryRow.value = null;
+};
+const currentFactoryName = ref("");
+const getCurrentFactoryName = async () => {
+ let res = await userStore.getInfo();
+ currentFactoryName.value = res.user.currentFactoryName;
+};
+onMounted(() => {
+ searchForm.salesContractNo = route.query.salesContractNo;
+ getList();
+ userListNoPage().then((res) => {
+ userList.value = res.data;
+ });
+ getCurrentFactoryName();
+});
+</script>
+
+<style scoped lang="scss">
+.ml-10 {
+ margin-left: 10px;
+}
+
+:deep(.yellow) {
+ background-color: #faf0de;
+}
+
+:deep(.pink) {
+ background-color: #fae1de;
+}
+
+:deep(.red) {
+ background-color: #fae1de;
+}
+
+:deep(.purple) {
+ background-color: #f4defa;
+}
+
+.table_list {
+ margin-top: unset;
+}
+
+.actions {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10px;
+}
+
+.print-preview-dialog {
+ .el-dialog__body {
+ padding: 0;
+ max-height: 80vh;
+ overflow-y: auto;
+ }
+}
+
+.print-preview-container {
+ .print-preview-header {
+ padding: 15px;
+ border-bottom: 1px solid #e4e7ed;
+ text-align: center;
+
+ .el-button {
+ margin: 0 10px;
+ }
+ }
+
+ .print-preview-content {
+ padding: 20px;
+ background-color: #f5f5f5;
+ min-height: 400px;
+ }
+}
+
+.print-page {
+ width: 220mm;
+ height: 90mm;
+ padding: 10mm;
+ margin: 0 auto;
+ background: white;
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
+ margin-bottom: 10px;
+ box-sizing: border-box;
+}
+
+.delivery-note {
+ width: 100%;
+ height: 100%;
+ font-family: "SimSun", serif;
+ font-size: 10px;
+ line-height: 1.2;
+ display: flex;
+ flex-direction: column;
+}
+
+.header {
+ text-align: center;
+ margin-bottom: 8px;
+
+ .company-name {
+ font-size: 18px;
+ font-weight: bold;
+ margin-bottom: 4px;
+ }
+
+ .document-title {
+ font-size: 16px;
+ font-weight: bold;
+ }
+}
+
+.info-section {
+ margin-bottom: 8px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .info-row {
+ line-height: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 60px;
+ font-size: 14px;
+ }
+
+ .value {
+ margin-right: 20px;
+ min-width: 80px;
+ font-size: 14px;
+ }
+ }
+}
+
+.table-section {
+ margin-bottom: 4px;
+ flex: 1;
+
+ .product-table {
+ width: 100%;
+ border-collapse: collapse;
+ border: 1px solid #000;
+
+ th,
+ td {
+ border: 1px solid #000;
+ padding: 6px;
+ text-align: center;
+ font-size: 14px;
+ line-height: 1.4;
+ }
+
+ th {
+ font-weight: bold;
+ }
+
+ .total-label {
+ text-align: right;
+ font-weight: bold;
+ }
+
+ .total-value {
+ font-weight: bold;
+ }
+ }
+}
+
+.footer-section {
+ .footer-row {
+ display: flex;
+ margin-bottom: 3px;
+ line-height: 20px;
+ justify-content: space-between;
+
+ .footer-item {
+ display: flex;
+ margin-right: 20px;
+
+ .label {
+ font-weight: bold;
+ width: 80px;
+ font-size: 14px;
+ }
+
+ .value {
+ min-width: 80px;
+ font-size: 14px;
+ }
+
+ &.address-item {
+ .address-value {
+ min-width: 200px;
+ }
+ }
+ }
+ }
+}
+
+@media print {
+ .app-container {
+ display: none;
+ }
+
+ .print-page {
+ box-shadow: none;
+ margin: 0;
+ padding: 10mm;
+ padding-left: 20mm;
+ page-break-inside: avoid;
+ page-break-after: always;
+ }
+ .print-page:last-child {
+ page-break-after: avoid;
+ }
+}
+</style>
\ No newline at end of file
diff --git a/src/views/salesManagement/salesQuotation/index.vue b/src/views/salesManagement/salesQuotation/index.vue
new file mode 100644
index 0000000..fce764f
--- /dev/null
+++ b/src/views/salesManagement/salesQuotation/index.vue
@@ -0,0 +1,910 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="8">
+ <el-input
+ v-model="searchForm.quotationNo"
+ placeholder="璇疯緭鍏ユ姤浠峰崟鍙�"
+ clearable
+ @keyup.enter="handleSearch"
+ >
+ <template #prefix>
+ <el-icon><Search /></el-icon>
+ </template>
+ </el-input>
+ </el-col>
+ <el-col :span="8">
+ <el-select v-model="searchForm.customerId" placeholder="璇烽�夋嫨瀹㈡埛" clearable>
+ <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id">
+ {{
+ item.customerName + "鈥斺��" + item.taxpayerIdentificationNumber
+ }}
+ </el-option>
+ </el-select>
+ </el-col>
+<!-- <el-col :span="6">-->
+<!-- <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鎶ヤ环鐘舵��" clearable>-->
+<!-- <el-option label="鑽夌" value="鑽夌"></el-option>-->
+<!-- <el-option label="宸插彂閫�" value="宸插彂閫�"></el-option>-->
+<!-- <el-option label="瀹㈡埛纭" value="瀹㈡埛纭"></el-option>-->
+<!-- <el-option label="宸茶繃鏈�" value="宸茶繃鏈�"></el-option>-->
+<!-- </el-select>-->
+<!-- </el-col>-->
+ <el-col :span="8">
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ <el-button style="float: right;" type="primary" @click="handleAdd">
+ 鏂板鎶ヤ环
+ </el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鎶ヤ环鍒楄〃 -->
+ <el-table
+ :data="filteredList"
+ style="width: 100%"
+ v-loading="loading"
+ border
+ stripe
+ height="calc(100vh - 22em)"
+ >
+ <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+ <el-table-column prop="quotationNo" label="鎶ヤ环鍗曞彿" />
+ <el-table-column prop="customer" label="瀹㈡埛鍚嶇О" />
+ <el-table-column prop="salesperson" label="涓氬姟鍛�" width="100" />
+ <el-table-column prop="quotationDate" label="鎶ヤ环鏃ユ湡" width="120" />
+ <el-table-column prop="validDate" label="鏈夋晥鏈熻嚦" width="120" />
+ <el-table-column prop="status" label="瀹℃壒鐘舵��" width="120" align="center">
+ <template #default="{ row }">
+ <el-tag :type="getStatusType(row.status)" disable-transitions>
+ {{ row.status || '--' }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="totalAmount" label="鎶ヤ环閲戦" width="120">
+ <template #default="scope">
+ 楼{{ scope.row.totalAmount.toFixed(2) }}
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleEdit(scope.row)" :disabled="!['寰呭鎵�','鎷掔粷'].includes(scope.row.status)">缂栬緫</el-button>
+ <el-button link type="primary" @click="handleView(scope.row)" style="color: #67C23A">鏌ョ湅</el-button>
+ <el-button link type="danger" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="pagination.total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.currentPage"
+ :limit="pagination.pageSize"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <div class="quotation-form-container">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="quotation-form">
+ <!-- 鍩烘湰淇℃伅 -->
+ <el-card class="form-card" shadow="hover">
+ <template #header>
+ <div class="card-header-wrapper">
+ <el-icon class="card-icon"><Document /></el-icon>
+ <span class="card-title">鍩烘湰淇℃伅</span>
+ </div>
+ </template>
+ <div class="form-content">
+ <el-row :gutter="24">
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerId">
+ <el-select v-model="form.customerId" placeholder="璇烽�夋嫨瀹㈡埛" style="width: 100%" clearable filterable>
+ <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="涓氬姟鍛�" prop="salesperson">
+ <el-select v-model="form.salesperson" placeholder="璇烽�夋嫨涓氬姟鍛�" style="width: 100%" clearable filterable>
+ <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName"
+ :value="item.nickName" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="24">
+ <el-col :span="12">
+ <el-form-item label="鎶ヤ环鏃ユ湡" prop="quotationDate">
+ <el-date-picker
+ v-model="form.quotationDate"
+ type="date"
+ placeholder="閫夋嫨鎶ヤ环鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏈夋晥鏈熻嚦" prop="validDate">
+ <el-date-picker
+ v-model="form.validDate"
+ type="date"
+ placeholder="閫夋嫨鏈夋晥鏈�"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ clearable
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="24">
+ <el-col :span="12">
+ <el-form-item label="浠樻鏂瑰紡" prop="paymentMethod">
+ <el-input v-model="form.paymentMethod" placeholder="璇疯緭鍏ヤ粯娆炬柟寮�" clearable />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </div>
+ </el-card>
+
+ <!-- 浜у搧淇℃伅 -->
+ <el-card class="form-card" shadow="hover">
+ <template #header>
+ <div class="card-header-wrapper">
+ <el-icon class="card-icon"><Box /></el-icon>
+ <span class="card-title">浜у搧淇℃伅</span>
+ <el-button type="primary" size="small" @click="addProduct" class="header-btn">
+ <el-icon><Plus /></el-icon>
+ 娣诲姞浜у搧
+ </el-button>
+ </div>
+ </template>
+ <div class="form-content">
+ <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0">
+ <el-table-column prop="product" label="浜у搧鍚嶇О" width="200">
+ <template #default="scope">
+ <el-form-item :prop="`products.${scope.$index}.productId`" class="product-table-form-item">
+ <el-tree-select
+ v-model="scope.row.productId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ check-strictly
+ @change="getModels($event, scope.row)"
+ :data="productOptions"
+ :render-after-expand="false"
+ style="width: 100%"
+ />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="specification" label="瑙勬牸鍨嬪彿" width="200">
+ <template #default="scope">
+ <el-form-item :prop="`products.${scope.$index}.productModelId`" class="product-table-form-item">
+ <el-select
+ v-model="scope.row.productModelId"
+ placeholder="璇烽�夋嫨"
+ clearable
+ @change="getProductModel($event, scope.row)"
+ style="width: 100%"
+ >
+ <el-option
+ v-for="item in scope.row.modelOptions || []"
+ :key="item.id"
+ :label="item.model"
+ :value="item.id"
+ />
+ </el-select>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unit" label="鍗曚綅">
+ <template #default="scope">
+ <el-form-item :prop="`products.${scope.$index}.unit`" class="product-table-form-item">
+ <el-input v-model="scope.row.unit" placeholder="鍗曚綅" clearable/>
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column prop="unitPrice" label="鍗曚环">
+ <template #default="scope">
+ <el-form-item :prop="`products.${scope.$index}.unitPrice`" class="product-table-form-item">
+ <el-input-number v-model="scope.row.unitPrice" :min="0" :precision="2" style="width: 100%" />
+ </el-form-item>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="80" align="center">
+ <template #default="scope">
+ <el-button link type="danger" @click="removeProduct(scope.$index)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <el-empty v-else description="鏆傛棤浜у搧锛岃鐐瑰嚮娣诲姞浜у搧" :image-size="80" />
+ </div>
+ </el-card>
+
+ <!-- 澶囨敞淇℃伅 -->
+ <el-card class="form-card" shadow="hover">
+ <template #header>
+ <div class="card-header-wrapper">
+ <el-icon class="card-icon"><EditPen /></el-icon>
+ <span class="card-title">澶囨敞淇℃伅</span>
+ </div>
+ </template>
+ <div class="form-content">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input
+ type="textarea"
+ v-model="form.remark"
+ placeholder="璇疯緭鍏ュ娉ㄤ俊鎭紙閫夊~锛�"
+ :rows="4"
+ maxlength="500"
+ show-word-limit
+ ></el-input>
+ </el-form-item>
+ </div>
+ </el-card>
+ </el-form>
+ </div>
+ </FormDialog>
+
+ <!-- 鏌ョ湅璇︽儏瀵硅瘽妗� -->
+ <el-dialog v-model="viewDialogVisible" title="鎶ヤ环璇︽儏" width="800px">
+ <el-descriptions :column="2" border>
+ <el-descriptions-item label="鎶ヤ环鍗曞彿">{{ currentQuotation.quotationNo }}</el-descriptions-item>
+ <el-descriptions-item label="瀹㈡埛鍚嶇О">{{ currentQuotation.customer }}</el-descriptions-item>
+ <el-descriptions-item label="涓氬姟鍛�">{{ currentQuotation.salesperson }}</el-descriptions-item>
+ <el-descriptions-item label="鎶ヤ环鏃ユ湡">{{ currentQuotation.quotationDate }}</el-descriptions-item>
+ <el-descriptions-item label="鏈夋晥鏈熻嚦">{{ currentQuotation.validDate }}</el-descriptions-item>
+ <el-descriptions-item label="浠樻鏂瑰紡">{{ currentQuotation.paymentMethod }}</el-descriptions-item>
+<!-- <el-descriptions-item label="鎶ヤ环鐘舵��">-->
+<!-- <el-tag :type="getStatusType(currentQuotation.status)">{{ currentQuotation.status }}</el-tag>-->
+<!-- </el-descriptions-item>-->
+ <el-descriptions-item label="鎶ヤ环鎬婚" :span="2">
+ <span style="font-size: 18px; color: #e6a23c; font-weight: bold;">楼{{ currentQuotation.totalAmount?.toFixed(2) }}</span>
+ </el-descriptions-item>
+ </el-descriptions>
+
+ <div style="margin: 20px 0;">
+ <h4>浜у搧鏄庣粏</h4>
+ <el-table :data="currentQuotation.products" border style="width: 100%">
+ <el-table-column prop="product" label="浜у搧鍚嶇О" />
+ <el-table-column prop="specification" label="瑙勬牸鍨嬪彿" />
+ <el-table-column prop="unit" label="鍗曚綅" />
+ <el-table-column prop="unitPrice" label="鍗曚环">
+ <template #default="scope">
+ 楼{{ scope.row.unitPrice.toFixed(2) }}
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+
+ <div v-if="currentQuotation.remark" style="margin-top: 20px;">
+ <h4>澶囨敞</h4>
+ <p>{{ currentQuotation.remark }}</p>
+ </div>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Search, Document, Box, EditPen, Plus } from '@element-plus/icons-vue'
+import Pagination from '@/components/PIMTable/Pagination.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js'
+import {modelList, productTreeList} from "@/api/basicData/product.js";
+import {listCustomer} from "@/api/basicData/customer.js";
+import { userListNoPage } from "@/api/system/user.js";
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const searchForm = reactive({
+ quotationNo: '',
+ customerId: '',
+ status: ''
+})
+
+const quotationList = ref([])
+const userList = ref([])
+const productOptions = ref([]);
+const modelOptions = ref([]);
+const pagination = reactive({
+ total: 3,
+ currentPage: 1,
+ pageSize: 100
+})
+
+const dialogVisible = ref(false)
+const viewDialogVisible = ref(false)
+const dialogTitle = ref('鏂板鎶ヤ环')
+const form = reactive({
+ quotationNo: '',
+ customerId: undefined,
+ customer: '',
+ salesperson: '',
+ quotationDate: '',
+ validDate: '',
+ paymentMethod: '',
+ status: '鑽夌',
+ remark: '',
+ products: [],
+ subtotal: 0,
+ freight: 0,
+ otherFee: 0,
+ discountRate: 0,
+ discountAmount: 0,
+ totalAmount: 0
+})
+
+const baseRules = {
+ customer: [{ required: true, message: '璇烽�夋嫨瀹㈡埛', trigger: 'change' }],
+ salesperson: [{ required: true, message: '璇烽�夋嫨涓氬姟鍛�', trigger: 'change' }],
+ quotationDate: [{ required: true, message: '璇烽�夋嫨鎶ヤ环鏃ユ湡', trigger: 'change' }],
+ validDate: [{ required: true, message: '璇烽�夋嫨鏈夋晥鏈�', trigger: 'change' }],
+ paymentMethod: [{ required: true, message: '璇疯緭鍏ヤ粯娆炬柟寮�', trigger: 'blur' }]
+}
+
+const productRowRules = {
+ productId: [{ required: true, message: '璇烽�夋嫨浜у搧鍚嶇О', trigger: 'change' }],
+ productModelId: [{ required: true, message: '璇烽�夋嫨瑙勬牸鍨嬪彿', trigger: 'change' }],
+ unit: [{ required: true, message: '璇峰~鍐欏崟浣�', trigger: 'blur' }],
+ unitPrice: [{ required: true, message: '璇峰~鍐欏崟浠�', trigger: 'change' }]
+}
+const rules = computed(() => {
+ const r = { ...baseRules }
+ ;(form.products || []).forEach((_, i) => {
+ r[`products.${i}.productId`] = productRowRules.productId
+ r[`products.${i}.productModelId`] = productRowRules.productModelId
+ r[`products.${i}.unit`] = productRowRules.unit
+ r[`products.${i}.unitPrice`] = productRowRules.unitPrice
+ })
+ return r
+})
+const customerOption = ref([]);
+
+const isEdit = ref(false)
+const editId = ref(null)
+const currentQuotation = ref({})
+const formRef = ref()
+
+// 璁$畻灞炴��
+const filteredList = computed(() => {
+ let list = quotationList.value
+ return list
+})
+
+// 鏂规硶
+const getStatusType = (status) => {
+ const statusMap = {
+ '寰呭鎵�': 'info',
+ '瀹℃牳涓�': 'primary',
+ '閫氳繃': 'success',
+ '鎷掔粷': 'danger'
+ }
+ return statusMap[status] || 'info'
+}
+
+const resetSearch = () => {
+ searchForm.quotationNo = ''
+ searchForm.customer = ''
+ searchForm.status = ''
+ // 閲嶇疆鍒扮涓�椤靛苟閲嶆柊鏌ヨ
+ pagination.currentPage = 1
+ handleSearch()
+}
+
+const handleAdd = async () => {
+ dialogTitle.value = '鏂板鎶ヤ环'
+ isEdit.value = false
+ resetForm()
+ dialogVisible.value = true
+ getProductOptions();
+ fetchCustomerOptions()
+}
+
+const fetchCustomerOptions = () => {
+ if (customerOption.value.length > 0) return
+ listCustomer({current: -1,size:-1, type: 0}).then((res) => {
+ customerOption.value = res.data.records;
+ });
+}
+const getProductOptions = () => {
+ // 杩斿洖 Promise锛屼究浜庣紪杈戞椂 await 纭繚鑳藉弽鏄�
+ return productTreeList().then((res) => {
+ productOptions.value = convertIdToValue(res);
+ return productOptions.value
+ });
+};
+function convertIdToValue(data) {
+ return data.map((item) => {
+ const { id, children, ...rest } = item;
+ const newItem = {
+ ...rest,
+ value: id, // 灏� id 鏀逛负 value
+ };
+ if (children && children.length > 0) {
+ newItem.children = convertIdToValue(children);
+ }
+
+ return newItem;
+ });
+}
+// 鏍规嵁鍚嶇О鍙嶆煡鑺傜偣 id锛屼究浜庝粎瀛樺悕绉版椂鐨勫弽鏄�
+function findNodeIdByLabel(nodes, label) {
+ if (!label) return null;
+ for (let i = 0; i < nodes.length; i++) {
+ const node = nodes[i];
+ if (node.label === label) return node.value;
+ if (node.children && node.children.length > 0) {
+ const found = findNodeIdByLabel(node.children, label);
+ if (found !== null && found !== undefined) return found;
+ }
+ }
+ return null;
+}
+const getModels = (value, row) => {
+ if (!row) return;
+ // 濡傛灉娓呯┖閫夋嫨锛屽垯娓呯┖鐩稿叧瀛楁
+ if (!value) {
+ row.productId = '';
+ row.product = '';
+ row.modelOptions = [];
+ row.productModelId = '';
+ row.specification = '';
+ row.unit = '';
+ return;
+ }
+ // 鏇存柊 productId锛坴-model 宸茬粡鑷姩鏇存柊锛岃繖閲岀‘淇濅竴鑷存�э級
+ row.productId = value;
+ // 鎵惧埌瀵瑰簲鐨� label 骞惰祴鍊肩粰 row.product
+ const label = findNodeById(productOptions.value, value);
+ if (label) {
+ row.product = label;
+ }
+ // 鑾峰彇瑙勬牸鍨嬪彿鍒楄〃锛岃缃埌褰撳墠琛岀殑 modelOptions
+ modelList({ id: value }).then((res) => {
+ row.modelOptions = res || [];
+ });
+};
+const getProductModel = (value, row) => {
+ if (!row) return;
+ // 濡傛灉娓呯┖閫夋嫨锛屽垯娓呯┖鐩稿叧瀛楁
+ if (!value) {
+ row.productModelId = '';
+ row.specification = '';
+ row.unit = '';
+ return;
+ }
+ // 鏇存柊 productModelId锛坴-model 宸茬粡鑷姩鏇存柊锛岃繖閲岀‘淇濅竴鑷存�э級
+ row.productModelId = value;
+ const modelOptions = row.modelOptions || [];
+ const index = modelOptions.findIndex((item) => item.id === value);
+ if (index !== -1) {
+ row.specification = modelOptions[index].model;
+ row.unit = modelOptions[index].unit;
+ } else {
+ row.specification = '';
+ row.unit = '';
+ }
+};
+const findNodeById = (nodes, productId) => {
+ for (let i = 0; i < nodes.length; i++) {
+ if (nodes[i].value === productId) {
+ return nodes[i].label; // 鎵惧埌鑺傜偣锛岃繑鍥� label
+ }
+ if (nodes[i].children && nodes[i].children.length > 0) {
+ const foundLabel = findNodeById(nodes[i].children, productId);
+ if (foundLabel) {
+ return foundLabel; // 鍦ㄥ瓙鑺傜偣涓壘鍒帮紝杩斿洖 label
+ }
+ }
+ }
+ return null; // 娌℃湁鎵惧埌鑺傜偣锛岃繑鍥瀗ull
+};
+const handleView = (row) => {
+ // 鍙鍒堕渶瑕佺殑瀛楁锛岄伩鍏嶅皢缁勪欢寮曠敤鏀惧叆鍝嶅簲寮忓璞�
+ currentQuotation.value = {
+ quotationNo: row.quotationNo || '',
+ customer: row.customer || '',
+ salesperson: row.salesperson || '',
+ quotationDate: row.quotationDate || '',
+ validDate: row.validDate || '',
+ paymentMethod: row.paymentMethod || '',
+ status: row.status || '',
+ remark: row.remark || '',
+ products: row.products ? row.products.map(product => ({
+ productId: product.productId || '',
+ product: product.product || product.productName || '',
+ productModelId: product.productModelId || '',
+ specification: product.specification || '',
+ quantity: product.quantity || 0,
+ unit: product.unit || '',
+ unitPrice: product.unitPrice || 0,
+ amount: product.amount || 0
+ })) : [],
+ totalAmount: row.totalAmount || 0
+ }
+ viewDialogVisible.value = true
+}
+
+const handleEdit = async (row) => {
+ dialogTitle.value = '缂栬緫鎶ヤ环'
+ isEdit.value = true
+ editId.value = row.id
+ form.id = row.id || form.id || null
+ // 鍏堝姞杞戒骇鍝佹爲鏁版嵁锛屽惁鍒� el-tree-select 鏃犳硶鍙嶆樉浜у搧鍚嶇О
+ await getProductOptions()
+ await fetchCustomerOptions()
+
+ // 鍙鍒堕渶瑕佺殑瀛楁锛岄伩鍏嶅皢缁勪欢寮曠敤鏀惧叆鍝嶅簲寮忓璞�
+ form.quotationNo = row.quotationNo || ''
+ form.customer = row.customer || ''
+ form.customerId = row.customerId || undefined
+ form.salesperson = row.salesperson || ''
+ form.quotationDate = row.quotationDate || ''
+ form.validDate = row.validDate || ''
+ form.paymentMethod = row.paymentMethod || ''
+ form.status = row.status || '鑽夌'
+ form.remark = row.remark || ''
+ form.products = row.products ? await Promise.all(row.products.map(async (product) => {
+ const productName = product.product || product.productName || ''
+ // 浼樺厛鐢� productId锛涘鏋滃彧鏈夊悕绉帮紝灏濊瘯鍙嶆煡 id 浠ヤ究鏍戦�夋嫨鍣ㄥ弽鏄�
+ const resolvedProductId = product.productId
+ ? Number(product.productId)
+ : findNodeIdByLabel(productOptions.value, productName) || ''
+
+ // 濡傛灉鏈変骇鍝両D锛屽姞杞藉搴旂殑瑙勬牸鍨嬪彿鍒楄〃
+ let modelOptions = [];
+ let resolvedProductModelId = product.productModelId || '';
+
+ if (resolvedProductId) {
+ try {
+ const res = await modelList({ id: resolvedProductId });
+ modelOptions = res || [];
+
+ // 濡傛灉杩斿洖鐨勬暟鎹病鏈� productModelId锛屼絾鏈� specification 鍚嶇О锛屾牴鎹悕绉版煡鎵� ID
+ if (!resolvedProductModelId && product.specification) {
+ const foundModel = modelOptions.find(item => item.model === product.specification);
+ if (foundModel) {
+ resolvedProductModelId = foundModel.id;
+ }
+ }
+ } catch (error) {
+ console.error('鍔犺浇瑙勬牸鍨嬪彿澶辫触:', error);
+ }
+ }
+
+ return {
+ productId: resolvedProductId,
+ product: productName,
+ productModelId: resolvedProductModelId,
+ specification: product.specification || '',
+ quantity: product.quantity || 0,
+ unit: product.unit || '',
+ unitPrice: product.unitPrice || 0,
+ amount: product.amount || 0,
+ modelOptions: modelOptions // 涓烘瘡琛屾坊鍔犵嫭绔嬬殑瑙勬牸鍨嬪彿鍒楄〃
+ }
+ })) : []
+ form.subtotal = row.subtotal || 0
+ form.freight = row.freight || 0
+ form.otherFee = row.otherFee || 0
+ form.discountRate = row.discountRate || 0
+ form.discountAmount = row.discountAmount || 0
+ form.totalAmount = row.totalAmount || 0
+
+ dialogVisible.value = true
+}
+
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭鍒犻櫎璇ユ姤浠峰崟鍚楋紵', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ const index = quotationList.value.findIndex(item => item.id === row.id)
+ if (index > -1) {
+ deleteQuotation(row.id).then(res=>{
+ // console.log(res)
+ if(res.code===200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ handleSearch()
+ }
+ })
+ // quotationList.value.splice(index, 1)
+ // pagination.total--
+ // ElMessage.success('鍒犻櫎鎴愬姛')
+ }
+ })
+}
+
+const resetForm = () => {
+ form.customer = ''
+ form.salesperson = ''
+ form.quotationDate = ''
+ form.validDate = ''
+ form.paymentMethod = ''
+ form.status = '鑽夌'
+ form.remark = ''
+ form.products = []
+ form.subtotal = 0
+ form.freight = 0
+ form.otherFee = 0
+ form.discountRate = 0
+ form.discountAmount = 0
+ form.totalAmount = 0
+}
+
+const addProduct = () => {
+ form.products.push({
+ productId: '',
+ product: '',
+ productName: '',
+ productModelId: '',
+ quantity: 1,
+ unit: '',
+ unitPrice: 0,
+ amount: 0,
+ modelOptions: [] // 涓烘瘡琛屾坊鍔犵嫭绔嬬殑瑙勬牸鍨嬪彿鍒楄〃
+ })
+}
+
+const removeProduct = (index) => {
+ form.products.splice(index, 1)
+ calculateSubtotal()
+}
+
+const calculateAmount = (product) => {
+ product.amount = product.quantity * product.unitPrice
+ calculateSubtotal()
+}
+
+const calculateSubtotal = () => {
+ form.subtotal = form.products.reduce((sum, product) => sum + product.amount, 0)
+ calculateTotal()
+}
+
+const calculateTotal = () => {
+ form.discountAmount = form.subtotal * (form.discountRate / 100)
+ form.totalAmount = form.subtotal + form.freight + form.otherFee - form.discountAmount
+}
+
+const handleSubmit = () => {
+ formRef.value.validate((valid) => {
+ if (valid) {
+ if (form.products.length === 0) {
+ ElMessage.warning('璇疯嚦灏戞坊鍔犱竴涓骇鍝�')
+ return
+ }
+
+ // 璁$畻鎵�鏈変骇鍝佺殑鍗曚环鎬诲拰
+ form.totalAmount = form.products.reduce((sum, product) => {
+ const price = Number(product.unitPrice) || 0
+ return sum + price
+ }, 0)
+
+ form.customer = customerOption.value.find(item => item.id === form.customerId)?.customerName || ''
+ if (isEdit.value) {
+ // 缂栬緫
+ const index = quotationList.value.findIndex(item => item.id === editId.value)
+ if (index > -1) {
+ updateQuotation(form).then(res=>{
+ // console.log(res)
+ if(res.code===200){
+ ElMessage.success('缂栬緫鎴愬姛')
+ dialogVisible.value = false
+ handleSearch()
+ }
+ })
+ }
+ } else {
+ // 鏂板
+ addQuotation(form).then(res=>{
+ if(res.code===200){
+ ElMessage.success('鏂板鎴愬姛')
+ dialogVisible.value = false
+ handleSearch()
+ }
+ })
+ }
+
+ }
+ })
+}
+
+const handleCurrentChange = (val) => {
+ pagination.currentPage = val.page
+ pagination.pageSize = val.limit
+ // 鍒嗛〉鍙樺寲鏃堕噸鏂版煡璇㈠垪琛�
+ handleSearch()
+}
+const handleSearch = ()=>{
+ const params = {
+ // 鍚庣鍒嗛〉鍙傛暟锛歝urrent / size
+ current: pagination.currentPage,
+ size: pagination.pageSize,
+ ...searchForm
+ }
+ getQuotationList(params).then(res=>{
+ // console.log(res)
+ if(res.code===200){
+ // 鍙鍒堕渶瑕佺殑瀛楁锛岄伩鍏嶅皢缁勪欢寮曠敤鎴栧叾浠栧璞℃斁鍏ュ搷搴斿紡瀵硅薄
+ quotationList.value = (res.data.records || []).map(item => ({
+ id: item.id,
+ quotationNo: item.quotationNo || '',
+ customer: item.customer || '',
+ customerId: item.customerId || undefined,
+ salesperson: item.salesperson || '',
+ quotationDate: item.quotationDate || '',
+ validDate: item.validDate || '',
+ paymentMethod: item.paymentMethod || '',
+ status: item.status || '鑽夌',
+ // 瀹℃壒浜猴紙鐢ㄤ簬缂栬緫鏃跺弽鏄撅級
+ approveUserIds: item.approveUserIds || '',
+ remark: item.remark || '',
+ products: item.products ? item.products.map(product => ({
+ productId: product.productId || '',
+ product: product.product || product.productName || '',
+ productModelId: product.productModelId || '',
+ specification: product.specification || '',
+ quantity: product.quantity || 0,
+ unit: product.unit || '',
+ unitPrice: product.unitPrice || 0,
+ amount: product.amount || 0
+ })) : [],
+ subtotal: item.subtotal || 0,
+ freight: item.freight || 0,
+ otherFee: item.otherFee || 0,
+ discountRate: item.discountRate || 0,
+ discountAmount: item.discountAmount || 0,
+ totalAmount: item.totalAmount || 0
+ }))
+ pagination.total = res.data.total
+ }
+ })
+ // customerList().then((res) => {
+ // customerOption.value = res;
+ // });
+}
+
+const getUserList = async () => {
+ try {
+ const res = await userListNoPage()
+ userList.value = Array.isArray(res?.data) ? res.data : []
+ } catch (error) {
+ userList.value = []
+ ElMessage.error('鍔犺浇涓氬姟鍛樺垪琛ㄥけ璐�')
+ }
+}
+
+onMounted(()=>{
+ getUserList()
+ handleSearch()
+ fetchCustomerOptions()
+})
+</script>
+
+<style scoped lang="scss">
+.search-row {
+ margin-bottom: 20px;
+}
+
+.quotation-form-container {
+ padding: 10px 0;
+ max-height: calc(100vh - 200px);
+ overflow-y: auto;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #c1c1c1;
+ border-radius: 3px;
+
+ &:hover {
+ background: #a8a8a8;
+ }
+ }
+}
+
+.quotation-form {
+ .el-form-item {
+ margin-bottom: 22px;
+ }
+}
+
+.form-card {
+ margin-bottom: 24px;
+ border-radius: 8px;
+ transition: all 0.3s ease;
+
+ &:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
+ }
+
+ :deep(.el-card__header) {
+ padding: 16px 20px;
+ background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%);
+ border-bottom: 1px solid #ebeef5;
+ }
+
+ :deep(.el-card__body) {
+ padding: 20px;
+ }
+}
+
+.card-header-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ .card-icon {
+ font-size: 18px;
+ color: #409eff;
+ }
+
+ .card-title {
+ font-weight: 600;
+ font-size: 16px;
+ color: #303133;
+ flex: 1;
+ }
+
+ .header-btn {
+ margin-left: auto;
+ }
+}
+
+.form-content {
+ padding: 8px 0;
+}
+
+.product-table-form-item {
+ margin-bottom: 0;
+ :deep(.el-form-item__content) {
+ margin-left: 0 !important;
+ }
+ :deep(.el-form-item__label) {
+ width: auto;
+ min-width: auto;
+ }
+}
+
+.product-table {
+ :deep(.el-table__header) {
+ background-color: #f5f7fa;
+
+ th {
+ background-color: #f5f7fa !important;
+ color: #606266;
+ font-weight: 600;
+ }
+ }
+
+ :deep(.el-table__row) {
+ &:hover {
+ background-color: #f5f7fa;
+ }
+ }
+
+ :deep(.el-table__cell) {
+ padding: 12px 0;
+ }
+}
+
+.dialog-footer {
+ text-align: right;
+}
+
+</style>
diff --git a/src/views/salesManagement/salespersonManagement/index.vue b/src/views/salesManagement/salespersonManagement/index.vue
new file mode 100644
index 0000000..e0094ec
--- /dev/null
+++ b/src/views/salesManagement/salespersonManagement/index.vue
@@ -0,0 +1,371 @@
+<template>
+ <div class="app-container">
+ <el-card class="box-card">
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-input
+ v-model="searchForm.name"
+ placeholder="璇疯緭鍏ヤ笟鍔″憳濮撳悕"
+ clearable
+ @keyup.enter="handleSearch"
+ >
+ <template #prefix>
+ <el-icon><Search /></el-icon>
+ </template>
+ </el-input>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.department" placeholder="璇烽�夋嫨閮ㄩ棬" clearable>
+ <el-option label="閿�鍞儴" value="閿�鍞儴"></el-option>
+ <el-option label="甯傚満閮�" value="甯傚満閮�"></el-option>
+ <el-option label="瀹㈡湇閮�" value="瀹㈡湇閮�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="searchForm.status" placeholder="璇烽�夋嫨鐘舵��" clearable>
+ <el-option label="鍦ㄨ亴" value="鍦ㄨ亴"></el-option>
+ <el-option label="绂昏亴" value="绂昏亴"></el-option>
+ <el-option label="璇曠敤鏈�" value="璇曠敤鏈�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="handleSearch">鎼滅储</el-button>
+ <el-button @click="resetSearch">閲嶇疆</el-button>
+ <el-button type="primary" style="float: right;" @click="handleAdd">鏂板涓氬姟鍛�</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 涓氬姟鍛樺垪琛� -->
+ <el-table
+ :data="filteredList"
+ style="width: 100%"
+ v-loading="loading"
+ border
+ stripe
+ height="calc(100vh - 22em)"
+ >
+ <el-table-column prop="id" label="ID" width="80" align="center"/>
+ <el-table-column prop="name" label="濮撳悕" width="120" />
+ <el-table-column prop="phone" label="鑱旂郴鐢佃瘽" width="140" />
+ <el-table-column prop="email" label="閭" width="200" />
+ <el-table-column prop="department" label="閮ㄩ棬" width="100" />
+ <el-table-column prop="position" label="鑱屼綅" width="100" />
+ <el-table-column prop="hireDate" label="鍏ヨ亴鏃ユ湡" width="120" />
+ <el-table-column prop="status" label="鐘舵��" width="80">
+ <template #default="scope">
+ <el-tag :type="getStatusType(scope.row.status)">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="permissions" label="鏉冮檺">
+ <template #default="scope">
+ <el-tag v-for="perm in scope.row.permissionsList" :key="perm" size="small" style="margin-right: 5px;">
+ {{ perm }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleEdit(scope.row)">缂栬緫</el-button>
+ <el-button link type="primary" @click="handlePermissions(scope.row)">鏉冮檺</el-button>
+ <el-button link type="danger" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 鍒嗛〉 -->
+ <pagination
+ :total="total"
+ layout="total, sizes, prev, pager, next, jumper"
+ :page="pagination.current"
+ :limit="pagination.size"
+ @pagination="handleCurrentChange"
+ />
+ </el-card>
+
+ <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+ <FormDialog v-model="dialogVisible" :title="dialogTitle" :width="'600px'" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false">
+ <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="濮撳悕" prop="name">
+ <el-input v-model="form.name" placeholder="璇疯緭鍏ュ鍚�"></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="phone">
+ <el-input v-model="form.phone" placeholder="璇疯緭鍏ヨ仈绯荤數璇�"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�"></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬" prop="department">
+ <el-select v-model="form.department" placeholder="璇烽�夋嫨閮ㄩ棬" style="width: 100%">
+ <el-option label="閿�鍞儴" value="閿�鍞儴"></el-option>
+ <el-option label="甯傚満閮�" value="甯傚満閮�"></el-option>
+ <el-option label="瀹㈡湇閮�" value="瀹㈡湇閮�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鑱屼綅" prop="position">
+ <el-input v-model="form.position" placeholder="璇疯緭鍏ヨ亴浣�"></el-input>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏ヨ亴鏃ユ湡" prop="hireDate">
+ <el-date-picker
+ v-model="form.hireDate"
+ type="date"
+ placeholder="閫夋嫨鍏ヨ亴鏃ユ湡"
+ style="width: 100%"
+ format="YYYY-MM-DD"
+ value-format="YYYY-MM-DD"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%">
+ <el-option label="鍦ㄨ亴" value="鍦ㄨ亴"></el-option>
+ <el-option label="绂昏亴" value="绂昏亴"></el-option>
+ <el-option label="璇曠敤鏈�" value="璇曠敤鏈�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ </FormDialog>
+
+ <!-- 鏉冮檺璁剧疆瀵硅瘽妗� -->
+ <FormDialog v-model="permissionDialogVisible" title="鏉冮檺璁剧疆" :width="'500px'" @close="permissionDialogVisible = false" @confirm="savePermissions" @cancel="permissionDialogVisible = false">
+ <el-form label-width="100px">
+ <el-form-item label="涓氬姟鍛樺鍚�">
+ <span>{{ currentSalesperson.name }}</span>
+ </el-form-item>
+ <el-form-item label="鏉冮檺璁剧疆">
+ <el-checkbox-group v-model="currentPermissions">
+ <el-checkbox label="璁㈠崟绠$悊">璁㈠崟绠$悊</el-checkbox>
+ <el-checkbox label="瀹㈡埛绠$悊">瀹㈡埛绠$悊</el-checkbox>
+ <el-checkbox label="璐㈠姟绠$悊">璐㈠姟绠$悊</el-checkbox>
+ <el-checkbox label="鍙戣揣绠$悊">鍙戣揣绠$悊</el-checkbox>
+ <el-checkbox label="鎶ヨ〃鏌ョ湅">鎶ヨ〃鏌ョ湅</el-checkbox>
+ <el-checkbox label="绯荤粺璁剧疆">绯荤粺璁剧疆</el-checkbox>
+ </el-checkbox-group>
+ </el-form-item>
+ </el-form>
+ </FormDialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed,onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {listPage,add,update,deleteSalespersonManagement} from '@/api/salesManagement/salespersonManagement.js'
+import { Plus, Search } from '@element-plus/icons-vue'
+import Pagination from '@/components/PIMTable/Pagination.vue'
+import FormDialog from '@/components/Dialog/FormDialog.vue'
+
+const salespersonList = ref([])
+const total = ref(0)
+
+onMounted(() => {
+ getList()
+})
+const getList = () => {
+ loading.value = true
+ listPage({...pagination,...searchForm}).then(res => {
+ salespersonList.value = res.data.records
+ total.value = res.data.total
+ loading.value = false
+ })
+}
+
+// 鍝嶅簲寮忔暟鎹�
+const loading = ref(false)
+const searchForm = reactive({
+ name: '',
+ department: '',
+ status: ''
+})
+
+
+
+const pagination = reactive({
+ current: 1,
+ size: 10
+})
+
+const dialogVisible = ref(false)
+const dialogTitle = ref('鏂板涓氬姟鍛�')
+const form = reactive({
+ name: '',
+ phone: '',
+ email: '',
+ department: '',
+ position: '',
+ hireDate: '',
+ status: '鍦ㄨ亴'
+})
+
+const rules = {
+ name: [{ required: true, message: '璇疯緭鍏ュ鍚�', trigger: 'blur' }],
+ phone: [{ required: true, message: '璇疯緭鍏ヨ仈绯荤數璇�', trigger: 'blur' }],
+ email: [{ required: true, message: '璇疯緭鍏ラ偖绠�', trigger: 'blur' }],
+ department: [{ required: true, message: '璇烽�夋嫨閮ㄩ棬', trigger: 'change' }],
+ position: [{ required: true, message: '璇疯緭鍏ヨ亴浣�', trigger: 'blur' }],
+ hireDate: [{ required: true, message: '璇烽�夋嫨鍏ヨ亴鏃ユ湡', trigger: 'change' }],
+ status: [{ required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }]
+}
+
+const isEdit = ref(false)
+const editId = ref(null)
+const permissionDialogVisible = ref(false)
+const currentSalesperson = ref({})
+const currentPermissions = ref([])
+const formRef = ref()
+
+// 璁$畻灞炴��
+const filteredList = computed(() => {
+ let list = salespersonList.value
+ if (searchForm.name) {
+ list = list.filter(item => item.name.includes(searchForm.name))
+ }
+ if (searchForm.department) {
+ list = list.filter(item => item.department === searchForm.department)
+ }
+ if (searchForm.status) {
+ list = list.filter(item => item.status === searchForm.status)
+ }
+ return list
+})
+
+// 鏂规硶
+const getStatusType = (status) => {
+ const statusMap = {
+ '鍦ㄨ亴': 'success',
+ '绂昏亴': 'danger',
+ '璇曠敤鏈�': 'warning'
+ }
+ return statusMap[status] || 'info'
+}
+
+const handleSearch = () => {
+ getList()
+ // 鎼滅储閫昏緫宸插湪computed涓鐞�
+}
+
+const resetSearch = () => {
+ searchForm.name = ''
+ searchForm.department = ''
+ searchForm.status = ''
+}
+
+const handleAdd = () => {
+ dialogTitle.value = '鏂板涓氬姟鍛�'
+ isEdit.value = false
+ form.name = ''
+ form.phone = ''
+ form.email = ''
+ form.department = ''
+ form.position = ''
+ form.hireDate = ''
+ form.status = '鍦ㄨ亴'
+ dialogVisible.value = true
+}
+
+const handleEdit = (row) => {
+ dialogTitle.value = '缂栬緫涓氬姟鍛�'
+ isEdit.value = true
+ editId.value = row.id
+ Object.assign(form, row)
+ dialogVisible.value = true
+}
+
+const handleDelete = (row) => {
+ ElMessageBox.confirm('纭鍒犻櫎璇ヤ笟鍔″憳鍚楋紵', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ let ids = [row.id]
+ deleteSalespersonManagement(ids).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ getList()
+ }
+ })
+ })
+}
+
+const handlePermissions = (row) => {
+ currentSalesperson.value = row
+ currentPermissions.value = row.permissions.split(",")
+ permissionDialogVisible.value = true
+}
+
+const savePermissions = () => {
+ let splice = currentPermissions.value;
+ if(splice[0] === ''){
+ splice.splice(0,1)
+ }
+ currentSalesperson.value.permissions = splice.join(",")
+ update(currentSalesperson.value).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鏉冮檺璁剧疆鎴愬姛')
+ permissionDialogVisible.value = false
+ getList()
+ }
+ })
+}
+
+const handleSubmit = () => {
+ formRef.value.validate((valid) => {
+ if (valid) {
+ if (isEdit.value) {
+ // 缂栬緫
+ update(form).then(res => {
+ if(res.code === 200){
+ ElMessage.success('缂栬緫鎴愬姛')
+ dialogVisible.value = false
+ getList()
+ }
+ })
+ } else {
+ add(form).then(res => {
+ if(res.code === 200){
+ ElMessage.success('鏂板鎴愬姛')
+ dialogVisible.value = false
+ getList()
+ }
+ })
+
+ }
+
+ }
+ })
+}
+
+const handleCurrentChange = (val) => {
+ pagination.value.currentPage = val.page
+ pagination.value.pageSize = val.limit
+}
+</script>
+
+<style scoped>
+.search-row {
+ margin-bottom: 20px;
+}
+</style>
diff --git a/src/views/salesManagement/strategyControl/index.vue b/src/views/salesManagement/strategyControl/index.vue
new file mode 100644
index 0000000..dbdae38
--- /dev/null
+++ b/src/views/salesManagement/strategyControl/index.vue
@@ -0,0 +1,1587 @@
+<template>
+ <div class="app-container strategy-control">
+ <el-tabs v-model="activeTab" type="border-card" class="main-tabs" @tab-change="handleTabChange">
+ <!-- 浠锋牸绛栫暐閰嶇疆 -->
+ <el-tab-pane label="浠锋牸绛栫暐閰嶇疆" name="priceStrategy">
+ <el-card class="box-card">
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-select v-model="priceSearchForm.customerName" placeholder="璇烽�夋嫨瀹㈡埛" clearable>
+ <el-option label="鍏ㄩ儴瀹㈡埛" value=""></el-option>
+ <el-option label="鍗庝笢寤烘潗闆嗗洟" value="鍗庝笢寤烘潗闆嗗洟"></el-option>
+ <el-option label="闀挎睙娣峰嚌鍦熷叕鍙�" value="闀挎睙娣峰嚌鍦熷叕鍙�"></el-option>
+ <el-option label="娴︽睙姘存偿鍒跺搧鍘�" value="娴︽睙姘存偿鍒跺搧鍘�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="priceSearchForm.productType" placeholder="璇烽�夋嫨姘存偿绫诲瀷" clearable>
+ <el-option label="鍏ㄩ儴绫诲瀷" value=""></el-option>
+ <el-option label="鏅�氱閰哥洂姘存偿" value="鏅�氱閰哥洂姘存偿"></el-option>
+ <el-option label="鐭挎福纭呴吀鐩愭按娉�" value="鐭挎福纭呴吀鐩愭按娉�"></el-option>
+ <el-option label="澶嶅悎纭呴吀鐩愭按娉�" value="澶嶅悎纭呴吀鐩愭按娉�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="priceSearchForm.strategyType" placeholder="绛栫暐绫诲瀷" clearable>
+ <el-option label="鍏ㄩ儴绛栫暐" value=""></el-option>
+ <el-option label="涓撳睘浠锋牸" value="涓撳睘浠锋牸"></el-option>
+ <el-option label="闃舵鎶ヤ环" value="闃舵鎶ヤ环"></el-option>
+ <el-option label="淇冮攢鎶樻墸" value="淇冮攢鎶樻墸"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="searchPriceStrategy">鏌ヨ</el-button>
+ <el-button @click="resetPriceSearch">閲嶇疆</el-button>
+ <el-button type="primary" @click="handleAddPriceStrategy">鏂板绛栫暐</el-button>
+ </el-col>
+ </el-row>
+
+ <el-table :data="priceStrategyList" border stripe v-loading="priceLoading" height="calc(100vh - 26em)">
+ <el-table-column prop="id" label="ID" width="60" align="center"/>
+ <el-table-column prop="strategyNo" label="绛栫暐缂栧彿" width="150"/>
+ <el-table-column prop="strategyType" label="绛栫暐绫诲瀷" width="100">
+ <template #default="scope">
+ <el-tag :type="getStrategyTypeColor(scope.row.strategyType)">
+ {{ scope.row.strategyType }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" width="180"/>
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" width="200"/>
+ <el-table-column prop="specification" label="瑙勬牸鍨嬪彿" width="120"/>
+ <el-table-column prop="basePrice" label="鍩虹浠锋牸" width="100">
+ <template #default="scope">
+ 楼{{ scope.row.basePrice }}/鍚�
+ </template>
+ </el-table-column>
+ <el-table-column prop="strategyPrice" label="绛栫暐浠锋牸" width="120">
+ <template #default="scope">
+ <span style="color: #f56c6c; font-weight: bold;">
+ {{ scope.row.strategyPrice }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="validPeriod" label="鏈夋晥鏈�" width="200">
+ <template #default="scope">
+ {{ scope.row.startDate }} 鑷� {{ scope.row.endDate }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="80">
+ <template #default="scope">
+ <el-tag :type="scope.row.status === '鐢熸晥涓�' ? 'success' : 'info'">
+ {{ scope.row.status }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="200" fixed="right" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleViewPriceStrategy(scope.row)" style="color: #67C23A">鏌ョ湅</el-button>
+ <el-button link type="primary" @click="handleEditPriceStrategy(scope.row)">缂栬緫</el-button>
+ <el-button link type="danger" @click="handleDeletePriceStrategy(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ :total="pricePagination.total"
+ :page="pricePagination.currentPage"
+ :limit="pricePagination.pageSize"
+ @pagination="handlePricePageChange"
+ />
+ </el-card>
+ </el-tab-pane>
+
+ <!-- 鍚堝悓鎵ц鐩戞帶 -->
+ <el-tab-pane label="鍚堝悓鎵ц鐩戞帶" name="contractMonitor">
+ <el-card class="box-card">
+ <!-- 缁熻姒傝 -->
+ <el-row :gutter="20" class="stats-row">
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #ecf5ff;">
+ <el-icon :size="30" color="#409eff"><Document /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ contractStats.totalContracts }}</div>
+ <div class="stat-label">鍚堝悓鎬绘暟</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #f0f9ff;">
+ <el-icon :size="30" color="#67c23a"><Van /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ contractStats.deliveryRate }}%</div>
+ <div class="stat-label">浜や粯瀹屾垚鐜�</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #fef0f0;">
+ <el-icon :size="30" color="#e6a23c"><Tickets /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ contractStats.invoiceRate }}%</div>
+ <div class="stat-label">鍙戠エ寮�鍏风巼</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #f4f4f5;">
+ <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ contractStats.paymentRate }}%</div>
+ <div class="stat-label">鍥炴瀹屾垚鐜�</div>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-input v-model="contractSearchForm.contractNo" placeholder="璇疯緭鍏ュ悎鍚岀紪鍙�" clearable>
+ <template #prefix>
+ <el-icon><Search /></el-icon>
+ </template>
+ </el-input>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="contractSearchForm.customerName" placeholder="璇烽�夋嫨瀹㈡埛" clearable>
+ <el-option label="鍗庝笢寤烘潗闆嗗洟" value="鍗庝笢寤烘潗闆嗗洟"></el-option>
+ <el-option label="闀挎睙娣峰嚌鍦熷叕鍙�" value="闀挎睙娣峰嚌鍦熷叕鍙�"></el-option>
+ <el-option label="娴︽睙姘存偿鍒跺搧鍘�" value="娴︽睙姘存偿鍒跺搧鍘�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="contractSearchForm.executionStatus" placeholder="鎵ц鐘舵��" clearable>
+ <el-option label="寰呮墽琛�" value="寰呮墽琛�"></el-option>
+ <el-option label="鎵ц涓�" value="鎵ц涓�"></el-option>
+ <el-option label="宸插畬鎴�" value="宸插畬鎴�"></el-option>
+ <el-option label="寮傚父" value="寮傚父"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-button type="primary" @click="searchContract">鏌ヨ</el-button>
+ <el-button @click="resetContractSearch">閲嶇疆</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鍚堝悓鍒楄〃 -->
+ <el-table :data="contractList" border stripe v-loading="contractLoading" height="calc(100vh - 36em)">
+ <el-table-column type="expand">
+ <template #default="scope">
+ <div class="contract-detail-expand">
+ <el-steps :active="getContractStep(scope.row)" align-center>
+ <el-step title="璁㈠崟纭" :description="scope.row.orderDate">
+ <template #icon>
+ <el-icon :color="scope.row.orderStatus === '宸插畬鎴�' ? '#67c23a' : '#909399'">
+ <Check v-if="scope.row.orderStatus === '宸插畬鎴�'" />
+ <Clock v-else />
+ </el-icon>
+ </template>
+ </el-step>
+ <el-step title="璐х墿浜や粯" :description="`${scope.row.deliveryProgress}%`">
+ <template #icon>
+ <el-icon :color="scope.row.deliveryProgress === 100 ? '#67c23a' : '#409eff'">
+ <Check v-if="scope.row.deliveryProgress === 100" />
+ <Van v-else />
+ </el-icon>
+ </template>
+ </el-step>
+ <el-step title="鍙戠エ寮�鍏�" :description="`${scope.row.invoiceProgress}%`">
+ <template #icon>
+ <el-icon :color="scope.row.invoiceProgress === 100 ? '#67c23a' : '#e6a23c'">
+ <Check v-if="scope.row.invoiceProgress === 100" />
+ <Tickets v-else />
+ </el-icon>
+ </template>
+ </el-step>
+ <el-step title="娆鹃」鏀跺洖" :description="`${scope.row.paymentProgress}%`">
+ <template #icon>
+ <el-icon :color="scope.row.paymentProgress === 100 ? '#67c23a' : '#f56c6c'">
+ <Check v-if="scope.row.paymentProgress === 100" />
+ <Wallet v-else />
+ </el-icon>
+ </template>
+ </el-step>
+ </el-steps>
+ </div>
+ </template>
+ </el-table-column>
+ <el-table-column prop="contractNo" label="鍚堝悓缂栧彿" width="150"/>
+ <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" width="180"/>
+ <el-table-column prop="contractAmount" label="鍚堝悓閲戦" width="120">
+ <template #default="scope">
+ 楼{{ scope.row.contractAmount.toLocaleString() }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="signDate" label="绛捐鏃ユ湡" width="120"/>
+ <el-table-column label="鎵ц杩涘害" width="150">
+ <template #default="scope">
+ <el-progress
+ :percentage="scope.row.executionProgress"
+ :color="getProgressColor(scope.row.executionProgress)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column prop="deliveryProgress" label="浜や粯杩涘害" width="100">
+ <template #default="scope">
+ {{ scope.row.deliveryProgress }}%
+ </template>
+ </el-table-column>
+ <el-table-column prop="invoiceProgress" label="寮�绁ㄨ繘搴�" width="100">
+ <template #default="scope">
+ {{ scope.row.invoiceProgress }}%
+ </template>
+ </el-table-column>
+ <el-table-column prop="paymentProgress" label="鍥炴杩涘害" width="100">
+ <template #default="scope">
+ {{ scope.row.paymentProgress }}%
+ </template>
+ </el-table-column>
+ <el-table-column prop="executionStatus" label="鎵ц鐘舵��" width="100">
+ <template #default="scope">
+ <el-tag :type="getExecutionStatusType(scope.row.executionStatus)">
+ {{ scope.row.executionStatus }}
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="120" fixed="right" align="center">
+ <template #default="scope">
+ <el-button link type="primary" @click="handleViewContract(scope.row)" style="color: #67C23A">鏌ョ湅璇︽儏</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ :total="contractPagination.total"
+ :page="contractPagination.currentPage"
+ :limit="contractPagination.pageSize"
+ @pagination="handleContractPageChange"
+ />
+ </el-card>
+ </el-tab-pane>
+
+ <!-- 鍘嗗彶姣斾环鍒嗘瀽 -->
+ <el-tab-pane label="鍘嗗彶姣斾环鍒嗘瀽" name="priceComparison">
+ <el-card class="box-card">
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-select v-model="compareSearchForm.productName" placeholder="璇烽�夋嫨浜у搧" clearable>
+ <el-option label="P.O 42.5鏅�氱閰哥洂姘存偿" value="P.O 42.5鏅�氱閰哥洂姘存偿"></el-option>
+ <el-option label="P.S 32.5鐭挎福纭呴吀鐩愭按娉�" value="P.S 32.5鐭挎福纭呴吀鐩愭按娉�"></el-option>
+ <el-option label="P.C 32.5澶嶅悎纭呴吀鐩愭按娉�" value="P.C 32.5澶嶅悎纭呴吀鐩愭按娉�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="8">
+ <el-date-picker
+ v-model="compareSearchForm.dateRange"
+ type="daterange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ value-format="YYYY-MM-DD"
+ style="width: 100%"
+ />
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="compareSearchForm.region" placeholder="閿�鍞尯鍩�" clearable>
+ <el-option label="鍗庝笢鍦板尯" value="鍗庝笢鍦板尯"></el-option>
+ <el-option label="鍗庡崡鍦板尯" value="鍗庡崡鍦板尯"></el-option>
+ <el-option label="鍗庡寳鍦板尯" value="鍗庡寳鍦板尯"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="4">
+ <el-button type="primary" @click="searchPriceComparison">鏌ヨ</el-button>
+ <el-button @click="resetCompareSearch">閲嶇疆</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 浠锋牸瓒嬪娍鍥� -->
+ <div class="chart-container">
+ <div ref="priceChartRef" style="width: 100%; height: 350px;"></div>
+ </div>
+
+ <!-- 鍘嗗彶浠锋牸鍒楄〃 -->
+ <el-table :data="priceComparisonList" border stripe v-loading="compareLoading" style="margin-top: 20px;">
+ <el-table-column prop="date" label="鏃ユ湡" width="120"/>
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" width="200"/>
+ <el-table-column prop="specification" label="瑙勬牸" width="120"/>
+ <el-table-column prop="customerName" label="瀹㈡埛" width="180"/>
+ <el-table-column prop="region" label="鍖哄煙" width="100"/>
+ <el-table-column prop="quantity" label="鏁伴噺(鍚�)" width="100" align="right">
+ <template #default="scope">
+ {{ scope.row.quantity.toLocaleString() }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="price" label="鎴愪氦鍗曚环" width="100">
+ <template #default="scope">
+ 楼{{ scope.row.price }}/鍚�
+ </template>
+ </el-table-column>
+ <el-table-column prop="totalAmount" label="鎴愪氦閲戦" width="120">
+ <template #default="scope">
+ 楼{{ scope.row.totalAmount.toLocaleString() }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="priceChange" label="浠锋牸鍙樺姩" width="100">
+ <template #default="scope">
+ <span :style="{ color: scope.row.priceChange > 0 ? '#f56c6c' : scope.row.priceChange < 0 ? '#67c23a' : '#909399' }">
+ {{ scope.row.priceChange > 0 ? '+' : '' }}{{ scope.row.priceChange }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="remark" label="澶囨敞" show-overflow-tooltip/>
+ </el-table>
+ </el-card>
+ </el-tab-pane>
+
+ <!-- 鍒╂鼎鍒嗘瀽 -->
+ <el-tab-pane label="鍒╂鼎鍒嗘瀽" name="profitAnalysis">
+ <el-card class="box-card">
+ <!-- 鍒╂鼎缁熻鍗$墖 -->
+ <el-row :gutter="20" class="profit-stats-row">
+ <el-col :span="8">
+ <div class="profit-card">
+ <div class="profit-header">鎬婚攢鍞</div>
+ <div class="profit-value">楼{{ profitStats.totalSales.toLocaleString() }}</div>
+ <div class="profit-footer">
+ <span>杈冧笂鏈�</span>
+ <span :class="profitStats.salesGrowth > 0 ? 'growth-up' : 'growth-down'">
+ {{ profitStats.salesGrowth > 0 ? '+' : '' }}{{ profitStats.salesGrowth }}%
+ </span>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="profit-card">
+ <div class="profit-header">鎬绘垚鏈�</div>
+ <div class="profit-value">楼{{ profitStats.totalCost.toLocaleString() }}</div>
+ <div class="profit-footer">
+ <span>鎴愭湰鐜�</span>
+ <span class="cost-rate">{{ profitStats.costRate }}%</span>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="8">
+ <div class="profit-card">
+ <div class="profit-header">姣涘埄娑�</div>
+ <div class="profit-value profit-highlight">楼{{ profitStats.grossProfit.toLocaleString() }}</div>
+ <div class="profit-footer">
+ <span>姣涘埄鐜�</span>
+ <span class="gross-profit-rate">{{ profitStats.grossProfitRate }}%</span>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-select v-model="profitSearchForm.productType" placeholder="浜у搧绫诲瀷" clearable>
+ <el-option label="鏅�氱閰哥洂姘存偿" value="鏅�氱閰哥洂姘存偿"></el-option>
+ <el-option label="鐭挎福纭呴吀鐩愭按娉�" value="鐭挎福纭呴吀鐩愭按娉�"></el-option>
+ <el-option label="澶嶅悎纭呴吀鐩愭按娉�" value="澶嶅悎纭呴吀鐩愭按娉�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="profitSearchForm.customerName" placeholder="瀹㈡埛鍚嶇О" clearable>
+ <el-option label="鍗庝笢寤烘潗闆嗗洟" value="鍗庝笢寤烘潗闆嗗洟"></el-option>
+ <el-option label="闀挎睙娣峰嚌鍦熷叕鍙�" value="闀挎睙娣峰嚌鍦熷叕鍙�"></el-option>
+ <el-option label="娴︽睙姘存偿鍒跺搧鍘�" value="娴︽睙姘存偿鍒跺搧鍘�"></el-option>
+ </el-select>
+ </el-col>
+ <el-col :span="8">
+ <el-date-picker
+ v-model="profitSearchForm.dateRange"
+ type="monthrange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫湀浠�"
+ end-placeholder="缁撴潫鏈堜唤"
+ value-format="YYYY-MM"
+ style="width: 100%"
+ />
+ </el-col>
+ <el-col :span="4">
+ <el-button type="primary" @click="searchProfit">鏌ヨ</el-button>
+ <el-button @click="resetProfitSearch">閲嶇疆</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鍒╂鼎鍒嗘瀽鍥捐〃 -->
+ <div class="chart-container">
+ <div ref="profitChartRef" style="width: 100%; height: 350px;"></div>
+ </div>
+
+ <!-- 鍒╂鼎鏄庣粏琛� -->
+ <el-table :data="profitAnalysisList" border stripe v-loading="profitLoading" style="margin-top: 20px;" show-summary :summary-method="getProfitSummary">
+ <el-table-column prop="orderNo" label="璁㈠崟缂栧彿" width="150"/>
+ <el-table-column prop="customerName" label="瀹㈡埛鍚嶇О" width="180"/>
+ <el-table-column prop="productName" label="浜у搧鍚嶇О" width="200"/>
+ <el-table-column prop="quantity" label="鏁伴噺(鍚�)" width="100" align="right">
+ <template #default="scope">
+ {{ scope.row.quantity.toLocaleString() }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="salesPrice" label="閿�鍞崟浠�" width="100">
+ <template #default="scope">
+ 楼{{ scope.row.salesPrice }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="costPrice" label="鎴愭湰鍗曚环" width="100">
+ <template #default="scope">
+ 楼{{ scope.row.costPrice }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="salesAmount" label="閿�鍞噾棰�" width="120" align="right">
+ <template #default="scope">
+ 楼{{ scope.row.salesAmount.toLocaleString() }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="costAmount" label="鎴愭湰閲戦" width="120" align="right">
+ <template #default="scope">
+ 楼{{ scope.row.costAmount.toLocaleString() }}
+ </template>
+ </el-table-column>
+ <el-table-column prop="grossProfit" label="姣涘埄娑�" width="120" align="right">
+ <template #default="scope">
+ <span :style="{ color: scope.row.grossProfit > 0 ? '#67c23a' : '#f56c6c', fontWeight: 'bold' }">
+ 楼{{ scope.row.grossProfit.toLocaleString() }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="grossProfitRate" label="姣涘埄鐜�" width="100">
+ <template #default="scope">
+ <el-tag :type="getProfitRateType(scope.row.grossProfitRate)">
+ {{ scope.row.grossProfitRate }}%
+ </el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column prop="orderDate" label="璁㈠崟鏃ユ湡" width="120"/>
+ </el-table>
+
+ <pagination
+ :total="profitPagination.total"
+ :page="profitPagination.currentPage"
+ :limit="profitPagination.pageSize"
+ @pagination="handleProfitPageChange"
+ />
+ </el-card>
+ </el-tab-pane>
+
+ <!-- 鎸囨爣缁熻锛堝缁村害閿�鍞垎鏋愶級 -->
+ <el-tab-pane label="鎸囨爣缁熻" name="indicatorStats">
+ <el-card class="box-card">
+ <!-- KPI 姹囨�� -->
+ <el-row :gutter="20" class="stats-row">
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #ecf5ff;">
+ <el-icon :size="30" color="#409eff"><Document /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ indicatorKpis.orderCount.toLocaleString() }}</div>
+ <div class="stat-label">璁㈠崟鏁伴噺</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #f0f9ff;">
+ <el-icon :size="30" color="#67c23a"><Tickets /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">楼{{ indicatorKpis.salesAmount.toLocaleString() }}</div>
+ <div class="stat-label">閿�鍞</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #fef0f0;">
+ <el-icon :size="30" color="#e6a23c"><Van /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ indicatorKpis.shipmentRate }}%</div>
+ <div class="stat-label">鍙戣揣鐜�</div>
+ </div>
+ </div>
+ </el-col>
+ <el-col :span="6">
+ <div class="stat-card">
+ <div class="stat-icon" style="background: #f4f4f5;">
+ <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon>
+ </div>
+ <div class="stat-content">
+ <div class="stat-value">{{ indicatorKpis.collectionRate }}%</div>
+ <div class="stat-label">鍥炴鐜�</div>
+ </div>
+ </div>
+ </el-col>
+ </el-row>
+
+ <!-- 缁村害绛涢�� -->
+ <el-row :gutter="20" class="search-row">
+ <el-col :span="6">
+ <el-select v-model="indicatorFilter.product" placeholder="浜у搧" clearable>
+ <el-option label="鍏ㄩ儴浜у搧" value="" />
+ <el-option label="P.O 42.5鏅�氱閰哥洂姘存偿" value="P.O 42.5鏅�氱閰哥洂姘存偿" />
+ <el-option label="P.S 32.5鐭挎福纭呴吀鐩愭按娉�" value="P.S 32.5鐭挎福纭呴吀鐩愭按娉�" />
+ <el-option label="P.C 32.5澶嶅悎纭呴吀鐩愭按娉�" value="P.C 32.5澶嶅悎纭呴吀鐩愭按娉�" />
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="indicatorFilter.customer" placeholder="瀹㈡埛" clearable>
+ <el-option label="鍏ㄩ儴瀹㈡埛" value="" />
+ <el-option label="鍗庝笢寤烘潗闆嗗洟" value="鍗庝笢寤烘潗闆嗗洟" />
+ <el-option label="闀挎睙娣峰嚌鍦熷叕鍙�" value="闀挎睙娣峰嚌鍦熷叕鍙�" />
+ <el-option label="娴︽睙姘存偿鍒跺搧鍘�" value="娴︽睙姘存偿鍒跺搧鍘�" />
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-select v-model="indicatorFilter.region" placeholder="鍖哄煙" clearable>
+ <el-option label="鍏ㄩ儴鍖哄煙" value="" />
+ <el-option label="鍗庝笢鍦板尯" value="鍗庝笢鍦板尯" />
+ <el-option label="鍗庡崡鍦板尯" value="鍗庡崡鍦板尯" />
+ <el-option label="鍗庡寳鍦板尯" value="鍗庡寳鍦板尯" />
+ </el-select>
+ </el-col>
+ <el-col :span="6">
+ <el-date-picker v-model="indicatorFilter.dateRange" type="daterange" range-separator="鑷�"
+ start-placeholder="寮�濮嬫棩鏈�" end-placeholder="缁撴潫鏃ユ湡" value-format="YYYY-MM-DD" style="width: 100%" />
+ </el-col>
+ <el-col :span="24" style="text-align: right; margin-top: 10px;">
+ <el-button type="primary" @click="applyIndicatorFilter">鏌ヨ</el-button>
+ <el-button @click="resetIndicatorFilter">閲嶇疆</el-button>
+ <el-button @click="exportIndicatorTable">瀵煎嚭鎶ヨ〃</el-button>
+ <el-button @click="exportIndicatorChart">瀵煎嚭鍥捐〃</el-button>
+ </el-col>
+ </el-row>
+
+ <!-- 鍥捐〃鍖� -->
+ <div class="chart-container">
+ <div ref="indicatorChartRef" style="width: 100%; height: 360px;"></div>
+ </div>
+
+ <!-- 涓氱哗缁熻锛堝洟闃熺淮搴︼紝鏃犱釜浜哄鍚嶏級 -->
+ <el-table :data="teamPerformanceList" border stripe style="margin-top: 20px;">
+ <el-table-column prop="team" label="閿�鍞洟闃�" width="140" />
+ <el-table-column prop="orderCount" label="璁㈠崟鏁�" width="100" />
+ <el-table-column prop="salesAmount" label="閿�鍞" width="140">
+ <template #default="scope">楼{{ scope.row.salesAmount.toLocaleString() }}</template>
+ </el-table-column>
+ <el-table-column prop="shipmentRate" label="鍙戣揣鐜�" width="100">
+ <template #default="scope">{{ scope.row.shipmentRate }}%</template>
+ </el-table-column>
+ <el-table-column prop="collectionRate" label="鍥炴鐜�" width="100">
+ <template #default="scope">{{ scope.row.collectionRate }}%</template>
+ </el-table-column>
+ <el-table-column prop="attainment" label="鐩爣杈炬垚鐜�" width="120">
+ <template #default="scope">
+ <el-tag :type="scope.row.attainment >= 100 ? 'success' : scope.row.attainment >= 80 ? 'warning' : 'danger'">
+ {{ scope.row.attainment }}%
+ </el-tag>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-card>
+ </el-tab-pane>
+ </el-tabs>
+
+ <!-- 浠锋牸绛栫暐瀵硅瘽妗� -->
+ <el-dialog v-model="priceStrategyDialogVisible" :title="priceStrategyDialogTitle" width="900px" :close-on-click-modal="false">
+ <el-form :model="priceStrategyForm" :rules="priceStrategyRules" ref="priceStrategyFormRef" label-width="120px">
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="绛栫暐绫诲瀷" prop="strategyType">
+ <el-select v-model="priceStrategyForm.strategyType" placeholder="璇烽�夋嫨绛栫暐绫诲瀷" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'">
+ <el-option label="涓撳睘浠锋牸" value="涓撳睘浠锋牸"></el-option>
+ <el-option label="闃舵鎶ヤ环" value="闃舵鎶ヤ环"></el-option>
+ <el-option label="淇冮攢鎶樻墸" value="淇冮攢鎶樻墸"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+ <el-select v-model="priceStrategyForm.customerName" placeholder="璇烽�夋嫨瀹㈡埛" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'">
+ <el-option label="鍗庝笢寤烘潗闆嗗洟" value="鍗庝笢寤烘潗闆嗗洟"></el-option>
+ <el-option label="闀挎睙娣峰嚌鍦熷叕鍙�" value="闀挎睙娣峰嚌鍦熷叕鍙�"></el-option>
+ <el-option label="娴︽睙姘存偿鍒跺搧鍘�" value="娴︽睙姘存偿鍒跺搧鍘�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="浜у搧鍚嶇О" prop="productName">
+ <el-select v-model="priceStrategyForm.productName" placeholder="璇烽�夋嫨浜у搧" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'">
+ <el-option label="P.O 42.5鏅�氱閰哥洂姘存偿" value="P.O 42.5鏅�氱閰哥洂姘存偿"></el-option>
+ <el-option label="P.S 32.5鐭挎福纭呴吀鐩愭按娉�" value="P.S 32.5鐭挎福纭呴吀鐩愭按娉�"></el-option>
+ <el-option label="P.C 32.5澶嶅悎纭呴吀鐩愭按娉�" value="P.C 32.5澶嶅悎纭呴吀鐩愭按娉�"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙勬牸鍨嬪彿" prop="specification">
+ <el-input v-model="priceStrategyForm.specification" placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�" :disabled="priceStrategyDialogMode === 'view'" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鍩虹浠锋牸(鍏�/鍚�)" prop="basePrice">
+ <el-input-number v-model="priceStrategyForm.basePrice" :min="0" :precision="2" style="width: 100%;" :disabled="priceStrategyDialogMode === 'view'" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="绛栫暐浠锋牸" prop="strategyPrice">
+ <el-input v-model="priceStrategyForm.strategyPrice" placeholder="濡�: 楼350/鍚� 鎴� 9鎶�" :disabled="priceStrategyDialogMode === 'view'" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row :gutter="20">
+ <el-col :span="12">
+ <el-form-item label="鐢熸晥鏃ユ湡" prop="startDate">
+ <el-date-picker
+ v-model="priceStrategyForm.startDate"
+ type="date"
+ placeholder="閫夋嫨鐢熸晥鏃ユ湡"
+ style="width: 100%"
+ value-format="YYYY-MM-DD"
+ :disabled="priceStrategyDialogMode === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="澶辨晥鏃ユ湡" prop="endDate">
+ <el-date-picker
+ v-model="priceStrategyForm.endDate"
+ type="date"
+ placeholder="閫夋嫨澶辨晥鏃ユ湡"
+ style="width: 100%"
+ value-format="YYYY-MM-DD"
+ :disabled="priceStrategyDialogMode === 'view'"
+ />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-form-item label="绛栫暐璇存槑" prop="description">
+ <el-input type="textarea" v-model="priceStrategyForm.description" :rows="3" placeholder="璇疯緭鍏ョ瓥鐣ヨ鏄�" :disabled="priceStrategyDialogMode === 'view'" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <el-button @click="priceStrategyDialogVisible = false">{{ priceStrategyDialogMode === 'view' ? '鍏抽棴' : '鍙栨秷' }}</el-button>
+ <el-button v-if="priceStrategyDialogMode !== 'view'" type="primary" @click="handleSavePriceStrategy">淇濆瓨</el-button>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick, watch } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Document, Van, Tickets, Wallet, Check, Clock, Search } from '@element-plus/icons-vue'
+import * as echarts from 'echarts'
+import Pagination from '@/components/PIMTable/Pagination.vue'
+
+// 娲诲姩鏍囩椤�
+const activeTab = ref('priceStrategy')
+
+// ========== 浠锋牸绛栫暐閰嶇疆 ==========
+const priceLoading = ref(false)
+const priceSearchForm = reactive({
+ customerName: '',
+ productType: '',
+ strategyType: ''
+})
+
+const priceStrategyList = ref([
+ {
+ id: 1,
+ strategyNo: 'PS202501001',
+ strategyType: '涓撳睘浠锋牸',
+ customerName: '鍗庝笢寤烘潗闆嗗洟',
+ productName: 'P.O 42.5鏅�氱閰哥洂姘存偿',
+ specification: '50kg/琚�',
+ basePrice: 380,
+ strategyPrice: '楼350/鍚�',
+ startDate: '2025-01-01',
+ endDate: '2025-12-31',
+ status: '鐢熸晥涓�',
+ description: '鎴樼暐鍚堜綔瀹㈡埛涓撳睘浼樻儬浠锋牸'
+ },
+ {
+ id: 2,
+ strategyNo: 'PS202501002',
+ strategyType: '闃舵鎶ヤ环',
+ customerName: '闀挎睙娣峰嚌鍦熷叕鍙�',
+ productName: 'P.S 32.5鐭挎福纭呴吀鐩愭按娉�',
+ specification: '50kg/琚�',
+ basePrice: 320,
+ strategyPrice: '500鍚ㄤ互涓�9鎶�',
+ startDate: '2025-01-01',
+ endDate: '2025-06-30',
+ status: '鐢熸晥涓�',
+ description: '澶ф壒閲忛噰璐樁姊紭鎯�'
+ },
+ {
+ id: 3,
+ strategyNo: 'PS202501003',
+ strategyType: '淇冮攢鎶樻墸',
+ customerName: '娴︽睙姘存偿鍒跺搧鍘�',
+ productName: 'P.C 32.5澶嶅悎纭呴吀鐩愭按娉�',
+ specification: '50kg/琚�',
+ basePrice: 300,
+ strategyPrice: '8.5鎶�',
+ startDate: '2025-01-15',
+ endDate: '2025-02-28',
+ status: '鐢熸晥涓�',
+ description: '鏄ヨ妭淇冮攢娲诲姩'
+ },
+ {
+ id: 4,
+ strategyNo: 'PS202412015',
+ strategyType: '涓撳睘浠锋牸',
+ customerName: '鍗庝笢寤烘潗闆嗗洟',
+ productName: 'P.C 32.5澶嶅悎纭呴吀鐩愭按娉�',
+ specification: '50kg/琚�',
+ basePrice: 300,
+ strategyPrice: '楼285/鍚�',
+ startDate: '2024-10-01',
+ endDate: '2024-12-31',
+ status: '宸茶繃鏈�',
+ description: '绗洓瀛e害涓撳睘浠锋牸'
+ }
+])
+
+const pricePagination = reactive({
+ total: 4,
+ currentPage: 1,
+ pageSize: 10
+})
+
+const priceStrategyDialogVisible = ref(false)
+const priceStrategyDialogTitle = ref('鏂板浠锋牸绛栫暐')
+const priceStrategyDialogMode = ref('add') // add | edit | view
+const priceStrategyForm = reactive({
+ strategyType: '',
+ customerName: '',
+ productName: '',
+ specification: '',
+ basePrice: 0,
+ strategyPrice: '',
+ startDate: '',
+ endDate: '',
+ description: ''
+})
+
+const priceStrategyRules = {
+ strategyType: [{ required: true, message: '璇烽�夋嫨绛栫暐绫诲瀷', trigger: 'change' }],
+ customerName: [{ required: true, message: '璇烽�夋嫨瀹㈡埛', trigger: 'change' }],
+ productName: [{ required: true, message: '璇烽�夋嫨浜у搧', trigger: 'change' }],
+ basePrice: [{ required: true, message: '璇疯緭鍏ュ熀纭�浠锋牸', trigger: 'blur' }],
+ strategyPrice: [{ required: true, message: '璇疯緭鍏ョ瓥鐣ヤ环鏍�', trigger: 'blur' }],
+ startDate: [{ required: true, message: '璇烽�夋嫨鐢熸晥鏃ユ湡', trigger: 'change' }],
+ endDate: [{ required: true, message: '璇烽�夋嫨澶辨晥鏃ユ湡', trigger: 'change' }]
+}
+
+const priceStrategyFormRef = ref()
+
+// ========== 鍚堝悓鎵ц鐩戞帶 ==========
+const contractLoading = ref(false)
+const contractStats = reactive({
+ totalContracts: 48,
+ deliveryRate: 87.5,
+ invoiceRate: 82.3,
+ paymentRate: 75.6
+})
+
+const contractSearchForm = reactive({
+ contractNo: '',
+ customerName: '',
+ executionStatus: ''
+})
+
+const contractList = ref([
+ {
+ id: 1,
+ contractNo: 'CT202501001',
+ customerName: '鍗庝笢寤烘潗闆嗗洟',
+ contractAmount: 2850000,
+ signDate: '2025-01-05',
+ executionProgress: 85,
+ deliveryProgress: 90,
+ invoiceProgress: 85,
+ paymentProgress: 75,
+ executionStatus: '鎵ц涓�',
+ orderStatus: '宸插畬鎴�',
+ orderDate: '2025-01-05'
+ },
+ {
+ id: 2,
+ contractNo: 'CT202501002',
+ customerName: '闀挎睙娣峰嚌鍦熷叕鍙�',
+ contractAmount: 1650000,
+ signDate: '2025-01-08',
+ executionProgress: 95,
+ deliveryProgress: 100,
+ invoiceProgress: 100,
+ paymentProgress: 85,
+ executionStatus: '鎵ц涓�',
+ orderStatus: '宸插畬鎴�',
+ orderDate: '2025-01-08'
+ },
+ {
+ id: 3,
+ contractNo: 'CT202501003',
+ customerName: '娴︽睙姘存偿鍒跺搧鍘�',
+ contractAmount: 980000,
+ signDate: '2025-01-12',
+ executionProgress: 60,
+ deliveryProgress: 65,
+ invoiceProgress: 60,
+ paymentProgress: 50,
+ executionStatus: '鎵ц涓�',
+ orderStatus: '宸插畬鎴�',
+ orderDate: '2025-01-12'
+ },
+ {
+ id: 4,
+ contractNo: 'CT202412028',
+ customerName: '鍗庝笢寤烘潗闆嗗洟',
+ contractAmount: 3200000,
+ signDate: '2024-12-15',
+ executionProgress: 100,
+ deliveryProgress: 100,
+ invoiceProgress: 100,
+ paymentProgress: 100,
+ executionStatus: '宸插畬鎴�',
+ orderStatus: '宸插畬鎴�',
+ orderDate: '2024-12-15'
+ },
+ {
+ id: 5,
+ contractNo: 'CT202501004',
+ customerName: '闀挎睙娣峰嚌鍦熷叕鍙�',
+ contractAmount: 750000,
+ signDate: '2025-01-20',
+ executionProgress: 25,
+ deliveryProgress: 30,
+ invoiceProgress: 20,
+ paymentProgress: 0,
+ executionStatus: '寮傚父',
+ orderStatus: '宸插畬鎴�',
+ orderDate: '2025-01-20'
+ }
+])
+
+const contractPagination = reactive({
+ total: 5,
+ currentPage: 1,
+ pageSize: 10
+})
+
+// ========== 鍘嗗彶姣斾环鍒嗘瀽 ==========
+const compareLoading = ref(false)
+const compareSearchForm = reactive({
+ productName: '',
+ dateRange: [],
+ region: ''
+})
+
+const priceComparisonList = ref([
+ { date: '2025-01-20', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', specification: '50kg/琚�', customerName: '鍗庝笢寤烘潗闆嗗洟', region: '鍗庝笢鍦板尯', quantity: 5000, price: 350, totalAmount: 1750000, priceChange: 0, remark: '闀挎湡鍚堜綔瀹㈡埛' },
+ { date: '2025-01-15', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', specification: '50kg/琚�', customerName: '娴︿笢鏂板尯寤虹瓚鍏徃', region: '鍗庝笢鍦板尯', quantity: 3000, price: 365, totalAmount: 1095000, priceChange: +15, remark: '鐜版鐜拌揣' },
+ { date: '2025-01-10', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', specification: '50kg/琚�', customerName: '闀挎睙娣峰嚌鍦熷叕鍙�', region: '鍗庝笢鍦板尯', quantity: 8000, price: 345, totalAmount: 2760000, priceChange: -5, remark: '澶ф壒閲忎紭鎯�' },
+ { date: '2025-01-05', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', specification: '50kg/琚�', customerName: '姹熻嫃宸ョ▼闆嗗洟', region: '鍗庝笢鍦板尯', quantity: 4500, price: 360, totalAmount: 1620000, priceChange: +10, remark: '宸ョ▼椤圭洰涓撶敤' },
+ { date: '2024-12-28', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', specification: '50kg/琚�', customerName: '鍗庝笢寤烘潗闆嗗洟', region: '鍗庝笢鍦板尯', quantity: 6000, price: 355, totalAmount: 2130000, priceChange: +5, remark: '骞村簳澶囪揣' },
+ { date: '2024-12-20', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', specification: '50kg/琚�', customerName: '涓婃捣甯傛斂宸ョ▼', region: '鍗庝笢鍦板尯', quantity: 10000, price: 340, totalAmount: 3400000, priceChange: -10, remark: '鏀垮簻椤圭洰' }
+])
+
+const priceChartRef = ref(null)
+let priceChart = null
+
+// ========== 鍒╂鼎鍒嗘瀽 ==========
+const profitLoading = ref(false)
+const profitStats = reactive({
+ totalSales: 15680000,
+ totalCost: 11256000,
+ grossProfit: 4424000,
+ grossProfitRate: 28.2,
+ salesGrowth: 12.5,
+ costRate: 71.8
+})
+
+const profitSearchForm = reactive({
+ productType: '',
+ customerName: '',
+ dateRange: []
+})
+
+const profitAnalysisList = ref([
+ { orderNo: 'SO202501015', customerName: '鍗庝笢寤烘潗闆嗗洟', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', quantity: 5000, salesPrice: 350, costPrice: 245, salesAmount: 1750000, costAmount: 1225000, grossProfit: 525000, grossProfitRate: 30.0, orderDate: '2025-01-20' },
+ { orderNo: 'SO202501012', customerName: '闀挎睙娣峰嚌鍦熷叕鍙�', productName: 'P.S 32.5鐭挎福纭呴吀鐩愭按娉�', quantity: 3500, salesPrice: 288, costPrice: 210, salesAmount: 1008000, costAmount: 735000, grossProfit: 273000, grossProfitRate: 27.1, orderDate: '2025-01-18' },
+ { orderNo: 'SO202501008', customerName: '娴︽睙姘存偿鍒跺搧鍘�', productName: 'P.C 32.5澶嶅悎纭呴吀鐩愭按娉�', quantity: 2800, salesPrice: 255, costPrice: 185, salesAmount: 714000, costAmount: 518000, grossProfit: 196000, grossProfitRate: 27.5, orderDate: '2025-01-15' },
+ { orderNo: 'SO202501005', customerName: '鍗庝笢寤烘潗闆嗗洟', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', quantity: 6000, salesPrice: 350, costPrice: 248, salesAmount: 2100000, costAmount: 1488000, grossProfit: 612000, grossProfitRate: 29.1, orderDate: '2025-01-10' },
+ { orderNo: 'SO202501003', customerName: '姹熻嫃宸ョ▼闆嗗洟', productName: 'P.O 42.5鏅�氱閰哥洂姘存偿', quantity: 4500, salesPrice: 360, costPrice: 250, salesAmount: 1620000, costAmount: 1125000, grossProfit: 495000, grossProfitRate: 30.6, orderDate: '2025-01-08' },
+ { orderNo: 'SO202412025', customerName: '闀挎睙娣峰嚌鍦熷叕鍙�', productName: 'P.S 32.5鐭挎福纭呴吀鐩愭按娉�', quantity: 8000, salesPrice: 290, costPrice: 215, salesAmount: 2320000, costAmount: 1720000, grossProfit: 600000, grossProfitRate: 25.9, orderDate: '2024-12-28' }
+])
+
+const profitPagination = reactive({
+ total: 6,
+ currentPage: 1,
+ pageSize: 10
+})
+
+const profitChartRef = ref(null)
+let profitChart = null
+
+// ========== 鏂规硶 ==========
+
+// 浠锋牸绛栫暐鐩稿叧鏂规硶
+const getStrategyTypeColor = (type) => {
+ const colorMap = {
+ '涓撳睘浠锋牸': 'success',
+ '闃舵鎶ヤ环': 'primary',
+ '淇冮攢鎶樻墸': 'warning'
+ }
+ return colorMap[type] || 'info'
+}
+
+const searchPriceStrategy = () => {
+ priceLoading.value = true
+ setTimeout(() => {
+ priceLoading.value = false
+ }, 500)
+}
+
+const resetPriceSearch = () => {
+ priceSearchForm.customerName = ''
+ priceSearchForm.productType = ''
+ priceSearchForm.strategyType = ''
+}
+
+const handleAddPriceStrategy = () => {
+ priceStrategyDialogTitle.value = '鏂板浠锋牸绛栫暐'
+ resetPriceStrategyForm()
+ priceStrategyDialogMode.value = 'add'
+ priceStrategyDialogVisible.value = true
+}
+
+const handleViewPriceStrategy = (row) => {
+ priceStrategyDialogTitle.value = '鏌ョ湅浠锋牸绛栫暐'
+ Object.assign(priceStrategyForm, row)
+ priceStrategyDialogMode.value = 'view'
+ priceStrategyDialogVisible.value = true
+}
+
+const handleEditPriceStrategy = (row) => {
+ priceStrategyDialogTitle.value = '缂栬緫浠锋牸绛栫暐'
+ Object.assign(priceStrategyForm, row)
+ priceStrategyDialogMode.value = 'edit'
+ priceStrategyDialogVisible.value = true
+}
+
+const handleDeletePriceStrategy = (row) => {
+ ElMessageBox.confirm('纭鍒犻櫎璇ヤ环鏍肩瓥鐣ュ悧锛�', '鎻愮ず', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning'
+ }).then(() => {
+ // 鏈湴鍋囨暟鎹垹闄わ細浠庡垪琛ㄤ腑绉婚櫎骞舵洿鏂版�绘暟
+ const index = priceStrategyList.value.findIndex(item => item.id === row.id)
+ if (index > -1) {
+ priceStrategyList.value.splice(index, 1)
+ if (pricePagination.total > 0) pricePagination.total -= 1
+ }
+ ElMessage.success('鍒犻櫎鎴愬姛')
+ })
+}
+
+const resetPriceStrategyForm = () => {
+ Object.keys(priceStrategyForm).forEach(key => {
+ if (key === 'basePrice') {
+ priceStrategyForm[key] = 0
+ } else {
+ priceStrategyForm[key] = ''
+ }
+ })
+}
+
+const handleSavePriceStrategy = () => {
+ priceStrategyFormRef.value.validate((valid) => {
+ if (valid) {
+ ElMessage.success('淇濆瓨鎴愬姛')
+ priceStrategyDialogVisible.value = false
+ }
+ })
+}
+
+const handlePricePageChange = (val) => {
+ pricePagination.currentPage = val.page
+ pricePagination.pageSize = val.limit
+}
+
+// 鍚堝悓鎵ц鐩戞帶鐩稿叧鏂规硶
+const getExecutionStatusType = (status) => {
+ const statusMap = {
+ '寰呮墽琛�': 'info',
+ '鎵ц涓�': 'primary',
+ '宸插畬鎴�': 'success',
+ '寮傚父': 'danger'
+ }
+ return statusMap[status] || 'info'
+}
+
+const getProgressColor = (percentage) => {
+ if (percentage < 30) return '#f56c6c'
+ if (percentage < 70) return '#e6a23c'
+ return '#67c23a'
+}
+
+const getContractStep = (row) => {
+ if (row.paymentProgress === 100) return 4
+ if (row.invoiceProgress === 100) return 3
+ if (row.deliveryProgress === 100) return 2
+ if (row.orderStatus === '宸插畬鎴�') return 1
+ return 0
+}
+
+const searchContract = () => {
+ contractLoading.value = true
+ setTimeout(() => {
+ contractLoading.value = false
+ }, 500)
+}
+
+const resetContractSearch = () => {
+ contractSearchForm.contractNo = ''
+ contractSearchForm.customerName = ''
+ contractSearchForm.executionStatus = ''
+}
+
+const handleViewContract = (row) => {
+ ElMessage.info('鏌ョ湅鍚堝悓璇︽儏: ' + row.contractNo)
+}
+
+const handleContractPageChange = (val) => {
+ contractPagination.currentPage = val.page
+ contractPagination.pageSize = val.limit
+}
+
+// 鍘嗗彶姣斾环鍒嗘瀽鐩稿叧鏂规硶
+const searchPriceComparison = () => {
+ compareLoading.value = true
+ setTimeout(() => {
+ compareLoading.value = false
+ initPriceChart()
+ }, 500)
+}
+
+const resetCompareSearch = () => {
+ compareSearchForm.productName = ''
+ compareSearchForm.dateRange = []
+ compareSearchForm.region = ''
+}
+
+const initPriceChart = () => {
+ if (!priceChartRef.value) return
+
+ if (priceChart) {
+ priceChart.dispose()
+ }
+
+ priceChart = echarts.init(priceChartRef.value)
+
+ const option = {
+ title: {
+ text: '姘存偿浠锋牸瓒嬪娍鍒嗘瀽',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis',
+ formatter: '{b}<br/>{a}: 楼{c}/鍚�'
+ },
+ legend: {
+ data: ['P.O 42.5鏅�氱閰哥洂姘存偿'],
+ top: 30
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
+ },
+ xAxis: {
+ type: 'category',
+ boundaryGap: false,
+ data: ['2024-12-20', '2024-12-28', '2025-01-05', '2025-01-10', '2025-01-15', '2025-01-20']
+ },
+ yAxis: {
+ type: 'value',
+ name: '浠锋牸(鍏�/鍚�)',
+ min: 330,
+ max: 370
+ },
+ series: [
+ {
+ name: 'P.O 42.5鏅�氱閰哥洂姘存偿',
+ type: 'line',
+ data: [340, 355, 360, 345, 365, 350],
+ smooth: true,
+ itemStyle: {
+ color: '#409eff'
+ },
+ areaStyle: {
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+ { offset: 0, color: 'rgba(64, 158, 255, 0.3)' },
+ { offset: 1, color: 'rgba(64, 158, 255, 0.1)' }
+ ])
+ }
+ }
+ ]
+ }
+
+ priceChart.setOption(option)
+}
+
+// 鍒╂鼎鍒嗘瀽鐩稿叧鏂规硶
+const searchProfit = () => {
+ profitLoading.value = true
+ setTimeout(() => {
+ profitLoading.value = false
+ initProfitChart()
+ }, 500)
+}
+
+const resetProfitSearch = () => {
+ profitSearchForm.productType = ''
+ profitSearchForm.customerName = ''
+ profitSearchForm.dateRange = []
+}
+
+const getProfitRateType = (rate) => {
+ if (rate >= 30) return 'success'
+ if (rate >= 25) return 'warning'
+ return 'danger'
+}
+
+const getProfitSummary = (param) => {
+ const { columns, data } = param
+ const sums = []
+ columns.forEach((column, index) => {
+ if (index === 0) {
+ sums[index] = '鍚堣'
+ return
+ }
+ if (['quantity', 'salesAmount', 'costAmount', 'grossProfit'].includes(column.property)) {
+ const values = data.map(item => Number(item[column.property]))
+ if (!values.every(value => isNaN(value))) {
+ const total = values.reduce((prev, curr) => {
+ const value = Number(curr)
+ if (!isNaN(value)) {
+ return prev + curr
+ } else {
+ return prev
+ }
+ }, 0)
+ sums[index] = column.property === 'quantity' ? total.toLocaleString() : '楼' + total.toLocaleString()
+ }
+ } else if (column.property === 'grossProfitRate') {
+ // 璁$畻骞冲潎姣涘埄鐜�
+ const totalSales = data.reduce((sum, item) => sum + item.salesAmount, 0)
+ const totalProfit = data.reduce((sum, item) => sum + item.grossProfit, 0)
+ sums[index] = ((totalProfit / totalSales) * 100).toFixed(1) + '%'
+ }
+ })
+ return sums
+}
+
+const initProfitChart = () => {
+ if (!profitChartRef.value) return
+
+ if (profitChart) {
+ profitChart.dispose()
+ }
+
+ profitChart = echarts.init(profitChartRef.value)
+
+ const option = {
+ title: {
+ text: '閿�鍞笌鍒╂鼎瓒嬪娍鍒嗘瀽',
+ left: 'center'
+ },
+ tooltip: {
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ crossStyle: {
+ color: '#999'
+ }
+ }
+ },
+ legend: {
+ data: ['閿�鍞噾棰�', '鎴愭湰閲戦', '姣涘埄娑�', '姣涘埄鐜�'],
+ top: 30
+ },
+ grid: {
+ left: '3%',
+ right: '4%',
+ bottom: '3%',
+ containLabel: true
+ },
+ xAxis: [
+ {
+ type: 'category',
+ data: ['2024-12', '2025-01'],
+ axisPointer: {
+ type: 'shadow'
+ }
+ }
+ ],
+ yAxis: [
+ {
+ type: 'value',
+ name: '閲戦(涓囧厓)',
+ axisLabel: {
+ formatter: '{value}'
+ }
+ },
+ {
+ type: 'value',
+ name: '姣涘埄鐜�(%)',
+ min: 0,
+ max: 40,
+ axisLabel: {
+ formatter: '{value}%'
+ }
+ }
+ ],
+ series: [
+ {
+ name: '閿�鍞噾棰�',
+ type: 'bar',
+ data: [820, 950],
+ itemStyle: {
+ color: '#409eff'
+ }
+ },
+ {
+ name: '鎴愭湰閲戦',
+ type: 'bar',
+ data: [605, 670],
+ itemStyle: {
+ color: '#e6a23c'
+ }
+ },
+ {
+ name: '姣涘埄娑�',
+ type: 'bar',
+ data: [215, 280],
+ itemStyle: {
+ color: '#67c23a'
+ }
+ },
+ {
+ name: '姣涘埄鐜�',
+ type: 'line',
+ yAxisIndex: 1,
+ data: [26.2, 29.5],
+ itemStyle: {
+ color: '#f56c6c'
+ }
+ }
+ ]
+ }
+
+ profitChart.setOption(option)
+}
+
+const handleProfitPageChange = (val) => {
+ profitPagination.currentPage = val.page
+ profitPagination.pageSize = val.limit
+}
+
+// ========== 鎸囨爣缁熻锛堝缁村害鍒嗘瀽锛� ==========
+const indicatorKpis = reactive({
+ orderCount: 1280,
+ salesAmount: 9650000,
+ shipmentRate: 89.2,
+ collectionRate: 76.4
+})
+
+const indicatorFilter = reactive({
+ product: '',
+ customer: '',
+ region: '',
+ dateRange: []
+})
+
+const indicatorChartRef = ref(null)
+let indicatorChart = null
+
+const teamPerformanceList = ref([
+ { team: '鍗庝笢鍥㈤槦A', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, collectionRate: 80, attainment: 105 },
+ { team: '鍗庡寳鍥㈤槦B', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, collectionRate: 73, attainment: 92 },
+ { team: '鍗庡崡鍥㈤槦C', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, collectionRate: 70, attainment: 78 },
+ { team: '瑗垮崡鍥㈤槦D', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, collectionRate: 68, attainment: 74 }
+])
+
+const initIndicatorChart = () => {
+ if (!indicatorChartRef.value) return
+ if (indicatorChart) indicatorChart.dispose()
+ indicatorChart = echarts.init(indicatorChartRef.value)
+ const option = {
+ title: { text: '澶氱淮搴﹂攢鍞寚鏍囪秼鍔�', left: 'center' },
+ tooltip: { trigger: 'axis' },
+ legend: { data: ['璁㈠崟鏁�', '閿�鍞', '鍙戣揣鐜�', '鍥炴鐜�'], top: 30 },
+ grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
+ xAxis: { type: 'category', data: ['2024-12', '2025-01', '2025-02', '2025-03', '2025-04', '2025-05'] },
+ yAxis: [
+ { type: 'value', name: '鏁伴噺/閲戦', axisLabel: { formatter: '{value}' } },
+ { type: 'value', name: '姣斾緥(%)', min: 0, max: 100, axisLabel: { formatter: '{value}%' } }
+ ],
+ series: [
+ { name: '璁㈠崟鏁�', type: 'bar', data: [180, 220, 210, 260, 205, 225], itemStyle: { color: '#409eff' } },
+ { name: '閿�鍞', type: 'bar', data: [820, 950, 910, 1080, 980, 1020], itemStyle: { color: '#67c23a' } },
+ { name: '鍙戣揣鐜�', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } },
+ { name: '鍥炴鐜�', type: 'line', yAxisIndex: 1, data: [72, 76, 74, 79, 75, 78], itemStyle: { color: '#f56c6c' } }
+ ]
+ }
+ indicatorChart.setOption(option)
+}
+
+const applyIndicatorFilter = () => {
+ // 浣跨敤鍋囨暟鎹ā鎷熸煡璇紝鍒锋柊KPI鍜屽浘琛�
+ // 浠呮紨绀猴細闅忔満寰皟浠ヤ綋鐜扮瓫閫夋晥鏋�
+ const random = (base, delta) => {
+ const v = base + Math.round((Math.random() - 0.5) * delta)
+ return v < 0 ? 0 : v
+ }
+ indicatorKpis.orderCount = random(1280, 120)
+ indicatorKpis.salesAmount = random(9650000, 350000)
+ indicatorKpis.shipmentRate = (85 + Math.random() * 10).toFixed(1) * 1
+ indicatorKpis.collectionRate = (70 + Math.random() * 12).toFixed(1) * 1
+
+ setTimeout(() => initIndicatorChart(), 200)
+}
+
+const resetIndicatorFilter = () => {
+ indicatorFilter.product = ''
+ indicatorFilter.customer = ''
+ indicatorFilter.region = ''
+ indicatorFilter.dateRange = []
+ applyIndicatorFilter()
+}
+
+const exportIndicatorTable = () => {
+ // 瀵煎嚭鍥㈤槦涓氱哗涓篊SV锛堝亣瀵煎嚭锛�
+ const header = ['閿�鍞洟闃�', '璁㈠崟鏁�', '閿�鍞', '鍙戣揣鐜�(%)', '鍥炴鐜�(%)', '鐩爣杈炬垚鐜�(%)']
+ const rows = teamPerformanceList.value.map(r => [
+ r.team,
+ r.orderCount,
+ r.salesAmount,
+ r.shipmentRate,
+ r.collectionRate,
+ r.attainment
+ ])
+ const csv = [header, ...rows].map(r => r.join(',')).join('\n')
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
+ const url = URL.createObjectURL(blob)
+ const link = document.createElement('a')
+ link.href = url
+ link.download = '鎸囨爣缁熻-鍥㈤槦涓氱哗.csv'
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+ URL.revokeObjectURL(url)
+}
+
+const exportIndicatorChart = () => {
+ if (!indicatorChart) return
+ const url = indicatorChart.getDataURL({ type: 'png', pixelRatio: 2, backgroundColor: '#fff' })
+ const link = document.createElement('a')
+ link.href = url
+ link.download = '鎸囨爣缁熻-鍥捐〃.png'
+ document.body.appendChild(link)
+ link.click()
+ document.body.removeChild(link)
+}
+
+// 鐢熷懡鍛ㄦ湡
+onMounted(() => {
+ // 缁勪欢鎸傝浇鍚庝笉绔嬪嵆鍒濆鍖栧浘琛紝绛夊緟鐢ㄦ埛鍒囨崲鍒板搴旀爣绛鹃〉
+})
+
+// 鐩戝惉鏍囩椤靛垏鎹�
+watch(activeTab, (newVal) => {
+ nextTick(() => {
+ if (newVal === 'priceComparison') {
+ initPriceChart()
+ } else if (newVal === 'profitAnalysis') {
+ initProfitChart()
+ } else if (newVal === 'indicatorStats') {
+ initIndicatorChart()
+ }
+ })
+})
+
+const handleTabChange = () => {
+ // 鏍囩椤靛垏鎹㈠鐞�
+}
+</script>
+
+<style scoped>
+.strategy-control {
+ padding: 0;
+}
+
+.main-tabs {
+ border: none;
+ box-shadow: none;
+}
+
+.main-tabs :deep(.el-tabs__content) {
+ padding: 0;
+}
+
+.box-card {
+ border: none;
+ box-shadow: none;
+}
+
+.search-row {
+ margin-bottom: 20px;
+}
+
+/* 缁熻鍗$墖鏍峰紡 */
+.stats-row {
+ margin-bottom: 24px;
+}
+
+.stat-card {
+ display: flex;
+ align-items: center;
+ padding: 20px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+
+.stat-icon {
+ width: 60px;
+ height: 60px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 8px;
+ margin-right: 16px;
+}
+
+.stat-content {
+ flex: 1;
+}
+
+.stat-value {
+ font-size: 28px;
+ font-weight: bold;
+ color: #303133;
+ margin-bottom: 4px;
+}
+
+.stat-label {
+ font-size: 14px;
+ color: #909399;
+}
+
+/* 鍚堝悓璇︽儏灞曞紑鏍峰紡 */
+.contract-detail-expand {
+ padding: 30px 60px;
+ background: #f5f7fa;
+}
+
+.contract-detail-expand :deep(.el-step__title) {
+ font-size: 14px;
+}
+
+.contract-detail-expand :deep(.el-step__description) {
+ font-size: 12px;
+ margin-top: 4px;
+}
+
+/* 鍒╂鼎缁熻鍗$墖 */
+.profit-stats-row {
+ margin-bottom: 24px;
+}
+
+.profit-card {
+ padding: 24px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 12px;
+ color: #fff;
+ box-shadow: 0 4px 20px rgba(102, 126, 234, 0.4);
+}
+
+.profit-card:nth-child(2) .profit-card {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.profit-card:nth-child(3) .profit-card {
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+}
+
+.profit-header {
+ font-size: 14px;
+ opacity: 0.9;
+ margin-bottom: 12px;
+}
+
+.profit-value {
+ font-size: 32px;
+ font-weight: bold;
+ margin-bottom: 12px;
+}
+
+.profit-highlight {
+ color: #fff;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+.profit-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 13px;
+ opacity: 0.9;
+}
+
+.growth-up {
+ color: #fff;
+ font-weight: bold;
+}
+
+.growth-down {
+ color: #ffd04b;
+ font-weight: bold;
+}
+
+.cost-rate, .gross-profit-rate {
+ font-weight: bold;
+}
+
+/* 鍥捐〃瀹瑰櫒 */
+.chart-container {
+ margin: 20px 0;
+ padding: 20px;
+ background: #fff;
+ border-radius: 8px;
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+</style>
+
diff --git a/src/views/system/appVersion/index.vue b/src/views/system/appVersion/index.vue
new file mode 100644
index 0000000..6ff9ae6
--- /dev/null
+++ b/src/views/system/appVersion/index.vue
@@ -0,0 +1,270 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Upload" @click="openUploadDialog">涓婁紶APK</el-button>
+ </el-col>
+ </el-row>
+
+ <el-table v-loading="loading" :data="versionList">
+ <el-table-column label="ID" prop="id" align="center" width="80"/>
+ <el-table-column label="搴旂敤鍚嶇О" prop="name" align="center" min-width="150"/>
+ <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>
+ </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>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓浜�" prop="createUser" align="center" width="100"/>
+ <el-table-column label="鎿嶄綔" align="center" width="180" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" @click="downloadAttachment(scope.row)">涓嬭浇</el-button>
+ <el-button link type="success" @click="openQrDialog(scope.row)">鎵爜涓嬭浇</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.current"
+ v-model:limit="queryParams.size"
+ @pagination="getList"
+ />
+
+ <el-dialog title="涓婁紶APK" v-model="uploadOpen" width="560px" append-to-body @close="resetUploadForm">
+ <el-form ref="uploadRef" :model="uploadForm" :rules="uploadRules" label-width="90px">
+ <el-form-item label="搴旂敤鍚嶇О" prop="name">
+ <el-input v-model="uploadForm.name" placeholder="璇疯緭鍏ュ簲鐢ㄥ悕绉�"/>
+ </el-form-item>
+ <el-form-item label="鐗堟湰鍙�" prop="version">
+ <el-input v-model="uploadForm.version" placeholder="璇疯緭鍏ョ増鏈彿"/>
+ </el-form-item>
+ <el-form-item label="APK鏂囦欢" prop="storageBlobDTOList">
+ <FileUpload v-model:file-list="uploadForm.storageBlobDTOList" :limit="1" :file-type="['apk']"
+ :file-size="200"/>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" :loading="uploading" @click="submitUpload">纭� 瀹�</el-button>
+ <el-button @click="uploadOpen = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <el-dialog
+ v-model="qrOpen"
+ title="鎵爜涓嬭浇"
+ width="460px"
+ append-to-body
+ class="download-qr-dialog"
+ >
+ <div class="download-qr-content">
+ <div class="app-meta-card">
+ <div class="meta-row">
+ <span class="meta-label">搴旂敤鍚嶇О</span>
+ <span class="meta-value">{{ qrCurrentRow?.name || "-" }}</span>
+ </div>
+ <div class="meta-row">
+ <span class="meta-label">鐗堟湰缂栧彿</span>
+ <span class="meta-value">{{ qrCurrentRow?.version || "-" }}</span>
+ </div>
+ </div>
+ <div class="qr-box">
+ <img v-if="qrCodeUrl" :src="qrCodeUrl" alt="download qr code" class="qr-image" />
+ <div class="qr-tip">璇蜂娇鐢ㄦ墜鏈烘壂鐮佷笅杞藉簲鐢�</div>
+ </div>
+ </div>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button @click="qrOpen = false">鍏� 闂�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="SystemAppVersion">
+import {listAppVersion, add} from "@/api/system/appVersion";
+import FileUpload from "@/components/AttachmentUpload/file/index.vue";
+import QRCode from "qrcode";
+
+const {proxy} = getCurrentInstance();
+
+const loading = ref(false);
+const versionList = ref([]);
+const total = ref(0);
+
+const queryParams = reactive({
+ current: 1,
+ size: 10,
+});
+
+const uploadOpen = ref(false);
+const uploading = ref(false);
+const qrOpen = ref(false);
+const qrCodeUrl = ref("");
+const qrCurrentRow = ref(null);
+const uploadForm = reactive({
+ name: "",
+ version: "",
+ storageBlobDTOList: null,
+});
+
+const uploadRules = {
+ name: [{required: true, message: "璇疯緭鍏ュ簲鐢ㄥ悕绉�", trigger: "blur"}],
+ version: [{required: true, message: "璇疯緭鍏ョ増鏈彿", trigger: "blur"}],
+ storageBlobDTOList: [{required: true, message: "璇蜂笂浼燗PK鏂囦欢", trigger: "change"}],
+};
+
+function normalizeListResp(res) {
+ const data = res?.data || {};
+ const records = data.records || res?.rows || [];
+ const totalNum = Number(data.total ?? res?.total ?? 0);
+ return {
+ records: Array.isArray(records) ? records : [],
+ total: Number.isNaN(totalNum) ? 0 : totalNum,
+ };
+}
+
+function getList() {
+ loading.value = true;
+ listAppVersion(queryParams)
+ .then(res => {
+ const result = normalizeListResp(res);
+ versionList.value = result.records;
+ total.value = result.total;
+ })
+ .finally(() => {
+ loading.value = false;
+ });
+}
+
+function openUploadDialog() {
+ resetUploadForm();
+ uploadOpen.value = true;
+}
+
+function resetUploadForm() {
+ uploadForm.name = "";
+ uploadForm.version = "";
+ uploadForm.storageBlobDTOList = null;
+ proxy.resetForm("uploadRef");
+}
+
+
+function downloadAttachment(row) {
+ window.open(row.downloadURL, "_blank");
+}
+
+async function openQrDialog(row) {
+ if (!row?.downloadURL) {
+ proxy.$modal.msgError("褰撳墠璁板綍缂哄皯涓嬭浇鍦板潃锛屾棤娉曠敓鎴愪簩缁寸爜");
+ return;
+ }
+ try {
+ qrCodeUrl.value = await QRCode.toDataURL(row.downloadURL, {
+ width: 220,
+ margin: 2,
+ errorCorrectionLevel: "M",
+ color: {
+ dark: "#1f2937",
+ light: "#ffffff",
+ },
+ });
+ qrCurrentRow.value = row;
+ qrOpen.value = true;
+ } catch (error) {
+ proxy.$modal.msgError("浜岀淮鐮佺敓鎴愬け璐ワ紝璇风◢鍚庨噸璇�");
+ }
+}
+
+function submitUpload() {
+ proxy.$refs.uploadRef.validate(valid => {
+ if (!valid) return;
+ uploading.value = true;
+ add(uploadForm)
+ .then(() => {
+ proxy.$modal.msgSuccess("涓婁紶鎴愬姛");
+ uploadOpen.value = false;
+ getList();
+ })
+ .finally(() => {
+ uploading.value = false;
+ });
+ });
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
+
+<style scoped>
+.download-qr-content {
+ padding: 4px 4px 8px;
+}
+
+.app-meta-card {
+ border-radius: 10px;
+ padding: 12px 14px;
+ margin-bottom: 16px;
+ background: linear-gradient(135deg, #f0f7ff 0%, #ecfdf5 100%);
+ border: 1px solid #dbeafe;
+}
+
+.meta-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ line-height: 26px;
+}
+
+.meta-row + .meta-row {
+ margin-top: 6px;
+}
+
+.meta-label {
+ font-size: 13px;
+ color: #64748b;
+}
+
+.meta-value {
+ max-width: 260px;
+ font-size: 14px;
+ color: #0f172a;
+ font-weight: 600;
+ word-break: break-all;
+ text-align: right;
+}
+
+.qr-box {
+ border: 1px solid #e2e8f0;
+ border-radius: 12px;
+ padding: 18px 12px 14px;
+ text-align: center;
+ background: #ffffff;
+ box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
+}
+
+.qr-image {
+ width: 220px;
+ height: 220px;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+ padding: 8px;
+ background: #fff;
+}
+
+.qr-tip {
+ margin-top: 10px;
+ font-size: 13px;
+ color: #64748b;
+}
+</style>
diff --git a/src/views/system/config/index.vue b/src/views/system/config/index.vue
new file mode 100644
index 0000000..77d9f5a
--- /dev/null
+++ b/src/views/system/config/index.vue
@@ -0,0 +1,316 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="鍙傛暟鍚嶇О" prop="configName">
+ <el-input
+ v-model="queryParams.configName"
+ placeholder="璇疯緭鍏ュ弬鏁板悕绉�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍙傛暟閿悕" prop="configKey">
+ <el-input
+ v-model="queryParams.configKey"
+ placeholder="璇疯緭鍏ュ弬鏁伴敭鍚�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绯荤粺鍐呯疆" prop="configType">
+ <el-select v-model="queryParams.configType" placeholder="绯荤粺鍐呯疆" clearable style="width: 240px">
+ <el-option
+ v-for="dict in sys_yes_no"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" style="width: 308px;">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:config:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:config:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:config:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['system:config:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Refresh"
+ @click="handleRefreshCache"
+ v-hasPermi="['system:config:remove']"
+ >鍒锋柊缂撳瓨</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="configList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鍙傛暟涓婚敭" align="center" prop="configId" />
+ <el-table-column label="鍙傛暟鍚嶇О" align="center" prop="configName" :show-overflow-tooltip="true" />
+ <el-table-column label="鍙傛暟閿悕" align="center" prop="configKey" :show-overflow-tooltip="true" />
+ <el-table-column label="鍙傛暟閿��" align="center" prop="configValue" :show-overflow-tooltip="true" />
+ <el-table-column label="绯荤粺鍐呯疆" align="center" prop="configType">
+ <template #default="scope">
+ <dict-tag :options="sys_yes_no" :value="scope.row.configType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="150" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:config:edit']" >淇敼</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:config:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀瑰弬鏁伴厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="configRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="鍙傛暟鍚嶇О" prop="configName">
+ <el-input v-model="form.configName" placeholder="璇疯緭鍏ュ弬鏁板悕绉�" />
+ </el-form-item>
+ <el-form-item label="鍙傛暟閿悕" prop="configKey">
+ <el-input v-model="form.configKey" placeholder="璇疯緭鍏ュ弬鏁伴敭鍚�" />
+ </el-form-item>
+ <el-form-item label="鍙傛暟閿��" prop="configValue">
+ <el-input v-model="form.configValue" type="textarea" placeholder="璇疯緭鍏ュ弬鏁伴敭鍊�" />
+ </el-form-item>
+ <el-form-item label="绯荤粺鍐呯疆" prop="configType">
+ <el-radio-group v-model="form.configType">
+ <el-radio
+ v-for="dict in sys_yes_no"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Config">
+import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from "@/api/system/config"
+
+const { proxy } = getCurrentInstance()
+const { sys_yes_no } = proxy.useDict("sys_yes_no")
+
+const configList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const dateRange = ref([])
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ configName: undefined,
+ configKey: undefined,
+ configType: undefined
+ },
+ rules: {
+ configName: [{ required: true, message: "鍙傛暟鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ configKey: [{ required: true, message: "鍙傛暟閿悕涓嶈兘涓虹┖", trigger: "blur" }],
+ configValue: [{ required: true, message: "鍙傛暟閿�间笉鑳戒负绌�", trigger: "blur" }]
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ鍙傛暟鍒楄〃 */
+function getList() {
+ loading.value = true
+ listConfig(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
+ configList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ configId: undefined,
+ configName: undefined,
+ configKey: undefined,
+ configValue: undefined,
+ configType: "Y",
+ remark: undefined
+ }
+ proxy.resetForm("configRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.configId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞鍙傛暟"
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const configId = row.configId || ids.value
+ getConfig(configId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼鍙傛暟"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["configRef"].validate(valid => {
+ if (valid) {
+ if (form.value.configId != undefined) {
+ updateConfig(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addConfig(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const configIds = row.configId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鍙傛暟缂栧彿涓�"' + configIds + '"鐨勬暟鎹」锛�').then(function () {
+ return delConfig(configIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/config/export", {
+ ...queryParams.value
+ }, `config_${new Date().getTime()}.xlsx`)
+}
+
+/** 鍒锋柊缂撳瓨鎸夐挳鎿嶄綔 */
+function handleRefreshCache() {
+ refreshCache().then(() => {
+ proxy.$modal.msgSuccess("鍒锋柊缂撳瓨鎴愬姛")
+ })
+}
+
+getList()
+</script>
diff --git a/src/views/system/dept/index.vue b/src/views/system/dept/index.vue
new file mode 100644
index 0000000..a177314
--- /dev/null
+++ b/src/views/system/dept/index.vue
@@ -0,0 +1,289 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="閮ㄩ棬鍚嶇О" prop="deptName">
+ <el-input
+ v-model="queryParams.deptName"
+ placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="閮ㄩ棬鐘舵��" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:dept:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="Sort"
+ @click="toggleExpandAll"
+ >灞曞紑/鎶樺彔</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+ <el-table
+ v-if="refreshTable"
+ v-loading="loading"
+ :data="deptList"
+ row-key="deptId"
+ :default-expand-all="isExpandAll"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ >
+ <el-table-column prop="deptName" label="閮ㄩ棬鍚嶇О" width="260"></el-table-column>
+ <el-table-column prop="orderNum" label="鎺掑簭" width="200"></el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="100">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="200">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']">鏂板</el-button>
+ <el-button v-if="scope.row.parentId != 0" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 娣诲姞鎴栦慨鏀归儴闂ㄥ璇濇 -->
+ <el-dialog :title="title" v-model="open" width="600px" append-to-body>
+ <el-form ref="deptRef" :model="form" :rules="rules" label-width="80px">
+ <el-row>
+ <el-col :span="24" v-if="form.parentId !== 0">
+ <el-form-item label="涓婄骇閮ㄩ棬" prop="parentId">
+ <el-tree-select
+ v-model="form.parentId"
+ :data="deptOptions"
+ :props="{ value: 'deptId', label: 'deptName', children: 'children' }"
+ value-key="deptId"
+ placeholder="閫夋嫨涓婄骇閮ㄩ棬"
+ check-strictly
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬鍚嶇О" prop="deptName">
+ <el-input v-model="form.deptName" placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄剧ず鎺掑簭" prop="orderNum">
+ <el-input-number v-model="form.orderNum" controls-position="right" :min="0"/>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="璐熻矗浜�" prop="leader">
+ <el-input v-model="form.leader" placeholder="璇疯緭鍏ヨ礋璐d汉" maxlength="20" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑱旂郴鐢佃瘽" prop="phone">
+ <el-input v-model="form.phone" placeholder="璇疯緭鍏ヨ仈绯荤數璇�" maxlength="11" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閮ㄩ棬缂栧彿" prop="deptNick">
+ <el-input v-model="form.deptNick" placeholder="璇疯緭鍏ラ儴闂ㄧ紪鍙�" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Dept">
+import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const deptList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const title = ref("")
+const deptOptions = ref([])
+const isExpandAll = ref(true)
+const refreshTable = ref(true)
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ deptName: undefined,
+ status: undefined
+ },
+ rules: {
+ parentId: [{ required: true, message: "涓婄骇閮ㄩ棬涓嶈兘涓虹┖", trigger: "blur" }],
+ deptName: [{ required: true, message: "閮ㄩ棬鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ orderNum: [{ required: true, message: "鏄剧ず鎺掑簭涓嶈兘涓虹┖", trigger: "blur" }],
+ email: [{ type: "email", message: "璇疯緭鍏ユ纭殑閭鍦板潃", trigger: ["blur", "change"] }],
+ phone: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur" }],
+ deptNick: [{ required: true, message: "閮ㄩ棬缂栧彿涓嶈兘涓虹┖", trigger: "blur" }],
+ },
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ閮ㄩ棬鍒楄〃 */
+function getList() {
+ loading.value = true
+ listDept(queryParams.value).then(response => {
+ deptList.value = proxy.handleTree(response.data, "deptId")
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ deptId: undefined,
+ parentId: undefined,
+ deptName: undefined,
+ orderNum: 0,
+ leader: undefined,
+ phone: undefined,
+ email: undefined,
+ status: "0",
+ deptNick: undefined,
+ }
+ proxy.resetForm("deptRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd(row) {
+ reset()
+ listDept().then(response => {
+ deptOptions.value = proxy.handleTree(response.data, "deptId")
+ })
+ if (row != undefined) {
+ form.value.parentId = row.deptId
+ }
+ open.value = true
+ title.value = "娣诲姞閮ㄩ棬"
+}
+
+/** 灞曞紑/鎶樺彔鎿嶄綔 */
+function toggleExpandAll() {
+ refreshTable.value = false
+ isExpandAll.value = !isExpandAll.value
+ nextTick(() => {
+ refreshTable.value = true
+ })
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ listDeptExcludeChild(row.deptId).then(response => {
+ deptOptions.value = proxy.handleTree(response.data, "deptId")
+ })
+ getDept(row.deptId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼閮ㄩ棬"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["deptRef"].validate(valid => {
+ if (valid) {
+ if (form.value.deptId != undefined) {
+ updateDept(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addDept(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鍚嶇О涓�"' + row.deptName + '"鐨勬暟鎹」?').then(function() {
+ return delDept(row.deptId)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+getList()
+</script>
diff --git a/src/views/system/dict/data.vue b/src/views/system/dict/data.vue
new file mode 100644
index 0000000..f4e2c6f
--- /dev/null
+++ b/src/views/system/dict/data.vue
@@ -0,0 +1,362 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="瀛楀吀鍚嶇О" prop="dictType">
+ <el-select v-model="queryParams.dictType" style="width: 200px">
+ <el-option
+ v-for="item in typeOptions"
+ :key="item.dictId"
+ :label="item.dictName"
+ :value="item.dictType"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="瀛楀吀鏍囩" prop="dictLabel">
+ <el-input
+ v-model="queryParams.dictLabel"
+ placeholder="璇疯緭鍏ュ瓧鍏告爣绛�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="鏁版嵁鐘舵��" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:dict:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:dict:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:dict:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['system:dict:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Close"
+ @click="handleClose"
+ >鍏抽棴</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="瀛楀吀缂栫爜" align="center" prop="dictCode" />
+ <el-table-column label="瀛楀吀鏍囩" align="center" prop="dictLabel">
+ <template #default="scope">
+ <span v-if="(scope.row.listClass == '' || scope.row.listClass == 'default') && (scope.row.cssClass == '' || scope.row.cssClass == null)">{{ scope.row.dictLabel }}</span>
+ <el-tag v-else :type="scope.row.listClass == 'primary' ? '' : scope.row.listClass" :class="scope.row.cssClass">{{ scope.row.dictLabel }}</el-tag>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀛楀吀閿��" align="center" prop="dictValue" />
+ <el-table-column label="瀛楀吀鎺掑簭" align="center" prop="dictSort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="160" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀瑰弬鏁伴厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="dataRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="瀛楀吀绫诲瀷">
+ <el-input v-model="form.dictType" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="鏁版嵁鏍囩" prop="dictLabel">
+ <el-input v-model="form.dictLabel" placeholder="璇疯緭鍏ユ暟鎹爣绛�" />
+ </el-form-item>
+ <el-form-item label="鏁版嵁閿��" prop="dictValue">
+ <el-input v-model="form.dictValue" placeholder="璇疯緭鍏ユ暟鎹敭鍊�" />
+ </el-form-item>
+ <el-form-item label="鏍峰紡灞炴��" prop="cssClass">
+ <el-input v-model="form.cssClass" placeholder="璇疯緭鍏ユ牱寮忓睘鎬�" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず鎺掑簭" prop="dictSort">
+ <el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="鍥炴樉鏍峰紡" prop="listClass">
+ <el-select v-model="form.listClass">
+ <el-option
+ v-for="item in listClassOptions"
+ :key="item.value"
+ :label="item.label + '(' + item.value + ')'"
+ :value="item.value"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Data">
+import useDictStore from '@/store/modules/dict'
+import { optionselect as getDictOptionselect, getType } from "@/api/system/dict/type"
+import { listData, getData, delData, addData, updateData } from "@/api/system/dict/data"
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const dataList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const defaultDictType = ref("")
+const typeOptions = ref([])
+const route = useRoute()
+// 鏁版嵁鏍囩鍥炴樉鏍峰紡
+const listClassOptions = ref([
+ { value: "default", label: "榛樿" },
+ { value: "primary", label: "涓昏" },
+ { value: "success", label: "鎴愬姛" },
+ { value: "info", label: "淇℃伅" },
+ { value: "warning", label: "璀﹀憡" },
+ { value: "danger", label: "鍗遍櫓" }
+])
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ dictType: undefined,
+ dictLabel: undefined,
+ status: undefined
+ },
+ rules: {
+ dictLabel: [{ required: true, message: "鏁版嵁鏍囩涓嶈兘涓虹┖", trigger: "blur" }],
+ dictValue: [{ required: true, message: "鏁版嵁閿�间笉鑳戒负绌�", trigger: "blur" }],
+ dictSort: [{ required: true, message: "鏁版嵁椤哄簭涓嶈兘涓虹┖", trigger: "blur" }]
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ瀛楀吀绫诲瀷璇︾粏 */
+function getTypes(dictId) {
+ getType(dictId).then(response => {
+ queryParams.value.dictType = response.data.dictType
+ defaultDictType.value = response.data.dictType
+ getList()
+ })
+}
+
+/** 鏌ヨ瀛楀吀绫诲瀷鍒楄〃 */
+function getTypeList() {
+ getDictOptionselect().then(response => {
+ typeOptions.value = response.data
+ })
+}
+
+/** 鏌ヨ瀛楀吀鏁版嵁鍒楄〃 */
+function getList() {
+ loading.value = true
+ listData(queryParams.value).then(response => {
+ dataList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ dictCode: undefined,
+ dictLabel: undefined,
+ dictValue: undefined,
+ cssClass: undefined,
+ listClass: "default",
+ dictSort: 0,
+ status: "0",
+ remark: undefined
+ }
+ proxy.resetForm("dataRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 杩斿洖鎸夐挳鎿嶄綔 */
+function handleClose() {
+ const obj = { path: "/system/dict" }
+ proxy.$tab.closeOpenPage(obj)
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ queryParams.value.dictType = defaultDictType.value
+ handleQuery()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞瀛楀吀鏁版嵁"
+ form.value.dictType = queryParams.value.dictType
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.dictCode)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const dictCode = row.dictCode || ids.value
+ getData(dictCode).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼瀛楀吀鏁版嵁"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["dataRef"].validate(valid => {
+ if (valid) {
+ if (form.value.dictCode != undefined) {
+ updateData(form.value).then(response => {
+ useDictStore().removeDict(queryParams.value.dictType)
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addData(form.value).then(response => {
+ useDictStore().removeDict(queryParams.value.dictType)
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const dictCodes = row.dictCode || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎瀛楀吀缂栫爜涓�"' + dictCodes + '"鐨勬暟鎹」锛�').then(function() {
+ return delData(dictCodes)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ useDictStore().removeDict(queryParams.value.dictType)
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/dict/data/export", {
+ ...queryParams.value
+ }, `dict_data_${new Date().getTime()}.xlsx`)
+}
+
+getTypes(route.params && route.params.dictId)
+getTypeList()
+</script>
diff --git a/src/views/system/dict/index.vue b/src/views/system/dict/index.vue
new file mode 100644
index 0000000..6c67e1b
--- /dev/null
+++ b/src/views/system/dict/index.vue
@@ -0,0 +1,326 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="瀛楀吀鍚嶇О" prop="dictName">
+ <el-input
+ v-model="queryParams.dictName"
+ placeholder="璇疯緭鍏ュ瓧鍏稿悕绉�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="瀛楀吀绫诲瀷" prop="dictType">
+ <el-input
+ v-model="queryParams.dictType"
+ placeholder="璇疯緭鍏ュ瓧鍏哥被鍨�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="瀛楀吀鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:dict:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:dict:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:dict:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['system:dict:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Refresh"
+ @click="handleRefreshCache"
+ v-hasPermi="['system:dict:remove']"
+ >鍒锋柊缂撳瓨</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="typeList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="瀛楀吀缂栧彿" align="center" prop="dictId" />
+ <el-table-column label="瀛楀吀鍚嶇О" align="center" prop="dictName" :show-overflow-tooltip="true"/>
+ <el-table-column label="瀛楀吀绫诲瀷" align="center" :show-overflow-tooltip="true">
+ <template #default="scope">
+ <router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
+ <span>{{ scope.row.dictType }}</span>
+ </router-link>
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="澶囨敞" align="center" prop="remark" :show-overflow-tooltip="true" />
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="160" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dict:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dict:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀瑰弬鏁伴厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="dictRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="瀛楀吀鍚嶇О" prop="dictName">
+ <el-input v-model="form.dictName" placeholder="璇疯緭鍏ュ瓧鍏稿悕绉�" />
+ </el-form-item>
+ <el-form-item label="瀛楀吀绫诲瀷" prop="dictType">
+ <el-input v-model="form.dictType" placeholder="璇疯緭鍏ュ瓧鍏哥被鍨�" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Dict">
+import useDictStore from '@/store/modules/dict'
+import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type"
+import {onMounted} from "vue";
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const typeList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const dateRange = ref([])
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ dictName: undefined,
+ dictType: undefined,
+ status: undefined
+ },
+ rules: {
+ dictName: [{ required: true, message: "瀛楀吀鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ dictType: [{ required: true, message: "瀛楀吀绫诲瀷涓嶈兘涓虹┖", trigger: "blur" }]
+ },
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ瀛楀吀绫诲瀷鍒楄〃 */
+function getList() {
+ loading.value = true
+ listType(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
+ typeList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ dictId: undefined,
+ dictName: undefined,
+ dictType: undefined,
+ status: "0",
+ remark: undefined
+ }
+ proxy.resetForm("dictRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞瀛楀吀绫诲瀷"
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.dictId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const dictId = row.dictId || ids.value
+ getType(dictId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼瀛楀吀绫诲瀷"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["dictRef"].validate(valid => {
+ if (valid) {
+ if (form.value.dictId != undefined) {
+ updateType(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addType(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const dictIds = row.dictId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎瀛楀吀缂栧彿涓�"' + dictIds + '"鐨勬暟鎹」锛�').then(function() {
+ return delType(dictIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/dict/type/export", {
+ ...queryParams.value
+ }, `dict_${new Date().getTime()}.xlsx`)
+}
+
+/** 鍒锋柊缂撳瓨鎸夐挳鎿嶄綔 */
+function handleRefreshCache() {
+ refreshCache().then(() => {
+ proxy.$modal.msgSuccess("鍒锋柊鎴愬姛")
+ useDictStore().cleanDict()
+ })
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
new file mode 100644
index 0000000..3bea122
--- /dev/null
+++ b/src/views/system/menu/index.vue
@@ -0,0 +1,470 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="鑿滃崟鍚嶇О" prop="menuName">
+ <el-input
+ v-model="queryParams.menuName"
+ placeholder="璇疯緭鍏ヨ彍鍗曞悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="鑿滃崟鐘舵��" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:menu:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="Sort"
+ @click="toggleExpandAll"
+ >灞曞紑/鎶樺彔</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table
+ v-if="refreshTable"
+ v-loading="loading"
+ :data="menuList"
+ row-key="menuId"
+ :default-expand-all="isExpandAll"
+ :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+ >
+ <el-table-column prop="menuName" label="鑿滃崟鍚嶇О" :show-overflow-tooltip="true" width="160"></el-table-column>
+ <el-table-column prop="icon" label="鍥炬爣" align="center" width="100">
+ <template #default="scope">
+ <svg-icon :icon-class="scope.row.icon" />
+ </template>
+ </el-table-column>
+ <el-table-column prop="orderNum" label="鎺掑簭" width="60"></el-table-column>
+ <el-table-column prop="perms" label="鏉冮檺鏍囪瘑" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="component" label="缁勪欢璺緞" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="status" label="鐘舵��" width="80">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" width="160" prop="createTime">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="210" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']">鏂板</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <!-- 娣诲姞鎴栦慨鏀硅彍鍗曞璇濇 -->
+ <el-dialog :title="title" v-model="open" width="880px" append-to-body>
+ <el-form ref="menuRef" :model="form" :rules="rules" label-width="130px">
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="涓婄骇鑿滃崟">
+ <el-tree-select
+ v-model="form.parentId"
+ :data="menuOptions"
+ :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
+ value-key="menuId"
+ placeholder="閫夋嫨涓婄骇鑿滃崟"
+ check-strictly
+ />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鑿滃崟绫诲瀷" prop="menuType">
+ <el-radio-group v-model="form.menuType">
+ <el-radio value="M">鐩綍</el-radio>
+ <el-radio value="C">鑿滃崟</el-radio>
+ <el-radio value="F">鎸夐挳</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType != 'F'">
+ <el-form-item label="鑿滃崟鍥炬爣" prop="icon">
+ <el-popover
+ placement="bottom-start"
+ :width="540"
+ trigger="click"
+ >
+ <template #reference>
+ <el-input v-model="form.icon" placeholder="鐐瑰嚮閫夋嫨鍥炬爣" @blur="showSelectIcon" readonly>
+ <template #prefix>
+ <svg-icon
+ v-if="form.icon"
+ :icon-class="form.icon"
+ class="el-input__icon"
+ style="height: 32px;width: 16px;"
+ />
+ <el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon>
+ </template>
+ </el-input>
+ </template>
+ <icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
+ </el-popover>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鏄剧ず鎺掑簭" prop="orderNum">
+ <el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鑿滃崟鍚嶇О" prop="menuName">
+ <el-input v-model="form.menuName" placeholder="璇疯緭鍏ヨ彍鍗曞悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType == 'C'">
+ <el-form-item prop="routeName">
+ <template #label>
+ <span>
+ <el-tooltip content="榛樿涓嶅~鍒欏拰璺敱鍦板潃鐩稿悓锛氬鍦板潃涓猴細`user`锛屽垯鍚嶇О涓篳User`锛堟敞鎰忥細鍥犱负router浼氬垹闄ゅ悕绉扮浉鍚岃矾鐢憋紝涓洪伩鍏嶅悕瀛楃殑鍐茬獊锛岀壒娈婃儏鍐典笅璇疯嚜瀹氫箟锛屼繚璇佸敮涓�鎬э級" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 璺敱鍚嶇О
+ </span>
+ </template>
+ <el-input v-model="form.routeName" placeholder="璇疯緭鍏ヨ矾鐢卞悕绉�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType != 'F'">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="閫夋嫨鏄閾惧垯璺敱鍦板潃闇�瑕佷互`http(s)://`寮�澶�" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>鏄惁澶栭摼
+ </span>
+ </template>
+ <el-radio-group v-model="form.isFrame">
+ <el-radio value="0">鏄�</el-radio>
+ <el-radio value="1">鍚�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType != 'F'">
+ <el-form-item prop="path">
+ <template #label>
+ <span>
+ <el-tooltip content="璁块棶鐨勮矾鐢卞湴鍧�锛屽锛歚user`锛屽澶栫綉鍦板潃闇�鍐呴摼璁块棶鍒欎互`http(s)://`寮�澶�" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 璺敱鍦板潃
+ </span>
+ </template>
+ <el-input v-model="form.path" placeholder="璇疯緭鍏ヨ矾鐢卞湴鍧�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType == 'C'">
+ <el-form-item prop="component">
+ <template #label>
+ <span>
+ <el-tooltip content="璁块棶鐨勭粍浠惰矾寰勶紝濡傦細`system/user/index`锛岄粯璁ゅ湪`views`鐩綍涓�" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 缁勪欢璺緞
+ </span>
+ </template>
+ <el-input v-model="form.component" placeholder="璇疯緭鍏ョ粍浠惰矾寰�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType == 'C'">
+ <el-form-item prop="appComponent">
+ <template #label>
+ <span>
+ <el-tooltip content="APP 绔闂殑缁勪欢璺緞锛屽锛歚app/system/user/index`" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ APP缁勪欢璺緞
+ </span>
+ </template>
+ <el-input v-model="form.appComponent" placeholder="璇疯緭鍏� APP 缁勪欢璺緞锛堝彲閫夛級" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType != 'M'">
+ <el-form-item>
+ <el-input v-model="form.perms" placeholder="璇疯緭鍏ユ潈闄愭爣璇�" maxlength="100" />
+ <template #label>
+ <span>
+ <el-tooltip content="鎺у埗鍣ㄤ腑瀹氫箟鐨勬潈闄愬瓧绗︼紝濡傦細@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 鏉冮檺瀛楃
+ </span>
+ </template>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType == 'C'">
+ <el-form-item>
+ <el-input v-model="form.query" placeholder="璇疯緭鍏ヨ矾鐢卞弬鏁�" maxlength="255" />
+ <template #label>
+ <span>
+ <el-tooltip content='璁块棶璺敱鐨勯粯璁や紶閫掑弬鏁帮紝濡傦細`{"id": 1, "name": "ry"}`' placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 璺敱鍙傛暟
+ </span>
+ </template>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType == 'C'">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="閫夋嫨鏄垯浼氳`keep-alive`缂撳瓨锛岄渶瑕佸尮閰嶇粍浠剁殑`name`鍜屽湴鍧�淇濇寔涓�鑷�" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 鏄惁缂撳瓨
+ </span>
+ </template>
+ <el-radio-group v-model="form.isCache">
+ <el-radio value="0">缂撳瓨</el-radio>
+ <el-radio value="1">涓嶇紦瀛�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12" v-if="form.menuType != 'F'">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="閫夋嫨闅愯棌鍒欒矾鐢卞皢涓嶄細鍑虹幇鍦ㄤ晶杈规爮锛屼絾浠嶇劧鍙互璁块棶" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 鏄剧ず鐘舵��
+ </span>
+ </template>
+ <el-radio-group v-model="form.visible">
+ <el-radio
+ v-for="dict in sys_show_hide"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ <span>
+ <el-tooltip content="閫夋嫨鍋滅敤鍒欒矾鐢卞皢涓嶄細鍑虹幇鍦ㄤ晶杈规爮锛屼篃涓嶈兘琚闂�" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 鑿滃崟鐘舵��
+ </span>
+ </template>
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Menu">
+import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu"
+import SvgIcon from "@/components/SvgIcon"
+import IconSelect from "@/components/IconSelect"
+import {onMounted} from "vue";
+
+const { proxy } = getCurrentInstance()
+const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable")
+
+const menuList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const title = ref("")
+const menuOptions = ref([])
+const isExpandAll = ref(false)
+const refreshTable = ref(true)
+const iconSelectRef = ref(null)
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ menuName: undefined,
+ visible: undefined
+ },
+ rules: {
+ menuName: [{ required: true, message: "鑿滃崟鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ orderNum: [{ required: true, message: "鑿滃崟椤哄簭涓嶈兘涓虹┖", trigger: "blur" }],
+ path: [{ required: true, message: "璺敱鍦板潃涓嶈兘涓虹┖", trigger: "blur" }],
+ appComponent: [{ required: false, message: "APP缁勪欢璺緞涓嶈兘涓虹┖", trigger: "blur" }]
+ },
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ鑿滃崟鍒楄〃 */
+function getList() {
+ loading.value = true
+ listMenu(queryParams.value).then(response => {
+ menuList.value = proxy.handleTree(response.data, "menuId")
+ loading.value = false
+ })
+}
+
+/** 鏌ヨ鑿滃崟涓嬫媺鏍戠粨鏋� */
+function getTreeselect() {
+ menuOptions.value = []
+ listMenu().then(response => {
+ const menu = { menuId: 0, menuName: "涓荤被鐩�", children: [] }
+ menu.children = proxy.handleTree(response.data, "menuId")
+ menuOptions.value.push(menu)
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ menuId: undefined,
+ parentId: 0,
+ menuName: undefined,
+ icon: undefined,
+ menuType: "M",
+ orderNum: undefined,
+ isFrame: "1",
+ isCache: "0",
+ visible: "0",
+ status: "0",
+ appComponent: undefined
+ }
+ proxy.resetForm("menuRef")
+}
+
+/** 灞曠ず涓嬫媺鍥炬爣 */
+function showSelectIcon() {
+ iconSelectRef.value.reset()
+}
+
+/** 閫夋嫨鍥炬爣 */
+function selected(name) {
+ form.value.icon = name
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd(row) {
+ reset()
+ getTreeselect()
+ if (row != null && row.menuId) {
+ form.value.parentId = row.menuId
+ } else {
+ form.value.parentId = 0
+ }
+ open.value = true
+ title.value = "娣诲姞鑿滃崟"
+}
+
+/** 灞曞紑/鎶樺彔鎿嶄綔 */
+function toggleExpandAll() {
+ refreshTable.value = false
+ isExpandAll.value = !isExpandAll.value
+ nextTick(() => {
+ refreshTable.value = true
+ })
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+async function handleUpdate(row) {
+ reset()
+ await getTreeselect()
+ getMenu(row.menuId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼鑿滃崟"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["menuRef"].validate(valid => {
+ if (valid) {
+ if (form.value.menuId != undefined) {
+ updateMenu(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addMenu(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鍚嶇О涓�"' + row.menuName + '"鐨勬暟鎹」?').then(function() {
+ return delMenu(row.menuId)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/notice/index.vue b/src/views/system/notice/index.vue
new file mode 100644
index 0000000..8043db0
--- /dev/null
+++ b/src/views/system/notice/index.vue
@@ -0,0 +1,295 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="鍏憡鏍囬" prop="noticeTitle">
+ <el-input
+ v-model="queryParams.noticeTitle"
+ placeholder="璇疯緭鍏ュ叕鍛婃爣棰�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎿嶄綔浜哄憳" prop="createBy">
+ <el-input
+ v-model="queryParams.createBy"
+ placeholder="璇疯緭鍏ユ搷浣滀汉鍛�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="绫诲瀷" prop="noticeType">
+ <el-select v-model="queryParams.noticeType" placeholder="鍏憡绫诲瀷" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_notice_type"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:notice:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:notice:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:notice:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="搴忓彿" align="center" prop="noticeId" width="100" />
+ <el-table-column
+ label="鍏憡鏍囬"
+ align="center"
+ prop="noticeTitle"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="鍏憡绫诲瀷" align="center" prop="noticeType" width="100">
+ <template #default="scope">
+ <dict-tag :options="sys_notice_type" :value="scope.row.noticeType" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鐘舵��" align="center" prop="status" width="100">
+ <template #default="scope">
+ <dict-tag :options="sys_notice_status" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鑰�" align="center" prop="createBy" width="100" />
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="100">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:notice:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:notice:remove']" >鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀瑰叕鍛婂璇濇 -->
+ <el-dialog :title="title" v-model="open" width="780px" append-to-body>
+ <el-form ref="noticeRef" :model="form" :rules="rules" label-width="80px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鍏憡鏍囬" prop="noticeTitle">
+ <el-input v-model="form.noticeTitle" placeholder="璇疯緭鍏ュ叕鍛婃爣棰�" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鍏憡绫诲瀷" prop="noticeType">
+ <el-select v-model="form.noticeType" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="dict in sys_notice_type"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_notice_status"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="鍐呭">
+ <editor v-model="form.noticeContent" :min-height="192"/>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Notice">
+import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice"
+import {onMounted} from "vue";
+
+const { proxy } = getCurrentInstance()
+const { sys_notice_status, sys_notice_type } = proxy.useDict("sys_notice_status", "sys_notice_type")
+
+const noticeList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ noticeTitle: undefined,
+ createBy: undefined,
+ status: undefined
+ },
+ rules: {
+ noticeTitle: [{ required: true, message: "鍏憡鏍囬涓嶈兘涓虹┖", trigger: "blur" }],
+ noticeType: [{ required: true, message: "鍏憡绫诲瀷涓嶈兘涓虹┖", trigger: "change" }]
+ },
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ鍏憡鍒楄〃 */
+function getList() {
+ loading.value = true
+ listNotice(queryParams.value).then(response => {
+ noticeList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ noticeId: undefined,
+ noticeTitle: undefined,
+ noticeType: undefined,
+ noticeContent: undefined,
+ status: "0"
+ }
+ proxy.resetForm("noticeRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.noticeId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞鍏憡"
+}
+
+/**淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const noticeId = row.noticeId || ids.value
+ getNotice(noticeId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼鍏憡"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["noticeRef"].validate(valid => {
+ if (valid) {
+ if (form.value.noticeId != undefined) {
+ updateNotice(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addNotice(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const noticeIds = row.noticeId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鍏憡缂栧彿涓�"' + noticeIds + '"鐨勬暟鎹」锛�').then(function() {
+ return delNotice(noticeIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/post/index.vue b/src/views/system/post/index.vue
new file mode 100644
index 0000000..0e80652
--- /dev/null
+++ b/src/views/system/post/index.vue
@@ -0,0 +1,290 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
+ <el-form-item label="宀椾綅缂栫爜" prop="postCode">
+ <el-input
+ v-model="queryParams.postCode"
+ placeholder="璇疯緭鍏ュ矖浣嶇紪鐮�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="宀椾綅鍚嶇О" prop="postName">
+ <el-input
+ v-model="queryParams.postName"
+ placeholder="璇疯緭鍏ュ矖浣嶅悕绉�"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="宀椾綅鐘舵��" clearable style="width: 200px">
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:post:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:post:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:post:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['system:post:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="postList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="宀椾綅缂栧彿" align="center" prop="postId" />
+ <el-table-column label="宀椾綅缂栫爜" align="center" prop="postCode" />
+ <el-table-column label="宀椾綅鍚嶇О" align="center" prop="postName" />
+ <el-table-column label="宀椾綅鎺掑簭" align="center" prop="postSort" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="180" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:post:edit']">淇敼</el-button>
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:post:remove']">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀瑰矖浣嶅璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="postRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="宀椾綅鍚嶇О" prop="postName">
+ <el-input v-model="form.postName" placeholder="璇疯緭鍏ュ矖浣嶅悕绉�" />
+ </el-form-item>
+ <el-form-item label="宀椾綅缂栫爜" prop="postCode">
+ <el-input v-model="form.postCode" placeholder="璇疯緭鍏ョ紪鐮佸悕绉�" />
+ </el-form-item>
+ <el-form-item label="宀椾綅椤哄簭" prop="postSort">
+ <el-input-number v-model="form.postSort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="宀椾綅鐘舵��" prop="status">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�" />
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Post">
+import { listPost, addPost, delPost, getPost, updatePost } from "@/api/system/post"
+import {onMounted} from "vue";
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const postList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ postCode: undefined,
+ postName: undefined,
+ status: undefined
+ },
+ rules: {
+ postName: [{ required: true, message: "宀椾綅鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ postCode: [{ required: true, message: "宀椾綅缂栫爜涓嶈兘涓虹┖", trigger: "blur" }],
+ postSort: [{ required: true, message: "宀椾綅椤哄簭涓嶈兘涓虹┖", trigger: "blur" }],
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ宀椾綅鍒楄〃 */
+function getList() {
+ loading.value = true
+ listPost(queryParams.value).then(response => {
+ postList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 琛ㄥ崟閲嶇疆 */
+function reset() {
+ form.value = {
+ postId: undefined,
+ postCode: undefined,
+ postName: undefined,
+ postSort: 0,
+ status: "0",
+ remark: undefined
+ }
+ proxy.resetForm("postRef")
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.postId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ open.value = true
+ title.value = "娣诲姞宀椾綅"
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const postId = row.postId || ids.value
+ getPost(postId).then(response => {
+ form.value = response.data
+ open.value = true
+ title.value = "淇敼宀椾綅"
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["postRef"].validate(valid => {
+ if (valid) {
+ if (form.value.postId != undefined) {
+ updatePost(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addPost(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const postIds = row.postId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎宀椾綅缂栧彿涓�"' + postIds + '"鐨勬暟鎹」锛�').then(function() {
+ return delPost(postIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/post/export", {
+ ...queryParams.value
+ }, `post_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/role/authUser.vue b/src/views/system/role/authUser.vue
new file mode 100644
index 0000000..a460f3c
--- /dev/null
+++ b/src/views/system/role/authUser.vue
@@ -0,0 +1,182 @@
+
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true">
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="userName">
+ <el-input
+ v-model="queryParams.userName"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input
+ v-model="queryParams.phonenumber"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="openSelectUser"
+ v-hasPermi="['system:role:add']"
+ >娣诲姞鐢ㄦ埛</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="CircleClose"
+ :disabled="multiple"
+ @click="cancelAuthUserAll"
+ v-hasPermi="['system:role:remove']"
+ >鎵归噺鍙栨秷鎺堟潈</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Close"
+ @click="handleClose"
+ >鍏抽棴</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="鐢ㄦ埛鍚嶇О" prop="userName" :show-overflow-tooltip="true" />
+ <el-table-column label="鐢ㄦ埛鏄电О" prop="nickName" :show-overflow-tooltip="true" />
+ <el-table-column label="閭" prop="email" :show-overflow-tooltip="true" />
+ <el-table-column label="鎵嬫満" prop="phonenumber" :show-overflow-tooltip="true" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-button link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)" v-hasPermi="['system:role:remove']">鍙栨秷鎺堟潈</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ <select-user ref="selectRef" :roleId="queryParams.roleId" @ok="handleQuery" />
+ </div>
+</template>
+
+<script setup name="AuthUser">
+import selectUser from "./selectUser"
+import { allocatedUserList, authUserCancel, authUserCancelAll } from "@/api/system/role"
+import {onMounted} from "vue";
+
+const route = useRoute()
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const userList = ref([])
+const loading = ref(true)
+const showSearch = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const userIds = ref([])
+
+const queryParams = reactive({
+ pageNum: 1,
+ pageSize: 10,
+ roleId: route.params.roleId,
+ userName: undefined,
+ phonenumber: undefined,
+})
+
+/** 鏌ヨ鎺堟潈鐢ㄦ埛鍒楄〃 */
+function getList() {
+ loading.value = true
+ allocatedUserList(queryParams).then(response => {
+ userList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 杩斿洖鎸夐挳 */
+function handleClose() {
+ const obj = { path: "/system/role" }
+ proxy.$tab.closeOpenPage(obj)
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ userIds.value = selection.map(item => item.userId)
+ multiple.value = !selection.length
+}
+
+/** 鎵撳紑鎺堟潈鐢ㄦ埛琛ㄥ脊绐� */
+function openSelectUser() {
+ proxy.$refs["selectRef"].show()
+}
+
+/** 鍙栨秷鎺堟潈鎸夐挳鎿嶄綔 */
+function cancelAuthUser(row) {
+ proxy.$modal.confirm('纭瑕佸彇娑堣鐢ㄦ埛"' + row.userName + '"瑙掕壊鍚楋紵').then(function () {
+ return authUserCancel({ userId: row.userId, roleId: queryParams.roleId })
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍙栨秷鎺堟潈鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 鎵归噺鍙栨秷鎺堟潈鎸夐挳鎿嶄綔 */
+function cancelAuthUserAll(row) {
+ const roleId = queryParams.roleId
+ const uIds = userIds.value.join(",")
+ proxy.$modal.confirm("鏄惁鍙栨秷閫変腑鐢ㄦ埛鎺堟潈鏁版嵁椤�?").then(function () {
+ return authUserCancelAll({ roleId: roleId, userIds: uIds })
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍙栨秷鎺堟潈鎴愬姛")
+ }).catch(() => {})
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/role/index.vue b/src/views/system/role/index.vue
new file mode 100644
index 0000000..85ecf28
--- /dev/null
+++ b/src/views/system/role/index.vue
@@ -0,0 +1,587 @@
+<template>
+ <div class="app-container">
+ <el-form :model="queryParams" ref="queryRef" v-show="showSearch" :inline="true" label-width="68px">
+ <el-form-item label="瑙掕壊鍚嶇О" prop="roleName">
+ <el-input
+ v-model="queryParams.roleName"
+ placeholder="璇疯緭鍏ヨ鑹插悕绉�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鏉冮檺瀛楃" prop="roleKey">
+ <el-input
+ v-model="queryParams.roleKey"
+ placeholder="璇疯緭鍏ユ潈闄愬瓧绗�"
+ clearable
+ style="width: 240px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select
+ v-model="queryParams.status"
+ placeholder="瑙掕壊鐘舵��"
+ clearable
+ style="width: 240px"
+ >
+ <el-option
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :label="dict.label"
+ :value="dict.value"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="handleAdd"
+ v-hasPermi="['system:role:add']"
+ >鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleUpdate"
+ v-hasPermi="['system:role:edit']"
+ >淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['system:role:remove']"
+ >鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="warning"
+ plain
+ icon="Download"
+ @click="handleExport"
+ v-hasPermi="['system:role:export']"
+ >瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+ </el-row>
+
+ <!-- 琛ㄦ牸鏁版嵁 -->
+ <el-table v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="55" align="center" />
+ <el-table-column label="瑙掕壊缂栧彿" prop="roleId" width="120" />
+ <el-table-column label="瑙掕壊鍚嶇О" prop="roleName" :show-overflow-tooltip="true" width="150" />
+ <el-table-column label="鏉冮檺瀛楃" prop="roleKey" :show-overflow-tooltip="true" width="150" />
+ <el-table-column label="鏄剧ず椤哄簭" prop="roleSort" width="100" />
+ <el-table-column label="鐘舵��" align="center" width="100">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ active-value="0"
+ inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="淇敼" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鏁版嵁鏉冮檺" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒嗛厤鐢ㄦ埛" placement="top" v-if="scope.row.roleId !== 1">
+ <el-button link type="primary" icon="User" @click="handleAuthUser(scope.row)" v-hasPermi="['system:role:edit']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+
+ <!-- 娣诲姞鎴栦慨鏀硅鑹查厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="500px" append-to-body>
+ <el-form ref="roleRef" :model="form" :rules="rules" label-width="100px">
+ <el-form-item label="瑙掕壊鍚嶇О" prop="roleName">
+ <el-input v-model="form.roleName" placeholder="璇疯緭鍏ヨ鑹插悕绉�" />
+ </el-form-item>
+ <el-form-item prop="roleKey">
+ <template #label>
+ <span>
+ <el-tooltip content="鎺у埗鍣ㄤ腑瀹氫箟鐨勬潈闄愬瓧绗︼紝濡傦細@PreAuthorize(`@ss.hasRole('admin')`)" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ 鏉冮檺瀛楃
+ </span>
+ </template>
+ <el-input v-model="form.roleKey" placeholder="璇疯緭鍏ユ潈闄愬瓧绗�" />
+ </el-form-item>
+ <el-form-item label="瑙掕壊椤哄簭" prop="roleSort">
+ <el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
+ </el-form-item>
+ <el-form-item label="鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio
+ v-for="dict in sys_normal_disable"
+ :key="dict.value"
+ :value="dict.value"
+ >{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鑿滃崟鏉冮檺">
+ <el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">灞曞紑/鎶樺彔</el-checkbox>
+ <el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">鍏ㄩ��/鍏ㄤ笉閫�</el-checkbox>
+ <el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">鐖跺瓙鑱斿姩</el-checkbox>
+ <el-tree
+ class="tree-border"
+ :data="menuOptions"
+ show-checkbox
+ ref="menuRef"
+ node-key="id"
+ :check-strictly="!form.menuCheckStrictly"
+ empty-text="鍔犺浇涓紝璇风◢鍊�"
+ :props="{ label: 'label', children: 'children' }"
+ ></el-tree>
+ </el-form-item>
+ <el-form-item label="澶囨敞">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�"></el-input>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 鍒嗛厤瑙掕壊鏁版嵁鏉冮檺瀵硅瘽妗� -->
+ <el-dialog :title="title" v-model="openDataScope" width="500px" append-to-body>
+ <el-form :model="form" label-width="80px">
+ <el-form-item label="瑙掕壊鍚嶇О">
+ <el-input v-model="form.roleName" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="鏉冮檺瀛楃">
+ <el-input v-model="form.roleKey" :disabled="true" />
+ </el-form-item>
+ <el-form-item label="鏉冮檺鑼冨洿">
+ <el-select v-model="form.dataScope" @change="dataScopeSelectChange">
+ <el-option
+ v-for="item in dataScopeOptions"
+ :key="item.value"
+ :label="item.label"
+ :value="item.value"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鏁版嵁鏉冮檺" v-show="form.dataScope == 2">
+ <el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">灞曞紑/鎶樺彔</el-checkbox>
+ <el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">鍏ㄩ��/鍏ㄤ笉閫�</el-checkbox>
+ <el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">鐖跺瓙鑱斿姩</el-checkbox>
+ <el-tree
+ class="tree-border"
+ :data="deptOptions"
+ show-checkbox
+ default-expand-all
+ ref="deptRef"
+ node-key="id"
+ :check-strictly="!form.deptCheckStrictly"
+ empty-text="鍔犺浇涓紝璇风◢鍊�"
+ :props="{ label: 'label', children: 'children' }"
+ ></el-tree>
+ </el-form-item>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitDataScope">纭� 瀹�</el-button>
+ <el-button @click="cancelDataScope">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="Role">
+import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from "@/api/system/role"
+import { roleMenuTreeselect, treeselect as menuTreeselect } from "@/api/system/menu"
+import {onMounted} from "vue";
+
+const router = useRouter()
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const roleList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const dateRange = ref([])
+const menuOptions = ref([])
+const menuExpand = ref(false)
+const menuNodeAll = ref(false)
+const deptExpand = ref(true)
+const deptNodeAll = ref(false)
+const deptOptions = ref([])
+const openDataScope = ref(false)
+const menuRef = ref(null)
+const deptRef = ref(null)
+
+/** 鏁版嵁鑼冨洿閫夐」*/
+const dataScopeOptions = ref([
+ { value: "1", label: "鍏ㄩ儴鏁版嵁鏉冮檺" },
+ // { value: "2", label: "鑷畾鏁版嵁鏉冮檺" },
+ { value: "3", label: "鏈儴闂ㄦ暟鎹潈闄�" },
+ { value: "4", label: "鏈儴闂ㄥ強浠ヤ笅鏁版嵁鏉冮檺" },
+ { value: "5", label: "浠呮湰浜烘暟鎹潈闄�" }
+])
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ roleName: undefined,
+ roleKey: undefined,
+ status: undefined
+ },
+ rules: {
+ roleName: [{ required: true, message: "瑙掕壊鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }],
+ roleKey: [{ required: true, message: "鏉冮檺瀛楃涓嶈兘涓虹┖", trigger: "blur" }],
+ roleSort: [{ required: true, message: "瑙掕壊椤哄簭涓嶈兘涓虹┖", trigger: "blur" }]
+ },
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 鏌ヨ瑙掕壊鍒楄〃 */
+function getList() {
+ loading.value = true
+ listRole(proxy.addDateRange(queryParams.value, dateRange.value)).then(response => {
+ roleList.value = response.rows
+ total.value = response.total
+ loading.value = false
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const roleIds = row.roleId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎瑙掕壊缂栧彿涓�"' + roleIds + '"鐨勬暟鎹」?').then(function () {
+ return delRole(roleIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/role/export", {
+ ...queryParams.value,
+ }, `role_${new Date().getTime()}.xlsx`)
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.roleId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 瑙掕壊鐘舵�佷慨鏀� */
+function handleStatusChange(row) {
+ let text = row.status === "0" ? "鍚敤" : "鍋滅敤"
+ proxy.$modal.confirm('纭瑕�"' + text + '""' + row.roleName + '"瑙掕壊鍚�?').then(function () {
+ return changeRoleStatus(row.roleId, row.status)
+ }).then(() => {
+ proxy.$modal.msgSuccess(text + "鎴愬姛")
+ }).catch(function () {
+ row.status = row.status === "0" ? "1" : "0"
+ })
+}
+
+/** 鏇村鎿嶄綔 */
+function handleCommand(command, row) {
+ switch (command) {
+ case "handleDataScope":
+ handleDataScope(row)
+ break
+ case "handleAuthUser":
+ handleAuthUser(row)
+ break
+ default:
+ break
+ }
+}
+
+/** 鍒嗛厤鐢ㄦ埛 */
+function handleAuthUser(row) {
+ router.push("/system/role-auth/user/" + row.roleId)
+}
+
+/** 鏌ヨ鑿滃崟鏍戠粨鏋� */
+function getMenuTreeselect() {
+ menuTreeselect().then(response => {
+ menuOptions.value = response.data
+ })
+}
+
+/** 鎵�鏈夐儴闂ㄨ妭鐐规暟鎹� */
+function getDeptAllCheckedKeys() {
+ // 鐩墠琚�変腑鐨勯儴闂ㄨ妭鐐�
+ let checkedKeys = deptRef.value.getCheckedKeys()
+ // 鍗婇�変腑鐨勯儴闂ㄨ妭鐐�
+ let halfCheckedKeys = deptRef.value.getHalfCheckedKeys()
+ checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
+ return checkedKeys
+}
+
+/** 閲嶇疆鏂板鐨勮〃鍗曚互鍙婂叾浠栨暟鎹� */
+function reset() {
+ if (menuRef.value != undefined) {
+ menuRef.value.setCheckedKeys([])
+ }
+ menuExpand.value = false
+ menuNodeAll.value = false
+ deptExpand.value = true
+ deptNodeAll.value = false
+ form.value = {
+ roleId: undefined,
+ roleName: undefined,
+ roleKey: undefined,
+ roleSort: 0,
+ status: "0",
+ menuIds: [],
+ deptIds: [],
+ menuCheckStrictly: true,
+ deptCheckStrictly: true,
+ remark: undefined
+ }
+ proxy.resetForm("roleRef")
+}
+
+/** 娣诲姞瑙掕壊 */
+function handleAdd() {
+ reset()
+ getMenuTreeselect()
+ open.value = true
+ title.value = "娣诲姞瑙掕壊"
+}
+
+/** 淇敼瑙掕壊 */
+function handleUpdate(row) {
+ reset()
+ const roleId = row.roleId || ids.value
+ const roleMenu = getRoleMenuTreeselect(roleId)
+ getRole(roleId).then(response => {
+ form.value = response.data
+ form.value.roleSort = Number(form.value.roleSort)
+ open.value = true
+ nextTick(() => {
+ roleMenu.then((res) => {
+ let checkedKeys = res.checkedKeys
+ checkedKeys.forEach((v) => {
+ nextTick(() => {
+ menuRef.value.setChecked(v, true, false)
+ })
+ })
+ })
+ })
+ })
+ title.value = "淇敼瑙掕壊"
+}
+
+/** 鏍规嵁瑙掕壊ID鏌ヨ鑿滃崟鏍戠粨鏋� */
+function getRoleMenuTreeselect(roleId) {
+ return roleMenuTreeselect(roleId).then(response => {
+ menuOptions.value = response.menus
+ return response
+ })
+}
+
+/** 鏍规嵁瑙掕壊ID鏌ヨ閮ㄩ棬鏍戠粨鏋� */
+function getDeptTree(roleId) {
+ return deptTreeSelect(roleId).then(response => {
+ deptOptions.value = response.depts
+ return response
+ })
+}
+
+/** 鏍戞潈闄愶紙灞曞紑/鎶樺彔锛�*/
+function handleCheckedTreeExpand(value, type) {
+ if (type == "menu") {
+ let treeList = menuOptions.value
+ for (let i = 0; i < treeList.length; i++) {
+ menuRef.value.store.nodesMap[treeList[i].id].expanded = value
+ }
+ } else if (type == "dept") {
+ let treeList = deptOptions.value
+ for (let i = 0; i < treeList.length; i++) {
+ deptRef.value.store.nodesMap[treeList[i].id].expanded = value
+ }
+ }
+}
+
+/** 鏍戞潈闄愶紙鍏ㄩ��/鍏ㄤ笉閫夛級 */
+function handleCheckedTreeNodeAll(value, type) {
+ if (type == "menu") {
+ menuRef.value.setCheckedNodes(value ? menuOptions.value : [])
+ } else if (type == "dept") {
+ deptRef.value.setCheckedNodes(value ? deptOptions.value : [])
+ }
+}
+
+/** 鏍戞潈闄愶紙鐖跺瓙鑱斿姩锛� */
+function handleCheckedTreeConnect(value, type) {
+ if (type == "menu") {
+ form.value.menuCheckStrictly = value ? true : false
+ } else if (type == "dept") {
+ form.value.deptCheckStrictly = value ? true : false
+ }
+}
+
+/** 鎵�鏈夎彍鍗曡妭鐐规暟鎹� */
+function getMenuAllCheckedKeys() {
+ // 鐩墠琚�変腑鐨勮彍鍗曡妭鐐�
+ let checkedKeys = menuRef.value.getCheckedKeys()
+ // 鍗婇�変腑鐨勮彍鍗曡妭鐐�
+ let halfCheckedKeys = menuRef.value.getHalfCheckedKeys()
+ checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
+ return checkedKeys
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["roleRef"].validate(valid => {
+ if (valid) {
+ if (form.value.roleId != undefined) {
+ form.value.menuIds = getMenuAllCheckedKeys()
+ updateRole(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ form.value.menuIds = getMenuAllCheckedKeys()
+ addRole(form.value).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 閫夋嫨瑙掕壊鏉冮檺鑼冨洿瑙﹀彂 */
+function dataScopeSelectChange(value) {
+ if (value !== "2") {
+ deptRef.value.setCheckedKeys([])
+ }
+}
+
+/** 鍒嗛厤鏁版嵁鏉冮檺鎿嶄綔 */
+function handleDataScope(row) {
+ reset()
+ const deptTreeSelect = getDeptTree(row.roleId)
+ getRole(row.roleId).then(response => {
+ form.value = response.data
+ openDataScope.value = true
+ nextTick(() => {
+ deptTreeSelect.then(res => {
+ nextTick(() => {
+ if (deptRef.value) {
+ deptRef.value.setCheckedKeys(res.checkedKeys)
+ }
+ })
+ })
+ })
+ })
+ title.value = "鍒嗛厤鏁版嵁鏉冮檺"
+}
+
+/** 鎻愪氦鎸夐挳锛堟暟鎹潈闄愶級 */
+function submitDataScope() {
+ if (form.value.roleId != undefined) {
+ form.value.deptIds = getDeptAllCheckedKeys()
+ dataScope(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ openDataScope.value = false
+ getList()
+ })
+ }
+}
+
+/** 鍙栨秷鎸夐挳锛堟暟鎹潈闄愶級*/
+function cancelDataScope() {
+ openDataScope.value = false
+ reset()
+}
+
+onMounted(() => {
+ getList();
+});
+</script>
diff --git a/src/views/system/role/selectUser.vue b/src/views/system/role/selectUser.vue
new file mode 100644
index 0000000..3e3d8aa
--- /dev/null
+++ b/src/views/system/role/selectUser.vue
@@ -0,0 +1,144 @@
+<template>
+ <!-- 鎺堟潈鐢ㄦ埛 -->
+ <el-dialog title="閫夋嫨鐢ㄦ埛" v-model="visible" width="800px" top="5vh" append-to-body>
+ <el-form :model="queryParams" ref="queryRef" :inline="true">
+ <el-form-item label="鐢ㄦ埛鍚嶇О" prop="userName">
+ <el-input
+ v-model="queryParams.userName"
+ placeholder="璇疯緭鍏ョ敤鎴峰悕绉�"
+ clearable
+ style="width: 180px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input
+ v-model="queryParams.phonenumber"
+ placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�"
+ clearable
+ style="width: 180px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row>
+ <el-table @row-click="clickRow" ref="refTable" :data="userList" @selection-change="handleSelectionChange" height="260px">
+ <el-table-column type="selection" width="55"></el-table-column>
+ <el-table-column label="鐢ㄦ埛鍚嶇О" prop="userName" :show-overflow-tooltip="true" />
+ <el-table-column label="鐢ㄦ埛鏄电О" prop="nickName" :show-overflow-tooltip="true" />
+ <el-table-column label="閭" prop="email" :show-overflow-tooltip="true" />
+ <el-table-column label="鎵嬫満" prop="phonenumber" :show-overflow-tooltip="true" />
+ <el-table-column label="鐘舵��" align="center" prop="status">
+ <template #default="scope">
+ <dict-tag :options="sys_normal_disable" :value="scope.row.status" />
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-row>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleSelectUser">纭� 瀹�</el-button>
+ <el-button @click="visible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup name="SelectUser">
+import { authUserSelectAll, unallocatedUserList } from "@/api/system/role"
+
+const props = defineProps({
+ roleId: {
+ type: [Number, String]
+ }
+})
+
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable } = proxy.useDict("sys_normal_disable")
+
+const userList = ref([])
+const visible = ref(false)
+const total = ref(0)
+const userIds = ref([])
+
+const queryParams = reactive({
+ pageNum: 1,
+ pageSize: 10,
+ roleId: undefined,
+ userName: undefined,
+ phonenumber: undefined
+})
+
+// 鏄剧ず寮规
+function show() {
+ queryParams.roleId = props.roleId
+ getList()
+ visible.value = true
+}
+
+/**閫夋嫨琛� */
+function clickRow(row) {
+ proxy.$refs["refTable"].toggleRowSelection(row)
+}
+
+// 澶氶�夋閫変腑鏁版嵁
+function handleSelectionChange(selection) {
+ userIds.value = selection.map(item => item.userId)
+}
+
+// 鏌ヨ琛ㄦ暟鎹�
+function getList() {
+ unallocatedUserList(queryParams).then(res => {
+ userList.value = res.rows
+ total.value = res.total
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+const emit = defineEmits(["ok"])
+/** 閫夋嫨鎺堟潈鐢ㄦ埛鎿嶄綔 */
+function handleSelectUser() {
+ const roleId = queryParams.roleId
+ const uIds = userIds.value.join(",")
+ if (uIds == "") {
+ proxy.$modal.msgError("璇烽�夋嫨瑕佸垎閰嶇殑鐢ㄦ埛")
+ return
+ }
+ authUserSelectAll({ roleId: roleId, userIds: uIds }).then(res => {
+ proxy.$modal.msgSuccess(res.msg)
+ visible.value = false
+ emit("ok")
+ })
+}
+
+defineExpose({
+ show,
+})
+</script>
diff --git a/src/views/system/user/authRole.vue b/src/views/system/user/authRole.vue
new file mode 100644
index 0000000..3935ab1
--- /dev/null
+++ b/src/views/system/user/authRole.vue
@@ -0,0 +1,123 @@
+<template>
+ <div class="app-container">
+ <h4 class="form-header h4">鍩烘湰淇℃伅</h4>
+ <el-form :model="form" label-width="80px">
+ <el-row>
+ <el-col :span="8" :offset="2">
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickName">
+ <el-input v-model="form.nickName" disabled />
+ </el-form-item>
+ </el-col>
+ <el-col :span="8" :offset="2">
+ <el-form-item label="鐧诲綍璐﹀彿" prop="userName">
+ <el-input v-model="form.userName" disabled />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+
+ <h4 class="form-header h4">瑙掕壊淇℃伅</h4>
+ <el-table v-loading="loading" :row-key="getRowKey" @row-click="clickRow" ref="roleRef" @selection-change="handleSelectionChange" :data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)">
+ <el-table-column label="搴忓彿" width="55" type="index" align="center">
+ <template #default="scope">
+ <span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column>
+ <el-table-column label="瑙掕壊缂栧彿" align="center" prop="roleId" />
+ <el-table-column label="瑙掕壊鍚嶇О" align="center" prop="roleName" />
+ <el-table-column label="鏉冮檺瀛楃" align="center" prop="roleKey" />
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" width="180">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ </el-table>
+
+ <pagination v-show="total > 0" :total="total" v-model:page="pageNum" v-model:limit="pageSize" />
+
+ <el-form label-width="100px">
+ <div style="text-align: center;margin-left:-120px;margin-top:30px;">
+ <el-button type="primary" @click="submitForm()">鎻愪氦</el-button>
+ <el-button @click="close()">杩斿洖</el-button>
+ </div>
+ </el-form>
+ </div>
+</template>
+
+<script setup name="AuthRole">
+import { getAuthRole, updateAuthRole } from "@/api/system/user"
+
+const route = useRoute()
+const { proxy } = getCurrentInstance()
+
+const loading = ref(true)
+const total = ref(0)
+const pageNum = ref(1)
+const pageSize = ref(10)
+const roleIds = ref([])
+const roles = ref([])
+const form = ref({
+ nickName: undefined,
+ userName: undefined,
+ userId: undefined
+})
+
+/** 鍗曞嚮閫変腑琛屾暟鎹� */
+function clickRow(row) {
+ if (checkSelectable(row)) {
+ proxy.$refs["roleRef"].toggleRowSelection(row)
+ }
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ roleIds.value = selection.map(item => item.roleId)
+}
+
+/** 淇濆瓨閫変腑鐨勬暟鎹紪鍙� */
+function getRowKey(row) {
+ return row.roleId
+}
+
+// 妫�鏌ヨ鑹茬姸鎬�
+function checkSelectable(row) {
+ return row.status === "0" ? true : false
+}
+
+/** 鍏抽棴鎸夐挳 */
+function close() {
+ const obj = { path: "/system/user" }
+ proxy.$tab.closeOpenPage(obj)
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ const userId = form.value.userId
+ const rIds = roleIds.value.join(",")
+ updateAuthRole({ userId: userId, roleIds: rIds }).then(response => {
+ proxy.$modal.msgSuccess("鎺堟潈鎴愬姛")
+ close()
+ })
+}
+
+(() => {
+ const userId = route.params && route.params.userId
+ if (userId) {
+ loading.value = true
+ getAuthRole(userId).then(response => {
+ form.value = response.user
+ roles.value = response.roles
+ total.value = roles.value.length
+ nextTick(() => {
+ roles.value.forEach(row => {
+ if (row.flag) {
+ proxy.$refs["roleRef"].toggleRowSelection(row)
+ }
+ })
+ })
+ loading.value = false
+ })
+ }
+})()
+</script>
diff --git a/src/views/system/user/index.vue b/src/views/system/user/index.vue
new file mode 100644
index 0000000..f3059f8
--- /dev/null
+++ b/src/views/system/user/index.vue
@@ -0,0 +1,551 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="20" style="height: calc(100vh - 8em)">
+ <splitpanes :horizontal="appStore.device === 'mobile'" class="default-theme">
+ <!--閮ㄩ棬鏁版嵁-->
+ <pane size="16">
+ <el-col style="padding: 10px">
+ <div class="head-container">
+ <el-input v-model="deptNames" placeholder="璇疯緭鍏ラ儴闂ㄥ悕绉�" clearable prefix-icon="Search" style="margin-bottom: 20px" />
+ </div>
+ <div class="head-container">
+ <el-tree :data="deptOptions" :props="{ label: 'label', children: 'children' }" :expand-on-click-node="false" :filter-node-method="filterNode" ref="deptTreeRef" node-key="id" highlight-current default-expand-all @node-click="handleNodeClick" />
+ </div>
+ </el-col>
+ </pane>
+ <!--鐢ㄦ埛鏁版嵁-->
+ <pane size="84">
+ <el-col style="padding: 10px; height: 100%; display: flex; flex-direction: column;">
+ <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
+ <el-form-item label="鐧诲綍璐﹀彿" prop="userName">
+ <el-input v-model="queryParams.userName" placeholder="璇疯緭鍏ョ櫥褰曡处鍙�" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input v-model="queryParams.phonenumber" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" clearable style="width: 240px" @keyup.enter="handleQuery" />
+ </el-form-item>
+ <el-form-item label="鐘舵��" prop="status">
+ <el-select v-model="queryParams.status" placeholder="鐢ㄦ埛鐘舵��" clearable style="width: 240px">
+ <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" style="width: 308px">
+ <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="寮�濮嬫棩鏈�" end-placeholder="缁撴潫鏃ユ湡"></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['system:user:add']">鏂板</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['system:user:edit']">淇敼</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['system:user:remove']">鍒犻櫎</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['system:user:import']">瀵煎叆</el-button>
+ </el-col>
+ <el-col :span="1.5">
+ <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['system:user:export']">瀵煎嚭</el-button>
+ </el-col>
+ <right-toolbar v-model:showSearch="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
+ </el-row>
+
+ <div style="flex: 1; overflow: hidden;">
+ <el-table v-loading="loading" :data="userList" height="100%" @selection-change="handleSelectionChange">
+ <el-table-column type="selection" width="50" align="center" />
+ <el-table-column label="鐢ㄦ埛缂栧彿" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
+ <el-table-column label="鐧诲綍璐﹀彿" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="鐢ㄦ埛鏄电О" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="閮ㄩ棬" align="center" key="deptNames" prop="deptNames" v-if="columns[3].visible" :show-overflow-tooltip="true" />
+ <el-table-column label="鎵嬫満鍙风爜" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
+ <el-table-column label="鐘舵��" align="center" key="status" v-if="columns[5].visible">
+ <template #default="scope">
+ <el-switch
+ v-model="scope.row.status"
+ active-value="0"
+ inactive-value="1"
+ @change="handleStatusChange(scope.row)"
+ ></el-switch>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒涘缓鏃堕棿" align="center" prop="createTime" v-if="columns[6].visible" width="160">
+ <template #default="scope">
+ <span>{{ parseTime(scope.row.createTime) }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" align="center" width="150" class-name="small-padding fixed-width">
+ <template #default="scope">
+ <el-tooltip content="淇敼" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="閲嶇疆瀵嗙爜" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="Key" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']"></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒嗛厤瑙掕壊" placement="top" v-if="scope.row.userId !== 1">
+ <el-button link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)" v-hasPermi="['system:user:edit']"></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ </div>
+ <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+ </el-col>
+ </pane>
+ </splitpanes>
+ </el-row>
+
+ <!-- 娣诲姞鎴栦慨鏀圭敤鎴烽厤缃璇濇 -->
+ <el-dialog :title="title" v-model="open" width="600px" append-to-body>
+ <el-form :model="form" :rules="rules" ref="userRef" label-width="80px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item v-if="form.userId == undefined" label="鐧诲綍璐﹀彿" prop="userName">
+ <el-input v-model="form.userName" placeholder="璇疯緭鍏ョ敤鎴峰悕绉�" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item v-if="form.userId == undefined" label="鐢ㄦ埛瀵嗙爜" prop="password">
+ <el-input v-model="form.password" placeholder="璇疯緭鍏ョ敤鎴峰瘑鐮�" type="password" maxlength="20" show-password />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickName">
+ <el-input v-model="form.nickName" placeholder="璇疯緭鍏ョ敤鎴锋樀绉�" maxlength="30" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="褰掑睘閮ㄩ棬" prop="deptId">
+ <el-tree-select v-model="form.deptId" :data="enabledDeptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="璇烽�夋嫨褰掑睘閮ㄩ棬" check-strictly />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="宀椾綅" prop="postIds">
+ <el-select v-model="form.postIds" multiple placeholder="璇烽�夋嫨">
+ <el-option v-for="item in postOptions" :key="item.postId" :label="item.postName" :value="item.postId" :disabled="item.status == 1"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瑙掕壊" prop="roleIds">
+ <el-select v-model="form.roleIds" multiple placeholder="璇烽�夋嫨">
+ <el-option v-for="item in roleOptions" :key="item.roleId" :label="item.roleName" :value="item.roleId" :disabled="item.status == 1"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input v-model="form.phonenumber" placeholder="璇疯緭鍏ユ墜鏈哄彿鐮�" maxlength="11" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" placeholder="璇疯緭鍏ラ偖绠�" maxlength="50" />
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="鐢ㄦ埛鎬у埆">
+ <el-select v-model="form.sex" placeholder="璇烽�夋嫨">
+ <el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="鐘舵��">
+ <el-radio-group v-model="form.status">
+ <el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ <el-row>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞">
+ <el-input v-model="form.remark" type="textarea" placeholder="璇疯緭鍏ュ唴瀹�"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitForm">纭� 瀹�</el-button>
+ <el-button @click="cancel">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+
+ <!-- 鐢ㄦ埛瀵煎叆瀵硅瘽妗� -->
+ <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
+ <el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
+ <el-icon class="el-icon--upload"><upload-filled /></el-icon>
+ <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+ <template #tip>
+ <div class="el-upload__tip text-center">
+ <div class="el-upload__tip">
+ <el-checkbox v-model="upload.updateSupport" />鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+ </div>
+ <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
+ <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">涓嬭浇妯℃澘</el-link>
+ </div>
+ </template>
+ </el-upload>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="submitFileForm">纭� 瀹�</el-button>
+ <el-button @click="upload.open = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup name="User">
+import { getToken } from "@/utils/auth"
+import useAppStore from '@/store/modules/app'
+import { changeUserStatus, listUser, resetUserPwd, delUser, getUser, updateUser, addUser, deptTreeSelect } from "@/api/system/user"
+import { Splitpanes, Pane } from "splitpanes"
+import "splitpanes/dist/splitpanes.css"
+
+const router = useRouter()
+const appStore = useAppStore()
+const { proxy } = getCurrentInstance()
+const { sys_normal_disable, sys_user_sex } = proxy.useDict("sys_normal_disable", "sys_user_sex")
+
+const userList = ref([])
+const open = ref(false)
+const loading = ref(true)
+const showSearch = ref(true)
+const ids = ref([])
+const single = ref(true)
+const multiple = ref(true)
+const total = ref(0)
+const title = ref("")
+const dateRange = ref([])
+const deptNames = ref("")
+const deptOptions = ref(undefined)
+const enabledDeptOptions = ref(undefined)
+const initPassword = ref(undefined)
+const postOptions = ref([])
+const roleOptions = ref([])
+/*** 鐢ㄦ埛瀵煎叆鍙傛暟 */
+const upload = reactive({
+ // 鏄惁鏄剧ず寮瑰嚭灞傦紙鐢ㄦ埛瀵煎叆锛�
+ open: false,
+ // 寮瑰嚭灞傛爣棰橈紙鐢ㄦ埛瀵煎叆锛�
+ title: "",
+ // 鏄惁绂佺敤涓婁紶
+ isUploading: false,
+ // 鏄惁鏇存柊宸茬粡瀛樺湪鐨勭敤鎴锋暟鎹�
+ updateSupport: 0,
+ // 璁剧疆涓婁紶鐨勮姹傚ご閮�
+ headers: { Authorization: "Bearer " + getToken() },
+ // 涓婁紶鐨勫湴鍧�
+ url: import.meta.env.VITE_APP_BASE_API + "/system/user/importData"
+})
+// 鍒楁樉闅愪俊鎭�
+const columns = ref([
+ { key: 0, label: `鐢ㄦ埛缂栧彿`, visible: true },
+ { key: 1, label: `鐧诲綍璐﹀彿`, visible: true },
+ { key: 2, label: `鐢ㄦ埛鏄电О`, visible: true },
+ { key: 3, label: `閮ㄩ棬`, visible: true },
+ { key: 4, label: `鎵嬫満鍙风爜`, visible: true },
+ { key: 5, label: `鐘舵�乣, visible: true },
+ { key: 6, label: `鍒涘缓鏃堕棿`, visible: true }
+])
+
+const data = reactive({
+ form: {},
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ userName: undefined,
+ phonenumber: undefined,
+ status: undefined,
+ deptId: undefined
+ },
+ rules: {
+ userName: [{ required: true, message: "鐢ㄦ埛鍚嶇О涓嶈兘涓虹┖", trigger: "blur" }, { min: 2, max: 20, message: "鐢ㄦ埛鍚嶇О闀垮害蹇呴』浠嬩簬 2 鍜� 20 涔嬮棿", trigger: "blur" }],
+ nickName: [{ required: true, message: "鐢ㄦ埛鏄电О涓嶈兘涓虹┖", trigger: "blur" }],
+ password: [{ required: true, message: "鐢ㄦ埛瀵嗙爜涓嶈兘涓虹┖", trigger: "blur" }, { min: 5, max: 20, message: "鐢ㄦ埛瀵嗙爜闀垮害蹇呴』浠嬩簬 5 鍜� 20 涔嬮棿", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > \" ' \\\ |", trigger: "blur" }],
+ email: [{ type: "email", message: "璇疯緭鍏ユ纭殑閭鍦板潃", trigger: ["blur", "change"] }],
+ phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur" }],
+ deptId: [{ required: true, message: "褰掑睘閮ㄩ棬涓嶈兘涓虹┖", trigger: "change" }],
+ postIds: [{ required: true, message: "宀椾綅涓嶈兘涓虹┖", trigger: "change" }],
+ roleIds: [{ required: true, message: "瑙掕壊涓嶈兘涓虹┖", trigger: "change" }]
+ }
+})
+
+const { queryParams, form, rules } = toRefs(data)
+
+/** 閫氳繃鏉′欢杩囨护鑺傜偣 */
+const filterNode = (value, data) => {
+ if (!value) return true
+ return data.label.indexOf(value) !== -1
+}
+
+/** 鏍规嵁鍚嶇О绛涢�夐儴闂ㄦ爲 */
+watch(deptNames, val => {
+ proxy.$refs["deptTreeRef"].filter(val)
+})
+
+/** 鏌ヨ鐢ㄦ埛鍒楄〃 */
+function getList() {
+ loading.value = true
+ listUser(proxy.addDateRange(queryParams.value, dateRange.value)).then(res => {
+ loading.value = false
+ userList.value = res.rows
+ total.value = res.total
+ })
+}
+
+/** 鏌ヨ閮ㄩ棬涓嬫媺鏍戠粨鏋� */
+function getDeptTree() {
+ deptTreeSelect().then(response => {
+ deptOptions.value = response.data
+ enabledDeptOptions.value = filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
+ })
+}
+
+/** 杩囨护绂佺敤鐨勯儴闂� */
+function filterDisabledDept(deptList) {
+ return deptList.filter(dept => {
+ if (dept.disabled) {
+ return false
+ }
+ if (dept.children && dept.children.length) {
+ dept.children = filterDisabledDept(dept.children)
+ }
+ return true
+ })
+}
+
+/** 鑺傜偣鍗曞嚮浜嬩欢 */
+function handleNodeClick(data) {
+ queryParams.value.deptId = data.id
+ handleQuery()
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = []
+ proxy.resetForm("queryRef")
+ queryParams.value.deptId = undefined
+ proxy.$refs.deptTreeRef.setCurrentKey(null)
+ handleQuery()
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const userIds = row.userId || ids.value
+ proxy.$modal.confirm('鏄惁纭鍒犻櫎鐢ㄦ埛缂栧彿涓�"' + userIds + '"鐨勬暟鎹」锛�').then(function () {
+ return delUser(userIds)
+ }).then(() => {
+ getList()
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛")
+ }).catch(() => {})
+}
+
+/** 瀵煎嚭鎸夐挳鎿嶄綔 */
+function handleExport() {
+ proxy.download("system/user/export", {
+ ...queryParams.value,
+ },`user_${new Date().getTime()}.xlsx`)
+}
+
+/** 鐢ㄦ埛鐘舵�佷慨鏀� */
+function handleStatusChange(row) {
+ let text = row.status === "0" ? "鍚敤" : "鍋滅敤"
+ proxy.$modal.confirm('纭瑕�"' + text + '""' + row.userName + '"鐢ㄦ埛鍚�?').then(function () {
+ return changeUserStatus(row.userId, row.status)
+ }).then(() => {
+ proxy.$modal.msgSuccess(text + "鎴愬姛")
+ }).catch(function () {
+ row.status = row.status === "0" ? "1" : "0"
+ })
+}
+
+/** 鏇村鎿嶄綔 */
+function handleCommand(command, row) {
+ switch (command) {
+ case "handleResetPwd":
+ handleResetPwd(row)
+ break
+ case "handleAuthRole":
+ handleAuthRole(row)
+ break
+ default:
+ break
+ }
+}
+
+/** 璺宠浆瑙掕壊鍒嗛厤 */
+function handleAuthRole(row) {
+ const userId = row.userId
+ router.push("/system/user-auth/role/" + userId)
+}
+
+/** 閲嶇疆瀵嗙爜鎸夐挳鎿嶄綔 */
+function handleResetPwd(row) {
+ proxy.$prompt('璇疯緭鍏�"' + row.userName + '"鐨勬柊瀵嗙爜', "鎻愮ず", {
+ confirmButtonText: "纭畾",
+ cancelButtonText: "鍙栨秷",
+ closeOnClickModal: false,
+ inputPattern: /^.{5,20}$/,
+ inputErrorMessage: "鐢ㄦ埛瀵嗙爜闀垮害蹇呴』浠嬩簬 5 鍜� 20 涔嬮棿",
+ inputValidator: (value) => {
+ if (/<|>|"|'|\||\\/.test(value)) {
+ return "涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > \" ' \\\ |"
+ }
+ },
+ }).then(({ value }) => {
+ resetUserPwd(row.userId, value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛锛屾柊瀵嗙爜鏄細" + value)
+ })
+ }).catch(() => {})
+}
+
+/** 閫夋嫨鏉℃暟 */
+function handleSelectionChange(selection) {
+ ids.value = selection.map(item => item.userId)
+ single.value = selection.length != 1
+ multiple.value = !selection.length
+}
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImport() {
+ upload.title = "鐢ㄦ埛瀵煎叆"
+ upload.open = true
+}
+
+/** 涓嬭浇妯℃澘鎿嶄綔 */
+function importTemplate() {
+ proxy.download("system/user/importTemplate", {
+ }, `user_template_${new Date().getTime()}.xlsx`)
+}
+
+/**鏂囦欢涓婁紶涓鐞� */
+const handleFileUploadProgress = (event, file, fileList) => {
+ upload.isUploading = true
+}
+
+/** 鏂囦欢涓婁紶鎴愬姛澶勭悊 */
+const handleFileSuccess = (response, file, fileList) => {
+ upload.open = false
+ upload.isUploading = false
+ proxy.$refs["uploadRef"].handleRemove(file)
+ proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "瀵煎叆缁撴灉", { dangerouslyUseHTMLString: true })
+ getList()
+}
+
+/** 鎻愪氦涓婁紶鏂囦欢 */
+function submitFileForm() {
+ proxy.$refs["uploadRef"].submit()
+}
+
+/** 閲嶇疆鎿嶄綔琛ㄥ崟 */
+function reset() {
+ form.value = {
+ userId: undefined,
+ deptId: undefined,
+ userName: undefined,
+ nickName: undefined,
+ password: undefined,
+ phonenumber: undefined,
+ email: undefined,
+ sex: undefined,
+ status: "0",
+ remark: undefined,
+ postIds: [],
+ roleIds: []
+ }
+ proxy.resetForm("userRef")
+}
+
+/** 鍙栨秷鎸夐挳 */
+function cancel() {
+ open.value = false
+ reset()
+}
+
+/** 鏂板鎸夐挳鎿嶄綔 */
+function handleAdd() {
+ reset()
+ getUser().then(response => {
+ postOptions.value = response.posts
+ roleOptions.value = response.roles
+ open.value = true
+ title.value = "娣诲姞鐢ㄦ埛"
+ form.value.password = initPassword.value
+ })
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleUpdate(row) {
+ reset()
+ const userId = row.userId || ids.value
+ getUser(userId).then(response => {
+ form.value = response.data
+ postOptions.value = response.posts
+ roleOptions.value = response.roles
+ form.value.postIds = response.postIds
+ form.value.roleIds = response.roleIds
+ if (response.deptIds && response.deptIds.length > 0) {
+ form.value.deptId = response.deptIds[0]
+ }
+ open.value = true
+ title.value = "淇敼鐢ㄦ埛"
+ form.password = ""
+ })
+}
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ proxy.$refs["userRef"].validate(valid => {
+ if (valid) {
+ // 褰掑睘閮ㄩ棬铏界劧鏄崟閫夛紝浣嗗悗绔渶瑕佷紶鏁扮粍瀛楁 deptIds
+ const payload = {
+ ...form.value,
+ deptIds: form.value.deptId ? [form.value.deptId] : []
+ }
+ if (form.value.userId != undefined) {
+ updateUser(payload).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ open.value = false
+ getList()
+ })
+ } else {
+ addUser(payload).then(response => {
+ proxy.$modal.msgSuccess("鏂板鎴愬姛")
+ open.value = false
+ getList()
+ })
+ }
+ }
+ })
+}
+
+getDeptTree()
+getList()
+</script>
diff --git a/src/views/system/user/profile/index.vue b/src/views/system/user/profile/index.vue
new file mode 100644
index 0000000..d27c25b
--- /dev/null
+++ b/src/views/system/user/profile/index.vue
@@ -0,0 +1,87 @@
+<template>
+ <div class="app-container">
+ <el-row :gutter="20">
+ <el-col :span="6" :xs="24">
+ <el-card class="box-card">
+ <template v-slot:header>
+ <div class="clearfix">
+ <span>涓汉淇℃伅</span>
+ </div>
+ </template>
+ <div>
+ <div class="text-center">
+ <userAvatar />
+ </div>
+ <ul class="list-group list-group-striped">
+ <li class="list-group-item">
+ <svg-icon icon-class="user" />鐢ㄦ埛鍚嶇О
+ <div class="pull-right">{{ state.user.userName }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="phone" />鎵嬫満鍙风爜
+ <div class="pull-right">{{ state.user.phonenumber }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="email" />鐢ㄦ埛閭
+ <div class="pull-right">{{ state.user.email }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="tree" />鎵�灞為儴闂�
+ <div class="pull-right" v-if="state.user.dept">{{ state.user.dept.deptName }} / {{ state.postGroup }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="peoples" />鎵�灞炶鑹�
+ <div class="pull-right">{{ state.roleGroup }}</div>
+ </li>
+ <li class="list-group-item">
+ <svg-icon icon-class="date" />鍒涘缓鏃ユ湡
+ <div class="pull-right">{{ state.user.createTime }}</div>
+ </li>
+ </ul>
+ </div>
+ </el-card>
+ </el-col>
+ <el-col :span="18" :xs="24">
+ <el-card>
+ <template v-slot:header>
+ <div class="clearfix">
+ <span>鍩烘湰璧勬枡</span>
+ </div>
+ </template>
+ <el-tabs v-model="activeTab">
+ <el-tab-pane label="鍩烘湰璧勬枡" name="userinfo">
+ <userInfo :user="state.user" />
+ </el-tab-pane>
+ <el-tab-pane label="淇敼瀵嗙爜" name="resetPwd">
+ <resetPwd />
+ </el-tab-pane>
+ </el-tabs>
+ </el-card>
+ </el-col>
+ </el-row>
+ </div>
+</template>
+
+<script setup name="Profile">
+import userAvatar from "./userAvatar"
+import userInfo from "./userInfo"
+import resetPwd from "./resetPwd"
+import { getUserProfile } from "@/api/system/user"
+
+const activeTab = ref("userinfo")
+const state = reactive({
+ user: {},
+ roleGroup: {},
+ postGroup: {}
+})
+
+function getUser() {
+ getUserProfile().then(response => {
+ state.user = response.data
+ state.roleGroup = response.roleGroup
+ state.postGroup = response.postGroup
+ })
+}
+
+getUser()
+</script>
diff --git a/src/views/system/user/profile/resetPwd.vue b/src/views/system/user/profile/resetPwd.vue
new file mode 100644
index 0000000..308229a
--- /dev/null
+++ b/src/views/system/user/profile/resetPwd.vue
@@ -0,0 +1,59 @@
+<template>
+ <el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
+ <el-form-item label="鏃у瘑鐮�" prop="oldPassword">
+ <el-input v-model="user.oldPassword" placeholder="璇疯緭鍏ユ棫瀵嗙爜" type="password" show-password />
+ </el-form-item>
+ <el-form-item label="鏂板瘑鐮�" prop="newPassword">
+ <el-input v-model="user.newPassword" placeholder="璇疯緭鍏ユ柊瀵嗙爜" type="password" show-password />
+ </el-form-item>
+ <el-form-item label="纭瀵嗙爜" prop="confirmPassword">
+ <el-input v-model="user.confirmPassword" placeholder="璇风‘璁ゆ柊瀵嗙爜" type="password" show-password/>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="submit">淇濆瓨</el-button>
+ <el-button type="danger" @click="close">鍏抽棴</el-button>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+import { updateUserPwd } from "@/api/system/user"
+
+const { proxy } = getCurrentInstance()
+
+const user = reactive({
+ oldPassword: undefined,
+ newPassword: undefined,
+ confirmPassword: undefined
+})
+
+const equalToPassword = (rule, value, callback) => {
+ if (user.newPassword !== value) {
+ callback(new Error("涓ゆ杈撳叆鐨勫瘑鐮佷笉涓�鑷�"))
+ } else {
+ callback()
+ }
+}
+
+const rules = ref({
+ oldPassword: [{ required: true, message: "鏃у瘑鐮佷笉鑳戒负绌�", trigger: "blur" }],
+ newPassword: [{ required: true, message: "鏂板瘑鐮佷笉鑳戒负绌�", trigger: "blur" }, { min: 6, max: 20, message: "闀垮害鍦� 6 鍒� 20 涓瓧绗�", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "涓嶈兘鍖呭惈闈炴硶瀛楃锛�< > \" ' \\\ |", trigger: "blur" }],
+ confirmPassword: [{ required: true, message: "纭瀵嗙爜涓嶈兘涓虹┖", trigger: "blur" }, { required: true, validator: equalToPassword, trigger: "blur" }]
+})
+
+/** 鎻愪氦鎸夐挳 */
+function submit() {
+ proxy.$refs.pwdRef.validate(valid => {
+ if (valid) {
+ updateUserPwd(user.oldPassword, user.newPassword).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ })
+ }
+ })
+}
+
+/** 鍏抽棴鎸夐挳 */
+function close() {
+ proxy.$tab.closePage()
+}
+</script>
diff --git a/src/views/system/user/profile/userAvatar.vue b/src/views/system/user/profile/userAvatar.vue
new file mode 100644
index 0000000..ff1c9ef
--- /dev/null
+++ b/src/views/system/user/profile/userAvatar.vue
@@ -0,0 +1,168 @@
+<template>
+ <div class="user-info-head" @click="editCropper()">
+ <img :src="options.img" title="鐐瑰嚮涓婁紶澶村儚" class="img-circle img-lg" />
+ <el-dialog :title="title" v-model="open" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
+ <el-row>
+ <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+ <vue-cropper ref="cropper" :img="options.img" :info="true" :autoCrop="options.autoCrop"
+ :autoCropWidth="options.autoCropWidth" :autoCropHeight="options.autoCropHeight" :fixedBox="options.fixedBox"
+ :outputType="options.outputType" @realTime="realTime" v-if="visible" />
+ </el-col>
+ <el-col :xs="24" :md="12" :style="{ height: '350px' }">
+ <div class="avatar-upload-preview">
+ <img :src="options.previews.url" :style="options.previews.img" />
+ </div>
+ </el-col>
+ </el-row>
+ <br />
+ <el-row>
+ <el-col :lg="2" :md="2">
+ <el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
+ <el-button>
+ 閫夋嫨
+ <el-icon class="el-icon--right">
+ <Upload />
+ </el-icon>
+ </el-button>
+ </el-upload>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 2 }" :md="2">
+ <el-button icon="Plus" @click="changeScale(1)"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="Minus" @click="changeScale(-1)"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 1, offset: 1 }" :md="2">
+ <el-button icon="RefreshRight" @click="rotateRight()"></el-button>
+ </el-col>
+ <el-col :lg="{ span: 2, offset: 6 }" :md="2">
+ <el-button type="primary" @click="uploadImg()">鎻� 浜�</el-button>
+ </el-col>
+ </el-row>
+ </el-dialog>
+ </div>
+</template>
+
+<script setup>
+import "vue-cropper/dist/index.css"
+import { VueCropper } from "vue-cropper"
+import { uploadAvatar } from "@/api/system/user"
+import useUserStore from "@/store/modules/user"
+
+const userStore = useUserStore()
+const { proxy } = getCurrentInstance()
+
+const open = ref(false)
+const visible = ref(false)
+const title = ref("淇敼澶村儚")
+
+//鍥剧墖瑁佸壀鏁版嵁
+const options = reactive({
+ img: userStore.avatar, // 瑁佸壀鍥剧墖鐨勫湴鍧�
+ autoCrop: true, // 鏄惁榛樿鐢熸垚鎴浘妗�
+ autoCropWidth: 200, // 榛樿鐢熸垚鎴浘妗嗗搴�
+ autoCropHeight: 200, // 榛樿鐢熸垚鎴浘妗嗛珮搴�
+ fixedBox: true, // 鍥哄畾鎴浘妗嗗ぇ灏� 涓嶅厑璁告敼鍙�
+ outputType: "png", // 榛樿鐢熸垚鎴浘涓篜NG鏍煎紡
+ filename: 'avatar', // 鏂囦欢鍚嶇О
+ previews: {} //棰勮鏁版嵁
+})
+
+/** 缂栬緫澶村儚 */
+function editCropper() {
+ open.value = true
+}
+
+/** 鎵撳紑寮瑰嚭灞傜粨鏉熸椂鐨勫洖璋� */
+function modalOpened() {
+ visible.value = true
+}
+
+/** 瑕嗙洊榛樿涓婁紶琛屼负 */
+function requestUpload() { }
+
+/** 鍚戝乏鏃嬭浆 */
+function rotateLeft() {
+ proxy.$refs.cropper.rotateLeft()
+}
+
+/** 鍚戝彸鏃嬭浆 */
+function rotateRight() {
+ proxy.$refs.cropper.rotateRight()
+}
+
+/** 鍥剧墖缂╂斁 */
+function changeScale(num) {
+ num = num || 1
+ proxy.$refs.cropper.changeScale(num)
+}
+
+/** 涓婁紶棰勫鐞� */
+function beforeUpload(file) {
+ if (file.type.indexOf("image/") == -1) {
+ proxy.$modal.msgError("鏂囦欢鏍煎紡閿欒锛岃涓婁紶鍥剧墖绫诲瀷,濡傦細JPG锛孭NG鍚庣紑鐨勬枃浠躲��")
+ } else {
+ const reader = new FileReader()
+ reader.readAsDataURL(file)
+ reader.onload = () => {
+ options.img = reader.result
+ options.filename = file.name
+ }
+ }
+}
+
+/** 涓婁紶鍥剧墖 */
+function uploadImg() {
+ proxy.$refs.cropper.getCropBlob(data => {
+ let formData = new FormData()
+ formData.append("avatarfile", data, options.filename)
+ uploadAvatar(formData).then(response => {
+ open.value = false
+ options.img = import.meta.env.VITE_APP_BASE_API + '/profile/' + response.imgUrl
+ userStore.avatar = options.img
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ visible.value = false
+ })
+ })
+}
+
+/** 瀹炴椂棰勮 */
+function realTime(data) {
+ options.previews = data
+}
+
+/** 鍏抽棴绐楀彛 */
+function closeDialog() {
+ options.img = userStore.avatar
+ options.visible = false
+}
+</script>
+
+<style lang='scss' scoped>
+.user-info-head {
+ position: relative;
+ display: inline-block;
+ height: 120px;
+}
+
+.user-info-head:hover:after {
+ content: "+";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ color: #eee;
+ background: rgba(0, 0, 0, 0.5);
+ font-size: 24px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ cursor: pointer;
+ line-height: 110px;
+ border-radius: 50%;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/system/user/profile/userInfo.vue b/src/views/system/user/profile/userInfo.vue
new file mode 100644
index 0000000..77f6924
--- /dev/null
+++ b/src/views/system/user/profile/userInfo.vue
@@ -0,0 +1,67 @@
+<template>
+ <el-form ref="userRef" :model="form" :rules="rules" label-width="80px">
+ <el-form-item label="鐢ㄦ埛鏄电О" prop="nickName">
+ <el-input v-model="form.nickName" maxlength="30" />
+ </el-form-item>
+ <el-form-item label="鎵嬫満鍙风爜" prop="phonenumber">
+ <el-input v-model="form.phonenumber" maxlength="11" />
+ </el-form-item>
+ <el-form-item label="閭" prop="email">
+ <el-input v-model="form.email" maxlength="50" />
+ </el-form-item>
+ <el-form-item label="鎬у埆">
+ <el-radio-group v-model="form.sex">
+ <el-radio value="0">鐢�</el-radio>
+ <el-radio value="1">濂�</el-radio>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" @click="submit">淇濆瓨</el-button>
+ <el-button type="danger" @click="close">鍏抽棴</el-button>
+ </el-form-item>
+ </el-form>
+</template>
+
+<script setup>
+import { updateUserProfile } from "@/api/system/user"
+
+const props = defineProps({
+ user: {
+ type: Object
+ }
+})
+
+const { proxy } = getCurrentInstance()
+
+const form = ref({})
+const rules = ref({
+ nickName: [{ required: true, message: "鐢ㄦ埛鏄电О涓嶈兘涓虹┖", trigger: "blur" }],
+ email: [{ required: true, message: "閭鍦板潃涓嶈兘涓虹┖", trigger: "blur" }, { type: "email", message: "璇疯緭鍏ユ纭殑閭鍦板潃", trigger: ["blur", "change"] }],
+ phonenumber: [{ required: true, message: "鎵嬫満鍙风爜涓嶈兘涓虹┖", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "璇疯緭鍏ユ纭殑鎵嬫満鍙风爜", trigger: "blur" }],
+})
+
+/** 鎻愪氦鎸夐挳 */
+function submit() {
+ proxy.$refs.userRef.validate(valid => {
+ if (valid) {
+ updateUserProfile(form.value).then(response => {
+ proxy.$modal.msgSuccess("淇敼鎴愬姛")
+ props.user.phonenumber = form.value.phonenumber
+ props.user.email = form.value.email
+ })
+ }
+ })
+}
+
+/** 鍏抽棴鎸夐挳 */
+function close() {
+ proxy.$tab.closePage()
+}
+
+// 鍥炴樉褰撳墠鐧诲綍鐢ㄦ埛淇℃伅
+watch(() => props.user, user => {
+ if (user) {
+ form.value = { nickName: user.nickName, phonenumber: user.phonenumber, email: user.email, sex: user.sex }
+ }
+},{ immediate: true })
+</script>
diff --git a/src/views/tideLogin.vue b/src/views/tideLogin.vue
new file mode 100644
index 0000000..e4a82f7
--- /dev/null
+++ b/src/views/tideLogin.vue
@@ -0,0 +1,15 @@
+<template>
+ <div></div>
+</template>
+<script setup>
+import useUserStore from '@/store/modules/user'
+const userStore = useUserStore()
+let { proxy } = getCurrentInstance()
+function goLogin() {
+ userStore.TideLogin({code : proxy.$route.query.code}).then(() => {
+ proxy.$router.push({ path: redirect || "/" }).catch(() => { });
+ })
+}
+goLogin()
+</script>
+<style scoped></style>
diff --git a/src/views/tool/build/CodeTypeDialog.vue b/src/views/tool/build/CodeTypeDialog.vue
new file mode 100644
index 0000000..1a75789
--- /dev/null
+++ b/src/views/tool/build/CodeTypeDialog.vue
@@ -0,0 +1,71 @@
+<template>
+ <el-dialog v-model="open" width="500px" title="閫夋嫨鐢熸垚绫诲瀷" @open="onOpen" @close="onClose">
+ <el-form ref="codeTypeForm" :model="formData" :rules="rules" label-width="100px">
+ <el-form-item label="鐢熸垚绫诲瀷" prop="type">
+ <el-radio-group v-model="formData.type">
+ <el-radio-button v-for="(item, index) in typeOptions" :key="index" :label="item.value">
+ {{ item.label }}
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="showFileName" label="鏂囦欢鍚�" prop="fileName">
+ <el-input v-model="formData.fileName" placeholder="璇疯緭鍏ユ枃浠跺悕" clearable />
+ </el-form-item>
+ </el-form>
+
+ <template #footer>
+ <el-button type="primary" @click="handelConfirm">纭畾</el-button>
+ <el-button @click="onClose">鍙栨秷</el-button>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+const open = defineModel()
+const props = defineProps({
+ showFileName: Boolean
+})
+const emit = defineEmits(['confirm'])
+const formData = ref({
+ fileName: undefined,
+ type: 'file'
+})
+const codeTypeForm = ref()
+const rules = {
+ fileName: [{
+ required: true,
+ message: '璇疯緭鍏ユ枃浠跺悕',
+ trigger: 'blur'
+ }],
+ type: [{
+ required: true,
+ message: '鐢熸垚绫诲瀷涓嶈兘涓虹┖',
+ trigger: 'change'
+ }]
+}
+const typeOptions = ref([
+ {
+ label: '椤甸潰',
+ value: 'file'
+ },
+ {
+ label: '寮圭獥',
+ value: 'dialog'
+ }
+])
+function onOpen() {
+ if (props.showFileName) {
+ formData.value.fileName = `${+new Date()}.vue`
+ }
+}
+function onClose() {
+ open.value = false
+}
+function handelConfirm() {
+ codeTypeForm.value.validate(valid => {
+ if (!valid) return
+ emit('confirm', { ...formData.value })
+ onClose()
+ })
+}
+</script>
\ No newline at end of file
diff --git a/src/views/tool/build/DraggableItem.vue b/src/views/tool/build/DraggableItem.vue
new file mode 100644
index 0000000..9ae2354
--- /dev/null
+++ b/src/views/tool/build/DraggableItem.vue
@@ -0,0 +1,68 @@
+<template>
+ <el-col :span="element.span" :class="className" @click.stop="activeItem(element)">
+ <el-form-item :label="element.label" :label-width="element.labelWidth ? element.labelWidth + 'px' : null"
+ :required="element.required" v-if="element.layout === 'colFormItem'">
+ <render :key="element.tag" :conf="element" v-model="element.defaultValue" />
+ </el-form-item>
+ <el-row :gutter="element.gutter" :class="element.class" @click.stop="activeItem(element)" v-else>
+ <span class="component-name"> {{ element.componentName }} </span>
+ <draggable group="componentsGroup" :animation="340" :list="element.children" class="drag-wrapper" item-key="label"
+ ref="draggableItemRef" :component-data="getComponentData()">
+ <template #item="scoped">
+ <draggable-item :key="scoped.element.renderKey" :drawing-list="element.children" :element="scoped.element"
+ :index="index" :active-id="activeId" :form-conf="formConf" @activeItem="activeItem(scoped.element)"
+ @copyItem="copyItem(scoped.element, element.children)"
+ @deleteItem="deleteItem(scoped.index, element.children)" />
+ </template>
+ </draggable>
+ </el-row>
+ <span class="drawing-item-copy" title="澶嶅埗" @click.stop="copyItem(element)">
+ <el-icon><CopyDocument /></el-icon>
+ </span>
+ <span class="drawing-item-delete" title="鍒犻櫎" @click.stop="deleteItem(index)">
+ <el-icon><Delete /></el-icon>
+ </span>
+ </el-col>
+</template>
+<script setup name="DraggableItem">
+import draggable from "vuedraggable/dist/vuedraggable.common"
+import render from '@/utils/generator/render'
+
+const props = defineProps({
+ element: Object,
+ index: Number,
+ drawingList: Array,
+ activeId: {
+ type: [String, Number]
+ },
+ formConf: Object
+})
+const className = ref('')
+const draggableItemRef = ref(null)
+const emits = defineEmits(['activeItem', 'copyItem', 'deleteItem'])
+
+function activeItem(item) {
+ emits('activeItem', item)
+}
+function copyItem(item, parent) {
+ emits('copyItem', item, parent ?? props.drawingList)
+}
+function deleteItem(item, parent) {
+ emits('deleteItem', item, parent ?? props.drawingList)
+}
+
+function getComponentData() {
+ return {
+ gutter: props.element.gutter,
+ justify: props.element.justify,
+ align: props.element.align
+ }
+}
+
+watch(() => props.activeId, (val) => {
+ className.value = (props.element.layout === 'rowFormItem' ? 'drawing-row-item' : 'drawing-item') + (val === props.element.formId ? ' active-from-item' : '')
+ if (props.formConf.unFocusedComponentBorder) {
+ className.value += ' unfocus-bordered'
+ }
+}, { immediate: true })
+</script>
\ No newline at end of file
diff --git a/src/views/tool/build/IconsDialog.vue b/src/views/tool/build/IconsDialog.vue
new file mode 100644
index 0000000..98d9c13
--- /dev/null
+++ b/src/views/tool/build/IconsDialog.vue
@@ -0,0 +1,115 @@
+<template>
+ <div class="icon-dialog">
+ <el-dialog v-model="value" width="980px" :close-on-click-modal="false" :modal-append-to-body="false" @open="onOpen"
+ @close="onClose">
+ <template #header="{ close, titleId, titleClass }">
+ 閫夋嫨鍥炬爣
+ <el-input v-model="key" size="small" :style="{ width: '260px' }" placeholder="璇疯緭鍏ュ浘鏍囧悕绉�" prefix-icon="Search"
+ clearable />
+ </template>
+ <ul class="icon-ul">
+ <li v-for="icon in iconList" :key="icon" :class="active === icon ? 'active-item' : ''" @click="onSelect(icon)">
+ <div>
+ <el-icon :size="30">
+ <component :is="icon" />
+ </el-icon>
+ <div>{{ icon }}</div>
+ </div>
+ </li>
+ </ul>
+ </el-dialog>
+ </div>
+</template>
+<script setup>
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import { watch } from 'vue'
+
+const iconList = ref([])
+const originList = []
+const key = ref('')
+const active = ref('')
+const emit = defineEmits(['select'])
+const value = defineModel()
+for (const [key] of Object.entries(ElementPlusIconsVue)) {
+ iconList.value.push(key)
+ originList.push(key)
+}
+
+function onOpen() { }
+function onClose() { }
+function onSelect(icon) {
+ active.value = icon
+ emit('select', icon)
+ value.value = false
+}
+
+watch(key, (val) => {
+ if (val) {
+ iconList.value = originList.filter(name => name.indexOf(val) > -1)
+ } else {
+ iconList.value = originList
+ }
+})
+</script>
+<style lang="scss" scoped>
+.icon-ul {
+ margin: 0;
+ padding: 0;
+ font-size: 0;
+
+ li {
+ list-style-type: none;
+ text-align: center;
+ font-size: 14px;
+ display: inline-flex;
+ width: 16.66%;
+ box-sizing: border-box;
+ height: 108px;
+ padding: 6px 6px 6px 6px;
+ cursor: pointer;
+ overflow: hidden;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ background: #f2f2f2;
+ }
+
+ &.active-item {
+ background: #e1f3fb;
+ color: #7a6df0
+ }
+
+ i {
+ font-size: 30px;
+ line-height: 50px;
+ margin-bottom: 10px;
+ }
+ }
+}
+
+.icon-dialog {
+ :deep() {
+ .el-dialog {
+ border-radius: 8px;
+ margin-bottom: 0;
+ margin-top: 4vh !important;
+ display: flex;
+ flex-direction: column;
+ max-height: 92vh;
+ overflow: hidden;
+ box-sizing: border-box;
+
+ .el-dialog__header {
+ padding-top: 14px;
+ }
+
+ .el-dialog__body {
+ margin: 0 20px 20px 20px;
+ padding: 0;
+ overflow: auto;
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/tool/build/RightPanel.vue b/src/views/tool/build/RightPanel.vue
new file mode 100644
index 0000000..5fe80fb
--- /dev/null
+++ b/src/views/tool/build/RightPanel.vue
@@ -0,0 +1,906 @@
+<template>
+ <div class="right-board">
+ <el-tabs v-model="currentTab" stretch class="center-tabs">
+ <el-tab-pane label="缁勪欢灞炴��" name="field" />
+ <el-tab-pane label="琛ㄥ崟灞炴��" name="form" />
+ </el-tabs>
+ <div class="field-box">
+ <a class="document-link" target="_blank" :href="documentLink" title="鏌ョ湅缁勪欢鏂囨。">
+ <el-icon>
+ <Link />
+ </el-icon>
+ </a>
+ <el-scrollbar class="right-scrollbar">
+ <!-- 缁勪欢灞炴�� -->
+ <el-form v-show="currentTab === 'field' && showField" size="default" label-width="90px" label-position="top"
+ style="">
+ <el-form-item v-if="activeData.changeTag" label="缁勪欢绫诲瀷">
+ <el-select v-model="activeData.tagIcon" placeholder="璇烽�夋嫨缁勪欢绫诲瀷" :style="{ width: '100%' }" @change="tagChange">
+ <el-option-group v-for="group in tagList" :key="group.label" :label="group.label">
+ <el-option v-for="item in group.options" :key="item.label" :label="item.label" :value="item.tagIcon">
+ <svg-icon class="node-icon" :icon-class="item.tagIcon" style="margin-right: 10px;" />
+ <span> {{ item.label }}</span>
+ </el-option>
+ </el-option-group>
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="activeData.vModel !== undefined" label="瀛楁鍚�">
+ <el-input v-model="activeData.vModel" placeholder="璇疯緭鍏ュ瓧娈靛悕锛坴-model锛�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.componentName !== undefined" label="缁勪欢鍚�">
+ {{ activeData.componentName }}
+ </el-form-item>
+ <el-form-item v-if="activeData.label !== undefined" label="鏍囬">
+ <el-input v-model="activeData.label" placeholder="璇疯緭鍏ユ爣棰�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.placeholder !== undefined" label="鍗犱綅鎻愮ず">
+ <el-input v-model="activeData.placeholder" placeholder="璇疯緭鍏ュ崰浣嶆彁绀�" />
+ </el-form-item>
+ <el-form-item v-if="activeData['start-placeholder'] !== undefined" label="寮�濮嬪崰浣�">
+ <el-input v-model="activeData['start-placeholder']" placeholder="璇疯緭鍏ュ崰浣嶆彁绀�" />
+ </el-form-item>
+ <el-form-item v-if="activeData['end-placeholder'] !== undefined" label="缁撴潫鍗犱綅">
+ <el-input v-model="activeData['end-placeholder']" placeholder="璇疯緭鍏ュ崰浣嶆彁绀�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.span !== undefined" label="琛ㄥ崟鏍呮牸">
+ <el-slider v-model="activeData.span" :max="24" :min="1" :marks="{ 12: '' }" @change="spanChange" />
+ </el-form-item>
+ <el-form-item v-if="activeData.layout === 'rowFormItem'" label="鏍呮牸闂撮殧">
+ <el-input-number v-model="activeData.gutter" :min="0" placeholder="鏍呮牸闂撮殧" />
+ </el-form-item>
+
+ <el-form-item v-if="activeData.justify !== undefined" label="姘村钩鎺掑垪">
+ <el-select v-model="activeData.justify" placeholder="璇烽�夋嫨姘村钩鎺掑垪" :style="{ width: '100%' }">
+ <el-option v-for="(item, index) in justifyOptions" :key="index" :label="item.label" :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="activeData.align !== undefined" label="鍨傜洿鎺掑垪">
+ <el-radio-group v-model="activeData.align">
+ <el-radio-button label="top" />
+ <el-radio-button label="middle" />
+ <el-radio-button label="bottom" />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="activeData.labelWidth !== undefined" label="鏍囩瀹藉害">
+ <el-input v-model.number="activeData.labelWidth" type="number" placeholder="璇疯緭鍏ユ爣绛惧搴�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.style && activeData.style.width !== undefined" label="缁勪欢瀹藉害">
+ <el-input v-model="activeData.style.width" placeholder="璇疯緭鍏ョ粍浠跺搴�" clearable />
+ </el-form-item>
+ <el-form-item v-if="activeData.vModel !== undefined" label="榛樿鍊�">
+ <el-input :value="setDefaultValue(activeData.defaultValue)" placeholder="璇疯緭鍏ラ粯璁ゅ��"
+ @input="onDefaultValueInput" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-checkbox-group'" label="鑷冲皯搴旈��">
+ <el-input-number :value="activeData.min" :min="0" placeholder="鑷冲皯搴旈��"
+ @input="$set(activeData, 'min', $event ? $event : undefined)" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-checkbox-group'" label="鏈�澶氬彲閫�">
+ <el-input-number :value="activeData.max" :min="0" placeholder="鏈�澶氬彲閫�"
+ @input="$set(activeData, 'max', $event ? $event : undefined)" />
+ </el-form-item>
+ <el-form-item v-if="activeData.prepend !== undefined" label="鍓嶇紑">
+ <el-input v-model="activeData.prepend" placeholder="璇疯緭鍏ュ墠缂�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.append !== undefined" label="鍚庣紑">
+ <el-input v-model="activeData.append" placeholder="璇疯緭鍏ュ悗缂�" />
+ </el-form-item>
+ <el-form-item v-if="activeData['prefix-icon'] !== undefined" label="鍓嶅浘鏍�">
+ <el-input v-model="activeData['prefix-icon']" placeholder="璇疯緭鍏ュ墠鍥炬爣鍚嶇О">
+ <template #append>
+ <el-button icon="Pointer" @click="openIconsDialog('prefix-icon')">
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item v-if="activeData['suffix-icon'] !== undefined" label="鍚庡浘鏍�">
+ <el-input v-model="activeData['suffix-icon']" placeholder="璇疯緭鍏ュ悗鍥炬爣鍚嶇О">
+ <template #append>
+ <el-button icon="Pointer" @click="openIconsDialog('suffix-icon')">
+ 閫夋嫨
+ </el-button>
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-cascader'" label="閫夐」鍒嗛殧绗�">
+ <el-input v-model="activeData.separator" placeholder="璇疯緭鍏ラ�夐」鍒嗛殧绗�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.autosize !== undefined" label="鏈�灏忚鏁�">
+ <el-input-number v-model="activeData.autosize.minRows" :min="1" placeholder="鏈�灏忚鏁�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.autosize !== undefined" label="鏈�澶ц鏁�">
+ <el-input-number v-model="activeData.autosize.maxRows" :min="1" placeholder="鏈�澶ц鏁�" />
+ </el-form-item>
+ <el-form-item v-if="activeData.min !== undefined" label="鏈�灏忓��">
+ <el-input-number v-model="activeData.min" placeholder="鏈�灏忓��" />
+ </el-form-item>
+ <el-form-item v-if="activeData.max !== undefined" label="鏈�澶у��">
+ <el-input-number v-model="activeData.max" placeholder="鏈�澶у��" />
+ </el-form-item>
+ <el-form-item v-if="activeData.step !== undefined" label="姝ラ暱">
+ <el-input-number v-model="activeData.step" placeholder="姝ユ暟" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-input-number'" label="绮惧害">
+ <el-input-number v-model="activeData.precision" :min="0" placeholder="绮惧害" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-input-number'" label="鎸夐挳浣嶇疆">
+ <el-radio-group v-model="activeData['controls-position']">
+ <el-radio-button label="">
+ 榛樿
+ </el-radio-button>
+ <el-radio-button label="right">
+ 鍙充晶
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="activeData.maxlength !== undefined" label="鏈�澶氳緭鍏�">
+ <el-input v-model="activeData.maxlength" placeholder="璇疯緭鍏ュ瓧绗﹂暱搴�">
+ <template slot="append">
+ 涓瓧绗�
+ </template>
+ </el-input>
+ </el-form-item>
+ <el-form-item v-if="activeData['active-text'] !== undefined" label="寮�鍚彁绀�">
+ <el-input v-model="activeData['active-text']" placeholder="璇疯緭鍏ュ紑鍚彁绀�" />
+ </el-form-item>
+ <el-form-item v-if="activeData['inactive-text'] !== undefined" label="鍏抽棴鎻愮ず">
+ <el-input v-model="activeData['inactive-text']" placeholder="璇疯緭鍏ュ叧闂彁绀�" />
+ </el-form-item>
+ <el-form-item v-if="activeData['active-value'] !== undefined" label="寮�鍚��">
+ <el-input :value="setDefaultValue(activeData['active-value'])" placeholder="璇疯緭鍏ュ紑鍚��"
+ @input="onSwitchValueInput($event, 'active-value')" />
+ </el-form-item>
+ <el-form-item v-if="activeData['inactive-value'] !== undefined" label="鍏抽棴鍊�">
+ <el-input :value="setDefaultValue(activeData['inactive-value'])" placeholder="璇疯緭鍏ュ叧闂��"
+ @input="onSwitchValueInput($event, 'inactive-value')" />
+ </el-form-item>
+ <el-form-item v-if="activeData.type !== undefined && 'el-date-picker' === activeData.tag" label="鏃堕棿绫诲瀷">
+ <el-select v-model="activeData.type" placeholder="璇烽�夋嫨鏃堕棿绫诲瀷" :style="{ width: '100%' }"
+ @change="dateTypeChange">
+ <el-option v-for="(item, index) in dateOptions" :key="index" :label="item.label" :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="activeData.name !== undefined" label="鏂囦欢瀛楁鍚�">
+ <el-input v-model="activeData.name" placeholder="璇疯緭鍏ヤ笂浼犳枃浠跺瓧娈靛悕" />
+ </el-form-item>
+ <el-form-item v-if="activeData.accept !== undefined" label="鏂囦欢绫诲瀷">
+ <el-select v-model="activeData.accept" placeholder="璇烽�夋嫨鏂囦欢绫诲瀷" :style="{ width: '100%' }" clearable>
+ <el-option label="鍥剧墖" value="image/*" />
+ <el-option label="瑙嗛" value="video/*" />
+ <el-option label="闊抽" value="audio/*" />
+ <el-option label="excel" value=".xls,.xlsx" />
+ <el-option label="word" value=".doc,.docx" />
+ <el-option label="pdf" value=".pdf" />
+ <el-option label="txt" value=".txt" />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="activeData.fileSize !== undefined" label="鏂囦欢澶у皬">
+ <el-input v-model.number="activeData.fileSize" placeholder="璇疯緭鍏ユ枃浠跺ぇ灏�">
+ <el-select slot="append" v-model="activeData.sizeUnit" :style="{ width: '66px' }">
+ <el-option label="KB" value="KB" />
+ <el-option label="MB" value="MB" />
+ <el-option label="GB" value="GB" />
+ </el-select>
+ </el-input>
+ </el-form-item>
+ <el-form-item v-if="activeData.action !== undefined" label="涓婁紶鍦板潃">
+ <el-input v-model="activeData.action" placeholder="璇疯緭鍏ヤ笂浼犲湴鍧�" clearable />
+ </el-form-item>
+ <el-form-item v-if="activeData['list-type'] !== undefined" label="鍒楄〃绫诲瀷">
+ <el-radio-group v-model="activeData['list-type']" size="small">
+ <el-radio-button label="text">
+ text
+ </el-radio-button>
+ <el-radio-button label="picture">
+ picture
+ </el-radio-button>
+ <el-radio-button label="picture-card">
+ picture-card
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="activeData.buttonText !== undefined" v-show="'picture-card' !== activeData['list-type']"
+ label="鎸夐挳鏂囧瓧">
+ <el-input v-model="activeData.buttonText" placeholder="璇疯緭鍏ユ寜閽枃瀛�" />
+ </el-form-item>
+ <el-form-item v-if="activeData['range-separator'] !== undefined" label="鍒嗛殧绗�">
+ <el-input v-model="activeData['range-separator']" placeholder="璇疯緭鍏ュ垎闅旂" />
+ </el-form-item>
+ <el-form-item v-if="activeData['picker-options'] !== undefined" label="鏃堕棿娈�">
+ <el-input v-model="activeData['picker-options'].selectableRange" placeholder="璇疯緭鍏ユ椂闂存" />
+ </el-form-item>
+ <el-form-item v-if="activeData.format !== undefined" label="鏃堕棿鏍煎紡">
+ <el-input :value="activeData.format" placeholder="璇疯緭鍏ユ椂闂存牸寮�" @input="setTimeValue($event)" />
+ </el-form-item>
+ <template v-if="['el-checkbox-group', 'el-radio-group', 'el-select'].indexOf(activeData.tag) > -1">
+ <el-divider>閫夐」</el-divider>
+ <draggable :list="activeData.options" :animation="340" group="selectItem" handle=".option-drag"
+ item-key="label">
+ <template #item="{ element, index }">
+ <div :key="index" class="select-item">
+ <div class="select-line-icon option-drag">
+ <i class="el-icon-s-operation" />
+ </div>
+ <el-input v-model="element.label" placeholder="閫夐」鍚�" size="small" />
+ <el-input placeholder="閫夐」鍊�" size="small" :value="element.value"
+ @input="setOptionValue(element, $event)" />
+ <div class="close-btn select-line-icon" @click="activeData.options.splice(index, 1)">
+ <el-icon>
+ <Remove />
+ </el-icon>
+ </div>
+ </div>
+ </template>
+ </draggable>
+ <div>
+ <el-button icon="CirclePlus" style="margin-left: 8px; margin-top: 10px;" text bg type="primary"
+ @click="addSelectItem">
+ 娣诲姞閫夐」
+ </el-button>
+ </div>
+ <el-divider />
+ </template>
+
+ <template v-if="['el-cascader'].indexOf(activeData.tag) > -1">
+ <el-divider>閫夐」</el-divider>
+ <el-form-item label="鏁版嵁绫诲瀷">
+ <el-radio-group v-model="activeData.dataType" size="small">
+ <el-radio-button label="dynamic">
+ 鍔ㄦ�佹暟鎹�
+ </el-radio-button>
+ <el-radio-button label="static">
+ 闈欐�佹暟鎹�
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+
+ <template v-if="activeData.dataType === 'dynamic'">
+ <el-form-item label="鏍囩閿悕">
+ <el-input v-model="activeData.labelKey" placeholder="璇疯緭鍏ユ爣绛鹃敭鍚�" />
+ </el-form-item>
+ <el-form-item label="鍊奸敭鍚�">
+ <el-input v-model="activeData.valueKey" placeholder="璇疯緭鍏ュ�奸敭鍚�" />
+ </el-form-item>
+ <el-form-item label="瀛愮骇閿悕">
+ <el-input v-model="activeData.childrenKey" placeholder="璇疯緭鍏ュ瓙绾ч敭鍚�" />
+ </el-form-item>
+ </template>
+
+ <el-tree v-if="activeData.dataType === 'static'" draggable :data="activeData.options" node-key="id"
+ :expand-on-click-node="false" :render-content="renderContent" />
+ <div v-if="activeData.dataType === 'static'">
+ <el-button icon="CirclePlus" style="margin-left: 0; margin-top: 10px;" type="primary" text bg
+ @click="addTreeItem">
+ 娣诲姞鐖剁骇
+ </el-button>
+ </div>
+ <el-divider />
+ </template>
+
+ <el-form-item v-if="activeData.optionType !== undefined" label="閫夐」鏍峰紡">
+ <el-radio-group v-model="activeData.optionType">
+ <el-radio-button label="default">
+ 榛樿
+ </el-radio-button>
+ <el-radio-button label="button">
+ 鎸夐挳
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="activeData['active-color'] !== undefined" label="寮�鍚鑹�">
+ <el-color-picker v-model="activeData['active-color']" />
+ </el-form-item>
+ <el-form-item v-if="activeData['inactive-color'] !== undefined" label="鍏抽棴棰滆壊">
+ <el-color-picker v-model="activeData['inactive-color']" />
+ </el-form-item>
+
+ <el-form-item v-if="activeData['allow-half'] !== undefined" label="鍏佽鍗婇��">
+ <el-switch v-model="activeData['allow-half']" />
+ </el-form-item>
+ <el-form-item v-if="activeData['show-text'] !== undefined" label="杈呭姪鏂囧瓧">
+ <el-switch v-model="activeData['show-text']" @change="rateTextChange" />
+ </el-form-item>
+ <el-form-item v-if="activeData['show-score'] !== undefined" label="鏄剧ず鍒嗘暟">
+ <el-switch v-model="activeData['show-score']" @change="rateScoreChange" />
+ </el-form-item>
+ <el-form-item v-if="activeData['show-stops'] !== undefined" label="鏄剧ず闂存柇鐐�">
+ <el-switch v-model="activeData['show-stops']" />
+ </el-form-item>
+ <el-form-item v-if="activeData.range !== undefined" label="鑼冨洿閫夋嫨">
+ <el-switch v-model="activeData.range" @change="rangeChange" />
+ </el-form-item>
+ <el-form-item v-if="activeData.border !== undefined && activeData.optionType === 'default'" label="鏄惁甯﹁竟妗�">
+ <el-switch v-model="activeData.border" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-color-picker'" label="棰滆壊鏍煎紡">
+ <el-select v-model="activeData['color-format']" placeholder="璇烽�夋嫨棰滆壊鏍煎紡" :style="{ width: '100%' }"
+ @change="colorFormatChange">
+ <el-option v-for="(item, index) in colorFormatOptions" :key="index" :label="item.label"
+ :value="item.value" />
+ </el-select>
+ </el-form-item>
+ <el-form-item v-if="activeData.size !== undefined &&
+ (activeData.optionType === 'button' ||
+ activeData.border ||
+ activeData.tag === 'el-color-picker')" label="閫夐」灏哄">
+ <el-radio-group v-model="activeData.size">
+ <el-radio-button label="large">
+ 杈冨ぇ
+ </el-radio-button>
+ <el-radio-button label="default">
+ 榛樿
+ </el-radio-button>
+ <el-radio-button label="small">
+ 杈冨皬
+ </el-radio-button>
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item v-if="activeData['show-word-limit'] !== undefined" label="杈撳叆缁熻">
+ <el-switch v-model="activeData['show-word-limit']" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-input-number'" label="涓ユ牸姝ユ暟">
+ <el-switch v-model="activeData['step-strictly']" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-cascader'" label="鏄惁澶氶��">
+ <el-switch v-model="activeData.props.props.multiple" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-cascader'" label="灞曠ず鍏ㄨ矾寰�">
+ <el-switch v-model="activeData['show-all-levels']" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-cascader'" label="鍙惁绛涢��">
+ <el-switch v-model="activeData.filterable" />
+ </el-form-item>
+ <el-form-item v-if="activeData.clearable !== undefined" label="鑳藉惁娓呯┖">
+ <el-switch v-model="activeData.clearable" />
+ </el-form-item>
+ <el-form-item v-if="activeData.showTip !== undefined" label="鏄剧ず鎻愮ず">
+ <el-switch v-model="activeData.showTip" />
+ </el-form-item>
+ <el-form-item v-if="activeData.multiple !== undefined" label="澶氶�夋枃浠�">
+ <el-switch v-model="activeData.multiple" />
+ </el-form-item>
+ <el-form-item v-if="activeData['auto-upload'] !== undefined" label="鑷姩涓婁紶">
+ <el-switch v-model="activeData['auto-upload']" />
+ </el-form-item>
+ <el-form-item v-if="activeData.readonly !== undefined" label="鏄惁鍙">
+ <el-switch v-model="activeData.readonly" />
+ </el-form-item>
+ <el-form-item v-if="activeData.disabled !== undefined" label="鏄惁绂佺敤">
+ <el-switch v-model="activeData.disabled" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-select'" label="鏄惁鍙悳绱�">
+ <el-switch v-model="activeData.filterable" />
+ </el-form-item>
+ <el-form-item v-if="activeData.tag === 'el-select'" label="鏄惁澶氶��">
+ <el-switch v-model="activeData.multiple" @change="multipleChange" />
+ </el-form-item>
+ <el-form-item v-if="activeData.required !== undefined" label="鏄惁蹇呭~">
+ <el-switch v-model="activeData.required" />
+ </el-form-item>
+
+ <template v-if="activeData.layoutTree">
+ <el-divider>甯冨眬缁撴瀯鏍�</el-divider>
+ <el-tree :data="[activeData]" :props="layoutTreeProps" node-key="renderKey" default-expand-all draggable>
+ <template #default="{ node, data }">
+ <span class="node-label">
+ <svg-icon class="node-icon" :icon-class="data.tagIcon" style="margin-right: 5px;" />
+ {{ node.label }}
+ </span>
+ </template>
+ </el-tree>
+ </template>
+
+ <template v-if="activeData.layout === 'colFormItem'">
+ <el-divider>姝e垯鏍¢獙</el-divider>
+ <div v-for="(item, index) in activeData.regList" :key="index" class="reg-item">
+ <span class="close-btn" @click="activeData.regList.splice(index, 1)">
+ <el-icon>
+ <Close />
+ </el-icon>
+ </span>
+ <el-form-item label="琛ㄨ揪寮�">
+ <el-input v-model="item.pattern" placeholder="璇疯緭鍏ユ鍒�" />
+ </el-form-item>
+ <el-form-item label="閿欒鎻愮ず" style="margin-bottom:0">
+ <el-input v-model="item.message" placeholder="璇疯緭鍏ラ敊璇彁绀�" />
+ </el-form-item>
+ </div>
+ <div>
+ <el-button icon="CirclePlus" style="margin-left: 0; margin-top: 10px;" type="primary" text bg
+ @click="addReg">
+ 娣诲姞瑙勫垯
+ </el-button>
+ </div>
+ </template>
+ </el-form>
+ <!-- 琛ㄥ崟灞炴�� -->
+ <el-form v-show="currentTab === 'form'" label-width="90px" label-position="top">
+ <el-form-item label="琛ㄥ崟鍚�">
+ <el-input v-model="formConf.formRef" placeholder="璇疯緭鍏ヨ〃鍗曞悕锛坮ef锛�" />
+ </el-form-item>
+ <el-form-item label="琛ㄥ崟妯″瀷">
+ <el-input v-model="formConf.formModel" placeholder="璇疯緭鍏ユ暟鎹ā鍨�" />
+ </el-form-item>
+ <el-form-item label="鏍¢獙妯″瀷">
+ <el-input v-model="formConf.formRules" placeholder="璇疯緭鍏ユ牎楠屾ā鍨�" />
+ </el-form-item>
+ <el-form-item label="琛ㄥ崟灏哄">
+ <el-radio-group v-model="formConf.size">
+ <el-radio-button label="large" value="杈冨ぇ" />
+ <el-radio-button label="default" value="榛樿" />
+ <el-radio-button label="small" value="杈冨皬" />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏍囩瀵归綈">
+ <el-radio-group v-model="formConf.labelPosition">
+ <el-radio-button label="left" value="宸﹀榻�" />
+ <el-radio-button label="right" value="鍙冲榻�" />
+ <el-radio-button label="top" value="椤堕儴瀵归綈" />
+ </el-radio-group>
+ </el-form-item>
+ <el-form-item label="鏍囩瀹藉害">
+ <el-input-number v-model="formConf.labelWidth" placeholder="鏍囩瀹藉害" />
+ </el-form-item>
+ <el-form-item label="鏍呮牸闂撮殧">
+ <el-input-number v-model="formConf.gutter" :min="0" placeholder="鏍呮牸闂撮殧" />
+ </el-form-item>
+ <el-form-item label="绂佺敤琛ㄥ崟">
+ <el-switch v-model="formConf.disabled" />
+ </el-form-item>
+ <el-form-item label="琛ㄥ崟鎸夐挳">
+ <el-switch v-model="formConf.formBtns" />
+ </el-form-item>
+ <el-form-item label="鏄剧ず鏈�変腑缁勪欢杈规">
+ <el-switch v-model="formConf.unFocusedComponentBorder" />
+ </el-form-item>
+ </el-form>
+ </el-scrollbar>
+ </div>
+ <icons-dialog v-model="iconsVisible" :current="activeData[currentIconModel]" @select="setIcon" />
+ <treeNode-dialog v-model="dialogVisible" @commit="addNode" />
+
+ </div>
+</template>
+
+<script setup>
+import draggable from "vuedraggable/dist/vuedraggable.common"
+import { isNumberStr } from '@/utils/index'
+import IconsDialog from './IconsDialog'
+import TreeNodeDialog from './TreeNodeDialog'
+import { inputComponents, selectComponents } from '@/utils/generator/config'
+
+const { proxy } = getCurrentInstance()
+const dateTimeFormat = {
+ date: 'YYYY-MM-DD',
+ week: 'YYYY 绗� ww 鍛�',
+ month: 'YYYY-MM',
+ year: 'YYYY',
+ datetime: 'YYYY-MM-DD HH:mm:ss',
+ daterange: 'YYYY-MM-DD',
+ monthrange: 'YYYY-MM',
+ datetimerange: 'YYYY-MM-DD HH:mm:ss'
+}
+const props = defineProps({
+ showField: Boolean,
+ activeData: Object,
+ formConf: Object
+})
+
+const data = reactive({
+ currentTab: 'field',
+ currentNode: null,
+ dialogVisible: false,
+ iconsVisible: false,
+ currentIconModel: null,
+ dateTypeOptions: [
+ {
+ label: '鏃�(date)',
+ value: 'date'
+ },
+ {
+ label: '鍛�(week)',
+ value: 'week'
+ },
+ {
+ label: '鏈�(month)',
+ value: 'month'
+ },
+ {
+ label: '骞�(year)',
+ value: 'year'
+ },
+ {
+ label: '鏃ユ湡鏃堕棿(datetime)',
+ value: 'datetime'
+ }
+ ],
+ dateRangeTypeOptions: [
+ {
+ label: '鏃ユ湡鑼冨洿(daterange)',
+ value: 'daterange'
+ },
+ {
+ label: '鏈堣寖鍥�(monthrange)',
+ value: 'monthrange'
+ },
+ {
+ label: '鏃ユ湡鏃堕棿鑼冨洿(datetimerange)',
+ value: 'datetimerange'
+ }
+ ],
+ colorFormatOptions: [
+ {
+ label: 'hex',
+ value: 'hex'
+ },
+ {
+ label: 'rgb',
+ value: 'rgb'
+ },
+ {
+ label: 'rgba',
+ value: 'rgba'
+ },
+ {
+ label: 'hsv',
+ value: 'hsv'
+ },
+ {
+ label: 'hsl',
+ value: 'hsl'
+ }
+ ],
+ justifyOptions: [
+ {
+ label: 'start',
+ value: 'start'
+ },
+ {
+ label: 'end',
+ value: 'end'
+ },
+ {
+ label: 'center',
+ value: 'center'
+ },
+ {
+ label: 'space-around',
+ value: 'space-around'
+ },
+ {
+ label: 'space-between',
+ value: 'space-between'
+ }
+ ],
+ layoutTreeProps: {
+ label(data, node) {
+ return data.componentName || `${data.label}: ${data.vModel}`
+ }
+ }
+})
+
+const { currentTab, currentNode, dialogVisible, iconsVisible, currentIconModel, dateTypeOptions, dateRangeTypeOptions, colorFormatOptions, justifyOptions, layoutTreeProps } = toRefs(data)
+
+const documentLink = computed(() => props.activeData.document || 'https://element-plus.org/zh-CN/guide/installation')
+
+const dateOptions = computed(() => {
+ if (props.activeData.type !== undefined && props.activeData.tag === 'el-date-picker') {
+ if (props.activeData['start-placeholder'] === undefined) {
+ return dateTypeOptions.value
+ }
+ return dateRangeTypeOptions.value
+ }
+ return []
+})
+
+const tagList = ref([
+ {
+ label: '杈撳叆鍨嬬粍浠�',
+ options: inputComponents
+ },
+ {
+ label: '閫夋嫨鍨嬬粍浠�',
+ options: selectComponents
+ }
+])
+
+const emit = defineEmits(['tag-change'])
+
+function addReg() {
+ props.activeData.regList.push({
+ pattern: '',
+ message: ''
+ })
+}
+function addSelectItem() {
+ props.activeData.options.push({
+ label: '',
+ value: ''
+ })
+}
+
+function addTreeItem() {
+ ++proxy.idGlobal
+ dialogVisible.value = true
+ currentNode.value = props.activeData.options
+}
+
+function renderContent(h, { node, data, store }) {
+ return h('div', {
+ class: "custom-tree-node"
+ }, [
+ h('span', node.label),
+ h('span', {
+ class: "node-operation"
+ }, [
+ h(resolveComponent('el-link'), {
+ type: "primary",
+ icon: "Plus",
+ underline: false,
+ onClick: () => {
+ append(data)
+
+ }
+ }),
+ h(resolveComponent('el-link'), {
+ type: "danger",
+ icon: "Delete",
+ underline: false,
+ style: "margin-left: 5px;",
+ onClick: () => {
+ remove(node, data)
+ }
+ })
+ ])
+ ])
+}
+function append(data) {
+ if (!data.children) {
+ data.children = []
+ }
+ dialogVisible.value = true
+ currentNode.value = data.children
+}
+function remove(node, data) {
+ const { parent } = node
+ const children = parent.data.children || parent.data
+ const index = children.findIndex(d => d.id === data.id)
+ children.splice(index, 1)
+}
+function addNode(data) {
+ currentNode.value.push(data)
+}
+
+function setOptionValue(item, val) {
+ item.value = isNumberStr(val) ? +val : val
+}
+function setDefaultValue(val) {
+ if (Array.isArray(val)) {
+ return val.join(',')
+ }
+ if (['string', 'number'].indexOf(val) > -1) {
+ return val
+ }
+ if (typeof val === 'boolean') {
+ return `${val}`
+ }
+ return val
+}
+
+function onDefaultValueInput(str) {
+ if (Array.isArray(props.activeData.defaultValue)) {
+ // 鏁扮粍
+ props.activeData.defaultValue = str.split(',').map(val => (isNumberStr(val) ? +val : val))
+ } else if (['true', 'false'].indexOf(str) > -1) {
+ // 甯冨皵
+ props.activeData.defaultValue = JSON.parse(str)
+ } else {
+ // 瀛楃涓插拰鏁板瓧
+ props.activeData.defaultValue = isNumberStr(str) ? +str : str
+ }
+}
+
+function onSwitchValueInput(val, name) {
+ if (['true', 'false'].indexOf(val) > -1) {
+ props.activeData[name] = JSON.parse(val)
+ } else {
+ props.activeData[name] = isNumberStr(val) ? +val : val
+ }
+}
+
+function setTimeValue(val, type) {
+ const valueFormat = type === 'week' ? dateTimeFormat.date : val
+ props.activeData.defaultValue = null
+ props.activeData['value-format'] = valueFormat
+ props.activeData.format = val
+}
+
+function spanChange(val) {
+ props.formConf.span = val
+}
+
+function multipleChange(val) {
+ props.activeData.defaultValue = val ? [] : ''
+}
+
+function dateTypeChange(val) {
+ setTimeValue(dateTimeFormat[val], val)
+}
+
+function rangeChange(val) {
+ props.activeData.defaultValue = val ? [props.activeData.min, props.activeData.max] : props.activeData.min
+}
+
+function rateTextChange(val) {
+ if (val) props.activeData['show-score'] = false
+}
+
+function rateScoreChange(val) {
+ if (val) props.activeData['show-text'] = false
+}
+
+function colorFormatChange(val) {
+ props.activeData.defaultValue = null
+ props.activeData['show-alpha'] = val.indexOf('a') > -1
+ props.activeData.renderKey = +new Date() // 鏇存柊renderKey,閲嶆柊娓叉煋璇ョ粍浠�
+}
+
+function openIconsDialog(model) {
+ iconsVisible.value = true
+ currentIconModel.value = model
+}
+
+function setIcon(val) {
+ props.activeData[currentIconModel.value] = val
+}
+
+function tagChange(tagIcon) {
+ let target = inputComponents.find(item => item.tagIcon === tagIcon)
+ if (!target) target = selectComponents.find(item => item.tagIcon === tagIcon)
+ emit('tag-change', target)
+}
+</script>
+
+<style lang="scss" scoped>
+.right-board {
+ width: 350px;
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding-top: 3px;
+
+ &:deep() {
+ .el-tabs__header {
+ margin: 0;
+ }
+
+ .el-input-group__append .el-button {
+ display: inline-flex;
+ }
+ }
+
+ .field-box {
+ position: relative;
+ height: calc(100vh - 50px - 40px - 42px);
+ box-sizing: border-box;
+ overflow: hidden;
+ }
+
+ .el-scrollbar {
+ height: 100%;
+
+ &:deep() {
+ .el-scrollbar__view {
+ padding: 30px 20px;
+ }
+
+ }
+ }
+}
+
+.reg-item {
+ padding: 12px 6px;
+ background: var(--el-border-color-extra-light);
+ position: relative;
+ border-radius: 4px;
+
+ .close-btn {
+ position: absolute;
+ right: -6px;
+ top: -6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 16px;
+ height: 16px;
+ line-height: 16px;
+ background: rgba(0, 0, 0, .2);
+ border-radius: 50%;
+ color: #fff;
+ z-index: 1;
+ cursor: pointer;
+ font-size: 12px;
+ }
+}
+
+.select-item {
+ display: flex;
+ border: 1px dashed #fff;
+ box-sizing: border-box;
+
+ & .close-btn {
+ cursor: pointer;
+ color: #f56c6c;
+ }
+
+ & .el-input+.el-input {
+ margin-left: 4px;
+ }
+}
+
+.select-item+.select-item {
+ margin-top: 4px;
+}
+
+.select-item.sortable-chosen {
+ border: 1px dashed #409eff;
+}
+
+.select-line-icon {
+ line-height: 32px;
+ font-size: 22px;
+ padding: 0 4px;
+ color: #777;
+}
+
+.option-drag {
+ cursor: move;
+}
+
+.time-range {
+ .el-date-editor {
+ width: 227px;
+ }
+
+ :deep() {
+ .el-icon-time {
+ display: none;
+ }
+ }
+}
+
+.document-link {
+ position: absolute;
+ display: flex;
+ width: 26px;
+ height: 26px;
+ top: 0;
+ left: 0;
+ cursor: pointer;
+ background: #409eff;
+ z-index: 1;
+ border-radius: 0 0 6px 0;
+ justify-content: center;
+ align-items: center;
+ color: #fff;
+ font-size: 18px;
+}
+
+.node-label {
+ font-size: 14px;
+}
+
+.node-icon {
+ color: #bebfc3;
+}
+
+.custom-tree-node {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ padding-right: 8px;
+}
+</style>
\ No newline at end of file
diff --git a/src/views/tool/build/TreeNodeDialog.vue b/src/views/tool/build/TreeNodeDialog.vue
new file mode 100644
index 0000000..372d3af
--- /dev/null
+++ b/src/views/tool/build/TreeNodeDialog.vue
@@ -0,0 +1,93 @@
+<template>
+ <div>
+ <el-dialog title="娣诲姞閫夐」" v-model="open" width="800px" :close-on-click-modal="false" :modal-append-to-body="false"
+ @open="onOpen" @close="onClose">
+ <el-form ref="treeNodeForm" :model="formData" :rules="rules" label-width="100px">
+ <el-col :span="24">
+ <el-form-item label="閫夐」鍚�" prop="label">
+ <el-input v-model="formData.label" placeholder="璇疯緭鍏ラ�夐」鍚�" clearable />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="閫夐」鍊�" prop="value">
+ <el-input v-model="formData.value" placeholder="璇疯緭鍏ラ�夐」鍊�" clearable>
+ <template #append>
+ <el-select v-model="dataType" :style="{ width: '100px' }">
+ <el-option v-for="(item, index) in dataTypeOptions" :key="index" :label="item.label" :value="item.value"
+ :disabled="item.disabled" />
+ </el-select>
+ </template>
+
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-form>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handelConfirm">纭� 瀹�</el-button>
+ <el-button @click="onClose">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+ </div>
+</template>
+<script setup>
+const open = defineModel()
+const emit = defineEmits(['confirm'])
+const formData = ref({
+ label: undefined,
+ value: undefined
+})
+const rules = {
+ label: [
+ {
+ required: true,
+ message: '璇疯緭鍏ラ�夐」鍚�',
+ trigger: 'blur'
+ }
+ ],
+ value: [
+ {
+ required: true,
+ message: '璇疯緭鍏ラ�夐」鍊�',
+ trigger: 'blur'
+ }
+ ]
+}
+const dataType = ref('string')
+const dataTypeOptions = ref([
+ {
+ label: '瀛楃涓�',
+ value: 'string'
+ },
+ {
+ label: '鏁板瓧',
+ value: 'number'
+ }
+])
+const id = ref(100)
+const treeNodeForm = ref()
+
+function onOpen() {
+ formData.value = {
+ label: undefined,
+ value: undefined
+ }
+}
+
+function onClose() {
+ open.value = false
+}
+
+function handelConfirm() {
+ treeNodeForm.value.validate(valid => {
+ if (!valid) return
+ if (dataType.value === 'number') {
+ formData.value.value = parseFloat(formData.value.value)
+ }
+ formData.value.id = id.value++
+ emit('commit', formData.value)
+ onClose()
+ })
+}
+</script>
diff --git a/src/views/tool/build/index.vue b/src/views/tool/build/index.vue
new file mode 100644
index 0000000..0895955
--- /dev/null
+++ b/src/views/tool/build/index.vue
@@ -0,0 +1,653 @@
+<template>
+ <div class="container">
+ <div class="left-board">
+ <div class="logo-wrapper">
+ <div class="logo">
+ <img :src="logo" alt="logo"> Form Generator
+ </div>
+ </div>
+ <el-scrollbar class="left-scrollbar">
+ <div class="components-list">
+ <div class="components-title">
+ <svg-icon icon-class="component" />杈撳叆鍨嬬粍浠�
+ </div>
+ <draggable class="components-draggable" :list="inputComponents"
+ :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
+ draggable=".components-item" :sort="false" @end="onEnd" item-key="label">
+ <template #item="{ element, index }">
+ <div :key="index" class="components-item" @click="addComponent(element)">
+ <div class="components-body">
+ <svg-icon :icon-class="element.tagIcon" />
+ {{ element.label }}
+ </div>
+ </div>
+ </template>
+ </draggable>
+ <div class="components-title">
+ <svg-icon icon-class="component" />閫夋嫨鍨嬬粍浠�
+ </div>
+ <draggable class="components-draggable" :list="selectComponents"
+ :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
+ draggable=".components-item" :sort="false" @end="onEnd" item-key="label">
+ <template #item="{ element, index }">
+ <div :key="index" class="components-item" @click="addComponent(element)">
+ <div class="components-body">
+ <svg-icon :icon-class="element.tagIcon" />
+ {{ element.label }}
+ </div>
+ </div>
+ </template>
+ </draggable>
+ <div class="components-title">
+ <svg-icon icon-class="component" /> 甯冨眬鍨嬬粍浠�
+ </div>
+ <draggable class="components-draggable" :list="layoutComponents"
+ :group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
+ draggable=".components-item" :sort="false" @end="onEnd" item-key="label">
+ <template #item="{ element, index }">
+ <div :key="index" class="components-item" @click="addComponent(element)">
+ <div class="components-body">
+ <svg-icon :icon-class="element.tagIcon" />
+ {{ element.label }}
+ </div>
+ </div>
+ </template>
+ </draggable>
+ </div>
+ </el-scrollbar>
+ </div>
+ <div class="center-board">
+ <div class="action-bar">
+ <el-button icon="Download" type="primary" text @click="download">
+ 瀵煎嚭vue鏂囦欢
+ </el-button>
+ <el-button class="copy-btn-main" icon="DocumentCopy" type="primary" text @click="copy">
+ 澶嶅埗浠g爜
+ </el-button>
+ <el-button class="delete-btn" icon="Delete" text @click="empty" type="danger">
+ 娓呯┖
+ </el-button>
+ </div>
+ <el-scrollbar class="center-scrollbar">
+ <el-row class="center-board-row" :gutter="formConf.gutter">
+ <el-form :size="formConf.size" :label-position="formConf.labelPosition" :disabled="formConf.disabled"
+ :label-width="formConf.labelWidth + 'px'">
+ <draggable class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup"
+ item-key="label">
+ <template #item="{ element, index }">
+ <draggable-item :key="element.renderKey" :drawing-list="drawingList" :element="element" :index="index"
+ :active-id="activeId" :form-conf="formConf" @activeItem="activeFormItem" @copyItem="drawingItemCopy"
+ @deleteItem="drawingItemDelete" />
+ </template>
+ </draggable>
+ <div v-show="!drawingList.length" class="empty-info">
+ 浠庡乏渚ф嫋鍏ユ垨鐐归�夌粍浠惰繘琛岃〃鍗曡璁�
+ </div>
+ </el-form>
+ </el-row>
+ </el-scrollbar>
+ </div>
+ <right-panel :active-data="activeData" :form-conf="formConf" :show-field="!!drawingList.length"
+ @tag-change="tagChange" />
+
+ <code-type-dialog v-model="dialogVisible" title="閫夋嫨鐢熸垚绫诲瀷" :showFileName="showFileName" @confirm="generate" />
+ <input id="copyNode" type="hidden">
+ </div>
+</template>
+
+<script setup>
+import draggable from "vuedraggable/dist/vuedraggable.common"
+import ClipboardJS from 'clipboard'
+import beautifier from 'js-beautify'
+import logo from '@/assets/logo/logo.png'
+import { inputComponents, selectComponents, layoutComponents, formConf as formConfData } from '@/utils/generator/config'
+import { beautifierConf } from '@/utils/index'
+import drawingDefalut from '@/utils/generator/drawingDefalut'
+import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html'
+import { makeUpJs } from '@/utils/generator/js'
+import { makeUpCss } from '@/utils/generator/css'
+import Download from '@/plugins/download'
+import { ElNotification } from 'element-plus'
+import DraggableItem from './DraggableItem'
+import RightPanel from './RightPanel'
+import CodeTypeDialog from './CodeTypeDialog'
+import { onMounted, watch } from 'vue'
+
+const drawingList = ref(drawingDefalut)
+const { proxy } = getCurrentInstance()
+const dialogVisible = ref(false)
+const showFileName = ref(false)
+const operationType = ref('')
+const idGlobal = ref(100)
+const activeData = ref(drawingDefalut[0])
+const activeId = ref(drawingDefalut[0].formId)
+const generateConf = ref(null)
+const formData = ref({})
+const formConf = ref(formConfData)
+let oldActiveId
+let tempActiveData
+
+function activeFormItem(element) {
+ activeData.value = element
+ activeId.value = element.formId
+}
+function copy() {
+ dialogVisible.value = true
+ showFileName.value = false
+ operationType.value = 'copy'
+}
+function download() {
+ dialogVisible.value = true
+ showFileName.value = true
+ operationType.value = 'download'
+}
+function empty() {
+ proxy.$modal.confirm('纭畾瑕佹竻绌烘墍鏈夌粍浠跺悧锛�', '鎻愮ず', { type: 'warning' }).then(() => {
+ idGlobal.value = 100
+ drawingList.value = []
+ }
+ )
+}
+
+function onEnd(obj, a) {
+ if (obj.from !== obj.to) {
+ activeData.value = tempActiveData
+ activeId.value = idGlobal.value
+ }
+}
+
+function addComponent(item) {
+ const clone = cloneComponent(item)
+ drawingList.value.push(clone)
+ activeFormItem(clone)
+}
+
+function cloneComponent(origin) {
+ const clone = JSON.parse(JSON.stringify(origin))
+ clone.formId = ++idGlobal.value
+ clone.span = formConf.value.span
+ clone.renderKey = +new Date() // 鏀瑰彉renderKey鍚庡彲浠ュ疄鐜板己鍒舵洿鏂扮粍浠�
+ if (!clone.layout) clone.layout = 'colFormItem'
+ if (clone.layout === 'colFormItem') {
+ clone.vModel = `field${idGlobal.value}`
+ clone.placeholder !== undefined && (clone.placeholder += clone.label)
+ tempActiveData = clone
+ } else if (clone.layout === 'rowFormItem') {
+ delete clone.label
+ clone.componentName = `row${idGlobal.value}`
+ clone.gutter = formConf.value.gutter
+ tempActiveData = clone
+ }
+ return tempActiveData
+}
+
+function drawingItemCopy(item, parent) {
+ let clone = JSON.parse(JSON.stringify(item))
+ clone = createIdAndKey(clone)
+ parent.push(clone)
+ activeFormItem(clone)
+}
+
+
+function createIdAndKey(item) {
+ item.formId = ++idGlobal.value
+ item.renderKey = +new Date()
+ if (item.layout === 'colFormItem') {
+ item.vModel = `field${idGlobal.value}`
+ } else if (item.layout === 'rowFormItem') {
+ item.componentName = `row${idGlobal.value}`
+ }
+ if (Array.isArray(item.children)) {
+ item.children = item.children.map(childItem => createIdAndKey(childItem))
+ }
+ return item
+}
+
+function drawingItemDelete(index, parent) {
+ parent.splice(index, 1)
+ nextTick(() => {
+ const len = drawingList.value.length
+ if (len) {
+ activeFormItem(drawingList.value[len - 1])
+ }
+ })
+}
+
+function tagChange(newTag) {
+ newTag = cloneComponent(newTag)
+ newTag.vModel = activeData.value.vModel
+ newTag.formId = activeId.value
+ newTag.span = activeData.value.span
+ delete activeData.value.tag
+ delete activeData.value.tagIcon
+ delete activeData.value.document
+ Object.keys(newTag).forEach(key => {
+ if (activeData.value[key] !== undefined
+ && typeof activeData.value[key] === typeof newTag[key]) {
+ newTag[key] = activeData.value[key]
+ }
+ })
+ activeData.value = newTag
+ updateDrawingList(newTag, drawingList.value)
+}
+
+
+function updateDrawingList(newTag, list) {
+ const index = list.findIndex(item => item.formId === activeId.value)
+ if (index > -1) {
+ list.splice(index, 1, newTag)
+ } else {
+ list.forEach(item => {
+ if (Array.isArray(item.children)) updateDrawingList(newTag, item.children)
+ })
+ }
+}
+function generate(data) {
+ generateConf.value = data
+ nextTick(() => {
+ switch (operationType.value) {
+ case 'copy':
+ execCopy(data)
+ break
+ case 'download':
+ execDownload(data)
+ break
+ default:
+ break
+ }
+ })
+}
+
+function execDownload(data) {
+ const codeStr = generateCode()
+ const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
+ Download.saveAs(blob, data.fileName)
+}
+
+function execCopy(data) {
+ document.getElementById('copyNode').click()
+}
+function AssembleFormData() {
+ formData.value = { fields: JSON.parse(JSON.stringify(drawingList.value)), ...formConf.value }
+}
+function generateCode() {
+ const { type } = generateConf.value
+ AssembleFormData()
+ const script = vueScript(makeUpJs(formData.value, type))
+ const html = vueTemplate(makeUpHtml(formData.value, type))
+ const css = cssStyle(makeUpCss(formData.value))
+ return beautifier.html(html + script + css, beautifierConf.html)
+}
+watch(() => activeData.value.label, (val, oldVal) => {
+ if (
+ activeData.value.placeholder === undefined
+ || !activeData.value.tag
+ || oldActiveId !== activeId.value
+ ) {
+ return
+ }
+ activeData.value.placeholder = activeData.value.placeholder.replace(oldVal, '') + val
+})
+watch(activeId, (val) => {
+ oldActiveId = val
+}, { immediate: true })
+
+onMounted(() => {
+ const clipboard = new ClipboardJS('#copyNode', {
+ text: trigger => {
+ const codeStr = generateCode()
+ ElNotification({ title: '鎴愬姛', message: '浠g爜宸插鍒跺埌鍓垏鏉匡紝鍙矘璐淬��', type: 'success' })
+ return codeStr
+ }
+ })
+ clipboard.on('error', e => {
+ proxy.$modal.msgError('浠g爜澶嶅埗澶辫触')
+ })
+})
+</script>
+
+<style lang='scss'>
+$lighterBlue: #2C51D9;
+
+.container {
+ position: relative;
+ width: 100%;
+ background-color: var(--el-bg-color-overlay);
+ height: calc(100vh - 50px - 40px);
+ overflow: hidden;
+
+ .left-board {
+ width: 260px;
+ position: absolute;
+ left: 0;
+ top: 0;
+ height: calc(100vh - 50px - 40px);
+
+ .logo-wrapper {
+ position: relative;
+ height: 42px;
+ border-bottom: 1px solid var(--el-border-color-extra-light);
+ box-sizing: border-box;
+
+ .logo {
+ position: absolute;
+ left: 12px;
+ top: 6px;
+ line-height: 30px;
+ color: #00afff;
+ font-weight: 600;
+ font-size: 17px;
+ white-space: nowrap;
+
+ >img {
+ width: 30px;
+ height: 30px;
+ vertical-align: top;
+ }
+
+ .github {
+ display: inline-block;
+ vertical-align: sub;
+ margin-left: 15px;
+
+ >img {
+ height: 22px;
+ }
+ }
+ }
+ }
+
+ .left-scrollbar {
+ .el-scrollbar__wrap {
+ box-sizing: border-box;
+ overflow-x: hidden !important;
+ margin-bottom: 0 !important;
+
+ .components-list {
+ padding: 8px;
+ box-sizing: border-box;
+ height: 100%;
+
+ .components-title {
+ font-size: 14px;
+ // color: #222;
+ margin: 6px 2px;
+
+ .svg-icon {
+ // color: #666;
+ font-size: 18px;
+ margin-right: 5px;
+ }
+ }
+
+ .components-draggable {
+ padding-bottom: 20px;
+
+ .components-item {
+ display: inline-block;
+ width: 48%;
+ margin: 1%;
+ transition: transform 0ms !important;
+
+ .components-body {
+ padding: 8px 10px;
+ background: var(--el-border-color-extra-light);
+ font-size: 12px;
+ cursor: move;
+ border: 1px dashed var(--el-border-color-extra-light);
+ border-radius: 3px;
+
+ .svg-icon {
+ // color: #777;
+ font-size: 15px;
+ margin-right: 5px;
+ }
+
+ &:hover {
+ border: 1px dashed #787be8;
+ color: #787be8;
+
+ .svg-icon {
+ color: #787be8;
+ }
+ }
+ }
+ }
+ }
+
+
+ }
+ }
+ }
+ }
+
+ .center-board {
+ height: calc(100vh - 50px - 40px);
+ width: auto;
+ margin: 0 350px 0 260px;
+ box-sizing: border-box;
+
+ .action-bar {
+ position: relative;
+ height: 42px;
+ padding: 0 15px;
+ box-sizing: border-box;
+ ;
+ border: 1px solid var(--el-border-color-extra-light);
+ border-top: none;
+ border-left: none;
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+
+ u .delete-btn {
+ color: #F56C6C;
+ }
+ }
+
+ .center-scrollbar {
+ height: calc(100vh - 50px - 40px - 42px);
+ overflow: hidden;
+ border-left: 1px solid var(--el-border-color-extra-light);
+ border-right: 1px solid var(--el-border-color-extra-light);
+ box-sizing: border-box;
+
+ .el-scrollbar__view {
+ overflow-x: hidden;
+ }
+
+ .center-board-row {
+ padding: 12px 12px 15px 12px;
+ box-sizing: border-box;
+
+ &>.el-form {
+ // 69 = 12+15+42
+ height: calc(100vh - 50px - 40px - 69px);
+ flex: 1;
+
+ .drawing-board {
+ height: 100%;
+ position: relative;
+
+ .components-body {
+ padding: 0;
+ margin: 0;
+ font-size: 0;
+ }
+
+ .sortable-ghost {
+ position: relative;
+ display: block;
+ overflow: hidden;
+
+ &::before {
+ content: " ";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 0;
+ height: 3px;
+ background: rgb(89, 89, 223);
+ z-index: 2;
+ }
+ }
+
+ .components-item.sortable-ghost {
+ width: 100%;
+ height: 60px;
+ background: var(--el-border-color-extra-light);
+ }
+
+ .active-from-item {
+ &>.el-form-item {
+ background: var(--el-border-color-extra-light);
+ border-radius: 6px;
+ }
+
+ &>.drawing-item-copy,
+ &>.drawing-item-delete {
+ display: initial;
+ }
+
+ &>.component-name {
+ color: $lighterBlue;
+ }
+
+ .el-input__wrapper {
+ box-shadow: 0 0 0 1px var(--el-input-hover-border-color) inset;
+ }
+ }
+
+ .el-form-item {
+ margin-bottom: 15px;
+ }
+ }
+
+ .drawing-item {
+ position: relative;
+ cursor: move;
+
+ &.unfocus-bordered:not(.activeFromItem)>div:first-child {
+ border: 1px dashed #ccc;
+ }
+
+ .el-form-item {
+ padding: 12px 10px;
+ }
+ }
+
+ .drawing-row-item {
+ position: relative;
+ cursor: move;
+ box-sizing: border-box;
+ border: 1px dashed #ccc;
+ border-radius: 3px;
+ padding: 0 2px;
+ margin-bottom: 15px;
+
+ .drawing-row-item {
+ margin-bottom: 2px;
+ }
+
+ .el-col {
+ margin-top: 22px;
+ }
+
+ .el-form-item {
+ margin-bottom: 0;
+ }
+
+ .drag-wrapper {
+ min-height: 80px;
+ flex: 1;
+ display: flex;
+ flex-wrap: wrap;
+ }
+
+ &.active-from-item {
+ border: 1px dashed $lighterBlue;
+ }
+
+ .component-name {
+ position: absolute;
+ top: 0;
+ left: 0;
+ font-size: 12px;
+ color: #bbb;
+ display: inline-block;
+ padding: 0 6px;
+ }
+ }
+
+ .drawing-item,
+ .drawing-row-item {
+ &:hover {
+ &>.el-form-item {
+ background: var(--el-border-color-extra-light);
+ border-radius: 6px;
+ }
+
+ &>.drawing-item-copy,
+ &>.drawing-item-delete {
+ display: initial;
+ }
+ }
+
+ &>.drawing-item-copy,
+ &>.drawing-item-delete {
+ display: none;
+ position: absolute;
+ top: -10px;
+ width: 22px;
+ height: 22px;
+ line-height: 22px;
+ text-align: center;
+ border-radius: 50%;
+ font-size: 12px;
+ border: 1px solid;
+ cursor: pointer;
+ z-index: 1;
+ }
+
+ &>.drawing-item-copy {
+ right: 56px;
+ border-color: $lighterBlue;
+ color: $lighterBlue;
+ background: #fff;
+
+ &:hover {
+ background: $lighterBlue;
+ color: #fff;
+ }
+ }
+
+ &>.drawing-item-delete {
+ right: 24px;
+ border-color: #F56C6C;
+ color: #F56C6C;
+ background: #fff;
+
+ &:hover {
+ background: #F56C6C;
+ color: #fff;
+ }
+ }
+ }
+
+ .empty-info {
+ position: absolute;
+ top: 46%;
+ left: 0;
+ right: 0;
+ text-align: center;
+ font-size: 18px;
+ color: #ccb1ea;
+ letter-spacing: 4px;
+ }
+
+ }
+ }
+ }
+ }
+}
+</style>
diff --git a/src/views/tool/gen/basicInfoForm.vue b/src/views/tool/gen/basicInfoForm.vue
new file mode 100644
index 0000000..8bfc373
--- /dev/null
+++ b/src/views/tool/gen/basicInfoForm.vue
@@ -0,0 +1,48 @@
+<template>
+ <el-form ref="basicInfoForm" :model="info" :rules="rules" label-width="150px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item label="琛ㄥ悕绉�" prop="tableName">
+ <el-input placeholder="璇疯緭鍏ヤ粨搴撳悕绉�" v-model="info.tableName" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="琛ㄦ弿杩�" prop="tableComment">
+ <el-input placeholder="璇疯緭鍏�" v-model="info.tableComment" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="瀹炰綋绫诲悕绉�" prop="className">
+ <el-input placeholder="璇疯緭鍏�" v-model="info.className" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item label="浣滆��" prop="functionAuthor">
+ <el-input placeholder="璇疯緭鍏�" v-model="info.functionAuthor" />
+ </el-form-item>
+ </el-col>
+ <el-col :span="24">
+ <el-form-item label="澶囨敞" prop="remark">
+ <el-input type="textarea" :rows="3" v-model="info.remark"></el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </el-form>
+</template>
+
+<script setup>
+defineProps({
+ info: {
+ type: Object,
+ default: null
+ }
+})
+
+// 琛ㄥ崟鏍¢獙
+const rules = ref({
+ tableName: [{ required: true, message: "璇疯緭鍏ヨ〃鍚嶇О", trigger: "blur" }],
+ tableComment: [{ required: true, message: "璇疯緭鍏ヨ〃鎻忚堪", trigger: "blur" }],
+ className: [{ required: true, message: "璇疯緭鍏ュ疄浣撶被鍚嶇О", trigger: "blur" }],
+ functionAuthor: [{ required: true, message: "璇疯緭鍏ヤ綔鑰�", trigger: "blur" }]
+})
+</script>
diff --git a/src/views/tool/gen/createTable.vue b/src/views/tool/gen/createTable.vue
new file mode 100644
index 0000000..ef6f8f3
--- /dev/null
+++ b/src/views/tool/gen/createTable.vue
@@ -0,0 +1,46 @@
+<template>
+ <!-- 鍒涘缓琛� -->
+ <el-dialog title="鍒涘缓琛�" v-model="visible" width="800px" top="5vh" append-to-body>
+ <span>鍒涘缓琛ㄨ鍙�(鏀寔澶氫釜寤鸿〃璇彞)锛�</span>
+ <el-input type="textarea" :rows="10" placeholder="璇疯緭鍏ユ枃鏈�" v-model="content"></el-input>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleImportTable">纭� 瀹�</el-button>
+ <el-button @click="visible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { createTable } from "@/api/tool/gen"
+
+const visible = ref(false)
+const content = ref("")
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(["ok"])
+
+/** 鏄剧ず寮规 */
+function show() {
+ visible.value = true
+}
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImportTable() {
+ if (content.value === "") {
+ proxy.$modal.msgError("璇疯緭鍏ュ缓琛ㄨ鍙�")
+ return
+ }
+ createTable({ sql: content.value }).then(res => {
+ proxy.$modal.msgSuccess(res.msg)
+ if (res.code === 200) {
+ visible.value = false
+ emit("ok")
+ }
+ })
+}
+
+defineExpose({
+ show,
+})
+</script>
diff --git a/src/views/tool/gen/editTable.vue b/src/views/tool/gen/editTable.vue
new file mode 100644
index 0000000..874fc94
--- /dev/null
+++ b/src/views/tool/gen/editTable.vue
@@ -0,0 +1,200 @@
+<template>
+ <el-card>
+ <el-tabs v-model="activeName">
+ <el-tab-pane label="鍩烘湰淇℃伅" name="basic">
+ <basic-info-form ref="basicInfo" :info="info" />
+ </el-tab-pane>
+ <el-tab-pane label="瀛楁淇℃伅" name="columnInfo">
+ <el-table ref="dragTable" :data="columns" row-key="columnId" :max-height="tableHeight">
+ <el-table-column label="搴忓彿" type="index" min-width="5%"/>
+ <el-table-column
+ label="瀛楁鍒楀悕"
+ prop="columnName"
+ min-width="10%"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="瀛楁鎻忚堪" min-width="10%">
+ <template #default="scope">
+ <el-input v-model="scope.row.columnComment"></el-input>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="鐗╃悊绫诲瀷"
+ prop="columnType"
+ min-width="10%"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column label="Java绫诲瀷" min-width="11%">
+ <template #default="scope">
+ <el-select v-model="scope.row.javaType">
+ <el-option label="Long" value="Long" />
+ <el-option label="String" value="String" />
+ <el-option label="Integer" value="Integer" />
+ <el-option label="Double" value="Double" />
+ <el-option label="BigDecimal" value="BigDecimal" />
+ <el-option label="Date" value="Date" />
+ <el-option label="Boolean" value="Boolean" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="java灞炴��" min-width="10%">
+ <template #default="scope">
+ <el-input v-model="scope.row.javaField"></el-input>
+ </template>
+ </el-table-column>
+
+ <el-table-column label="鎻掑叆" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isInsert"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="缂栬緫" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isEdit"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="鍒楄〃" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isList"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏌ヨ" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isQuery"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏌ヨ鏂瑰紡" min-width="10%">
+ <template #default="scope">
+ <el-select v-model="scope.row.queryType">
+ <el-option label="=" value="EQ" />
+ <el-option label="!=" value="NE" />
+ <el-option label=">" value="GT" />
+ <el-option label=">=" value="GTE" />
+ <el-option label="<" value="LT" />
+ <el-option label="<=" value="LTE" />
+ <el-option label="LIKE" value="LIKE" />
+ <el-option label="BETWEEN" value="BETWEEN" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="蹇呭~" min-width="5%">
+ <template #default="scope">
+ <el-checkbox true-label="1" false-label="0" v-model="scope.row.isRequired"></el-checkbox>
+ </template>
+ </el-table-column>
+ <el-table-column label="鏄剧ず绫诲瀷" min-width="12%">
+ <template #default="scope">
+ <el-select v-model="scope.row.htmlType">
+ <el-option label="鏂囨湰妗�" value="input" />
+ <el-option label="鏂囨湰鍩�" value="textarea" />
+ <el-option label="涓嬫媺妗�" value="select" />
+ <el-option label="鍗曢�夋" value="radio" />
+ <el-option label="澶嶉�夋" value="checkbox" />
+ <el-option label="鏃ユ湡鎺т欢" value="datetime" />
+ <el-option label="鍥剧墖涓婁紶" value="imageUpload" />
+ <el-option label="鏂囦欢涓婁紶" value="fileUpload" />
+ <el-option label="瀵屾枃鏈帶浠�" value="editor" />
+ </el-select>
+ </template>
+ </el-table-column>
+ <el-table-column label="瀛楀吀绫诲瀷" min-width="12%">
+ <template #default="scope">
+ <el-select v-model="scope.row.dictType" clearable filterable placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="dict in dictOptions"
+ :key="dict.dictType"
+ :label="dict.dictName"
+ :value="dict.dictType">
+ <span style="float: left">{{ dict.dictName }}</span>
+ <span style="float: right; color: #8492a6; font-size: 13px">{{ dict.dictType }}</span>
+ </el-option>
+ </el-select>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-tab-pane>
+ <el-tab-pane label="鐢熸垚淇℃伅" name="genInfo">
+ <gen-info-form ref="genInfo" :info="info" :tables="tables" />
+ </el-tab-pane>
+ </el-tabs>
+ <el-form label-width="100px">
+ <div style="text-align: center;margin-left:-100px;margin-top:10px;">
+ <el-button type="primary" @click="submitForm()">鎻愪氦</el-button>
+ <el-button @click="close()">杩斿洖</el-button>
+ </div>
+ </el-form>
+ </el-card>
+</template>
+
+<script setup name="GenEdit">
+import { getGenTable, updateGenTable } from "@/api/tool/gen"
+import { optionselect as getDictOptionselect } from "@/api/system/dict/type"
+import basicInfoForm from "./basicInfoForm"
+import genInfoForm from "./genInfoForm"
+
+const route = useRoute()
+const { proxy } = getCurrentInstance()
+
+const activeName = ref("columnInfo")
+const tableHeight = ref(document.documentElement.scrollHeight - 245 + "px")
+const tables = ref([])
+const columns = ref([])
+const dictOptions = ref([])
+const info = ref({})
+
+/** 鎻愪氦鎸夐挳 */
+function submitForm() {
+ const basicForm = proxy.$refs.basicInfo.$refs.basicInfoForm
+ const genForm = proxy.$refs.genInfo.$refs.genInfoForm
+ Promise.all([basicForm, genForm].map(getFormPromise)).then(res => {
+ const validateResult = res.every(item => !!item)
+ if (validateResult) {
+ const genTable = Object.assign({}, info.value)
+ genTable.columns = columns.value
+ genTable.params = {
+ treeCode: info.value.treeCode,
+ treeName: info.value.treeName,
+ treeParentCode: info.value.treeParentCode,
+ parentMenuId: info.value.parentMenuId
+ }
+ updateGenTable(genTable).then(res => {
+ proxy.$modal.msgSuccess(res.msg)
+ if (res.code === 200) {
+ close()
+ }
+ })
+ } else {
+ proxy.$modal.msgError("琛ㄥ崟鏍¢獙鏈�氳繃锛岃閲嶆柊妫�鏌ユ彁浜ゅ唴瀹�")
+ }
+ })
+}
+
+function getFormPromise(form) {
+ return new Promise(resolve => {
+ form.validate(res => {
+ resolve(res)
+ })
+ })
+}
+
+function close() {
+ const obj = { path: "/tool/gen", query: { t: Date.now(), pageNum: route.query.pageNum } }
+ proxy.$tab.closeOpenPage(obj)
+}
+
+(() => {
+ const tableId = route.params && route.params.tableId
+ if (tableId) {
+ // 鑾峰彇琛ㄨ缁嗕俊鎭�
+ getGenTable(tableId).then(res => {
+ columns.value = res.data.rows
+ info.value = res.data.info
+ tables.value = res.data.tables
+ })
+ /** 鏌ヨ瀛楀吀涓嬫媺鍒楄〃 */
+ getDictOptionselect().then(response => {
+ dictOptions.value = response.data
+ })
+ }
+})()
+</script>
diff --git a/src/views/tool/gen/genInfoForm.vue b/src/views/tool/gen/genInfoForm.vue
new file mode 100644
index 0000000..b416a89
--- /dev/null
+++ b/src/views/tool/gen/genInfoForm.vue
@@ -0,0 +1,305 @@
+<template>
+ <el-form ref="genInfoForm" :model="info" :rules="rules" label-width="150px">
+ <el-row>
+ <el-col :span="12">
+ <el-form-item prop="tplCategory">
+ <template #label>鐢熸垚妯℃澘</template>
+ <el-select v-model="info.tplCategory" @change="tplSelectChange">
+ <el-option label="鍗曡〃锛堝鍒犳敼鏌ワ級" value="crud" />
+ <el-option label="鏍戣〃锛堝鍒犳敼鏌ワ級" value="tree" />
+ <el-option label="涓诲瓙琛紙澧炲垹鏀规煡锛�" value="sub" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="tplWebType">
+ <template #label>鍓嶇绫诲瀷</template>
+ <el-select v-model="info.tplWebType">
+ <el-option label="Vue2 Element UI 妯$増" value="element-ui" />
+ <el-option label="Vue3 Element Plus 妯$増" value="element-plus" />
+ </el-select>
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="packageName">
+ <template #label>
+ 鐢熸垚鍖呰矾寰�
+ <el-tooltip content="鐢熸垚鍦ㄥ摢涓猨ava鍖呬笅锛屼緥濡� com.ruoyi.system" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="info.packageName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="moduleName">
+ <template #label>
+ 鐢熸垚妯″潡鍚�
+ <el-tooltip content="鍙悊瑙d负瀛愮郴缁熷悕锛屼緥濡� system" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="info.moduleName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="businessName">
+ <template #label>
+ 鐢熸垚涓氬姟鍚�
+ <el-tooltip content="鍙悊瑙d负鍔熻兘鑻辨枃鍚嶏紝渚嬪 user" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="info.businessName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="functionName">
+ <template #label>
+ 鐢熸垚鍔熻兘鍚�
+ <el-tooltip content="鐢ㄤ綔绫绘弿杩帮紝渚嬪 鐢ㄦ埛" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="info.functionName" />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item prop="genType">
+ <template #label>
+ 鐢熸垚浠g爜鏂瑰紡
+ <el-tooltip content="榛樿涓簔ip鍘嬬缉鍖呬笅杞斤紝涔熷彲浠ヨ嚜瀹氫箟鐢熸垚璺緞" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-radio v-model="info.genType" value="0">zip鍘嬬缉鍖�</el-radio>
+ <el-radio v-model="info.genType" value="1">鑷畾涔夎矾寰�</el-radio>
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 涓婄骇鑿滃崟
+ <el-tooltip content="鍒嗛厤鍒版寚瀹氳彍鍗曚笅锛屼緥濡� 绯荤粺绠$悊" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-tree-select
+ v-model="info.parentMenuId"
+ :data="menuOptions"
+ :props="{ value: 'menuId', label: 'menuName', children: 'children' }"
+ value-key="menuId"
+ placeholder="璇烽�夋嫨绯荤粺鑿滃崟"
+ check-strictly
+ />
+ </el-form-item>
+ </el-col>
+
+ <el-col :span="24" v-if="info.genType == '1'">
+ <el-form-item prop="genPath">
+ <template #label>
+ 鑷畾涔夎矾寰�
+ <el-tooltip content="濉啓纾佺洏缁濆璺緞锛岃嫢涓嶅~鍐欙紝鍒欑敓鎴愬埌褰撳墠Web椤圭洰涓�" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-input v-model="info.genPath">
+ <template #append>
+ <el-dropdown>
+ <el-button type="primary">
+ 鏈�杩戣矾寰勫揩閫熼�夋嫨
+ <i class="el-icon-arrow-down el-icon--right"></i>
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item @click="info.genPath = '/'">鎭㈠榛樿鐨勭敓鎴愬熀纭�璺緞</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ </template>
+ </el-input>
+ </el-form-item>
+ </el-col>
+ </el-row>
+
+ <template v-if="info.tplCategory == 'tree'">
+ <h4 class="form-header">鍏朵粬淇℃伅</h4>
+ <el-row v-show="info.tplCategory == 'tree'">
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 鏍戠紪鐮佸瓧娈�
+ <el-tooltip content="鏍戞樉绀虹殑缂栫爜瀛楁鍚嶏紝 濡傦細dept_id" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="info.treeCode" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(column, index) in info.columns"
+ :key="index"
+ :label="column.columnName + '锛�' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 鏍戠埗缂栫爜瀛楁
+ <el-tooltip content="鏍戞樉绀虹殑鐖剁紪鐮佸瓧娈靛悕锛� 濡傦細parent_Id" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="info.treeParentCode" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(column, index) in info.columns"
+ :key="index"
+ :label="column.columnName + '锛�' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 鏍戝悕绉板瓧娈�
+ <el-tooltip content="鏍戣妭鐐圭殑鏄剧ず鍚嶇О瀛楁鍚嶏紝 濡傦細dept_name" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="info.treeName" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(column, index) in info.columns"
+ :key="index"
+ :label="column.columnName + '锛�' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </template>
+
+ <template v-if="info.tplCategory == 'sub'">
+ <h4 class="form-header">鍏宠仈淇℃伅</h4>
+ <el-row>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 鍏宠仈瀛愯〃鐨勮〃鍚�
+ <el-tooltip content="鍏宠仈瀛愯〃鐨勮〃鍚嶏紝 濡傦細sys_user" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="info.subTableName" placeholder="璇烽�夋嫨" @change="subSelectChange">
+ <el-option
+ v-for="(table, index) in tables"
+ :key="index"
+ :label="table.tableName + '锛�' + table.tableComment"
+ :value="table.tableName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ <el-col :span="12">
+ <el-form-item>
+ <template #label>
+ 瀛愯〃鍏宠仈鐨勫閿悕
+ <el-tooltip content="瀛愯〃鍏宠仈鐨勫閿悕锛� 濡傦細user_id" placement="top">
+ <el-icon><question-filled /></el-icon>
+ </el-tooltip>
+ </template>
+ <el-select v-model="info.subTableFkName" placeholder="璇烽�夋嫨">
+ <el-option
+ v-for="(column, index) in subColumns"
+ :key="index"
+ :label="column.columnName + '锛�' + column.columnComment"
+ :value="column.columnName"
+ ></el-option>
+ </el-select>
+ </el-form-item>
+ </el-col>
+ </el-row>
+ </template>
+
+ </el-form>
+</template>
+
+<script setup>
+import { listMenu } from "@/api/system/menu"
+
+const subColumns = ref([])
+const menuOptions = ref([])
+const { proxy } = getCurrentInstance()
+
+const props = defineProps({
+ info: {
+ type: Object,
+ default: null
+ },
+ tables: {
+ type: Array,
+ default: null
+ }
+})
+
+// 琛ㄥ崟鏍¢獙
+const rules = ref({
+ tplCategory: [{ required: true, message: "璇烽�夋嫨鐢熸垚妯℃澘", trigger: "blur" }],
+ packageName: [{ required: true, message: "璇疯緭鍏ョ敓鎴愬寘璺緞", trigger: "blur" }],
+ moduleName: [{ required: true, message: "璇疯緭鍏ョ敓鎴愭ā鍧楀悕", trigger: "blur" }],
+ businessName: [{ required: true, message: "璇疯緭鍏ョ敓鎴愪笟鍔″悕", trigger: "blur" }],
+ functionName: [{ required: true, message: "璇疯緭鍏ョ敓鎴愬姛鑳藉悕", trigger: "blur" }]
+})
+
+function subSelectChange(value) {
+ props.info.subTableFkName = ""
+}
+
+function tplSelectChange(value) {
+ if (value !== "sub") {
+ props.info.subTableName = ""
+ props.info.subTableFkName = ""
+ }
+}
+
+function setSubTableColumns(value) {
+ for (var item in props.tables) {
+ const name = props.tables[item].tableName
+ if (value === name) {
+ subColumns.value = props.tables[item].columns
+ break
+ }
+ }
+}
+
+/** 鏌ヨ鑿滃崟涓嬫媺鏍戠粨鏋� */
+function getMenuTreeselect() {
+ listMenu().then(response => {
+ menuOptions.value = proxy.handleTree(response.data, "menuId")
+ })
+}
+
+onMounted(() => {
+ getMenuTreeselect()
+})
+
+watch(() => props.info.subTableName, val => {
+ setSubTableColumns(val)
+})
+
+watch(() => props.info.tplWebType, val => {
+ if (val === '') {
+ props.info.tplWebType = "element-plus"
+ }
+})
+</script>
diff --git a/src/views/tool/gen/importTable.vue b/src/views/tool/gen/importTable.vue
new file mode 100644
index 0000000..23dbf28
--- /dev/null
+++ b/src/views/tool/gen/importTable.vue
@@ -0,0 +1,126 @@
+<template>
+ <!-- 瀵煎叆琛� -->
+ <el-dialog title="瀵煎叆琛�" v-model="visible" width="800px" top="5vh" append-to-body>
+ <el-form :model="queryParams" ref="queryRef" :inline="true">
+ <el-form-item label="琛ㄥ悕绉�" prop="tableName">
+ <el-input
+ v-model="queryParams.tableName"
+ placeholder="璇疯緭鍏ヨ〃鍚嶇О"
+ clearable
+ style="width: 180px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="琛ㄦ弿杩�" prop="tableComment">
+ <el-input
+ v-model="queryParams.tableComment"
+ placeholder="璇疯緭鍏ヨ〃鎻忚堪"
+ clearable
+ style="width: 180px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery">鎼滅储</el-button>
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+ <el-row>
+ <el-table @row-click="clickRow" ref="table" :data="dbTableList" @selection-change="handleSelectionChange" height="260px">
+ <el-table-column type="selection" width="55"></el-table-column>
+ <el-table-column prop="tableName" label="琛ㄥ悕绉�" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="tableComment" label="琛ㄦ弿杩�" :show-overflow-tooltip="true"></el-table-column>
+ <el-table-column prop="createTime" label="鍒涘缓鏃堕棿"></el-table-column>
+ <el-table-column prop="updateTime" label="鏇存柊鏃堕棿"></el-table-column>
+ </el-table>
+ <pagination
+ v-show="total>0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ </el-row>
+ <template #footer>
+ <div class="dialog-footer">
+ <el-button type="primary" @click="handleImportTable">纭� 瀹�</el-button>
+ <el-button @click="visible = false">鍙� 娑�</el-button>
+ </div>
+ </template>
+ </el-dialog>
+</template>
+
+<script setup>
+import { listDbTable, importTable } from "@/api/tool/gen"
+
+const total = ref(0)
+const visible = ref(false)
+const tables = ref([])
+const dbTableList = ref([])
+const { proxy } = getCurrentInstance()
+
+const queryParams = reactive({
+ pageNum: 1,
+ pageSize: 10,
+ tableName: undefined,
+ tableComment: undefined
+})
+
+const emit = defineEmits(["ok"])
+
+/** 鏌ヨ鍙傛暟鍒楄〃 */
+function show() {
+ getList()
+ visible.value = true
+}
+
+/** 鍗曞嚮閫夋嫨琛� */
+function clickRow(row) {
+ proxy.$refs.table.toggleRowSelection(row)
+}
+
+/** 澶氶�夋閫変腑鏁版嵁 */
+function handleSelectionChange(selection) {
+ tables.value = selection.map(item => item.tableName)
+}
+
+/** 鏌ヨ琛ㄦ暟鎹� */
+function getList() {
+ listDbTable(queryParams).then(res => {
+ dbTableList.value = res.rows
+ total.value = res.total
+ })
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.pageNum = 1
+ getList()
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ proxy.resetForm("queryRef")
+ handleQuery()
+}
+
+/** 瀵煎叆鎸夐挳鎿嶄綔 */
+function handleImportTable() {
+ const tableNames = tables.value.join(",")
+ if (tableNames == "") {
+ proxy.$modal.msgError("璇烽�夋嫨瑕佸鍏ョ殑琛�")
+ return
+ }
+ importTable({ tables: tableNames }).then(res => {
+ proxy.$modal.msgSuccess(res.msg)
+ if (res.code === 200) {
+ visible.value = false
+ emit("ok")
+ }
+ })
+}
+
+defineExpose({
+ show,
+})
+</script>
diff --git a/src/views/tool/gen/index.vue b/src/views/tool/gen/index.vue
new file mode 100644
index 0000000..aaddeb6
--- /dev/null
+++ b/src/views/tool/gen/index.vue
@@ -0,0 +1,437 @@
+<template>
+ <div class="app-container">
+ <el-form
+ :model="queryParams"
+ ref="queryRef"
+ :inline="true"
+ v-show="showSearch"
+ >
+ <el-form-item label="琛ㄥ悕绉�" prop="tableName">
+ <el-input
+ v-model="queryParams.tableName"
+ placeholder="璇疯緭鍏ヨ〃鍚嶇О"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="琛ㄦ弿杩�" prop="tableComment">
+ <el-input
+ v-model="queryParams.tableComment"
+ placeholder="璇疯緭鍏ヨ〃鎻忚堪"
+ clearable
+ style="width: 200px"
+ @keyup.enter="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="鍒涘缓鏃堕棿" style="width: 308px">
+ <el-date-picker
+ v-model="dateRange"
+ value-format="YYYY-MM-DD"
+ type="daterange"
+ range-separator="-"
+ start-placeholder="寮�濮嬫棩鏈�"
+ end-placeholder="缁撴潫鏃ユ湡"
+ ></el-date-picker>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="Search" @click="handleQuery"
+ >鎼滅储</el-button
+ >
+ <el-button icon="Refresh" @click="resetQuery">閲嶇疆</el-button>
+ </el-form-item>
+ </el-form>
+
+ <el-row :gutter="10" class="mb8">
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Download"
+ :disabled="multiple"
+ @click="handleGenTable"
+ v-hasPermi="['tool:gen:code']"
+ >鐢熸垚</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="primary"
+ plain
+ icon="Plus"
+ @click="openCreateTable"
+ v-hasRole="['admin']"
+ >鍒涘缓</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="info"
+ plain
+ icon="Upload"
+ @click="openImportTable"
+ v-hasPermi="['tool:gen:import']"
+ >瀵煎叆</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="success"
+ plain
+ icon="Edit"
+ :disabled="single"
+ @click="handleEditTable"
+ v-hasPermi="['tool:gen:edit']"
+ >淇敼</el-button
+ >
+ </el-col>
+ <el-col :span="1.5">
+ <el-button
+ type="danger"
+ plain
+ icon="Delete"
+ :disabled="multiple"
+ @click="handleDelete"
+ v-hasPermi="['tool:gen:remove']"
+ >鍒犻櫎</el-button
+ >
+ </el-col>
+ <right-toolbar
+ v-model:showSearch="showSearch"
+ @queryTable="getList"
+ ></right-toolbar>
+ </el-row>
+
+ <el-table
+ ref="genRef"
+ v-loading="loading"
+ :data="tableList"
+ @selection-change="handleSelectionChange"
+ :default-sort="defaultSort"
+ @sort-change="handleSortChange"
+ >
+ <el-table-column
+ type="selection"
+ align="center"
+ width="55"
+ ></el-table-column>
+ <el-table-column label="搴忓彿" type="index" width="50" align="center">
+ <template #default="scope">
+ <span>{{
+ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1
+ }}</span>
+ </template>
+ </el-table-column>
+ <el-table-column
+ label="琛ㄥ悕绉�"
+ align="center"
+ prop="tableName"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="琛ㄦ弿杩�"
+ align="center"
+ prop="tableComment"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="瀹炰綋"
+ align="center"
+ prop="className"
+ :show-overflow-tooltip="true"
+ />
+ <el-table-column
+ label="鍒涘缓鏃堕棿"
+ align="center"
+ prop="createTime"
+ width="160"
+ sortable="custom"
+ :sort-orders="['descending', 'ascending']"
+ />
+ <el-table-column
+ label="鏇存柊鏃堕棿"
+ align="center"
+ prop="updateTime"
+ width="160"
+ sortable="custom"
+ :sort-orders="['descending', 'ascending']"
+ />
+ <el-table-column
+ label="鎿嶄綔"
+ align="center"
+ width="330"
+ class-name="small-padding fixed-width"
+ >
+ <template #default="scope">
+ <el-tooltip content="棰勮" placement="top">
+ <el-button
+ link
+ type="primary"
+ icon="View"
+ @click="handlePreview(scope.row)"
+ v-hasPermi="['tool:gen:preview']"
+ ></el-button>
+ </el-tooltip>
+ <el-tooltip content="缂栬緫" placement="top">
+ <el-button
+ link
+ type="primary"
+ icon="Edit"
+ @click="handleEditTable(scope.row)"
+ v-hasPermi="['tool:gen:edit']"
+ ></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍒犻櫎" placement="top">
+ <el-button
+ link
+ type="primary"
+ icon="Delete"
+ @click="handleDelete(scope.row)"
+ v-hasPermi="['tool:gen:remove']"
+ ></el-button>
+ </el-tooltip>
+ <el-tooltip content="鍚屾" placement="top">
+ <el-button
+ link
+ type="primary"
+ icon="Refresh"
+ @click="handleSynchDb(scope.row)"
+ v-hasPermi="['tool:gen:edit']"
+ ></el-button>
+ </el-tooltip>
+ <el-tooltip content="鐢熸垚浠g爜" placement="top">
+ <el-button
+ link
+ type="primary"
+ icon="Download"
+ @click="handleGenTable(scope.row)"
+ v-hasPermi="['tool:gen:code']"
+ ></el-button>
+ </el-tooltip>
+ </template>
+ </el-table-column>
+ </el-table>
+ <pagination
+ v-show="total > 0"
+ :total="total"
+ v-model:page="queryParams.pageNum"
+ v-model:limit="queryParams.pageSize"
+ @pagination="getList"
+ />
+ <!-- 棰勮鐣岄潰 -->
+ <el-dialog
+ :title="preview.title"
+ v-model="preview.open"
+ width="80%"
+ top="5vh"
+ append-to-body
+ class="scrollbar"
+ >
+ <el-tabs v-model="preview.activeName">
+ <el-tab-pane
+ v-for="(value, key) in preview.data"
+ :label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
+ :name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
+ :key="value"
+ >
+ <el-link
+ :underline="false"
+ icon="DocumentCopy"
+ v-copyText="value"
+ v-copyText:callback="copyTextSuccess"
+ style="float: right"
+ > 澶嶅埗</el-link
+ >
+ <pre>{{ value }}</pre>
+ </el-tab-pane>
+ </el-tabs>
+ </el-dialog>
+ <import-table ref="importRef" @ok="handleQuery" />
+ <create-table ref="createRef" @ok="handleQuery" />
+ </div>
+</template>
+
+<script setup name="Gen">
+import {
+ listTable,
+ previewTable,
+ delTable,
+ genCode,
+ synchDb,
+} from "@/api/tool/gen";
+import router from "@/router";
+import importTable from "./importTable";
+import createTable from "./createTable";
+
+const route = useRoute();
+const { proxy } = getCurrentInstance();
+
+const tableList = ref([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+const tableNames = ref([]);
+const dateRange = ref([]);
+const uniqueId = ref("");
+const defaultSort = ref({ prop: "createTime", order: "descending" });
+
+const data = reactive({
+ queryParams: {
+ pageNum: 1,
+ pageSize: 10,
+ tableName: undefined,
+ tableComment: undefined,
+ orderByColumn: defaultSort.value.prop,
+ isAsc: defaultSort.value.order,
+ },
+ preview: {
+ open: false,
+ title: "浠g爜棰勮",
+ data: {},
+ activeName: "domain.java",
+ },
+});
+
+const { queryParams, preview } = toRefs(data);
+
+onActivated(() => {
+ const time = route.query.t;
+ if (time != null && time != uniqueId.value) {
+ uniqueId.value = time;
+ queryParams.value.pageNum = Number(route.query.pageNum);
+ dateRange.value = [];
+ proxy.resetForm("queryForm");
+ getList();
+ }
+});
+
+/** 鏌ヨ琛ㄩ泦鍚� */
+function getList() {
+ loading.value = true;
+ listTable(proxy.addDateRange(queryParams.value, dateRange.value)).then(
+ (response) => {
+ tableList.value = response.rows;
+ total.value = response.total;
+ loading.value = false;
+ }
+ );
+}
+
+/** 鎼滅储鎸夐挳鎿嶄綔 */
+function handleQuery() {
+ queryParams.value.pageNum = 1;
+ getList();
+}
+
+/** 鐢熸垚浠g爜鎿嶄綔 */
+function handleGenTable(row) {
+ const tbNames = row.tableName || tableNames.value;
+ if (tbNames == "") {
+ proxy.$modal.msgError("璇烽�夋嫨瑕佺敓鎴愮殑鏁版嵁");
+ return;
+ }
+ if (row.genType === "1") {
+ genCode(row.tableName).then((response) => {
+ proxy.$modal.msgSuccess("鎴愬姛鐢熸垚鍒拌嚜瀹氫箟璺緞锛�" + row.genPath);
+ });
+ } else {
+ proxy.$download.zip(
+ "/tool/gen/batchGenCode?tables=" + tbNames,
+ "ruoyi.zip"
+ );
+ }
+}
+
+/** 鍚屾鏁版嵁搴撴搷浣� */
+function handleSynchDb(row) {
+ const tableName = row.tableName;
+ proxy.$modal
+ .confirm('纭瑕佸己鍒跺悓姝�"' + tableName + '"琛ㄧ粨鏋勫悧锛�')
+ .then(function () {
+ return synchDb(tableName);
+ })
+ .then(() => {
+ proxy.$modal.msgSuccess("鍚屾鎴愬姛");
+ })
+ .catch(() => {});
+}
+
+/** 鎵撳紑瀵煎叆琛ㄥ脊绐� */
+function openImportTable() {
+ proxy.$refs["importRef"].show();
+}
+
+/** 鎵撳紑鍒涘缓琛ㄥ脊绐� */
+function openCreateTable() {
+ proxy.$refs["createRef"].show();
+}
+
+/** 閲嶇疆鎸夐挳鎿嶄綔 */
+function resetQuery() {
+ dateRange.value = [];
+ proxy.resetForm("queryRef");
+ queryParams.value.pageNum = 1;
+ proxy.$refs["genRef"].sort(defaultSort.value.prop, defaultSort.value.order);
+}
+
+/** 棰勮鎸夐挳 */
+function handlePreview(row) {
+ previewTable(row.tableId).then((response) => {
+ preview.value.data = response.data;
+ preview.value.open = true;
+ preview.value.activeName = "domain.java";
+ });
+}
+
+/** 澶嶅埗浠g爜鎴愬姛 */
+function copyTextSuccess() {
+ proxy.$modal.msgSuccess("澶嶅埗鎴愬姛");
+}
+
+// 澶氶�夋閫変腑鏁版嵁
+function handleSelectionChange(selection) {
+ ids.value = selection.map((item) => item.tableId);
+ tableNames.value = selection.map((item) => item.tableName);
+ single.value = selection.length != 1;
+ multiple.value = !selection.length;
+}
+
+/** 鎺掑簭瑙﹀彂浜嬩欢 */
+function handleSortChange(column, prop, order) {
+ queryParams.value.orderByColumn = column.prop;
+ queryParams.value.isAsc = column.order;
+ getList();
+}
+
+/** 淇敼鎸夐挳鎿嶄綔 */
+function handleEditTable(row) {
+ const tableId = row.tableId || ids.value[0];
+ const tableName = row.tableName || tableNames.value[0];
+ const params = { pageNum: queryParams.value.pageNum };
+ proxy.$tab.openPage(
+ "淇敼[" + tableName + "]鐢熸垚閰嶇疆",
+ "/tool/gen-edit/index/" + tableId,
+ params
+ );
+}
+
+/** 鍒犻櫎鎸夐挳鎿嶄綔 */
+function handleDelete(row) {
+ const tableIds = row.tableId || ids.value;
+ proxy.$modal
+ .confirm('鏄惁纭鍒犻櫎琛ㄧ紪鍙蜂负"' + tableIds + '"鐨勬暟鎹」锛�')
+ .then(function () {
+ return delTable(tableIds);
+ })
+ .then(() => {
+ getList();
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ })
+ .catch(() => {});
+}
+
+getList();
+</script>
diff --git a/src/views/tool/swagger/index.vue b/src/views/tool/swagger/index.vue
new file mode 100644
index 0000000..f29c40f
--- /dev/null
+++ b/src/views/tool/swagger/index.vue
@@ -0,0 +1,9 @@
+<template>
+ <i-frame v-model:src="url"></i-frame>
+</template>
+
+<script setup>
+import iFrame from '@/components/iFrame'
+
+const url = ref(import.meta.env.VITE_APP_BASE_API + "/swagger-ui/index.html")
+</script>
diff --git a/vite/plugins/auto-import.js b/vite/plugins/auto-import.js
new file mode 100644
index 0000000..ba51689
--- /dev/null
+++ b/vite/plugins/auto-import.js
@@ -0,0 +1,12 @@
+import autoImport from 'unplugin-auto-import/vite'
+
+export default function createAutoImport() {
+ return autoImport({
+ imports: [
+ 'vue',
+ 'vue-router',
+ 'pinia'
+ ],
+ dts: false
+ })
+}
diff --git a/vite/plugins/compression.js b/vite/plugins/compression.js
new file mode 100644
index 0000000..9767308
--- /dev/null
+++ b/vite/plugins/compression.js
@@ -0,0 +1,28 @@
+import compression from 'vite-plugin-compression'
+
+export default function createCompression(env) {
+ const { VITE_BUILD_COMPRESS } = env
+ const plugin = []
+ if (VITE_BUILD_COMPRESS) {
+ const compressList = VITE_BUILD_COMPRESS.split(',')
+ if (compressList.includes('gzip')) {
+ // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#浣跨敤gzip瑙e帇缂╅潤鎬佹枃浠�
+ plugin.push(
+ compression({
+ ext: '.gz',
+ deleteOriginFile: false
+ })
+ )
+ }
+ if (compressList.includes('brotli')) {
+ plugin.push(
+ compression({
+ ext: '.br',
+ algorithm: 'brotliCompress',
+ deleteOriginFile: false
+ })
+ )
+ }
+ }
+ return plugin
+}
diff --git a/vite/plugins/index.js b/vite/plugins/index.js
new file mode 100644
index 0000000..7715adc
--- /dev/null
+++ b/vite/plugins/index.js
@@ -0,0 +1,15 @@
+import vue from '@vitejs/plugin-vue'
+
+import createAutoImport from './auto-import'
+import createSvgIcon from './svg-icon'
+import createCompression from './compression'
+import createSetupExtend from './setup-extend'
+
+export default function createVitePlugins(viteEnv, isBuild = false) {
+ const vitePlugins = [vue()]
+ vitePlugins.push(createAutoImport())
+ vitePlugins.push(createSetupExtend())
+ vitePlugins.push(createSvgIcon(isBuild))
+ isBuild && vitePlugins.push(...createCompression(viteEnv))
+ return vitePlugins
+}
diff --git a/vite/plugins/setup-extend.js b/vite/plugins/setup-extend.js
new file mode 100644
index 0000000..eac072c
--- /dev/null
+++ b/vite/plugins/setup-extend.js
@@ -0,0 +1,5 @@
+import setupExtend from 'unplugin-vue-setup-extend-plus/vite'
+
+export default function createSetupExtend() {
+ return setupExtend({})
+}
diff --git a/vite/plugins/svg-icon.js b/vite/plugins/svg-icon.js
new file mode 100644
index 0000000..f23beff
--- /dev/null
+++ b/vite/plugins/svg-icon.js
@@ -0,0 +1,10 @@
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+import path from 'path'
+
+export default function createSvgIcon(isBuild) {
+ return createSvgIconsPlugin({
+ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
+ symbolId: 'icon-[dir]-[name]',
+ svgoOptions: isBuild
+ })
+}
--
Gitblit v1.9.3