gaoluyang
5 天以前 07f9f8657d057a38792c3822acc9b08d83478967
合并代码
已添加50个文件
已修改198个文件
已删除2个文件
37919 ■■■■ 文件已修改
FILE_UPLOAD_README.md 480 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
jsconfig.json 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/common.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customer.js 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/customerFile.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/parameterMaintenance.js 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/productModel.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/productProcess.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/basicData/storageAttachment.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/collaborativeApproval/approvalManagement.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/equipmentManagement/sparePartsUsage.js 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInRecord.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockInventory.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockOut.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/stockUninventory.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/class.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/procurementInvoiceLedger.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/procurementManagement/purchase_return_order.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRoute.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteFile.js 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/processRouteItem.js 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productBom.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productProcessRoute.js 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productStructure.js 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionCosting.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionOrder.js 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/productionProcess.js 71 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionManagement/workOrder.js 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/productionPlan/productionPlan.js 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/deliveryLedger.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/appVersion.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/element-ui.scss 123 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/index.scss 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/sidebar.scss 125 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/styles/variables.module.scss 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue 4423 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AttachmentPreview/image/index.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AttachmentUpload/file/index.vue 309 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AttachmentUpload/image/index.vue 335 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Breadcrumb/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/FileList.vue 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Dialog/FileListDialog.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Editor/index.vue 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImagePreview/index.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ImageUpload/index.vue 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 253 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/Pagination.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PageHeader/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProcessParamListDialog.vue 642 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/ProjectManagement/ProgressReportDialog.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PurchaseAIChatSidebar/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/SvgIcon/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/AppMain.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Navbar.vue 56 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/Logo.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/Sidebar/index.vue 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/ScrollPane.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/components/TagsView/index.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layout/index.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/permission.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/modules/user.js 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/generator/html.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFile/index.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/customerFileOpenSea/index.vue 1803 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/parameterMaintenance/index.vue 793 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/ProductSelectDialog.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/product/index.vue 286 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/basicData/supplierManage/components/HomeTab.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalManagement/index.vue 881 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue 184 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/fileList.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/approvalProcess/index.vue 420 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/attendanceManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/customerVisit/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/enterpriseBook/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/knowledgeBase/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/meetingBoard/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/noticeManagement/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/notificationManagement/summary/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/planTemplate/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/processTracking/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/purchaseApproval/index.vue 440 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rpaManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue 87 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/collaborativeApproval/sealManagement/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/afterSalesHandling/index.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/expiryAfterSales/index.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/components/formDia.vue 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/feedbackRegistration/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/energyManagement/dynamicEnergySaving/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/brand/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/calibration/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/defectManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/formDia.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 21 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/MaintainModal.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairModal.vue 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/spareParts/index.vue 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 578 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/example/DynamicTableExample.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/bookshelf/index.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/borrow/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/document/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/fileManagement/return/index.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/accounting/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/fixedAssets.vue 462 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/assets/intangibleAssets.vue 458 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/expenseManagement/index.vue 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/generalLedger/index.vue 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/input-invoice.vue 409 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/payment.vue 377 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/paymentApply.vue 360 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/purchaseIn.vue 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/payable/reconciliation.vue 469 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/invoiceApply.vue 363 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/outputInvoice.vue 373 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/receipt.vue 356 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/reconciliation.vue 469 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/salesOut.vue 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/receivable/salesReturn.vue 305 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/revenueManagement/index.vue 219 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/salesRefund/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/detailLedger.vue 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/generalLedger.vue 230 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/financialManagement/voucher/index.vue 836 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/Record.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/dispatchLog/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/issueManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/New.vue 166 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Qualified.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Record.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Subtract.vue 46 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/Unqualified.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockManagement/index.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockWarning/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/transportTaskManagement/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleFuelManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lavorissue/statistics/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/login.vue 377 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/milestoneList.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/taskTree.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/projectDetail.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/analytics/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/index.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/classsSheduling/index.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/components/formDia.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/dimission/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/employeeRecord/index.vue 141 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/monthlyStatistics/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/socialSecuritySet/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentEntry/index.vue 32 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentHistory/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/paymentLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementInvoiceLedger/index.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 300 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementReport/index.vue 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/New.vue 276 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/purchaseReturnOrder/index.vue 287 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productManagement/productIdentifier/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/New.vue 81 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/index.vue 206 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processRoute/processRouteItem/index.vue 1102 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/processStatistics/index.vue 268 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/Detail/index.vue 121 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productStructure/index.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionCosting/index.vue 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/New.vue 130 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue 284 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue 391 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionOrder/index.vue 715 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/Edit.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/New.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionProcess/index.vue 1300 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionReporting/index.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/productionTraceability/index.vue 706 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrder/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderEdit/index.vue 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue 320 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionManagement/workOrderManagement/index.vue 301 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/components/PIMTable.vue 470 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/productionPlan/productionPlan/index.vue 1332 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/formDia.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/index.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/projectDetail.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/components/ProjectTypeDialog.vue 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/projectType/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/roles/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/metricBinding/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/metricMaintenance/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/nonconformingManagement/index.vue 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/dataDashboard/index0.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/reportManagement/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/accidentReportingRecord/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/dangerInvestigation/index.vue 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/emergencyPlanReview/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/hazardSourceLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/hazardousMaterialsControl/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeQualifications/index.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safeWorkApproval/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/safeProduction/safetyTrainingAssessment/index.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 260 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/indicatorStats/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/invoiceLedger/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/invoiceRegistration/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/orderManagement/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPayment/index.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPaymentHistory/index.vue 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPaymentLedger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/components/formDia.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/returnOrder/index.vue 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/fileList.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 1524 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 262 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/strategyControl/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/index.vue 270 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/role/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/systemArchitecture/index.vue 436 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/tool/build/CodeTypeDialog.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
FILE_UPLOAD_README.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,480 @@
# æœ¬åœ°æ–‡ä»¶ä¸Šä¼  README
本文档基于以下实现整理:
- `src/components/AttachmentUpload/file/index.vue`
- `src/components/AttachmentUpload/image/index.vue`
- `src/components/AttachmentPreview/image/index.vue`
- `src/components/Dialog/FileList.vue`
- `src/api/basicData/common.js`
- `src/api/publicApi/commonFile.js`
相关组件已在 `src/main.js` ä¸­æ³¨å†Œä¸ºå…¨å±€ç»„件,可直接在页面中使用:
- `FileUpload`
- `ImageUpload`
- `ImagePreview`
- `FileListDialog`
## 1. åŠŸèƒ½æ¦‚è§ˆ
当前这套上传能力主要分为 4 éƒ¨åˆ†ï¼š
1. `FileUpload`:普通文件上传,支持拖拽、批量上传、预览、删除
2. `ImageUpload`:图片上传,支持图片墙展示、预览、删除
3. `ImagePreview`:图片列表预览展示
4. `FileListDialog`:业务附件弹窗,支持查询、上传、删除、下载
上传底层统一走接口:
- `POST /common/upload`
对应方法在 `src/api/basicData/common.js`:
```js
uploadFile(data)
```
## 2. ä¸Šä¼ æŽ¥å£è¯´æ˜Ž
### 2.1 é€šç”¨ä¸Šä¼ æŽ¥å£
文件上传组件和图片上传组件都调用了:
```js
import { uploadFile } from '@/api/basicData/common'
```
接口特征:
- è¯·æ±‚方式:`POST`
- åœ°å€ï¼š`/common/upload`
- è¯·æ±‚类型:`multipart/form-data`
- æ”¯æŒ `FormData` æ‰¹é‡ä¸Šä¼ 
- é»˜è®¤å­—段名:`files`
组件内部会这样组装参数:
```js
const formData = new FormData()
validFiles.forEach((file) => {
  formData.append(props.uploadFieldName, file.raw)
})
```
### 2.2 ä¸Šä¼ è¿”回值要求
上传成功后,组件会尝试从以下结构中提取数组:
- `response`
- `response.data`
- `response.data.data`
- `response.payload`
- `response.payload.data`
- `response.rows`
- `response.result`
因此后端返回数组时,上面任意一种结构都可以被识别。
组件展示时常用到的字段有:
- æ–‡ä»¶åï¼š`name` / `originalFilename` / `fileName` / `uidFilename`
- æ–‡ä»¶åœ°å€ï¼š`url` / `downloadURL`
- å›¾ç‰‡åœ°å€ï¼š`url` / `previewURL` / `previewUrl`
- ä¸»é”®ï¼š`id`
建议上传接口返回的单项对象尽量包含:
```js
{
  id: 1,
  originalFilename: 'demo.pdf',
  downloadURL: 'https://xxx/demo.pdf',
  previewURL: 'https://xxx/demo.png'
}
```
## 3. FileUpload æ–‡ä»¶ä¸Šä¼ ç»„ä»¶
组件路径:
`src/components/AttachmentUpload/file/index.vue`
### 3.1 åŸºç¡€ç”¨æ³•
```vue
<template>
  <FileUpload v-model:file-list="fileList" />
</template>
<script setup>
import { ref } from 'vue'
const fileList = ref([])
</script>
```
### 3.2 å¸¸ç”¨å±žæ€§
| å±žæ€§ | è¯´æ˜Ž | ç±»åž‹ | é»˜è®¤å€¼ |
| --- | --- | --- | --- |
| `fileList` | ç»‘定文件列表 | `Array` | `[]` |
| `limit` | æœ€å¤§ä¸Šä¼ æ•°é‡ | `Number` | `10` |
| `fileSize` | å•个文件大小限制,单位 MB | `Number` | `50` |
| `fileType` | å…è®¸ä¸Šä¼ çš„æ–‡ä»¶ç±»åž‹ï¼Œå¦‚ `['pdf', 'docx']` | `Array` | `[]` |
| `buttonText` | ä¸Šä¼ æç¤ºæ–‡æ¡ˆ | `String` | `单击选择文件` |
| `disabled` | æ˜¯å¦ç¦ç”¨ | `Boolean` | `false` |
| `uploadFieldName` | `FormData` å­—段名 | `String` | `files` |
| `index` | è¡¨æ ¼/列表行模式下的当前行索引 | `Number` | `-1` |
| `childrenKey` | è¡Œå†…挂载字段名 | `String` | `files` |
### 3.3 äº‹ä»¶
| äº‹ä»¶ | è¯´æ˜Ž |
| --- | --- |
| `update:fileList` | æ–‡ä»¶åˆ—表变化时触发 |
| `change` | æ–‡ä»¶åˆ—表变化时触发,返回最新列表 |
### 3.4 é™åˆ¶è§„则
组件内已实现:
- æ–‡ä»¶æ•°é‡é™åˆ¶
- æ–‡ä»¶å¤§å°é™åˆ¶
- æ–‡ä»¶ç±»åž‹æ ¡éªŒ
- ä¸Šä¼ ä¸­çŠ¶æ€é”å®š
- å¤±è´¥åŽè‡ªåŠ¨æ¸…ç©ºå½“å‰é€‰æ‹©é˜Ÿåˆ—
例如限制 PDF/Word:
```vue
<FileUpload
  v-model:file-list="fileList"
  :limit="5"
  :file-size="20"
  :file-type="['pdf', 'doc', 'docx']"
/>
```
### 3.5 è¿”回数据格式建议
`FileUpload` æ›´é€‚合接收这样的列表:
```js
[
  {
    id: 1,
    originalFilename: '合同.pdf',
    downloadURL: 'https://xxx/contract.pdf'
  }
]
```
因为组件打开文件时会优先读取:
```js
url || downloadURL || previewURL || previewUrl
```
### 3.6 è¡Œå†…嵌套模式
如果上传组件放在表格某一行中,可配合 `index` å’Œ `childrenKey` ä½¿ç”¨ï¼š
```vue
<FileUpload
  v-model:file-list="tableData"
  :index="scope.$index"
  children-key="files"
/>
```
此时组件会自动读写:
```js
tableData[scope.$index].files
```
## 4. ImageUpload å›¾ç‰‡ä¸Šä¼ ç»„ä»¶
组件路径:
`src/components/AttachmentUpload/image/index.vue`
### 4.1 åŸºç¡€ç”¨æ³•
```vue
<template>
  <ImageUpload v-model:file-list="imageList" />
</template>
<script setup>
import { ref } from 'vue'
const imageList = ref([])
</script>
```
### 4.2 é»˜è®¤è¡Œä¸º
图片上传组件默认:
- æœ€å¤šä¸Šä¼  `10` å¼ 
- å•张不超过 `10MB`
- é»˜è®¤æ”¯æŒæ ¼å¼ï¼š`png / jpg / jpeg / webp`
- ä½¿ç”¨ `picture-card` é£Žæ ¼å±•示
- ç‚¹å‡»ç¼©ç•¥å›¾å¯é¢„览大图
### 4.3 å¸¸ç”¨ç¤ºä¾‹
```vue
<ImageUpload
  v-model:file-list="imageList"
  :limit="9"
  :file-size="5"
  :file-type="['png', 'jpg', 'jpeg']"
  button-text="上传图片"
/>
```
### 4.4 è¿”回数据格式建议
`ImageUpload` å±•示图片时优先读取:
```js
url || previewURL || previewUrl
```
建议后端返回:
```js
[
  {
    id: 1,
    originalFilename: '现场图片.jpg',
    previewURL: 'https://xxx/image.jpg'
  }
]
```
### 4.5 è¡Œå†…嵌套模式
图片组件同样支持行内字段写回:
```vue
<ImageUpload
  v-model:file-list="tableData"
  :index="scope.$index"
  children-key="images"
/>
```
默认写回字段为 `images`。
## 5. ImagePreview å›¾ç‰‡é¢„览组件
组件路径:
`src/components/AttachmentPreview/image/index.vue`
### 5.1 åŸºç¡€ç”¨æ³•
```vue
<ImagePreview :file-list="imageList" />
```
### 5.2 å¯é…ç½®é¡¹
| å±žæ€§ | è¯´æ˜Ž | ç±»åž‹ | é»˜è®¤å€¼ |
| --- | --- | --- | --- |
| `fileList` | å›¾ç‰‡åˆ—表 | `Array` | `[]` |
| `thumbSize` | ç¼©ç•¥å›¾å¤§å° | `Number` | `72` |
| `gap` | ç¼©ç•¥å›¾é—´è· | `Number` | `10` |
### 5.3 æ•°æ®è¦æ±‚
组件会过滤没有 `previewURL` çš„项,因此如果要正常显示,建议至少包含:
```js
[
  {
    previewURL: 'https://xxx/image.jpg',
    originalFilename: '图片1.jpg'
  }
]
```
如果列表为空,组件显示“暂无图片”。
## 6. FileListDialog é™„件弹窗组件
组件路径:
`src/components/Dialog/FileList.vue`
这个组件适合业务表单或详情页里的“附件管理”场景,能力包括:
- æ ¹æ®ä¸šåŠ¡ä¸»é”®æŸ¥è¯¢é™„ä»¶åˆ—è¡¨
- æ‰“开弹窗查看附件
- åœ¨å¼¹çª—中继续上传附件
- åˆ é™¤é™„ä»¶
- ä¸‹è½½é™„ä»¶
### 6.1 ç»„件属性
| å±žæ€§ | è¯´æ˜Ž | ç±»åž‹ | é»˜è®¤å€¼ |
| --- | --- | --- | --- |
| `visible` | æ˜¯å¦æ˜¾ç¤ºå¼¹çª— | `Boolean` | å¿…ä¼  |
| `recordType` | ä¸šåŠ¡ç±»åž‹ | `String` | `''` |
| `recordId` | ä¸šåС䏻键 | `Number` | `0` |
| `title` | å¼¹çª—标题 | `String` | `附件` |
| `width` | å¼¹çª—宽度 | `String` | `50%` |
| `showActions` | æ˜¯å¦æ˜¾ç¤ºä¸‹è½½/删除操作列 | `Boolean` | `true` |
### 6.2 åŸºç¡€ç”¨æ³•
```vue
<template>
  <el-button @click="visible = true">查看附件</el-button>
  <FileListDialog
    v-model:visible="visible"
    record-type="salesLedger"
    :record-id="rowId"
    title="附件列表"
  />
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const rowId = ref(1001)
</script>
```
### 6.3 ç»„件内部依赖的接口
`FileListDialog` æœ¬èº«ä¸ç›´æŽ¥è°ƒç”¨ `commonFile.js`,而是依赖:
- `attachmentList`
- `createAttachment`
- `deleteAttachment`
处理逻辑为:
1. æ‰“开弹窗后根据 `recordType + recordId` æŸ¥è¯¢é™„ä»¶
2. ç‚¹å‡»â€œä¸Šä¼ é™„件”后,内部使用 `AttachmentUpload/file` å…ˆä¸Šä¼ åˆ° `/common/upload`
3. ä¸Šä¼ æˆåŠŸåŽï¼Œå°†è¿”å›žçš„æ–‡ä»¶å¯¹è±¡å’Œå·²æœ‰åˆ—è¡¨ä¸€èµ·æäº¤ç»™ `createAttachment`
4. åˆ é™¤æ—¶è°ƒç”¨ `deleteAttachment`
5. ä¸‹è½½æ—¶ç›´æŽ¥ `window.open(downloadURL, '_blank')`
因此这里要特别注意:
- `recordType` å’Œ `recordId` å¿…须是有效业务标识
- ä¸Šä¼ æˆåŠŸè¿”å›žçš„æ•°æ®ï¼Œéœ€è¦èƒ½è¢« `createAttachment` ç›´æŽ¥æŽ¥æ”¶
- åˆ—表中的下载地址字段应为 `downloadURL`
## 7. commonFile.js è¯´æ˜Ž
文件路径:
`src/api/publicApi/commonFile.js`
当前文件提供的是公共文件删除接口:
```js
delCommonFile(ids)
delCommonFileInvoiceLedger(ids)
```
对应接口:
- `/commonFile/delCommonFile`
- `/invoiceLedger/delFile`
这两个方法更适合已经和具体业务绑定后的“删除已保存附件”场景,不负责上传文件本身。
示例:
```js
import { delCommonFile } from '@/api/publicApi/commonFile'
await delCommonFile([1, 2, 3])
```
## 8. æŽ¨èä½¿ç”¨æ–¹å¼
### 8.1 æ™®é€šä¸šåŠ¡è¡¨å•ä¸Šä¼ é™„ä»¶
```vue
<FileUpload v-model:file-list="form.storageBlobDTOs" />
```
提交表单时直接带上:
```js
{
  ...form,
  storageBlobDTOs: form.storageBlobDTOs
}
```
### 8.2 å›¾ç‰‡ç±»ä¸šåŠ¡
```vue
<ImageUpload v-model:file-list="form.images" />
<ImagePreview :file-list="form.images" />
```
### 8.3 å·²è½åº“附件管理
```vue
<FileListDialog
  v-model:visible="dialogVisible"
  :record-type="recordType"
  :record-id="recordId"
/>
```
适合详情页、审批页、台账页这类“查看并维护当前业务附件”的场景。
## 9. æ³¨æ„äº‹é¡¹
1. `FileUpload` å’Œ `ImageUpload` åªæ˜¯è´Ÿè´£æŠŠæ–‡ä»¶å…ˆä¼ åˆ° `/common/upload`,不等于已经和业务数据绑定。
2. å¦‚果业务需要持久化附件关系,仍需要在保存表单时把返回的文件对象提交给业务接口。
3. `ImagePreview` å½“前只识别 `previewURL`,如果后端只返回 `url`,预览组件将不会展示,最好统一补齐 `previewURL`。
4. `FileListDialog` ä¾èµ– `recordType`、`recordId` æŸ¥è¯¢å’Œä¿å­˜é™„件关系,新增业务时要先确认后端关联接口可用。
5. åˆ é™¤æœ¬åœ°åˆ—表项和删除已保存附件是两件事:
   - ä¸Šä¼ ç»„件里的删除:只会从当前前端绑定数组中移除
   - `commonFile.js` / `deleteAttachment`:才是真正调用后端删除
## 10. ä¸€å¥—最常见的页面写法
```vue
<template>
  <el-form :model="form">
    <el-form-item label="附件">
      <FileUpload v-model:file-list="form.storageBlobDTOs" :limit="5" />
    </el-form-item>
    <el-form-item label="现场图片">
      <ImageUpload v-model:file-list="form.images" :limit="9" />
    </el-form-item>
    <el-form-item label="图片预览">
      <ImagePreview :file-list="form.images" />
    </el-form-item>
  </el-form>
</template>
<script setup>
import { ref } from 'vue'
const form = ref({
  storageBlobDTOs: [],
  images: [],
})
</script>
```
如果你的目标是“先上传,再跟业务一起保存”,这套写法可以直接作为基础模板使用。
jsconfig.json
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "~/*": ["./*"]
    }
  },
  "include": ["src/**/*.js", "src/**/*.vue", "vite.config.js"]
}
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',
    },
  })
}
src/api/basicData/customer.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,75 @@
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 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
    })
}
src/api/basicData/customerFile.js
@@ -1,57 +1,5 @@
// å®¢æˆ·æ¡£æ¡ˆé¡µé¢æŽ¥å£
import request from '@/utils/request'
// åˆ†é¡µæŸ¥è¯¢
export function listCustomer(query) {
    return request({
        url: '/basic/customer/list',
        method: 'get',
        params: query
    })
}
// æŸ¥è¯¢å®¢æˆ·æ¡£æ¡ˆè¯¦ç»†
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
    })
}
// æ–°å¢žå®¢æˆ·è·Ÿè¿›
export function addCustomerFollow(data) {
    return request({
        url: '/basic/customer-follow/add',
@@ -60,7 +8,6 @@
    })
}
// ä¿®æ”¹å®¢æˆ·è·Ÿè¿›
export function updateCustomerFollow(data) {
  return request({
    url: '/basic/customer-follow/edit',
@@ -68,7 +15,7 @@
    data: data,
  })
}
// åˆ é™¤å®¢æˆ·è·Ÿè¿›
export function delCustomerFollow(id) {
    return request({
        url: '/basic/customer-follow/'+id,
@@ -76,7 +23,6 @@
    })
}
// å›žè®¿æé†’-新增/更新
export function addReturnVisit(data) {
    return request({
        url: '/basic/customer-follow/return-visit',
@@ -84,7 +30,7 @@
        data: data
    })
}
// èŽ·å–å›žè®¿æé†’è¯¦æƒ…
export function getReturnVisit(id) {
    return request({
        url: '/basic/customer-follow/return-visit/' + id,
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",
  });
}
src/api/basicData/productModel.js
@@ -7,3 +7,11 @@
        params: query
    })
}
export function productModelListByUrl(url, query) {
    return request({
        url,
        method: 'get',
        params: query
    })
}
src/api/basicData/productProcess.js
@@ -1,10 +1,10 @@
import request from '@/utils/request'
import request from "@/utils/request";
// å·¥åºåˆ—表分页查询
export function productProcessListPage(query) {
  return request({
    url: '/productProcess/listPage',
    method: 'get',
    params: query
  })
    url: "/technologyOperation/listPage",
    method: "get",
    params: query,
  });
}
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
    })
}
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,
    })
}
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,
  });
};
src/api/inventoryManagement/stockInRecord.js
@@ -25,3 +25,20 @@
        data: ids,
    });
};
export const batchDeletePendingStockInRecords = (ids) => {
    return request({
        url: "/stockInRecord/pending",
        method: "delete",
        data: ids,
    });
};
// æ‰¹é‡å®¡æ‰¹å…¥åº“记录(approvalStatus: approved/rejected)
export const batchApproveStockInRecords = (data) => {
    return request({
        url: "/stockInRecord/approve",
        method: "post",
        data,
    });
};
src/api/inventoryManagement/stockInventory.js
@@ -8,6 +8,15 @@
    });
};
// åˆ†é¡µæŸ¥è¯¢è”合库存记录列表(包含商品信息)
export const getStockInventoryListPageCombined = (params) => {
    return request({
        url: "/stockInventory/pageListCombinedStockInventory",
        method: "get",
        params,
    });
};
// åˆ›å»ºåº“存记录
export const createStockInventory = (params) => {
    return request({
@@ -21,6 +30,24 @@
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,
    });
@@ -60,3 +87,11 @@
    });
};
export const getStockInventoryByModelId = (productModelId) => {
    return request({
        url: "/stockInventory/getByModelId",
        method: "get",
        params: { productModelId },
    });
};
src/api/inventoryManagement/stockOut.js
@@ -17,3 +17,21 @@
        data: ids,
    });
}
//删除待审批出库信息
export const delPendingStockOut = (ids) => {
    return request({
        url: "/stockOutRecord/pending",
        method: "delete",
        data: ids,
    });
}
// æ‰¹é‡å®¡æ‰¹å‡ºåº“记录(approvalStatus: approved/rejected)
export const batchApproveStockOutRecords = (data) => {
    return request({
        url: "/stockOutRecord/approve",
        method: "post",
        data,
    });
}
src/api/inventoryManagement/stockUninventory.js
@@ -26,6 +26,24 @@
    });
};
// æ–°å¢žå…¥åº“记录(仅创建记录,不调整库存)
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({
src/api/personnelManagement/class.js
@@ -71,6 +71,7 @@
    url: "/personalShift/export",
    method: "get",
    params: query,
    responseType: "blob",
  });
}
src/api/procurementManagement/procurementInvoiceLedger.js
@@ -75,14 +75,6 @@
  });
}
export function productUploadFile(data) {
  return request({
    url: "/file/uploadFile",
    method: "post",
    data: data,
  });
}
// export function getProductRecordById(params) {
//   return request({
//     url: "/purchase/registration/getProductRecordById",
src/api/procurementManagement/purchase_return_order.js
@@ -18,3 +18,21 @@
        data
    });
}
// æŸ¥çœ‹è¯¦æƒ…
// 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",
    });
}
src/api/productionManagement/processRoute.js
@@ -4,7 +4,7 @@
// åˆ†é¡µæŸ¥è¯¢
export function listPage(query) {
  return request({
    url: "/processRoute/page",
    url: "/technologyRouting/page",
    method: "get",
    params: query,
  });
@@ -12,31 +12,38 @@
export function add(data) {
  return request({
    url: "/processRoute",
    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: '/processRoute/' + ids,
    method: 'delete',
  })
    url: "/technologyRouting/delete",
    method: "delete",
    data: ids,
  });
}
export function update(data) {
  return request({
    url: '/processRoute',
    method: 'put',
    url: "/technologyRouting/editTechRoute",
    method: "put",
    data: data,
  })
  });
}
// èŽ·å–è¯¦æƒ…
export function getById(id) {
  return request({
    url: `/processRoute/${id}`,
    method: 'get',
  })
    method: "get",
  });
}
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,
  });
}
src/api/productionManagement/processRouteItem.js
@@ -4,7 +4,7 @@
// åˆ—表查询
export function findProcessRouteItemList(query) {
  return request({
    url: "/processRouteItem/list",
    url: "/technologyRoutingOperation/list",
    method: "get",
    params: query,
  });
@@ -12,8 +12,15 @@
export function addOrUpdateProcessRouteItem(data) {
  return request({
    url: "/processRouteItem",
    url: "/technologyRoutingOperation/add",
    method: "post",
    data: data,
  });
}
export function addOrUpdateProcessRouteItem1(data) {
  return request({
    url: "/technologyRoutingOperation",
    method: "put",
    data: data,
  });
}
@@ -21,7 +28,7 @@
// æŽ’序接口
export function sortProcessRouteItem(data) {
  return request({
    url: "/processRouteItem/sort",
    url: "/technologyRoutingOperation/sort",
    method: "post",
    data: data,
  });
@@ -32,7 +39,54 @@
  // å°†id数组转换为逗号分隔的字符串,拼接到URL后面
  const idsStr = Array.isArray(ids) ? ids.join(",") : ids;
  return request({
    url: `/processRouteItem/batchDelete/${idsStr}`,
    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,
  });
}
src/api/productionManagement/productBom.js
@@ -4,7 +4,7 @@
// åˆ†é¡µæŸ¥è¯¢
export function listPage(query) {
  return request({
    url: "/productBom/listPage",
    url: "/technologyBom/listPage",
    method: "get",
    params: query,
  });
@@ -13,16 +13,24 @@
// æ–°å¢ž
export function add(data) {
  return request({
    url: "/productBom/add",
    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: "/productBom/update",
    url: "/technologyBom/update",
    method: "put",
    data: data,
  });
@@ -31,7 +39,7 @@
// æ‰¹é‡åˆ é™¤
export function batchDelete(ids) {
  return request({
    url: "/productBom/batchDelete",
    url: "/technologyBom/batchDelete",
    method: "delete",
    data: ids,
  });
@@ -40,7 +48,7 @@
// æ ¹æ®äº§å“åž‹å·ID查询BOM
export function getByModel(productModelId) {
  return request({
    url: "/productBom/getByModel",
    url: "/technologyBom/getByModel",
    method: "get",
    params: { productModelId },
  });
@@ -49,7 +57,7 @@
// å¯¼å‡ºBOM
export function exportBom(bomId) {
  return request({
    url: "/productBom/exportBom",
    url: "/technologyBom/exportBom",
    method: "post",
    params: { bomId },
    responseType: "blob",
@@ -59,7 +67,7 @@
//  ä¸‹è½½æ¨¡æ¿
export function downloadTemplate() {
  return request({
    url: "/productBom/downloadTemplate",
    url: "/technologyBom/downloadTemplate",
    method: "get",
    responseType: "blob",
  });
src/api/productionManagement/productProcessRoute.js
@@ -4,7 +4,7 @@
// åˆ—表查询
export function findProductProcessRouteItemList(query) {
  return request({
    url: "/productProcessRoute/list",
    url: "/productionOrderRouting/list",
    method: "get",
    params: query,
  });
@@ -12,7 +12,7 @@
export function addOrUpdateProductProcessRouteItem(data) {
  return request({
    url: "/productProcessRoute/updateRouteItem",
    url: "/productionOrderRouting/updateRouteItem",
    method: "post",
    data: data,
  });
@@ -21,7 +21,7 @@
// ç”Ÿäº§è®¢å•下:新增工艺路线项目
export function addRouteItem(data) {
  return request({
    url: "/productProcessRoute/addRouteItem",
    url: "/productionOrderRouting/addRouteItem",
    method: "post",
    data,
  });
@@ -30,7 +30,7 @@
// èŽ·å–ç”Ÿäº§è®¢å•å…³è”çš„å·¥è‰ºè·¯çº¿ä¸»ä¿¡æ¯
export function listMain(orderId) {
  return request({
    url: "/productProcessRoute/listMain",
    url: "/productionOrderRouting/listMain",
    method: "get",
    params: { orderId },
  });
@@ -39,7 +39,7 @@
// åˆ é™¤å·¥è‰ºè·¯çº¿é¡¹ç›®ï¼ˆè·¯ç”±åŽæ‹¼æŽ¥ id)
export function deleteRouteItem(id) {
  return request({
    url: `/productProcessRoute/deleteRouteItem/${id}`,
    url: `/productionOrderRouting/deleteRouteItem/${id}`,
    method: "delete",
  });
}
@@ -47,8 +47,39 @@
// ç”Ÿäº§è®¢å•下:排序工艺路线项目
export function sortRouteItem(data) {
  return request({
    url: "/productProcessRoute/sortRouteItem",
    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",
  });
}
src/api/productionManagement/productStructure.js
@@ -4,14 +4,43 @@
// åˆ†é¡µæŸ¥è¯¢
export function queryList(id) {
  return request({
    url: "/productStructure/listBybomId/" + id,
    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",
    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,
  });
src/api/productionManagement/productionCosting.js
@@ -14,7 +14,7 @@
// salesLedger/productionAccounting/page
export function salesLedgerProductionAccountingList(query) {
  return request({
    url: "/salesLedger/productionAccounting/page",
    url: "/productionAccount/listPage",
    method: "get",
    params: query,
  });
@@ -24,7 +24,7 @@
//
export function salesLedgerProductionAccountingListProductionDetails(query) {
  return request({
    url: "/salesLedger/productionAccounting/listProductionDetails",
    url: "/productionAccount/listProductionDetails",
    method: "get",
    params: query,
  });
src/api/productionManagement/productionOrder.js
@@ -12,7 +12,7 @@
export function productOrderListPage(query) {
  return request({
    url: "/productOrder/page",
    url: "/productionOrder/page",
    method: "get",
    params: query,
  });
@@ -30,7 +30,7 @@
// ç”Ÿäº§è®¢å•-绑定工艺路线
export function bindingRoute(data) {
  return request({
    url: "/productOrder/bindingRoute",
    url: "/productionOrder/bindingRoute",
    method: "post",
    data,
  });
@@ -39,7 +39,16 @@
// ç”Ÿäº§è®¢å•-新增
export function addProductOrder(data) {
  return request({
    url: "/productOrder/addProductOrder",
    url: "/productionOrder/addOrder",
    method: "post",
    data: data,
  });
}
// ç”Ÿäº§è®¢å•-修改
export function updateProductOrder(data) {
  return request({
    url: "/productionOrder/updateOrder",
    method: "post",
    data: data,
  });
@@ -47,8 +56,9 @@
export function delProductOrder(ids) {
  return request({
    url: `/productOrder/${ids}`,
    url: `/productionOrder/delete`,
    method: "delete",
    data: ids,
  });
}
@@ -61,6 +71,92 @@
  });
}
// ç”Ÿäº§è®¢å•-领料台账列表
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,
  });
}
// èŽ·å–ç‚’æœºæ­£åœ¨å·¥ä½œé‡æ•°æ®
export function schedulingList(query) {
  return request({
src/api/productionManagement/productionProcess.js
@@ -4,7 +4,7 @@
// åˆ†é¡µæŸ¥è¯¢
export function listPage(query) {
  return request({
    url: "/productProcess/listPage",
    url: "/technologyOperation/listPage",
    method: "get",
    params: query,
  });
@@ -12,15 +12,23 @@
export function processList(query) {
  return request({
    url: "/productProcess/list",
    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: "/productProcess",
    url: "/technologyOperation/add",
    method: "post",
    data: data,
  });
@@ -28,32 +36,24 @@
export function del(data) {
  return request({
    url: '/productProcess/batchDelete',
    method: 'delete',
    url: "/technologyOperation/batchDelete",
    method: "delete",
    data: data,
  })
  });
}
export function update(data) {
  return request({
    url: '/productProcess/update',
    method: 'put',
    url: "/technologyOperation/update",
    method: "put",
    data: data,
  })
}
// å·¥åºæŸ¥è¯¢
export function list() {
    return request({
        url: "/productProcess/list",
        method: "get",
    });
}
// å¯¼å…¥æ•°æ®
export function importData(data) {
  return request({
    url: "/productProcess/importData",
    url: "/technologyOperation/importData",
    method: "post",
    data: data,
  });
@@ -62,8 +62,43 @@
// ä¸‹è½½æ¨¡æ¿
export function downloadTemplate() {
  return request({
    url: "/productProcess/downloadTemplate",
    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",
  });
}
src/api/productionManagement/workOrder.js
@@ -2,7 +2,7 @@
export function productWorkOrderPage(query) {
  return request({
    url: "/productWorkOrder/page",
    url: "/productionOperationTask/page",
    method: "get",
    params: query,
  });
@@ -10,7 +10,7 @@
export function updateProductWorkOrder(data) {
  return request({
    url: "/productWorkOrder/updateProductWorkOrder",
    url: "/productionOperationTask/updateProductWorkOrder",
    method: "post",
    data: data,
  });
@@ -24,12 +24,74 @@
  });
}
export function assignProductWorkOrder(data) {
  return request({
    url: "/productionOperationTask/assign",
    method: "post",
    data: data,
  });
}
// ä¸‹è½½å·¥å•流转卡(返回文件流)
export function downProductWorkOrder(id) {
  return request({
    url: "/productWorkOrder/down",
    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,
  });
}
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,
  });
}
src/api/salesManagement/deliveryLedger.js
@@ -11,6 +11,13 @@
}
// ä¿®æ”¹å‘货台账
export function getDeliveryDetail(id) {
  return request({
    url: `/shippingInfo/getDateil/${id}`,
    method: "get",
  });
}
export function addOrUpdateDeliveryLedger(query) {
  return request({
    url: "/shippingInfo/update",
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
  });
}
src/assets/styles/element-ui.scss
@@ -52,44 +52,51 @@
  left: 0;
  position: relative;
  margin: 0 auto;
  border-radius: 8px;
  border-radius: 24px;
  padding: 0 !important;
  border: 1px solid var(--surface-border);
  box-shadow: var(--shadow-md);
  background: rgba(255, 255, 255, 0.96);
}
.el-dialog__header {
  background: #f5f6f7;
  padding: 12px 16px;
  border-radius: 8px 8px 0 0;
  background: linear-gradient(180deg, rgba(247, 250, 248, 0.98), rgba(242, 247, 244, 0.88));
  padding: 18px 24px 14px;
  border-bottom: 1px solid var(--surface-border);
  border-radius: 24px 24px 0 0;
}
.el-dialog__title {
  font-weight: 400;
  font-size: 16px;
  color: #2e3033;
  font-weight: 600;
  font-size: 17px;
  color: var(--text-primary);
}
.el-dialog__body {
  padding: 16px 40px 0 40px;
  padding: 24px 24px 0;
  max-height: 74vh;
  overflow-y: auto;
}
.el-dialog__footer {
  text-align: center;
  padding: 16px;
  padding: 18px 24px 24px;
}
.el-message-box {
  padding: 0 !important;
  border-radius: 8px;
  border-radius: 22px;
  border: 1px solid var(--surface-border);
  box-shadow: var(--shadow-md);
}
.el-message-box__header {
  background: #f5f6f7;
  padding: 12px 16px;
  border-radius: 8px 8px 0 0;
  background: linear-gradient(180deg, rgba(247, 250, 248, 0.98), rgba(242, 247, 244, 0.88));
  padding: 18px 24px 14px;
  border-bottom: 1px solid var(--surface-border);
  border-radius: 22px 22px 0 0;
}
.el-message-box__title {
  font-weight: 400;
  font-size: 16px;
  color: #2e3033;
  font-weight: 600;
  font-size: 17px;
  color: var(--text-primary);
}
.el-message-box__content {
  padding: 16px 40px 0 40px;
  padding: 24px 24px 0;
}
.el-message-box__container {
  justify-content: center;
@@ -108,7 +115,7 @@
.el-table__expanded-cell {
  padding: 0 !important;
  .el-table__header-wrapper {
    background-color: #f5f8ff !important;
    background-color: var(--surface-soft) !important;
  }
}
@@ -152,3 +159,83 @@
.el-dropdown .el-dropdown-link {
  color: var(--el-color-primary) !important;
}
.el-button {
  border-radius: 12px;
  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: 12px;
  box-shadow: 0 0 0 1px rgba(216, 225, 219, 0.92) inset !important;
  background: rgba(255, 255, 255, 0.9);
}
.el-input__wrapper.is-focus,
.el-select__wrapper.is-focused,
.el-textarea__inner:focus {
  box-shadow: 0 0 0 1px rgba(0, 47, 167, 0.28) inset !important;
}
.el-card {
  border: 1px solid var(--surface-border);
  box-shadow: var(--shadow-sm);
  background: rgba(255, 255, 255, 0.88);
}
.el-table {
  --el-table-border-color: var(--surface-border);
  --el-table-header-bg-color: var(--surface-soft);
  --el-table-row-hover-bg-color: #f1f6f4;
  --el-table-current-row-bg-color: #e9f0ed;
  border-radius: 18px;
}
.el-table th.el-table__cell {
  background: var(--surface-soft) !important;
  color: var(--text-secondary);
  font-weight: 600;
}
.el-table tr,
.el-table td.el-table__cell,
.el-table__body tr > td.el-table__cell {
  background: var(--surface-base) !important;
}
.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;
}
src/assets/styles/index.scss
@@ -12,11 +12,16 @@
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
  font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
  background:
    radial-gradient(circle at top left, rgba(214, 226, 219, 0.8), transparent 28%),
    linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
  color: var(--text-primary);
}
label {
  font-weight: 700;
  font-weight: 600;
  color: var(--text-secondary);
}
html {
@@ -26,6 +31,12 @@
#app {
  height: 100%;
}
html,
body,
#app {
  background-color: var(--app-bg);
}
*,
@@ -123,7 +134,7 @@
//main-container全局样式
.app-container {
  padding: 20px;
  padding: 20px 24px 24px;
}
.search_form {
  display: flex;
@@ -131,15 +142,17 @@
  justify-content: space-between;
  .search_title {
    font-size: 14px;
    font-weight: 700;
    color: #333333;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--text-secondary);
  }
}
.table_list {
  height: calc(100vh - 11em);
  margin-top: 20px;
  background: #fff;
  padding: 18px
  background: rgba(255, 255, 255, 0.88);
  border: 1px solid var(--surface-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-sm);
  padding: 18px;
}
.components-container {
  margin: 30px 50px;
@@ -176,11 +189,11 @@
.link-type,
.link-type:focus {
  color: #337ab7;
  color: var(--el-color-primary);
  cursor: pointer;
  &:hover {
    color: rgb(32, 160, 255);
    color: #165e57;
  }
}
@@ -193,3 +206,17 @@
    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);
  }
}
src/assets/styles/sidebar.scss
@@ -4,7 +4,7 @@
    transition: margin-left 0.28s;
    margin-left: $base-sidebar-width;
    position: relative;
    background: #f5f7fb;
    background: transparent;
  }
  .sidebarHide {
@@ -22,8 +22,9 @@
    left: 0;
    z-index: 1001;
    overflow: hidden;
    -webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
    box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
    padding: 12px 0 16px 16px;
    background: transparent;
    box-shadow: none;
    // reset element-ui css
    .horizontal-collapse-transition {
@@ -45,7 +46,8 @@
    &.has-logo {
      .el-scrollbar {
        height: calc(100% - 50px);
        height: calc(100% - 72px);
        margin-top: 10px;
      }
    }
@@ -67,6 +69,11 @@
      border: none;
      height: 100%;
      width: 100% !important;
      padding: 10px 8px 18px;
      border-radius: 22px;
      background: var(--menu-surface);
      backdrop-filter: blur(18px);
      box-shadow: var(--shadow-sm);
    }
    .el-menu-item,
@@ -81,25 +88,32 @@
    }
    // menu hover
    .sub-menu-title-noDropdown,
    .submenu-title-noDropdown,
    .el-sub-menu__title {
      &:hover {
        background-color: rgba(212, 221, 255, 0.8) !important;
        background-color: var(--menu-hover) !important;
        border-radius: 14px;
      }
    }
    & .theme-light .is-active > .el-sub-menu__title {
      color: #fff !important;
      color: var(--current-color) !important;
    }
    & .nest-menu .el-sub-menu > .el-sub-menu__title,
    & .el-sub-menu .el-menu-item {
      min-width: $base-sidebar-width !important;
      min-width: 0 !important;
      margin: 0 12px 6px;
      width: calc(100% - 24px);
      padding-left: 8px !important;
      padding-right: 8px !important;
      box-sizing: border-box;
      &:hover {
        background-color: rgba(212, 221, 255, 0.8) !important;
        background-color: var(--menu-hover) !important;
      }
      &.is-active {
        background-color: #fff !important;
        background-color: var(--menu-active-bg) !important;
        border-radius: 14px;
      }
    }
@@ -108,29 +122,57 @@
      //background-color: transparent;
      &:hover {
        background-color: rgba(212, 221, 255, 0.8) !important;
        background-color: var(--menu-hover) !important;
        border-radius: 14px;
      }
    }
  }
  .hideSidebar {
    .sidebar-container {
      width: 54px !important;
      width: 68px !important;
      padding-left: 0;
      padding-right: 0;
    }
    .main-container {
      margin-left: 54px;
      margin-left: 84px;
    }
    .sub-menu-title-noDropdown {
    .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: 20px;
          margin-left: 0;
        }
      }
      .el-menu-tooltip__trigger {
        width: 100%;
        display: inline-flex !important;
        align-items: center;
        justify-content: center;
        .svg-icon {
          width: 18px;
          height: 18px;
          margin-right: 0;
          flex-shrink: 0;
        }
      }
    }
@@ -139,16 +181,45 @@
      & > .el-sub-menu__title {
        padding: 0 !important;
        display: flex !important;
        align-items: center;
        justify-content: center;
        .svg-icon {
          margin-left: 20px;
          margin-left: 0;
          margin-right: 0;
        }
      }
    }
    .el-menu--collapse {
      width: 100% !important;
      padding: 10px 6px 18px;
      > .el-menu-item,
      .el-sub-menu {
        & > .el-sub-menu__title {
        & > .el-sub-menu__title,
        &.el-menu-item {
          margin: 0 0 6px;
          width: 100%;
          padding-left: 0 !important;
          padding-right: 0 !important;
          box-sizing: border-box;
          display: flex !important;
          align-items: center;
          justify-content: center;
          .svg-icon {
            width: 18px;
            height: 18px;
            margin-right: 0;
            flex-shrink: 0;
          }
          &:hover {
            border-radius: 14px;
          }
          & > span {
            height: 0;
            width: 0;
@@ -210,12 +281,20 @@
  .nest-menu .el-sub-menu > .el-sub-menu__title,
  .el-menu-item {
    min-width: 0 !important;
    margin: 0 12px 6px;
    width: calc(100% - 24px);
    padding-left: 8px !important;
    padding-right: 8px !important;
    box-sizing: border-box;
    &:hover {
      // you can use $sub-menuHover
      background-color: rgba(212, 221, 255, 0.56) !important;
      background-color: var(--menu-hover) !important;
    }
    &.is-active {
      background-color: rgba(212, 221, 255, 0.56) !important;
      background-color: var(--menu-active-bg) !important;
      border-radius: 14px;
    }
  }
@@ -223,9 +302,13 @@
  > .el-menu--popup {
    max-height: 100vh;
    overflow-y: auto;
    padding: 8px;
    border-radius: 18px;
    border: 1px solid var(--surface-border);
    box-shadow: var(--shadow-md);
    &::-webkit-scrollbar-track-piece {
      background: #d3dce6;
      background: #dfe7e1;
    }
    &::-webkit-scrollbar {
@@ -233,7 +316,7 @@
    }
    &::-webkit-scrollbar-thumb {
      background: #99a9bf;
      background: #9aa79e;
      border-radius: 20px;
    }
  }
src/assets/styles/variables.module.scss
@@ -8,35 +8,35 @@
$yellow: #fec171;
$panGreen: #30b08f;
// é»˜è®¤ä¸»é¢˜å˜é‡
$menuText: #bfcbd9;
$menuActiveText: #409eff;
$menuBg: #304156;
$menuHover: #263445;
// menu palette
$menuText: #677287;
$menuActiveText: #1f7a72;
$menuBg: #f4f7f4;
$menuHover: #e7eeea;
// æµ…色主题theme-light
$menuLightBg: #002fa7;
$menuLightHover: #f0f1f5;
$menuLightText: #fff;
$menuLightActiveText: #002fa7;
// light theme
$menuLightBg: #f4f7f4;
$menuLightHover: #e7eeea;
$menuLightText: #3b4658;
$menuLightActiveText: #1f7a72;
// åŸºç¡€å˜é‡
$base-sidebar-width: 200px;
$sideBarWidth: 200px;
// layout
$base-sidebar-width: 216px;
$sideBarWidth: 216px;
// èœå•暗色变量
$base-menu-color: #bfcbd9;
$base-menu-color-active: #f4f4f5;
$base-menu-background: #304156;
$base-sub-menu-background: #1f2d3d;
$base-sub-menu-hover: #fff;
// sidebar
$base-menu-color: #677287;
$base-menu-color-active: #1f7a72;
$base-menu-background: #f4f7f4;
$base-sub-menu-background: #eef3ef;
$base-sub-menu-hover: #ffffff;
// ç»„件变量
$--color-primary: #409eff;
// component
$--color-primary: #1f7a72;
$--color-success: #67c23a;
$--color-warning: #e6a23c;
$--color-danger: #f56c6c;
$--color-info: #909399;
$--color-warning: #d89b41;
$--color-danger: #d25b52;
$--color-info: #7d8797;
:export {
  menuText: $menuText;
@@ -48,7 +48,6 @@
  menuLightText: $menuLightText;
  menuLightActiveText: $menuLightActiveText;
  sideBarWidth: $sideBarWidth;
  // å¯¼å‡ºåŸºç¡€é¢œè‰²
  blue: $blue;
  lightBlue: $light-blue;
  red: $red;
@@ -57,7 +56,6 @@
  tiffany: $tiffany;
  yellow: $yellow;
  panGreen: $panGreen;
  // å¯¼å‡ºç»„件颜色
  colorPrimary: $--color-primary;
  colorSuccess: $--color-success;
  colorWarning: $--color-warning;
@@ -65,23 +63,45 @@
  colorInfo: $--color-info;
}
// CSS变量定义
:root {
  /* äº®è‰²æ¨¡å¼å˜é‡ */
  --sidebar-bg: #{$menuBg};
  --sidebar-text: #{$menuText};
  --sidebar-muted: #93a0b1;
  --menu-hover: #{$menuHover};
  --menu-active-bg: #dfe9e4;
  --menu-surface: rgba(255, 255, 255, 0.72);
  --navbar-bg: #ffffff;
  --navbar-text: #303133;
  --app-bg: #eef2ee;
  --app-bg-accent: #dfe8e2;
  --surface-base: #ffffff;
  --surface-soft: #f7faf8;
  --surface-muted: #eff4f1;
  --surface-border: #d8e1db;
  --surface-border-strong: #c9d5ce;
  --text-primary: #21313f;
  --text-secondary: #5f6d7e;
  --text-tertiary: #8a98a8;
  --shadow-sm: 0 10px 30px rgba(31, 49, 38, 0.06);
  --shadow-md: 0 18px 50px rgba(31, 49, 38, 0.1);
  --radius-lg: 24px;
  --radius-md: 18px;
  --radius-sm: 12px;
  /* splitpanes default-theme å˜é‡ */
  --navbar-bg: rgba(255, 255, 255, 0.78);
  --navbar-text: #21313f;
  --navbar-hover: rgba(31, 122, 114, 0.08);
  --tags-bg: transparent;
  --tags-item-bg: rgba(255, 255, 255, 0.74);
  --tags-item-border: rgba(201, 213, 206, 0.88);
  --tags-item-text: #5f6d7e;
  --tags-item-hover: rgba(31, 122, 114, 0.08);
  --tags-close-hover: rgba(31, 122, 114, 0.18);
  --splitpanes-default-bg: #ffffff;
}
// æš—黑模式变量
html.dark {
  /* é»˜è®¤é€šç”¨ */
  --el-bg-color: #141414;
  --el-bg-color-overlay: #1d1e1f;
  --el-text-color-primary: #ffffff;
@@ -89,18 +109,15 @@
  --el-border-color: #434343;
  --el-border-color-light: #434343;
  /* ä¾§è¾¹æ  */
  --sidebar-bg: #141414;
  --sidebar-text: #ffffff;
  --menu-hover: #2d2d2d;
  --menu-active-text: #{$menuActiveText};
  /* é¡¶éƒ¨å¯¼èˆªæ  */
  --navbar-bg: #141414;
  --navbar-text: #ffffff;
  --navbar-hover: #141414;
  /* æ ‡ç­¾æ  */
  --tags-bg: #141414;
  --tags-item-bg: #1d1e1f;
  --tags-item-border: #303030;
@@ -108,36 +125,29 @@
  --tags-item-hover: #2d2d2d;
  --tags-close-hover: #64666a;
  /* splitpanes ç»„件暗黑模式变量 */
  --splitpanes-bg: #141414;
  --splitpanes-border: #303030;
  --splitpanes-splitter-bg: #1d1e1f;
  --splitpanes-splitter-hover-bg: #2d2d2d;
  /* blockquote æš—黑模式变量 */
  --blockquote-bg: #1d1e1f;
  --blockquote-border: #303030;
  --blockquote-text: #d0d0d0;
  /* Cron æ—¶é—´è¡¨è¾¾å¼ æ¨¡å¼å˜é‡ */
  --cron-border: #303030;
  /* splitpanes default-theme æš—黑模式变量 */
  --splitpanes-default-bg: #141414;
  /* ä¾§è¾¹æ èœå•覆盖 */
  .sidebar-container {
    .el-menu-item,
    .menu-title {
      color: var(--el-text-color-regular);
    }
    & .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;
    }
  }
  /* é¡¶éƒ¨æ æ èœå•覆盖 */
  .el-menu--horizontal {
    .el-menu-item {
      &:not(.is-disabled) {
@@ -149,7 +159,6 @@
    }
  }
  /* åˆ†å‰²çª—格覆盖 */
  .splitpanes {
    background-color: var(--splitpanes-bg);
@@ -173,7 +182,6 @@
    }
  }
  /* è¡¨æ ¼æ ·å¼è¦†ç›– */
  .el-table {
    --el-table-header-bg-color: var(--el-bg-color-overlay) !important;
    --el-table-header-text-color: var(--el-text-color-regular) !important;
@@ -189,7 +197,6 @@
    }
  }
  /* æ ‘组件高亮样式覆盖 */
  .el-tree {
    .el-tree-node.is-current > .el-tree-node__content {
      background-color: var(--el-bg-color-overlay) !important;
@@ -201,20 +208,17 @@
    }
  }
  /* ä¸‹æ‹‰èœå•样式覆盖 */
  .el-dropdown-menu__item:not(.is-disabled):focus,
  .el-dropdown-menu__item:not(.is-disabled):hover {
    background-color: var(--navbar-hover) !important;
  }
  /* blockquote样式覆盖 */
  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);
  }
src/components/AIChatSidebar/index.vue
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
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>
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>
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>
src/components/Breadcrumb/index.vue
@@ -88,11 +88,30 @@
.app-breadcrumb.el-breadcrumb {
  display: inline-block;
  font-size: 14px;
  line-height: 50px;
  line-height: 56px;
  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: #002FA7;
    color: var(--current-color);
    font-weight: 600;
    cursor: text;
  }
}
src/components/Dialog/FileList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,245 @@
<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="120"
                         align="center">
          <template #default="scope">
            <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>
</template>
<script setup>
  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";
  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 handleUpload = () => {
    uploadDialogVisible.value = true;
  };
  const saveUpload = async () => {
    // æ£€æŸ¥æ˜¯å¦æœ‰æ–°ä¸Šä¼ çš„æ–‡ä»¶
    if (newFileList.value.length > 0) {
      try {
        await createAttachment({
          application: "file",
          recordType: props.recordType,
          recordId: props.recordId,
          storageBlobDTOs: [...newFileList.value, ...tableData.value],
        });
        newFileList.value = [];
        // åˆ·æ–°åˆ—表
        setList();
      } catch (error) {
        proxy?.$modal?.msgError("上传失败");
      }
    }
    uploadDialogVisible.value = false;
  };
  const closeUpload = () => {
    newFileList.value = [];
    uploadDialogVisible.value = false;
  };
  const handleDelete = async (row, index) => {
    try {
      await deleteAttachment([row.storageAttachmentId]);
      proxy?.$modal?.msgSuccess("删除成功");
      setList();
    } catch (error) {
      proxy?.$modal?.msgError("删除失败");
    }
  };
  const setList = () => {
    attachmentList({
      recordType: props.recordType,
      recordId: props.recordId,
    }).then(res => {
      if (res && res.data) {
        tableData.value = 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>
src/components/Dialog/FileListDialog.vue
@@ -77,6 +77,7 @@
                @pagination="paginationSearch"
                @change="handleChange" />
  </el-dialog>
<!-- // todo é™„件预览相关 -->
  <filePreview v-if="showPreview"
               ref="filePreviewRef" />
</template>
src/components/Editor/index.vue
@@ -5,11 +5,11 @@
      :before-upload="handleBeforeUpload"
      :on-success="handleUploadSuccess"
      :on-error="handleUploadError"
      name="file"
      name="files"
      :show-file-list="false"
      :headers="headers"
      class="editor-img-uploader"
      v-if="type == 'url'"
      v-if="type === 'url'"
    >
      <i ref="uploadRef" class="editor-img-uploader"></i>
    </el-upload>
@@ -27,15 +27,15 @@
</template>
<script setup>
import axios from "axios";
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/upload"); // ä¸Šä¼ çš„图片服务器地址
const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/public/upload"); // ä¸Šä¼ çš„图片服务器地址
const headers = ref({
  Authorization: "Bearer " + getToken(),
});
@@ -157,21 +157,40 @@
function handleUploadSuccess(res, file) {
  // å¦‚果上传成功
  if (res.code == 200) {
    const imageUrl = resolveImageUrl(res);
    if (!imageUrl) {
      proxy.$modal.msgError("未获取到图片地址");
      return;
    }
    // èŽ·å–å¯Œæ–‡æœ¬å®žä¾‹
    let quill = toRaw(quillEditorRef.value).getQuill();
    // èŽ·å–å…‰æ ‡ä½ç½®
    let length = quill.selection.savedRange.index;
    const selection = quill.getSelection(true);
    const length = selection ? selection.index : quill.getLength();
    // æ’入图片,res.url为服务器返回的图片链接地址
    quill.insertEmbed(
      length,
      "image",
      import.meta.env.VITE_APP_BASE_API + res.fileName
    );
    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 "";
}
// ä¸Šä¼ å¤±è´¥å¤„理
@@ -196,17 +215,10 @@
function insertImage(file) {
  const formData = new FormData();
  formData.append("file", file);
  axios
    .post(uploadUrl.value, formData, {
      headers: {
        "Content-Type": "multipart/form-data",
        Authorization: headers.value.Authorization,
      },
  formData.append("files", file);
  uploadPublicFile(formData).then((res) => {
    handleUploadSuccess(res)
    })
    .then((res) => {
      handleUploadSuccess(res.data);
    });
}
</script>
src/components/ImagePreview/index.vue
ÎļþÒÑɾ³ý
src/components/ImageUpload/index.vue
ÎļþÒÑɾ³ý
src/components/PIMTable/PIMTable.vue
@@ -1,10 +1,9 @@
<template>
  <el-table
    ref="multipleTable"
  <el-table ref="multipleTable"
    v-loading="tableLoading"
    :border="border"
    :data="tableData"
    :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
            :header-cell-style="mergedHeaderCellStyle"
    :height="height"
    :highlight-current-row="highlightCurrentRow"
    :row-class-name="rowClassName"
@@ -19,18 +18,17 @@
    @current-change="currentChange"
    @selection-change="handleSelectionChange"
    @expand-change="expandChange"
    class="lims-table"
  >
    <el-table-column
      align="center"
            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"
                     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"
@@ -45,38 +43,34 @@
      :sortable="!!item.sortable"
      :type="item.type"
      :width="item.width"
    >
                     :minWidth="item.minWidth">
      <template #header="scope">
        <div class="pim-table-header-cell">
        <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 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"
      <template v-if="item.hasOwnProperty('colunmTemplate')"
                #[item.colunmTemplate]="scope">
        <slot v-if="item.theadSlot"
          :name="item.theadSlot"
          :index="scope.$index"
          :row="scope.row"
        />
              :row="scope.row" />
      </template>
      <template #default="scope">
        <!-- æ’æ§½ -->
        <div v-if="item.dataType == 'slot'">
          <slot
            v-if="item.slot"
          <slot v-if="item.slot"
            :index="scope.$index"
            :name="item.slot"
            :row="scope.row"
          />
                :row="scope.row" />
        </div>
        <!-- è¿›åº¦æ¡ -->
        <div v-else-if="item.dataType == 'progress'">
@@ -84,28 +78,21 @@
        </div>
        <!-- å›¾ç‰‡ -->
        <div v-else-if="item.dataType == 'image'">
          <img
            :src="javaApi + '/img/' + scope.row[item.prop]"
          <img :src="javaApi + '/img/' + scope.row[item.prop]"
            alt=""
            style="width: 40px; height: 40px; margin-top: 10px"
          />
               style="width: 40px; height: 40px; margin-top: 10px" />
        </div>
        <!-- tag -->
        <div v-else-if="item.dataType == 'tag'">
          <el-tag
            v-if="
          <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)"
          >
                  :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(
          <el-tag v-for="(tag, index) in dataTypeFn(
              scope.row[item.prop],
              item.formatData
            )"
@@ -115,43 +102,35 @@
            "
            :key="index"
            :title="formatters(scope.row[item.prop], item.formatData)"
            :type="formatType(tag, item.formatType)"
          >
                  :type="formatType(tag, item.formatType)">
            {{ item.tagGroup ? tag[item.tagGroup.label] ?? tag : tag }}
          </el-tag>
          <el-tag
            v-else
          <el-tag v-else
            :title="formatters(scope.row[item.prop], item.formatData)"
            :type="formatType(scope.row[item.prop], item.formatType)"
          >
                  :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'"
        <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="o.disabled ? o.disabled(scope.row) : false"
                       :disabled="isOperationDisabled(o, scope.row)"
              :plain="o.plain"
              type="primary"
              :style="{
                color:
                  o.name === '删除' || o.name === 'delete'
                    ? '#f56c6c'
                    : o.color,
                color: getOperationColor(o, scope.row),
                fontWeight: 'bold',
              }"
              link
              @click.stop="o.clickFun(scope.row)"
              :key="key"
            >
                       :key="key">
              {{ o.name }}
            </el-button>
            <el-upload
              :action="
            <el-upload :action="
                javaApi +
                o.url +
                '?id=' +
@@ -160,7 +139,7 @@
              ref="uploadRef"
              :multiple="o.multiple ? o.multiple : false"
              :limit="1"
              :disabled="o.disabled ? o.disabled(scope.row) : false"
                       :disabled="isOperationDisabled(o, scope.row)"
              :accept="
                o.accept
                  ? o.accept
@@ -183,28 +162,27 @@
                  handleSuccessUp(response, file, fileList, scope.$index)
              "
              :on-exceed="onExceed"
              :show-file-list="false"
            >
              <el-button
                link
                       :show-file-list="false">
              <el-button link
                type="primary"
                :disabled="o.disabled ? o.disabled(scope.row) : false"
                >{{ o.name }}</el-button
              >
                         :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'"
        <div v-else-if="item.dataType == 'link'"
          class="cell link"
          style="width: 100%"
          @click="goLink(scope.row, item.linkMethod)"
        >
             @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%">
        <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)
@@ -213,19 +191,17 @@
      </template>
    </el-table-column>
  </el-table>
  <pagination
        v-if="isShowPagination"
  <pagination v-if="isShowPagination"
    :total="page.total"
    :layout="page.layout"
    :page="page.current"
    :limit="page.size"
    @pagination="paginationSearch"
  />
              @pagination="paginationSearch" />
</template>
<script setup>
import pagination from "./Pagination.vue";
import { ref, inject, getCurrentInstance } from "vue";
  import { computed, ref, inject, getCurrentInstance } from "vue";
import { ElMessage } from "element-plus";
// èŽ·å–å…¨å±€çš„ uploadHeader
@@ -233,7 +209,12 @@
const uploadHeader = proxy.uploadHeader;
const javaApi = proxy.javaApi;
const emit = defineEmits(["pagination", "expand-change", "selection-change", "row-click"]);
  const emit = defineEmits([
    "pagination",
    "expand-change",
    "selection-change",
    "row-click",
  ]);
// Filters
const typeFn = (val, row) => {
@@ -278,6 +259,10 @@
    type: Boolean,
    default: false,
  },
    selectable: {
      type: Function,
      default: () => true,
    },
    isShowPagination: {
    type: Boolean,
    default: true,
@@ -312,7 +297,7 @@
  },
  rowKey: {
    type: String,
    default: 'id',
      default: "id",
  },
  page: {
    type: Object,
@@ -333,12 +318,19 @@
  },
});
  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) => {
  const indexMethod = index => {
  return (props.page.current - 1) * props.page.size + index + 1;
};
@@ -356,7 +348,7 @@
};
// èŽ·å–çˆ¶ç»„ä»¶æ–¹æ³•ï¼ˆç¤ºä¾‹å®žçŽ°ï¼‰
const getParentMethod = (methodName) => {
  const getParentMethod = methodName => {
  const parentMethods = inject("parentMethods", {});
  return parentMethods[methodName];
};
@@ -366,11 +358,80 @@
    return format(val);
  } else return val;
};
  const validTagTypes = ["primary", "success", "info", "warning", "danger"];
const formatType = (val, format) => {
  if (typeof format === "function") {
    return format(val);
  } else return "";
    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;
};
// æ–‡ä»¶å˜åŒ–处理
@@ -406,7 +467,7 @@
  }
};
const resetUploadComponent = (index) => {
  const resetUploadComponent = index => {
  uploadKeys[index] = Date.now();
};
@@ -427,7 +488,7 @@
  emit("pagination", { page: page, limit: limit });
};
const rowClick = (row) => {
  const rowClick = row => {
  emit("row-click", row);
};
@@ -435,12 +496,18 @@
  emit("expand-change", row, expandedRows);
};
const handleSelectionChange = (newSelection) => {
  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;
@@ -453,4 +520,8 @@
.pim-table-header-extra :deep(.el-select) {
  width: 100%;
}
  .pim-table-header-title {
    font-weight: 600;
  }
</style>
src/components/PIMTable/Pagination.vue
@@ -91,7 +91,6 @@
<style scoped>
.pagination-container {
  background: #fff;
  padding: 16px 0;
  margin-top: 0;
}
.pagination-container.hidden {
src/components/PageHeader/index.vue
@@ -43,6 +43,11 @@
<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) {
@@ -50,4 +55,9 @@
  align-items: center;
  gap: 8px;
}
.page-header-wrapper :deep(.el-page-header__content) {
  font-weight: 600;
  color: var(--text-primary);
}
</style>
src/components/ProcessParamListDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,642 @@
<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"
                        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"
                    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 editParamRules = ref({
    // standardValue: [{ required: true, message: "请输入标准值", 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>
src/components/ProjectManagement/ProgressReportDialog.vue
@@ -116,25 +116,14 @@
      </el-row>
      <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>
        <FileUpload v-model:file-list="form.storageBlobDTOs" />
      </el-form-item>
    </el-form>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="visible = false">取消</el-button>
        <el-button type="primary" @click="submit">确定</el-button>
        <el-button @click="visible = false">取消</el-button>
      </div>
    </template>
  </el-dialog>
@@ -144,6 +133,7 @@
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 },
@@ -161,11 +151,6 @@
})
const formRef = ref()
const fileList = ref([])
const upload = reactive({
  url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
  headers: { Authorization: 'Bearer ' + getToken() }
})
const form = ref({
  planNodeId: undefined,
@@ -180,7 +165,7 @@
  managerName: '',
  departmentName: '',
  remark: '',
  attachmentIds: []
  storageBlobDTOs: []
})
const rules = {
@@ -217,9 +202,8 @@
    managerName: info.managerName || '',
    departmentName: info.departmentName || '',
    remark: '',
    attachmentIds: []
    storageBlobDTOs: []
  }
  fileList.value = []
}
watch(
@@ -237,30 +221,6 @@
  form.value.completionProgress = 100
  form.value.totalProgress = 100
  if (!form.value.actualEndTime) form.value.actualEndTime = form.value.reportDate || ''
}
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() {
src/components/PurchaseAIChatSidebar/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
<template>
  <AIChatSidebar :assistants="assistants" default-assistant="purchase" />
</template>
<script setup>
import { ShoppingCart } from '@element-plus/icons-vue'
import AIChatSidebar from '@/components/AIChatSidebar/index.vue'
const assistants = [
  {
    key: 'purchase',
    label: '采购助理',
    title: '采购智能助理',
    tooltip: '采购智能助理',
    icon: ShoppingCart,
    apiBase: '/purchase-ai',
    storageKey: 'purchase_ai_chat_uuid',
    placeholder: '请输入采购问题... (Enter å‘送, Shift+Enter æ¢è¡Œ)',
    welcomeMessage: '你好',
    allowFileUpload: true,
    allowMultipleFileUpload: true,
    fileAnalyzeUrl: '/purchase-ai/analyze-files',
    emptySessionText: '暂无采购会话'
  }
]
</script>
src/components/SvgIcon/index.vue
@@ -9,7 +9,7 @@
  props: {
    iconClass: {
      type: String,
      required: true
      default: ''
    },
    className: {
      type: String,
src/layout/components/AppMain.vue
@@ -43,16 +43,17 @@
  width: 100%;
  position: relative;
  overflow: hidden;
  background: #F5F7FB;
  background: transparent;
}
.route-view-wrapper {
  width: 100%;
  height: 100%;
  padding: 120px 16px 24px 0;
}
.fixed-header + .app-main {
  padding-top: 50px;
  padding-top: 0;
}
.hasTagsView {
@@ -62,7 +63,7 @@
  }
  .fixed-header + .app-main {
    padding-top: 84px;
    padding-top: 0;
  }
}
</style>
@@ -81,11 +82,11 @@
}
::-webkit-scrollbar-track {
  background-color: #f1f1f1;
  background-color: rgba(218, 225, 220, 0.8);
}
::-webkit-scrollbar-thumb {
  background-color: #c0c0c0;
  background-color: #b2bdb5;
  border-radius: 3px;
}
</style>
src/layout/components/Navbar.vue
@@ -159,14 +159,18 @@
<style lang='scss' scoped>
.navbar {
  height: 50px;
  height: 56px;
  overflow: hidden;
  position: relative;
  background: var(--navbar-bg);
  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
  border: 1px solid rgba(216, 225, 219, 0.9);
  border-radius: 22px;
  backdrop-filter: blur(18px);
  box-shadow: var(--shadow-sm);
  padding: 0 18px;
  .hamburger-container {
    line-height: 46px;
    line-height: 52px;
    height: 100%;
    float: left;
    cursor: pointer;
@@ -174,7 +178,7 @@
    -webkit-tap-highlight-color: transparent;
    &:hover {
      background: rgba(0, 0, 0, 0.025);
      background: var(--navbar-hover);
    }
  }
@@ -195,7 +199,7 @@
  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;
    align-items: center;
    display: flex;
    &:focus {
@@ -203,19 +207,21 @@
    }
    .right-menu-item {
      display: inline-block;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 0 8px;
      height: 100%;
      font-size: 18px;
      color: var(--navbar-text);
      vertical-align: text-bottom;
      border-radius: 14px;
      &.hover-effect {
        cursor: pointer;
        transition: background 0.3s;
        &:hover {
          background: rgba(0, 0, 0, 0.025);
          background: var(--navbar-hover);
        }
      }
@@ -234,7 +240,7 @@
    }
    .notification-container {
      margin-right: 20px;
      margin-right: 12px;
      display: flex;
      align-items: center;
      cursor: pointer;
@@ -247,24 +253,39 @@
    }
    .avatar-container {
      margin-right: 40px;
      margin-right: 4px;
      height: 100%;
      display: flex;
      align-items: center;
      :deep(.el-dropdown) {
        height: 100%;
        display: flex;
        align-items: center;
      }
      .avatar-wrapper {
        margin-top: 5px;
        position: relative;
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 10px;
        padding: 6px 10px 6px 6px;
        height: 44px;
        border-radius: 999px;
        background: rgba(247, 250, 248, 0.92);
        border: 1px solid var(--surface-border);
        .user-avatar {
          cursor: pointer;
          width: 40px;
          height: 40px;
          width: 34px;
          height: 34px;
          border-radius: 50px;
        }
        i {
          cursor: pointer;
          position: absolute;
          right: -20px;
          top: 14px;
          position: static;
          font-size: 12px;
        }
      }
@@ -277,6 +298,9 @@
<style lang="scss">
.notification-popover {
  padding: 0 !important;
  border-radius: 20px !important;
  border: 1px solid var(--surface-border) !important;
  box-shadow: var(--shadow-md) !important;
  
  .el-popover__title {
    display: none;
src/layout/components/Sidebar/Logo.vue
@@ -1,13 +1,12 @@
<template>
  <div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
  <div class="sidebar-logo-container" :class="{ collapse }">
    <transition name="sidebarLogoFade">
      <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
        <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="公司Logo" />
        <h1 class="sidebar-title">{{ title }}</h1>
        <img :src="faviconUrl" class="sidebar-logo sidebar-favicon" alt="站点图标" />
      </router-link>
      <router-link v-else key="expand" class="sidebar-logo-link" to="/">
        <img v-if="logoUrl" :src="logoUrl" class="sidebar-logo" @error="handleImageError" alt="公司Logo" />
        <h1 class="sidebar-title">{{ title }}</h1>
        <h1 v-if="!logoUrl" class="sidebar-title">{{ title }}</h1>
      </router-link>
    </transition>
  </div>
@@ -16,7 +15,7 @@
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import useUserStore from '@/store/modules/user'
import defaultLogo from '@/assets/logo/logo.png' // å¯¼å…¥é»˜è®¤logo
import defaultLogo from '@/assets/logo/logo.png'
defineProps({
  collapse: {
@@ -27,24 +26,22 @@
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()
})
// åŠ¨æ€logo路径
const logoUrl = ref('')
// æ£€æŸ¥logo是否存在并设置url
const updateLogoUrl = () => {
  if (!cleanFactoryName.value) {
    logoUrl.value = defaultLogo
    return
  }
  // ä½¿ç”¨Vite的动态导入
  try {
    const dynamicLogo = import.meta.glob('/src/assets/logo/*.png', { eager: true })
    const logoPath = `/src/assets/logo/${cleanFactoryName.value}.png`
@@ -60,17 +57,12 @@
  }
}
// åˆå§‹åŒ–和监听变化
onMounted(() => {
  updateLogoUrl()
  // ç›‘听工厂名称变化
  watch(() => userStore.currentFactoryName, updateLogoUrl)
})
// å›¾ç‰‡åŠ è½½é”™è¯¯å¤„ç†
const handleImageError = (event) => {
  console.warn('Logo加载失败,使用默认Logo')
const handleImageError = () => {
  logoUrl.value = defaultLogo
}
</script>
@@ -90,39 +82,60 @@
.sidebar-logo-container {
  position: relative;
  width: 100% !important;
  height: 50px !important;
  line-height: 50px;
  background: #fff;
  height: 56px !important;
  line-height: 56px;
  background: rgba(255, 255, 255, 0.78);
  border: 1px solid var(--surface-border);
  border-radius: 22px;
  text-align: center;
  overflow: hidden;
  box-shadow: var(--shadow-sm);
  & .sidebar-logo-link {
  .sidebar-logo-link {
    height: 100%;
    width: 100%;
    & .sidebar-logo {
      width: 100%;
      height: 100%;
      // height: 32px;
      vertical-align: middle;
      margin-right: 12px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0 18px 0 14px;
    }
    & .sidebar-title {
  .sidebar-logo {
    width: auto;
    max-width: 250px;
    max-height: 50px;
    height: auto;
    vertical-align: middle;
    object-fit: contain;
    object-position: center;
  }
  .sidebar-title {
      display: inline-block;
      margin: 0;
      color: v-bind(getLogoTextColor);
    color: var(--text-primary);
      font-weight: 600;
      line-height: 50px;
    line-height: 1.2;
      font-size: 14px;
      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
    font-family: "Segoe UI", "PingFang SC", sans-serif;
      vertical-align: middle;
    }
  }
  &.collapse {
    .sidebar-logo-link {
      padding: 0;
    }
    .sidebar-logo {
      margin-right: 0px;
      max-width: 30px;
      max-height: 30px;
    }
    .sidebar-favicon {
      width: 24px;
      height: 24px;
      max-width: 24px;
      max-height: 24px;
    }
  }
}
src/layout/components/Sidebar/index.vue
@@ -1,11 +1,21 @@
<template>
  <div :class="{ 'has-logo': showLogo }" class="sidebar-container">
    <logo v-if="showLogo" :collapse="isCollapse" />
  <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"
      <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>
@@ -13,53 +23,49 @@
</template>
<script setup>
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.module.scss'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
  import Logo from "./Logo";
  import SidebarItem from "./SidebarItem";
  import variables from "@/assets/styles/variables.module.scss";
  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 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 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(() => {
  if (settingsStore.isDark) {
    return 'var(--sidebar-bg)'
  }
  // æµ…色主题时,直接用主题色
  return sideTheme.value === 'theme-dark' ? variables.menuBg : settingsStore.theme
})
  const getMenuBackground = computed(() => "var(--sidebar-bg)");
// èŽ·å–èœå•æ–‡å­—é¢œè‰²
const getMenuTextColor = computed(() => {
  if (settingsStore.isDark) {
    return 'var(--sidebar-text)'
      return "var(--sidebar-text)";
  }
  return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
})
    return sideTheme.value === "theme-dark"
      ? variables.menuText
      : variables.menuLightText;
  });
const activeMenu = computed(() => {
  const { meta, path } = route
    const { meta, path } = route;
  if (meta.activeMenu) {
    return meta.activeMenu
      return meta.activeMenu;
  }
  return path
})
    return path;
  });
</script>
<style lang="scss" scoped>
.sidebar-container {
  background-color: v-bind(getMenuBackground);
    border-radius: 22px;
    overflow: hidden;
  .scrollbar-wrapper {
    background-color: v-bind(getMenuBackground);
@@ -69,26 +75,68 @@
    border: none;
    height: 100%;
    width: 100% !important;
      border-radius: 22px;
    .el-menu-item,
    .el-sub-menu__title {
        margin-bottom: 6px;
        border-radius: 14px;
        color: v-bind(getMenuTextColor);
      &:hover {
        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
          border-radius: 14px;
      }
    }
    .el-menu-item {
      color: v-bind(getMenuTextColor);
      &.is-active {
        color: var(--menu-active-text, #409eff);
        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
          color: v-bind(theme);
          background-color: var(--menu-active-bg, rgba(0, 0, 0, 0.06)) !important;
          font-weight: 600;
      }
    }
    .el-sub-menu__title {
      color: v-bind(getMenuTextColor);
    }
      :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
        color: v-bind(theme) !important;
        font-weight: 600;
        background-color: var(--menu-active-bg, rgba(0, 0, 0, 0.06)) !important;
        border-radius: 14px;
        margin: 0 10px 6px !important;
        // width: calc(100% - 20px) !important;
        padding-left: 10px !important;
        padding-right: 10px !important;
        box-sizing: border-box;
        overflow: hidden;
        background-clip: padding-box;
      }
      :deep(.el-menu-item.is-active) {
        margin: 0 10px 6px !important;
        width: calc(100% - 20px) !important;
        padding-left: 10px !important;
        padding-right: 10px !important;
        box-sizing: border-box;
        overflow: hidden;
        background-clip: padding-box;
        border-radius: 14px;
      }
      :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),
      :deep(.el-menu-item.is-active .svg-icon) {
        color: v-bind(theme) !important;
      }
      :deep(.el-sub-menu__title:hover),
      :deep(.el-menu-item:hover) {
        border-radius: 14px;
      }
  }
}
</style>
src/layout/components/TagsView/ScrollPane.vue
@@ -101,7 +101,7 @@
    bottom: 0px;
  }
  :deep(.el-scrollbar__wrap) {
    height: 39px;
    height: 42px;
  }
}
</style>
src/layout/components/TagsView/index.vue
@@ -13,8 +13,8 @@
        @contextmenu.prevent="openMenu(tag, $event)"
      >
        {{ tag.title }}
        <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
          <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
        <span v-if="!isAffix(tag)" class="tags-view-close" @click.prevent.stop="closeSelectedTag(tag)">
          <close class="el-icon-close" />
        </span>
      </router-link>
    </scroll-pane>
@@ -259,42 +259,52 @@
<style lang="scss" scoped>
.tags-view-container {
  height: 34px;
  height: 42px;
  width: 100%;
  background: transparent;
  margin-top: 10px;
  padding: 4px 10px;
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid rgba(216, 225, 219, 0.92);
  border-radius: 20px;
  backdrop-filter: blur(18px);
  box-shadow: var(--shadow-sm);
  .tags-view-wrapper {
    display: flex;
    align-items: center;
    min-height: 42px;
    .tags-view-item {
      display: inline-block;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      position: relative;
      cursor: pointer;
      height: 32px;
      line-height: 32px;
      //border: 1px solid var(--tags-item-border, #d8dce5);
      color: #4E5463;
      background: #E5E7EA;
      height: 34px;
      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 16px;
      font-size: 12px;
      //margin-left: 5px;
      //margin-top: 4px;
      margin-right: 8px;
      flex-shrink: 0;
      gap: 6px;
      transition: all 0.24s ease;
      //&:first-of-type {
      //  margin-left: 8px;
      //}
      //
      //&:last-of-type {
      //  margin-right: 15px;
      //}
      &:hover {
        background: var(--tags-item-hover, #eee);
        border-color: rgba(31, 122, 114, 0.18);
      }
      &.active {
        background-color: #FFFFFF !important;
        color: #2C51D9;
        color: var(--el-color-primary);
        box-shadow: 0 10px 24px rgba(31, 122, 114, 0.12);
        border-color: rgba(31, 122, 114, 0.2) !important;
      }
    }
    //.tags-view-item div {
    //  transform: skew(12deg);
    //  display: inline-block;
    //}
  }
  .contextmenu {
@@ -304,12 +314,12 @@
    position: absolute;
    list-style-type: none;
    padding: 5px 0;
    border-radius: 4px;
    border-radius: 16px;
    font-size: 12px;
    font-weight: 400;
    color: var(--tags-item-text, #333);
    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
    border: 1px solid var(--el-border-color-light, #e4e7ed);
    box-shadow: var(--shadow-md);
    border: 1px solid var(--surface-border, #e4e7ed);
    li {
      margin: 0;
@@ -327,27 +337,53 @@
<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 {
      width: 16px;
      height: 16px;
      vertical-align: 2px;
      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-block;
        vertical-align: -3px;
        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;
        width: 12px !important;
        height: 12px !important;
      }
    }
  }
src/layout/index.vue
@@ -1,8 +1,14 @@
<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: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
  <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: needTagsView, sidebarHide: sidebar.hide }"
         class="main-container">
      <div :class="{ 'fixed-header': fixedHeader }">
        <navbar @setLayout="setLayout" />
        <tags-view v-if="needTagsView" />
@@ -10,58 +16,66 @@
      <app-main />
      <settings ref="settingRef" />
    </div>
    <AIChatSidebar v-if="aiEnabled" />
  </div>
</template>
<script setup>
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
  import { useWindowSize } from "@vueuse/core";
  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 useSettingsStore from '@/store/modules/settings'
  import useAppStore from "@/store/modules/app";
  import useUserStore from "@/store/modules/user";
  import useSettingsStore from "@/store/modules/settings";
const settingsStore = useSettingsStore()
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 fixedHeader = computed(() => settingsStore.fixedHeader)
  const settingsStore = useSettingsStore();
  const userStore = useUserStore();
  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 fixedHeader = computed(() => settingsStore.fixedHeader);
  const aiEnabled = computed(() => Number(userStore.aiEnabled) === 1);
const classObj = computed(() => ({
  hideSidebar: !sidebar.value.opened,
  openSidebar: sidebar.value.opened,
  withoutAnimation: sidebar.value.withoutAnimation,
  mobile: device.value === 'mobile'
}))
    mobile: device.value === "mobile",
  }));
const { width, height } = useWindowSize()
const WIDTH = 992 // refer to Bootstrap's responsive design
  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 })
  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 })
      useAppStore().toggleDevice("mobile");
      useAppStore().closeSideBar({ withoutAnimation: true });
  } else {
    useAppStore().toggleDevice('desktop')
      useAppStore().toggleDevice("desktop");
  }
})
  });
function handleClickOutside() {
  useAppStore().closeSideBar({ withoutAnimation: false })
    useAppStore().closeSideBar({ withoutAnimation: false });
}
const settingRef = ref(null)
  const settingRef = ref(null);
function setLayout() {
  settingRef.value.openSetting()
    settingRef.value.openSetting();
}
</script>
@@ -74,6 +88,12 @@
  position: relative;
  height: 100%;
  width: 100%;
    background: radial-gradient(
        circle at top,
        rgba(223, 232, 226, 0.95),
        transparent 32%
      ),
      linear-gradient(180deg, #f7faf8 0%, var(--app-bg) 100%);
  &.mobile.openSidebar {
    position: fixed;
@@ -93,19 +113,21 @@
.fixed-header {
  position: fixed;
  top: 0;
  right: 0;
    top: 0px;
    padding-top: 12px;
    right: 16px;
  z-index: 9;
  width: calc(100% - #{$base-sidebar-width});
  transition: width 0.28s;
    width: calc(100% - #{$base-sidebar-width} - 32px);
    transition: width 0.28s, right 0.28s;
    padding-bottom: 8px;
    background-color: #f3f6f4;
}
.hideSidebar .fixed-header {
  width: calc(100% - 54px);
    width: calc(100% - 100px);
}
.sidebarHide .fixed-header {
  width: 100%;
    width: calc(100% - 32px);
}
.mobile .fixed-header {
src/main.js
@@ -43,11 +43,13 @@
// å¯Œæ–‡æœ¬ç»„ä»¶
import Editor from "@/components/Editor";
// æ–‡ä»¶ä¸Šä¼ ç»„ä»¶
import FileUpload from "@/components/FileUpload";
import FileUpload from "@/components/AttachmentUpload/file";
// å›¾ç‰‡ä¸Šä¼ ç»„ä»¶
import ImageUpload from "@/components/ImageUpload";
import ImageUpload from "@/components/AttachmentUpload/image";
// å›¾ç‰‡é¢„览组件
import ImagePreview from "@/components/ImagePreview";
import ImagePreview from "@/components/AttachmentPreview/image";
// é™„件弹窗组件
import FileListDialog from "@/components/Dialog/FileList.vue";
// å­—典标签组件
import DictTag from "@/components/DictTag";
// è¡¨æ ¼ç»„ä»¶
@@ -92,6 +94,7 @@
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);
src/router/index.js
@@ -47,6 +47,20 @@
    component: () => import("@/views/register"),
    hidden: 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: "/:pathMatch(.*)*",
    component: () => import("@/views/error/404"),
@@ -119,6 +133,119 @@
      },
    ],
  },
  // è´¢åŠ¡ç®¡ç†æ¨¡å—è·¯ç”±
  // {
  //   path: "/financial",
  //   component: Layout,
  //   hidden: false,
  //   redirect: "/financial/general-ledger",
  //   alwaysShow: true,
  //   meta: { title: "财务管理", icon: "money" },
  //   children: [
  //     {
  //       path: "general-ledger",
  //       component: () => import("@/views/financialManagement/generalLedger/index.vue"),
  //       name: "GeneralLedger",
  //       meta: { title: "总帐科目" },
  //     },
  //     {
  //       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: "receivable-reconciliation",
  //       component: () => import("@/views/financialManagement/receivable/reconciliation.vue"),
  //       name: "ReceivableReconciliation",
  //       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: "purchase-in",
  //       component: () => import("@/views/financialManagement/payable/purchaseIn.vue"),
  //       name: "PurchaseIn",
  //       meta: { title: "采购入库" },
  //     },
  //     {
  //       path: "payable-reconciliation",
  //       component: () => import("@/views/financialManagement/payable/reconciliation.vue"),
  //       name: "PayableReconciliation",
  //       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: "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: "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: "科目明细帐" },
  //     },
  //   ],
  // },
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
src/store/modules/permission.js
@@ -4,6 +4,7 @@
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')
@@ -36,15 +37,18 @@
        return new Promise(resolve => {
          // å‘后端请求路由数据
          getRouters().then(res => {
            const sdata = JSON.parse(JSON.stringify(res.data))
            const rdata = JSON.parse(JSON.stringify(res.data))
            const defaultData = JSON.parse(JSON.stringify(res.data))
            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)
            // å°†è´¢åŠ¡ç®¡ç†è·¯ç”±åˆå¹¶åˆ°ä¾§è¾¹æ 
            this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
            this.setDefaultRoutes(sidebarRoutes)
            this.setTopbarRoutes(defaultRoutes)
@@ -56,6 +60,37 @@
  })
// éåŽ†åŽå°ä¼ æ¥çš„è·¯ç”±å­—ç¬¦ä¸²ï¼Œè½¬æ¢ä¸ºç»„ä»¶å¯¹è±¡
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) {
src/store/modules/user.js
@@ -13,7 +13,8 @@
      name: '',
      avatar: '',
      roles: [],
      permissions: []
      permissions: [],
      aiEnabled: 0
    }),
    actions: {
      // ç™»å½•
@@ -63,6 +64,7 @@
            this.roleName = user.roles[0].roleName
            this.currentDeptId = user.tenantId
            this.currentLoginTime = this.getCurrentTime()
            this.aiEnabled = Number(res.aiEnabled) === 1 ? 1 : 0
            resolve(res)
          }).catch(error => {
            reject(error)
@@ -76,6 +78,7 @@
            this.token = ''
            this.roles = []
            this.permissions = []
            this.aiEnabled = 0
            removeToken()
            resolve()
          }).catch(error => {
src/utils/generator/html.js
@@ -8,8 +8,8 @@
  return `<el-dialog v-model="dialogVisible"  @open="onOpen" @close="onClose" title="Dialog Titile">
    ${str}
    <template #footer>
      <el-button @click="close">取消</el-button>
      <el-button type="primary" @click="handelConfirm">确定</el-button>
     <el-button @click="close">取消</el-button>
    </template>
  </el-dialog>`
}
src/views/basicData/customerFile/index.vue
@@ -1,6 +1,6 @@
<template>
  <div class="app-container">
    <div class="search_form">
    <div class="search_form" style="margin-bottom: 20px;">
      <div>
        <span class="search_title">客户名称:</span>
        <el-input v-model="searchForm.customerName"
@@ -211,7 +211,8 @@
                 :limit="1"
                 accept=".xlsx, .xls"
                 :headers="upload.headers"
                 :action="upload.url + '?updateSupport=' + upload.updateSupport"
                 :action="upload.url"
                 :data="upload.data"
                 :disabled="upload.isUploading"
                 :before-upload="upload.beforeUpload"
                 :on-progress="upload.onProgress"
@@ -276,9 +277,8 @@
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitReminderForm">确认</el-button>
          <el-button @click="closeReminderDialog">取消</el-button>
          <el-button type="primary"
                     @click="submitReminderForm">提交</el-button>
        </div>
      </template>
    </el-dialog>
@@ -360,9 +360,8 @@
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitNegotiationForm">确认</el-button>
          <el-button @click="closeNegotiationDialog">取消</el-button>
          <el-button type="primary"
                     @click="submitNegotiationForm">提交</el-button>
        </div>
      </template>
    </el-dialog>
@@ -494,7 +493,6 @@
            <template #default="{ row }">
              <el-button type="info"
                         link
                         size="small"
                         @click="openAttachmentDialog(row)">
                <el-icon>
                  <Paperclip />
@@ -510,13 +508,11 @@
            <template #default="{ row, $index }">
              <el-button type="primary"
                         link
                         size="small"
                         @click="editNegotiationRecord(row, $index)">
                ä¿®æ”¹
              </el-button>
              <el-button type="danger"
                         link
                         size="small"
                         @click="deleteNegotiationRecord(row, $index)">
                åˆ é™¤
              </el-button>
@@ -587,13 +583,11 @@
              <template #default="{ row, $index }">
                <el-button type="primary"
                           link
                           size="small"
                           @click="downloadAttachment(row)">
                  ä¸‹è½½
                </el-button>
                <el-button type="danger"
                           link
                           size="small"
                           @click="deleteAttachment(row, $index)">
                  åˆ é™¤
                </el-button>
@@ -619,17 +613,13 @@
  import { onMounted, ref, reactive, getCurrentInstance, toRefs } from "vue";
  import { Search, Paperclip, Upload } from "@element-plus/icons-vue";
  import {
    addCustomer,
    delCustomer,
    getCustomer,
    listCustomer,
    updateCustomer,
    addCustomerFollow,
    updateCustomerFollow,
    delCustomerFollow,
    addReturnVisit,
    getReturnVisit,
  } from "@/api/basicData/customerFile.js";
  import {listCustomer, getCustomer, addCustomer, updateCustomer, delCustomer} from "@/api/basicData/customer.js";
  import { ElMessageBox } from "element-plus";
  import { userListNoPage } from "@/api/system/user.js";
  import useUserStore from "@/store/modules/user";
@@ -733,7 +723,7 @@
    },
    {
      label: "地址及联系方式",
      prop: "addressPhone",
      prop: "companyAddress",
      width: 250,
    },
    {
@@ -775,6 +765,24 @@
      prop: "maintainer",
    },
    {
      label: "客户来源",
      prop: "type",
      dataType: "tag",
      width: 100,
      formatData: value => {
        if (value === 1 || value === "1") {
          return "公海";
        }
        return "私海";
      },
      formatType: value => {
        if (value === 1 || value === "1") {
          return "warning";
        }
        return "success";
      },
    },
    {
      label: "维护时间",
      prop: "maintenanceTime",
      width: 100,
@@ -784,7 +792,7 @@
      label: "操作",
      align: "center",
      fixed: "right",
      width: 250,
      width: 290,
      operation: [
        {
          name: "编辑",
@@ -794,10 +802,10 @@
          },
        },
        {
          name: "详情",
                    name: "添加洽谈进度",
          type: "text",
          clickFun: row => {
            openDetailDialog(row);
                        openNegotiationDialog(row);
          },
        },
        {
@@ -808,10 +816,10 @@
          },
        },
        {
          name: "添加洽谈进度",
                    name: "详情",
          type: "text",
          clickFun: row => {
            openNegotiationDialog(row);
                        openDetailDialog(row);
          },
        },
      ],
@@ -844,6 +852,7 @@
    searchForm: {
      customerName: "",
      customerType: "",
      type: 0
    },
    form: {
      customerName: "",
@@ -858,6 +867,7 @@
      bankAccount: "",
      bankCode: "",
      customerType: "",
      type: 0
    },
    rules: {
      customerName: [{ required: true, message: "请输入", trigger: "blur" }],
@@ -889,6 +899,9 @@
    headers: { Authorization: "Bearer " + getToken() },
    // ä¸Šä¼ çš„地址
    url: import.meta.env.VITE_APP_BASE_API + "/basic/customer/importData",
    data: {
      type: 0
    },
    // æ–‡ä»¶ä¸Šä¼ å‰çš„回调
    beforeUpload: file => {
      console.log("文件即将上传", file);
@@ -961,8 +974,8 @@
    tableLoading.value = true;
    listCustomer({ ...searchForm.value, ...page }).then(res => {
      tableLoading.value = false;
      tableData.value = res.records;
      page.total = res.total;
      tableData.value = res.data.records;
      page.total = res.data.total;
    });
  };
  // è¡¨æ ¼é€‰æ‹©æ•°æ®
@@ -994,6 +1007,7 @@
        contactPhone: "",
      },
    ];
    form.value.type = 0;
    form.value.maintenanceTime = getCurrentDate();
    userListNoPage().then(res => {
      userList.value = res.data;
@@ -1069,7 +1083,7 @@
      type: "warning",
    })
      .then(() => {
        proxy.download("/basic/customer/export", {}, "客户档案.xlsx");
        proxy.download("/basic/customer/export", {type: 0}, "客户档案.xlsx");
      })
      .catch(() => {
        proxy.$modal.msg("已取消");
@@ -1079,12 +1093,11 @@
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»–人维护的数据
      const unauthorizedData = selectedRows.value.filter(
        item => item.maintainer !== userStore.nickName
        item => item.type === 1 || item.type === "1"
      );
      if (unauthorizedData.length > 0) {
        proxy.$modal.msgWarning("不可删除他人维护的数据");
        proxy.$modal.msgWarning("公海分配的客户不能删除");
        return;
      }
      ids = selectedRows.value.map(item => item.id);
@@ -1100,7 +1113,7 @@
      .then(() => {
        tableLoading.value = true;
        delCustomer(ids)
          .then(res => {
          .then(() => {
            proxy.$modal.msgSuccess("删除成功");
            getList();
          })
@@ -1169,8 +1182,6 @@
          };
        }
        console.log("提交回访提醒数据:", submitvalue.value);
        // è°ƒç”¨æŽ¥å£
        addReturnVisit(submitvalue.value)
          .then(res => {
@@ -1198,14 +1209,6 @@
    negotiationForm.followUpTime = "";
    negotiationForm.followerUserName = userStore.nickName; // é»˜è®¤å½“前登录人
    negotiationForm.content = "";
    // {
    //     "customerId": 152,
    //     "followUpMethod": "电话沟通",
    //     "followUpLevel": "没有意向",
    //     "followUpTime": "2026-03-04T15:30:00",
    //     "followerUserName": "管理员账号",
    //     "content": "111"
    // }
    negotiationDialogVisible.value = true;
  };
@@ -1227,23 +1230,6 @@
        if (isEdit) {
          // ä¿®æ”¹æ“ä½œ
          console.log("修改洽谈进度数据:", negotiationForm);
          // è¿™é‡Œå¯ä»¥è°ƒç”¨æ›´æ–°æŽ¥å£
          // å®žé™…项目中需要根据后端接口进行调整
          // ç¤ºä¾‹ï¼šupdateCustomerFollow(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 => {
@@ -1278,7 +1264,6 @@
  // æ‰“开详情弹窗
  const openDetailDialog = row => {
    // è°ƒç”¨getCustomer接口获取客户详情
    getCustomer(row.id).then(res => {
      // å¡«å……客户基本信息
      Object.assign(detailForm, res.data);
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>仅允许导入xls、xlsx格式文件。</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">
                æ”¯æŒä¸Šä¼ å›¾ç‰‡ã€æ–‡æ¡£ç­‰æ–‡ä»¶ï¼Œå•个文件不超过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() };
  // åŠ¨æ€æž„å»ºä¸Šä¼ URL
  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}, "客户档案.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);
          // è¿™é‡Œå¯ä»¥è°ƒç”¨æ›´æ–°æŽ¥å£
          // å®žé™…项目中需要根据后端接口进行调整
          // ç¤ºä¾‹ï¼šupdateCustomerFollow(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(() => {
        // è¿™é‡Œå¯ä»¥è°ƒç”¨åˆ é™¤æŽ¥å£
        // å®žé™…项目中需要根据后端接口进行调整
        // ç¤ºä¾‹ï¼šdeleteCustomerFollow(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;
    // è½¬æ¢ä¸ºç¬¦åˆElement 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",
      }));
      // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
      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;
    // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
    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.name(row.url);
    } 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);
              // æ›´æ–°åŽŸè®°å½•ä¸­çš„files字段
              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>
src/views/basicData/parameterMaintenance/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,793 @@
<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="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 } from "vue";
  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: "",
  });
  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 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;
    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;
    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>
src/views/basicData/product/ProductSelectDialog.vue
@@ -1,12 +1,12 @@
<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 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 label="产品型号">
        <el-input v-model="query.model" placeholder="输入产品型号" clearable @keyup.enter="onSearch" />
      </el-form-item>
      <el-form-item>
@@ -20,8 +20,8 @@
      @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="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>
@@ -43,7 +43,7 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { productModelList } from '@/api/basicData/productModel'
import { productModelList, productModelListByUrl } from '@/api/basicData/productModel'
export type ProductRow = {
  id: number;
@@ -55,6 +55,8 @@
const props = defineProps<{
  modelValue: boolean;
  single?: boolean; // æ˜¯å¦åªèƒ½é€‰æ‹©ä¸€ä¸ªï¼Œé»˜è®¤false(可选择多个)
  topProductParentId?: number; // ä¸€çº§äº§å“id
  requestUrl?: string; // è‡ªå®šä¹‰æŸ¥è¯¢æŽ¥å£
}>();
const emit = defineEmits(['update:modelValue', 'confirm']);
@@ -154,14 +156,19 @@
  loading.value = true;
  try {
    multipleSelection.value = []; // ç¿»é¡µ/搜索后清空选择更符合预期
    const res: any = await productModelList({
    const params = {
      productName: query.productName.trim(),
      model: query.model.trim(),
      current: page.pageNum,
      size: page.pageSize,
    });
    tableData.value = res.records;
    total.value = res.total;
      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;
  }
src/views/basicData/product/index.vue
@@ -2,37 +2,34 @@
  <div class="app-container product-view">
    <div class="left">
      <div>
        <el-input
          v-model="search"
        <el-input v-model="search"
          style="width: 210px"
          placeholder="输入关键字进行搜索"
          @change="searchFilter"
          @clear="searchFilter"
          clearable
          prefix-icon="Search"
        />
        <el-button
                  prefix-icon="Search" />
        <el-button v-if="false"
          type="primary"
          @click="openProDia('addOne')"
          style="margin-left: 10px"
          >新增产品大类</el-button
        >
                   style="margin-left: 10px">新增产品大类</el-button>
      </div>
      <div ref="containerRef">
        <el-tree
          ref="tree"
        <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"
        >
                 style="height: calc(100vh - 190px); overflow-y: auto">
          <template #default="{ node, data }">
            <div class="custom-tree-node">
              <span class="tree-node-content">
@@ -43,23 +40,23 @@
                <span class="tree-node-label">{{ data.label }}</span>
              </span>
              <div>
                <el-button
                  type="primary"
                <el-button type="primary"
                  link
                  @click="openProDia('edit', data)"
                >
                           :disabled="isTopLevelNode(data, node)"
                           @click="openProDia('edit', data)">
                  ç¼–辑
                </el-button>
                <el-button type="primary" link @click="openProDia('add', data)" :disabled="node.level >= 3">
                <el-button type="primary"
                           link
                           @click="openProDia('add', data)">
                  æ·»åŠ äº§å“
                </el-button>
                <el-button
                  v-if="!node.childNodes.length"
                <el-button v-if="!node.childNodes.length"
                  style="margin-left: 4px"
                  type="danger"
                  link
                  @click="remove(node, data)"
                >
                           :disabled="isTopLevelNode(data, node)"
                           @click="remove(node, data)">
                  åˆ é™¤
                </el-button>
              </div>
@@ -69,103 +66,109 @@
      </div>
    </div>
    <div class="right">
      <div style="margin-bottom: 10px" v-if="isShowButton">
        <el-button type="primary" @click="openModelDia('add')">
      <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"
        <ImportExcel :product-id="currentId"
                     @uploadSuccess="getModelList" />
        <el-button type="danger"
          @click="handleDelete"
          style="margin-left: 10px"
          plain
        >
                   plain>
          åˆ é™¤
        </el-button>
      </div>
      <PIMTable
        rowKey="id"
      <PIMTable rowKey="id"
        :column="tableColumn"
        :tableData="tableData"
        :page="page"
        :isSelection="true"
        @selection-change="handleSelectionChange"
        :tableLoading="tableLoading"
        @pagination="pagination"
      ></PIMTable>
                @pagination="pagination"></PIMTable>
    </div>
    <el-dialog v-model="productDia" title="产品" width="400px" @keydown.enter.prevent>
      <el-form
        :model="form"
    <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"
      >
               ref="formRef">
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="产品名称:" prop="productName">
              <el-input
                v-model="form.productName"
            <el-form-item label="产品名称:"
                          prop="productName">
              <el-input v-model="form.productName"
                placeholder="请输入产品名称"
                maxlength="20"
                show-word-limit
                clearable
                @keydown.enter.prevent
              />
                        @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 type="primary"
                     @click="submitForm">确认</el-button>
          <el-button @click="closeProDia">取消</el-button>
        </div>
      </template>
    </el-dialog>
    <el-dialog
      v-model="modelDia"
    <el-dialog v-model="modelDia"
      title="规格型号"
      width="400px"
      @close="closeModelDia"
      @keydown.enter.prevent
    >
      <el-form
        :model="modelForm"
               @keydown.enter.prevent>
      <el-form :model="modelForm"
        label-width="140px"
        label-position="top"
        :rules="modelRules"
        ref="modelFormRef"
      >
               ref="modelFormRef">
        <el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="规格型号:" prop="model">
              <el-input
                v-model="modelForm.model"
              <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
              />
                        @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"
            <el-form-item label="单位:"
                          prop="unit">
              <el-input v-model="modelForm.unit"
                placeholder="请输入单位"
                clearable
                @keydown.enter.prevent
              />
                        @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 type="primary"
                     @click="submitModelForm">确认</el-button>
          <el-button @click="closeModelDia">取消</el-button>
        </div>
      </template>
@@ -174,7 +177,7 @@
</template>
<script setup>
import { ref } from "vue";
  import { nextTick, ref } from "vue";
import { ElMessageBox } from "element-plus";
import {
  addOrEditProduct,
@@ -189,6 +192,92 @@
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);
@@ -201,6 +290,10 @@
const list = ref([]);
const expandedKeys = ref([]);
const tableColumn = ref([
    {
      label: "产品编号",
      prop: "productCode",
    },
  {
    label: "规格型号",
    prop: "model",
@@ -217,7 +310,7 @@
      {
        name: "编辑",
        type: "text",
        clickFun: (row) => {
          clickFun: row => {
          openModelDia("edit", row);
        },
      },
@@ -246,10 +339,12 @@
  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);
@@ -257,23 +352,47 @@
const getProductTreeList = () => {
  treeLoad.value = true;
  productTreeList()
    .then((res) => {
      list.value = res;
      list.value.forEach((a) => {
        expandedKeys.value.push(a.label);
      .then(res => {
        list.value = res || [];
        normalizeExpandedKeys(list.value);
        expandedKeys.value = Array.from(expandedKeySet);
        treeKey.value += 1;
        nextTick(() => {
          tree.value?.setDefaultExpandedKeys?.(expandedKeys.value);
      });
      treeLoad.value = false;
    })
    .catch((err) => {
      .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) => {
    if (data && type === "edit" && isTopLevelNode(data)) {
      proxy.$modal.msgWarning("一级节点不能编辑或删除");
      return;
    }
  operationType.value = type;
  productDia.value = true;
  form.value.productName = "";
@@ -286,7 +405,8 @@
  modelOperationType.value = type;
  modelDia.value = true;
  modelForm.value.model = "";
  modelForm.value.model = "";
    modelForm.value.unit = "";
    modelForm.value.productCode = "";
  modelForm.value.id = "";
  if (type === "edit") {
    modelForm.value = { ...data };
@@ -294,7 +414,7 @@
};
// æäº¤äº§å“åç§°ä¿®æ”¹
const submitForm = () => {
  proxy.$refs.formRef.validate((valid) => {
    proxy.$refs.formRef.validate(valid => {
    if (valid) {
      if (operationType.value === "add") {
        form.value.parentId = currentId.value;
@@ -306,7 +426,7 @@
        form.value.id = currentId.value;
        form.value.parentId = "";
      }
      addOrEditProduct(form.value).then((res) => {
        addOrEditProduct(form.value).then(res => {
        proxy.$modal.msgSuccess("提交成功");
        closeProDia();
        getProductTreeList();
@@ -322,6 +442,10 @@
// åˆ é™¤äº§å“
const remove = (node, data) => {
    if (isTopLevelNode(data, node)) {
      proxy.$modal.msgWarning("一级节点不能编辑或删除");
      return;
    }
  let ids = [];
  ids.push(data.id);
  ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除提示", {
@@ -332,7 +456,7 @@
    .then(() => {
      tableLoading.value = true;
      delProduct(ids)
        .then((res) => {
          .then(res => {
          proxy.$modal.msgSuccess("删除成功");
          getProductTreeList();
        })
@@ -356,10 +480,10 @@
// æäº¤è§„格型号修改
const submitModelForm = () => {
  proxy.$refs.modelFormRef.validate((valid) => {
    proxy.$refs.modelFormRef.validate(valid => {
    if (valid) {
      modelForm.value.productId = currentId.value;
      addOrEditProductModel(modelForm.value).then((res) => {
        addOrEditProductModel(modelForm.value).then(res => {
        proxy.$modal.msgSuccess("提交成功");
        closeModelDia();
        getModelList();
@@ -373,12 +497,12 @@
  modelDia.value = false;
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  const handleSelectionChange = selection => {
  selectedRows.value = selection;
};
// æŸ¥è¯¢è§„格型号
const pagination = (obj) => {
  const pagination = obj => {
  page.current = obj.page;
  page.size = obj.limit;
  getModelList();
@@ -389,7 +513,7 @@
    id: currentId.value,
    current: page.current,
    size: page.size,
  }).then((res) => {
    }).then(res => {
    console.log("res", res);
    tableData.value = res.records;
    page.total = res.total;
@@ -400,7 +524,7 @@
const handleDelete = () => {
  let ids = [];
  if (selectedRows.value.length > 0) {
    ids = selectedRows.value.map((item) => item.id);
      ids = selectedRows.value.map(item => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
@@ -413,7 +537,7 @@
    .then(() => {
      tableLoading.value = true;
      delProductModel(ids)
        .then((res) => {
          .then(res => {
          proxy.$modal.msgSuccess("删除成功");
          getModelList();
        })
src/views/basicData/supplierManage/components/HomeTab.vue
@@ -1,7 +1,7 @@
<template>
  <div class="app-container">
    <div class="search_form">
      <div>
    <div class="search_form">
      <div style="margin-bottom: 10px;">
        <span class="search_title">供应商档案:</span>
        <el-input
            v-model="searchForm.supplierName"
@@ -15,7 +15,7 @@
        >搜索</el-button
        >
      </div>
      <div>
      <div style="margin-bottom: 10px;">
        <el-button type="primary" @click="openForm('add')"
        >新增供应商</el-button
        >
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>
src/views/collaborativeApproval/approvalProcess/components/approvalDia.vue
@@ -39,39 +39,6 @@
                        </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>
      <!-- æŠ¥ä»·å®¡æ‰¹ï¼šå±•示报价详情(复用销售报价"查看详情对话框"内容结构) -->
@@ -228,7 +195,6 @@
    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";
@@ -248,7 +214,6 @@
const formRef = ref(null);
const userStore = useUserStore()
const productOptions = ref([]);
const userList = ref([])
const quotationLoading = ref(false)
const currentQuotation = ref({})
const purchaseLoading = ref(false)
@@ -258,9 +223,7 @@
const data = reactive({
    form: {
        approveTime: "",
        approveId: "",
        approveUser: "",
        approveDeptId: "",
        approveReason: "",
        checkResult: "",
@@ -295,9 +258,6 @@
  dialogFormVisible.value = true;
  currentQuotation.value = {}
  currentPurchase.value = {}
    userListNoPageByTenantId().then((res) => {
        userList.value = res.data;
    });
    form.value = {...row}
    // ç«‹å³æ¸…除表单验证状态(因为字段是disabled的,不需要验证)
    nextTick(() => {
src/views/collaborativeApproval/approvalProcess/components/infoFormDia.vue
@@ -97,96 +97,11 @@
            </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">
              <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,docx,xls,xlsx,ppt,pptx,pdf,txt,xml,jpg,jpeg,png,gif,bmp,rar,zip,7z
                  </div>
                </template>
              </el-upload>
              <FileUpload v-model:file-list="fileList" />
            </el-form-item>
          </el-col>
        </el-row>
@@ -211,13 +126,11 @@
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);
@@ -231,24 +144,18 @@
});
const data = reactive({
  form: {
    approveTime: "",
    approveId: "",
    approveUser: "",
        approveDeptId: "",
    approveDeptName: "",
    approveReason: "",
    checkResult: "",
    tempFileIds: [],
    approverList: [], // æ–°å¢žå­—段,存储所有节点的审批人id
    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" }],
@@ -268,18 +175,7 @@
  }
})
// å®¡æ‰¹äººèŠ‚ç‚¹ç›¸å…³
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) {
@@ -295,15 +191,7 @@
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();
  
  // èŽ·å–å½“å‰ç”¨æˆ·ä¿¡æ¯å¹¶è®¾ç½®éƒ¨é—¨ID
  form.value.approveDeptId = userStore.currentDeptId
@@ -316,18 +204,7 @@
        currentApproveStatus.value = row.approveStatus
    approveProcessGetInfo({id: row.approveId,approveReason: '1'}).then(res => {
            form.value = {...res.data}
      // åæ˜¾å®¡æ‰¹äºº
      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
      }
      fileList.value = res.data.storageBlobVOS
    })
  }
}
@@ -362,15 +239,7 @@
}
// æäº¤äº§å“è¡¨å•
const submitForm = () => {
  // æ”¶é›†æ‰€æœ‰èŠ‚ç‚¹çš„å®¡æ‰¹äººid
  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) {
@@ -401,6 +270,8 @@
      return
    }
  }
  form.value.storageBlobDTOList = fileList.value
  proxy.$refs.formRef.validate(valid => {
    if (valid) {
      if (operationType.value === "add" || currentApproveStatus.value == 3) {
@@ -424,47 +295,6 @@
  dialogFormVisible.value = false;
  emit('close')
};
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
  // æ ¡æ£€æ–‡ä»¶å¤§å°
  // if (file.size > 1024 * 1024 * 10) {
  //   proxy.$modal.msgError("上传文件大小不能超过10MB!");
  //   return false;
  // }
  proxy.$modal.loading("正在上传文件,请稍候...");
  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 (!form.value.tempFileIds) {
      form.value.tempFileIds = [];
    }
    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("删除成功");
    });
  }
}
defineExpose({
  openDialog,
src/views/collaborativeApproval/approvalProcess/fileList.vue
@@ -4,9 +4,9 @@
      <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>
          <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>
src/views/collaborativeApproval/approvalProcess/index.vue
@@ -1,55 +1,114 @@
<template>
  <div class="app-container">
    <!-- æ ‡ç­¾é¡µåˆ‡æ¢ä¸åŒçš„审批类型 -->
    <el-tabs v-model="activeTab" @tab-change="handleTabChange" class="approval-tabs">
      <el-tab-pane label="公出管理" name="1"></el-tab-pane>
      <el-tab-pane label="请假管理" name="2"></el-tab-pane>
      <el-tab-pane label="出差管理" name="3"></el-tab-pane>
      <el-tab-pane label="报销管理" name="4"></el-tab-pane>
      <el-tab-pane label="采购审批" name="5"></el-tab-pane>
      <el-tab-pane label="报价审批" name="6"></el-tab-pane>
      <el-tab-pane label="发货审批" name="7"></el-tab-pane>
    </el-tabs>
    <!-- å®¡æ‰¹ç±»åž‹åˆ‡æ¢ - ç´§å‡‘标签式 -->
    <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>
    
    <div class="search_form">
      <div>
        <span class="search_title">流程编号:</span>
    <!-- æœç´¢å’Œæ“ä½œåŒºåŸŸ -->
    <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"
            style="width: 240px"
            placeholder="请输入流程编号搜索"
            @change="handleQuery"
              placeholder="请输入流程编号"
            clearable
            :prefix-icon="Search"
              @keyup.enter="handleQuery"
              class="search-input"
        />
        <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>
          <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"
        >新增</el-button>
        <el-button @click="handleOut">导出</el-button>
            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"
        >删除</el-button>
            class="action-btn danger"
          >
            <el-icon><Delete /></el-icon>
            åˆ é™¤
          </el-button>
      </div>
    </div>
    <div class="table_list">
    </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"
@@ -60,8 +119,11 @@
          :tableLoading="tableLoading"
          @pagination="pagination"
          :total="page.total"
        class="custom-table"
      ></PIMTable>
    </div>
    </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 ref="fileListRef" />
@@ -70,7 +132,7 @@
<script setup>
import FileList from "./fileList.vue";
import { Search } from "@element-plus/icons-vue";
import { Search, Plus, Delete, Download, RefreshRight, DocumentChecked } 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';
@@ -85,13 +147,37 @@
// å½“前选中的标签页,默认为公出管理
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 = (tabName) => {
const handleTabChange = () => {
  // åˆ‡æ¢æ ‡ç­¾é¡µæ—¶é‡ç½®æœç´¢æ¡ä»¶å’Œåˆ†é¡µï¼Œå¹¶é‡æ–°åŠ è½½æ•°æ®
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
@@ -107,6 +193,13 @@
  },
});
const { searchForm } = toRefs(data);
// é‡ç½®æœç´¢
const resetQuery = () => {
  searchForm.value.approveId = '';
  searchForm.value.approveStatus = '';
  handleQuery();
};
// åŠ¨æ€è¡¨æ ¼åˆ—é…ç½®ï¼Œæ ¹æ®å®¡æ‰¹ç±»åž‹ç”Ÿæˆåˆ—
const tableColumnCopy = computed(() => {
@@ -238,7 +331,7 @@
    },
  ];
  // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示“附件”操作
  // æŠ¥ä»·å®¡æ‰¹ï¼ˆç±»åž‹ 6)不展示"附件"操作
  if (!isQuotationType) {
    actionOperations.push({
      name: "附件",
@@ -294,6 +387,8 @@
    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;
  })
@@ -388,7 +483,256 @@
</script>
<style scoped>
.approval-tabs {
  margin-bottom: 10px;
.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>
src/views/collaborativeApproval/attendanceManagement/index.vue
@@ -303,8 +303,8 @@
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
src/views/collaborativeApproval/customerVisit/index.vue
@@ -45,7 +45,7 @@
        <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)">查看</el-button>
            <el-button link type="primary" size="small" @click="viewDetail(scope.row)" style="color: #67C23A">查看</el-button>
          </template>
        </el-table-column>
      </el-table>
src/views/collaborativeApproval/enterpriseBook/index.vue
@@ -275,8 +275,8 @@
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="showAddContactDialog = false">取消</el-button>
        <el-button type="primary" @click="addContact">确定</el-button>
        <el-button @click="showAddContactDialog = false">取消</el-button>
      </template>
    </el-dialog>
  </div>
src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -1,6 +1,6 @@
<template>
  <div class="app-container">
    <div class="search_form">
    <div class="search_form" style="margin-bottom: 20px;">
      <div>
        <span class="search_title">知识标题:</span>
        <el-input
@@ -382,7 +382,7 @@
        }
      },
      {
        name: "查看",
        name: "详情",
        type: "text",
        clickFun: (row) => {
          viewKnowledge(row);
src/views/collaborativeApproval/meetingBoard/index.vue
@@ -83,9 +83,7 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Clock, Location, User, UserFilled } from '@element-plus/icons-vue'
import Editor from "@/components/Editor/index.vue";
import {getMeetSummaryItems,getMeetSummary} from '@/api/collaborativeApproval/meeting.js'
import dayjs from "dayjs";
src/views/collaborativeApproval/noticeManagement/index.vue
@@ -210,7 +210,6 @@
                  v-if="scope.row.editing"
                  link
                  type="primary"
                  size="small"
                  @click="handleSaveNoticeType(scope.row)"
              >
                ä¿å­˜
@@ -219,7 +218,6 @@
                  v-if="scope.row.editing"
                  link
                  type="info"
                  size="small"
                  @click="handleCancelEdit(scope.row)"
              >
                å–消
@@ -228,7 +226,6 @@
                  v-if="!scope.row.editing"
                  link
                  type="primary"
                  size="small"
                  @click="handleEditNoticeType(scope.row)"
              >
                ç¼–辑
@@ -237,7 +234,6 @@
                  v-if="!scope.row.editing"
                  link
                  type="danger"
                  size="small"
                  @click="handleDeleteNoticeType(scope.row)"
              >
                åˆ é™¤
@@ -933,7 +929,7 @@
}
.dialog-footer {
  text-align: right;
  text-align: center;
}
.notice-type-container {
src/views/collaborativeApproval/notificationManagement/index.vue
@@ -131,8 +131,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue
@@ -93,8 +93,8 @@
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="cancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
@@ -313,8 +313,6 @@
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
    text-align: center;
}
</style>
src/views/collaborativeApproval/notificationManagement/summary/index.vue
@@ -146,8 +146,8 @@
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="minutesDialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="submitMinutes">保 å­˜</el-button>
          <el-button @click="minutesDialogVisible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
@@ -367,9 +367,7 @@
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  text-align: center;
}
.content-section h4 {
src/views/collaborativeApproval/planTemplate/index.vue
@@ -123,7 +123,7 @@
              </div>
              <div class="plan-actions">
                <el-button size="small" @click="handleEditPlan(plan)">编辑</el-button>
                <el-button size="small" @click="handleViewDetail(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>
src/views/collaborativeApproval/processTracking/index.vue
@@ -59,7 +59,7 @@
          <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)">详情</el-button>
              <el-button type="text" @click="viewDetails(row)" style="color: #67C23A">详情</el-button>
            </template>
          </el-table-column>
        </el-table>
src/views/collaborativeApproval/purchaseApproval/index.vue
@@ -2,43 +2,52 @@
  <div class="app-container">
    <div class="search_form">
      <div>
        <el-form :model="searchForm" :inline="true">
        <el-form :model="searchForm"
                 :inline="true">
          <el-form-item label="供应商名称:">
            <el-input v-model="searchForm.supplierName" placeholder="请输入" clearable prefix-icon="Search"
            <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"
            <el-input v-model="searchForm.purchaseContractNumber"
                style="width: 240px"
                placeholder="请输入"
                @change="handleQuery"
                clearable
                :prefix-icon="Search"
            />
                      :prefix-icon="Search" />
          </el-form-item>
          <el-form-item label="销售合同号:">
            <el-input v-model="searchForm.salesContractNo" placeholder="请输入" clearable prefix-icon="Search"
            <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"
            <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-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>
        <el-button type="danger"
                   plain
                   @click="handleDelete">删除</el-button>
      </div>
      <el-table
        :data="tableData"
      <el-table :data="tableData"
        border
        v-loading="tableLoading"
        @selection-change="handleSelectionChange"
@@ -48,150 +57,125 @@
        :summary-method="summarizeMainTable"
        @expand-change="expandChange"
        height="calc(100vh - 18.5em)"
        :row-class-name="tableRowClassName"
      >
        <el-table-column align="center" type="selection" width="55" />
                :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"
            <el-table :data="props.row.children"
              border
              show-summary
              :summary-method="summarizeChildrenTable"
            >
              <el-table-column
                align="center"
                      :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="含税单价(元)"
                               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="含税总价(元)"
                               :formatter="formattedNumber" />
              <el-table-column label="含税总价(元)"
                prop="taxInclusiveTotalPrice"
                :formatter="formattedNumber"
              />
              <el-table-column
                label="不含税总价(元)"
                               :formatter="formattedNumber" />
              <el-table-column label="不含税总价(元)"
                prop="taxExclusiveTotalPrice"
                :formatter="formattedNumber"
              />
                               :formatter="formattedNumber" />
            </el-table>
          </template>
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
          label="采购合同号"
        <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="销售合同号"
                         show-overflow-tooltip />
        <el-table-column label="销售合同号"
          prop="salesContractNo"
          width="200"
          show-overflow-tooltip
        />
        <el-table-column
          label="供应商名称"
                         show-overflow-tooltip />
        <el-table-column label="供应商名称"
          width="240"
          prop="supplierName"
          show-overflow-tooltip
        />
        <el-table-column label="订单状态" width="100" align="center">
                         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">正常</el-tag>
            <el-tag v-if="scope.row.isInvalid"
                    type="danger"
                    size="small">失效</el-tag>
            <el-tag v-else
                    type="success"
                    size="small">正常</el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="项目名称"
        <el-table-column label="项目名称"
          prop="projectName"
          width="420"
          show-overflow-tooltip
        />
        <el-table-column
            label="审批状态"
                         show-overflow-tooltip />
        <el-table-column label="审批状态"
            prop="approvalStatus"
            width="200"
            show-overflow-tooltip
        >
                         show-overflow-tooltip>
          <template #default="scope">
            <el-tag
                size="small"
            >
            <el-tag size="small">
              {{ approvalStatusText[scope.row.approvalStatus] || '未知状态' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          label="付款方式"
        <el-table-column label="付款方式"
          width="100"
          prop="paymentMethod"
          show-overflow-tooltip
        />
        <el-table-column
          label="合同金额(元)"
                         show-overflow-tooltip />
        <el-table-column label="合同金额(元)"
          prop="contractAmount"
           width="200"
          show-overflow-tooltip
          :formatter="formattedNumber"
        />
        <el-table-column
          label="录入人"
                         :formatter="formattedNumber" />
        <el-table-column label="录入人"
          prop="recorderName"
           width="100"
          show-overflow-tooltip
        />
        <el-table-column
          label="录入日期"
                         show-overflow-tooltip />
        <el-table-column label="录入日期"
          prop="entryDate"
           width="100"
          show-overflow-tooltip
        />
        <el-table-column
          fixed="right"
                         show-overflow-tooltip />
        <el-table-column fixed="right"
          label="操作"
          min-width="150"
          align="center"
        >
                         align="center">
          <template #default="scope">
            <el-button
              link
            <el-button link
              type="primary"
              size="small"
              @click="approvePurchase(scope.row)"
              :disabled="scope.row.approvalStatus !== 0"
              >审批</el-button
            >
            <el-button
                link
                       :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
            >
                       :disabled="scope.row.approvalStatus !== 0">拒绝审批</el-button>
          </template>
        </el-table-column>
      </el-table>
      <pagination
        v-show="total > 0"
      <pagination v-show="total > 0"
        :total="total"
        layout="total, sizes, prev, pager, next, jumper"
        :page="page.current"
        :limit="page.size"
        @pagination="paginationChange"
      />
                  @pagination="paginationChange" />
    </div>
  </div>
</template>
@@ -199,7 +183,14 @@
<script setup>
import { getToken } from "@/utils/auth";
import pagination from "@/components/PIMTable/Pagination.vue";
import { ref, onMounted, reactive, toRefs, getCurrentInstance, nextTick } from "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";
@@ -218,11 +209,11 @@
  productList,
  getPurchaseById,
  getOptions,
  createPurchaseNo, updateApprovalStatus,
    createPurchaseNo,
    updateApprovalStatus,
} from "@/api/procurementManagement/procurementLedger.js";
import useFormData from "@/hooks/useFormData.js";
import QRCode from "qrcode";
const { proxy } = getCurrentInstance();
const tableData = ref([]);
@@ -254,9 +245,9 @@
// è®¢å•审批状态显示文本
const approvalStatusText = {
  0: '待审批',
  1: '审批通过',
  2: '审批失败'
    0: "待审批",
    1: "审批通过",
    2: "审批失败",
};
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
@@ -338,14 +329,14 @@
  },
});
const { productForm, productRules } = toRefs(productFormData);
const upload = reactive({
  // ä¸Šä¼ çš„地址
  url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  // è®¾ç½®ä¸Šä¼ çš„请求头部
  headers: { Authorization: "Bearer " + getToken() },
});
  // const upload = reactive({
  //   // ä¸Šä¼ çš„地址
  //   url: import.meta.env.VITE_APP_BASE_API + "/file/upload",
  //   // è®¾ç½®ä¸Šä¼ çš„请求头部
  //   headers: { Authorization: "Bearer " + getToken() },
  // });
const changeDaterange = (value) => {
  const changeDaterange = value => {
  if (value) {
    searchForm.entryDateStart = dayjs(value[0]).format("YYYY-MM-DD");
    searchForm.entryDateEnd = dayjs(value[1]).format("YYYY-MM-DD");
@@ -366,7 +357,7 @@
  getList();
};
// å­è¡¨åˆè®¡æ–¹æ³•
const summarizeChildrenTable = (param) => {
  const summarizeChildrenTable = param => {
  return proxy.summarizeTable(
    param,
    [
@@ -384,7 +375,7 @@
    }
  );
};
const paginationChange = (obj) => {
  const paginationChange = obj => {
  page.current = obj.page;
  page.size = obj.limit;
  getList();
@@ -393,15 +384,15 @@
  tableLoading.value = true;
  const { entryDate, ...rest } = searchForm;
  purchaseListPage({ ...rest, ...page })
    .then((res) => {
      .then(res => {
      tableLoading.value = false;
      // tableData.value = res.data.records;
      // å¤„理数据,添加失效状态标记
      tableData.value = res.data.records.map(record => ({
        ...record,
        isInvalid: record.isWhite === 1
          isInvalid: record.isWhite === 1,
      }));
      tableData.value.map((item) => {
        tableData.value.map(item => {
        item.children = [];
      });
      total.value = res.data.total;
@@ -412,10 +403,10 @@
    });
};
// è¡¨æ ¼é€‰æ‹©æ•°æ®
const handleSelectionChange = (selection) => {
  const handleSelectionChange = selection => {
  selectedRows.value = selection;
};
const productSelected = (selectedRows) => {
  const productSelected = selectedRows => {
  productSelectedRows.value = selectedRows;
};
const expandedRowKeys = ref([]);
@@ -424,8 +415,8 @@
  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);
        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;
        }
@@ -439,11 +430,11 @@
  }
};
// ä¸»è¡¨åˆè®¡æ–¹æ³•
const summarizeMainTable = (param) => {
  const summarizeMainTable = param => {
  return proxy.summarizeTable(param, ["contractAmount"]);
};
// å­è¡¨åˆè®¡æ–¹æ³•
const summarizeProTable = (param) => {
  const summarizeProTable = param => {
  return proxy.summarizeTable(param, [
    "taxInclusiveUnitPrice",
    "taxInclusiveTotalPrice",
@@ -457,25 +448,25 @@
  productData.value = [];
  fileList.value = [];
  if (operationType.value == "add") {
    createPurchaseNo().then((res) => {
      createPurchaseNo().then(res => {
      form.value.purchaseContractNumber = res.data;
    });
  }
  userListNoPage().then((res) => {
    userListNoPage().then(res => {
    userList.value = res.data;
  });
  getSalesNo().then((res) => {
    getSalesNo().then(res => {
    salesContractList.value = res;
  });
  getOptions().then((res) => {
    getOptions().then(res => {
    // ä¾›åº”商过滤出isWhite=0 çš„æ•°æ®
    supplierList.value = res.data.filter((item) => item.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) => {
      getPurchaseById({ id: row.id, type: 2 }).then(res => {
      form.value = { ...res };
      productData.value = form.value.productData;
      if (form.value.salesLedgerFiles) {
@@ -523,14 +514,14 @@
  if (operationType.value === "edit") {
    let ids = [];
    ids.push(file.id);
    delLedgerFile(ids).then((res) => {
      delLedgerFile(ids).then(res => {
      proxy.$modal.msgSuccess("删除成功");
    });
  }
}
// æäº¤è¡¨å•
const submitForm = (n) => {
  proxy.$refs["formRef"].validate((valid) => {
  const submitForm = n => {
    proxy.$refs["formRef"].validate(valid => {
    if (valid) {
      if (productData.value.length > 0) {
        form.value.productData = proxy.HaveJson(productData.value);
@@ -540,12 +531,12 @@
      }
      let tempFileIds = [];
      if (fileList.value.length > 0) {
        tempFileIds = fileList.value.map((item) => item.tempId);
          tempFileIds = fileList.value.map(item => item.tempId);
      }
      form.value.tempFileIds = tempFileIds;
      form.value.type = 2;
      form.value.approvalStatus = n;
      addOrEditPurchase(form.value).then((res) => {
        addOrEditPurchase(form.value).then(res => {
        proxy.$modal.msgSuccess("提交成功");
        closeDia();
        getList();
@@ -571,14 +562,15 @@
  getProductOptions();
};
const getProductOptions = () => {
  productTreeList().then((res) => {
    productTreeList().then(res => {
    productOptions.value = convertIdToValue(res);
  });
};
const getModels = (value) => {
  const getModels = value => {
  if (value) {
    productForm.value.productCategory = findNodeById(productOptions.value, value) || "";
    modelList({ id: value }).then((res) => {
      productForm.value.productCategory =
        findNodeById(productOptions.value, value) || "";
      modelList({ id: value }).then(res => {
      modelOptions.value = res;
    });
  } else {
@@ -586,8 +578,8 @@
    modelOptions.value = [];
  }
};
const getProductModel = (value) => {
  const index = modelOptions.value.findIndex((item) => item.id === 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;
@@ -611,7 +603,7 @@
  return null; // æ²¡æœ‰æ‰¾åˆ°èŠ‚ç‚¹ï¼Œè¿”å›žnull
};
function convertIdToValue(data) {
  return data.map((item) => {
    return data.map(item => {
    const { id, children, ...rest } = item;
    const newItem = {
      ...rest,
@@ -626,7 +618,7 @@
}
// æäº¤äº§å“è¡¨å•
const submitProduct = () => {
  proxy.$refs["productFormRef"].validate((valid) => {
    proxy.$refs["productFormRef"].validate(valid => {
    if (valid) {
      if (operationType.value === "edit") {
        submitProductEdit();
@@ -647,10 +639,10 @@
const submitProductEdit = () => {
  productForm.value.salesLedgerId = currentId.value;
  productForm.value.type = 2;
  addOrUpdateSalesLedgerProduct(productForm.value).then((res) => {
    addOrUpdateSalesLedgerProduct(productForm.value).then(res => {
    proxy.$modal.msgSuccess("提交成功");
    closeProductDia();
    getPurchaseById({ id: currentId.value, type: 2 }).then((res) => {
      getPurchaseById({ id: currentId.value, type: 2 }).then(res => {
      productData.value = res.productData;
    });
  });
@@ -662,9 +654,9 @@
    return;
  }
  if (operationType.value === "add") {
    productSelectedRows.value.forEach((selectedRow) => {
      productSelectedRows.value.forEach(selectedRow => {
      const index = productData.value.findIndex(
        (product) => product.id === selectedRow.id
          product => product.id === selectedRow.id
      );
      if (index !== -1) {
        productData.value.splice(index, 1);
@@ -673,7 +665,7 @@
  } else {
    let ids = [];
    if (productSelectedRows.value.length > 0) {
      ids = productSelectedRows.value.map((item) => item.id);
        ids = productSelectedRows.value.map(item => item.id);
    }
    ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "导出", {
      confirmButtonText: "确认",
@@ -681,11 +673,11 @@
      type: "warning",
    })
      .then(() => {
        delProduct(ids).then((res) => {
          delProduct(ids).then(res => {
          proxy.$modal.msgSuccess("删除成功");
          closeProductDia();
          getSalesLedgerWithProducts({ id: currentId.value, type: 2 }).then(
            (res) => {
              res => {
              productData.value = res.productData;
            }
          );
@@ -702,34 +694,46 @@
  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('审批成功');
  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('已取消审批');
      .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('审批成功');
  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('已取消审批');
      .catch(() => {
        proxy.$modal.msg("已取消审批");
  });
};
@@ -752,12 +756,14 @@
  let ids = [];
  if (selectedRows.value.length > 0) {
        // æ£€æŸ¥æ˜¯å¦æœ‰ä»–人维护的数据
        const unauthorizedData = selectedRows.value.filter(item => item.recorderName !== userStore.nickName);
      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);
      ids = selectedRows.value.map(item => item.id);
  } else {
    proxy.$modal.msgWarning("请选择数据");
    return;
@@ -768,7 +774,7 @@
    type: "warning",
  })
    .then(() => {
      delPurchase(ids).then((res) => {
        delPurchase(ids).then(res => {
        proxy.$modal.msgSuccess("删除成功");
        getList();
      });
@@ -803,47 +809,59 @@
      );
  }
};
const reverseMathNum = (field) => {
  const reverseMathNum = field => {
    if (!productForm.value.taxRate) {
        proxy.$modal.msgWarning("请先选择税率");
        return;
    }
  const taxRate = Number(productForm.value.taxRate);
  if (!taxRate) return;
  if (field === 'taxInclusiveTotalPrice') {
    if (field === "taxInclusiveTotalPrice") {
    // å·²çŸ¥å«ç¨Žæ€»ä»·å’Œæ•°é‡ï¼Œåç®—含税单价
    if (productForm.value.quantity) {
      productForm.value.taxInclusiveUnitPrice =
        (Number(productForm.value.taxInclusiveTotalPrice) / Number(productForm.value.quantity)).toFixed(2);
        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.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.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);
      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);
        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.quantity = (
          Number(productForm.value.taxInclusiveTotalPrice) /
          Number(productForm.value.taxInclusiveUnitPrice)
        ).toFixed(2);
    }
  }
};
// é”€å”®åˆåŒé€‰æ‹©æ”¹å˜æ–¹æ³•
const salesLedgerChange = async (row) => {
  const salesLedgerChange = async row => {
  console.log("row", row);
  var index = salesContractList.value.findIndex((item) => item.id == row);
    var index = salesContractList.value.findIndex(item => item.id == row);
  console.log("index", index);
  if (index > -1) {
    form.value.projectName = salesContractList.value[index].projectName;
@@ -861,12 +879,12 @@
};
// æ˜¾ç¤ºäºŒç»´ç 
const showQRCode = async (row) => {
  const showQRCode = async row => {
  try {
    // æž„建二维码内容,只包含采购合同号(纯文本)
    const qrContent = row.purchaseContractNumber || '';
      const qrContent = row.purchaseContractNumber || "";
    // æ£€æŸ¥å†…容是否为空
    if (!qrContent || qrContent.trim() === '') {
      if (!qrContent || qrContent.trim() === "") {
      proxy.$modal.msgWarning("该行没有采购合同号,无法生成二维码");
      return;
    }
@@ -874,13 +892,13 @@
      width: 200,
      margin: 2,
      color: {
        dark: '#000000',
        light: '#FFFFFF'
      }
          dark: "#000000",
          light: "#FFFFFF",
        },
    });
    qrCodeDialogVisible.value = true;
  } catch (error) {
    console.error('生成二维码失败:', error);
      console.error("生成二维码失败:", error);
    proxy.$modal.msgError("生成二维码失败:" + error.message);
  }
};
@@ -892,7 +910,7 @@
    return;
  }
  
  const a = document.createElement('a');
    const a = document.createElement("a");
  a.href = qrCodeUrl.value;
  a.download = `采购合同号二维码_${new Date().getTime()}.png`;
  document.body.appendChild(a);
@@ -914,8 +932,12 @@
  scanRemark: "",
});
const scanAddRules = {
  purchaseContractNumber: [{ required: true, message: "请输入采购合同号", trigger: "blur" }],
  supplierName: [{ required: true, message: "请输入供应商名称", trigger: "blur" }],
    purchaseContractNumber: [
      { required: true, message: "请输入采购合同号", trigger: "blur" },
    ],
    supplierName: [
      { required: true, message: "请输入供应商名称", trigger: "blur" },
    ],
  projectName: [{ required: true, message: "请输入项目名称", trigger: "blur" }],
};
@@ -949,12 +971,12 @@
};
// è§£æžæ‰«ç å†…容(模拟解析二维码数据)
const parseScanContent = (content) => {
  const parseScanContent = content => {
  if (!content) return;
  
  // æ¨¡æ‹Ÿè§£æžäºŒç»´ç å†…容,这里可以根据实际需求调整解析逻辑
  // å‡è®¾æ‰«ç å†…容格式为:合同号|供应商|项目|金额|付款方式
  const parts = content.split('|');
    const parts = content.split("|");
  if (parts.length >= 3) {
    scanAddForm.purchaseContractNumber = parts[0] || "";
    scanAddForm.supplierName = parts[1] || "";
@@ -972,7 +994,7 @@
// æäº¤æ‰«ç æ–°å¢ž
const submitScanAdd = () => {
  proxy.$refs["scanAddFormRef"].validate((valid) => {
    proxy.$refs["scanAddFormRef"].validate(valid => {
    if (valid) {
      // æž„建新增数据
      const newData = {
@@ -984,7 +1006,7 @@
        recorderName: scanAddForm.recorderName,
        entryDate: getCurrentDate(),
        remark: scanAddForm.scanRemark,
        type: 2
          type: 2,
      };
      
      // æ¨¡æ‹Ÿæ–°å¢žæˆåŠŸ
@@ -998,7 +1020,7 @@
};
// æ‰“开扫码登记对话框
const openScanDialog = (row) => {
  const openScanDialog = row => {
  scanForm.purchaseContractNumber = row.purchaseContractNumber;
  scanForm.supplierName = row.supplierName;
  scanForm.projectName = row.projectName;
@@ -1018,7 +1040,7 @@
// æäº¤æ‰«ç ç™»è®°
const submitScan = () => {
  proxy.$refs["scanFormRef"].validate((valid) => {
    proxy.$refs["scanFormRef"].validate(valid => {
    if (valid) {
      // æ·»åŠ æ‰«ç è®°å½•
      scanRecords.value.push({
@@ -1048,7 +1070,7 @@
// æ·»åŠ è¡Œç±»åæ–¹æ³•
const tableRowClassName = ({ row }) => {
  return row.isInvalid ? 'invalid-row' : '';
    return row.isInvalid ? "invalid-row" : "";
};
onMounted(() => {
src/views/collaborativeApproval/rpaManagement/index.vue
@@ -80,8 +80,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="submitForm">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
src/views/collaborativeApproval/rulesRegulationsManagement/index.vue
@@ -126,9 +126,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button type="primary" @click="submitRegulation">发布制度</el-button>
          <el-button @click="showRegulationDialog = false">取消</el-button>
          <el-button type="primary"
                     @click="submitRegulation">发布制度</el-button>
        </span>
      </template>
    </el-dialog>
@@ -213,14 +212,7 @@
        </el-table-column>
      </el-table>
    </el-dialog>
    <FileListDialog ref="fileListDialogRef"
                    v-model="fileDialogVisible"
                    :show-upload-button="true"
                    :show-delete-button="true"
                    :delete-method="handleAttachmentDelete"
                    :rules-regulations-management-id="currentFileRuleId"
                    :name-column-label="'附件名称'"
                    @upload="handleAttachmentUpload"/>
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" record-type="rules_regulations_management" :record-id="recordId"  />
  </div>
</template>
@@ -236,7 +228,7 @@
    addReadingStatus,
    updateReadingStatus,
  } from "@/api/collaborativeApproval/sealManagement.js";
  import FileListDialog from "@/components/Dialog/FileListDialog.vue";
  const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
  import {
    listRuleFiles,
    delRuleFile,
@@ -255,14 +247,7 @@
    total: 0,
  });
  // é™„件弹窗
  const fileDialogVisible = ref(false);
  const fileListDialogRef = ref(null);
  const currentFileRuleId = ref(null);
  const filePage = reactive({
    current: 1,
    size: 1000,
    total: 0,
  });
  // è§„章制度相关
  const showRegulationDialog = ref(false);
  const showRegulationDetailDialog = ref(false);
@@ -341,10 +326,10 @@
      fixed: "right",
      align: "center",
      operation: [
        { name: "查看", clickFun: (row) => viewRegulation(row) },
        { name: "编辑", clickFun: (row) => handleEdit(row) },
        { name: "废弃", clickFun: (row) => repealEdit(row) },
        { name: "版本历史", clickFun: (row) => viewVersionHistory(row) },
        { name: "详情", clickFun: (row) => viewRegulation(row) },
        { name: "附件", clickFun: (row) => openFileDialog(row) },
      ],
    },
@@ -565,63 +550,15 @@
    );
  };
  // é™„件:查询
  const fetchRuleFiles = async rulesRegulationsManagementId => {
    const params = {
      current: filePage.current,
      size: filePage.size,
      rulesRegulationsManagementId,
    };
    const res = await listRuleFiles(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 => {
    currentFileRuleId.value = row.id;
    fileDialogVisible.value = true;
    await fetchRuleFiles(row.id);
  };
  const recordId =ref(0)
  const fileDialogVisible = ref(false)
  // åˆ·æ–°é™„件列表
  const refreshFileList = async () => {
    if (!currentFileRuleId.value) return;
    await fetchRuleFiles(currentFileRuleId.value);
  };
  // ä¸Šä¼ é™„件(由子组件触发)
  const handleAttachmentUpload = async filePayload => {
    if (!currentFileRuleId.value) return;
    const payload = {
      name: filePayload?.fileName || filePayload?.name,
      url: filePayload?.fileUrl || filePayload?.url,
      rulesRegulationsManagementId: currentFileRuleId.value,
    };
    await addRuleFile(payload);
    ElMessage.success("文件上传成功");
    await refreshFileList();
  };
  // åˆ é™¤é™„ä»¶
  const handleAttachmentDelete = async row => {
    if (!row?.id) return false;
    try {
      await ElMessageBox.confirm("确认删除该附件?", "提示", { type: "warning" });
    } catch {
      return false;
  // æ‰“开附件弹框
  const openFileDialog = async (row) => {
    recordId.value = row.id
    fileDialogVisible.value = true
    }
    await delRuleFile([row.id]);
    ElMessage.success("删除成功");
    await refreshFileList();
  };
  // èŽ·å–è§„ç« åˆ¶åº¦åˆ—è¡¨æ•°æ®
  const getRegulationList = async () => {
@@ -687,8 +624,6 @@
  }
  .dialog-footer {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    text-align: center;
  }
</style>
src/views/collaborativeApproval/sealManagement/index.vue
@@ -236,7 +236,6 @@
    fixed: 'right',
    align: 'center',
    operation: [
      { name: '查看', clickFun: (row) => viewSealDetail(row) },
      {
        name: '审批',
        clickFun: (row) => approveSeal(row),
@@ -246,7 +245,8 @@
        name: '拒绝',
        clickFun: (row) => rejectSeal(row),
        showHide: (row) => row.status === 'pending'
      }
      },
            { name: '详情', clickFun: (row) => viewSealDetail(row) }
    ]
  }
])
src/views/customerService/afterSalesHandling/index.vue
@@ -1,6 +1,6 @@
<template>
    <div class="app-container">
        <div class="search-wrapper">
        <div class="search-wrapper mb20">
      <el-form
          :model="searchForm"
          class="demo-form-inline"
@@ -102,33 +102,19 @@
            ></PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
        <FileListDialog
            ref="fileListRef"
            v-model="fileListDialogVisible"
            title="售后附件"
            :show-upload-button="true"
            :show-delete-button="true"
            :upload-method="handleFileUpload"
            :delete-method="handleFileDelete"
        />
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" record-type="after_sales_service" :record-id="recordId"  />
    </div>
</template>
<script setup>
import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick, defineAsyncComponent} from "vue";
import FormDia from "@/views/customerService/afterSalesHandling/components/formDia.vue";
import FileListDialog from "@/components/Dialog/FileListDialog.vue";
import { ElMessageBox } from "element-plus";
import request from "@/utils/request";
import { getToken } from "@/utils/auth";
import {
    afterSalesServiceListPage,
    afterSalesServiceFileListPage,
    afterSalesServiceFileDel,
} from "@/api/customerService/index.js";
import useUserStore from "@/store/modules/user.js";
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
const data = reactive({
    searchForm: {
@@ -303,144 +289,15 @@
  })
}
// æ‰“开附件弹窗
const recordId =ref(0)
const fileDialogVisible = ref(false)
// æ‰“开附件弹框
const openFilesFormDia = async (row) => {
    currentFileRow.value = row
    try {
        const res = await afterSalesServiceFileListPage({
            afterSalesServiceId: row.id,
            current: 1,
            size: 100,
        })
        if (res.code === 200 && fileListRef.value) {
            const fileList = (res.data?.records || []).map((item) => ({
                name: item.name || item.fileName,
                url: item.url || item.fileUrl,
                id: item.id,
                ...item,
            }))
            fileListRef.value.open(fileList)
            fileListDialogVisible.value = true
        } else {
            fileListRef.value?.open([])
            fileListDialogVisible.value = true
        }
    } catch (error) {
        proxy.$modal.msgError("获取附件列表失败")
        fileListRef.value?.open([])
        fileListDialogVisible.value = true
    }
}
// ä¸Šä¼ é™„ä»¶
const handleFileUpload = async () => {
    if (!currentFileRow.value) {
        proxy.$modal.msgWarning("请先选择数据")
        return
    }
    return new Promise((resolve) => {
        const input = document.createElement("input")
        input.type = "file"
        input.style.display = "none"
        input.onchange = async (e) => {
            const file = e.target.files[0]
            if (!file) {
                resolve(null)
                return
            }
            try {
                const formData = new FormData()
                formData.append("file", file)
                formData.append("id", currentFileRow.value.id)
                const uploadRes = await request({
                    url: "/afterSalesService/file/upload",
                    method: "post",
                    data: formData,
                    headers: {
                        "Content-Type": "multipart/form-data",
                        Authorization: `Bearer ${getToken()}`,
                    },
                })
                if (uploadRes.code === 200) {
                    proxy.$modal.msgSuccess("文件上传成功")
                    // é‡æ–°èŽ·å–æ–‡ä»¶åˆ—è¡¨
                    const listRes = await afterSalesServiceFileListPage({
                        afterSalesServiceId: currentFileRow.value.id,
                        current: 1,
                        size: 100,
                    })
                    if (listRes.code === 200 && fileListRef.value) {
                        const fileList = (listRes.data?.records || []).map((item) => ({
                            name: item.fileName,
                            url: item.fileUrl,
                            id: item.id,
                            ...item,
                        }))
                        fileListRef.value.setList(fileList)
                    }
                    resolve({ name: file.name, url: "", id: null })
                } else {
                    proxy.$modal.msgError(uploadRes.msg || "文件上传失败")
                    resolve(null)
                }
            } catch (err) {
                proxy.$modal.msgError("文件上传失败")
                resolve(null)
            } finally {
                document.body.removeChild(input)
            }
        }
        document.body.appendChild(input)
        input.click()
    })
}
// åˆ é™¤é™„ä»¶
const handleFileDelete = async (row) => {
    try {
        // æ·»åŠ ç¡®è®¤å¯¹è¯æ¡†
        const confirmResult = await ElMessageBox.confirm(
            '确定要删除这个附件吗?',
            '删除确认',
            {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
            }
        )
        if (confirmResult === 'confirm') {
            const res = await afterSalesServiceFileDel(row.id)
            if (res.code === 200) {
                proxy.$modal.msgSuccess("删除成功")
                if (currentFileRow.value && fileListRef.value) {
                    const listRes = await afterSalesServiceFileListPage({
                        afterSalesServiceId: currentFileRow.value.id,
                        current: 1,
                        size: 100,
                    })
                    if (listRes.code === 200) {
                        const fileList = (listRes.data?.records || []).map((item) => ({
                            name: item.fileName,
                            url: item.fileUrl,
                            id: item.id,
                            ...item,
                        }))
                        fileListRef.value.setList(fileList)
                    }
                }
            } else {
                proxy.$modal.msgError(res.msg || "删除失败")
                return false
            }
        }
    } catch (error) {
        // å¦‚果用户取消删除,不显示错误信息
        if (error !== 'cancel') {
            proxy.$modal.msgError("删除失败")
        }
        return false
    }
  recordId.value = row.id
  fileDialogVisible.value = true
}
// æŸ¥è¯¢åˆ—表
src/views/customerService/expiryAfterSales/index.vue
@@ -1,6 +1,6 @@
<template>
    <div class="app-container">
        <div class="search_form">
        <div class="search_form mb20">
            <div>
                <span class="search_title">临期日期:</span>
                <el-date-picker
@@ -34,11 +34,12 @@
                >重置</el-button
                >
            </div>
        </div>
        <div class="table_actions" style="margin-bottom: 10px;">
            <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"
src/views/customerService/feedbackRegistration/components/formDia.vue
@@ -79,9 +79,9 @@
              </el-form-item>
            </el-col>
            <el-col :span="4">
              <el-form-item label="问题描述:" prop="disRes">
              <el-form-item label="问题描述:" prop="proDesc">
                <el-input
                    v-model="form.disRes"
                    v-model="form.proDesc"
                    placeholder="请输入问题描述"
                />
              </el-form-item>
@@ -106,6 +106,11 @@
                :column="tableColumn"
                :tableData="tableData"
            >
              <template #approveStatus="{ row }">
                <el-tag :type="getApproveStatusType(row)" size="small">
                  {{ getApproveStatusText(row) }}
                </el-tag>
              </template>
              <template #shippingStatus="{ row }">
                <el-tag :type="getShippingStatusType(row)" size="small">
                  {{ getShippingStatusText(row) }}
@@ -155,7 +160,7 @@
    productModelIds: "",
    customerId: null,
    salesContractNo: "",
    disRes: "",
    proDesc: "",
    customerName: ""
    },
    rules: {
@@ -207,6 +212,7 @@
    taxInclusiveUnitPrice: row?.taxInclusiveUnitPrice ?? 0,
    taxInclusiveTotalPrice: row?.taxInclusiveTotalPrice ?? 0,
    taxExclusiveTotalPrice: row?.taxExclusiveTotalPrice ?? 0,
    noQuantity: row?.noQuantity ?? 0,
  }
}
@@ -219,9 +225,8 @@
    prop: "approveStatus",
    width: 100,
    align: "center",
    dataType: "tag",
    formatData: (v) => (v === 1 ? "充足" : "不足"),
    formatType: (v) => (v === 1 ? "success" : "danger"),
    dataType: "slot",
    slot: "approveStatus",
  },
  {
    label: "发货状态",
@@ -304,9 +309,15 @@
})
const customerNameChange = (val) => {
  form.value.salesContractNo = "";
  form.value.salesLedgerId = null;
  tableData.value = [];
  associatedSalesOrderNumberOptions.value = [];
  const opt = customerNameOptions.value.find(item => item.value === val);
  if (opt) {
    form.value.customerId = opt.id;
  } else {
    form.value.customerId = null;
  }
  getSalesLedger({
    customerName: form.value.customerName
@@ -320,6 +331,22 @@
      }))
    }
  })
}
const getApproveStatusText = (row) => {
  if (!row) return '不足'
  if (row.approveStatus === 1 && (!row.shippingDate || !row.shippingCarNumber)) {
    return '充足'
  }
  if (row.approveStatus === 0 && (row.shippingDate || row.shippingCarNumber)) {
    return '已出库'
  }
  return '不足'
}
const getApproveStatusType = (row) => {
  const statusText = getApproveStatusText(row)
  return statusText === '不足' ? 'danger' : 'success'
}
const getShippingStatusText = (row) => {
@@ -365,7 +392,11 @@
// æ‰“开弹框
const openDialog =async (type, row) => {
  // è¯·æ±‚多个接口,获取数据
  let res = await getAllCustomerList();
  let res = await getAllCustomerList({
    current: 1,
  size: 1000,
  total: 0,
  });
  if(res.records){
    customerNameOptions.value = res.records.map(item => ({
      label: item.customerName,
src/views/customerService/feedbackRegistration/index.vue
@@ -255,7 +255,7 @@
  },
  {
    label: "问题描述",
    prop: "disRes",
    prop: "proDesc",
    width:300,
  },
  {
@@ -267,7 +267,6 @@
  {
    dataType: "action",
    label: "操作",
    align: "center",
    fixed: 'right',
    operation: [
      {
@@ -404,15 +403,19 @@
      });
};
const getStatsCountByStatus = (list, status) => {
  if (!Array.isArray(list)) return 0;
  return list.find((item) => item?.status === status)?.count || 0;
};
  // èŽ·å–ç»Ÿè®¡æ•°æ®å¹¶åˆ·æ–°é¡¶éƒ¨å¡ç‰‡
  const getSalesLedgerDetails = () => {
    getSalesLedgerDetail({}).then((res) => {
      if (res.code === 200) {
        statsList.value[0].count = res.data.filter((item) => item.status === 3)[0].count;
        statsList.value[1].count = res.data.filter((item) => item.status === 2)[0].count;
        statsList.value[2].count = res.data.filter((item) => item.status === 1)[0].count;
        // });
        const statsData = Array.isArray(res.data) ? res.data : [];
        statsList.value[0].count = getStatsCountByStatus(statsData, 3);
        statsList.value[1].count = getStatsCountByStatus(statsData, 2);
        statsList.value[2].count = getStatsCountByStatus(statsData, 1);
      }
    });
  }
@@ -491,7 +494,6 @@
.table_list {
  height: calc(100vh - 380px);
  min-height: 360px;
  background: #fff;
  margin-top: 20px;
  display: flex;
src/views/energyManagement/dynamicEnergySaving/index.vue
@@ -158,13 +158,11 @@
        <el-table-column label="操作">
          <template #default="scope">
            <el-button 
              size="small"
              @click="updateModel(scope.row)"
            >
              æ›´æ–°æ¨¡åž‹
            </el-button>
            <el-button 
              size="small"
              type="danger" 
              @click="deleteModel(scope.row)"
            >
src/views/equipmentManagement/brand/index.vue
@@ -60,8 +60,8 @@
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="visible = false">取消</el-button>
        <el-button type="primary" @click="handleSubmit">确定</el-button>
        <el-button @click="visible = false">取消</el-button>
      </template>
    </el-dialog>
  </div>
src/views/equipmentManagement/calibration/index.vue
@@ -1,6 +1,6 @@
<template>
    <div class="app-container">
        <div class="search_form">
        <div class="search_form mb20">
            <div>
                <span class="search_title">检定日期:</span>
                <el-date-picker
src/views/equipmentManagement/defectManagement/index.vue
@@ -65,8 +65,8 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="showRegisterDialog = false">取消</el-button>
          <el-button type="primary" @click="submitDefectForm">确定</el-button>
          <el-button @click="showRegisterDialog = false">取消</el-button>
        </span>
      </template>
    </el-dialog>
src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -91,8 +91,8 @@
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="cancel">取消</el-button>
          <el-button type="primary" @click="submitForm">保存</el-button>
          <el-button @click="cancel">取消</el-button>
        </div>
      </template>
    </el-dialog>
src/views/equipmentManagement/inspectionManagement/components/viewFiles.vue
@@ -134,40 +134,6 @@
const currentMediaIndex = ref(0);
const mediaList = ref([]); // å­˜å‚¨å½“前要查看的媒体列表(含图片和视频对象)
const mediaType = ref('image'); // image | video
const javaApi = proxy.javaApi;
// å¤„理 URL:将 Windows è·¯å¾„转换为可访问的 URL
function processFileUrl(fileUrl) {
  if (!fileUrl) return '';
  // å¦‚æžœ URL æ˜¯ Windows è·¯å¾„格式(包含反斜杠),需要转换
  if (fileUrl && fileUrl.indexOf('\\') > -1) {
    // æŸ¥æ‰¾ uploads å…³é”®å­—的位置,从那里开始提取相对路径
    const uploadsIndex = fileUrl.toLowerCase().indexOf('uploads');
    if (uploadsIndex > -1) {
      // ä»Ž uploads å¼€å§‹æå–路径,并将反斜杠替换为正斜杠
      const relativePath = fileUrl.substring(uploadsIndex).replace(/\\/g, '/');
      fileUrl = '/' + relativePath;
    } else {
      // å¦‚果没有找到 uploads,提取最后一个目录和文件名
      const parts = fileUrl.split('\\');
      const fileName = parts[parts.length - 1];
      fileUrl = '/uploads/' + fileName;
    }
  }
  // ç¡®ä¿æ‰€æœ‰éž http å¼€å¤´çš„ URL éƒ½æ‹¼æŽ¥ baseUrl
  if (fileUrl && !fileUrl.startsWith('http')) {
    // ç¡®ä¿è·¯å¾„以 / å¼€å¤´
    if (!fileUrl.startsWith('/')) {
      fileUrl = '/' + fileUrl;
    }
    // æ‹¼æŽ¥ baseUrl
    fileUrl = javaApi + fileUrl;
  }
  return fileUrl;
}
// å¤„理每一类数据:分离图片和视频
function processItems(items) {
@@ -180,24 +146,18 @@
  }
  
  items.forEach(item => {
    if (!item || !item.url) return;
    if (!item || !item.previewURL || !item.contentType) return;
    
    // å¤„理文件 URL
    const fileUrl = processFileUrl(item.url);
    const fileUrl = item.previewURL;
    const contentType = String(item.contentType).toLowerCase();
    
    // æ ¹æ®æ–‡ä»¶æ‰©å±•名判断是图片还是视频
    const urlLower = fileUrl.toLowerCase();
    if (urlLower.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/)) {
    // æ ¹æ® contentType åˆ¤æ–­æ˜¯å›¾ç‰‡è¿˜æ˜¯è§†é¢‘
    if (contentType.startsWith('image/')) {
      images.push(fileUrl);
    } else if (urlLower.match(/\.(mp4|avi|mov|wmv|flv|mkv|webm)$/)) {
    } else if (contentType.startsWith('video/')) {
      videos.push(fileUrl);
    } else if (item.contentType) {
      // å¦‚果有 contentType,使用 contentType åˆ¤æ–­
      if (item.contentType.startsWith('image/')) {
        images.push(fileUrl);
      } else if (item.contentType.startsWith('video/')) {
        videos.push(fileUrl);
      }
    }
  });
  
@@ -207,10 +167,9 @@
// æ‰“开弹窗并加载数据
const openDialog = async (row) => {
  // ä½¿ç”¨æ­£ç¡®çš„字段名:commonFileListBefore, commonFileListAfter
  // productionIssues å¯èƒ½ä¸å­˜åœ¨ï¼Œä½¿ç”¨ç©ºæ•°ç»„
  const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBefore || []);
  const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfter || []);
  const { images: issueImgs, videos: issueVids } = processItems(row.productionIssues || []);
  const { images: beforeImgs, videos: beforeVids } = processItems(row.commonFileListBeforeVO || []);
  const { images: afterImgs, videos: afterVids } = processItems(row.commonFileListAfterVO || []);
  const { images: issueImgs, videos: issueVids } = processItems(row.commonFileListVO || []);
  
  beforeProductionImgs.value = beforeImgs;
  beforeProductionVideos.value = beforeVids;
src/views/equipmentManagement/ledger/Form.vue
@@ -100,22 +100,18 @@
      </el-col>
      <el-col :span="12">
        <el-form-item label="税率(%)" prop="taxRate">
          <!-- <el-input
            v-model="form.taxRate"
            placeholder="请输入税率"
            type="number"
          >
            <template #append> % </template>
          </el-input> -->
          <el-select
            v-model="form.taxRate"
            placeholder="请选择"
            clearable
            @change="mathNum"
          >
            <el-option label="1" :value="1" />
            <el-option label="6" :value="6" />
            <el-option label="13" :value="13" />
            <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>
@@ -174,7 +170,10 @@
  calculateTaxExclusiveTotalPrice,
} from "@/utils/summarizeTable";
import { ElMessage } from "element-plus";
import {ref} from "vue";
import {ref, getCurrentInstance} from "vue";
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
defineOptions({
  name: "设备台账表单",
src/views/equipmentManagement/ledger/index.vue
@@ -4,7 +4,7 @@
      <el-form-item label="设备名称">
        <el-input
          v-model="filters.deviceName"
          style="width: 240px"
          style="width: 200px"
          placeholder="请输入设备名称"
          clearable
          @change="getTableData"
@@ -13,7 +13,7 @@
      <el-form-item label="规格型号">
        <el-input
            v-model="filters.deviceModel"
            style="width: 240px"
            style="width: 200px"
            placeholder="请输入规格型号"
            clearable
            @change="getTableData"
@@ -22,7 +22,7 @@
      <el-form-item label="供应商">
        <el-input
            v-model="filters.supplierName"
            style="width: 240px"
            style="width: 200px"
            placeholder="请输入供应商"
            clearable
            @change="getTableData"
@@ -42,6 +42,7 @@
        <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"
@@ -77,6 +78,37 @@
        </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>仅允许导入xls、xlsx格式文件。</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>
@@ -84,12 +116,13 @@
import { usePaginationApi } from "@/hooks/usePaginationApi";
// import { Search } from "@element-plus/icons-vue";
import { getLedgerPage, delLedger } from "@/api/equipmentManagement/ledger";
import { onMounted, getCurrentInstance } from "vue";
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";
import { ref } from "vue";
defineOptions({
  name: "设备台账",
@@ -102,6 +135,21 @@
const qrDialogVisible = ref(false);
const qrCodeUrl = ref("");
const qrRowData = ref(null);
// å¯¼å…¥ç›¸å…³
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,
@@ -262,6 +310,36 @@
  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();
});
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
@@ -228,39 +228,6 @@
    form.value.entryDate = getCurrentDate();
}
// ä¸Šä¼ å‰æ ¡æ£€
function handleBeforeUpload(file) {
    proxy.$modal.loading("正在上传文件,请稍候...");
    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(file.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("删除成功");
        });
    }
}
// å¤„理有效日期输入,只允许正整数
const handleValidInput = (value) => {
    if (value === '' || value === null || value === undefined) {
src/views/equipmentManagement/measurementEquipment/index.vue
@@ -1,6 +1,6 @@
<template>
    <div class="app-container">
        <div class="search_form">
        <div class="search_form mb20">
            <div>
                <span class="search_title">录入日期:</span>
                <el-date-picker
src/views/equipmentManagement/repair/Modal/MaintainModal.vue
@@ -32,23 +32,61 @@
          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();
// ä¿å­˜æŠ¥ä¿®è®°å½•çš„id
const repairId = ref();
@@ -61,6 +99,16 @@
  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) => {
@@ -71,16 +119,59 @@
      ? 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 {
    const { code } = await addMaintain({ id: repairId.value, ...form });
    // é¢†ç”¨æ•°é‡æ ¡éªŒ
    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 {
@@ -88,13 +179,34 @@
  }
};
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;
};
@@ -103,6 +215,7 @@
  visible.value = true;
  await nextTick();
  setForm(row);
  fetchSparePartOptions()
};
defineExpose({
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -48,6 +48,11 @@
            <el-input v-model="form.repairName" placeholder="请输入报修人" />
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="项目">
            <el-input v-model="form.machineryCategory" placeholder="请输入项目" />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="id">
        <el-col :span="12">
@@ -72,12 +77,20 @@
          </el-form-item>
        </el-col>
      </el-row>
      <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 FileUpload from "@/components/AttachmentUpload/file/index.vue";
import {
  addRepair,
  editRepair,
@@ -101,6 +114,7 @@
const userStore = useUserStore();
const deviceOptions = ref([]);
const fileList = ref([]);
const loadDeviceName = async () => {
  const { data } = await getDeviceLedger();
@@ -115,6 +129,8 @@
  repairName: userStore.nickName, // æŠ¥ä¿®äºº
  remark: undefined, // æ•…障现象
  status: 0, // æŠ¥ä¿®çŠ¶æ€
  machineryCategory: undefined,
  storageBlobDTOs: [],
});
const setDeviceModel = (deviceId) => {
@@ -130,6 +146,8 @@
  form.repairName = data.repairName;
  form.remark = data.remark;
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.storageBlobDTOs = data.storageBlobVOs || [];
};
const sendForm = async () => {
@@ -161,6 +179,7 @@
const openAdd = async () => {
  id.value = undefined;
  visible.value = true;
  fileList.value = [];
  await nextTick();
  await loadDeviceName();
};
src/views/equipmentManagement/repair/index.vue
@@ -127,22 +127,31 @@
          >
            åˆ é™¤
          </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"/>
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" :record-type="'device_repair'" :record-id="recordId"  />
  </div>
</template>
<script setup>
import { onMounted, getCurrentInstance, computed } from "vue";
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";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
defineOptions({
  name: "设备报修",
@@ -186,6 +195,11 @@
        label: "规格型号",
        align: "center",
        prop: "deviceModel",
      },
      {
        label: "项目",
        align: "center",
        prop: "machineryCategory",
      },
      {
        label: "报修日期",
@@ -253,6 +267,15 @@
  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;
src/views/equipmentManagement/spareParts/index.vue
@@ -1,5 +1,7 @@
<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="备件名称">
@@ -19,7 +21,7 @@
                <el-button type="primary" @click="addCategory" >新增</el-button>
            </div>
        </div>
                <div class="table_list">
    <PIMTable
        rowKey="id"
        :column="columns"
@@ -33,6 +35,7 @@
        <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">
@@ -85,23 +88,59 @@
      </el-form>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false" :disabled="formLoading">取消</el-button>
          <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, watch } from 'vue';
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
@@ -126,6 +165,35 @@
  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: "设备名称",
@@ -267,6 +335,48 @@
    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 = () => {
@@ -430,7 +540,6 @@
  margin-top: 20px;
  display: flex;
  justify-content: flex-end;
  padding: 16px 0;
}
.el-table__header-wrapper th {
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue
@@ -38,6 +38,41 @@
          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>
  </FormDialog>
</template>
@@ -49,6 +84,8 @@
import dayjs from "dayjs";
import useUserStore from "@/store/modules/user";
import { ElMessage } from "element-plus";
import {computed, ref} from "vue";
import {getSparePartsList} from "@/api/equipmentManagement/spareParts.js";
defineOptions({
  name: "保养模态框",
@@ -67,6 +104,17 @@
  maintenanceActuallyTime: undefined, // å®žé™…保养日期
  maintenanceResult: 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) => {
@@ -78,6 +126,19 @@
      : 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 = [];
  }
};
/**
@@ -86,11 +147,41 @@
const sendForm = async () => {
  loading.value = true;
  try {
    const { code } = await addMaintenance({ id: planId.value, ...form });
    // é¢†ç”¨æ•°é‡æ ¡éªŒ
    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 {
@@ -98,13 +189,34 @@
  }
};
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;
};
@@ -112,6 +224,7 @@
  planId.value = id; // ä¿å­˜è®¡åˆ’保养记录的id
  visible.value = true;
  await nextTick();
  fetchSparePartOptions()
  setForm(row);
};
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -32,6 +32,12 @@
          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"
@@ -67,6 +73,13 @@
          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>
@@ -84,6 +97,7 @@
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: "设备保养新增计划",
@@ -108,6 +122,8 @@
  maintenancePlanTime: undefined, // è®¡åˆ’保养日期
  createUser: undefined, // å½•入人
  status: 0, //保修状态
  machineryCategory: undefined,
  storageBlobDTOs: [],
});
const setDeviceModel = (deviceId) => {
@@ -125,9 +141,13 @@
  form.deviceModel = data.deviceModel;
  form.createUser = Number(data.createUser);
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  if (data.maintenancePlanTime) {
  form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
    "YYYY-MM-DD HH:mm:ss"
  );
  }
  form.storageBlobDTOs = data.storageBlobVOs || [];
};
// ç”¨æˆ·åˆ—表
src/views/equipmentManagement/upkeep/index.vue
@@ -1,51 +1,58 @@
<template>
  <div class="app-container">
    <el-tabs v-model="activeTab" @tab-change="handleTabChange">
    <el-tabs v-model="activeTab"
             @tab-change="handleTabChange">
      <!-- å®šæ—¶ä»»åŠ¡ç®¡ç†tab -->
      <el-tab-pane label="定时任务管理" name="scheduled">
      <el-tab-pane label="定时任务管理"
                   name="scheduled">
        <div class="search_form">
          <el-form :model="scheduledFilters" :inline="true">
          <el-form :model="scheduledFilters"
                   :inline="true">
            <el-form-item label="任务名称">
              <el-input
                  v-model="scheduledFilters.taskName"
              <el-input v-model="scheduledFilters.taskName"
                  style="width: 240px"
                  placeholder="请输入任务名称"
                  clearable
                  :prefix-icon="Search"
                  @change="getScheduledTableData"
              />
                        @change="getScheduledTableData" />
            </el-form-item>
            <el-form-item label="任务状态">
              <el-select v-model="scheduledFilters.status" placeholder="请选择任务状态" clearable style="width: 200px">
                <el-option label="启用" value="1" />
                <el-option label="停用" value="0" />
              <el-select v-model="scheduledFilters.status"
                         placeholder="请选择任务状态"
                         clearable
                         style="width: 200px">
                <el-option label="启用"
                           value="1" />
                <el-option label="停用"
                           value="0" />
              </el-select>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="getScheduledTableData">搜索</el-button>
              <el-button type="primary"
                         @click="getScheduledTableData">搜索</el-button>
              <el-button @click="resetScheduledFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1" size="large">定时任务管理</el-text>
            <el-text class="mx-1"
                     size="large">定时任务管理</el-text>
            <div>
              <el-button type="primary" icon="Plus" @click="addScheduledTask">
              <el-button type="primary"
                         icon="Plus"
                         @click="addScheduledTask">
                æ–°å¢žä»»åŠ¡
              </el-button>
              <el-button
                type="danger"
              <el-button type="danger"
                icon="Delete"
                :disabled="scheduledMultipleList.length <= 0"
                @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))"
              >
                         @click="delScheduledTaskByIds(scheduledMultipleList.map((item) => item.id))">
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
          <PIMTable
            rowKey="id"
          <PIMTable rowKey="id"
            isSelection
            :column="scheduledColumns"
            :tableData="scheduledDataList"
@@ -55,102 +62,93 @@
              total: scheduledPagination.total,
            }"
            @selection-change="handleScheduledSelectionChange"
            @pagination="changeScheduledPage"
          >
                    @pagination="changeScheduledPage">
            <template #statusRef="{ row }">
              <el-tag v-if="row.status === 1" type="success">启用</el-tag>
              <el-tag v-if="row.status === 0" type="danger">停用</el-tag>
              <el-tag v-if="row.status === 1"
                      type="success">启用</el-tag>
              <el-tag v-if="row.status === 0"
                      type="danger">停用</el-tag>
            </template>
            <template #operation="{ row }">
              <el-button
                type="primary"
              <el-button type="primary"
                link
                @click="editScheduledTask(row)"
              >
                         @click="editScheduledTask(row)">
                ç¼–辑
              </el-button>
              <el-button
                type="danger"
              <el-button type="danger"
                link
                @click="delScheduledTaskByIds(row.id)"
              >
                         @click="delScheduledTaskByIds(row.id)">
                åˆ é™¤
              </el-button>
            </template>
          </PIMTable>
        </div>
      </el-tab-pane>
      <!-- ä»»åŠ¡è®°å½•tab(原设备保养页面) -->
      <el-tab-pane label="任务记录" name="record">
      <el-tab-pane label="任务记录"
                   name="record">
        <div class="search_form">
          <el-form :model="filters" :inline="true">
          <el-form :model="filters"
                   :inline="true">
            <el-form-item label="设备名称">
              <el-input
                  v-model="filters.deviceName"
              <el-input v-model="filters.deviceName"
                  style="width: 240px"
                  placeholder="请输入设备名称"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData"
              />
                        @change="getTableData" />
            </el-form-item>
            <el-form-item label="计划保养日期">
              <el-date-picker
                  v-model="filters.maintenancePlanTime"
              <el-date-picker v-model="filters.maintenancePlanTime"
                  type="date"
                  placeholder="请选择计划保养日期"
                  size="default"
                  @change="(date) => handleDateChange(date,2)"
              />
                              @change="(date) => handleDateChange(date,2)" />
            </el-form-item>
            <el-form-item label="实际保养日期">
              <el-date-picker
                  v-model="filters.maintenanceActuallyTime"
              <el-date-picker v-model="filters.maintenanceActuallyTime"
                  type="date"
                  placeholder="请选择实际保养日期"
                  size="default"
                  @change="(date) => handleDateChange(date,1)"
              />
                              @change="(date) => handleDateChange(date,1)" />
            </el-form-item>
            <el-form-item label="实际保养人">
              <el-input
                  v-model="filters.maintenanceActuallyName"
              <el-input v-model="filters.maintenanceActuallyName"
                  style="width: 240px"
                  placeholder="请输入实际保养人"
                  clearable
                  :prefix-icon="Search"
                  @change="getTableData"
              />
                        @change="getTableData" />
            </el-form-item>
            <el-form-item>
              <el-button type="primary" @click="getTableData">搜索</el-button>
              <el-button type="primary"
                         @click="getTableData">搜索</el-button>
              <el-button @click="resetFilters">重置</el-button>
            </el-form-item>
          </el-form>
        </div>
        <div class="table_list">
          <div class="actions">
            <el-text class="mx-1" size="large">任务记录</el-text>
            <el-text class="mx-1"
                     size="large">任务记录</el-text>
            <div>
              <el-button type="success" icon="Van" @click="addPlan">
              <el-button type="success"
                         icon="Van"
                         @click="addPlan">
                æ–°å¢žè®¡åˆ’
              </el-button>
              <el-button @click="handleOut">
                å¯¼å‡º
              </el-button>
              <el-button
                type="danger"
              <el-button type="danger"
                icon="Delete"
                :disabled="multipleList.length <= 0 || hasFinishedStatus"
                @click="delRepairByIds(multipleList.map((item) => item.id))"
              >
                         @click="delRepairByIds(multipleList.map((item) => item.id))">
                æ‰¹é‡åˆ é™¤
              </el-button>
            </div>
          </div>
         <PIMTable
        rowKey="id"
          <PIMTable rowKey="id"
        isSelection
        :column="columns"
        :tableData="dataList"
@@ -160,15 +158,17 @@
          total: pagination.total,
        }"
        @selection-change="handleSelectionChange"
        @pagination="changePage"
      >
                    @pagination="changePage">
        <template #maintenanceResultRef="{ row }">
          <div>{{ row.maintenanceResult || '-' }}</div>
        </template>
        <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 === 0" type="warning">待保养</el-tag>
              <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 === 0"
                      type="warning">待保养</el-tag>
        </template>
        <template #operation="{ row }">
          <!-- è¿™ä¸ªåŠŸèƒ½è·Ÿæ–°å¢žä¿å…»åŠŸèƒ½ä¸€æ¨¡ä¸€æ ·ï¼Œæœ‰å•¥æ„ä¹‰ï¼Ÿ -->
@@ -179,35 +179,27 @@
          >
            æ–°å¢žä¿å…»
          </el-button> -->
          <el-button
            type="primary"
              <el-button type="primary"
            link
            :disabled="row.status === 1"
            @click="editPlan(row.id)"
          >
                         @click="editPlan(row.id)">
            ç¼–辑
          </el-button>
          <el-button
            type="success"
              <el-button type="success"
            link
            :disabled="row.status === 1"
            @click="addMaintain(row)"
          >
                         @click="addMaintain(row)">
            ä¿å…»
          </el-button>
          <el-button
            type="danger"
              <el-button type="danger"
            link
            :disabled="row.status === 1"
            @click="delRepairByIds(row.id)"
          >
                         @click="delRepairByIds(row.id)">
            åˆ é™¤
          </el-button>
          <el-button
            type="primary"
              <el-button type="primary"
            link
            @click="openFileDialog(row)"
          >
                         @click="openFileDialog(row)">
            é™„ä»¶
          </el-button>
        </template>
@@ -215,87 +207,90 @@
        </div>
      </el-tab-pane>
    </el-tabs>
    <PlanModal ref="planModalRef" @ok="getTableData" />
        <MaintenanceModal ref="maintainModalRef" @ok="getTableData" />
        <FormDia ref="formDiaRef" @closeDia="getScheduledTableData" />
    <FileListDialog
      ref="fileListDialogRef"
      v-model="fileDialogVisible"
      :show-upload-button="true"
      :show-delete-button="true"
      :delete-method="handleAttachmentDelete"
      :name-column-label="'附件名称'"
      :rulesRegulationsManagementId="currentMaintenanceTaskId"
      @upload="handleAttachmentUpload" />
    <PlanModal ref="planModalRef"
               @ok="getTableData" />
    <MaintenanceModal ref="maintainModalRef"
                      @ok="getTableData" />
    <FormDia ref="formDiaRef"
             @closeDia="getScheduledTableData" />
    <FileList v-if="fileDialogVisible"
              v-model:visible="fileDialogVisible"
              :record-type="'device_maintenance'"
              :record-id="currentMaintenanceTaskId" />
  </div>
</template>
<script setup>
import { ref, onMounted, reactive, getCurrentInstance, nextTick, computed } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import PlanModal from './Form/PlanModal.vue'
import MaintenanceModal from './Form/MaintenanceModal.vue'
import FormDia from './Form/formDia.vue'
import FileListDialog from '@/components/Dialog/FileListDialog.vue'
  import {
    ref,
    onMounted,
    reactive,
    getCurrentInstance,
    nextTick,
    computed,
    defineAsyncComponent,
  } from "vue";
  import { Search } from "@element-plus/icons-vue";
  import { ElMessage, ElMessageBox } from "element-plus";
  import PlanModal from "./Form/PlanModal.vue";
  import MaintenanceModal from "./Form/MaintenanceModal.vue";
  import FormDia from "./Form/formDia.vue";
import {
  getUpkeepPage,
  delUpkeep,
  deviceMaintenanceTaskList,
  deviceMaintenanceTaskDel,
} from '@/api/equipmentManagement/upkeep'
import {
  listMaintenanceTaskFiles,
  addMaintenanceTaskFile,
  delMaintenanceTaskFile,
} from '@/api/equipmentManagement/maintenanceTaskFile'
import dayjs from 'dayjs'
  } from "@/api/equipmentManagement/upkeep";
  import dayjs from "dayjs";
const { proxy } = getCurrentInstance()
  const { proxy } = getCurrentInstance();
  const FileList = defineAsyncComponent(() =>
    import("@/components/Dialog/FileList.vue")
  );
// Tab相关
const activeTab = ref('scheduled')
  const activeTab = ref("scheduled");
// è®¡åˆ’弹窗控制器
const planModalRef = ref()
  const planModalRef = ref();
// ä¿å…»å¼¹çª—控制器
const maintainModalRef = ref()
  const maintainModalRef = ref();
// å®šæ—¶ä»»åŠ¡å¼¹çª—æŽ§åˆ¶å™¨
const formDiaRef = ref()
  const formDiaRef = ref();
// é™„件弹窗
const fileListDialogRef = ref(null)
const fileDialogVisible = ref(false)
const currentMaintenanceTaskId = ref(null)
  const fileListDialogRef = ref(null);
  const fileDialogVisible = ref(false);
  const currentMaintenanceTaskId = ref(null);
// ä»»åŠ¡è®°å½•tab(原设备保养页面)相关变量
const filters = reactive({
  deviceName: '',
  maintenancePlanTime: '',
  maintenanceActuallyTime: '',
  maintenanceActuallyName: '',
})
    deviceName: "",
    maintenancePlanTime: "",
    maintenanceActuallyTime: "",
    maintenanceActuallyName: "",
  });
const dataList = ref([])
  const dataList = ref([]);
const pagination = ref({
  currentPage: 1,
  pageSize: 10,
  total: 0,
})
const multipleList = ref([])
  });
  const multipleList = ref([]);
// å®šæ—¶ä»»åŠ¡ç®¡ç†tab相关变量
const scheduledFilters = reactive({
  taskName: '',
  status: '',
})
    taskName: "",
    status: "",
  });
const scheduledDataList = ref([])
  const scheduledDataList = ref([]);
const scheduledPagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
})
const scheduledMultipleList = ref([])
  });
  const scheduledMultipleList = ref([]);
// å®šæ—¶ä»»åŠ¡ç®¡ç†è¡¨æ ¼åˆ—é…ç½®
const scheduledColumns = ref([
@@ -309,33 +304,37 @@
        label: "频次",
        minWidth: 150,
        // PIMTable ä½¿ç”¨çš„æ˜¯ formatData,而不是 Element-Plus çš„ formatter
        formatData: (cell) => ({
      formatData: cell =>
        ({
            DAILY: "每日",
            WEEKLY: "每周",
            MONTHLY: "每月",
            QUARTERLY: "季度"
        }[cell] || "")
          QUARTERLY: "季度",
        }[cell] || ""),
    },
    {
        prop: "frequencyDetail",
        label: "开始日期与时间",
        minWidth: 150,
        // åŒæ ·æ”¹ç”¨ formatData,PIMTable å†…部会把单元格值传进来
        formatData: (cell) => {
            if (typeof cell !== 'string') return '';
      formatData: cell => {
        if (typeof cell !== "string") return "";
            let val = cell;
            const replacements = {
                MON: '周一',
                TUE: '周二',
                WED: '周三',
                THU: '周四',
                FRI: '周五',
                SAT: '周六',
                SUN: '周日'
          MON: "周一",
          TUE: "周二",
          WED: "周三",
          THU: "周四",
          FRI: "周五",
          SAT: "周六",
          SUN: "周日",
            };
            // ä½¿ç”¨æ­£åˆ™ä¸€æ¬¡æ€§æ›¿æ¢æ‰€æœ‰åŒ¹é…é¡¹
            return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, match => replacements[match]);
        }
        return val.replace(
          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
          match => replacements[match]
        );
      },
    },
    { prop: "registrant", label: "登记人", minWidth: 100 },
    { prop: "registrationDate", label: "登记日期", minWidth: 100 },
@@ -347,7 +346,7 @@
        align: "center",
        width: "200px",
    },
])
  ]);
// ä»»åŠ¡è®°å½•è¡¨æ ¼åˆ—é…ç½®ï¼ˆåŽŸè®¾å¤‡ä¿å…»è¡¨æ ¼åˆ—ï¼‰
const columns = ref([
@@ -365,12 +364,17 @@
        label: "计划保养日期",
        align: "center",
        prop: "maintenancePlanTime",
        formatData: (cell) => dayjs(cell).format("YYYY-MM-DD"),
      formatData: cell => dayjs(cell).format("YYYY-MM-DD"),
    },
    {
        label: "录入人",
        align: "center",
        prop: "createUserName",
    },
    {
      label: "项目",
      align: "center",
      prop: "machineryCategory",
    },
    // {
    //   label: "录入日期",
@@ -388,7 +392,7 @@
        label: "实际保养日期",
        align: "center",
        prop: "maintenanceActuallyTime",
        formatData: (cell) =>
      formatData: cell =>
            cell ? dayjs(cell).format("YYYY-MM-DD HH:mm:ss") : "-",
    },
    {
@@ -413,16 +417,16 @@
        align: "center",
        width: "350px",
    },
])
  ]);
// Tab切换处理
const handleTabChange = (tabName) => {
  if (tabName === 'record') {
    getTableData()
  } else if (tabName === 'scheduled') {
    getScheduledTableData()
  const handleTabChange = tabName => {
    if (tabName === "record") {
      getTableData();
    } else if (tabName === "scheduled") {
      getScheduledTableData();
  }
}
  };
// å®šæ—¶ä»»åŠ¡ç®¡ç†ç›¸å…³æ–¹æ³•
const getScheduledTableData = async () => {
@@ -432,64 +436,64 @@
      size: scheduledPagination.pageSize,
      taskName: scheduledFilters.taskName || undefined,
      status: scheduledFilters.status || undefined,
    }
    const { code, data } = await deviceMaintenanceTaskList(params)
      };
      const { code, data } = await deviceMaintenanceTaskList(params);
    if (code === 200) {
      scheduledDataList.value = data?.records || []
      scheduledPagination.total = data?.total || 0
        scheduledDataList.value = data?.records || [];
        scheduledPagination.total = data?.total || 0;
    }
  } catch (error) {
    ElMessage.error('获取定时任务列表失败')
      ElMessage.error("获取定时任务列表失败");
  }
}
  };
const resetScheduledFilters = () => {
  scheduledFilters.taskName = ''
  scheduledFilters.status = ''
  getScheduledTableData()
}
    scheduledFilters.taskName = "";
    scheduledFilters.status = "";
    getScheduledTableData();
  };
const handleScheduledSelectionChange = (selection) => {
  scheduledMultipleList.value = selection
}
  const handleScheduledSelectionChange = selection => {
    scheduledMultipleList.value = selection;
  };
const changeScheduledPage = (page) => {
  scheduledPagination.currentPage = page.page
  scheduledPagination.pageSize = page.limit
  getScheduledTableData()
}
  const changeScheduledPage = page => {
    scheduledPagination.currentPage = page.page;
    scheduledPagination.pageSize = page.limit;
    getScheduledTableData();
  };
const addScheduledTask = () => {
  nextTick(() => {
        formDiaRef.value?.openDialog('add');
      formDiaRef.value?.openDialog("add");
    });
}
  };
const editScheduledTask = (row) => {
  const editScheduledTask = row => {
  if (row) {
        nextTick(() => {
            formDiaRef.value?.openDialog('edit', row);
        formDiaRef.value?.openDialog("edit", row);
        });
  }
}
  };
const delScheduledTaskByIds = async (ids) => {
  const delScheduledTaskByIds = async ids => {
  try {
    await ElMessageBox.confirm('确定删除选中的定时任务吗?', '提示', {
      type: 'warning',
    })
    const payload = Array.isArray(ids) ? ids : [ids]
    await deviceMaintenanceTaskDel(payload)
    ElMessage.success('删除定时任务成功')
    getScheduledTableData()
      await ElMessageBox.confirm("确定删除选中的定时任务吗?", "提示", {
        type: "warning",
      });
      const payload = Array.isArray(ids) ? ids : [ids];
      await deviceMaintenanceTaskDel(payload);
      ElMessage.success("删除定时任务成功");
      getScheduledTableData();
  } catch (error) {
    // ç”¨æˆ·å–消删除
  }
}
  };
const handleScheduledOut = () => {
  ElMessage.info('导出定时任务功能待实现')
}
    ElMessage.info("导出定时任务功能待实现");
  };
// ä»»åŠ¡è®°å½•ç›¸å…³æ–¹æ³•ï¼ˆåŽŸè®¾å¤‡ä¿å…»é¡µé¢æ–¹æ³•ï¼‰
const getTableData = async () => {
@@ -498,186 +502,124 @@
      current: pagination.value.currentPage,
      size: pagination.value.pageSize,
      deviceName: filters.deviceName || undefined,
      maintenancePlanTime: filters.maintenancePlanTime ? dayjs(filters.maintenancePlanTime).format('YYYY-MM-DD') : undefined,
      maintenanceActuallyTime: filters.maintenanceActuallyTime ? dayjs(filters.maintenanceActuallyTime).format('YYYY-MM-DD') : undefined,
        maintenancePlanTime: filters.maintenancePlanTime
          ? dayjs(filters.maintenancePlanTime).format("YYYY-MM-DD")
          : undefined,
        maintenanceActuallyTime: filters.maintenanceActuallyTime
          ? dayjs(filters.maintenanceActuallyTime).format("YYYY-MM-DD")
          : undefined,
      maintenanceActuallyName: filters.maintenanceActuallyName || undefined,
    }
      };
    const { code, data } = await getUpkeepPage(params)
      const { code, data } = await getUpkeepPage(params);
    if (code === 200) {
      dataList.value = data.records
      pagination.value.total = data.total
        dataList.value = data.records;
        pagination.value.total = data.total;
    }
  } catch (error) {
    console.log(error);
  }
}
  };
const resetFilters = () => {
  filters.deviceName = ''
  filters.maintenancePlanTime = ''
  filters.maintenanceActuallyTime = ''
  filters.maintenanceActuallyName = ''
  getTableData()
}
    filters.deviceName = "";
    filters.maintenancePlanTime = "";
    filters.maintenanceActuallyTime = "";
    filters.maintenanceActuallyName = "";
    getTableData();
  };
const handleSelectionChange = (selection) => {
  multipleList.value = selection
}
  const handleSelectionChange = selection => {
    multipleList.value = selection;
  };
// æ£€æŸ¥é€‰ä¸­çš„记录中是否有完结状态的
const hasFinishedStatus = computed(() => {
  return multipleList.value.some(item => item.status === 1)
})
    return multipleList.value.some(item => item.status === 1);
  });
const changePage = (page) => {
  pagination.value.currentPage = page.page
  pagination.value.pageSize = page.limit
  getTableData()
}
  const changePage = page => {
    pagination.value.currentPage = page.page;
    pagination.value.pageSize = page.limit;
    getTableData();
  };
const addMaintain = (row) => {
  maintainModalRef.value.open(row.id, row)
}
  const addMaintain = row => {
    maintainModalRef.value.open(row.id, row);
  };
const addPlan = () => {
  planModalRef.value.openModal()
}
    planModalRef.value.openModal();
  };
const editPlan = (id) => {
  planModalRef.value.openEdit(id)
}
  const editPlan = id => {
    planModalRef.value.openEdit(id);
  };
const delRepairByIds = async (ids) => {
  const delRepairByIds = async ids => {
  // æ£€æŸ¥æ˜¯å¦æœ‰å®Œç»“状态的记录
  const hasFinished = multipleList.value.some(item => item.status === 1)
    const hasFinished = multipleList.value.some(item => item.status === 1);
  if (hasFinished) {
    ElMessage.warning('不能删除状态为完结的记录')
    return
      ElMessage.warning("不能删除状态为完结的记录");
      return;
  }
  
  try {
    await ElMessageBox.confirm('确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?', '警告', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    })
      await ElMessageBox.confirm("确认删除保养数据, æ­¤æ“ä½œä¸å¯é€†?", "警告", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      });
    
    const { code } = await delUpkeep(ids)
      const { code } = await delUpkeep(ids);
    if (code === 200) {
      ElMessage.success('删除成功')
      getTableData()
        ElMessage.success("删除成功");
        getTableData();
    }
  } catch (error) {
    // ç”¨æˆ·å–消删除
  }
}
  };
const handleOut = () => {
  ElMessageBox.confirm('选中的内容将被导出,是否确认导出?', '导出', {
    confirmButtonText: '确认',
    cancelButtonText: '取消',
    type: 'warning',
    ElMessageBox.confirm("选中的内容将被导出,是否确认导出?", "导出", {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
  })
    .then(() => {
      proxy.download('/device/maintenance/export', {}, '设备保养.xlsx')
        proxy.download("/device/maintenance/export", {}, "设备保养.xlsx");
    })
    .catch(() => {
      ElMessage.info('已取消')
    })
}
        ElMessage.info("已取消");
      });
  };
const handleDateChange = (date, type) => {
  if (type === 1) {
    filters.maintenanceActuallyTime = date ? dayjs(date).format('YYYY-MM-DD') : ''
      filters.maintenanceActuallyTime = date
        ? dayjs(date).format("YYYY-MM-DD")
        : "";
  } else {
    filters.maintenancePlanTime = date ? dayjs(date).format('YYYY-MM-DD') : ''
      filters.maintenancePlanTime = date ? dayjs(date).format("YYYY-MM-DD") : "";
  }
  getTableData()
}
// é™„件相关方法
// æŸ¥è¯¢é™„件列表
const fetchMaintenanceTaskFiles = async (deviceMaintenanceId) => {
  try {
    const params = {
      current: 1,
      size: 100,
      deviceMaintenanceId,
      rulesRegulationsManagementId:deviceMaintenanceId
    }
    const res = await listMaintenanceTaskFiles(params)
    const records = res?.data?.records || []
    const mapped = records.map(item => ({
      id: item.id,
      name: item.fileName || item.name,
      url: item.fileUrl || item.url,
      raw: item,
    }))
    fileListDialogRef.value?.setList(mapped)
  } catch (error) {
    ElMessage.error('获取附件列表失败')
  }
}
    getTableData();
  };
// æ‰“开附件弹窗
const openFileDialog = async (row) => {
  currentMaintenanceTaskId.value = row.id
  fileDialogVisible.value = true
  await fetchMaintenanceTaskFiles(row.id)
}
// åˆ·æ–°é™„件列表
const refreshFileList = async () => {
  if (!currentMaintenanceTaskId.value) return
  await fetchMaintenanceTaskFiles(currentMaintenanceTaskId.value)
}
// ä¸Šä¼ é™„ä»¶
const handleAttachmentUpload = async (filePayload) => {
  if (!currentMaintenanceTaskId.value) return
  try {
    const payload = {
      name: filePayload?.fileName || filePayload?.name,
      url: filePayload?.fileUrl || filePayload?.url,
      deviceMaintenanceId: currentMaintenanceTaskId.value,
    }
    await addMaintenanceTaskFile(payload)
    ElMessage.success('文件上传成功')
    await refreshFileList()
  } catch (error) {
    ElMessage.error('文件上传失败')
  }
}
// åˆ é™¤é™„ä»¶
const handleAttachmentDelete = async (row) => {
  if (!row?.id) return false
  try {
    await ElMessageBox.confirm('确认删除该附件?', '提示', { type: 'warning' })
  } catch {
    return false
  }
  try {
    await delMaintenanceTaskFile(row.id)
    ElMessage.success('删除成功')
    await refreshFileList()
    return true
  } catch (error) {
    ElMessage.error('删除失败')
    return false
  }
}
  const openFileDialog = async row => {
    currentMaintenanceTaskId.value = row.id;
    fileDialogVisible.value = true;
  };
onMounted(() => {
  // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
  if (activeTab.value === 'scheduled') {
    getScheduledTableData()
    if (activeTab.value === "scheduled") {
      getScheduledTableData();
  } else {
    getTableData()
      getTableData();
  }
})
  });
</script>
<style lang="scss" scoped>
src/views/example/DynamicTableExample.vue
@@ -94,8 +94,8 @@
      
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleSubmit">确定</el-button>
          <el-button @click="dialogVisible = false">取消</el-button>
        </div>
      </template>
    </el-dialog>
src/views/fileManagement/bookshelf/index.vue
@@ -97,8 +97,8 @@
      </el-tree>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="keepVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="keepVisible = false" >ç¡® å®š</el-button>
          <el-button @click="keepVisible = false">取 æ¶ˆ</el-button>
        </span>
      </template>
    </el-dialog>
@@ -115,8 +115,8 @@
      </el-row>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="warehouseVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="confirmWarehouse" :loading="upLoadWarehouse">ç¡® å®š</el-button>
          <el-button @click="warehouseVisible = false">取 æ¶ˆ</el-button>
        </span>
      </template>
    </el-dialog>
@@ -149,8 +149,8 @@
      </el-row>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="shelvesVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="confirmShelves" :loading="upLoadShelves">ç¡® å®š</el-button>
          <el-button @click="shelvesVisible = false">取 æ¶ˆ</el-button>
        </span>
      </template>
    </el-dialog>
src/views/fileManagement/borrow/index.vue
@@ -100,16 +100,14 @@
           </el-col>
           <el-col :span="12">
             <el-form-item label="借阅书籍:" prop="documentationId">
               <!-- <el-select v-model="borrowForm.documentationId" placeholder="请选择借阅书籍" style="width: 100%" @change="handleScanContent">
                 <el-option
                   v-for="item in documentList"
                   :key="item.id"
                   :label="item.docName || item.name"
                   :value="item.id"
                 />
               </el-select> -->
               <div style="display: flex; gap: 10px;">
                <el-select v-model="borrowForm.documentationId" placeholder="请选择借阅书籍" style="flex: 1;width: 100px;" @change="handleSelectChange">
                <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" 
@@ -118,6 +116,13 @@
                  />
                </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;"
@@ -205,6 +210,7 @@
const selectedRows = ref([]);
const documentList = ref([]); // æ–‡æ¡£åˆ—表,用于借阅书籍选择
const scanContent = ref() // æ‰«ç å†…容
const currentEditDocName = ref(''); // ç¼–辑时存储的文档名称
// åˆ†é¡µç›¸å…³
const pagination = reactive({
  currentPage: 1,
@@ -282,6 +288,7 @@
      {
        name: "编辑",
        type: "text",
        disabled: (row) => row.borrowStatus === '归还',
        clickFun: (row) => {
          openBorrowDia('edit', row)
        },
@@ -428,11 +435,14 @@
  if (type === "edit") {
    // ç¼–辑模式,加载现有数据
    Object.assign(borrowForm, data);
    // å­˜å‚¨æ–‡æ¡£åç§°ç”¨äºŽæ˜¾ç¤º
    currentEditDocName.value = data.docName || '';
  } else {
    // æ–°å¢žæ¨¡å¼ï¼Œæ¸…空表单
    Object.keys(borrowForm).forEach(key => {
      borrowForm[key] = "";
    });
    currentEditDocName.value = ''; // æ¸…空编辑时的文档名称
         // è®¾ç½®é»˜è®¤çŠ¶æ€
     borrowForm.borrowStatus = "借阅";
    // è®¾ç½®å½“前日期为借阅日期
@@ -445,6 +455,7 @@
  proxy.$refs.borrowFormRef.resetFields();
  borrowDia.value = false;
  scanContent.value = ''; // æ¸…空扫码内容
  currentEditDocName.value = ''; // æ¸…空编辑时的文档名称
};
// æäº¤å€Ÿé˜…表单
@@ -625,7 +636,7 @@
}
.dialog-footer {
  text-align: right;
  text-align: center;
}
:deep(.el-form-item__label) {
src/views/fileManagement/document/index.vue
@@ -862,12 +862,14 @@
      documentForm[key] = "";
    });
    documentForm.attachments = []; // æ–°å¢žæ¨¡å¼ä¸‹ä¹Ÿæ¸…空附件
    // è®¾ç½®é»˜è®¤å€¼ - ä½¿ç”¨å­—典数据的第一个选项作为默认值
    // è®¾ç½®é»˜è®¤å€¼ - æ–‡æ¡£çŠ¶æ€é»˜è®¤è®¾ç½®ä¸º"正常"
    if (document_status.value && document_status.value.length > 0) {
      documentForm.docStatus = document_status.value[0].value;
      const normalStatus = document_status.value.find(item => item.label === '正常');
      documentForm.docStatus = normalStatus ? normalStatus.value : document_status.value[0].value;
    }
    if (document_urgency.value && document_urgency.value.length > 0) {
      documentForm.urgencyLevel = document_urgency.value[0].value;
      const normalUrgency = document_urgency.value.find(item => item.label === '普通');
      documentForm.urgencyLevel = normalUrgency ? normalUrgency.value : document_urgency.value[0].value;
    }
  }
};
@@ -1298,7 +1300,7 @@
}
.dialog-footer {
  text-align: right;
  text-align: center;
}
.operation-column {
src/views/fileManagement/return/index.vue
@@ -103,16 +103,14 @@
                 <el-row :gutter="20">
           <el-col :span="12">
             <el-form-item label="文档:" prop="borrowId">
               <!-- <el-select v-model="returnForm.borrowId" placeholder="请选择文档" style="flex: 1;" @change="handleDocumentChange">
                 <el-option
                   v-for="item in documentList"
                   :key="item.id"
                   :label="item.docName || item.name"
                   :value="item.id"
                 />
               </el-select> -->
               <div style="display: flex; gap: 10px;">
                <el-select v-model="returnForm.borrowId" placeholder="请选择文档" style="width: 120px;" @change="handleDocumentChange">
                <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" 
@@ -121,6 +119,13 @@
                  />
                </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;"
@@ -215,6 +220,7 @@
const documentList = ref([]); // æ–‡æ¡£åˆ—表
const borrowInfoList = ref([]); // å€Ÿé˜…信息列表
const scanContent = ref(); // æ‰«ç å†…容
const currentEditDocName = ref(''); // ç¼–辑时存储的文档名称
// åˆ†é¡µç›¸å…³
const pagination = reactive({
@@ -286,6 +292,7 @@
      {
        name: "编辑",
        type: "text",
        disabled: (row) => row.borrowStatus === '归还',
        clickFun: (row) => {
          openReturnDia('edit', row)
        },
@@ -396,15 +403,14 @@
  if (type === "edit") {
    // ç¼–辑模式,加载现有数据
    Object.assign(returnForm, data);
    // ç¼–辑模式下,文档选择后自动填充借阅人和应归还日期
    if (returnForm.borrowId) {
      handleDocumentChange(returnForm.borrowId);
    }
    // å­˜å‚¨æ–‡æ¡£åç§°ç”¨äºŽæ˜¾ç¤º
    currentEditDocName.value = data.docName || '';
  } else {
    // æ–°å¢žæ¨¡å¼ï¼Œæ¸…空表单
    Object.keys(returnForm).forEach(key => {
      returnForm[key] = "";
    });
    currentEditDocName.value = ''; // æ¸…空编辑时的文档名称
    // è®¾ç½®é»˜è®¤çŠ¶æ€
    returnForm.borrowStatus = "归还";
    // è®¾ç½®å½“前日期为归还日期
@@ -418,6 +424,7 @@
  returnDia.value = false;
  scanContent.value = ''; // æ¸…空扫码内容
  borrowInfoList.value = []; // æ¸…空借阅信息列表
  currentEditDocName.value = ''; // æ¸…空编辑时的文档名称
};
// æäº¤å½’还表单
@@ -677,7 +684,7 @@
}
.dialog-footer {
  text-align: right;
  text-align: center;
}
:deep(.el-form-item__label) {
src/views/financialManagement/accounting/index.vue
@@ -526,7 +526,6 @@
/* é¡µé¢èƒŒæ™¯ */
main {
  background: #f5f5f5;
  padding: 0;
  margin: 0 -20px;
  padding: 0 20px 20px;
src/views/financialManagement/assets/fixedAssets.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,462 @@
<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"
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @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" 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-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, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
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", slot: "category" },
  { label: "规格型号", prop: "specification", width: "120" },
  { label: "资产原值", prop: "originalValue", slot: "originalValue" },
  { label: "累计折旧", prop: "accumulatedDepreciation", slot: "accumulatedDepreciation" },
  { label: "资产净值", prop: "netValue", slot: "netValue" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const form = reactive({
  assetCode: "",
  assetName: "",
  category: "",
  specification: "",
  purchaseDate: "",
  originalValue: 0,
  usefulLife: 5,
  residualRate: 5,
  accumulatedDepreciation: 0,
  netValue: 0,
  location: "",
  department: "",
  keeper: "",
  status: "in_use",
  remark: "",
});
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 mockData = [
  { id: 1, assetCode: "GD2024001", assetName: "办公电脑", category: "electronic", specification: "联想ThinkPad X1", purchaseDate: "2023-01-15", originalValue: 8000, usefulLife: 5, residualRate: 5, accumulatedDepreciation: 1520, netValue: 6480, location: "办公室", department: "财务部", keeper: "张三", status: "in_use", remark: "" },
  { id: 2, assetCode: "GD2024002", assetName: "打印机", category: "electronic", specification: "惠普M479fdw", purchaseDate: "2023-03-20", originalValue: 3500, usefulLife: 5, residualRate: 5, accumulatedDepreciation: 532, netValue: 2968, location: "文印室", department: "行政部", keeper: "李四", status: "in_use", remark: "" },
  { id: 3, assetCode: "GD2024003", assetName: "办公桌椅", category: "furniture", specification: "实木办公桌", purchaseDate: "2023-06-10", originalValue: 2500, usefulLife: 10, residualRate: 5, accumulatedDepreciation: 118.75, netValue: 2381.25, location: "办公室", department: "销售部", keeper: "王五", status: "in_use", remark: "" },
  { id: 4, assetCode: "GD2024004", assetName: "商务车", category: "vehicle", specification: "别克GL8", purchaseDate: "2022-08-01", originalValue: 280000, usefulLife: 10, residualRate: 5, accumulatedDepreciation: 53200, netValue: 226800, location: "停车场", department: "行政部", keeper: "赵六", status: "in_use", remark: "" },
];
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 map = { in_use: "在用", idle: "闲置", scrapped: "报废" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { in_use: "success", idle: "warning", scrapped: "info" };
  return map[status] || "";
};
const calculateNetValue = () => {
  form.netValue = Number((form.originalValue - form.accumulatedDepreciation).toFixed(2));
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.assetCode) {
    result = result.filter(item => item.assetCode.includes(filters.assetCode));
  }
  if (filters.assetName) {
    result = result.filter(item => item.assetName.includes(filters.assetName));
  }
  if (filters.category) {
    result = result.filter(item => item.category === filters.category);
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
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;
  dialogTitle.value = "新增固定资产";
  Object.assign(form, {
    assetCode: "GD" + Date.now().toString().slice(-8),
    assetName: "",
    category: "",
    specification: "",
    purchaseDate: new Date().toISOString().split('T')[0],
    originalValue: 0,
    usefulLife: 5,
    residualRate: 5,
    accumulatedDepreciation: 0,
    netValue: 0,
    location: "",
    department: "",
    keeper: "",
    status: "in_use",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑固定资产";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看资产: ${row.assetName}`);
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该固定资产吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
    getTableData();
  });
};
const handleDepreciation = () => {
  ElMessageBox.confirm("确认进行本月折旧计提吗?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    mockData.forEach(item => {
      if (item.status === "in_use") {
        const monthlyDepreciation = (item.originalValue * (1 - item.residualRate / 100)) / (item.usefulLife * 12);
        item.accumulatedDepreciation = Number((item.accumulatedDepreciation + monthlyDepreciation).toFixed(2));
        item.netValue = Number((item.originalValue - item.accumulatedDepreciation).toFixed(2));
      }
    });
    ElMessage.success("折旧计提完成");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      calculateNetValue();
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
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>
src/views/financialManagement/assets/intangibleAssets.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,458 @@
<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"
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @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" 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-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 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 { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
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", slot: "category" },
  { label: "证书编号", prop: "certificateNo", width: "150" },
  { label: "资产原值", prop: "originalValue", slot: "originalValue" },
  { label: "累计摊销", prop: "accumulatedAmortization", slot: "accumulatedAmortization" },
  { label: "资产净值", prop: "netValue", slot: "netValue" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const form = reactive({
  assetCode: "",
  assetName: "",
  category: "",
  certificateNo: "",
  acquisitionDate: "",
  originalValue: 0,
  amortizationPeriod: 10,
  residualRate: 0,
  accumulatedAmortization: 0,
  netValue: 0,
  validityDate: "",
  status: "in_use",
  description: "",
  remark: "",
});
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 mockData = [
  { id: 1, assetCode: "WX2024001", assetName: "ERP软件许可", category: "software", certificateNo: "SW-2023-001", acquisitionDate: "2023-01-01", originalValue: 50000, amortizationPeriod: 10, residualRate: 0, accumulatedAmortization: 5000, netValue: 45000, validityDate: "2033-01-01", status: "in_use", description: "企业资源计划管理系统", remark: "" },
  { id: 2, assetCode: "WX2024002", assetName: "发明专利", category: "patent", certificateNo: "ZL202210123456.7", acquisitionDate: "2022-06-15", originalValue: 100000, amortizationPeriod: 20, residualRate: 0, accumulatedAmortization: 3750, netValue: 96250, validityDate: "2042-06-15", status: "in_use", description: "一种新型生产工艺", remark: "" },
  { id: 3, assetCode: "WX2024003", assetName: "商标权", category: "trademark", certificateNo: "TM-2023-008", acquisitionDate: "2023-03-10", originalValue: 20000, amortizationPeriod: 10, residualRate: 0, accumulatedAmortization: 1500, netValue: 18500, validityDate: "2033-03-10", status: "in_use", description: "公司品牌商标", remark: "" },
  { id: 4, assetCode: "WX2024004", assetName: "土地使用权", category: "land", certificateNo: "土国用(2023)第001号", acquisitionDate: "2023-07-01", originalValue: 500000, amortizationPeriod: 50, residualRate: 0, accumulatedAmortization: 5000, netValue: 495000, validityDate: "2073-07-01", status: "in_use", description: "工业用地使用权", remark: "" },
];
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 map = { in_use: "在用", idle: "闲置", amortized: "已摊销完毕" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { in_use: "success", idle: "warning", amortized: "info" };
  return map[status] || "";
};
const calculateNetValue = () => {
  form.netValue = Number((form.originalValue - form.accumulatedAmortization).toFixed(2));
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.assetCode) {
    result = result.filter(item => item.assetCode.includes(filters.assetCode));
  }
  if (filters.assetName) {
    result = result.filter(item => item.assetName.includes(filters.assetName));
  }
  if (filters.category) {
    result = result.filter(item => item.category === filters.category);
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
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;
  dialogTitle.value = "新增无形资产";
  Object.assign(form, {
    assetCode: "WX" + Date.now().toString().slice(-8),
    assetName: "",
    category: "",
    certificateNo: "",
    acquisitionDate: new Date().toISOString().split('T')[0],
    originalValue: 0,
    amortizationPeriod: 10,
    residualRate: 0,
    accumulatedAmortization: 0,
    netValue: 0,
    validityDate: "",
    status: "in_use",
    description: "",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑无形资产";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看资产: ${row.assetName}`);
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该无形资产吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
    getTableData();
  });
};
const handleAmortization = () => {
  ElMessageBox.confirm("确认进行本月摊销计提吗?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    mockData.forEach(item => {
      if (item.status === "in_use") {
        const monthlyAmortization = (item.originalValue * (1 - item.residualRate / 100)) / (item.amortizationPeriod * 12);
        item.accumulatedAmortization = Number((item.accumulatedAmortization + monthlyAmortization).toFixed(2));
        item.netValue = Number((item.originalValue - item.accumulatedAmortization).toFixed(2));
        if (item.netValue <= 0) {
          item.status = "amortized";
          item.netValue = 0;
        }
      }
    });
    ElMessage.success("摊销计提完成");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      calculateNetValue();
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
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>
src/views/financialManagement/expenseManagement/index.vue
@@ -76,27 +76,18 @@
      </PIMTable>
    </div>
    <Modal ref="modalRef" @success="getTableData"></Modal>
    <FileListDialog
      ref="fileListRef"
      v-model="fileListDialogVisible"
      :show-upload-button="true"
      :show-delete-button="true"
      :upload-method="handleUpload"
      :delete-method="handleFileDelete"
    />
    <FileList v-if="fileDialogVisible"  v-model:visible="fileDialogVisible" record-type="account_expense" :record-id="recordId"  />
  </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
import { listPage, delAccountExpense, fileListPage, fileAdd, fileDel } from "@/api/financialManagement/expenseManagement";
import { onMounted, getCurrentInstance, ref, computed } from "vue";
import { listPage, delAccountExpense } from "@/api/financialManagement/expenseManagement";
import {onMounted, getCurrentInstance, ref, computed, defineAsyncComponent} from "vue";
import Modal from "./Modal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
import dayjs from "dayjs";
import FileListDialog from "@/components/Dialog/FileListDialog.vue";
import request from "@/utils/request";
import { getToken } from "@/utils/auth";
const FileList = defineAsyncComponent(() => import("@/components/Dialog/FileList.vue"));
defineOptions({
  name: "支出管理",
@@ -108,9 +99,6 @@
const modalRef = ref();
const { checkout_payment } = proxy.useDict("checkout_payment");
const { expense_types } = proxy.useDict("expense_types");
const fileListRef = ref(null);
const fileListDialogVisible = ref(false);
const currentFileRow = ref(null);
const accountType = ref('支出');
const {
@@ -315,156 +303,16 @@
      proxy.$modal.msg("已取消");
    });
};
// æ‰“开附件弹窗
const recordId =ref(0)
const fileDialogVisible = ref(false)
// æ‰“开附件弹框
const openFilesFormDia = async (row) => {
  currentFileRow.value = row;
  accountType.value = '支出';
  try {
    const res = await fileListPage({
      accountId: row.id,
      accountType: accountType.value,
      current: 1,
      size: 100
    });
    if (res.code === 200 && fileListRef.value) {
      // å°†æ•°æ®è½¬æ¢ä¸º FileListDialog éœ€è¦çš„æ ¼å¼
      const fileList = (res.data?.records || []).map(item => ({
        name: item.name,
        url: item.url,
        id: item.id,
        ...item
      }));
      fileListRef.value.open(fileList);
      fileListDialogVisible.value = true;
  recordId.value = row.id
  fileDialogVisible.value = true
    }
  } catch (error) {
    proxy.$modal.msgError("获取附件列表失败");
  }
};
// ä¸Šä¼ é™„ä»¶
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 = {
            accountId: currentFileRow.value.id,
            accountType: accountType.value,
            name: uploadRes.data.originalName || file.name,
            url: uploadRes.data.tempPath || uploadRes.data.url
          };
          const saveRes = await fileAdd(fileData);
          if (saveRes.code === 200) {
            proxy.$modal.msgSuccess("文件上传成功");
            // é‡æ–°åŠ è½½æ–‡ä»¶åˆ—è¡¨
            const listRes = await fileListPage({
              accountId: currentFileRow.value.id,
              accountType: accountType.value,
              current: 1,
              size: 100
            });
            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);
            }
            // è¿”回新文件信息
            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 handleFileDelete = async (row) => {
  try {
    const res = await fileDel([row.id]);
    if (res.code === 200) {
      proxy.$modal.msgSuccess("删除成功");
      // é‡æ–°åŠ è½½æ–‡ä»¶åˆ—è¡¨
      if (currentFileRow.value && fileListRef.value) {
        const listRes = await fileListPage({
          accountId: currentFileRow.value.id,
          accountType: accountType.value,
          current: 1,
          size: 100
        });
        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);
        }
      }
      return true; // è¿”回 true è¡¨ç¤ºåˆ é™¤æˆåŠŸï¼Œç»„ä»¶ä¼šæ›´æ–°åˆ—è¡¨
    } else {
      proxy.$modal.msgError(res.msg || "删除失败");
      return false;
    }
  } catch (error) {
    proxy.$modal.msgError("删除失败");
    return false;
  }
};
onMounted(() => {
  getTableData();
src/views/financialManagement/generalLedger/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,291 @@
<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="asset" />
          <el-option label="负债类" value="liability" />
          <el-option label="权益类" value="equity" />
          <el-option label="成本类" value="cost" />
          <el-option label="损益类" value="profit_loss" />
        </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>
      <PIMTable
        rowKey="id"
        :column="columns"
        :tableData="dataList"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      >
        <template #subjectType="{ row }">
          <el-tag :type="getSubjectTypeType(row.subjectType)">{{ getSubjectTypeLabel(row.subjectType) }}</el-tag>
        </template>
        <template #balanceDirection="{ row }">
          <el-tag :type="row.balanceDirection === 'debit' ? 'success' : 'danger'">
            {{ row.balanceDirection === 'debit' ? '借方' : '贷方' }}
          </el-tag>
        </template>
        <template #status="{ row }">
          <el-tag :type="row.status === 'active' ? 'success' : 'info'">
            {{ row.status === 'active' ? '启用' : '禁用' }}
          </el-tag>
        </template>
        <template #operation="{ row }">
          <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="600px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
        <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="asset" />
            <el-option label="负债类" value="liability" />
            <el-option label="权益类" value="equity" />
            <el-option label="成本类" value="cost" />
            <el-option label="损益类" value="profit_loss" />
          </el-select>
        </el-form-item>
        <el-form-item label="余额方向" prop="balanceDirection">
          <el-radio-group v-model="form.balanceDirection">
            <el-radio label="debit">借方</el-radio>
            <el-radio label="credit">贷方</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio label="active">启用</el-radio>
            <el-radio label="inactive">禁用</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 } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "总帐科目",
});
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", slot: "subjectType" },
  { label: "余额方向", prop: "balanceDirection", slot: "balanceDirection" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "备注", prop: "remark", showOverflowTooltip: true },
  { label: "操作", prop: "operation", slot: "operation", width: "150", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const form = reactive({
  subjectCode: "",
  subjectName: "",
  subjectType: "",
  balanceDirection: "debit",
  status: "active",
  remark: "",
});
const rules = {
  subjectCode: [{ required: true, message: "请输入科目编码", trigger: "blur" }],
  subjectName: [{ required: true, message: "请输入科目名称", trigger: "blur" }],
  subjectType: [{ required: true, message: "请选择科目类型", trigger: "change" }],
};
const mockData = [
  { id: 1, subjectCode: "1001", subjectName: "库存现金", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" },
  { id: 2, subjectCode: "1002", subjectName: "银行存款", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" },
  { id: 3, subjectCode: "1122", subjectName: "应收账款", subjectType: "asset", balanceDirection: "debit", status: "active", remark: "" },
  { id: 4, subjectCode: "2202", subjectName: "应付账款", subjectType: "liability", balanceDirection: "credit", status: "active", remark: "" },
  { id: 5, subjectCode: "4001", subjectName: "实收资本", subjectType: "equity", balanceDirection: "credit", status: "active", remark: "" },
  { id: 6, subjectCode: "5001", subjectName: "生产成本", subjectType: "cost", balanceDirection: "debit", status: "active", remark: "" },
  { id: 7, subjectCode: "6001", subjectName: "主营业务收入", subjectType: "profit_loss", balanceDirection: "credit", status: "active", remark: "" },
  { id: 8, subjectCode: "6401", subjectName: "主营业务成本", subjectType: "profit_loss", balanceDirection: "debit", status: "active", remark: "" },
];
const getSubjectTypeLabel = (type) => {
  const map = {
    asset: "资产类",
    liability: "负债类",
    equity: "权益类",
    cost: "成本类",
    profit_loss: "损益类",
  };
  return map[type] || type;
};
const getSubjectTypeType = (type) => {
  const map = {
    asset: "success",
    liability: "danger",
    equity: "warning",
    cost: "info",
    profit_loss: "primary",
  };
  return map[type] || "";
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.subjectCode) {
    result = result.filter(item => item.subjectCode.includes(filters.subjectCode));
  }
  if (filters.subjectName) {
    result = result.filter(item => item.subjectName.includes(filters.subjectName));
  }
  if (filters.subjectType) {
    result = result.filter(item => item.subjectType === filters.subjectType);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.subjectCode = "";
  filters.subjectName = "";
  filters.subjectType = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增科目";
  Object.assign(form, {
    subjectCode: "",
    subjectName: "",
    subjectType: "",
    balanceDirection: "debit",
    status: "active",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑科目";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该科目吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
</style>
src/views/financialManagement/payable/input-invoice.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,409 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="发票代码:">
        <el-input v-model="filters.invoiceCode" placeholder="请输入发票代码" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="发票号码:">
        <el-input v-model="filters.invoiceNo" placeholder="请输入发票号码" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="供应商:">
        <el-select v-model="filters.supplierId" placeholder="请选择供应商" clearable style="width: 200px;">
          <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item label="认证状态:">
        <el-select v-model="filters.certifyStatus" placeholder="请选择认证状态" clearable style="width: 150px;">
          <el-option label="未认证" value="uncertified" />
          <el-option label="已认证" value="certified" />
          <el-option label="认证失败" value="failed" />
        </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-button type="success" @click="handleBatchCertify" icon="Check" :disabled="selectedRows.length === 0">批量认证</el-button>
        </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">录入发票</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 #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 #certifyStatus="{ row }">
          <el-tag :type="getCertifyStatusType(row.certifyStatus)">{{ getCertifyStatusLabel(row.certifyStatus) }}</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="success" link @click="handleCertify(row)" v-if="row.certifyStatus === 'uncertified'">认证</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="发票代码" prop="invoiceCode">
              <el-input v-model="form.invoiceCode" placeholder="请输入发票代码" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="发票号码" prop="invoiceNo">
              <el-input v-model="form.invoiceNo" placeholder="请输入发票号码" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="供应商" prop="supplierId">
              <el-select v-model="form.supplierId" placeholder="请选择供应商" style="width: 100%;">
                <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </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%;" />
            </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%;" @change="calculateTax" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="税率" prop="taxRate">
              <el-select v-model="form.taxRate" placeholder="请选择税率" style="width: 100%;" @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-col :span="8">
            <el-form-item label="税额">
              <el-input v-model="form.taxAmount" disabled />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="认证状态" prop="certifyStatus">
              <el-select v-model="form.certifyStatus" placeholder="请选择认证状态" style="width: 100%;" disabled>
                <el-option label="未认证" value="uncertified" />
                <el-option label="已认证" value="certified" />
                <el-option label="认证失败" value="failed" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="认证日期" prop="certifyDate">
              <el-date-picker v-model="form.certifyDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" 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="请输入发票内容" />
        </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 type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "进项发票",
});
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
const filters = reactive({
  invoiceCode: "",
  invoiceNo: "",
  supplierId: "",
  certifyStatus: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "发票代码", prop: "invoiceCode", width: "130" },
  { label: "发票号码", prop: "invoiceNo", width: "120" },
  { label: "供应商", prop: "supplierName", width: "180" },
  { label: "开票日期", prop: "invoiceDate", width: "120" },
  { label: "金额", prop: "amount", slot: "amount" },
  { label: "税额", prop: "taxAmount", slot: "taxAmount" },
  { label: "价税合计", prop: "totalAmount", slot: "totalAmount" },
  { label: "认证状态", prop: "certifyStatus", slot: "certifyStatus" },
  { label: "操作", prop: "operation", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
const selectedRows = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const supplierList = [
  { id: 1, name: "北京原材料供应商" },
  { id: 2, name: "上海电子元器件公司" },
  { id: 3, name: "广州包装材料厂" },
  { id: 4, name: "深圳五金配件公司" },
];
const form = reactive({
  invoiceCode: "",
  invoiceNo: "",
  supplierId: "",
  invoiceDate: "",
  amount: 0,
  taxRate: 13,
  taxAmount: 0,
  totalAmount: 0,
  certifyStatus: "uncertified",
  certifyDate: "",
  content: "",
  remark: "",
});
const rules = {
  invoiceCode: [{ required: true, message: "请输入发票代码", trigger: "blur" }],
  invoiceNo: [{ required: true, message: "请输入发票号码", trigger: "blur" }],
  supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }],
  invoiceDate: [{ required: true, message: "请选择开票日期", trigger: "change" }],
  amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
  taxRate: [{ required: true, message: "请选择税率", trigger: "change" }],
};
const mockData = [
  { id: 1, invoiceCode: "0440021001", invoiceNo: "12345678", supplierId: 1, supplierName: "北京原材料供应商", invoiceDate: "2024-01-08", amount: 8000, taxRate: 13, taxAmount: 1040, totalAmount: 9040, certifyStatus: "certified", certifyDate: "2024-01-15", content: "原材料采购", remark: "" },
  { id: 2, invoiceCode: "0440021002", invoiceNo: "87654321", supplierId: 2, supplierName: "上海电子元器件公司", invoiceDate: "2024-01-10", amount: 12000, taxRate: 13, taxAmount: 1560, totalAmount: 13560, certifyStatus: "uncertified", certifyDate: "", content: "电子元器件", remark: "" },
  { id: 3, invoiceCode: "0440021003", invoiceNo: "11112222", supplierId: 3, supplierName: "广州包装材料厂", invoiceDate: "2024-01-12", amount: 3500, taxRate: 13, taxAmount: 455, totalAmount: 3955, certifyStatus: "certified", certifyDate: "2024-01-18", content: "包装材料", remark: "" },
];
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 getCertifyStatusLabel = (status) => {
  const map = { uncertified: "未认证", certified: "已认证", failed: "认证失败" };
  return map[status] || status;
};
const getCertifyStatusType = (status) => {
  const map = { uncertified: "info", certified: "success", failed: "danger" };
  return map[status] || "";
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.invoiceCode) {
    result = result.filter(item => item.invoiceCode.includes(filters.invoiceCode));
  }
  if (filters.invoiceNo) {
    result = result.filter(item => item.invoiceNo.includes(filters.invoiceNo));
  }
  if (filters.supplierId) {
    result = result.filter(item => item.supplierId === filters.supplierId);
  }
  if (filters.certifyStatus) {
    result = result.filter(item => item.certifyStatus === filters.certifyStatus);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.invoiceCode = "";
  filters.invoiceNo = "";
  filters.supplierId = "";
  filters.certifyStatus = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "录入发票";
  Object.assign(form, {
    invoiceCode: "",
    invoiceNo: "",
    supplierId: "",
    invoiceDate: new Date().toISOString().split('T')[0],
    amount: 0,
    taxRate: 13,
    taxAmount: 0,
    totalAmount: 0,
    certifyStatus: "uncertified",
    certifyDate: "",
    content: "",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑发票";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看发票: ${row.invoiceCode}-${row.invoiceNo}`);
};
const handleCertify = (row) => {
  ElMessageBox.confirm("确认认证该发票吗?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].certifyStatus = "certified";
      mockData[index].certifyDate = new Date().toISOString().split('T')[0];
    }
    ElMessage.success("认证成功");
    getTableData();
  });
};
const handleBatchCertify = () => {
  ElMessageBox.confirm(`确认批量认证选中的 ${selectedRows.value.length} å¼ å‘票吗?`, "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    selectedRows.value.forEach(row => {
      const index = mockData.findIndex(item => item.id === row.id);
      if (index !== -1 && mockData[index].certifyStatus === "uncertified") {
        mockData[index].certifyStatus = "certified";
        mockData[index].certifyDate = new Date().toISOString().split('T')[0];
      }
    });
    ElMessage.success("批量认证成功");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const supplier = supplierList.find(item => item.id === form.supplierId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, supplierName: supplier?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, supplierName: supplier?.name });
        ElMessage.success("录入成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  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>
src/views/financialManagement/payable/payment.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,377 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="付款单号:">
        <el-input v-model="filters.paymentCode" placeholder="请输入付款单号" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="供应商:">
        <el-select v-model="filters.supplierId" placeholder="请选择供应商" clearable style="width: 200px;">
          <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :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 label="银行转账" value="bank_transfer" />
          <el-option label="现金" value="cash" />
          <el-option label="支票" value="check" />
          <el-option label="汇票" value="draft" />
        </el-select>
      </el-form-item>
      <el-form-item label="状态:">
        <el-select v-model="filters.status" placeholder="请选择状态" clearable style="width: 150px;">
          <el-option label="待付款" value="pending" />
          <el-option label="已完成" value="completed" />
          <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="totalPaymentAmount" precision="2" prefix="Â¥" />
        </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增付款</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 #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="row.status === 'completed' ? 'success' : row.status === 'pending' ? 'warning' : 'info'">
            {{ row.status === 'completed' ? '已完成' : row.status === 'pending' ? '待付款' : '已取消' }}
          </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="row.status === 'pending'">编辑</el-button>
          <el-button type="success" link @click="handleComplete(row)" v-if="row.status === 'pending'">完成</el-button>
          <el-button type="danger" link @click="handleCancel(row)" v-if="row.status === 'pending'">取消</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="付款单号" prop="paymentCode">
              <el-input v-model="form.paymentCode" placeholder="系统自动生成" disabled />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="关联申请单" prop="applyCode">
              <el-select v-model="form.applyCode" placeholder="请选择关联申请单" style="width: 100%;" :disabled="isEdit">
                <el-option v-for="item in applyList" :key="item.applyCode" :label="item.applyCode" :value="item.applyCode" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="供应商" prop="supplierId">
              <el-select v-model="form.supplierId" placeholder="请选择供应商" style="width: 100%;" :disabled="isEdit">
                <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="付款日期" prop="paymentDate">
              <el-date-picker v-model="form.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="amount">
              <el-input-number v-model="form.amount" :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="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%;">
                <el-option label="银行转账" value="bank_transfer" />
                <el-option label="现金" value="cash" />
                <el-option label="支票" value="check" />
                <el-option label="汇票" value="draft" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="银行账号" prop="bankAccount" v-if="form.paymentMethod === 'bank_transfer'">
              <el-input v-model="form.bankAccount" placeholder="请输入银行账号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="开户行" prop="bankName" v-if="form.paymentMethod === 'bank_transfer'">
              <el-input v-model="form.bankName" placeholder="请输入开户行" />
            </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 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 { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "付款单",
});
const filters = reactive({
  paymentCode: "",
  supplierId: "",
  paymentMethod: "",
  status: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "付款单号", prop: "paymentCode", width: "150" },
  { label: "关联申请单", prop: "applyCode", width: "150" },
  { label: "供应商", prop: "supplierName", width: "180" },
  { label: "付款日期", prop: "paymentDate", width: "120" },
  { label: "付款金额", prop: "amount", slot: "amount" },
  { label: "付款方式", prop: "paymentMethod", slot: "paymentMethod" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "备注", prop: "remark", showOverflowTooltip: true },
  { label: "操作", prop: "operation", slot: "operation", width: "220", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const supplierList = [
  { id: 1, name: "北京原材料供应商" },
  { id: 2, name: "上海电子元器件公司" },
  { id: 3, name: "广州包装材料厂" },
  { id: 4, name: "深圳五金配件公司" },
];
const applyList = [
  { applyCode: "FK2024001", supplierId: 1, amount: 5000 },
  { applyCode: "FK2024002", supplierId: 2, amount: 8000 },
  { applyCode: "FK2024003", supplierId: 3, amount: 3000 },
];
const form = reactive({
  paymentCode: "",
  applyCode: "",
  supplierId: "",
  paymentDate: "",
  amount: 0,
  paymentMethod: "bank_transfer",
  bankAccount: "",
  bankName: "",
  remark: "",
});
const rules = {
  applyCode: [{ required: true, message: "请选择关联申请单", trigger: "change" }],
  supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }],
  paymentDate: [{ required: true, message: "请选择付款日期", trigger: "change" }],
  amount: [{ required: true, message: "请输入付款金额", trigger: "blur" }],
  paymentMethod: [{ required: true, message: "请选择付款方式", trigger: "change" }],
};
const mockData = [
  { id: 1, paymentCode: "FKD2024001", applyCode: "FK2024001", supplierId: 1, supplierName: "北京原材料供应商", paymentDate: "2024-01-15", amount: 5000, paymentMethod: "bank_transfer", status: "completed", bankAccount: "6222021234567890123", bankName: "工商银行", remark: "" },
  { id: 2, paymentCode: "FKD2024002", applyCode: "FK2024002", supplierId: 2, supplierName: "上海电子元器件公司", paymentDate: "2024-01-18", amount: 8000, paymentMethod: "bank_transfer", status: "pending", bankAccount: "6222029876543210987", bankName: "建设银行", remark: "" },
  { id: 3, paymentCode: "FKD2024003", applyCode: "FK2024003", supplierId: 3, supplierName: "广州包装材料厂", paymentDate: "2024-01-20", amount: 3000, paymentMethod: "cash", status: "completed", remark: "" },
];
const totalPaymentAmount = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.amount), 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 = (method) => {
  const map = {
    bank_transfer: "银行转账",
    cash: "现金",
    check: "支票",
    draft: "汇票",
  };
  return map[method] || method;
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.paymentCode) {
    result = result.filter(item => item.paymentCode.includes(filters.paymentCode));
  }
  if (filters.supplierId) {
    result = result.filter(item => item.supplierId === filters.supplierId);
  }
  if (filters.paymentMethod) {
    result = result.filter(item => item.paymentMethod === filters.paymentMethod);
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.paymentCode = "";
  filters.supplierId = "";
  filters.paymentMethod = "";
  filters.status = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增付款";
  Object.assign(form, {
    paymentCode: "FKD" + Date.now().toString().slice(-8),
    applyCode: "",
    supplierId: "",
    paymentDate: new Date().toISOString().split('T')[0],
    amount: 0,
    paymentMethod: "bank_transfer",
    bankAccount: "",
    bankName: "",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑付款";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看付款单: ${row.paymentCode}`);
};
const handleComplete = (row) => {
  ElMessageBox.confirm("确认该付款单已完成吗?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "completed";
    }
    ElMessage.success("付款完成");
    getTableData();
  });
};
const handleCancel = (row) => {
  ElMessageBox.confirm("确认取消该付款单吗?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "cancelled";
    }
    ElMessage.success("已取消");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const supplier = supplierList.find(item => item.id === form.supplierId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, supplierName: supplier?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, supplierName: supplier?.name, status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  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>
src/views/financialManagement/payable/paymentApply.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,360 @@
<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.supplierId" placeholder="请选择供应商" clearable style="width: 200px;">
          <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :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="pending" />
          <el-option label="已审批" value="approved" />
          <el-option label="已驳回" value="rejected" />
          <el-option label="已付款" value="paid" />
        </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="handleBatchApply" icon="Document" :disabled="selectedRows.length === 0">批量申请</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 #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="row.status === 'pending'">编辑</el-button>
          <el-button type="success" link @click="handleAudit(row)" v-if="row.status === 'pending'">审批</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="申请单号" prop="applyCode">
              <el-input v-model="form.applyCode" 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%;" :disabled="isEdit">
                <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :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="amount">
              <el-input-number v-model="form.amount" :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="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%;">
                <el-option label="银行转账" value="bank_transfer" />
                <el-option label="现金" value="cash" />
                <el-option label="支票" value="check" />
                <el-option label="汇票" value="draft" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <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%;" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="期望付款日期" prop="expectedDate">
              <el-date-picker v-model="form.expectedDate" type="date" placeholder="选择日期" value-format="YYYY-MM-DD" style="width: 100%;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="关联入库单" prop="relatedDocs">
          <el-select v-model="form.relatedDocs" multiple placeholder="请选择关联入库单" style="width: 100%;">
            <el-option v-for="item in inList" :key="item.inCode" :label="item.inCode" :value="item.inCode" />
          </el-select>
        </el-form-item>
        <el-form-item label="付款事由" prop="reason">
          <el-input v-model="form.reason" 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 type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "付款申请",
});
const filters = reactive({
  applyCode: "",
  supplierId: "",
  status: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "申请单号", prop: "applyCode", width: "150" },
  { label: "供应商", prop: "supplierName", width: "180" },
  { label: "付款金额", prop: "amount", slot: "amount" },
  { label: "付款方式", prop: "paymentMethod", slot: "paymentMethod" },
  { label: "申请日期", prop: "applyDate", width: "120" },
  { label: "期望付款日", prop: "expectedDate", width: "120" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "200", fixed: "right" },
];
const dataList = ref([]);
const selectedRows = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const supplierList = [
  { id: 1, name: "北京原材料供应商" },
  { id: 2, name: "上海电子元器件公司" },
  { id: 3, name: "广州包装材料厂" },
  { id: 4, name: "深圳五金配件公司" },
];
const inList = [
  { inCode: "RK2024001", supplierId: 1 },
  { inCode: "RK2024002", supplierId: 2 },
  { inCode: "RK2024003", supplierId: 3 },
];
const form = reactive({
  applyCode: "",
  supplierId: "",
  amount: 0,
  paymentMethod: "bank_transfer",
  applyDate: "",
  expectedDate: "",
  relatedDocs: [],
  reason: "",
  remark: "",
});
const rules = {
  supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }],
  amount: [{ required: true, message: "请输入付款金额", trigger: "blur" }],
  paymentMethod: [{ required: true, message: "请选择付款方式", trigger: "change" }],
  applyDate: [{ required: true, message: "请选择申请日期", trigger: "change" }],
  expectedDate: [{ required: true, message: "请选择期望付款日期", trigger: "change" }],
};
const mockData = [
  { id: 1, applyCode: "FK2024001", supplierId: 1, supplierName: "北京原材料供应商", amount: 5000, paymentMethod: "bank_transfer", applyDate: "2024-01-12", expectedDate: "2024-01-15", status: "pending", relatedDocs: ["RK2024001"], reason: "支付原材料货款", remark: "" },
  { id: 2, applyCode: "FK2024002", supplierId: 2, supplierName: "上海电子元器件公司", amount: 8000, paymentMethod: "bank_transfer", applyDate: "2024-01-14", expectedDate: "2024-01-18", status: "approved", relatedDocs: ["RK2024002"], reason: "支付电子元器件货款", remark: "" },
  { id: 3, applyCode: "FK2024003", supplierId: 3, supplierName: "广州包装材料厂", amount: 3000, paymentMethod: "cash", applyDate: "2024-01-16", expectedDate: "2024-01-20", status: "paid", relatedDocs: ["RK2024003"], reason: "支付包装材料货款", remark: "" },
];
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const getPaymentMethodLabel = (method) => {
  const map = {
    bank_transfer: "银行转账",
    cash: "现金",
    check: "支票",
    draft: "汇票",
  };
  return map[method] || method;
};
const getStatusLabel = (status) => {
  const map = { pending: "待审批", approved: "已审批", rejected: "已驳回", paid: "已付款" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { pending: "warning", approved: "success", rejected: "danger", paid: "primary" };
  return map[status] || "";
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.applyCode) {
    result = result.filter(item => item.applyCode.includes(filters.applyCode));
  }
  if (filters.supplierId) {
    result = result.filter(item => item.supplierId === filters.supplierId);
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.applyCode = "";
  filters.supplierId = "";
  filters.status = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增付款申请";
  Object.assign(form, {
    applyCode: "FK" + Date.now().toString().slice(-8),
    supplierId: "",
    amount: 0,
    paymentMethod: "bank_transfer",
    applyDate: new Date().toISOString().split('T')[0],
    expectedDate: "",
    relatedDocs: [],
    reason: "",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑付款申请";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看申请单: ${row.applyCode}`);
};
const handleAudit = (row) => {
  ElMessageBox.confirm("确认审批通过该付款申请吗?", "提示", {
    confirmButtonText: "通过",
    cancelButtonText: "驳回",
    distinguishCancelAndClose: true,
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "approved";
    }
    ElMessage.success("审批通过");
    getTableData();
  }).catch((action) => {
    if (action === "cancel") {
      const index = mockData.findIndex(item => item.id === row.id);
      if (index !== -1) {
        mockData[index].status = "rejected";
      }
      ElMessage.warning("已驳回");
      getTableData();
    }
  });
};
const handleBatchApply = () => {
  ElMessage.success(`批量申请 ${selectedRows.value.length} æ¡è®°å½•`);
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const supplier = supplierList.find(item => item.id === form.supplierId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, supplierName: supplier?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, supplierName: supplier?.name, status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
.text-danger {
  color: #f56c6c;
  font-weight: bold;
}
</style>
src/views/financialManagement/payable/purchaseIn.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="入库单号:">
        <el-input v-model="filters.inCode" placeholder="请输入入库单号" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="供应商:">
        <el-select v-model="filters.supplierId" placeholder="请选择供应商" clearable style="width: 200px;">
          <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :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="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 @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 #amount="{ row }">
          <span class="text-primary">Â¥{{ formatMoney(row.amount) }}</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="row.status === 'pending'">编辑</el-button>
          <el-button type="danger" link @click="handleDelete(row)" v-if="row.status === 'pending'">删除</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @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="inCode">
              <el-input v-model="form.inCode" placeholder="请输入入库单号" :disabled="isEdit" />
            </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%;" :disabled="isEdit">
                <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :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="inDate">
              <el-date-picker v-model="form.inDate" 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="amount">
              <el-input-number v-model="form.amount" :min="0" :precision="2" style="width: 100%;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="入库明细" prop="details">
          <el-table :data="form.details" border style="width: 100%">
            <el-table-column prop="materialName" label="物料名称" width="150">
              <template #default="{ $index }">
                <el-input v-model="form.details[$index].materialName" placeholder="物料名称" />
              </template>
            </el-table-column>
            <el-table-column prop="spec" label="规格" width="120">
              <template #default="{ $index }">
                <el-input v-model="form.details[$index].spec" placeholder="规格" />
              </template>
            </el-table-column>
            <el-table-column prop="quantity" label="数量" width="100">
              <template #default="{ $index }">
                <el-input-number v-model="form.details[$index].quantity" :min="0" style="width: 100%;" />
              </template>
            </el-table-column>
            <el-table-column prop="unitPrice" label="单价" width="120">
              <template #default="{ $index }">
                <el-input-number v-model="form.details[$index].unitPrice" :min="0" :precision="2" style="width: 100%;" />
              </template>
            </el-table-column>
            <el-table-column prop="total" label="金额" width="120">
              <template #default="{ row }">
                <span>Â¥{{ formatMoney(row.quantity * row.unitPrice) }}</span>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="80">
              <template #default="{ $index }">
                <el-button type="danger" link @click="removeDetail($index)">删除</el-button>
              </template>
            </el-table-column>
          </el-table>
          <el-button type="primary" link @click="addDetail" style="margin-top: 10px;">+ æ·»åŠ æ˜Žç»†</el-button>
        </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 } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "采购入库",
});
const filters = reactive({
  inCode: "",
  supplierId: "",
  dateRange: [],
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "入库单号", prop: "inCode", width: "150" },
  { label: "供应商", prop: "supplierName", width: "180" },
  { label: "入库日期", prop: "inDate", width: "120" },
  { label: "入库金额", prop: "amount", slot: "amount" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "备注", prop: "remark", showOverflowTooltip: true },
  { label: "操作", prop: "operation", slot: "operation", width: "200", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const supplierList = [
  { id: 1, name: "北京原材料供应商" },
  { id: 2, name: "上海电子元器件公司" },
  { id: 3, name: "广州包装材料厂" },
  { id: 4, name: "深圳五金配件公司" },
];
const form = reactive({
  inCode: "",
  supplierId: "",
  inDate: "",
  amount: 0,
  details: [],
  remark: "",
});
const rules = {
  inCode: [{ required: true, message: "请输入入库单号", trigger: "blur" }],
  supplierId: [{ required: true, message: "请选择供应商", trigger: "change" }],
  inDate: [{ required: true, message: "请选择入库日期", trigger: "change" }],
  amount: [{ required: true, message: "请输入入库金额", trigger: "blur" }],
};
const mockData = [
  { id: 1, inCode: "RK2024001", supplierId: 1, supplierName: "北京原材料供应商", inDate: "2024-01-10", amount: 8000, status: "approved", details: [{ materialName: "钢材", spec: "Q235", quantity: 10, unitPrice: 500 }], remark: "" },
  { id: 2, inCode: "RK2024002", supplierId: 2, supplierName: "上海电子元器件公司", inDate: "2024-01-12", amount: 12000, status: "pending", details: [{ materialName: "芯片", spec: "STM32", quantity: 100, unitPrice: 80 }], remark: "" },
  { id: 3, inCode: "RK2024003", supplierId: 3, supplierName: "广州包装材料厂", inDate: "2024-01-15", amount: 3500, status: "approved", details: [{ materialName: "纸箱", spec: "50*40*30", quantity: 500, unitPrice: 5 }], remark: "" },
];
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 map = { pending: "待审核", approved: "已审核", rejected: "已驳回" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { pending: "warning", approved: "success", rejected: "danger" };
  return map[status] || "";
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.inCode) {
    result = result.filter(item => item.inCode.includes(filters.inCode));
  }
  if (filters.supplierId) {
    result = result.filter(item => item.supplierId === filters.supplierId);
  }
  if (filters.dateRange && filters.dateRange.length === 2) {
    result = result.filter(item => item.inDate >= filters.dateRange[0] && item.inDate <= filters.dateRange[1]);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.inCode = "";
  filters.supplierId = "";
  filters.dateRange = [];
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const addDetail = () => {
  form.details.push({ materialName: "", spec: "", quantity: 0, unitPrice: 0 });
};
const removeDetail = (index) => {
  form.details.splice(index, 1);
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增入库";
  Object.assign(form, {
    inCode: "RK" + Date.now().toString().slice(-8),
    supplierId: "",
    inDate: new Date().toISOString().split('T')[0],
    amount: 0,
    details: [{ materialName: "", spec: "", quantity: 0, unitPrice: 0 }],
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑入库";
  Object.assign(form, row);
  if (!form.details || form.details.length === 0) {
    form.details = [{ materialName: "", spec: "", quantity: 0, unitPrice: 0 }];
  }
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看入库单: ${row.inCode}`);
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该入库单吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const supplier = supplierList.find(item => item.id === form.supplierId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, supplierName: supplier?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, supplierName: supplier?.name, status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
.text-primary {
  color: #409eff;
  font-weight: bold;
}
</style>
src/views/financialManagement/payable/reconciliation.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,469 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="供应商:">
        <el-select v-model="filters.supplierId" placeholder="请选择供应商" clearable style="width: 200px;">
          <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :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"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      >
        <template #beginBalance="{ row }">
          <span :class="row.beginBalance >= 0 ? 'text-success' : 'text-danger'">Â¥{{ formatMoney(row.beginBalance) }}</span>
        </template>
        <template #currentPayable="{ row }">
          <span class="text-danger">Â¥{{ formatMoney(row.currentPayable) }}</span>
        </template>
        <template #currentPayment="{ row }">
          <span class="text-success">Â¥{{ formatMoney(row.currentPayment) }}</span>
        </template>
        <template #endBalance="{ row }">
          <span :class="row.endBalance >= 0 ? 'text-success' : 'text-danger'">Â¥{{ formatMoney(row.endBalance) }}</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>
        </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%">
        <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>-</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%;" @change="onSupplierChange">
                <el-option v-for="item in supplierList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="对账月份" prop="period">
              <el-date-picker v-model="generateForm.period" type="month" placeholder="选择月份" value-format="YYYY-MM" style="width: 100%;" @change="onPeriodChange" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div v-if="purchaseData.length > 0" class="purchase-section">
        <div class="section-title">本月采购数据</div>
        <el-table :data="purchaseData" border 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="date" label="日期" width="120" />
          <el-table-column prop="code" label="单据编号" width="150" />
          <el-table-column prop="type" label="类型" width="100">
            <template #default="{ row }">
              <el-tag :type="row.type === '入库' ? 'success' : 'danger'">{{ row.type }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="amount" label="金额" width="120">
            <template #default="{ row }">
              <span :class="row.type === '入库' ? 'text-danger' : 'text-success'">Â¥{{ formatMoney(row.amount) }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="remark" label="备注" />
        </el-table>
        <div class="summary-row">
          <span>期初余额: <strong class="text-primary">Â¥{{ formatMoney(generateForm.beginBalance) }}</strong></span>
          <span>本期应付: <strong class="text-danger">Â¥{{ formatMoney(generateForm.currentPayable) }}</strong></span>
          <span>本期付款: <strong class="text-success">Â¥{{ formatMoney(generateForm.currentPayment) }}</strong></span>
          <span>期末余额: <strong class="text-primary">Â¥{{ formatMoney(calculateEndBalance(generateForm.beginBalance, generateForm.currentPayable, generateForm.currentPayment)) }}</strong></span>
        </div>
      </div>
      <div v-else-if="generateForm.supplierId && !purchaseLoading" class="empty-tip">
        <el-empty description="该供应商本月暂无采购数据" />
      </div>
      <template #footer>
        <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate">确认生成</el-button>
        <el-button @click="generateDialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ElMessage } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "应付对账",
});
const filters = reactive({
  supplierId: "",
  startMonth: "",
  endMonth: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "对账单号", prop: "statementCode", width: "150" },
  { label: "供应商", prop: "supplierName", width: "180" },
  { label: "对账期间", prop: "period", width: "150" },
  { label: "期初余额", prop: "beginBalance", slot: "beginBalance" },
  { label: "本期应付", prop: "currentPayable", slot: "currentPayable" },
  { label: "本期付款", prop: "currentPayment", slot: "currentPayment" },
  { label: "期末余额", prop: "endBalance", slot: "endBalance" },
  { label: "操作", prop: "operation", slot: "operation", width: "150", fixed: "right" },
];
const dataList = ref([]);
const detailDialogVisible = ref(false);
const currentSupplier = ref("");
const currentPeriod = ref("");
const detailData = ref([]);
const generateDialogVisible = ref(false);
const purchaseLoading = ref(false);
const purchaseData = ref([]);
const selectedPurchases = ref([]);
const generateForm = reactive({
  supplierId: "",
  supplierName: "",
  period: "",
  beginBalance: 0,
  currentPayable: 0,
  currentPayment: 0,
});
const canGenerate = computed(() => {
  return generateForm.supplierId && generateForm.period && selectedPurchases.value.length > 0;
});
const supplierList = [
  { id: 1, name: "北京原材料供应商" },
  { id: 2, name: "上海电子元器件公司" },
  { id: 3, name: "广州包装材料厂" },
  { id: 4, name: "深圳五金配件公司" },
];
const mockData = [
  { id: 1, statementCode: "DZ202401001", supplierId: 1, supplierName: "北京原材料供应商", period: "2024-01", beginBalance: 20000, currentPayable: 15000, currentPayment: 10000, endBalance: 25000 },
  { id: 2, statementCode: "DZ202401002", supplierId: 2, supplierName: "上海电子元器件公司", period: "2024-01", beginBalance: 10000, currentPayable: 20000, currentPayment: 15000, endBalance: 15000 },
  { id: 3, statementCode: "DZ202402001", supplierId: 1, supplierName: "北京原材料供应商", period: "2024-02", beginBalance: 25000, currentPayable: 18000, currentPayment: 20000, endBalance: 23000 },
];
const calculateEndBalance = (beginBalance, currentPayable, currentPayment) => {
  return beginBalance + currentPayable - currentPayment;
};
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.supplierId) {
    result = result.filter(item => item.supplierId === filters.supplierId);
  }
  if (filters.startMonth && filters.endMonth) {
    result = result.filter(item => item.period >= filters.startMonth && item.period <= filters.endMonth);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.supplierId = "";
  filters.startMonth = "";
  filters.endMonth = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const generateStatement = () => {
  generateForm.supplierId = "";
  generateForm.supplierName = "";
  generateForm.period = "";
  generateForm.beginBalance = 0;
  generateForm.currentPayable = 0;
  generateForm.currentPayment = 0;
  purchaseData.value = [];
  selectedPurchases.value = [];
  generateDialogVisible.value = true;
};
const onSupplierChange = (supplierId) => {
  const supplier = supplierList.find(item => item.id === supplierId);
  if (supplier) {
    generateForm.supplierName = supplier.name;
  }
  loadPurchaseData();
};
const onPeriodChange = () => {
  loadPurchaseData();
};
const loadPurchaseData = () => {
  if (!generateForm.supplierId || !generateForm.period) {
    purchaseData.value = [];
    return;
  }
  purchaseLoading.value = true;
  setTimeout(() => {
    const mockPurchaseData = [
      { id: 1, date: generateForm.period + "-05", code: "RK2024001", type: "入库", amount: 8000, remark: "原材料采购" },
      { id: 2, date: generateForm.period + "-10", code: "FK2024001", type: "付款", amount: 5000, remark: "支付货款" },
      { id: 3, date: generateForm.period + "-15", code: "RK2024002", type: "入库", amount: 12000, remark: "电子元器件" },
      { id: 4, date: generateForm.period + "-18", code: "TH2024001", type: "退货", amount: 2000, remark: "质量问题退货" },
      { id: 5, date: generateForm.period + "-22", code: "RK2024003", type: "入库", amount: 6000, remark: "包装材料" },
      { id: 6, date: generateForm.period + "-25", code: "FK2024002", type: "付款", amount: 8000, remark: "支付货款" },
    ];
    purchaseData.value = mockPurchaseData;
    const lastPeriod = getLastPeriod(generateForm.period);
    const lastStatement = mockData.find(item =>
      item.supplierId === generateForm.supplierId && item.period === lastPeriod
    );
    generateForm.beginBalance = lastStatement ? lastStatement.endBalance : 0;
    calculateSummary();
    purchaseLoading.value = false;
  }, 500);
};
const getLastPeriod = (period) => {
  const [year, month] = period.split("-").map(Number);
  if (month === 1) {
    return `${year - 1}-12`;
  }
  return `${year}-${String(month - 1).padStart(2, "0")}`;
};
const calculateSummary = () => {
  let payable = 0;
  let payment = 0;
  selectedPurchases.value.forEach(item => {
    if (item.type === "入库") {
      payable += item.amount;
    } else if (item.type === "退货") {
      payable -= item.amount;
    } else if (item.type === "付款") {
      payment += item.amount;
    }
  });
  generateForm.currentPayable = payable;
  generateForm.currentPayment = payment;
};
const handlePurchaseSelectionChange = (selection) => {
  selectedPurchases.value = selection;
  calculateSummary();
};
const confirmGenerate = () => {
  const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
  const endBalance = calculateEndBalance(generateForm.beginBalance, generateForm.currentPayable, generateForm.currentPayment);
  mockData.unshift({
    id: newId,
    statementCode: "DZ" + Date.now(),
    supplierId: generateForm.supplierId,
    supplierName: generateForm.supplierName,
    period: generateForm.period,
    beginBalance: generateForm.beginBalance,
    currentPayable: generateForm.currentPayable,
    currentPayment: generateForm.currentPayment,
    endBalance,
  });
  generateDialogVisible.value = false;
  ElMessage.success("对账单生成成功");
  getTableData();
};
const viewDetail = (row) => {
  currentSupplier.value = row.supplierName;
  currentPeriod.value = row.period;
  const purchaseInAmount = Math.floor(row.currentPayable * 0.7);
  const returnAmount = Math.floor(row.currentPayable * 0.1);
  const firstPayment = Math.floor(row.currentPayment * 0.5);
  const secondPayment = row.currentPayment - firstPayment;
  let runningBalance = row.beginBalance;
  detailData.value = [
    { date: row.period + "-01", type: "期初", code: "-", debit: 0, credit: 0, balance: runningBalance, remark: "期初余额" },
    { date: row.period + "-05", type: "入库", code: "RK2024001", debit: 0, credit: purchaseInAmount, balance: runningBalance += purchaseInAmount, remark: "采购入库" },
    { date: row.period + "-10", type: "付款", code: "FK2024001", debit: firstPayment, credit: 0, balance: runningBalance -= firstPayment, remark: "支付货款" },
    { date: row.period + "-15", type: "入库", code: "RK2024002", debit: 0, credit: row.currentPayable - purchaseInAmount - returnAmount, balance: runningBalance += (row.currentPayable - purchaseInAmount - returnAmount), remark: "采购入库" },
    { date: row.period + "-20", type: "退货", code: "TH2024001", debit: 0, credit: -returnAmount, balance: runningBalance -= returnAmount, remark: "采购退货" },
    { date: row.period + "-25", type: "付款", code: "FK2024002", debit: secondPayment, credit: 0, balance: runningBalance -= secondPayment, remark: "支付货款" },
  ];
  detailDialogVisible.value = true;
};
const printStatement = (row) => {
  ElMessage.info(`打印对账单: ${row.statementCode}`);
};
const printDetail = () => {
  ElMessage.info("打印明细");
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
.text-success {
  color: #67c23a;
}
.text-danger {
  color: #f56c6c;
}
.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;
}
.text-primary {
  color: #409eff;
}
</style>
src/views/financialManagement/receivable/invoiceApply.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,363 @@
<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.name" :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="pending" />
          <el-option label="已审核" value="approved" />
          <el-option label="已驳回" value="rejected" />
          <el-option label="已开票" value="invoiced" />
        </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="handleBatchApply" icon="Document" :disabled="selectedRows.length === 0">批量申请</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 #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)">{{ 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="row.status === 'pending'">编辑</el-button>
          <el-button type="success" link @click="handleAudit(row)" v-if="row.status === 'pending'">审核</el-button>
          <el-button type="warning" link @click="handleInvoice(row)" v-if="row.status === 'approved'">开票</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="申请单号" prop="applyCode">
              <el-input v-model="form.applyCode" placeholder="系统自动生成" disabled />
            </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="isEdit">
                <el-option v-for="item in customerList" :key="item.id" :label="item.name" :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="amount">
              <el-input-number v-model="form.amount" :min="0" :precision="2" style="width: 100%;" />
            </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%;">
                <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%;">
                <el-option label="增值税专用发票" value="special" />
                <el-option label="增值税普通发票" value="normal" />
                <el-option label="电子发票" value="electronic" />
              </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%;" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="发票内容" prop="content">
          <el-input v-model="form.content" 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 type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "开票申请",
});
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
const filters = reactive({
  applyCode: "",
  customerId: "",
  status: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "申请单号", prop: "applyCode", width: "150" },
  { label: "客户名称", prop: "customerName", width: "180" },
  { label: "开票金额", prop: "amount", slot: "amount" },
  { label: "税率", prop: "taxRate", slot: "taxRate" },
  { label: "发票类型", prop: "invoiceTypeLabel", width: "130" },
  { label: "申请日期", prop: "applyDate", width: "120" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "操作", prop: "operation", slot: "operation", width: "200", fixed: "right" },
];
const dataList = ref([]);
const selectedRows = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const customerList = [
  { id: 1, name: "北京科技有限公司" },
  { id: 2, name: "上海贸易公司" },
  { id: 3, name: "广州实业有限公司" },
  { id: 4, name: "深圳电子公司" },
];
const form = reactive({
  applyCode: "",
  customerId: "",
  amount: 0,
  taxRate: 13,
  invoiceType: "special",
  applyDate: "",
  content: "",
  remark: "",
});
const rules = {
  customerId: [{ required: true, 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" }],
};
const mockData = [
  { id: 1, applyCode: "KP2024001", customerId: 1, customerName: "北京科技有限公司", amount: 5000, taxRate: 13, invoiceType: "special", invoiceTypeLabel: "增值税专用发票", applyDate: "2024-01-15", status: "pending", content: "软件服务费", remark: "" },
  { id: 2, applyCode: "KP2024002", customerId: 2, customerName: "上海贸易公司", amount: 8000, taxRate: 13, invoiceType: "normal", invoiceTypeLabel: "增值税普通发票", applyDate: "2024-01-16", status: "approved", content: "商品销售", remark: "" },
  { id: 3, applyCode: "KP2024003", customerId: 3, customerName: "广州实业有限公司", amount: 12000, taxRate: 6, invoiceType: "electronic", invoiceTypeLabel: "电子发票", applyDate: "2024-01-18", status: "invoiced", content: "技术服务费", remark: "" },
];
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 map = { pending: "待审核", approved: "已审核", rejected: "已驳回", invoiced: "已开票" };
  return map[status] || status;
};
const getStatusType = (status) => {
  const map = { pending: "warning", approved: "success", rejected: "danger", invoiced: "primary" };
  return map[status] || "";
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.applyCode) {
    result = result.filter(item => item.applyCode.includes(filters.applyCode));
  }
  if (filters.customerId) {
    result = result.filter(item => item.customerId === filters.customerId);
  }
  if (filters.status) {
    result = result.filter(item => item.status === filters.status);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.applyCode = "";
  filters.customerId = "";
  filters.status = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const handleSelectionChange = (selection) => {
  selectedRows.value = selection;
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增开票申请";
  Object.assign(form, {
    applyCode: "KP" + Date.now().toString().slice(-8),
    customerId: "",
    amount: 0,
    taxRate: 13,
    invoiceType: "special",
    applyDate: new Date().toISOString().split('T')[0],
    content: "",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑开票申请";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看申请单: ${row.applyCode}`);
};
const handleAudit = (row) => {
  ElMessageBox.confirm("确认审核通过该开票申请吗?", "提示", {
    confirmButtonText: "通过",
    cancelButtonText: "驳回",
    distinguishCancelAndClose: true,
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "approved";
    }
    ElMessage.success("审核通过");
    getTableData();
  }).catch((action) => {
    if (action === "cancel") {
      const index = mockData.findIndex(item => item.id === row.id);
      if (index !== -1) {
        mockData[index].status = "rejected";
      }
      ElMessage.warning("已驳回");
      getTableData();
    }
  });
};
const handleInvoice = (row) => {
  ElMessageBox.confirm("确认已开具发票?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "invoiced";
    }
    ElMessage.success("开票完成");
    getTableData();
  });
};
const handleBatchApply = () => {
  ElMessage.success(`批量申请 ${selectedRows.value.length} æ¡è®°å½•`);
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const customer = customerList.find(item => item.id === form.customerId);
      const invoiceTypeMap = { special: "增值税专用发票", normal: "增值税普通发票", electronic: "电子发票" };
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, customerName: customer?.name, invoiceTypeLabel: invoiceTypeMap[form.invoiceType] };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, customerName: customer?.name, invoiceTypeLabel: invoiceTypeMap[form.invoiceType], status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  getTableData();
});
</script>
<style lang="scss" scoped>
.actions {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
}
.text-primary {
  color: #409eff;
  font-weight: bold;
}
</style>
src/views/financialManagement/receivable/outputInvoice.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,373 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="发票代码:">
        <el-input v-model="filters.invoiceCode" placeholder="请输入发票代码" clearable style="width: 200px;" />
      </el-form-item>
      <el-form-item label="发票号码:">
        <el-input v-model="filters.invoiceNo" 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.name" :value="item.id" />
        </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="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 #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 #invoiceType="{ row }">
          <el-tag :type="row.invoiceType === 'special' ? 'danger' : 'primary'">{{ row.invoiceTypeLabel }}</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" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="发票代码" prop="invoiceCode">
              <el-input v-model="form.invoiceCode" placeholder="请输入发票代码" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="发票号码" prop="invoiceNo">
              <el-input v-model="form.invoiceNo" placeholder="请输入发票号码" />
            </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%;">
                <el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </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%;" />
            </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%;" @change="handleInvoiceTypeChange">
                <el-option label="增值税专用发票" value="special" />
                <el-option label="增值税普通发票" value="normal" />
                <el-option label="电子发票" value="electronic" />
              </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%;" @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%;" @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="请输入发票内容" />
        </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 type="primary" @click="submitForm">确定</el-button>
        <el-button @click="dialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed, getCurrentInstance } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "销项发票",
});
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
const filters = reactive({
  invoiceCode: "",
  invoiceNo: "",
  customerId: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "发票代码", prop: "invoiceCode", width: "130" },
  { label: "发票号码", prop: "invoiceNo", width: "120" },
  { label: "客户名称", prop: "customerName", width: "180" },
  { label: "开票日期", prop: "invoiceDate", width: "120" },
  { label: "金额", prop: "amount", slot: "amount" },
  { label: "税额", prop: "taxAmount", slot: "taxAmount" },
  { label: "价税合计", prop: "totalAmount", slot: "totalAmount" },
  { label: "发票类型", prop: "invoiceType", slot: "invoiceType" },
  { label: "操作", prop: "operation", slot: "operation", width: "180", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const customerList = [
  { id: 1, name: "北京科技有限公司" },
  { id: 2, name: "上海贸易公司" },
  { id: 3, name: "广州实业有限公司" },
  { id: 4, name: "深圳电子公司" },
];
const form = reactive({
  invoiceCode: "",
  invoiceNo: "",
  customerId: "",
  invoiceDate: "",
  invoiceType: "special",
  taxRate: 13,
  amount: 0,
  taxAmount: 0,
  totalAmount: 0,
  content: "",
  remark: "",
});
const rules = {
  invoiceCode: [{ required: true, message: "请输入发票代码", trigger: "blur" }],
  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 mockData = [
  { id: 1, invoiceCode: "0440021001", invoiceNo: "12345678", customerId: 1, customerName: "北京科技有限公司", invoiceDate: "2024-01-15", amount: 5000, taxRate: 13, taxAmount: 650, totalAmount: 5650, invoiceType: "special", invoiceTypeLabel: "增值税专用发票", content: "软件服务费", remark: "" },
  { id: 2, invoiceCode: "0440021002", invoiceNo: "87654321", customerId: 2, customerName: "上海贸易公司", invoiceDate: "2024-01-16", amount: 8000, taxRate: 13, taxAmount: 1040, totalAmount: 9040, invoiceType: "normal", invoiceTypeLabel: "增值税普通发票", content: "商品销售", remark: "" },
  { id: 3, invoiceCode: "0440021003", invoiceNo: "11112222", customerId: 3, customerName: "广州实业有限公司", invoiceDate: "2024-01-18", amount: 12000, taxRate: 6, taxAmount: 720, totalAmount: 12720, invoiceType: "electronic", invoiceTypeLabel: "电子发票", content: "技术服务费", remark: "" },
];
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 = () => {
  if (form.invoiceType === "special") {
    form.taxRate = 13;
  } else {
    form.taxRate = 13;
  }
  calculateTax();
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.invoiceCode) {
    result = result.filter(item => item.invoiceCode.includes(filters.invoiceCode));
  }
  if (filters.invoiceNo) {
    result = result.filter(item => item.invoiceNo.includes(filters.invoiceNo));
  }
  if (filters.customerId) {
    result = result.filter(item => item.customerId === filters.customerId);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.invoiceCode = "";
  filters.invoiceNo = "";
  filters.customerId = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "录入发票";
  Object.assign(form, {
    invoiceCode: "",
    invoiceNo: "",
    customerId: "",
    invoiceDate: new Date().toISOString().split('T')[0],
    invoiceType: "special",
    taxRate: 13,
    amount: 0,
    taxAmount: 0,
    totalAmount: 0,
    content: "",
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑发票";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看发票: ${row.invoiceCode}-${row.invoiceNo}`);
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认作废该发票吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("作废成功");
    getTableData();
  });
};
const handleImport = () => {
  ElMessage.info("导入功能");
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const customer = customerList.find(item => item.id === form.customerId);
      const invoiceTypeMap = { special: "增值税专用发票", normal: "增值税普通发票", electronic: "电子发票" };
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, customerName: customer?.name, invoiceTypeLabel: invoiceTypeMap[form.invoiceType] };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, customerName: customer?.name, invoiceTypeLabel: invoiceTypeMap[form.invoiceType] });
        ElMessage.success("录入成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  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>
src/views/financialManagement/receivable/receipt.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,356 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="收款单号:">
        <el-input v-model="filters.receiptCode" 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.name" :value="item.id" />
        </el-select>
      </el-form-item>
      <el-form-item label="收款方式:">
        <el-select v-model="filters.receiptMethod" placeholder="请选择收款方式" clearable style="width: 150px;">
          <el-option label="银行转账" value="bank_transfer" />
          <el-option label="现金" value="cash" />
          <el-option label="支票" value="check" />
          <el-option label="汇票" value="draft" />
          <el-option label="支付宝" value="alipay" />
          <el-option label="微信" value="wechat" />
        </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="totalReceiptAmount" precision="2" prefix="Â¥" />
        </div>
        <div>
          <el-button type="primary" @click="add" icon="Plus">新增收款</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 #amount="{ row }">
          <span class="text-success">Â¥{{ formatMoney(row.amount) }}</span>
        </template>
        <template #receiptMethod="{ row }">
          <el-tag>{{ getReceiptMethodLabel(row.receiptMethod) }}</el-tag>
        </template>
        <template #status="{ row }">
          <el-tag :type="row.status === 'confirmed' ? 'success' : 'warning'">{{ row.status === 'confirmed' ? '已确认' : '待确认' }}</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="row.status === 'pending'">编辑</el-button>
          <el-button type="success" link @click="handleConfirm(row)" v-if="row.status === 'pending'">确认</el-button>
          <el-button type="danger" link @click="handleDelete(row)" v-if="row.status === 'pending'">删除</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDialog :title="dialogTitle" v-model="dialogVisible" width="800px" @confirm="submitForm" @cancel="dialogVisible = false">
      <el-form :model="form" :rules="rules" ref="formRef" label-width="120px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="收款单号" prop="receiptCode">
              <el-input v-model="form.receiptCode" placeholder="系统自动生成" disabled />
            </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="isEdit">
                <el-option v-for="item in customerList" :key="item.id" :label="item.name" :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="receiptDate">
              <el-date-picker v-model="form.receiptDate" 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="amount">
              <el-input-number v-model="form.amount" :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="receiptMethod">
              <el-select v-model="form.receiptMethod" placeholder="请选择收款方式" style="width: 100%;">
                <el-option label="银行转账" value="bank_transfer" />
                <el-option label="现金" value="cash" />
                <el-option label="支票" value="check" />
                <el-option label="汇票" value="draft" />
                <el-option label="支付宝" value="alipay" />
                <el-option label="微信" value="wechat" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="银行账号" prop="bankAccount" v-if="form.receiptMethod === 'bank_transfer'">
              <el-input v-model="form.bankAccount" placeholder="请输入银行账号" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="关联单据" prop="relatedDocs">
          <el-select v-model="form.relatedDocs" multiple placeholder="请选择关联单据" style="width: 100%;">
            <el-option v-for="item in outList" :key="item.outCode" :label="item.outCode" :value="item.outCode" />
          </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>
      <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, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "收款单",
});
const filters = reactive({
  receiptCode: "",
  customerId: "",
  receiptMethod: "",
});
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", slot: "amount" },
  { label: "收款方式", prop: "receiptMethod", slot: "receiptMethod" },
  { label: "状态", prop: "status", slot: "status" },
  { label: "备注", prop: "remark", showOverflowTooltip: true },
  { label: "操作", prop: "operation", slot: "operation", width: "220", fixed: "right" },
];
const dataList = ref([]);
const dialogVisible = ref(false);
const dialogTitle = ref("");
const formRef = ref(null);
const isEdit = ref(false);
const currentId = ref(null);
const customerList = [
  { id: 1, name: "北京科技有限公司" },
  { id: 2, name: "上海贸易公司" },
  { id: 3, name: "广州实业有限公司" },
  { id: 4, name: "深圳电子公司" },
];
const outList = [
  { outCode: "CK2024001", customerId: 1 },
  { outCode: "CK2024002", customerId: 2 },
  { outCode: "CK2024003", customerId: 3 },
];
const form = reactive({
  receiptCode: "",
  customerId: "",
  receiptDate: "",
  amount: 0,
  receiptMethod: "bank_transfer",
  bankAccount: "",
  relatedDocs: [],
  remark: "",
});
const rules = {
  customerId: [{ required: true, message: "请选择客户", trigger: "change" }],
  receiptDate: [{ required: true, message: "请选择收款日期", trigger: "change" }],
  amount: [{ required: true, message: "请输入收款金额", trigger: "blur" }],
  receiptMethod: [{ required: true, message: "请选择收款方式", trigger: "change" }],
};
const mockData = [
  { id: 1, receiptCode: "SK2024001", customerId: 1, customerName: "北京科技有限公司", receiptDate: "2024-01-16", amount: 3000, receiptMethod: "bank_transfer", status: "confirmed", relatedDocs: ["CK2024001"], remark: "" },
  { id: 2, receiptCode: "SK2024002", customerId: 2, customerName: "上海贸易公司", receiptDate: "2024-01-18", amount: 5000, receiptMethod: "cash", status: "pending", relatedDocs: ["CK2024002"], remark: "" },
  { id: 3, receiptCode: "SK2024003", customerId: 3, customerName: "广州实业有限公司", receiptDate: "2024-01-20", amount: 8000, receiptMethod: "alipay", status: "confirmed", relatedDocs: ["CK2024003"], remark: "" },
];
const totalReceiptAmount = computed(() => {
  return dataList.value.reduce((sum, item) => sum + Number(item.amount), 0);
});
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const getReceiptMethodLabel = (method) => {
  const map = {
    bank_transfer: "银行转账",
    cash: "现金",
    check: "支票",
    draft: "汇票",
    alipay: "支付宝",
    wechat: "微信",
  };
  return map[method] || method;
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.receiptCode) {
    result = result.filter(item => item.receiptCode.includes(filters.receiptCode));
  }
  if (filters.customerId) {
    result = result.filter(item => item.customerId === filters.customerId);
  }
  if (filters.receiptMethod) {
    result = result.filter(item => item.receiptMethod === filters.receiptMethod);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
const resetFilters = () => {
  filters.receiptCode = "";
  filters.customerId = "";
  filters.receiptMethod = "";
  pagination.currentPage = 1;
  getTableData();
};
const changePage = ({ current, size }) => {
  pagination.currentPage = current;
  pagination.pageSize = size;
  getTableData();
};
const add = () => {
  isEdit.value = false;
  dialogTitle.value = "新增收款";
  Object.assign(form, {
    receiptCode: "SK" + Date.now().toString().slice(-8),
    customerId: "",
    receiptDate: new Date().toISOString().split('T')[0],
    amount: 0,
    receiptMethod: "bank_transfer",
    bankAccount: "",
    relatedDocs: [],
    remark: "",
  });
  dialogVisible.value = true;
};
const edit = (row) => {
  isEdit.value = true;
  currentId.value = row.id;
  dialogTitle.value = "编辑收款";
  Object.assign(form, row);
  dialogVisible.value = true;
};
const view = (row) => {
  ElMessage.info(`查看收款单: ${row.receiptCode}`);
};
const handleConfirm = (row) => {
  ElMessageBox.confirm("确认该收款单吗?", "提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "info",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData[index].status = "confirmed";
    }
    ElMessage.success("确认成功");
    getTableData();
  });
};
const handleDelete = (row) => {
  ElMessageBox.confirm("确认删除该收款单吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  }).then(() => {
    const index = mockData.findIndex(item => item.id === row.id);
    if (index !== -1) {
      mockData.splice(index, 1);
    }
    ElMessage.success("删除成功");
    getTableData();
  });
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
const submitForm = () => {
  formRef.value.validate((valid) => {
    if (valid) {
      const customer = customerList.find(item => item.id === form.customerId);
      if (isEdit.value) {
        const index = mockData.findIndex(item => item.id === currentId.value);
        if (index !== -1) {
          mockData[index] = { ...mockData[index], ...form, customerName: customer?.name };
        }
        ElMessage.success("编辑成功");
      } else {
        const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
        mockData.push({ id: newId, ...form, customerName: customer?.name, status: "pending" });
        ElMessage.success("新增成功");
      }
      dialogVisible.value = false;
      getTableData();
    }
  });
};
onMounted(() => {
  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;
}
</style>
src/views/financialManagement/receivable/reconciliation.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,469 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <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.name" :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"
        :page="{
          current: pagination.currentPage,
          size: pagination.pageSize,
          total: pagination.total,
        }"
        @pagination="changePage"
      >
        <template #beginBalance="{ row }">
          <span :class="row.beginBalance >= 0 ? 'text-success' : 'text-danger'">Â¥{{ formatMoney(row.beginBalance) }}</span>
        </template>
        <template #currentReceivable="{ row }">
          <span class="text-primary">Â¥{{ formatMoney(row.currentReceivable) }}</span>
        </template>
        <template #currentReceipt="{ row }">
          <span class="text-success">Â¥{{ formatMoney(row.currentReceipt) }}</span>
        </template>
        <template #endBalance="{ row }">
          <span :class="row.endBalance >= 0 ? 'text-success' : 'text-danger'">Â¥{{ formatMoney(row.endBalance) }}</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>
        </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%">
        <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%;" @change="onCustomerChange">
                <el-option v-for="item in customerList" :key="item.id" :label="item.name" :value="item.id" />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="对账月份" prop="period">
              <el-date-picker v-model="generateForm.period" type="month" placeholder="选择月份" value-format="YYYY-MM" style="width: 100%;" @change="onPeriodChange" />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <div v-if="salesData.length > 0" class="sales-section">
        <div class="section-title">本月销售数据</div>
        <el-table :data="salesData" border 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="date" label="日期" width="120" />
          <el-table-column prop="code" label="单据编号" width="150" />
          <el-table-column prop="type" label="类型" width="100">
            <template #default="{ row }">
              <el-tag :type="row.type === '出库' ? 'success' : row.type === '收款' ? 'primary' : 'danger'">{{ row.type }}</el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="amount" label="金额" width="120">
            <template #default="{ row }">
              <span :class="row.type === '出库' ? 'text-primary' : row.type === '收款' ? 'text-success' : 'text-danger'">Â¥{{ formatMoney(row.amount) }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="remark" label="备注" />
        </el-table>
        <div class="summary-row">
          <span>期初余额: <strong class="text-primary">Â¥{{ formatMoney(generateForm.beginBalance) }}</strong></span>
          <span>本期应收: <strong class="text-primary">Â¥{{ formatMoney(generateForm.currentReceivable) }}</strong></span>
          <span>本期收款: <strong class="text-success">Â¥{{ formatMoney(generateForm.currentReceipt) }}</strong></span>
          <span>期末余额: <strong :class="calculateEndBalance(generateForm.beginBalance, generateForm.currentReceivable, generateForm.currentReceipt) >= 0 ? 'text-success' : 'text-danger'">Â¥{{ formatMoney(calculateEndBalance(generateForm.beginBalance, generateForm.currentReceivable, generateForm.currentReceipt)) }}</strong></span>
        </div>
      </div>
      <div v-else-if="generateForm.customerId && !salesLoading" class="empty-tip">
        <el-empty description="该客户本月暂无销售数据" />
      </div>
      <template #footer>
        <el-button type="primary" @click="confirmGenerate" :disabled="!canGenerate">确认生成</el-button>
        <el-button @click="generateDialogVisible = false">取消</el-button>
      </template>
    </FormDialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from "vue";
import { ElMessage } from "element-plus";
import FormDialog from "@/components/Dialog/FormDialog.vue";
defineOptions({
  name: "应收对账",
});
const filters = reactive({
  customerId: "",
  startMonth: "",
  endMonth: "",
});
const pagination = reactive({
  currentPage: 1,
  pageSize: 10,
  total: 0,
});
const columns = [
  { label: "对账单号", prop: "statementCode", width: "150" },
  { label: "客户名称", prop: "customerName", width: "180" },
  { label: "对账期间", prop: "period", width: "150" },
  { label: "期初余额", prop: "beginBalance", slot: "beginBalance" },
  { label: "本期应收", prop: "currentReceivable", slot: "currentReceivable" },
  { label: "本期收款", prop: "currentReceipt", slot: "currentReceipt" },
  { label: "期末余额", prop: "endBalance", slot: "endBalance" },
  { label: "操作", prop: "operation", slot: "operation", width: "150", fixed: "right" },
];
const dataList = ref([]);
const detailDialogVisible = ref(false);
const currentCustomer = ref("");
const currentPeriod = ref("");
const detailData = ref([]);
const generateDialogVisible = ref(false);
const salesLoading = ref(false);
const salesData = ref([]);
const selectedSales = ref([]);
const generateForm = reactive({
  customerId: "",
  customerName: "",
  period: "",
  beginBalance: 0,
  currentReceivable: 0,
  currentReceipt: 0,
});
const canGenerate = computed(() => {
  return generateForm.customerId && generateForm.period && selectedSales.value.length > 0;
});
const customerList = [
  { id: 1, name: "北京科技有限公司" },
  { id: 2, name: "上海贸易公司" },
  { id: 3, name: "广州实业有限公司" },
  { id: 4, name: "深圳电子公司" },
];
const mockData = [
  { id: 1, statementCode: "DZ202401001", customerId: 1, customerName: "北京科技有限公司", period: "2024-01", beginBalance: 10000, currentReceivable: 15000, currentReceipt: 8000, endBalance: 17000 },
  { id: 2, statementCode: "DZ202401002", customerId: 2, customerName: "上海贸易公司", period: "2024-01", beginBalance: 5000, currentReceivable: 12000, currentReceipt: 10000, endBalance: 7000 },
  { id: 3, statementCode: "DZ202402001", customerId: 1, customerName: "北京科技有限公司", period: "2024-02", beginBalance: 17000, currentReceivable: 20000, currentReceipt: 15000, endBalance: 22000 },
];
const calculateEndBalance = (beginBalance, currentReceivable, currentReceipt) => {
  return beginBalance + currentReceivable - currentReceipt;
};
const formatMoney = (value) => {
  if (value === undefined || value === null) return "0.00";
  return Number(value).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
const getTableData = () => {
  let result = [...mockData];
  if (filters.customerId) {
    result = result.filter(item => item.customerId === filters.customerId);
  }
  if (filters.startMonth && filters.endMonth) {
    result = result.filter(item => item.period >= filters.startMonth && item.period <= filters.endMonth);
  }
  pagination.total = result.length;
  dataList.value = result.slice((pagination.currentPage - 1) * pagination.pageSize, pagination.currentPage * pagination.pageSize);
};
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.period = "";
  generateForm.beginBalance = 0;
  generateForm.currentReceivable = 0;
  generateForm.currentReceipt = 0;
  salesData.value = [];
  selectedSales.value = [];
  generateDialogVisible.value = true;
};
const onCustomerChange = (customerId) => {
  const customer = customerList.find(item => item.id === customerId);
  if (customer) {
    generateForm.customerName = customer.name;
  }
  loadSalesData();
};
const onPeriodChange = () => {
  loadSalesData();
};
const loadSalesData = () => {
  if (!generateForm.customerId || !generateForm.period) {
    salesData.value = [];
    return;
  }
  salesLoading.value = true;
  setTimeout(() => {
    const mockSalesData = [
      { id: 1, date: generateForm.period + "-03", code: "CK2024001", type: "出库", amount: 8000, remark: "产品A销售" },
      { id: 2, date: generateForm.period + "-08", code: "SK2024001", type: "收款", amount: 5000, remark: "客户回款" },
      { id: 3, date: generateForm.period + "-12", code: "CK2024002", type: "出库", amount: 12000, remark: "产品B销售" },
      { id: 4, date: generateForm.period + "-15", code: "TH2024001", type: "退货", amount: 2000, remark: "质量问题退货" },
      { id: 5, date: generateForm.period + "-20", code: "CK2024003", type: "出库", amount: 5000, remark: "产品C销售" },
      { id: 6, date: generateForm.period + "-25", code: "SK2024002", type: "收款", amount: 8000, remark: "客户回款" },
    ];
    salesData.value = mockSalesData;
    const lastPeriod = getLastPeriod(generateForm.period);
    const lastStatement = mockData.find(item =>
      item.customerId === generateForm.customerId && item.period === lastPeriod
    );
    generateForm.beginBalance = lastStatement ? lastStatement.endBalance : 0;
    calculateSummary();
    salesLoading.value = false;
  }, 500);
};
const getLastPeriod = (period) => {
  const [year, month] = period.split("-").map(Number);
  if (month === 1) {
    return `${year - 1}-12`;
  }
  return `${year}-${String(month - 1).padStart(2, "0")}`;
};
const calculateSummary = () => {
  let receivable = 0;
  let receipt = 0;
  selectedSales.value.forEach(item => {
    if (item.type === "出库") {
      receivable += item.amount;
    } else if (item.type === "退货") {
      receivable -= item.amount;
    } else if (item.type === "收款") {
      receipt += item.amount;
    }
  });
  generateForm.currentReceivable = receivable;
  generateForm.currentReceipt = receipt;
};
const handleSalesSelectionChange = (selection) => {
  selectedSales.value = selection;
  calculateSummary();
};
const confirmGenerate = () => {
  const newId = mockData.length > 0 ? Math.max(...mockData.map(item => item.id)) + 1 : 1;
  const endBalance = calculateEndBalance(generateForm.beginBalance, generateForm.currentReceivable, generateForm.currentReceipt);
  mockData.unshift({
    id: newId,
    statementCode: "DZ" + Date.now(),
    customerId: generateForm.customerId,
    customerName: generateForm.customerName,
    period: generateForm.period,
    beginBalance: generateForm.beginBalance,
    currentReceivable: generateForm.currentReceivable,
    currentReceipt: generateForm.currentReceipt,
    endBalance,
  });
  generateDialogVisible.value = false;
  ElMessage.success("对账单生成成功");
  getTableData();
};
const viewDetail = (row) => {
  currentCustomer.value = row.customerName;
  currentPeriod.value = row.period;
  const saleOutAmount = Math.floor(row.currentReceivable * 0.6);
  const returnAmount = Math.floor(row.currentReceivable * 0.1);
  const firstReceipt = Math.floor(row.currentReceipt * 0.4);
  const secondReceipt = row.currentReceipt - firstReceipt;
  let runningBalance = row.beginBalance;
  detailData.value = [
    { date: row.period + "-01", type: "期初", code: "-", debit: 0, credit: 0, balance: runningBalance, remark: "期初余额" },
    { date: row.period + "-05", type: "出库", code: "CK2024001", debit: saleOutAmount, credit: 0, balance: runningBalance += saleOutAmount, remark: "销售出库" },
    { date: row.period + "-10", type: "收款", code: "SK2024001", debit: 0, credit: firstReceipt, balance: runningBalance -= firstReceipt, remark: "客户回款" },
    { date: row.period + "-15", type: "出库", code: "CK2024002", debit: row.currentReceivable - saleOutAmount - returnAmount, credit: 0, balance: runningBalance += (row.currentReceivable - saleOutAmount - returnAmount), remark: "销售出库" },
    { date: row.period + "-20", type: "退货", code: "TH2024001", debit: 0, credit: returnAmount, balance: runningBalance -= returnAmount, remark: "销售退货" },
    { date: row.period + "-25", type: "收款", code: "SK2024002", debit: 0, credit: secondReceipt, balance: runningBalance -= secondReceipt, remark: "客户回款" },
  ];
  detailDialogVisible.value = true;
};
const printStatement = (row) => {
  ElMessage.info(`打印对账单: ${row.statementCode}`);
};
const printDetail = () => {
  ElMessage.info("打印明细");
};
const handleOut = () => {
  ElMessage.success("导出成功");
};
onMounted(() => {
  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>
在上述文件截断后对比
src/views/financialManagement/receivable/salesOut.vue src/views/financialManagement/receivable/salesReturn.vue src/views/financialManagement/revenueManagement/index.vue src/views/financialManagement/salesRefund/components/ReceiptandRefundPopupWindow.vue src/views/financialManagement/salesRefund/index.vue src/views/financialManagement/voucher/detailLedger.vue src/views/financialManagement/voucher/generalLedger.vue src/views/financialManagement/voucher/index.vue src/views/index.vue src/views/inventoryManagement/dispatchLog/Record.vue src/views/inventoryManagement/dispatchLog/index.vue src/views/inventoryManagement/issueManagement/index.vue src/views/inventoryManagement/receiptManagement/Record.vue src/views/inventoryManagement/receiptManagement/index.vue src/views/inventoryManagement/stockManagement/FrozenAndThaw.vue src/views/inventoryManagement/stockManagement/New.vue src/views/inventoryManagement/stockManagement/Qualified.vue src/views/inventoryManagement/stockManagement/Record.vue src/views/inventoryManagement/stockManagement/Subtract.vue src/views/inventoryManagement/stockManagement/Unqualified.vue src/views/inventoryManagement/stockManagement/index.vue src/views/inventoryManagement/stockWarning/index.vue src/views/inventoryManagement/transportTaskManagement/index.vue src/views/inventoryManagement/vehicleFuelManagement/index.vue src/views/lavorissue/statistics/index.vue src/views/login.vue src/views/oaSystem/projectManagement/components/milestoneList.vue src/views/oaSystem/projectManagement/components/taskTree.vue src/views/oaSystem/projectManagement/index.vue src/views/oaSystem/projectManagement/projectDetail.vue src/views/personnelManagement/analytics/index.vue src/views/personnelManagement/attendanceCheckin/checkinRules/components/form.vue src/views/personnelManagement/attendanceCheckin/checkinRules/index.vue src/views/personnelManagement/attendanceCheckin/index.vue src/views/personnelManagement/classsSheduling/index.vue src/views/personnelManagement/contractManagement/index.vue src/views/personnelManagement/dimission/components/formDia.vue src/views/personnelManagement/dimission/index.vue src/views/personnelManagement/employeeRecord/components/JobInfoSection.vue src/views/personnelManagement/employeeRecord/index.vue src/views/personnelManagement/monthlyStatistics/index.vue src/views/personnelManagement/socialSecuritySet/index.vue src/views/procurementManagement/paymentEntry/index.vue src/views/procurementManagement/paymentHistory/index.vue src/views/procurementManagement/paymentLedger/index.vue src/views/procurementManagement/procurementInvoiceLedger/index.vue src/views/procurementManagement/procurementLedger/index.vue src/views/procurementManagement/procurementReport/index.vue src/views/procurementManagement/purchaseReturnOrder/New.vue src/views/procurementManagement/purchaseReturnOrder/index.vue src/views/productManagement/productIdentifier/index.vue src/views/productionManagement/processRoute/New.vue src/views/productionManagement/processRoute/index.vue src/views/productionManagement/processRoute/processRouteItem/index.vue src/views/productionManagement/processStatistics/index.vue src/views/productionManagement/productStructure/Detail/index.vue src/views/productionManagement/productStructure/index.vue src/views/productionManagement/productionCosting/index.vue src/views/productionManagement/productionOrder/New.vue src/views/productionManagement/productionOrder/components/MaterialDetailDialog.vue src/views/productionManagement/productionOrder/components/MaterialLedgerDialog.vue src/views/productionManagement/productionOrder/components/MaterialSupplementDialog.vue src/views/productionManagement/productionOrder/components/PrintMaterialRequisition.vue src/views/productionManagement/productionOrder/index.vue src/views/productionManagement/productionProcess/Edit.vue src/views/productionManagement/productionProcess/New.vue src/views/productionManagement/productionProcess/index.vue src/views/productionManagement/productionReporting/index.vue src/views/productionManagement/productionTraceability/index.vue src/views/productionManagement/workOrder/index.vue src/views/productionManagement/workOrderEdit/index.vue src/views/productionManagement/workOrderManagement/components/MaterialDialog.vue src/views/productionManagement/workOrderManagement/index.vue src/views/productionPlan/productionPlan/components/PIMTable.vue src/views/productionPlan/productionPlan/index.vue src/views/projectManagement/Management/components/formDia.vue src/views/projectManagement/Management/index.vue src/views/projectManagement/Management/projectDetail.vue src/views/projectManagement/projectType/components/ProjectTypeDialog.vue src/views/projectManagement/projectType/index.vue src/views/projectManagement/roles/index.vue src/views/qualityManagement/finalInspection/index.vue src/views/qualityManagement/metricBinding/index.vue src/views/qualityManagement/metricMaintenance/index.vue src/views/qualityManagement/nonconformingManagement/index.vue src/views/qualityManagement/processInspection/index.vue src/views/qualityManagement/rawMaterialInspection/index.vue src/views/reportAnalysis/dataDashboard/components/basic/left-bottom.vue src/views/reportAnalysis/dataDashboard/index0.vue src/views/reportAnalysis/productionAnalysis/components/left-bottom.vue src/views/reportAnalysis/productionAnalysis/components/left-top.vue src/views/reportAnalysis/reportManagement/index.vue src/views/safeProduction/accidentReportingRecord/index.vue src/views/safeProduction/dangerInvestigation/index.vue src/views/safeProduction/emergencyPlanReview/index.vue src/views/safeProduction/hazardSourceLedger/index.vue src/views/safeProduction/hazardousMaterialsControl/index.vue src/views/safeProduction/safeQualifications/index.vue src/views/safeProduction/safeWorkApproval/components/infoFormDia.vue src/views/safeProduction/safeWorkApproval/index.vue src/views/safeProduction/safetyTrainingAssessment/index.vue src/views/salesManagement/deliveryLedger/index.vue src/views/salesManagement/indicatorStats/index.vue src/views/salesManagement/invoiceLedger/index.vue src/views/salesManagement/invoiceRegistration/index.vue src/views/salesManagement/orderManagement/index.vue src/views/salesManagement/receiptPayment/index.vue src/views/salesManagement/receiptPaymentHistory/index.vue src/views/salesManagement/receiptPaymentLedger/index.vue src/views/salesManagement/returnOrder/components/formDia.vue src/views/salesManagement/returnOrder/index.vue src/views/salesManagement/salesLedger/fileList.vue src/views/salesManagement/salesLedger/index.vue src/views/salesManagement/salesQuotation/index.vue src/views/salesManagement/strategyControl/index.vue src/views/system/appVersion/index.vue src/views/system/role/index.vue src/views/systemArchitecture/index.vue src/views/tool/build/CodeTypeDialog.vue vite.config.js